feat(forge): sign every forge body in Rust, and redirect the gh verbs it covers - #6545
Conversation
…covers Eleven forge verbs land as three tools: `pull_request` (create, update, close, merge, comment, edit_comment), `issue` (create, update, close, comment, edit_comment) and `watch_ci` (one branch, its checks, a verdict). Grouped by the thing they act on rather than split one per verb, because a tool schema rides the cached prompt prefix on every call in the session whether or not that session ever opens a pull request. They are tools rather than prompt instructions so the attribution footer is appended in Rust, after the model has stopped having a say. A prompt can ask for a footer and a model can forget it or reword it; a hook cannot repair that, because `HookDecision` is Allow or Deny with no arm that rewrites. `bash` now refuses the eleven `gh` subcommands these tools cover, and names the tool and action to use instead. The rest of `gh` stays open, `git` is untouched, and the refusal is gated on the slot: a workspace with no forge attached has no tool to point at, so nothing is refused there. The adapters live in `stella-cli`, one crate up, so the tools hold `ForgeSlots` the host fills after assembly -- the shape `DispatcherSlot` and the `question` registration already use. A tool whose slot is empty is still listed and says what would set one up, so the schema list never depends on settings the prompt cache cannot see change. `merge` refuses without `confirm: true`. It is the one verb nothing walks back, and the guard is about the verb rather than the deployment, so a workspace approval gate stacks with it rather than replacing it.
…iene Attaches the forge. `forge_install::attach` reads `[self_driving.attribution]` and probes for `gh`, then fills the registry's slots: the tracker and pull request adapters when `gh` is present, the attribution always. One call site, in `write_dirs::registry_rooted_at`, so every door that builds a registry gets the same wiring. `[self_driving.attribution]` grows from one key to five -- `commit`, `pull_request`, `issue`, `issue_comment` and `pull_request_comment` -- each defaulting to the shipped signature. The section is source-tracked so it travels with the repository, and an installed plugin may rewrite it, so a downstream distribution signs in its own name without forking. Setting a key to "" signs that one surface with nothing. The system prompt gains a git hygiene paragraph, in both the full and pipeline variants. Fetch first, branch from the fetched tip rather than from whatever the local checkout points at, never commit to main or master, never force-push, never push a branch you did not create. A dirty tree stops the session and asks the driver: those edits are someone's unfinished work, so no stashing, no discarding, and no committing them into a branch of your own to get them out of the way. With nobody to ask, work only in ways that leave those files alone and say the tree was dirty in the answer. A short forge paragraph rides the same prefix, pointing at `pull_request` and `issue` over `gh`, saying the tools sign what they write so the model must not add a footer of its own, and saying that pushing, branching and committing are still git through bash. Whether a task gets its own worktree is not settled here. That is #1377's subject, and the handoff for it is on that issue. Refs #1377
`issue.toml`, `pull_request.toml` and `watch_ci.toml` join the generated set so `make tool-docs` matches the declarations again. Unrelated fix in the same change, because the regeneration touches every page anyway: the template itself carried prose the repository bans. A filler adverb and an honesty declaration sat in the header banner and in the unmeasured-usage and no-example texts, so all twenty existing pages carried them too. Rewritten at the source, which is the only copy that can stay correct. The risk note now states the risk rather than announcing that it is about to.
Adds the `[self_driving.attribution]` cards for the four new surfaces, and says what the section governs: every session, not only the self-driving loop, because the `pull_request` and `issue` tools sign what they write whoever is driving. Two unrelated fixes ride along, both in the section this change was editing. The wordmark was capitalised in seven lines, which `make brand-case` fails on. The five default strings were spelled out five times in `defaultValue` attributes, one 95-character sentence repeated verbatim; they move into a fenced block, which reads once and which the guard skips by design, since a config excerpt is a quotation rather than brand copy. The guard cannot see a JSX attribute value even when the component renders it inside a code span. Widening it to look there is a real gap, but it has no self-test today, so a change to it would ship untested. Left alone.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Sorry @macanderson, your pull request is larger than the review limit of 150,000 diff characters
|
You have reached your Codex usage limits for security reviews. Please try again later. |
Sourcery declined this PR for sizeSourcery posted one line and no assessment table:
So there are no The measurement
Why splitting does not reach the limitLifting out the template repair and its 19 regenerated pages — the one part of The seam that would save enough does not exist. The three new tool pages are What was done insteadA full review pass was run against the diff rather than leaving the change The checksAll 30 settled before this PR left draft: 27 pass, 3 skipping (the CodeQL |
`pull_request` and `issue` each carry several verbs under one `action`. The session's gate above the tool stack answers for a whole tool, so `"pull_request": "off"` took `merge` and `comment` together. An operator who wanted the agent to comment but never merge had no way to say it. That is `AGENTS.md invariant 9`'s second reason, and it was broken. `ToolPolicy` now takes a dotted key, `"<tool>.<action>"`, answered by `allows_action`. Both forge tools ask it per action before they do anything else. It can only narrow: `allows_action` asks `allows` first, so a tool switched off by name, by group or by the wildcard stays off whatever an action key says. The refusal is `ErrorClass::RefusedByPolicy` and names the key, so a model reads why it stopped instead of retrying. The tools read the session's own resolved `ToolPolicy`, handed down the one assembly point, rather than loading settings a second time. `stella tools` names the withheld actions beside a tool that is on. Without it the switch is invisible until a session hits the refusal. ADR 0044 records the four conditions that let a tool group verbs, since `invariant 9` read literally forbids what shipped. Its text is amended in place to name the mechanism; the numbering is an address, so nothing is renumbered. Two unrelated fixes ride along: the settings page's group list was missing `forge`, and it documented no per-action key. Witnesses, one per tool, because one gate serves both and each can lose its call site on its own: `a_switched_off_merge_refuses_while_comment_still_runs` and `a_switched_off_close_refuses_while_comment_still_runs`. Both were proven by control. The class assertion is what makes the first bite: without the gate, `merge` falls through to the `confirm: true` check and is refused as `InvalidInput` without reaching the forge, so the "nothing reached the provider" assertion passes either way. Refs #6545
The redirect matched the literal token `gh`, so six spellings walked past it: `command gh`, `env gh`, `sudo gh`, `GH_TOKEN=x gh`, `/usr/bin/gh`, and a `gh` opened inside `$(...)` or backticks. Each one reached the forge with whatever footer the model typed, which is the choice this plane exists to take away. `mentions_in_line` now asks three questions instead of one. `names_gh` reads the last path segment, so `/usr/bin/gh` and `./gh` are `gh`. `space_substitutions` spaces out `(`, `)` and the backtick, so a substitution opener becomes its own token rather than being glued to the word after it. `is_command_position` walks back over wrappers and leading assignments and stops at a separator, so `env -i gh` is a command and `echo env gh` is not. Proven by control: with `WRAPPERS` emptied and `names_gh` reduced to `word == "gh"`, the suite fails on `command gh pr create --fill`. Five more findings from the same review, each one its own fix: - The redirect read the forge slots with `.read().ok()?`, which turns a poisoned lock into "nothing is attached" and lets every covered `gh` command through. `ForgeSlots::has_forge` and `has_tracker` state the poison policy once, next to the lock. - An unknown action was answered by whatever failed next. On a workspace with no `gh`, `action: "frobnicate"` read as "no forge is configured". `known_action` runs before the slot lookup. The match arm at the end stays, for the opposite drift: an `ACTIONS` entry with no arm. - `gh pr ready` ran before `gh pr edit`, so a failed text write left the old description marked ready to read. The text is written first, and a draft flip that fails after it says so. - The five `"created by stella*"` fixtures named a footer this crate does not ship. They take `stella_autonomy::SIGNATURE`, so they cannot drift from it again. - `sign()`'s idempotence guard is tail-anchored, and the comment now says that is the whole promise: a footer quoted mid-body gets a second one at the end, because moving it would mean editing what the model wrote. Refs #6546
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 68e5de9. Configure here.
| verb: ("pr", "checks"), | ||
| tool: "watch_ci", | ||
| action: "the pull request's branch", | ||
| }, |
There was a problem hiding this comment.
Redirect names invalid tool actions
Medium Severity
The redirect exists so a refused gh call retries successfully, but three action strings are not valid tool inputs. gh pr ready is sent to update with draft, which is not an action enum value and reads as draft: true — the opposite of marking ready. gh pr merge is sent to merge with confirm: true, and gh pr checks is sent to watch_ci with an action that tool does not take.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 68e5de9. Configure here.
| // what the record needs; a line that reads like a footer inside the | ||
| // text is the model's prose. | ||
| return trimmed.to_owned(); | ||
| } |
There was a problem hiding this comment.
Empty signed bodies get a second footer
Low Severity
The new idempotence check only recognizes a footer that starts with the two-newline separator. An empty body is signed as ---\n{signature} without that leading blank line, so signing that result again does not match and appends a second footer. The function claims to be idempotent against its own output.
Reviewed by Cursor Bugbot for commit 68e5de9. Configure here.
| } | ||
| prefix = rest; | ||
| } | ||
| } |
There was a problem hiding this comment.
Wrapper flag values skip the redirect
Medium Severity
is_command_position walks back over wrapper names and their flags, but a flag value is treated as a non-command word and stops the walk. Spellings such as sudo -u user gh pr create, nice -n 10 gh pr merge, and env -u FOO gh issue comment therefore do not redirect and can still write an unsigned body.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 68e5de9. Configure here.
The prose gate rejects "used to". The comments now say what the refusal names: ready is update with draft set to false, merge's confirm is its own field, and watch_ci takes branch. Refs #6545 Co-authored-by: Mac Anderson <mac@oxagen.sh>
env -S and --split-string run the split string, so treating them as values let gh pr create through. A single-dash cluster is read letter by letter, so sudo -Eu root and env -iu FOO still name the command. Refs #6545 Co-authored-by: Mac Anderson <mac@oxagen.sh>
## What & why Bugbot reviewed #6545 once and left two medium findings in `forge_redirect`. Both are fixed here. `gh pr ready` was refused with action `update with draft`, which is not an enum value on `pull_request` and reads as `draft: true`. Ready is action `update` with `draft: false`. `gh pr merge` now names action `merge` and `confirm: true` as its own field. `gh pr checks` points at `watch_ci` with `branch` set to the pull request's head. That tool has no `action`. A wrapper flag that takes a value (`sudo -u user`, `nice -n 10`, `env -u FOO`) was treated as a command word, so the `gh` after it was not redirected. The parse now consumes that value for the wrapper flags that take one. `sudo -n echo gh` stays open, because `-n` on sudo takes no value and `echo` is the command. The low finding, an empty body signed twice, is residue in #6553. Refs #6545 Refs #6553 ## The witness - [x] This PR includes a witness test (fails on `main`, passes here), **or** - [ ] No witness needed (pure refactor / docs / CI) — because: `every_redirect_names_a_real_tool_input` reads the action between backticks and requires it to be in the tool schema's enum. On main the ready refusal puts `update with draft` in those backticks, which is not an enum value. `a_wrapper_flag_value_does_not_hide_the_command` requires `sudo -u user gh pr create`, `nice -n 10 gh pr merge`, and `env -u FOO gh issue comment` to redirect. On main `is_command_position` stops on the flag's value and returns `None`. `cargo test -p stella-tools --lib forge::redirect` — 12 passed. ## The gate - [x] `cargo fmt --check` (formatted with `cargo fmt -p stella-tools`) - [x] `cargo clippy -p stella-tools --lib -- -D warnings` passed - [x] `cargo test -p stella-tools --lib forge::redirect` passed - [x] Docs updated where behavior/flags changed (the refusal text is the documentation the model reads) - [ ] CLA signed (the bot prompts on your first PR — nothing to do per commit) - [x] `Refs #6545` appears above and as a commit trailer. This PR does not close that issue; it is already merged. ## Fix over file - [x] Extra fixes in this PR: none beyond the two medium review findings — **or** none - [x] Filed, with the reason a fix could not ride this PR: the empty-body footer is #6553. It is a low finding on merged code, outside the two review findings this PR fixes. The docs deploy failure on `331543e` is the same `ssm:SendCommand` `AccessDeniedException` for `gha-deploy-stella` that failed the previous main push (run 35527940671). The role policy is outside this repository, so this PR does not change `.github/workflows/docs.yml`. ## Ground-rule check - [x] No I/O added to `stella-core`; no new deps without justification below - [x] No new outbound network calls (Stella never phones home) ## Anything reviewers should know? Bugbot and Sourcery each reviewed #6545 once, so the medium findings are fixed here rather than filed. `command -v gh` is now left open: `-v` takes the name and does not run it. `docs guards` and `main is not known-broken` fail on main's account (#6548). #6550 registers SCR-006 in the document manifest and #6552 rewrites its prose. This branch merges main again once they land. <div><a href="https://cursor.com/agents/bc-74228cf7-e29e-4613-a179-fc7ac74615cd?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/automations/c630848f-b60f-11f1-bb68-864e54d14197"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/view-automation-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/view-automation-light.png"><img alt="View Automation" width="141" height="28" src="https://cursor.com/assets/images/view-automation-dark.png"></picture></a> </div> ## Summary by Sourcery Ensure forge redirects both detect wrapped `gh` invocations reliably and provide schema-accurate retry instructions. Bug Fixes: - Correct forge redirect guidance so refused GitHub CLI commands reference valid tool inputs, including draft, confirmation, and branch parameters. - Prevent wrapper flags that consume values from hiding executable `gh` commands from redirect enforcement. Enhancements: - Refine command-position parsing for shell assignments, wrappers, flag clusters, and wrapper-specific options while preserving non-executing cases such as `command -v gh`. - Distinguish signing write tools from read-only CI monitoring in redirect messages. Documentation: - Update refusal guidance to match the schemas and inputs agents should use when retrying redirected commands. Tests: - Add regression coverage for wrapper flag values, clustered options, non-executing command forms, and schema-valid redirect guidance. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes bash-layer enforcement of which `gh` invocations are blocked and the guidance agents get on retry; incorrect parsing could either block benign commands or miss wrapped `gh` writes. > > **Overview** > Fixes **forge `gh` redirects** so refusal text matches real tool schemas and wrapped shell spellings cannot skip signing. > > **Refusal copy** now goes through a `refusal()` helper: each redirect uses a `call` snippet (e.g. action `update` plus `draft: false` for `gh pr ready`, `confirm: true` for merge, `branch` for `gh pr checks` → `watch_ci`) and only mentions signing for write tools. > > **Command-position detection** is rewritten to parse forward from the last separator, consuming wrapper flags and their values per wrapper (`sudo -u`, `nice -n`, `env -u`, clusters like `-Eu`, GNU `env -S`). That closes the hole where `sudo -u user gh …` was treated like `echo gh …` and allowed unsigned forge writes; `command -v gh` and `sudo -n echo gh` stay allowed. > > **Tests** add witness coverage that every redirect names a schema-valid action/input and that value-taking wrapper flags still trigger redirects. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 5c5fc38. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Mac Anderson <mac@oxagen.sh>


