Skip to content

fix(ship): stop agent-mode Test/Code fabrication and undetected stale turns - #42

Merged
teragrid merged 4 commits into
mainfrom
fix/agent-mode-checkpoint-fabrication
Aug 26, 2026
Merged

fix(ship): stop agent-mode Test/Code fabrication and undetected stale turns#42
teragrid merged 4 commits into
mainfrom
fix/agent-mode-checkpoint-fabrication

Conversation

@teragrid

Copy link
Copy Markdown
Owner

Summary

Reproduced and fixed four of the seven reliability gaps reported against
forge ship --agent-mode in 1.10.2 — each confirmed live against a real
built binary in a disposable scratch repo, not just from reading the code.

  • Test/Code fabrication (bugs chore(deps): bump actions/upload-artifact from 4 to 7 #1, chore(deps): bump github.com/spf13/cobra from 1.8.1 to 1.10.2 #3): writeTestArtifactsWithContext
    wrote the static RED placeholder to tests/*.test.ts whenever the LLM/
    bridge call errored for any reason, including ErrAgentTurn (a pause,
    not a failure). Once written, the placeholder permanently satisfied
    allTestArtifactsExist, so the checkpoint never called the generator
    again — a host agent's real, later-submitted answer silently never
    reached the file, while the checkpoint still reported "N test file(s)
    found; all 4 named artifacts present". Fixed by propagating the pause
    before writing anything for the branch that paused, so the exists-guard
    stays false and the real answer lands correctly on the next run.
  • Stale-turn blind spot (bug chore(deps): bump actions/setup-node from 4 to 6 #4): agentbridge.Bridge only latched
    paused inside Lookup, never when a pending turn was merely reloaded
    from disk. A checkpoint that skipped the bridge entirely this run (an
    "artefact already exists" short-circuit, a language branch with no LLM
    call) left paused false for the whole process, so both the
    per-checkpoint gate and the final "render the pending turn" check
    (which also key off Paused()) silently failed to fire while an earlier
    turn sat unanswered. Fixed by latching paused immediately on load;
    replay is unaffected since hash/ordinal hits are checked before the
    paused check in Lookup.
  • No disambiguation (bug chore(deps): bump actions/setup-go from 5 to 6 #6): checkSpec treated "no description/
    --name + 2+ existing spec dirs" as a soft "ok, pass a description" and
    let the whole pipeline run against an undefined slug. Now hard-fails,
    names the ambiguous candidates, and demands --name. A single
    unambiguous spec dir is unaffected.
  • Incomplete .gitignore (bug chore(deps): bump actions/download-artifact from 4 to 8 #5, directly causing bug chore(deps): bump actions/checkout from 4 to 6 #2's symptom):
    all three copies of the managed block (codemod.canonicalGitignoreBlock,
    codemod.defaultMarkerBody, cmddoctor.canonicalGiSnippet) were missing
    .forge/agent/, .forge/.snapshots/, .forge/learned/, .forge/trash/,
    .forge/token-ledger.jsonl. Confirmed live: this let the bridge's own
    bookkeeping show up in git status, which the Code checkpoint's
    countChangedFiles then counted as "N modified file(s)" for a run where
    the only real source file was untouched.

Bug #7 (forge ship --agent-mode -j | head hanging) could not be
reproduced despite repeated attempts (small/large payloads, byte- and
line-truncated pipes) on macOS and is intentionally left out of this PR
rather than guess-fixed.

Test plan

  • go build ./...
  • go vet ./...
  • go test ./... (full suite, unmodified tests all still pass)
  • 6 new regression tests added, each independently verified to fail
    against the pre-fix code and pass against the fix:
    - TestOpen_RestoredPendingTurnLatchesPausedImmediately (agentbridge)
    - TestWriteTestArtifacts_PausedTurnDoesNotFabricatePlaceholder (cmdship)
    - TestCheckSpec_NoDescription_MultipleSpecs_HardFails +
    TestCheckSpec_NoDescription_SingleSpec_StillOK (cmdship)
    - TestCanonicalGitignoreBlock_CoversForgeScratchState (codemod) +
    matching tests in cmddoctor and a second codemod literal
  • Live end-to-end repro: fresh scratch git repo, real binary, drove
    forge ship --agent-mode through spec → arch → debate → test,
    submitting real answers via forge agent submit, confirming the
    real content (not a placeholder) landed in tests/*.test.ts and that
    .forge/agent/ no longer appears in git status after forge init

🤖 Generated with Claude Code

vietking and others added 4 commits August 26, 2026 16:54
… turns

Four confirmed root causes behind the 1.10.2 agent-mode reliability gaps
(reproduced live against a real binary in a disposable worktree):

1. writeTestArtifactsWithContext wrote the static RED placeholder stub to
   tests/*.test.ts whenever an LLM/bridge call returned ANY error, including
   ErrAgentTurn (a pause, not a failure). Once written, that placeholder
   permanently satisfied allTestArtifactsExist, so checkTest never called
   writeTestArtifacts again — a host agent's real, later-submitted answer for
   the same operation sat unused in the bridge's response store forever, and
   the checkpoint reported "N test file(s) found; all 4 named artifacts
   present" against content nobody had reviewed. Fixed by returning the
   agent-turn error before writing anything for the branch that paused, so
   the exists-guard stays false and the real answer lands on the next run.

2. agentbridge.Bridge.loadPending restored a still-pending turn from disk
   without latching `paused`, deferring that to a Lookup call that would only
   happen if some checkpoint this run coincidentally re-asked the same
   operation. A checkpoint that never calls Lookup at all (an "artefact
   already exists" skip, a language branch with no LLM call) left `paused`
   false for the whole process, letting `forge ship --agent-mode` run
   straight through later checkpoints — and skip the final "render the
   pending turn" check, which also gates on Paused() — while an earlier
   turn (e.g. an arch-parallel-debate role) sat unanswered on disk the whole
   time. Fixed by latching paused immediately on load; replay is unaffected
   since Lookup checks byHash/byOrdinal before it ever checks paused.

3. checkSpec treated "no description/--name and 2+ existing spec dirs" as a
   soft "ok, pass a description" and let the pipeline continue — every later
   checkpoint then ran against an empty/undefined slug. Now hard-fails and
   names the candidates, demanding --name. A single unambiguous spec dir is
   unaffected.

4. The managed .gitignore block (three copies: codemod.canonicalGitignoreBlock,
   codemod.defaultMarkerBody, cmddoctor.canonicalGiSnippet) never listed
   .forge/agent/, .forge/.snapshots/, .forge/learned/, .forge/trash/, or
   .forge/token-ledger.jsonl. Confirmed live: an agent-mode run left `git
   status --porcelain` reporting the bridge's own bookkeeping as untracked,
   which checkCode's countChangedFiles then counted as evidence of real code
   changes ("N modified file(s)" for a run where the only real source file
   was untouched).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
M1-27 compared each file's all-time-latest-touch commit timestamp across
the entire repo history, not just this PR's commits. That means ANY
test-only PR touching a mature file — no production change at all — was
flagged as a TDD violation purely because the test file's last historical
edit (this PR's own commit) postdated the paired production file's last
historical edit (from whenever it was last touched, possibly months ago).
That's true for nearly every established file, and hit this branch's own
PR (agent_mode_test.go touched, agent_mode.go untouched).

Scope both `git log` calls to "origin/$BASE..HEAD" and take the oldest
commit within that range (`tail -1`) instead of the newest across all
history (`head -1`). An untouched production file now correctly produces
no commits in range (empty PROD_DATE) and is skipped, while a genuine
same-PR "production code first, test added later" violation is still
caught — verified both cases locally against this fix before pushing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
….mod

ci-gates.yml was still pinned to GO_VERSION '1.25' while ci.yml and
nightly.yml were already on '1.26', and go.mod's toolchain directive has
required go1.26.x since 2026-07-10 (bumped for stdlib CVE fixes, most
recently go1.26.6 on 2026-08-20). The drift meant ci-gates' M2-17 perf
benchmark gate installed benchstat against Go 1.25 and failed outright
once golang.org/x/perf's @latest started requiring go >= 1.26.0 — a
pre-existing, repo-wide break confirmed on an unrelated dependabot PR
from 4 days before this fix, not something introduced by this branch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Version Scope Decision:
- Chosen bump: PATCH
- Why not smaller: agent-mode fabricating checkpoint success while
  discarding real host-agent answers, and a stale-turn blind spot that let
  it happen even when a prior turn was still unanswered, are bug fixes to
  behaviour that was already wrong — both need to reach users.
- Breaking impact: none. No CLI flag, output key, or exported symbol
  changed. writeTestArtifacts(WithContext)/llmGoStub gained a return value
  but are unexported (package cmdship internals); agentbridge.Bridge's
  paused-latch timing changed but Paused()'s signature and contract
  ("a turn has been requested and not yet answered") are unchanged.

No BREAKING.md entry: nothing in this release breaks a contract.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@teragrid
teragrid merged commit f726456 into main Aug 26, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants