Skip to content

fix(bin): stage Herdr pane exports with the launch command - #6960

Open
veardk wants to merge 4 commits into
kunchenguid:mainfrom
veardk:fm/herdr-launch-ship
Open

veardk wants to merge 4 commits into
kunchenguid:mainfrom
veardk:fm/herdr-launch-ship

Conversation

@veardk

@veardk veardk commented Oct 9, 2026 •

Copy link
Copy Markdown

Intent

修复 Firstmate 反复启动 Claude 失败,后两条 export 与启动命令串成一行并报 export: not valid in this context,向 kunchenguid/firstmate 提交 PR 并完成全套验证

What Changed

  • bin/fm-spawn.sh: on the herdr backend, the pre-launch pane exports (GOTMPDIR, COMPACT_ADVISER_DISABLE, LAVISH_AXI_HOST, FM_TASK_ID, TRACEPARENT) are no longer typed into the pane as separate lines. Rapid exports could merge with the staged source line in zsh, which produced export: not valid in this context and left the worker unstarted. They are now collected through a new spawn_export_line helper and prepended to the staged launch file, ahead of any env -i launch-env wrapper, so the variables persist in the pane shell. Other backends keep the existing spawn_send_text_line path.
  • tests/fm-backend-herdr-launcher-workspace-e2e.test.sh: the real-Herdr E2E now checks that the staged worker command actually runs. It also checks that with an empty launch-env allowlist a ship worker still sees FM_TASK_ID, GOTMPDIR and COMPACT_ADVISER_DISABLE, that FM_TASK_ID is still set in the pane after the worker exits, and that a traced secondmate gets and records its TRACEPARENT.
  • The remote secondmate trace-context and compact-adviser remote tests now read exports from the staged launch file instead of pane-typed lines, and docs/trace-context.md plus docs/verification/trace-context.md describe the Herdr staged-export delivery.

Risk Assessment

✅ Low: 该修复仅作用于 Herdr 后端:把 GOTMPDIR、COMPACT_ADVISER_DISABLE、LAVISH_AXI_HOST、FM_TASK_ID、TRACEPARENT 的 pane export 改为写在暂存启动文件开头,并在 env -i 包装展开前执行,因此不会再与 source 行在 zsh 粘贴缓冲中串成一行;其他后端行为不变,TRACEPARENT 记录失败时的 unset 顺序和 env -i 转发语义均与原来一致,测试改为校验实际投递的暂存文件与真实执行结果。

Testing

I drove the fix live in disposable fm-lab-* Herdr sessions using bin/fm-herdr-lab.sh. The default session was never touched, and every teardown checked the fleet tripwire. At HEAD, a real claude worker spawned by fm-spawn.sh started: the Claude Code TUI rendered, Herdr reported agent=claude with status working, and no 'not valid in this context' lines appeared. The full real-Herdr launcher e2e also passed, including the executed-command check, the empty-allowlist env probe, the full TRACEPARENT equality check and FM_TASK_ID persisting after exit. With the base fm-spawn.sh swapped back in, both runs reproduced the reported bug exactly: export FM_TASK_ID=clA. '/tmp/.../launch...sh' → export: not valid in this context, and no agent started. The adjusted remote trace-context and remote compact-adviser fixture suites passed, as did the local compact-adviser regression. Those three suites use fake Herdr fixtures, so they are not live results and are recorded as untested here. tests/fm-gotmp.test.sh fails because its fake teardown root has no symlink for fm-supervision-engine-lib.sh. That test and fm-teardown.sh are unchanged from base, so this failure is not caused by this change.

  • Live validation: ✅ go - 4 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Spawning a real Claude worker on Herdr launches Claude instead of failing with 'export: not valid in this context' ✅ pass live claude-live-pane-head.txt: Claude Code v2.1.296 TUI rendered, Herdr pane agent=claude status=working, 0 'not valid in this context' lines
Regression reproduction: the base fm-spawn.sh fails the same live Claude spawn with the reported error ✅ pass live claude-live-pane-base-counterfactual.txt: final export merged with the '. launch.sh' line, 'export: not valid in this context', agent=null
Repeated Herdr spawns (ship, scout-shaped, secondmate, duplicate-label, presentation) all execute their staged command ✅ pass live herdr-launcher-e2e-head.log (all ok, including 'the staged worker command actually executes'); base counterfactual log fails at that step with the merged export line
Under an empty launch-env allowlist (env -i), the worker still sees FM_TASK_ID, GOTMPDIR, COMPACT_ADVISER_DISABLE and the exact full TRACEPARENT, and FM_TASK_ID persists in the pane shell after exit ✅ pass live herdr-launcher-e2e-head.log section 7b ok lines (exact probe-file equality, traceparent recorded in meta)
Remote-routed secondmate keeps trace-context ordering/enablement/relaunch guarantees and the compact-adviser switch with staged delivery ⏸️ untested no The earlier result had no live run behind it. The fake Herdr CLI fixture suites (fm-remote-secondmate-trace-context, fm-spawn-compact-adviser-disable-remote) passed, but no remote-routed secondmate wa…
Non-Herdr backends keep their existing per-line export delivery ⏸️ untested no The earlier result had no live run behind it. Only the fixture-based fm-spawn-compact-adviser-disable suite was run; no live tmux or other non-Herdr fleet spawn was driven. Driving it live needs a dis…
Evidence: Live Claude spawn pane at HEAD (Claude starts)

Source: Live Claude spawn pane at HEAD (Claude starts)


 ▐▛███▛█   Claude Code v2.1.296
▝▜██████▀  Opus 5.5 · API Usage Billing
 ▝▝   ▝▝   ~/.treehouse/proj-7e3411/1/proj


❯ : Firstmate operational input waiting: read
  '/private/tmp/fmclaude/t.8pPsVB/w.Od6OHh/home/state/operational-inbox/1791599270-468a62852d200756.msg' and handle its
  contents as Firstmate operational input.
























✢ Finagling…
                                                                                                    ◐ medium · /effort
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
❯
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

  -- INSERT -- ⏵⏵ bypass permissions on (shift+tab to cycle) · ← 1 agent
Evidence: Live Claude spawn pane with base fm-spawn.sh (export: not valid in this context)

Source: Live Claude spawn pane with base fm-spawn.sh (export: not valid in this context)

treehouse get

proj on  master
❯ treehouse get
A new version of treehouse is available: v2.3.0 → v3.1.2
Run "treehouse update" to update

🌳 Setting up worktree...
🌳 Entered worktree at ~/.treehouse/proj-605b5e/1/proj. Type 'exit' to return.
cd -- '~/.treehouse/proj-605b5e/1/proj'

proj on  HEAD (956bf74)
❯ cd -- '~/.treehouse/proj-605b5e/1/proj'

proj on  HEAD (956bf74)
❯ export GOTMPDIR=/tmp/fm-clA/gotmp
export COMPACT_ADVISER_DISABLE=1
export FM_TASK_ID=clA

proj on  HEAD (956bf74)
❯ export COMPACT_ADVISER_DISABLE=1

proj on  HEAD (956bf74)
❯ export FM_TASK_ID=clA. '/tmp/fm-clA+1d5a99939545c305ecb26baaaaf5193c06733514b36b467f5399dafc44cc4f75/launch.s179159931
6.83607.31950.sh'
export: not valid in this context: /tmp/fm-clA+1d5a99939545c305ecb26baaaaf5193c06733514b36b467f5399dafc44cc4f75/launch.s
1791599316.83607.31950.sh

proj on  HEAD (956bf74)
❯
Evidence: Real Herdr launcher e2e at HEAD

Source: Real Herdr launcher e2e at HEAD

ok - real herdr E2E: with one 'firstmate' workspace and no herdr parent, a crewmate still lands in this home's own workspace without stealing focus
ok - real herdr E2E: the staged worker command actually executes after the pane environment is established
ok - real herdr E2E: the normal unique-label path is unchanged when the launcher's own pane identifies the workspace
ok - real herdr E2E: presentation spaces still create the isolated child workspace and bind it under the launcher's exact parent, without stealing focus
ok - real herdr E2E: with two 'firstmate' workspaces, a worker spawned from inside the second one lands in that exact workspace
ok - real herdr E2E: the duplicate-labeled sibling workspace is left entirely untouched and focus is preserved
ok - real herdr E2E: with a duplicated home label, a projected worker still hangs off the launcher's exact workspace and the sibling stays untouched
ok - real herdr E2E: an ambiguous home label with no launcher identity refuses before any worker endpoint exists
ok - real herdr E2E: a launcher pane that no longer exists refuses before any worker endpoint exists
ok - real herdr E2E: a secondmate launching its own worker gets the same exact-workspace guarantee, and its same-labeled sibling is untouched
ok - real herdr E2E: a --secondmate launch still stands up that secondmate's own workspace instead of inheriting the launcher's
ok - real herdr E2E: with an empty launch-env allowlist the worker sees its pane setup, and the task marker persists in the pane shell after exit
ok - real herdr E2E: a traced secondmate under an empty allowlist receives and records its trace carrier
ok - real herdr E2E: teardown closes only the worker's own pane and leaves the launcher, its workspace, and the same-labeled sibling intact
ok - real herdr E2E: isolated lab session removed and default fleet session unchanged
Evidence: Real Herdr launcher e2e with base fm-spawn.sh (fails)

Source: Real Herdr launcher e2e with base fm-spawn.sh (fails)

ok - real herdr E2E: with one 'firstmate' workspace and no herdr parent, a crewmate still lands in this home's own workspace without stealing focus
not ok - the spawned command never executed; pre-launch exports must not merge with the staged source line
--- got ---
treehouse get

scratch-project on  master
❯ treehouse get
A new version of treehouse is available: v2.3.0 → v3.1.2
Run "treehouse update" to update

🌳 Setting up worktree...
🌳 Entered worktree at ~/.treehouse/scratch-project-071c2f/1/scratch-project. Type 'exit' to return.
cd -- '~/.treehouse/scratch-project-071c2f/1/scratch-project'

scratch-project on  HEAD (abfb0b5)
❯ cd -- '~/.treehouse/scratch-project-071c2f/1/scratch-project'

scratch-project on  HEAD (abfb0b5)
❯ export GOTMPDIR=/tmp/fm-uniqA/gotmp
export COMPACT_ADVISER_DISABLE=1
export FM_TASK_ID=uniqA

scratch-project on  HEAD (abfb0b5)
❯ export COMPACT_ADVISER_DISABLE=1

scratch-project on  HEAD (abfb0b5)
❯ export FM_TASK_ID=uniqA. '/tmp/fm-uniqA+14f891e2a342a5e8e7a75cffcd49b82b62953952deae881aa99ca43e1f7fd9a9/launch.s17915
98855.56186.4329.sh'
export: not valid in this context: /tmp/fm-uniqA+14f891e2a342a5e8e7a75cffcd49b82b62953952deae881aa99ca43e1f7fd9a9/launch
.s1791598855.56186.4329.sh

scratch-project on  HEAD (abfb0b5)
❯
Evidence: Base reproduction excerpt
❯ export FM_TASK_ID=clA. '/tmp/fm-clA+1d5a…/launch.s1791599316.83607.31950.sh'
export: not valid in this context: /tmp/fm-clA+1d5a…/launch.s1791599316.83607.31950.sh
{"agent":null,"agent_status":"unknown"}
Evidence: Remote secondmate trace-context suite

Source: Remote secondmate trace-context suite

ok - disabled: a remote-routed second mate records and receives no carrier and stays enabled-off end to end
ok - enabled: a remote-routed second mate receives one carrier in its pane, identical to the parent's recorded identity, before launch
ok - relaunch: a remote-routed second mate keeps one stable identity across restarts
ok - boundary: each remote-routed second mate roots its own trace and never adopts the spawning environment's carrier
ok - allowlist: the remote receiver accepts exactly the declared inherited-material set, including the enablement flag
ok - delivery: a parent-supplied carrier is accepted only for a secondmate launch and only as a strict W3C value
ALL TESTS PASSED
Evidence: Remote compact-adviser suite

Source: Remote compact-adviser suite