What & why
Stella writes to a forge through
bashandgh. Nothing signs what she writes,so a commit, a pull request body, an issue and a comment all reach GitHub with no
record of what produced them. The prompt cannot fix this:
HookDecisionisAlloworDeny, with no rewrite arm, so a hook can refuse aghcall butcannot append a footer to it. Attribution is deterministic only if Rust appends
it.
This PR adds a forge plane of three tools, signs every body they send, and turns
the
ghsubcommands they cover into a refusal that names the tool to callinstead.
Refs #1377
The three tools, for eleven verbs
Every tool schema rides the byte-stable prompt prefix on every model call in the
session (invariant 7), so eleven tools is eleven schemas billed per call. The
verbs group by object instead:
pull_requestcreate,update,close,merge,comment,edit_commentissuecreate,update,close,comment,edit_commentwatch_ciRead literally, invariant 9 forbids that: a parameter may scope an operation
and may never select one. So the grouping is a decision, and it is recorded as
ADR 0044 rather
than asserted in a PR body. The record states the four conditions that let a
tool group verbs, and the last of them is the new mechanism below. Nothing else
in the tree groups this way.
mergerefuses withoutconfirm: true, because itis the one action here that writes to a shared branch and stays there.
Switching off one action
Invariant 9's second reason is that per-tool policy must be able to withhold the
destructive verb without withholding the benign one. Grouping broke that: the
session's gate above the tool stack answers for a whole tool, so
"pull_request": "off"tookmergeandcommenttogether.ToolPolicytakes a dotted key now,"<tool>.<action>", answered byallows_action. Both forge tools ask it per action before anything else runs:{ "tools": { "pull_request.merge": "off", "issue.close": "off" } }That agent opens pull requests and comments on them, and never merges one.
An action key can only narrow, because
allows_actionasksallowsfirst. Atool switched off by its own name, by its catalog group, or by
"*"stays offwhatever an action key says, which keeps
deny_all_from's rule true of actionsas well. The tools read the session's own resolved
ToolPolicy, handed down theone assembly point (
write_dirs::registry_rooted_attoforge_install::attachto
ToolRegistry::attach_forge), so the gate and the tools cannot come todisagree about what is switched off. The refusal is
ErrorClass::RefusedByPolicyand names the key.stella toolsprints a tool that is on with an action withheld as on, and namesthe withheld actions beside it. Without that line the switch is invisible
everywhere until a session hits the refusal.
Invariant 9's text is amended in place to name this mechanism. The numbering is
an address, so nothing is renumbered.
issuereaches whatever trackerstella.tomlnames, throughstella_protocol::issue::IssueProvider. A Linear workspace gets Linear. That isthe half
gh issue createcould never do.What is blocked, and what is not
bashandghstay open. Aghsubcommand is refused only when a tool herecovers it:
pr create/edit/ready/close/merge/comment/checksandissue create/edit/close/comment.gh pr view,gh pr list,gh pr diff,gh issue list,gh issue viewandgh run watchare all deliberately absentfrom the table, and so is every other
ghsubcommand, and so is all ofgit.The refusal text names the tool, the action, and says the rest of
ghis open.With neither adapter attached,
forge_redirectreturnsNoneand theghcallruns: the gate is the slot, so a workspace with no forge configured loses
nothing.
The attribution text
Five keys under
[self_driving.attribution]instella.toml, each defaultingto the string this repository asked for:
commit,pull_request,issue,issue_comment,pull_request_comment.stella_autonomy::signis idempotentagainst its own output, so an
updatethat echoes a signed body back gets onefooter rather than two.
Git hygiene in the prompt
git_hygiene!()inSYSTEM_PROMPTandPIPELINE_SYSTEM_PROMPT: branch fromthe fetched tip of the remote default branch before writing, and ask the driver
what to do with a dirty tree rather than deciding for them.
The witness
main, pass here)cargo test -p stella-tools forgeis 31 tests, all new, all failing onmainbecause the module does not exist there. They cover each action's argument
errors, the merge confirmation gate, the redirect table in both directions (a
covered subcommand refuses, an uncovered one runs), the empty-slot passthrough,
and
sign's idempotence.Two of them hold the per-action gate, one per tool, because one gate serves both
tools and each can lose its call site on its own:
a_switched_off_merge_refuses_while_comment_still_runsanda_switched_off_close_refuses_while_comment_still_runs. Each asserts the errorclass, the key named in the message, that the provider recorded nothing, and
that a second action still runs.
Both were proven by control rather than asserted: deleting the
permitscallfrom
pr.rsfails the first, and deleting it fromissue.rsfails the second.Doing it turned up something the test would otherwise have papered over. Without
the gate,
mergedoes not reach the forge anyway: it falls through to theconfirm: truecheck and is refused asInvalidInput. So the "nothing reachedthe provider" assertion passes either way, and only the class assertion tells
the two refusals apart. That is recorded in ADR 0044's consequences.
cargo test -p stella-tool-facts policyis 18 tests, coveringallows_action'sprecedence and the narrowing rule.
The gate
cargo fmt --all— cleanmake prose— OK on all three ratchets (counts, reading grade, header density)make guards-fast— greencargo test -p stella-tools forge— 27 passed, 0 failedRUSTDOCFLAGS="-D warnings" cargo doc -p stella-tools --no-deps --document-private-items— cleancargo clippy --workspace/cargo test --workspace— not run here. CLAUDE.mdreserves the workspace build for CI on this machine, so this PR's CI run is
its evidence.
docs/tools/pull_request.md,issue.md,watch_ci.md, andthe five keys in
website/content/docs/stella-toml.mdxFix over file
RISK_NOTEand two of the fallbacktexts carried a filler adverb and an honesty declaration, so every page the
template generates shipped both.
scripts/prose-baseline.txtrecords thatas 40 entries: one
filler-adverband onehonesty-declarationfor each ofthe 19
.tomlpages and fordocs/tools/README.md. Fixing the templateclears all 40 at once —
python3 scripts/check-prose.py --reportnow showsno hit of either pattern anywhere under
docs/tools/, only theissue-referencehits that come from the per-tool declarations rather thanthe template. Those 40 baseline entries are therefore stale-high. Reclaiming
that slack is
make prose-retighten, which AGENTS.md says is a PR of itsown, so it is deliberately not done here.
website/content/docs/stella-toml.mdxspelled the wordmark capitalisedon seven lines, which
make brand-casefails on, and repeated one95-character default string verbatim in five
defaultValueattributes. Thefive defaults move into one fenced block, which reads once and which the
guard skips by design, since a config excerpt is a quotation rather than
brand copy.
one handoff posted as a comment on Git Tools & Agent Git Workflow Bindings #1377 (below).
What was deferred, and why
The
[run] create_worktreeskey is not here. A no-worktree path is 250 to 400lines across roughly 14 call sites and carries two hazards that make a partial
fix worse than the defect:
graph_seed::seed_from_parent(root, path)with equal paths asks SQLite to copya database onto itself. Its own test
the_worktree_gets_its_own_file_not_the_repositorysasserts the oppositeinvariant.
tree_changereadsgit status --porcelainand attributes all dirt to theturn. In the repository root an operator's uncommitted edits would make a no-op
turn report
Changed, anddrive.rswould open a pull request on it.It also needs a new exclusivity invariant (root mode is one session per clone,
so
parallel.rswaves can never use it) and aLoopConfigthreading decision.That is larger than this session, which is AGENTS.md's third deferral case. The
full map is posted as a comment on #1377.
Ground-rule check
stella-core. The forge tools live instella-tools, andthe adapters a crate above in
stella-cli, reached through a host-filledslot.
configured and the
ghbinary they already installed.Anything reviewers should know?
Why the adapters are reached through a slot.
PullRequestProviderandIssueProviderare implemented instella-cli, one crate abovestella-tools,so the tool cannot own them.
ForgeSlotsis the same shape ascrate::subagent::DispatcherSlotandcrate::registry::question: the host fillsit at registry construction. A tool with an empty slot is still listed, so the
schema list never depends on settings the prompt cache cannot see change.
This PR closes nothing. #1377 is the wider git-tools issue and this advances
it without finishing it, so the trailer is
Refs, notCloses. It wants thecloses-nothinglabel rather thanno-issue: the change is substantial.watch_ciisread_onlybut not speculation-safe. Each call spends sharedrate-limit quota, so the engine must not fire it speculatively.
Commit
622a53262's message carries a false claim. Read this instead. Itsays
check-brand-case.sh"cannot see a JSX attribute value." That is wrong,and I did not check it before writing it. Probing the guard with a fixture
containing four shapes — a single-line
defaultValue, atitleattribute,plain prose, and a multi-line attribute — flags all four:
The guard strips fenced blocks, inline code spans, and URLs, and reads
everything else, JSX attributes included. So the real shape is the opposite of
what that commit message says: the guard saw the five
defaultValuestrings andcorrectly refused them, and moving them into a fenced block is me taking the
remedy the guard offers, not routing around a blind spot.
What is true is narrower, and is a design limitation rather than a defect: there
is no way to exempt an attribute value that the component renders as a code
span, short of moving it into a fence. The fence is the better document anyway,
so nothing is filed for it — an open design question with no defect behind it is
not an issue (AGENTS.md § "Fix over file"). The commit message cannot be
rewritten without a force-push, which this repository forbids, so this note is
the correction.
Review round:
68e5de9e9A peer review of
52119dc35found six defects. All six are fixed here. Eachis a separate fix in one commit, named so a reviewer can read them apart.
The
ghredirect matched a literal token, and six spellings walked pastit.
command gh,env gh,sudo gh,GH_TOKEN=x gh,/usr/bin/gh, and aghopened inside$(...)or backticks all reached the forge carryingwhatever footer the model typed.
mentions_in_linenow asks three questions:names_ghreads the last path segment,space_substitutionsturns asubstitution opener into its own token, and
is_command_positionwalks backover wrappers and leading assignments to a separator. Two tests cover 22
spellings, split by direction: the wrapped and pathed forms must be refused,
and the same words inside an argument (
echo command gh pr create,git commit -m "gh pr create") must stay open.Proven by control, not asserted. With
WRAPPERSemptied andnames_ghreduced to
word == "gh",cargo test -p stella-tools --lib forgefails onmust be refused: command gh pr create --fill. Restored, 38 pass.One lock had two policies. The redirect read the forge slots with
.read().ok()?, so a poisoned lock read as "nothing is attached" and openedevery covered
ghcommand.ForgeSlots::has_forgeandhas_trackerstate thepoison policy once, beside the lock, and the redirect asks them.
An unknown action was answered by whatever failed next. On a workspace with
no
gh,action: "frobnicate"reported "no forge is configured", which sendsthe reader after a missing binary instead of a misspelled word.
known_actionruns before the slot lookup. The trailing match arm stays and catches the
opposite drift: an
ACTIONSentry nobody wrote an arm for. Witness:an_unknown_action_is_not_reported_as_a_missing_forge.A failed description write could leave a pull request marked ready.
pull_request_provider::updaterangh pr readybeforegh pr edit, so aghthat failed on the text left reviewers the old description under a marksaying it was ready to read. The text is written first. Neither order is
atomic; only one of them can publish the wrong thing.
Five fixtures named a footer this crate does not ship.
"created by stella*"inself_driving_cmd's tests becamestella_autonomy::SIGNATURE, so they cannot drift from it again.sign()'s idempotence is tail-anchored, and now says so. A footer quotedmid-body gets a second one at the end. Reaching into the body to find and move
it would mean editing what the model wrote, which is the one thing this plane
does not do.
Deferred, and why
Commit signing is the one surface the model can still paraphrase, and it is
filed as #6546 rather than fixed here. Both fixes are maintainer decisions
under AGENTS.md § "Fix over file" case 1: a
prepare-commit-msghook isdeterministic and covers humans too, but this repository already sets
core.hooksPath=.githooksand a second hook home is a decision; a commit toolis deterministic and agent-scoped, but it costs a schema on the byte-stable
prefix (invariant 7) and takes
git commitaway frombash. The issue carriesboth, the file paths, and what done looks like. It moves reliability.
Evidence
cargo test -p stella-tools --lib forge— 38 pass.cargo test -p stella-cli --bin stella self_driving_cmd— 267 pass.cargo fmt --check,make prose,make guards-fast— clean.cargo clippy -p stella-tools -p stella-autonomy -p stella-cli --all-targets -D warnings— clean.cargo doc -p stella-tools -p stella-autonomy --document-private-itemswithRUSTDOCFLAGS="-D warnings"— clean, after it caught a private intra-doc linkthis round added. The workspace build and suite are CI's, per CLAUDE.md.
Reviewer sign-off
The reviewer that raised these findings re-read the tree at
68e5de9e9andreports nothing outstanding. It checked its own ranked list against the source
rather than against this description, which caught a numbering mismatch: the
six fixes above map to its suggestions, and its ranked findings 3 to 6 are the
--raw-fieldflag, thestella.example.tomlcommit claim, theforge_installscope doc, and the title prefix onupdate. All four are inthe tree.
One of its findings is worth restating, because the fix reads like a
refactor.
split_whitespaceerased newlines, so a two-line scripttokenised into one stream and
is_command_wordcomparedghagainst thelast word of the line above it.
git push -u origin HEADfollowed bygh pr create --fillwalked straight through the redirect, and a newline isthe most ordinary separator a model writes.
mentionsscans per line now, soa line boundary is a command boundary and
index == 0is correct rather thanaccidental. Witness:
a_command_on_the_next_line_is_still_checked, withnaming_the_command_on_a_later_line_is_still_not_running_itas the controlthat per-line scanning does not turn
echo gh pr createinto a run.