[Bugfix #1267] Builder clean-exit relaunch reruns fresh instead of resuming the ended conversation - #1317
Conversation
… --resume A resumed builder baked `<cmd> --resume <id>` in as the single command its `while true` loop reran, so the Enter-gated relaunch after a deliberate quit started the very conversation the user had just ended — the opposite of the rule #1264 established for architects (clean exit → rerun, no recovery). The loop now carries two invocations. `initial` is the entry point and what a crash restarts, so recovery still resumes — that is what an unnatural death wants. `fresh` is what the keypress relaunches: the ordinary role-injected, prompt-carrying command a non-resume spawn would have used, so "fresh" needs no definition of its own. The switch is sticky, so a crash *after* a clean exit restarts fresh rather than reviving the abandoned session. They are bash functions, not a command string in a variable: the fresh invocation carries its own quoting (`--append-system-prompt "$(cat '…')"`), which would be word-split if re-expanded. All five generated-script sites go through one `buildLaunchLoop`; when the two commands are equal — every non-resume variant — it returns the historical single-command loop byte for byte. Role injection and the harness worktree files are consequently prepared on the resume path too: the relaunch is a genuine fresh launch and needs them. `.builder-prompt.txt` is the deliberate exception — `afx reset` reads the spawn-time `## Mode:` heading out of it precisely because `--resume` never rewrites it, and `resolveMode` cannot recover a spawn-time `--soft`. The fresh command reads that file; it does not regenerate it. Issue #1267's second question — whether builders should keep the Enter gate or auto-rerun like architects — is left open by design. Only *what* the relaunch runs changes here, not *when* it runs.
…launches The bug was bash-level: the generated string was "correct", the loop semantics were not. So the new suite runs the real loop under bash with a fake agent that logs its argv and exits on a scripted sequence of codes, then asserts the exact invocation order — clean exit relaunches fresh, a crash still resumes, the switch is sticky, EOF exits without relaunching, and the fresh command's quoting survives (one argument, not word-split). Three of the seven fail against the pre-fix single-command loop. Also guards the cross-module edge: `afx reset` refuses to type into a builder whose harness it cannot name, and names it by scanning `.builder-start.sh` for a command-position binary — the new `codev_launch=…` / `"$codev_launch"` lines must neither shadow the harness line nor be mistaken for one. The #929 resume assertions move from the whole script to the *entry* launcher, which is what they were always about; the script now legitimately contains a second, role-injected command.
…n means something CMAP (codex): the fake agent logged "$*", which renders one argument "two words" identically to two arguments "two" "words" — so the assertion that the fresh command's "$(cat …)" survives as a single argument was not actually testing anything. Log "$@" one-per-`|` instead: `two words|` vs `two|words|`.
From claude's review, none blocking: - The fake agent ran off the end of its scripted exit codes into `exit ""`, which bash rejects with 255 — a *crash*, so the loop would auto-restart and spin until the runner's 30s timeout. A future regression that adds an unexpected relaunch should fail as a wrong invocation list, not a hang; default the code to 0. - Document why `.builder-prompt.txt` is guaranteed present on the resume path: it is an invariant of `startBuilderSession` (every non-resume branch writes it, and worktree-mode spawns are generated elsewhere and never reach it), not an assumption about the worktree. - `reset/reorient.ts` said `.builder-role.md` is "the copy injected at spawn"; resume now refreshes it too.
CMAP review (3-way)
No blocking issues from any reviewer. Four findings, all addressed: codex — the quoting test wasn't testing anything. The fake agent logged claude — the fake agent could hang instead of failing. Running past the claude — claude — stale comment. claude also flagged one unreproducible Full suite after the fixes: 4051 passed, 0 failed, 48 skipped (202 files). |
Summary
A builder's
while truelaunch loop baked one command in. On the resume paththat command is
<cmd> --resume <id>, so the Enter-gated relaunch after aclean exit restarted the very conversation the user had just deliberately
ended — the opposite of the rule #1264 (fixed for architects, PR #1266) states:
clean exit → rerun fresh, no recovery.
The loop now carries two invocations and switches between them.
Root Cause
spawn-worktree.tsgenerated, for a resumed builder:LAUNCH_LOOP_TAILwas a shared constant with nowhere to put a second command,so the clean-exit branch had only
continueavailable to it. Reproduced beforetouching anything, with a fake agent standing in for the harness:
Fix
buildLaunchLoop(initial, fresh)is now the single generator behind all fivescript variants.
initial— entry, and what a crash restarts. On the resume path thatstays
--resume <id>: recovery is exactly what an unnatural death wants.fresh— what the keypress relaunches. It is the ordinary role-injected,prompt-carrying command a non-resume spawn would have used, so "fresh" needs
no definition of its own.
reviving the session the user walked away from.
historical single-command loop byte for byte.
Generated resume script:
Bash functions rather than a command string in a variable: the fresh invocation
carries its own quoting (
--append-system-prompt "$(cat '…')"), which would beword-split if re-expanded. There is a test for exactly that.
Two consequences worth flagging:
path too. The relaunch is a genuine fresh launch and needs them.
.builder-prompt.txtis still never rewritten on resume.afx resetreadsthe spawn-time
## Mode: STRICT|SOFTheading out of it precisely because--resumedoes not regenerate it, andresolveModecannot recover a spawn-time--softfrom protocol defaults — regenerating it would silently flip a softbuilder to strict. The fresh command reads that file.
The relaunch prompt says "relaunch fresh" now, in all variants — accurate for
each of them, and on the resume path it corrects a genuinely misleading message.
Deliberately not done
Issue #1267's second question — whether builders should keep the Enter gate
(#1244) or auto-rerun like architects — is left open, as the issue frames it.
Only what the relaunch runs changes here, not when it runs. Builders are
porch-driven and a fresh conversation's interaction with porch task state still
wants thinking through before auto-rerun is safe; that is a separate decision,
not a side effect of this fix.
Test Plan
New
bugfix-1267-launch-loop.test.tsexecutes the generated bash against afake agent that logs its argv and exits on a scripted sequence of codes, then
asserts the exact invocation order. String assertions could not have caught this
bug — the old string was "correct"; the loop semantics were wrong.
[--resume abc, --fresh-args][--resume abc, --resume abc]— recovery preserved[--resume abc, --fresh-args, --fresh-args]— sticky[--resume abc]— exits, relaunches nothing"$(cat …)"arrives as one argumentinitial === freshcodev_launchafx resetdetectionclaudethrough the dual-launcher shapeVerified these fail against the pre-fix loop: 3 of 7 fail when
buildLaunchLoopis forced back to single-command, pass after.
pnpm build✓unchanged apart from the message.
Fixes #1267