Repository navigation
dev-lead fix-reviews: posts "Fixed" replies for changes that were never pushed, and edits tests to match its own code #2013
Description
Summary
During one PR (combsmith/.github#1220, a release-gate change to scripts/canary-rollout.sh), dev-lead three times produced output that contradicted the actual state of the branch. One left "Fixed" replies standing for a push that was refused; one pushed a commit that turned CI red; one rewrote an existing test to match its own change. A human/Claude session was driving the same branch at the same time and had to undo or reconcile each one.
Verified 2026-10-02 against the PR history, the dev-lead run logs and a local bats run. The three incidents are confirmed. Two of the original hypotheses were wrong and are corrected under "What the logs show". Incident 1 is the same defect as #1959 ("dev-lead fix-reviews acts on a superseded head SHA: posts "Fixed in …" claims and dispositions for changes it never pushes"), now reproduced on a second repo.
What happened (all on combsmith/.github#1220)
1. "Fixed" replies posted before a push that was then refused (2026-10-01, 19:38Z)
Run: https://github.com/petry-projects/.github/actions/runs/36914059791 (repository_dispatch dev-lead-reviews-retry, intent review-changes, INTENT_HEAD_SHA=571a3b8, the PR's first commit).
| Time (UTC) | Event |
|---|---|
| 19:25:30 | Agent step starts on 571a3b8. |
| 19:28 – 19:31 | The Claude session pushes 522850f, 51e708c, 1be2f96. The run keeps working on 571a3b8. |
| 19:38:33 – 19:38:55 | dev-lead posts three "Fixed" replies with dev-lead:addressed and dev-lead:claim {"sha":"571a3b8…"} markers: discussion_r4159678193, r4159679597, r4159681591. |
| 19:39:15 | The harness detects the other commits, tries to rebase, hits conflicts in scripts/canary-rollout.sh and tests/canary_rollout.bats, and refuses to force-push (the #1607 guard). The run ends as failure. |
The guard worked. The replies were posted before the push was attempted and were never retracted.
What the replies claimed, against the branch:
- Never existed on any head: startup failures routed through
_run_is_staleplus two new tests;mapfileparsing; "a transport failure is not cached". The startup claim is the opposite of the branch, which deliberately counts everystartup_failureand pins that with the test "startup_failure runs are never attributed to an old release — they still BLOCK". - Already on the branch, written by the Claude session: the on-disk SHA cache and in-process parsing (
51e708c, eight minutes earlier). - Landed later, from the Claude session: the sha256 cache key (
ad41f9d, 19:45Z).
2. 15a919e pushed with the existing suite red (2026-10-02, 01:05Z)
CodeRabbit offered two fixes: "Either include the ring in the success and failure log keys, or mark the agent as failed whenever any of its writes failed in the run." dev-lead took the second. It added a test but left the existing test "sync-promotion-failures gives success precedence when agent is in both logs" (combsmith/.github#1023), which the change breaks. Lint and bats went red on that commit; a local run confirms the new test passes and the existing one fails.
The same run contradicted itself:
- 01:05:03: thread reply "Fixed in scripts/canary-rollout.sh …", posted before the push, with a claim marker naming
9318339(the parent, not the pushed commit). - 01:05:14:
@coderabbitai resolve. - 01:05:16: run summary
status=not-applied: "A commit was pushed, but it did not touch any region the review named". The review was anchored at line 1494; the edit was near line 2229.
CodeRabbit then marked the thread "✅ Confirmed as addressed by @don-petry". So a fix that broke CI cleared a review gate.
3. 13927fc rewrote an existing test to match the code (2026-10-02, 01:11Z)
A cubic finding, made against 15a919e, said: "Update the older test to assert failure precedence so the Bats suite can pass." By then the Claude session had replaced the "any failure wins" rule with a per-ring one (353cbfa, 01:10:32) and posted a correction in the thread (01:11:40). dev-lead pushed 13927fc at 01:11:45 anyway. It renamed the test to "failure precedence" and inverted its assertions, which reverses a deliberate behaviour: a failed write followed by a successful write of the same ring is a recovery.
- The rewritten test fails against the branch's logic (confirmed locally; CI's
Lint and batson this commit was cancelled by the next push, not red). - The reply's claim marker names
eb18b52, a commit that does not exist on GitHub. - Reverted in
af2555c, where all three related tests pass.
What the logs show
Confirmed:
- Replies are posted before the harness commits and pushes, and stay up when the push is refused or never happens (incident 1; same ordering in incident 2).
- The claim SHA is never the commit that carries the change. Five of five markers:
571a3b8three times (the run's starting head),9318339(the parent of the pushed commit),eb18b52(not on the remote). - A run stays pinned to its starting head. Incident 1 worked on
571a3b8for 14 minutes while the branch moved three times. - A commit was pushed with a previously-passing test red (incident 2), and a bot's "change the test so it passes" was applied literally (incident 3). The qa-lead advisory below notes the "full suite green, don't edit tests to pass" rule exists only in the prompt.
- dev-lead pushes to a branch another actor is driving, including five seconds after a correction was posted in the thread (incident 3).
Refuted:
No fetch/merge/rebase before pushing, and no abort when the remote head has moved.The harness rebases and refuses to force-push (dev-lead force-pushes from a stale base and silently discards maintainer steering commits (observed on #1604: a human commit was destroyed and its decision reverted) #1607). Nothing to add there.A failed push is reported as a success.The incident 1 run ended asfailureand posted no success summary. The real inconsistency is in incident 2: the summary saidnot-appliedwhile the thread reply said "Fixed" and@coderabbitai resolvewas posted.
Proposed changes (for whoever owns dev-lead)
- Never leave a "Fixed/addressed" reply standing unless the push landed and the pushed diff touches the cited lines. The claim SHA must be a commit made by this pass and present on the remote head. Retract the reply on every other outcome (push refused, no commit, run failed).
- Re-read the PR head before posting anything. If it differs from the starting head, post no per-thread replies and re-queue against the new head (dev-lead fix-reviews acts on a superseded head SHA: posts "Fixed in …" claims and dispositions for changes it never pushes #1959, acceptance criterion 1). The rebase-or-refuse push guard already exists and stays as it is.
- Run the full test suite before pushing. A previously-passing test turning red is a reason to question the change, not to edit the test. Only change an existing test with an explicit, cited justification, and escalate to a human when a bot suggestion contradicts an existing test.
- Back off when another actor is pushing to the branch (recent non-dev-lead commits, a Claude/human session attached), or require an opt-in label for dev-lead to push to a branch it did not create.
- Keep the thread reply,
@coderabbitai resolveand the run summary consistent. Do not post "Fixed" or ask a bot to resolve when the run's own status isnot-applied.
Acceptance criteria
- No false claim stands. A refused, failed or absent push leaves no "Fixed" or
dev-lead:addressedreply standing (not posted, or retracted). A successful push cites the pushed commit SHA, and that commit is on the remote head. - Every claim-posting intent is covered. Criteria 1 and 4 hold for
fix-reviews,fix-bot-commentandreview-changes(andhuman-pr, which shares its code path). Incident 1 was areview-changespass. A reply for a fix the harness did push must not be retracted just because that intent's prompt told the model to cite the pre-pass head. - The suite gate is a harness check, not prompt text. Before pushing, the harness runs the repo's test command and refuses to push when a test that passed on the pre-pass head fails on the pass's result. A failure that already existed on the pre-pass head does not block. If no test command can be determined, the run summary says the suite was not run; it never reports or implies green.
- No silent test rewrite. dev-lead cannot change, delete or skip an existing test without an explicit, cited justification.
- Reproduction. Push to the branch between dev-lead's checkout and its push (a local bare remote is fine), and confirm the push guard still refuses or rebases and that no claim reply survives.
- Tests. bats coverage for each criterion, including a fixture for the
15a919eshape: the pass adds a new test and breaks an existing one without editing it, and the push is refused and escalated.
Remaining work on PR #2019
PR #2019 ("feat: implement issue #2013 …") was reviewed against this body on 2026-10-02 at head 5f96fdb. Criteria 1, 4 and 5 are met for fix-reviews and fix-bot-comment. What is still open:
review-changesis half-wired (criterion 2).prompts/dev-lead/review-changes.mdstill tells the model to citegit rev-parse HEAD(lines 80 and 90) and "Do not commit or push" (line 128).fix-reviews.mdandfix-bot-comment.mdwere updated; this one was not.retract_unlanded_claimsdoes run forreview-changes, so every claim cites the pre-pass head, fails aspredates-passand is retracted, even when the harness committed and pushed the fix. The thread then stays open under a retraction notice.- The test-tamper guard in
commit_and_pushmatches onlyfix-reviews|fix-bot-comment. - Fix: give
review-changes.mdthe same "commit before you claim" and test rules asfix-reviews.md, extend the guard'scasetoreview-changesandhuman-pr, and add bats tests for both.
- Nothing in the harness stops a push with the suite red (criterion 3). The only change is prompt text ("Run the full test suite"). The
15a919eshape passes every new check: it edits no existing test, so the tamper guard is silent, and its claim is a real in-pass commit. - Housekeeping.
- The new ADR is numbered
0010, which now collides with0010-ingress-job-level-concurrency-and-exhaustive-schema.mdonmain. Renumber it. - The ADR and the header of
scripts/lib/claim-landing.shsay CodeRabbit confirmed a thread on the strength of a never-pushed reply. Per "What the logs show" above, the reply it confirmed described15a919e, which was pushed. Correct both. - The PR description still quotes the old issue text ("From the issue: (observed on …)").
- The new ADR is numbered
Update 2026-10-03: still open after re-review
Items 1–3 above are done (verified in the diff, threads resolved). The pr-review agent then escalated PR #2019 to needs-human-review at 2026-10-03T02:39Z. One finding is real and not yet fixed:
-
The owner's access token is reachable from the code the regression guard runs.
_trg_runinscripts/lib/test-regression-guard.shremoves a fixed list of variables, but the test process can still getsecrets.GH_PAT_DON_PETRYtwo ways:.github/workflows/dev-lead-reusable.ymlsetsCOPILOT_GITHUB_TOKEN: ${{ secrets.GH_PAT_DON_PETRY }}in theRun fix-reviews,Run fix-bot-comment,Run review-changes(and other) steps. It is not on the scrub list.- "Checkout caller repo" checks out with the same token and the default saved credentials, so any test can read it from git config.
The Google API keys (
GOOGLE_API_KEY*,GEMINI_API_KEY) are not removed either. Required:- Run the test command with an allowlist of environment variables (
env -iplus what a test suite needs, such asPATH,HOME,LANG,TMPDIR,CI), not a denylist. - Make the saved git credential unreachable from the test process, for example by running the suite in a scratch copy of the tree that has no credential in its git config.
- Add a bats test whose "suite" prints its environment and git config, and assert that no token value or credential header appears.
Do not change other behaviour in the same pass. The escalation also raised a "weakens CI" hard stop; that is for the human reviewer to judge, not something to code around.
Not in scope for this issue; tracked in #2032 ("dev-lead claim retraction: replies from cancelled runs are never swept, and a clean rebase retracts claims for fixes that landed"): replies left by cancelled runs, and true claims retracted after a clean rebase.
References
- PR: fix(canary-rollout): old-release failures no longer block the candidate; evaluate every ring pair independently .github#1220
- Incident 1 run: https://github.com/petry-projects/.github/actions/runs/36914059791
- Bad commits:
15a919e,13927fc(reverted inaf2555c); revert explanation: fix(canary-rollout): old-release failures no longer block the candidate; evaluate every ring pair independently .github#1220 (comment) - Related: dev-lead fix-reviews acts on a superseded head SHA: posts "Fixed in …" claims and dispositions for changes it never pushes #1959 (same defect as incident 1); dev-lead force-pushes from a stale base and silently discards maintainer steering commits (observed on #1604: a human commit was destroyed and its decision reverted) #1607 (the force-push guard, which works); dev-lead claim retraction: replies from cancelled runs are never swept, and a clean rebase retracts claims for fixes that landed (follow-up to #2013) #2032 (follow-up for the retraction sweep's two remaining gaps).
- Hard-gate context: the "Pull Request Reviews" section of
AGENTS.mdin petry-projects/.github requires agents to resolve all review threads; it says nothing about conflicting bot suggestions or tests.
Activity
don-petry commentedon Oct 2, 2026
@petry-projects/solution-architect - Review and advise
github-actions commentedon Oct 2, 2026
Thanks for the detailed write-up and for tracing this through the PR history, it's very helpful. To help us pin down the cause, do you have the workflow run URL (or logs) for the dev-lead
fix-reviewsrun that posted the 19:38Z "Fixed" replies on combsmith/.github#1220, so we can check whether the push was rejected or never attempted before the reply was posted?don-petry commentedon Oct 2, 2026
Solution Architect — structural advisory
Governing ADR: ADR-0004 Pure-logic classifiers with bats tests ("Fail Loud, Never Fake") and ADR-0005 Advisory by default; write access is an opt-in gate. Neither one covers how a write-mode persona should share a branch with other actors pushing at the same time. That decision is not on record.
Alignment: drifts. ADR-0005's gate works as designed: dev-lead's write surfaces are armed by the
dev-leadlabel. The problem is that after a push, the "Fixed" reply is still a claim the model makes about itself, not a result a pure classifier has checked. That breaks ADR-0004.What I'd shore up (highest leverage first):
lib/addressed-claim-verify.sh) only runs at thread-resolution time. It checks that the claim SHA is on the head branch and that its diff touches the claimed files. A stale SHA (the PR's first commit) passes both checks trivially. The rule should be: the claim SHA must be a commit made by this run, after the run's starting head.Escalate? yes. A false "Fixed" reply can already satisfy CodeRabbit's review gate, so a claim nobody verified can clear a merge gate.
Advisory only — I comment, I do not change code. Ground: BMAD Method architect (bmad-agent-architect v6.8.0). Opt out on this item with the solution-architect:hands-off label.
don-petry commentedon Oct 2, 2026
@petry-projects/qa-lead - Review and provide direction from QA perspective
don-petry commentedon Oct 2, 2026
QA Lead — test-risk advisory
Risk tier: HIGH — a false "Fixed" can clear the review-thread merge gate (thread resolution), and a rewritten test lets a real defect merge. The push-overwrite guard is already tested (
test_push_no_clobber.bats,test_incorporate_remote_head.bats), but the claim and reply side is not.What I'd shore up (highest leverage first):
dev-lead-fix-reviews.sh~L1043, pr-review can't approve ANY open PR: maintainer-comment gate needs pushedDate (always null) and treats codeant/qodo/auto-rebase comments as maintainer findings — 18/18 PRs stranded #1813). The review-thread claim gate (~L776) only checks that the commit is on the head branch and thatsha^..HEADtouches the claimed file, so the PR's own first commit passes. That is the571a3b8case. Fixture: PR commit A touches F, this pass pushes B, claim{sha:A, files:[F]}→ assert the thread stays unresolved.ghstub that records mutations). The model posts the addressed reply before the harness commits and pushes. Makepush_no_clobberfail, then make the no-op guard return 3. Assert that no unretracted addressed or claim reply survives the pass, and that a successful pass cites the pushed SHA.@test/assertion without a cited justification, mark it not-applied and escalate. Adding a new test passes; inverting an old one is flagged.09using a local bare remote, injecting a commit between checkout and push (no live GitHub, no sleeps). Assert it merges or aborts and posts zero claim replies.Escalate? yes — the thread-path claim gate accepts a commit that predates this pass, so false "Fixed" claims can resolve required threads.
Advisory only — I comment, I do not change code. Ground: BMAD Test Architecture (bmad-tea v1.19.0). Opt out on this item with the qa-lead:hands-off label.
don-petry commentedon Oct 2, 2026
Dev-Lead Implementation Plan
Issue: #2013 — dev-lead
fix-reviews: posts "Fixed" replies for changes that were never pushed, and edits tests to match its own codeScope
Make a dev-lead "Fixed" claim something the harness checks instead of something the model asserts about itself (ADR-0004). A claim must cite a commit this pass produced that is on the remote PR head. Any claim reply posted this pass that fails that check is retracted on every outcome: push rejected, push unverified, no-op guard, engine failure, no commit, or a stale SHA. The push itself is verified against the remote head. A pass that rewrites or deletes an existing test, or adds a skip to one, without a cited justification is refused and escalated.
Implementation Checklist
scripts/lib/addressed-claim-verify.sh: add the pureacv_claim_in_pass <base> <on_ref> <in_base>verdict (landed/unknown-base/not-on-ref/predates-pass).acv_gather_commit_factstakes an optional base/ref and reportsin_base.scripts/dev-lead-fix-reviews.shresolve_addressed_bot_threads: requireacv_claim_in_passagainstRESOLUTION_BASE_SHA. This gives the thread path the same check the pr-review can't approve ANY open PR: maintainer-comment gate needs pushedDate (always null) and treats codeant/qodo/auto-rebase comments as maintainer findings — 18/18 PRs stranded #1813 issue-comment path already has, and closes the571a3b8stale-SHA hole.scripts/lib/claim-landing.sh, containing:cl_push_landed_verdict;cl_select_pass_claims, which picks our claim replies created since the pass started;cl_retract_body, which removes the addressed and claim markers, adds a retraction notice and a<!-- dev-lead:retracted -->marker;cl_remote_head.dev-lead-fix-reviews.sh:PASS_START_ISOat checkout;commit_and_pushchecks after the push that the remote head contains the pushed HEAD, and treats a mismatch as a failed push;retract_unlanded_claimsruns after everycommit_and_pushoutcome (0/1/3), before the push-failureexit 1, and on engine failure, for fix-reviews / fix-bot-comment / review-changes.scripts/lib/test-tamper-guard.sh. A pure classifier over the pass's per-file numstat and added skip lines, plus aTest-Change-Justification:commit trailer, returnsclean/justified/tampered. It is wired intocommit_and_pushforfix-reviewsandfix-bot-comment. Atamperedpass is not pushed: it gets the needs-human-review label and a comment, auto-merge is disabled, and it returns 3 so the existing "flagged, not pushed" handling applies.prompts/dev-lead/fix-reviews.mdandfix-bot-comment.md:docs/architecture/adr/0010-…: how write-mode personas behave on a branch other actors push to (incorporate the remote head or stand down, never force; verify claims against the landed head; retract unlanded claims)..github/workflows/lint.yml: register the new bats files.Tests to Write
test_addressed_claim_verify.bats:acv_claim_in_passtruth table. A PR commit A cited after this pass pushed B givespredates-pass.test_claim_landing.bats:test_test_tamper_guard.bats: adding a new test isclean; inverting or deleting an existing test istampered; the same edit with a justification trailer isjustified; adding askipistampered; non-test files are ignored.test_fix_reviews.bats:Test Command
bats tests/dev-lead/unit/(plusbash .dev-lead/scripts/dev-lead-lint.sh,shellcheck scripts/*.sh scripts/lib/*.sh)don-petry commentedon Oct 2, 2026
Dev-Lead: Implementation Complete — PR #2019
The implementation for issue #2013 is durable: commits are pushed and a pull request is open. This record is backed by verifiable artifacts you can confirm in one click.
fix-reviews: posts "Fixed" replies for changes that were never pushed, and edits tests to match its own code #2019afc923405e5e0d70bb0a35f83332a49e709834bcmain: 12 file(s), +1444/-27 linesReview happens on the PR. Acceptance criteria and test results are evidenced by the PR's diff and CI checks — not asserted here. If a later stage fails, this claim is retracted in place.
feat: implement issue #2013 — dev-lead `fix-reviews`: posts "Fixed" r…
feat: implement issue #2013 — dev-lead `fix-reviews`: posts "Fixed" r…
feat: implement issue #2013 — dev-lead `fix-reviews`: posts "Fixed" r…
fix(reviews): REST labels for auto-rebase:ready and issue hold; prese…
feat: implement issue #2142 — dev-lead escalations do not apply needs…