fix(auto-improvement): publish only the commit the pipeline committed - #8981
Conversation
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS Sound root-cause fix — immutable object ids replace TOCTOU-prone symbolic refs — but the diff's security-surface hunks outgrew the description, and two named publish paths keep the hole. WatchThe description's accounting ("+2217/-156 across six files", "Two test files outside the change's own") no longer matches the diff (11 files, +3574/-232). The unaccounted hunks are exactly the security-surface expansion: the durable quarantine-marker subsystem in
Suggestions
[DESIGN-REVIEWED] f495a1e |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
GPT 5.6 Review — ✅ human override acceptedHuman judgment by @chenmingwei23 overrides the GPT 5.6 finding for This comment is updated in place on each push. The model was not re-run because an authorized human decision supersedes it. False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of First-Principles-Verdict: CONCERNS The rebase retry now requires git ≥ 2.41 ( Not justified as shipped
What this change shipsInventory (10 items) — 9 justifiedIntent: Make the auto-improvement pipeline publish, scan and record exactly the commit it verified, and never reuse a clone whose rollback failed — a FIX (provenance: tests added here fail on base, e.g. the amending-reviewer test).
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] f495a1e |
1eb2cd3 to
c7347c8
Compare
c7347c8 to
aefc33f
Compare
aefc33f to
c19de50
Compare
c19de50 to
bf586f4
Compare
bf586f4 to
214a624
Compare
21bff65 to
96fcc43
Compare
96fcc43 to
551d47f
Compare
551d47f to
a18920b
Compare
|
Converting this to a draft. Work on it stops here. The change is complete and mutation-verified, and the board is otherwise clean on the current head: The remaining red is a hardening gap in this change: a failed clone retirement leaves the clone #9220 carries what is worth keeping: an open design question for the retirement fallback, and a Nothing is deleted. The branch and its single commit stay in place. |
|
@chenmingwei23 I re-checked this PR against Nothing of this PR's goal has landed. On What is still missing on Three things to settle before this is reviewable:
Finally, the current head also touches Posted from the 2026-09-08 open-PR relationship audit (read-only, one auditor per PR); reply here if any of this is wrong. |
`_direct_push` was handed the sha its finalizer produced -- the commit that was measured, reproduced and written to the ledger -- and then pushed `HEAD:refs/heads/<dest>`. Nothing compared the two, so whatever HEAD pointed at when the push ran is what got published. The window between them is not empty. `_prepush_review_clean` runs an agent in that same clone with `Bash`/`Edit` for up to 30 turns, its prompt invites it to "fix a trivial finding in the clone and re-review", and the runner's git denylist covers only `push` and `remote set-url`, so `git commit --amend` is permitted there. The gate compares full object ids and fails closed when it has no valid one. It sits after the review gate and before the credential scan and the push, and deliberately not around the push itself: `_push_with_rebase` rewrites HEAD on purpose and re-verifies the replayed tree. The anchor is a full forty-hex object id, captured at the commit and never re-derived. A short sha is an ambiguous NAME: git reads a revision as a ref name before an abbreviated object id, and a prefix stops naming one object as soon as a second object shares it. Either reading is arrangeable by anything that can write the repository between a read and the capture, so a path that re-resolved an abbreviation would verify one id and publish another -- the substitution this change exists to refuse. The finalizers therefore return a validated full id rather than `rev-parse --short HEAD`, and the lookup that re-resolved it is gone rather than guarded; a short form is derived for display only. Six residual holes in the same path, all raised by the GPT review: The credential scan read a RENDERED DIFF, which is a report rather than the bytes that get published. For a blob git judges binary it prints only "Binary files a and b differ" with no content, and a single NUL byte anywhere in a file is enough to trigger that, so a credential inside such a blob passed the gate unread. Measured on a real repository: a blob holding NUL, then a key, then NUL renders as that one summary line with zero occurrences of the key, while `cat-file blob` on the same object yields it. The scan now enumerates the revision's own changed blobs with `diff-tree -r --root` and reads each one with `cat-file blob`, so the scanned bytes and the transferred bytes are the same bytes. Two consequences: a root commit is covered by the same listing rather than a separate weaker branch, and a WHOLE blob is scanned rather than the changed hunks, because a hunk is a view of an object and it is the object that lands on the remote. Deletions, gitlinks and null destination ids carry no blob and are skipped; every failed read refuses. A replay writes a NEW commit object, and git takes its committer identity from configuration rather than from the commit being replayed, while author and message carry over. `GIT_SAFE_CONFIG` pins hooks, fsmonitor, attributes, excludes and submodule recursion but no identity, so the reviewing agent's shell -- whose git denylist covers only `push` and `remote set-url` -- could choose who the published commit says committed it, and nothing masks a global config either. The replay now pins its committer to the identity `src` already carries, read off that immutable object rather than from any config file: the pipeline committed it before any agent ran. Pinning alone is not the guarantee, because the `GIT_COMMITTER_*` environment outranks a `-c` override and author and message are not pinned at all, so the replayed commit's author, committer and message are compared against the authorized object's and a mismatch refuses the push. Comparing two objects to each other keeps configuration out of the answer. The credential scan read only what a commit CHANGES. A commit's header lines are separate bytes the push transfers just the same, and the root-commit branch suppresses them outright with `--format=`, so nothing scanned the author or committer identity or the message. `git rebase` takes the committer identity from configuration rather than from the commit it replays, which puts those bytes under the control of the same actor this path already distrusts for `git replace` -- the reviewer runner's git denylist covers only `push` and `remote set-url`. Measured on a real repository: a replayed commit whose identity held a key-shaped address carried it in the committer line while both scan branches printed nothing but the tree change. The scan now reads the raw commit object as well, so headers and message reach the scanner as the transport sees them. A credential once pushed cannot be unpublished, so this fails closed. The rebase retry captured the replayed commit from ambient `HEAD`, and every later check bound to that captured id -- including the HEAD-equality check, which compares HEAD against the same value. A commit substituted into HEAD before the capture was therefore corroborated by the whole chain rather than caught, and nothing reached back to the authorized source. HEAD is the only handle a rebase leaves, so the window cannot be closed; the fix makes it harmless by binding the replay to its RESULT -- the tree that a replay of the authorized source onto the fetched base must produce, computed from those two immutable ids by git's own merge (`merge-tree --write-tree`) and compared against the tree the captured commit carries. Quarantine after a failed rollback relied on renaming the clone aside, and that rename can fail -- from the same held-handle cause that failed the rollback, so the two are correlated rather than independently rare. The failure path only logged "MUST NOT be reused", which the next run does not read, and clone reuse attests git metadata and remotes without ever inspecting the branch tip. A marker is now persisted at a masked crew-home leaf and `_setup_safe_clone` refuses a marked clone. It clears structurally rather than on a timer: the marker names one directory and reports nothing once that directory is gone. Where the marker lives is what makes it trustworthy, and the scratch tree it started in could not. That tree is one the agent legitimately writes to, so a marker there can be truncated through a pre-placed hardlink, or simply deleted afterwards, which no open flag prevents. An app `data/` directory is no better: it is writable from inside the agent sandbox unless a leaf is explicitly listed. So the markers sit at `<crew home>/quarantined-clones`, added to `sandbox._CREW_HIDDEN_LEAVES` and to `security.sensitive_home_dirs()` -- the repository's two existing fences, mirroring `aws-control-staging`. Nothing in-sandbox reads a marker (they are written and consulted host-side), so HIDDEN is the right disposition of the three that list offers, and TOP-LEVEL matters on its own: a mask covers the name it binds over and not that name's ancestors, so a leaf under `apps/auto-improvement/data/` would sit below a directory an agent can rename out from under the mount. The root is refused outright if a link is planted at it, since that would put every marker outside the fence where no per-marker check could see it. The write and the read had to be taught to agree on what "present" means. `O_TRUNC` is gone from the open entirely -- an existing entry is the guard already standing, not a failure -- and the reader tests for the entry with `lstat` rather than `exists`. A dangling symlink is an existing entry to an `O_CREAT|O_EXCL|O_NOFOLLOW` open and an absent file to `exists`; split that way, a planted dangling link would let marking report success while the guard read no marker at all. Under `lstat` anything at that name counts, so a planted entry can only make the guard more conservative: it reports the clone quarantined, which refuses it. Refs #8452 A TREE rather than a patch identity, and the difference is not cosmetic. Two patch-identity forms were tried and both leak. The default `git patch-id` algorithm STRIPS WHITESPACE, and in Python whitespace is control flow, so a substituted commit that dedents a call out of its guard hashes identically to the authorized change -- measured on git 2.50.1 against a diff pair whose only difference was `+ publish()` inside an `if` versus `+ publish()` after it: one id under `--stable`, two under `--verbatim`. `--verbatim` fixes that but still ignores hunk POSITIONS, which it must in order to recognise a change replayed onto a moved base, so the same added and removed lines placed elsewhere in the file carry the same identity. A tree object names the exact content of every path, so relocation is a different tree and there is no residue left to argue about. The comparison fails closed: a merge that conflicts, or a git too old to know `--write-tree`, yields no usable tree and the publish is skipped, which costs a cycle and never publishes the wrong thing. The quarantine marker is written BEFORE retirement is attempted, not after it fails. Ordered after, the durable record depended on a path reached only once two things had already failed, so a transient failure of the marker write left the clone reusable with nothing recording that it must not be -- fail-open at the exact moment the guard is needed. Written first, the record exists before anything is disturbed, a rename that then succeeds merely makes it stale (the marker names a directory, so it stops reporting once that directory is gone and is pruned on the way past), and a write that fails is known before the tree is touched.
|
/ai-review override gpt f495a1e: The finding is the residual after three independent guards fail simultaneously, it is logged loudly at every stage, and the prescribed remedy adds a fourth durable write with the third's failure mode. Recording the reasoning so the override is auditable. The finding asks that a transient quarantine failure not leave a poisoned clone reusable. The ordering it prescribes is already the code: Two reasons not to patch it here. The trigger needs three filesystem operations to fail together, and the consequence is recoverable by an operator who has been told exactly what to do. And the prescribed fix -- persist a fallback quarantine record and have clone setup consult it -- is a fourth durable write whose failure mode is identical to the third's, so on a failing filesystem it fails too; that is how a seam earns a fifth finding rather than closing. This is the fourth hardening pass on the marker-writability and clone-certification seam. The three earlier ones are all still in the code: the marker moved to a masked crew-home leaf, the The residual is recorded in the local backlog for re-judgement rather than dropped. |
Human judgment recorded@chenmingwei23 marked the gpt AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
Problem / Motivation
Driver._direct_pushis handed the sha its finalizer produced -- the commit thepipeline measured, reproduced and wrote to its ledger -- and then published
HEAD:refs/heads/<dest>after scanningHEAD~1..HEAD. Nothing tied either of those tothe verified commit, so what reached the branch, and what got scanned on the way, were
whatever
HEADhappened to point at when each step ran.The window between the two is not empty.
_prepush_review_cleanruns an agent insidethat same clone with
allowed_tools=["Bash", "Read", "Edit", "Grep", "Glob"],max_turns=30andtimeout_s=420. That runner's git denylist is_FORBIDDEN_SUBCOMMANDS["git"] = (("push",), ("remote", "set-url")), sogit commitispermitted there. A reviewer can therefore amend the clone, report
REVIEW: clean, andthe commit that lands is one this pipeline never verified.
Why it matters
Everything the loop exists to guarantee is attached to a specific commit: the A/B
measurement, the reproduce measurement, the credential scan, the review verdict and the
committedledger row. If HEAD moves after the finalizer, all of that describes onecommit while a different one is on the branch -- and the ledger records the landed sha,
so the record looks consistent while the content was never checked. There is no
after-the-fact signal either: whether the published state was verified depends only on
when the amend happened, and a poll that reads the forge every few minutes cannot see a
state that existed for one.
What changed (motivation -> approach -> change)
Symptom: the pipeline can publish, and can credential-scan, a commit it never verified.
Root cause: the publish path addressed the clone through the symbolic ref
HEAD, whicheach step resolves separately. Every check against it is therefore a check-then-use pair,
and the review gate opens a long window in which an agent with
EditandBashrunsinside the very clone about to be published.
The change stops using the symbolic ref for anything that decides what gets published:
_direct_push, before thereview gate runs anything.
_revision_scans_clean(rev)credential-scans ONE revision's own content by id(
<rev>~1..<rev>, orshow --root <rev>), and BOTH publish paths call it.HEAD.git rebase FETCH_HEAD <rev>-- so whatgets replayed is an immutable id rather than whatever the branch points at, then resolves
the replacement to its own full id BEFORE the build gate runs, requires HEAD to still be
that object afterwards, scans THAT, and pushes THAT. Because that form detaches HEAD, the
branch is promoted onto the result with
git branch -fat exactly ONE place: afterre-verification, the identity check and the scan have all passed. Every other exit
re-attaches without moving the branch, which is both the conservative outcome and required
for correctness --
_reset_provisionalrolls back withgit reset --hard, which on adetached HEAD moves the detachment and leaves the branch still carrying the commit.
core.useReplaceRefs=false, set once in_git,because git substitutes objects named by
refs/replace/<oid>in READS while the pushtransport sends the ORIGINAL -- so a
git replace(not on the reviewer runner's denylist)could otherwise have a read see a clean decoy while the credential-bearing object was
transferred. Two instances were found one at a time, the credential scan and then the
rebase, which is why the setting belongs at the chokepoint rather than on the next call
someone remembers.
git rebasedrop ours as already-applied and leave HEAD at the remote tip, which everylater check would then bind to consistently while the ledger recorded an unrelated commit
as the one this pipeline landed.
means the repository is not in the state the push assumes. The restore is also
TRANSACTIONAL --
git branch -flands before the checkout can fail, so a failed checkoutputs the branch back where it was rather than leaving it carrying an unpushed commit.
Editgrant is withdrawn, not just the prompt sentence thatsanctioned using it. What decides whether a reviewer can modify the clone is the tool list, not
the prose telling it what to do, so removing the instruction while keeping the capability left a
reviewer that may still edit with no sanctioned reason to.
Bashstays -- the review needs gitto read the diff -- so this is least privilege rather than the boundary: a shell can still write
files, which is exactly why the identity checks are what refuse a moved HEAD.
base..rebased_id, notbase..HEAD.HEADresolves when that line runs, and the build gate just above it executes the targetrepository's own test suite, so a teardown can move it in between; the equality check before it
proved HEAD was the replay a moment earlier, which is a check-then-use pair rather than a
guarantee. It also makes the range COUNTED identical to the range PUBLISHED, since the push
sends
rebased_id. Same property as the fetch fix one step earlier: prefer the immutableidentifier already captured over the mutable name that resolves at read time.
commit that was refused and never published; the next winner commits on top of it, and that
winner's single-revision scan (
<rev>~1..<rev>) cannot see the parent its own push wouldpublish -- so refused content lands through a scanner that never looked at it. The rollback now
reports its outcome, latches the failure and stops the run, and
_direct_pushrefuses while thelatch is set. The failure STOPS the run rather than being retried -- a rollback that cannot
be trusted to have happened must not be followed by work that assumes it did, and a retry would
be one more thing whose own failure has to be handled. The halt is unconditional: no retry, no
further winner, no next cycle. It is enforced at three places because the publish gate alone was
not enough --
_apply_bug_winnercalls the PR pipeline'semit_bugBEFORE_direct_push, andthat path reaches
pr_recipe._push_fix_branch, which pushesHEAD:refs/heads/<branch>and knowsnothing about the latch. So the guard sits at the top of both winner-applying methods (upstream of
every push either can reach), again at the publish gate, and again through the run's stop flag,
which the cycle loop reads. One cycle can hold several bug winners, so a latch read only at cycle
boundaries would have let the next winner in the SAME cycle publish. And the clone is
QUARANTINED on disk, so a failed rollback blocks reuse of that clone across RUNS, not just
publishing within one. The latch lives in memory and clears when the process ends, while the
refused commit is on disk and the clone is reused -- so the next run would have started with a
clear latch on poisoned state, which delivers the guarantee for one process lifetime rather than
for the hazard. Retirement renames the clone out of its canonical name, and that name is the
predicate
clone_setup._setup_safe_clonebranches on for reuse, so the next run clones freshinstead of adopting it. It is the app's own primitive, whose docstring already says it "prevents
a later run from adopting a rejected provisional commit": nothing about how clones are located or
named changes, and the bytes are preserved for diagnosis.
FETCH_HEAD. That ref is amutable FILE in the clone, and the pre-push reviewer's shell can rewrite it between the fetch
and the rebase: the replay then lands on a substituted parent whose content nothing scanned,
and the push carries it. Both the rebase input and the replayed-commit count read that same
name, so they corroborated the substitution instead of catching it. Measured on a throwaway
repo with a bare remote: with
.git/FETCH_HEADrewritten to a prepared child,rev-list --count FETCH_HEAD..HEADstill reported1while the truth against the real tip was2, the scanned range held only our own file, and the remote ACCEPTED the push -- the foreignfile landed through a scanner that believed it had looked.
git fetch --porcelainreports thetip as
<flag> <old-oid> <new-oid> <ref>on its own stdout, so the id comes from the fetchitself and there is no ref to substitute. A fetch that reports no usable id -- including the
all-zero null id of a deleted ref -- refuses the retry.
srcis REQUIRED on the publish helper, and the three symbolic-source arms are gone.It shipped as
src: str = "HEAD""for callers that have nothing more specific"; there werenone. That default kept alive the pre-fetch tamper check being SKIPPED, a bare
rebase FETCH_HEADreplaying the BRANCH, and a post-pushrev-parse HEADfor the ledger --each the check-then-use shape this change exists to remove, reachable by anyone who later
added a caller and omitted the argument. Requiring the argument deletes all three. Pinned on
the signature, because a revived default changes nothing observable until someone omits the
argument, and then it changes what gets published.
_revision_scans_cleanno longer re-spellscore.useReplaceRefs=false:_gitinjects it onevery call, so it was one guarantee written twice, and only the chokepoint spelling is pinned. The
chokepoint covers THIS MODULE's reads only:
pr_recipeandbackend/commit.pydo their own gitreads and still honour
refs/replace/*, so their scans remain substitutable the same way. Out ofscope here, named so the guarantee is not read as repository-wide.
_reset_provisionalis now ONE atomicgit checkout -f -B <branch> <pre_sha>.git reset --hardacts on whatever is checked out, and this runs after the pre-pushreviewer has had a shell in the clone where
git checkoutis permitted -- so it couldhard-reset an unrelated branch (destroying its commits) or a detached HEAD (a rollback that
silently does not roll back). An intermediate fix read HEAD, checked the branch out if it
differed, then reset, which was the same check-then-use pair one level up: a backgrounded
setsid git checkout victimlanding in between put the reset back on the wrong branch. Asingle invocation that NAMES the branch has no window. Fixed at the chokepoint because every
caller shares the exposure, and this closes a LATENT path older than this PR -- the old code
reached the same
reset --hardthrough a scan or push failure._push_with_rebase, rather than apost-push
rev-parse HEADthat a concurrent move could make name an unrelated commit._head_is_the_committed_shacompares the retained id againstHEADbefore the scan, soa reviewer amend is refused early with a truthful reason, and again after the scan as a
tamper detector.
Because the scanned object and the published object are named by the same immutable id on
both paths, they are the same object by construction rather than by timing. That holds for
THIS publish path. Two sibling sites still push through
HEAD--pr_recipe._push_fix_branchand
backend/commit.py-- and the threat model above reaches them; they are out of scopehere and named so the claim is not read as repository-wide. That is the
part a re-check cannot buy, and this PR learned it twice. An earlier revision tried to
prove the scan had covered the published object with a post-scan re-check of
HEAD--wrong, because the re-check is itself a check-then-use pair. A later one kept
HEADonthe retry path and justified it with "
_reverify_headhas just run" -- the same mistakeagain, in the one place the carve-out had been left: the build gate is a check, the push
is a later use,
HEADis re-resolved in between, and the replayed object had never beencredential-scanned at all because the caller scans the PRE-rebase object. A
non-fast-forward retry is not an edge case on this path; the retry's own docstring
records losing 3 of 6 gate survivors to that race. A third revision then resolved the
replacement AFTER
_reverify_head-- and that gate runs the repository-under-improvement'sOWN test suite, i.e. arbitrary code from the tree being published, so a test teardown could
move HEAD and have the moved object captured, scanned and pushed as though the gate had
measured it. Credentials were still covered; the build-verification invariant was not. The
capture is now before the gate with an equality check after it. A fourth revision still let
git rebasereplay whatever HEAD pointed at rather than the authorized source, so the retrycould bind its own capture, verify and scan to a moved commit and pass every check while
publishing content that never descended from the verified object; the rebase input is now
named explicitly. A fifth revision checked HEAD before the
fetchand still letgit rebase FETCH_HEADreplay the branch -- a network operation lasting seconds sat between the checkand the use. That check remains, re-documented as a tamper detector rather than the
guarantee it was mistaken for; the guarantee now comes from naming the object.
Four things about it are deliberate.
resolves a revision through ref names before abbreviated object ids, so re-resolving
the finalizer's short sha later is defeatable by exactly the actor this gate distrusts:
amend, then
git branch <old-short-sha> HEAD, and both sides resolve to the amendedHEAD. A retained full id has no such input.
_direct_push, which isprecisely how the retry path came to publish an unscanned object: a check attached to
one path and forgotten on the other.
srcstill defaults toHEADso no other callerof
_push_with_rebasechanges behaviour.rather than a binding. What it still catches is worth keeping: HEAD differing there
means something wrote the clone after the review returned, and a clone being written by
an unknown actor is not one to publish from, even when the object about to be published
is provably the verified one.
rev-list -1rather thanrev-parse --verify, because this method already asks aDIFFERENT question with
rev-parse --verify --quiet <rev>~1-- "does a parent exist", aboolean that picks the scan's range. One verb carrying two unrelated questions is
indistinguishable to a reader and to any caller keyed on the argv, and
test_ai_spine_driver_coverage's git double is keyed on exactly that.Everything fails closed. An unresolvable object -- the committed one or the replayed one
-- is the absence of the check, not a pass.
_direct_pushreturnsFalseand records aSTATUS_ERRORledger row, the same disposition every other gate there uses; the retryreturns the original push rejection, which is what that method already does for a
rebased tree that fails re-verification. The commit stays local and recoverable and the
caller's existing rollback runs unchanged.
Two test files outside the change's own are touched, and both are consequences of the
mechanism rather than scope:
test/test_ai_spine_driver_coverage.py(+17): one line in the sharedgitfixturescripting the gate's
rev-list -1resolution -- its fake returns rc 0 with EMPTY stdoutfor an unscripted key, which the gate correctly reads as unresolvable, so without it
every direct-push test fails closed -- plus two scripted/asserted argv strings that now
name an object id instead of
HEAD. No test's intent is changed. The blast radius wasmeasured rather than estimated: one test broke on the scan change, one on the push
change.
test/test_spawn_audit.py(+29): sixBENIGN_SPAWNSkeys with the justification thataudit's own assertion asks for. The new tests spawn literal
gitargv with both-Cand
cwdpinned to a per-testtmp_pathclone; nothing in the argv, the cwd or theresolved binary is agent-influenced.
Tests
Six behavioural tests for the first push in
TestOnlyTheCommitThePipelineMadeIsPublished, driving the real_direct_pushagainst areal one-commit git repository, plus three for the retry path in the existing
TestPushRetriesOnRace. Real git rather than a stubbed_gitis load-bearing for thefirst group: ref-versus-abbreviation resolution order and
HEAD-versus-object-id rangesare the subject of two of them, and a stub cannot exercise either.
test_a_reviewer_amend_between_commit_and_push_refuses_to_publish-- the real fault atthe real seam: a reviewer that edits, amends, and returns
REVIEW: clean.test_a_ref_named_the_abbreviation_cannot_shadow_the_committed_object-- amend, thencreate a branch named the finalizer's short sha.
test_the_credential_scan_reads_the_committed_object_not_head-- HEAD is swapped to adecoy immediately after the pre-scan gate returns; the blob handed to the scanner must
still be the verified object's.
test_a_move_after_the_scan_cannot_change_what_is_published-- the move is injected bythe credential scanner itself, the last thing before the push.
test_an_unmoved_head_still_publishes-- the accepting case, which also pins that therevision handed to the push is the full object id and not
HEAD.test_an_unresolvable_committed_revision_fails_closed-- the fail-closed arm.test_the_retry_refuses_a_replayed_object_that_does_not_scan_clean-- a credential inthe REPLAYED object, which the caller's scan never saw, must stop the retry.
test_the_retry_refuses_when_the_replayed_commit_cannot_be_resolved-- fail-closed onthe retry path too; it must not fall back to pushing the symbolic ref. It asserts the
LOG MESSAGE, because the later HEAD-equality check would also refuse an empty id, so
without that the check could be deleted with nothing going red.
test_the_retry_refuses_when_head_moves_while_the_build_gate_runs-- the gate itselfmoves HEAD, which is exactly a test teardown's capability.
test_the_retry_refuses_to_rebase_a_head_that_is_no_longer_the_source-- and it refusesbefore fetching, so no rebase runs on an unauthorized HEAD.
test_a_replacement_ref_cannot_substitute_what_the_scan_reads-- realgit replaceagainst a real repository, with a control proving the replacement is in effect for an
ordinary read while the scan still sees the true object.
test_direct_push_reports_the_sha_that_actually_landed(pre-existing, rewritten) nowscripts
rev-parse HEADto a DIFFERENT value, so a regression to reading the ref reddens.test_the_retry_replays_the_retained_object_and_only_then_moves_the_branch-- asserts therebase names the object, that the bare form is NOT used, and that the branch promotion
happens after the gate and before the second push.
test_a_failed_rebase_leaves_the_branch_alone_and_publishes_nothing-- a rebase thatconflicts must abort, must not force-move the branch, must re-attach the clone, and must
publish nothing.
git branch -fis only correct when HEAD IS the replayed result, so thispins that the promotion is unreachable from a failure path.
test_a_non_fast_forward_triggers_exactly_one_rebase_and_retry(pre-existing, extended)now also asserts the retry's refspec is the resolved id and not
HEAD:refs/heads/<b>.Each injecting test asserts its injection APPLIED before asserting the consequence, and
the three first-push refusal tests assert WHICH of the two identity checks refused, so
neither can be deleted without a test going red.
Mutation-verified by hand, forty-nine mutations, each with its own red set:
HEADinstead of the retained idtest_direct_push_reads_the_fetch_url_off_the_clone_when_the_profile_has_noneHEADHEADinstead of the replayed idcore.useReplaceRefs=falsefrom the scanHEADinstead of the object that was sentcore.useReplaceRefs=falsefrom_gitreset --hardon whatever is checked outsrc="HEAD"defaultrebase FETCH_HEAD)HEADfor the ledgerFETCH_HEADinstead of the reported idFETCH_HEAD--porcelain, so no id is reportedHEADEditgrantWriteinstead (same capability, other name)Bashtoo, making the review vacuousThe
src="HEAD"mutation reddens ONLY a signature assertion, and that is the honest reading:a revived default changes nothing observable while every caller passes the argument, and
changes what gets published the moment one does not. It is pinned where it is reachable rather
than left unmeasured.
Each mutation asserted its anchor was unique before being applied, because an unapplied
mutation is indistinguishable from a surviving one. Two earlier rounds of this table had
SURVIVORS and neither was recorded as a pass. First: the post-scan check silently absorbed
every case the pre-scan check was supposed to catch, and the refspec assertion lives in the
other file -- fixed by pinning which check refuses and re-running that mutation where it is
observable. Second, and worse: the scan-binding test passed against a deliberately broken
gate, because its injection patched
driver.normalize_branchwhile_direct_pushimportsthat name INSIDE the function, so the patch was inert; patching the source module instead
fired too early, so the seam is now the gate's own return. Both were found by the
mutation, not by the green run. A third survivor appeared this round: once the post-gate
equality check existed, it absorbed the unresolvable-replacement refusal, so that mutation
stopped reddening -- fixed by asserting the log message rather than only the refusal. The
reordering it came from also broke three pre-existing tests that pin the atomic clone
RETIREMENT, because refusing at the capture preempted it; the capture now happens early
while the refusal stays in its original position. And one process mistake worth recording:
this round's mutation loop was run BEFORE committing, so
git checkout -- driver.pybetweenmutations restored the file from HEAD and discarded three uncommitted fixes. They were
re-applied and the rule is now explicit -- commit first, then mutate. The credential sample in the retry test was likewise
checked against the real
scan_content_for_secretsfirst -- the uppercaseAWS_SECRET_ACCESS_KEY = '...'spelling is NOT flagged, so the obvious sample would havemade that test pass for the wrong reason.
Run locally after committing (a diff-scoped gate run before the commit gates nothing):
test_dogfood_learnings.py323 passed at-n0,test_ai_spine_driver_coverage.py157,test_spawn_audit.py12,test_pr_recipe.py66. Every baselined gate inBackend Lint & Type Checkpasses:subprocess encoding, black, agent-SDK boundary, sync-IO-in-async,
lockdown-before-publish.
isort --check-onlyandflake8over CI's own paths are clean.mypy src/kiro_crew/reports 4 errors, all pre-existing on main intranscribe.pyandops_mission_control/backend/providers/cloudwatch.py, and 0 in any file this PR touches.Manual verification
N/A -- unit coverage is sufficient. The fault, the accepting case, the ref-shadowing,
mid-scan and post-scan variants and both retry-path refusals are all driven through the
real methods, and exercising the path live would mean authorizing a real direct push.
Related Issues
Refs #8452
For a future publish path
No shared publish seam exists in this app, so the halt is enforced at three hand-placed sites --
the top of
_apply_verdict, the top of_apply_bug_winner, and_direct_pushitself -- and theobject-id pinning lives in
_push_with_rebase. A publish path added later must adopt BOTHproperties independently: refuse while
_rollback_failedis latched, and address the commit by theid it verified rather than by
HEAD.pr_recipe._push_fix_branchandbackend/commit.pyare thetwo existing paths that do neither; they are known-unfixed and deliberately out of scope here.
What is in the diff, and why it is this size
+2217/-156across six files.driver.pyis+634/-82and is the fix plus its reasoning; theremaining
+1251/-26is test (test_dogfood_learnings.py +1213/-18,test_pr_recipe.py +32/-8,test_spawn_audit.py +33) andtest_ai_spine_driver_coverage.pyis+305/-47. The test bulk isnot incidental to a defect fix -- it is what makes forty-nine separate protections falsifiable,
each with its own red set, and several cases (abbreviated-sha shadowing, real
git replace, a realrebase detaching HEAD) cannot be exercised against a stubbed
_gitat all, so they run against realone-commit repositories. Every guard here refuses a publish, and a guard that no test can redden is
indistinguishable from one that was never wired up.
Declared: a mechanical reformat rides along
test/test_ai_spine_driver_coverage.pyLEAVES.github/black-baseline.txt(the diff's onlybaseline change,
-1, and no additions). Leaving the baseline means the whole file must beblack-clean, so a whole-file reformat is included. Its size cannot be stated as a clean subsetof the file's
+305/-47, because black reflowed lines that this PR also edits, so the mechanical andsubstantive hunks overlap: what is measurable is that running
blackon the BASE version aloneyields
+260/-74, i.e. most of that file's churn is reflow rather than new content, and everynon-test hunk in it is black's own output with no hand edits. The prune is
REQUIRED rather than opportunistic -- putting the entry back makes the gate fail with
0 new offender(s), 1 graduated entry to prune. The baseline is never grown by this PR: the diffagainst the base is exactly one deletion.
Pattern harvest
Rule candidate: review-prompt
Pattern: addressing a repository through a SYMBOLIC REF while holding a verified object id.
git push HEAD:refs/heads/<b>andgit diff HEAD~1..HEADeach resolveHEADat their owncall time, so every check against it is a check-then-use pair and each window belongs to
whatever else can write the clone. Adding another check does not help, because the new check
has a window of its own -- this PR made that mistake twice before naming the object id in
every step that must agree, which turns a timing argument into a structural one. Two
corollaries worth carrying: a check written inline in one path is how the sibling path comes
to lack it, so put it in a helper both call; and re-resolving an ABBREVIATED id is the same
bug with an extra edge, since git resolves ref names before abbreviated object ids and a ref
is something an untrusted actor can create.
Other suggestions
repository. The amends the issue reports were observed in worker worktrees under
oss/kirocrew-fix-*, and the process that wrote them is still unidentified; the issueitself rules this file out as that mechanism, because a commit created in the separate
push-disabled clone cannot write a
commit (amend)entry into a worktree's reflog. Theissue therefore stays open for the maintainer decision, and this PR leaves its scope
intact.
kirocrew-worktree-devskill'samend plus force-push sequence, and
issue_radar/backend/pipeline_fold.py-- areuntouched here. Measured while locating the right seam:
dev_fleetcontains no pushinvocation at all, and no shared publish helper exists that every push site routes
through, so a single central gate was not available. First Principles Review reached
the same conclusion independently by counting
HEAD:refs/headssites.test_dogfood_learnings.py. Its hunks sit around line1241 inside
TestTheStoredPushDestinationIsValidatedwhile this change appends at theend of the file and edits
TestPushRetriesOnRacenear the top, so the two do notcollide.
merge-treebetween the two heads does report conflicts, but that reading isconfounded: their merge-base is thousands of commits behind this branch, so those
conflicts are main's own history rather than an adjacency with this change.
Checklist