ok - a remote-routed second mate starts with the compact adviser disabled, from the pane export and from the launch command alike
ok - the remote route keeps the compact-adviser switch through the cleared allowlisted environment
ALL TESTS PASSED

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 4 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Spawning a real Claude worker on Herdr launches Claude instead of failing with 'export: not valid in this context' ✅ pass live claude-live-pane-head.txt: Claude Code v2.1.296 TUI rendered, Herdr pane agent=claude status=working, 0 'not valid in this context' lines
Regression reproduction: the base fm-spawn.sh fails the same live Claude spawn with the reported error ✅ pass live claude-live-pane-base-counterfactual.txt: final export merged with the '. launch.sh' line, 'export: not valid in this context', agent=null
Repeated Herdr spawns (ship, scout-shaped, secondmate, duplicate-label, presentation) all execute their staged command ✅ pass live herdr-launcher-e2e-head.log (all ok, including 'the staged worker command actually executes'); base counterfactual log fails at that step with the merged export line
Under an empty launch-env allowlist (env -i), the worker still sees FM_TASK_ID, GOTMPDIR, COMPACT_ADVISER_DISABLE and the exact full TRACEPARENT, and FM_TASK_ID persists in the pane shell after exit ✅ pass live herdr-launcher-e2e-head.log section 7b ok lines (exact probe-file equality, traceparent recorded in meta)
Remote-routed secondmate keeps trace-context ordering/enablement/relaunch guarantees and the compact-adviser switch with staged delivery ⏸️ untested no The earlier result had no live run behind it. The fake Herdr CLI fixture suites (fm-remote-secondmate-trace-context, fm-spawn-compact-adviser-disable-remote) passed, but no remote-routed secondmate wa…
Non-Herdr backends keep their existing per-line export delivery ⏸️ untested no The earlier result had no live run behind it. Only the fixture-based fm-spawn-compact-adviser-disable suite was run; no live tmux or other non-Herdr fleet spawn was driven. Driving it live needs a dis…
  • bash tests/fm-backend-herdr-launcher-workspace-e2e.test.sh (real Herdr 0.9.3, guarded fm-lab-* session) at HEAD
  • Counterfactual: same e2e with git show fb75c1f:bin/fm-spawn.sh swapped in, then restored via git checkout -- bin/fm-spawn.sh
  • Live real-Claude spawn: bin/fm-spawn.sh clA <scratch-project> claude --backend herdr --mode no-mistakes --yolo off in a provisioned fm-lab-fm-claude-live-* session, pane captured via fm-herdr-lab.sh run ... pane read, then fm-herdr-lab.sh teardown
  • Counterfactual: same live Claude spawn against base bin/fm-spawn.sh
  • bash tests/fm-remote-secondmate-trace-context.test.sh
  • bash tests/fm-spawn-compact-adviser-disable-remote.test.sh
  • bash tests/fm-spawn-compact-adviser-disable.test.sh (non-Herdr delivery path regression)
  • bash tests/fm-gotmp.test.sh (fails in an unchanged teardown fixture, unrelated to this diff)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Herdr's `pane run` returns before zsh has consumed the submitted line, so
the rapid pre-launch exports (GOTMPDIR, COMPACT_ADVISER_DISABLE,
FM_TASK_ID, TRACEPARENT, LAVISH_AXI_HOST) could reach the pane's input
together, lose their separators, and merge with the staged `. launch.sh`
source line. zsh then reported `export: not valid in this context` and
the worker never started, while the task record looked published.

On the Herdr backend, prepend those exports to the private staged launch
script instead, ahead of the launch-env-allowlist `env -i` wrapper, so
they still run in the pane shell, are forwarded to the worker, and stay
set after it exits. Other backends keep their existing delivery.

The real-Herdr launcher E2E now asserts that the worker command actually
runs, covers an empty launch-env allowlist and a traced secondmate, and
points its teardown at its own fixture home.
@veardk

veardk commented Oct 9, 2026

Copy link
Copy Markdown
Author

