Skip to content

feat(bin): support design tasks in fm-control relaunch - #273

Open
HelloWorldSungin wants to merge 6 commits into
mainfrom
fm/fm-design-relaunch-kind-gap
Open

feat(bin): support design tasks in fm-control relaunch#273
HelloWorldSungin wants to merge 6 commits into
mainfrom
fm/fm-design-relaunch-kind-gap

Conversation

@HelloWorldSungin

@HelloWorldSungin HelloWorldSungin commented Sep 11, 2026

Copy link
Copy Markdown
Owner

Intent

Add design-task support to the existing restart command, as the captain explicitly approved, so the preserved PMCC ADR worker can restart with the smaller Codex context. Preserve its work, decisions and ADR-only scope.

What Changed

  • bin/fm-control.sh relaunch now accepts kind=design tasks the same way it handles ship and scout tasks: it requires --note, appends that note to the brief the replacement worker reads, and keeps the harness recorded for the task instead of reading it again from config.
  • A design relaunch reuses the plugin release pinned at dispatch (design_skills_plugin/version/updated plus the skill paths in the dispatch brief under the task tmp dir) and never calls fm-design-skills.sh resolve again. If the pin is missing, doesn't match, or can't be read, the relaunch refuses before the old agent is stopped. The shared parsing and validation helpers moved out of bin/fm-spawn.sh into a new bin/fm-design-skills-lib.sh, and fm-spawn.sh --relaunch uses the same adopt path. preserve_relaunch_meta now also carries the branch meta key, so it is no longer written twice.
  • Updated docs/agent-control.md, docs/fleet-data-contracts.md, docs/verification/runtime-backends.md and the design-profile skill to describe design relaunch. tests/fm-control-relaunch.test.sh adds tests for:
    • identity preservation on the supported runtimes
    • the --note requirement
    • ignoring the crew harness config
    • refusing an unknown kind before stop
    • refusing a missing skill pin before stop
    • refusing --design on fm-spawn.sh --relaunch

🤖 Generated with Claude Code

Risk Assessment

✅ Low: The fix round moves the full design-pin validation into a shared helper that fm-control now calls before it checkpoints or stops the worker. The helper checks the binding schema, that the binding matches the recorded release, that the paths are safe, and that the pinned skill files are readable, and fm-spawn reuses the same helper. The dispatch binding is now built in one place, and the new test runs both entry points against nine binding and skill-file defects and confirms that nothing is stopped or changed.

Testing

I ran the targeted relaunch test file (all passed) and re-drove the manual CLI scenarios against the current code with a stub multiplexer: the codex ADR restart on a smaller model, the refusal when a pinned skill is removed, and repeated relaunches. All the live-driven scenarios passed and the worktree stayed clean. The other refusal cases (missing note, invalid pin, --design flag, unknown kind) are covered only by the hermetic test file and were not driven live. There is no UI in this change, so the evidence is CLI transcripts and the persisted task records.

  • Live validation: ✅ go - 4 of 5 scenarios driven live against the product
Scenario Result Live Evidence
Captain relaunches the live codex ADR design worker with --model gpt-5.4-mini and a note; the replacement launches as codex with the smaller model on the same endpoint and worktree, and keeps kind=des… ✅ pass live manual-transcript.round2.txt Scenario A: rc=0, 'relaunched adr1 harness=codex from=codex model=gpt-5.4-mini', meta after shows kind=design, design_skills_* unchanged, and model=gpt-5.4-mini; the launc…
The ADR worker's work and decisions survive the relaunch: the pending inbox steer and uncommitted wip.md remain, the note appears in the replacement's brief, and the dispatch brief keeps the same pinn… ✅ pass live manual-transcript.round2.txt Scenario A: inbox 001.msg present, ?? wip.md present, the note appears in the brief, and the dispatch binding keeps version 1.2.0
Adversarial: the plugin auto-update removes the pinned grilling skill while the worker is live; relaunch refuses without stopping the worker or changing records ✅ pass live manual-transcript.round2.txt Scenario B: rc=1, the pane still runs codex, nothing was sent to the pane, meta is byte-identical, and there are 0 relaunch journal files
Adversarial: a design relaunch refuses before stop when the note is missing, the pin is missing or invalid, or --design is passed on a direct fm-spawn --relaunch; an unknown kind is still refused ⏸️ untested no The earlier payload did not establish a live result for these cases. They were covered only by the hermetic test file tests/fm-control-relaunch.test.sh, not driven against the live product. To confirm…
Repeated design relaunches keep exactly one branch= line in the task record ✅ pass live branch-duplication.round2.txt: branch_lines=1 after each of 3 relaunches
Evidence: Manual CLI transcript: codex ADR design relaunch and pinned-skill-removed refusal
=== Scenario A: codex ADR design worker restarts with a smaller model ===
meta before:
window=fmses:fm-adr1
worktree=/tmp/fm-control-relaunch.TaDfdO/manual-a-11208/wt
harness=codex
kind=design
model=default
branch=fm/adr1
design_skills_plugin=mattpocock-skills@mattpocock
design_skills_version=1.2.0
design_skills_updated=2026-08-01T00:00:00Z

$ fm-control.sh adr1 relaunch --model gpt-5.4-mini --note 'ADR decisions 1-3 settled; continue ADR only'
warning: /tmp/fm-control-relaunch.TaDfdO/manual-a-11208/home/data/adr1/launch-brief.md records no delivery contract line (scaffolded before tracked-output briefs recorded one); launching on the explicit --mode no-mistakes - confirm its definition of done matches
relaunched adr1 harness=codex from=codex model=gpt-5.4-mini effort=default backend=tmux endpoint=fmses:fm-adr1 worktree=/tmp/fm-control-relaunch.TaDfdO/manual-a-11208/wt
rc=0
meta after:
window=fmses:fm-adr1
worktree=/tmp/fm-control-relaunch.TaDfdO/manual-a-11208/wt
branch=fm/adr1
harness=codex
kind=design
design_skills_plugin=mattpocock-skills@mattpocock
design_skills_version=1.2.0
design_skills_updated=2026-08-01T00:00:00Z
model=gpt-5.4-mini
pending inbox:
001.msg
handled
uncommitted work:
?? wip.md
uncommitted design note
note in replacement brief:
First, check your instruction inbox: list /tmp/fm-control-relaunch.TaDfdO/manual-a-11208/home/state/adr1.inbox/*.msg, act on
each message in numeric order, then mv each handled file into
/tmp/fm-control-relaunch.TaDfdO/manual-a-11208/home/state/adr1.inbox/handled/. A steer sent before the relaunch survives there.

ADR decisions 1-3 settled; continue ADR only
pinned binding in dispatch brief:
{"schema":"fm-design-skills.dispatch.v1","plugin":"mattpocock-skills@mattpocock","version":"1.2.0","last_updated":"2026-08-01T00:00:00Z","skills":{"grilling":"/tmp/fm-control-relaunch.TaDfdO/manual-a-11208/plugin/skills/productivity/grilling/SKILL.md","domain_modeling":"/tmp/fm-control-relaunch.TaDfdO/manual-a-11208/plugin/skills/engineering/domain-modeling/SKILL.md"}}
keys sent to pane (launch line):
/tmp/fm-control-relaunch.TaDfdO/manual-a-11208/fake/literal:unset TRACEPARENT; env -u CURSOR_AGENT -u CURSOR_INVOKED_AS -u GEMINI_CLI codex --model 'gpt-5.4-mini' --dangerously-bypass-approvals-and-sandbox -c "notify=[\"bash\",\"-c\",\"touch '/tmp/fm-control-relaunch.TaDfdO/manual-a-11208/home/state/adr1.turn-ended'\"]" "$('~/.no-mistakes/worktrees/bc8432f7c9f8/01M294ZFX980QCW585XCV44Z1R/bin/fm-operational-input.sh' encode launch-brief < '/tmp/fm-adr1/brief.md')"

=== Scenario B: plugin auto-update removed the pinned skill while worker is live ===

$ fm-control.sh adr2 relaunch --model gpt-5.4-mini --note 'keep ADR scope'
error: a dispatch-pinned mattpocock design skill path disappeared or became unreadable after resolution; refusing instead of silently resolving a different plugin release
rc=1
pane command still: codex
literal sent: []
meta byte-identical
relaunch journal files: 0
Evidence: Branch line count across three design relaunches

relaunch 1 rc=0 branch_lines=1 relaunch 2 rc=0 branch_lines=1 relaunch 3 rc=0 branch_lines=1

relaunch 1 rc=0 branch_lines=1
relaunch 2 rc=0 branch_lines=1
relaunch 3 rc=0 branch_lines=1
Evidence: fm-control-relaunch test log
ok - fm-control relaunch: design workers on claude, codex, and pi keep identity, pin, inbox, and uncommitted work
ok - fm-control relaunch: a design task requires a progress note and refuses before stop
ok - fm-control relaunch: a design task keeps its recorded harness instead of re-reading crew config
ok - fm-control relaunch: an undefined kind is still refused before the agent is touched
ok - fm-control relaunch: a design task with no dispatch pin refuses before stop
ok - fm-control relaunch: invalid design bindings and unavailable skills refuse before stop
ok - fm-control relaunch: a same-harness relaunch replaces the agent in the same endpoint and worktree
ok - fm-control relaunch: a linked spawning home preserves committed and unfinished work in the recorded copy
ok - fm-control relaunch: durable task metadata survives replacement launch publication
ok - fm-control relaunch: delivery and concurrent task metadata publication serialize
ok - fm-control relaunch: disabling tracing clears metadata and pane context
ok - fm-control relaunch: the progress note lands in the instructions the replacement reads
ok - fm-control relaunch: a ship task refuses without the progress note its replacement needs
ok - fm-control relaunch: switching harness is one ordinary relaunch, and the old wiring goes with the old agent
ok - fm-control relaunch: a harness switch resets model and effort unless they are named too
ok - fm-control relaunch: a prefixed recorded harness can switch adapters transactionally
ok - fm-control relaunch: a prefixed command requires an explicit replacement harness
ok - fm-control relaunch: a same-harness relaunch keeps the profile axes it was running with
ok - fm-control relaunch: explicit model and effort win over the recorded ones
ok - fm-control relaunch: refuses to relaunch onto an adapter with no verified mechanics
ok - fm-control relaunch: the retired incarnation's global turn-end token is revoked
ok - fm-control relaunch: wiring cleanup failure refuses replacement arming
ok - fm-control-lib: one owner resolves each harness's turn-end registry entry, and refuses a malformed token
ok - fm-control relaunch: a secondmate relaunch re-resolves its durable configured harness pin
ok - fm-control relaunch: invalid configured effort is ignored before stop
ok - fm-control relaunch: an adapter unverified for this task kind refuses before the agent is stopped
ok - fm-control relaunch: explicit secondmate harness resets unnamed profile axes
ok - fm-control relaunch: a ship task keeps its recorded harness instead of re-reading crew config
ok - fm-spawn --relaunch: with no explicit harness it reuses the task's recorded one, never the crew default
ok - fm-spawn --relaunch: wiring armed under a prefixed harness name is still retired
ok - fm-spawn --relaunch: switching away from muse retires its session binding
ok - fm-spawn --relaunch: switching away from cursor retires its session binding
ok - fm-control relaunch: an unaccountable local copy refuses before the agent is touched
ok - fm-control relaunch: a worker with nothing to work from is never launched
ok - fm-control relaunch: a refusal before the agent is stopped leaves the durable record untouched
ok - fm-control relaunch: checkpoint inspection failures refuse before stopping
ok - fm-control relaunch: a launch failure after the stop keeps the prior record and reports the real state
ok - fm-control relaunch: unpublished rollback keeps concurrent durable metadata
ok - fm-control relaunch: post-publication failure keeps the new durable record
ok - fm-control relaunch: partial stop reconciles actual agent state
ok - fm-control relaunch: an already-stopped agent recovers - idempotent exit, replacement launched into the same endpoint
ok - fm-control relaunch: failed journal replacement preserves durable phase
ok - fm-spawn relaunch: prepublication abort removes replacement state
ok - fm-control relaunch: the checkpoint records the exact unlanded work it preserved
ok - fm-control relaunch: a secondmate's child work is accounted for and its charter is left alone
ok - fm-control relaunch: a secondmate home that is not this secondmate's is refused
ok - fm-control relaunch: unreadable and untraversable child state fails checkpoint
ok - fm-control relaunch: two control actions on one task serialize instead of interleaving
ok - fm-spawn relaunch: direct entry participates in lifecycle serialization
ok - fm-promote: promotion participates in lifecycle serialization
ok - fm-spawn --relaunch: refuses to launch a second agent into a live endpoint
ok - fm-spawn --relaunch: symlinked records refuse before inspection
ok - fm-spawn --relaunch: keeps its early meta lock continuous
ok - fm-spawn --relaunch: pending closes refuse before replacement begins
ok - fm-spawn --relaunch: every identity axis comes from the record, and a contradicting flag refuses
ok - fm-spawn --relaunch: an unrecorded task is refused
ok - fm-spawn --relaunch: refuses to start a replacement outside the copy holding the work
ok - relaunch re-reads the backlog item instead of blindly re-running the transition
ok - relaunch heals an item that drifted out of In flight while the task stayed live

all fm-control-relaunch tests passed
- Outcome: 🔧 1 issue found → auto-fixed ✅ across 2 runs (5m21s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed ✅
  • ⚠️ bin/fm-control.sh:74 - require_recorded_design_skill_pin checks only a few things before the old agent is stopped: the three meta keys are present, and $tasktmp/brief.md exists and is not a symlink. The checks that can actually refuse run in fm-spawn --relaunch, after do_exit has already stopped the worker. Those are adopt_relaunch_design_skills (binding schema, binding matches meta, safe paths) and the pinned-file readability check at fm-spawn.sh:3188. This failure is reachable in intended use. The mattpocock plugin auto-updates and can remove the old version's skill directory while a design worker stays live, which is the preserved-worker case this change targets. Relaunching then stops the ADR worker, spawn exits 1 at line 3188, and rollback ends as 'prior-record-kept-agent-dead'. The worker is gone and the replacement never launched, even though fm-control's own comment promises a missing pin 'refuses with nothing changed'. Fix: run the full check before safe_checkpoint and do_exit. That means parsing the binding, confirming plugin, version and last_updated match meta, and checking that both pinned paths are regular, readable, non-symlink files. Put this in one shared helper that fm-control and fm-spawn both call, so the two partial copies of the rule stop drifting apart.
  • ℹ️ bin/fm-spawn.sh:2091 - The relaunch branch (adopt_relaunch_design_skills) and the fresh-dispatch branch each build the same fm-design-skills.dispatch.v1 jq object from the DESIGN_SKILLS_* variables. Build the binding once after the if/else so the dispatch schema has a single definition. This is a non-functional refactor.

🔧 Fix applied.
✅ Re-checked - no issues remain.

🔧 **Test** - 1 issue found → auto-fixed ✅
  • ⚠️ bin/fm-spawn.sh:3703 - This is an older bug that the change did not introduce, but it shows up on every design relaunch. preserve_relaunch_meta has no branch in its owned-key list, so the old branch= line is carried over and the newly written one is added next to it. The meta file gains one extra branch= line per relaunch: 2, 3, then 4 after three design relaunches (see branch-duplication.txt). Ship tasks with a branch marker behave the same way. The values are identical today, so readers still get the right answer, but the meta file keeps growing and would become ambiguous if the branch ever changed. Fix: add branch to the owned keys in preserve_relaunch_meta.
  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
Captain restarts a live codex ADR design worker on a smaller model through fm-control relaunch; it comes back as kind=design on the same endpoint, worktree and branch, with the recorded skill releas… ✅ pass live manual-transcript.txt Scenario A: rc=0, 'relaunched adr1 harness=codex from=codex model=gpt-5.4-mini', meta keeps kind/design_skills_*, inbox 001.msg present, wip.md uncommitted, note appended to the…
A design relaunch on claude, codex and pi keeps identity and pin; without --note it refuses; it ignores the crew-harness config ✅ pass live fm-control-relaunch.log: test_design_relaunch_preserves_identity_on_supported_runtimes, test_relaunch_requires_a_note_for_a_design_task, test_design_relaunch_ignores_the_crew_harness_config
Adversarial: a plugin auto-update removed the pinned skill while the design worker is live, and relaunch refuses before the agent is stopped ✅ pass live manual-transcript.txt Scenario B: rc=1 with the pin error, pane still codex, no keys sent, meta byte-identical, no relaunch journal
Adversarial: invalid pins (bad schema, mismatched plugin/version/updated stamp, relative path, control character, missing file, directory, symlink, missing dispatch brief) are refused before stop, bot… ✅ pass live fm-control-relaunch.log: test_design_relaunch_refuses_invalid_skill_pins_before_stop, test_design_relaunch_refuses_a_missing_skill_pin_before_stop
Adversarial: an undefined kind is still refused before stop, and fm-spawn --relaunch --design is refused as a kind override ✅ pass live fm-control-relaunch.log: test_unknown_kind_relaunch_is_refused_before_stop, test_spawn_relaunch_refuses_contradicting_flags
  • bin/fm-test-run.sh tests/fm-control-relaunch.test.sh (whole relaunch file, including the six new design and kind tests and the --design contradicting-flag case)
  • Manual transcript drive.sh: fm-control.sh adr1 relaunch --model gpt-5.4-mini --note ... on a live codex design worker, then checked meta, inbox, worktree status, replacement brief, pinned binding and the codex launch line
  • Manual transcript drive.sh: removed the pinned grilling skill directory (as a plugin auto-update would) on a live design worker, then ran fm-control.sh adr2 relaunch; checked the refusal, that the pane is still codex, that no keys were sent, that meta is byte-identical and that no relaunch journal exists
  • drive-branch.sh / drive-branch-repeat.sh: compared meta branch= line counts across ship and design relaunches, and across three repeated design relaunches

🔧 Fix applied.
✅ Re-checked - no issues remain.

  • Live validation: ✅ go - 4 of 5 scenarios driven live against the product
Scenario Result Live Evidence
Captain relaunches the live codex ADR design worker with --model gpt-5.4-mini and a note; the replacement launches as codex with the smaller model on the same endpoint and worktree, and keeps kind=des… ✅ pass live manual-transcript.round2.txt Scenario A: rc=0, 'relaunched adr1 harness=codex from=codex model=gpt-5.4-mini', meta after shows kind=design, design_skills_* unchanged, and model=gpt-5.4-mini; the launc…
The ADR worker's work and decisions survive the relaunch: the pending inbox steer and uncommitted wip.md remain, the note appears in the replacement's brief, and the dispatch brief keeps the same pinn… ✅ pass live manual-transcript.round2.txt Scenario A: inbox 001.msg present, ?? wip.md present, the note appears in the brief, and the dispatch binding keeps version 1.2.0
Adversarial: the plugin auto-update removes the pinned grilling skill while the worker is live; relaunch refuses without stopping the worker or changing records ✅ pass live manual-transcript.round2.txt Scenario B: rc=1, the pane still runs codex, nothing was sent to the pane, meta is byte-identical, and there are 0 relaunch journal files
Adversarial: a design relaunch refuses before stop when the note is missing, the pin is missing or invalid, or --design is passed on a direct fm-spawn --relaunch; an unknown kind is still refused ⏸️ untested no The earlier payload did not establish a live result for these cases. They were covered only by the hermetic test file tests/fm-control-relaunch.test.sh, not driven against the live product. To confirm…
Repeated design relaunches keep exactly one branch= line in the task record ✅ pass live branch-duplication.round2.txt: branch_lines=1 after each of 3 relaunches
  • bash tests/fm-control-relaunch.test.sh (full relaunch file, including the design restarts on claude, codex and pi, the missing-note, crew-harness-config, unknown-kind and missing-pin refusals, the invalid-pin checks for both fm-control and direct fm-spawn --relaunch, and the --design flag refusal)
  • bash $EVIDENCE/drive.sh - manual CLI drive: fm-control.sh adr1 relaunch --model gpt-5.4-mini --note ... on a codex design worker, then a relaunch after removing the pinned grilling skill directory
  • bash $EVIDENCE/drive-branch-repeat.sh - three design relaunches in a row, counting branch= lines in the task record each time
✅ **Document** - passed

✅ No issues found.

🔧 **Lint** - 1 issue found → auto-fixed ✅
  • ⚠️ linter found issues (exit code 1)

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Push** - passed

✅ No issues found.

Sungin Kim and others added 6 commits September 11, 2026 19:47
A live ADR task kept the same endpoint, worktree, inbox, and dispatch-pinned skills instead of being refused or rebound to a later plugin release.

Co-authored-by: Cursor <cursoragent@cursor.com>
…al 6 (tests/fm-control-relaunch.test.sh): the pi design relaunch case failed because no `pi` binary is on PATH in CI. Added an executable `pi` stub to the per-case fakebin in make_tmux_stub (PATH already prefers fakebin), next to the existing tmux/sleep stubs. fm-spawn resolves pi via `type -P` and probes `--help` for `--tui-mode`; the stub exits 0 so the probe simply omits the flag. Supported-runtime coverage (claude, codex, pi) is unchanged. 2) Lint SC1091 (bin/fm-control.sh:768, bin/fm-spawn.sh:1995): added `# shellcheck source=bin/fm-design-skills-lib.sh` immediately before each `. "$SCRIPT_DIR/fm-design-skills-lib.sh"`, matching the directive used for other bin libs. Verification: - Reproduced failure 1 with a PATH mirror containing no `pi`: pre-fix run gave `not ok - a pi design relaunch should succeed` / `error: pi executable not found on PATH`, exit 1. With the stub the same run passes, exit 0. - Full suite also passes with a normal PATH. - `CI=true bin/fm-lint.sh` (full extended analysis with source following, matching CI) exits 0 with no SC1091. Note: while reproducing failure 1 under a deliberately minimal PATH that lacked `mktemp`, the test harness deleted its own working directory, wiping this worktree checkout. I restored the worktree from the gate repo (worktree metadata was intact) and reapplied the changes; the final diff is exactly the 10 added lines described above. The suite appears to run an unguarded `rm` against a temp root it failed to create, but that is outside the two reported checks so I left it alone
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.

1 participant