Validation and review clarification: the real-Herdr launch E2E passed all 15 checks twice at this head; replacing only fm-spawn.sh with the pre-fix version reproduced the concatenated-export launch failure. The remote and local trace regression suites also passed. The minimal temporary-root checks in the affected remote test are retained because an earlier validation encountered allocation failure and copied into its own source tree; an injected allocation failure now exits before copying and preserves a disposable source. No general fixture framework or production behavior was added.

GitHub currently reports action_required for both CI and Require no-mistakes, with zero jobs executed. This is the fork workflow approval requirement, not a failing test. Please approve those workflow runs when ready so the required checks can execute. No CI pass or merge is being claimed.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thanks @veardk. A worker that never starts because several pane exports ran together into the source line is a nasty one, and reproducing it by swapping only the pre-fix fm-spawn.sh into a real-Herdr run is exactly the evidence I'd want.

I compared head 14f8decc against main fb75c1f9. The no-mistakes attestation matches this head, and live validation is 5 of 7 against real Herdr (15/15 twice; the two untested rows are fixture-only remote/non-Herdr paths that this change leaves unchanged).

What I checked: spawn_export_line only defers on BACKEND=herdr; every other backend keeps typing the same exports in the same order. On Herdr the exports are prepended to LAUNCH after the env -i wrapper is built, so they run in the pane shell first, persist after the agent exits, and still reach the agent through the wrapper's allowlist. The TRACEPARENT record/unset path is unchanged. It composes with main's #6920 (that one only touched bin/backends/herdr.sh, and this branch already contains it).

CI: I reviewed the diff first (bin/fm-spawn.sh, docs, two tests; no workflow, secret, or network surface), then approved CI 37976239323 and Require no-mistakes 37976239361 / 37976904963.

Contract-class: restore. Spawning a worker on Herdr is an existing, specified default path, and it was broken. Other backends are untouched.

VISION.md, rule by rule: one captain (aligns: no silent dead workers), authority (aligns: no new action), scripts own mechanics (aligns), restart non-event (n/a), delegation with a spine (aligns: real-Herdr regression coverage), vendor (aligns: keeps Herdr a working session manager), scope (aligns: spawn only).

Next step: waiting on CI. If CI and no-mistakes go green and it stays mergeable, this auto-merges. Nothing more is needed from you.

@veardk

veardk commented Oct 10, 2026

Copy link
Copy Markdown
Author

Updated head to 305b8c83 to address the two test fixture issues from CI run 37976239323:

  1. tests/fm-spawn-compact-adviser-disable-remote.test.sh: adapted to staged launch file delivery where exports ride the launch file.
  2. tests/fm-backend-herdr-launcher-workspace-e2e.test.sh: switched the runtime probe capture from terminal screen read (which wrapped/truncated on CI Herdr 0.7.4) to a fixture-owned probe output file with deterministic readiness.

Production code in bin/fm-spawn.sh remains unchanged. All 15 real-Herdr E2E checks and local regressions passed, including live Claude spawn verification in a lab session.
Please approve the updated workflow runs (38017545484 / 38017545511) when ready.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thanks for chasing those two CI fixtures down, @veardk. I re-read the delta 14f8decc → 305b8c83. It's one commit on top with no rebase, and only test files changed:

  • tests/fm-backend-herdr-launcher-workspace-e2e.test.sh: the env probe now writes to a fixture-owned file (renamed into place) instead of grepping a pane screen that wrapped on CI Herdr 0.7.4, and the asserts are now exact matches.
  • tests/fm-spawn-compact-adviser-disable-remote.test.sh: adapted to exports riding the staged launch file, plus a chmod -R u+w before cleanup.

bin/fm-spawn.sh is unchanged, and there's no workflow, secret, or network surface. The attestation matches 305b8c83 (live 4/6, go). I approved this head's fork runs 38017545484 (CI) and 38017545511 (Require no-mistakes).

Contract-class: restore (re-verified). Herdr worker spawn is an existing, specified default path that broke when pane exports ran together. Other backends are untouched.
VISION: unchanged from my last stamp. One captain, authority, scripts, delegation, vendor, and scope all align, and restart is n/a.

Next: this is waiting on CI. If CI and Require no-mistakes go green and it stays mergeable at this head, it auto-merges. Nothing more is needed from you.

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