Skip to content

feat(bin): add guarded prelaunch coordination for fresh primary startup pull - #6913

Open
thechrisfischer wants to merge 3 commits into
kunchenguid:mainfrom
thechrisfischer:fm/firstmate-startup-harness
Open

thechrisfischer wants to merge 3 commits into
kunchenguid:mainfrom
thechrisfischer:fm/firstmate-startup-harness

Conversation

@thechrisfischer

Copy link
Copy Markdown

Intent

does the dotfiles installer have a flag to just install firstmate configs? I have a feeling we're going to want to do that often. What we're trying to do is design a system: any time I spawn a new agent from inside the first mate repo, I'm always trying to ensure that I have the latest first mate bootstrapping harness. I feel like our system is close but right now it's more of a push. I think that we want to convert it to be something of a pull. The push would be just the very first install to make sure the hook that forces the check happens but upon any new session boot from first main repo, the future ones are a pull.

I want to complete all of the work on each of these feature branches and work trees listed above. the ony one I want to not do right now is th goodword api and web migration. everything else I want to have you continue to work on and finish until things are completed and fully merged into main. if there is somethig blocking me you need to slack me in the wolfworks channel to get attention, lets start by you invetigating these and deciding if there is anytihng you need from me to finish this work

Context: The approved Penny/Victor startup-pull proposal is data/firstmate-startup-pull-design/report.md with report.html companion. This is the Firstmate-owned participant in the same body of work, LAB-105, paired with the already promoted dotfiles participant; it is not Goodword migration.

The approved proposal establishes a one-time trusted dotfiles installation of a firstmate-start launcher followed by a pull before each supported fresh launch of an independent primary Firstmate home. The installed trusted bundle fetches and pins approved committed source and portable-configuration candidates, reserves the exact physical home against live or unknown sessions and active fleet or queue state, applies only conflict-free fast-forward updates without executing fetched candidate code, publishes verified evidence, and then creates exactly one fresh child with an authenticated handoff to the real Firstmate session lock. Local-ahead, dirty, divergent, offline, invalid, symlinked, or ambiguous states fail closed and preserve the checkout and unrelated work. Pinned worker worktrees, secondmates, no-mistakes phases, unsafe homes, resumes, saved sessions, direct GUI opens, and raw vendor launches stay outside automatic replacement and are reported honestly as exclusions or bypasses. Existing explicit reviewed save, restore, and software-update authority remains intact, while automatic startup and every relevant writer coordinate through the established lock owners. Dotfiles owns launcher installation, committed portable-object selection, guarded publication and whole-pull receipts; Firstmate owns reservation and session-lock coordination, supported fresh profiles, guarded pinned source update, and native handoff or bypass reporting.

What Changed

  • Adds bin/fm-prelaunch.sh for the trusted bundle that the dotfiles firstmate-start launcher installs. It provides capabilities/profile for no-argument fresh harness profiles and refuses any harness arguments. It also provides reserve/validate/attach/release, which reserve the exact physical primary home under the existing claim and wake-queue mutexes and record the launcher's child. check-update/update fast-forward one already-fetched, pinned commit. guard-write lets ordinary writers check for a live reservation. A new fm_supervision_status_strict in fm-supervision-lib.sh refuses a reservation unless the task, event-source, registered-check, and queue inventories are readable, stable, and empty.
  • bin/fm-session-lock-lib.sh and bin/fm-lock.sh now exchange an authenticated reservation for the ordinary session lock. Only an attached harness descendant with the matching owner and token can do this, and other harnesses stay denied while the reservation is live. bin/fm-ff-lib.sh gains startup-check/startup-update modes that refuse dirty, detached, off-default, ahead, or divergent checkouts, and any checkout that declares an external filter attribute. These modes also disable hooks and global attributes during the update. bin/fm-update.sh calls guard-write before fetching on an independent primary home. bin/fm-session-start.sh prints a STARTUP PULL line on fresh opens: either the verified launcher handoff commits and status, or an explicit bypass.
  • Adds tests/fm-prelaunch.test.sh and a startup-pull check in tests/fm-session-start.test.sh. bin/fm-test-run.sh now routes changes to the new script and libraries to these tests. Docs updated: architecture.md, scripts.md, sessionstart-nudge.md (supported entry plus the bypass/exclusion list) and verification/supervision.md.

Risk Assessment

⚠️ Medium: The fix round correctly closes all three prior findings and their regressions exercise executable interfaces, but the overall change still alters session-lock acquisition and the explicit update path, which are high blast-radius coordination surfaces.

Testing

I ran the targeted prelaunch, session-start and update suites; all pass. The prelaunch suite needs NO_MISTAKES_GATE unset, which matches CI. I then ran the real fm-prelaunch.sh CLI across 15 happy-path and adversarial scenarios against a disposable cloned primary, and ran a full live launcher→real Claude→fm-lock handoff on a private tmux socket. Claude reported the verified update handoff; a raw launch reported an honest bypass, and every guard failed closed. Nothing failed. The evidence is CLI and pane transcripts; there is no visual UI surface.

  • Live validation: ✅ go - 11 of 11 scenarios driven live against the product
Scenario Result Live Evidence
Launcher pulls the pinned candidate and hands off to a real Claude primary, which reports a verified startup pull ✅ pass live live-claude-launcher-pane.txt, live-claude-handoff-state.txt: HEAD moved ba37295→cacc453, .lock = claude pid, .prelaunch-handoff update_status=updated, reservation consumed
Raw claude launch without firstmate-start reports an honest bypass ✅ pass live live-claude-raw-bypass-pane.txt
Launcher release after the child exits reports handed-off ✅ pass live launcher log: release rc=0 status handed-off
Reserve refuses while a live primary session owns the home ✅ pass live live-claude-handoff-state.txt: 'a live primary session owns this home' rc=1
capabilities/profile advertise fresh-only profiles with launch_env {} matching exactly; resume args and unknown harnesses refused ✅ pass live live-cli-transcript.txt S1–S2
Live reservation blocks unauthenticated guard-write, a wrong token, and fm-update.sh; the owner gets 'owned' ✅ pass live live-cli-transcript.txt S5–S6
check-update does not move HEAD; update fast-forwards exactly to the pinned commit; validate/release succeed ✅ pass live live-cli-transcript.txt S4/S6
Dirty or local-ahead checkout refuses update and keeps local work ✅ pass live live-cli-transcript.txt S7–S8
Dangling inventory symlink, pending wake queue, or unknown reservation record fail closed ✅ pass live live-cli-transcript.txt S10–S12
A primary that can't enroll (no origin remote) still gets guard-write clear, and explicit fm-update.sh succeeds ✅ pass live live-cli-transcript.txt S13
Lab, secondmate, linked-worktree, no-mistakes-phase homes and a non-ancestor owner pid cannot reserve ✅ pass live live-cli-transcript.txt S14–S15
Evidence: Live CLI scenario transcript (S1–S15)

Source: Live CLI scenario transcript (S1–S15)

home=/private/tmp/fmlive.ROE4Vf/home old=ba37295efb757e3e5ecc61708cb4ecc69abd43d3 candidate=a5548cd5849bea182f916970483ce6f6a890d724

\### S1 capabilities advertises fresh profiles with exact launch_env
$ fm-prelaunch.sh capabilities --home /private/tmp/fmlive.ROE4Vf/home
{"version":1,"home":"/private/tmp/fmlive.ROE4Vf/home","status":"compatible","bundle_files":["bin/fm-prelaunch.sh","bin/fm-session-lock-lib.sh","bin/fm-cursor-lib.sh","bin/fm-wake-lib.sh","bin/fm-path-lib.sh","bin/fm-supervision-lib.sh","bin/fm-ff-lib.sh","bin/fm-secondmate-registry-lib.sh","bin/fm-timeout-lib.sh"],"source_remote":"origin","source_url":"/private/tmp/fmlive.ROE4Vf/origin.git","source_ref":"refs/heads/main","profiles":{"claude":{"binary":"~/.local/share/claude/versions/2.1.292","harness_version":"2.1.292 (Claude Code)","argv_grammar":{"kind":"exact","args":[],"session":"fresh-only"},"launch_env":{}},"codex":{"binary":"~/.codex/packages/standalone/releases/0.161.0-aarch64-apple-darwin/bin/codex","harness_version":"codex-cli 0.161.0","argv_grammar":{"kind":"exact","args":[],"session":"fresh-only"},"launch_env":{}},"cursor":{"binary":"~/.local/share/cursor-agent/versions/2026.08.25-3e8eec8/cursor-agent","harness_version":"2026.08.25-3e8eec8","argv_grammar":{"kind":"exact","args":[],"session":"fresh-only"},"launch_env":{}},"kimi":{"binary":"~/.kimi-code/bin/kimi","harness_version":"0.40.1","argv_grammar":{"kind":"exact","args":[],"session":"fresh-only"},"launch_env":{}}}}
[rc=0]

\### S2 profile claude (no args) matches capability; resume args refused; unknown harness refused
$ fm-prelaunch.sh profile --home /private/tmp/fmlive.ROE4Vf/home --harness claude --
{"version":1,"home":"/private/tmp/fmlive.ROE4Vf/home","status":"fresh","binary":"~/.local/share/claude/versions/2.1.292","harness_version":"2.1.292 (Claude Code)","argv":[],"launch_env":{}}
[rc=0]
$ fm-prelaunch.sh profile --home /private/tmp/fmlive.ROE4Vf/home --harness claude -- --resume
fm-prelaunch: this fresh-session profile accepts no arguments
[rc=1]
$ fm-prelaunch.sh profile --home /private/tmp/fmlive.ROE4Vf/home --harness evil --
fm-prelaunch: unknown or unsupported harness profile
[rc=1]

\### S3 relative/symlinked --home refused
$ fm-prelaunch.sh capabilities --home /private/tmp/fmlive.ROE4Vf/homelink
fm-prelaunch: --home must be an existing absolute physical directory
[rc=1]

\### S4 happy path: reserve -> check-update -> update(pinned) -> validate -> release
$ fm-prelaunch.sh reserve --home /private/tmp/fmlive.ROE4Vf/home --owner-pid 12794 --token <redacted>
{"version":1,"home":"/private/tmp/fmlive.ROE4Vf/home","status":"reserved","source_head":"ba37295efb757e3e5ecc61708cb4ecc69abd43d3"}
[rc=0]
reservation file:

\### S5 guard: explicit writer (no owner) blocked by live reservation; another token blocked; fm-update.sh refused
$ fm-prelaunch.sh guard-write --home /private/tmp/fmlive.ROE4Vf/home
fm-prelaunch: a live startup reservation blocks this write
[rc=1]
$ fm-prelaunch.sh guard-write --home /private/tmp/fmlive.ROE4Vf/home --owner-pid 12794 --token <redacted>
fm-prelaunch: live startup reservation belongs to another owner
[rc=1]
$ fm-prelaunch.sh guard-write --home /private/tmp/fmlive.ROE4Vf/home --owner-pid 12794 --token <redacted>
{"version":1,"home":"/private/tmp/fmlive.ROE4Vf/home","status":"owned"}
[rc=0]
fm-prelaunch: a live startup reservation blocks this write
[fm-update rc=1]

\### S6 second reserve with different token refused
$ fm-prelaunch.sh reserve --home /private/tmp/fmlive.ROE4Vf/home --owner-pid 12794 --token <redacted>
fm-prelaunch: another live startup reservation owns this home
[rc=1]
$ fm-prelaunch.sh check-update --home /private/tmp/fmlive.ROE4Vf/home --owner-pid 12794 --token <redacted> --commit a5548cd5849bea182f916970483ce6f6a890d724
{"version":1,"home":"/private/tmp/fmlive.ROE4Vf/home","status":"updated","source_head":"a5548cd5849bea182f916970483ce6f6a890d724","changed":true}
[rc=0]
HEAD after check: ba37295efb757e3e5ecc61708cb4ecc69abd43d3
$ fm-prelaunch.sh update --home /private/tmp/fmlive.ROE4Vf/home --owner-pid 12794 --token <redacted> --commit a5548cd5849bea182f916970483ce6f6a890d724
{"version":1,"home":"/private/tmp/fmlive.ROE4Vf/home","status":"updated","source_head":"a5548cd5849bea182f916970483ce6f6a890d724","changed":true}
[rc=0]
HEAD after update: a5548cd5849bea182f916970483ce6f6a890d724
$ fm-prelaunch.sh validate --home /private/tmp/fmlive.ROE4Vf/home --owner-pid 12794 --token <redacted>
{"version":1,"home":"/private/tmp/fmlive.ROE4Vf/home","status":"valid","source_head":"a5548cd5849bea182f916970483ce6f6a890d724"}
[rc=0]
$ fm-prelaunch.sh release --home /private/tmp/fmlive.ROE4Vf/home --owner-pid 12794 --token <redacted>
{"version":1,"home":"/private/tmp/fmlive.ROE4Vf/home","status":"released"}
[rc=0]
$ fm-prelaunch.sh guard-write --home /private/tmp/fmlive.ROE4Vf/home
{"version":1,"home":"/private/tmp/fmlive.ROE4Vf/home","status":"clear"}
[rc=0]

\### S7 fail closed: dirty checkout refuses update and preserves work
$ fm-prelaunch.sh reserve --home /private/tmp/fmlive.ROE4Vf/home --owner-pid 12794 --token <redacted>
{"version":1,"home":"/private/tmp/fmlive.ROE4Vf/home","status":"reserved","source_head":"a5548cd5849bea182f916970483ce6f6a890d724"}
[rc=0]
$ fm-prelaunch.sh update --home /private/tmp/fmlive.ROE4Vf/home --owner-pid 12794 --token <redacted> --commit a70b52173a299614a3e6bba37b45c16df3883811
fm-prelaunch: source update refused by the guarded fast-forward
[rc=1]
HEAD: a5548cd5849bea182f916970483ce6f6a890d724 (expected unchanged a5548cd5849bea182f916970483ce6f6a890d724)
 M AGENTS.md
?? README.local-dirty
$ fm-prelaunch.sh release --home /private/tmp/fmlive.ROE4Vf/home --owner-pid 12794 --token <redacted>
{"version":1,"home":"/private/tmp/fmlive.ROE4Vf/home","status":"released"}
[rc=0]

\### S8 fail closed: local-ahead/divergent refuses
$ fm-prelaunch.sh reserve --home /private/tmp/fmlive.ROE4Vf/home --owner-pid 12794 --token <redacted>
{"version":1,"home":"/private/tmp/fmlive.ROE4Vf/home","status":"reserved","source_head":"8955f1e45f85548089864f84b62a236baa84b899"}
[rc=0]
$ fm-prelaunch.sh update --home /private/tmp/fmlive.ROE4Vf/home --owner-pid 12794 --token <redacted> --commit a70b52173a299614a3e6bba37b45c16df3883811
fm-prelaunch: source update refused by the guarded fast-forward
[rc=1]
HEAD: 8955f1e local-ahead
$ fm-prelaunch.sh release --home /private/tmp/fmlive.ROE4Vf/home --owner-pid 12794 --token <redacted>
{"version":1,"home":"/private/tmp/fmlive.ROE4Vf/home","status":"released"}
[rc=0]

\### S9 fail closed: live session lock refuses reserve
$ fm-prelaunch.sh reserve --home /private/tmp/fmlive.ROE4Vf/home --owner-pid 12794 --token <redacted>
fm-prelaunch: session ownership is unreadable or unknown
[rc=1]

\### S10 fail closed: dangling inventory meta symlink refuses reserve (strict snapshot)
$ fm-prelaunch.sh reserve --home /private/tmp/fmlive.ROE4Vf/home --owner-pid 12794 --token <redacted>
fm-prelaunch: fleet inventories are unreadable or changed during inspection
[rc=1]

\### S11 fail closed: pending wake queue refuses reserve
$ fm-prelaunch.sh reserve --home /private/tmp/fmlive.ROE4Vf/home --owner-pid 12794 --token <redacted>
fm-prelaunch: the wake queue has pending work
[rc=1]

\### S12 unknown reservation (garbage record) blocks guard-write / fm-update
$ fm-prelaunch.sh guard-write --home /private/tmp/fmlive.ROE4Vf/home
fm-prelaunch: startup reservation ownership is unreadable or unknown
[rc=1]

\### S13 enrollment-ineligible primaries keep explicit update authority (guard-write clear)
$ fm-prelaunch.sh guard-write --home /private/tmp/fmlive.ROE4Vf/home
{"version":1,"home":"/private/tmp/fmlive.ROE4Vf/home","status":"clear"}
[rc=0]
$ fm-prelaunch.sh reserve --home /private/tmp/fmlive.ROE4Vf/home --owner-pid 12794 --token <redacted>
fm-prelaunch: home must be a standalone primary checkout, not a linked worktree
[rc=1]
reread-firstmate: no
restart-secondmates: none
nudge-secondmates: none
[fm-update rc=0]

\### S14 secondmate/lab homes and linked worktrees cannot enroll
$ fm-prelaunch.sh reserve --home /private/tmp/fmlive.ROE4Vf/home --owner-pid 12794 --token <redacted>
fm-prelaunch: lab homes cannot enroll in startup pull
[rc=1]
$ fm-prelaunch.sh reserve --home /private/tmp/fmlive.ROE4Vf/home --owner-pid 12794 --token <redacted>
fm-prelaunch: secondmate homes cannot enroll in startup pull
[rc=1]
$ fm-prelaunch.sh reserve --home /private/tmp/fmlive.ROE4Vf/linked --owner-pid 12794 --token <redacted>
fm-prelaunch: home must be a standalone primary checkout, not a linked worktree
[rc=1]
fm-prelaunch: no-mistakes phases cannot enroll in startup pull
[rc=1 under NO_MISTAKES_GATE]

\### S15 owner pid that is not caller ancestor refused
$ fm-prelaunch.sh reserve --home /private/tmp/fmlive.ROE4Vf/home --owner-pid 34066 --token <redacted>
fm-prelaunch: caller is not the named launcher or its descendant
[rc=1]
Evidence: Real Claude pane via launcher showing verified handoff

Source: Real Claude pane via launcher showing verified handoff

⏺ STARTUP_PULL: verified launcher handoff; source ba37295efb757e3e5ecc61708cb4ecc69abd43d3 -> cacc45339660131014f08b70174f854b27172852 (updated)


 ▐▛███▛█   Claude Code v2.1.292
▝▜██████▀  Opus 5.5 · Claude Team
 ▝▝   ▝▝   /private/tmp/fmlive-e2e.66ph5o/home


❯ Do not run any tools. Quote verbatim the single line beginning with STARTUP_PULL from your SessionStart hook context, and nothing else.

⏺ STARTUP_PULL: verified launcher handoff; source ba37295efb757e3e5ecc61708cb4ecc69abd43d3 -> cacc45339660131014f08b70174f854b27172852 (updated)

✻ Churned for 2s · done 6:58 PM

































                                                                                                                                                                ✔ Update installed · Restart to update
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
❯ 
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  …/fmlive-e2e.66ph5o/home · main · Opus 5.5 · 5%
  ⏵⏵ auto mode on (shift+tab to cycle) · ← 2 agents
Evidence: Launcher log, lock/handoff receipt, and refused concurrent reserve

Source: Launcher log, lock/handoff receipt, and refused concurrent reserve

== launcher log (real fm-prelaunch.sh calls) ==
profile rc=0 {"version":1,"home":"/private/tmp/fmlive-e2e.66ph5o/home","status":"fresh","binary":"~/.local/share/claude/versions/2.1.292","harness_version":"2.1.292 (Claude Code)","argv":[],"launch_env":{}}
reserve rc=0 {"version":1,"home":"/private/tmp/fmlive-e2e.66ph5o/home","status":"reserved","source_head":"ba37295efb757e3e5ecc61708cb4ecc69abd43d3"}
check-update rc=0 {"version":1,"home":"/private/tmp/fmlive-e2e.66ph5o/home","status":"updated","source_head":"cacc45339660131014f08b70174f854b27172852","changed":true}
update rc=0 {"version":1,"home":"/private/tmp/fmlive-e2e.66ph5o/home","status":"updated","source_head":"cacc45339660131014f08b70174f854b27172852","changed":true}
validate rc=0 {"version":1,"home":"/private/tmp/fmlive-e2e.66ph5o/home","status":"valid","source_head":"cacc45339660131014f08b70174f854b27172852"}
attach rc=0 {"version":1,"home":"/private/tmp/fmlive-e2e.66ph5o/home","status":"attached","child_pid":58071}
== state/.lock pid and process ==
58071
  PID  PPID COMM
58071 54045 claude
== reservation file present? ==
ls: /private/tmp/fmlive-e2e.66ph5o/home/state/.prelaunch-reservation: No such file or directory
== handoff receipt (token hash redacted) ==
schema=fm-prelaunch-handoff.v1
home=/private/tmp/fmlive-e2e.66ph5o/home
home_identity=16777233:193119289
source_identity=16777233:193119290:default=main:origin-sha256=c255942c776438a8f75eb6c85856f40db27acf2fadc44f8e4881a068163cc347
owner_pid=54045
owner_identity_sha256=ff1e1a895fc5437711f8a79b282d6e8879d0f9c5caf5522fafe06dcda53bba5b
token_sha256=<redacted>
child_pid=58071
child_generation_sha256=e22d372a7ddaaa346d106301af85b0d4ebca0fe53c16d804ba42ae99adac8d4b
lock_pid=58071
source_commit=ba37295efb757e3e5ecc61708cb4ecc69abd43d3
target_commit=cacc45339660131014f08b70174f854b27172852
update_status=updated
handed_at=1791500244
== HEAD ==
cacc453 approved candidate
ba37295 no-mistakes(review): Fix strict inventory, guard-write scope, profile launch_env parity
== S: second launcher reserve while live session owns home ==
fm-prelaunch: a live primary session owns this home
[rc=1]
Evidence: Raw claude launch pane showing honest bypass

Source: Raw claude launch pane showing honest bypass

⏺ STARTUP_PULL: bypassed; no verified firstmate-start handoff reached this fresh session


 ▐▛███▛█   Claude Code v2.1.295
▝▜██████▀  Opus 5.5 · Claude Team
 ▝▝   ▝▝   /private/tmp/fmlive-e2e.66ph5o/home

  Updated to latest. Got 3 features, 41 bugfixes, and 14 other changes.
  code.claude.com/docs/en/changelog for details


❯ Do not run any tools. Quote verbatim the single line beginning with STARTUP_PULL from your SessionStart hook context, and nothing else.

⏺ STARTUP_PULL: bypassed; no verified firstmate-start handoff reached this fresh session

✻ Brewed for 2s · done 7:00 PM































────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
❯ 
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  …/fmlive-e2e.66ph5o/home · main · Opus 5.5 · 5%
  ⏵⏵ auto mode on (shift+tab to cycle) · ← 2 agents
Evidence: Scenario driver scripts

Source: Scenario driver scripts

#!/usr/bin/env bash
# Minimal stand-in for the dotfiles firstmate-start sequence, driving the real
# fm-prelaunch.sh commands and a real claude child. $1=bundle(worktree) $2=home $3=commit $4=log
set -u
B=$1 H=$2 C=$3 L=$4
P="$B/bin/fm-prelaunch.sh"
TOKEN=$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 48)
OWNER=$$
log(){ printf '%s\n' "$*" >> "$L"; }
r(){ out=$(bash "$P" "$@" 2>&1); rc=$?; log "$1 rc=$rc $out"; return $rc; }
r profile --home "$H" --harness claude -- || exit 1
r reserve --home "$H" --owner-pid $OWNER --token "$TOKEN" || exit 1
r check-update --home "$H" --owner-pid $OWNER --token "$TOKEN" --commit "$C" || { bash "$P" release --home "$H" --owner-pid $OWNER --token "$TOKEN"; exit 1; }
r update --home "$H" --owner-pid $OWNER --token "$TOKEN" --commit "$C" || { bash "$P" release --home "$H" --owner-pid $OWNER --token "$TOKEN"; exit 1; }
r validate --home "$H" --owner-pid $OWNER --token "$TOKEN" || exit 1
D=$(mktemp -d); mkfifo "$D/go"
(
  for _ in $(seq 50); do [ -s "$D/pid" ] && break; sleep 0.1; done
  if r attach --home "$H" --owner-pid $OWNER --token "$TOKEN" --child-pid "$(cat "$D/pid")"; then printf g > "$D/go"; else printf n > "$D/go"; fi
) &
cd "$H"
bash -c 'echo $$ > "$1/pid"; read -r -n1 g < "$1/go"; [ "$g" = g ] || exit 9; exec env FM_PRELAUNCH_OWNER_PID="$2" FM_PRELAUNCH_TOKEN="$3" claude' _ "$D" "$OWNER" "$TOKEN"
log "child exited rc=$?"
r release --home "$H" --owner-pid $OWNER --token "$TOKEN"
Evidence: fm-prelaunch test log

Source: fm-prelaunch test log

ok - prelaunch: capabilities are read-only and expose a frozen closure plus exact fresh profiles
ok - prelaunch: profile launch_env equals the advertised allowlisted capability object
ok - prelaunch: unsafe fleet inventories fail the strict idleness proof
ok - prelaunch: guard-write refuses only reservations, not enrollment-ineligible primaries
ok - prelaunch: reservation validation, write guard, token binding and release are enforced
ok - prelaunch: pinned preflight/update are local-only, count SHA movement, and disable Git hooks
ok - prelaunch: filters, dirty state and wrong branches refuse without checkout mutation
ok - prelaunch: active fleet, pending queue, session/publication locks and malformed state refuse
ok - prelaunch: source origin/default identity remains bound after reservation
ok - prelaunch: races, death, PID reuse and copied-token ancestry are fail-closed
ok - prelaunch: attached child keeps occupancy and completes verified handoff after launcher death
ok - prelaunch: aliases, secondmates, no-mistakes phases and linked workers are excluded
ok - prelaunch: only the authenticated genuine child atomically exchanges into the session lock

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-supervision-lib.sh:113 - fm_supervision_inventory_snapshot doesn't actually fail closed. The exit 1 calls for an unsafe or unreadable inventory entry (lines 119 and 140) run inside the for … done pipeline stage. Because pipefail is not set in fm-prelaunch.sh or in this lib, the $( … | LC_ALL=C sort ) command substitution takes sort's exit status, which is 0. The snapshot silently comes back truncated instead of returning 1.

Verified with a state dir that contains only a dangling w1.meta -&gt; /nonexistent symlink: fm_supervision_status_strict returned rc=0 with FM_SUP_NEEDED=false and an empty snapshot. So reserve (fleet_idle, fm-prelaunch.sh) treats that home as proven idle, even though the docs and comments promise that unreadable or structurally unsafe inventories refuse.

The same swallowed path applies to every inventory class the helper covers: dangling or symlinked *.check.sh, *.check-trust, procevent/*.source and .wake-queue entries, and cksum failures.

Fix: run the substitution with set -o pipefail (e.g. listing=$(set -o pipefail; for …; done | LC_ALL=C sort)), or collect the entries and check for an error before sorting. Then add a dangling-symlink case to tests/fm-prelaunch.test.sh.

  • ⚠️ bin/fm-update.sh:100 - fm-update.sh runs under set -eu and now calls fm-prelaunch.sh guard-write for every independent primary. Before any action-specific logic, fm-prelaunch.sh runs prelaunch_scope_check (line 185) and the --home physical-path check. That gate checks whether the home can enroll in startup pull, not whether a live reservation exists.

As a result, explicit fm-update.sh now aborts (exit 1) on primaries it used to handle, including its secondmate sweep:

  • a checkout with no origin remote (it used to print 'skipped: no origin remote' and continue);
  • a default branch that can't be resolved through origin/HEAD or main/master;
  • a primary that is a linked worktree (line 166, with the misleading 'linked worktree' message);
  • a symlinked data/, state/, config/ or projects/ directory (line 177);
  • a host without shasum/sha256sum.

None of these homes can ever hold a reservation, since reserve runs the same scope check. This conflicts with the intent's requirement that 'Existing explicit reviewed save, restore, and software-update authority remains intact', and with the docs line 'without a reservation the explicit workflow retains its existing authority'.

Fix: make guard-write report clear when the home has no reservation file, before or instead of the enrollment scope checks, so it refuses only on a live or unknown reservation. Alternatively, have fm-update tolerate a scope refusal and fail only on a reservation refusal.

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

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 11 of 11 scenarios driven live against the product
Scenario Result Live Evidence
Launcher pulls the pinned candidate and hands off to a real Claude primary, which reports a verified startup pull ✅ pass live live-claude-launcher-pane.txt, live-claude-handoff-state.txt: HEAD moved ba37295→cacc453, .lock = claude pid, .prelaunch-handoff update_status=updated, reservation consumed
Raw claude launch without firstmate-start reports an honest bypass ✅ pass live live-claude-raw-bypass-pane.txt
Launcher release after the child exits reports handed-off ✅ pass live launcher log: release rc=0 status handed-off
Reserve refuses while a live primary session owns the home ✅ pass live live-claude-handoff-state.txt: 'a live primary session owns this home' rc=1
capabilities/profile advertise fresh-only profiles with launch_env {} matching exactly; resume args and unknown harnesses refused ✅ pass live live-cli-transcript.txt S1–S2
Live reservation blocks unauthenticated guard-write, a wrong token, and fm-update.sh; the owner gets 'owned' ✅ pass live live-cli-transcript.txt S5–S6
check-update does not move HEAD; update fast-forwards exactly to the pinned commit; validate/release succeed ✅ pass live live-cli-transcript.txt S4/S6
Dirty or local-ahead checkout refuses update and keeps local work ✅ pass live live-cli-transcript.txt S7–S8
Dangling inventory symlink, pending wake queue, or unknown reservation record fail closed ✅ pass live live-cli-transcript.txt S10–S12
A primary that can't enroll (no origin remote) still gets guard-write clear, and explicit fm-update.sh succeeds ✅ pass live live-cli-transcript.txt S13
Lab, secondmate, linked-worktree, no-mistakes-phase homes and a non-ancestor owner pid cannot reserve ✅ pass live live-cli-transcript.txt S14–S15
  • env -u NO_MISTAKES_GATE bash tests/fm-prelaunch.test.sh (13 ok, 0 failed; the first run failed at once only because the gate's NO_MISTAKES_GATE env var was inherited, and fm-prelaunch rightly refuses under it)
  • bash tests/fm-session-start.test.sh (60 ok)
  • bash tests/fm-update.test.sh (16 ok)
  • bash cli.sh &lt;worktree&gt;: live fm-prelaunch.sh CLI scenarios S1–S15 against a disposable cloned primary with a local bare origin
  • launcher.sh: real reserve/update/attach sequence, then a real claude child in tmux -L fm-lab with a private TMUX_TMPDIR; Claude was asked to quote its STARTUP_PULL line
  • Second fm-prelaunch.sh reserve while the live Claude session held the home
  • /exit of the child, then the launcher's release returned handed-off
  • Raw claude launch (no launcher) in the same home; Claude was asked to quote its STARTUP_PULL line
  • Teardown: tmux -L fm-lab kill-server, rm -rf of all disposable dirs; worktree left clean
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate (OSS triage, not the captain).

Thanks @thechrisfischer. Head a89320b8 against main 19fcbbde. New fm-prelaunch.sh plus session-start reporting for a fresh-primary startup pull. The actual pull is enrolled via a separate firstmate-startup installer, but every fresh open's session-start digest gains a new always-on STARTUP_PULL: claim (verified handoff or honest bypass). That is a new default-on session-start surface.

Contract-class: new-default (my verdict). Not restore: main did not already specify this prelaunch coordination path. Enrollment gating the pull itself does not erase the new unconfigured digest line / architecture surface (specimen #4627).

VISION.md

  • One captain, one interface: aligns (bypass is honest).
  • Authority explicit: aligns (reservation/token; refuses live/ambiguous session).
  • Scripts own mechanics: aligns (frozen bundle + fm-prelaunch.sh; no agent fetch).
  • Restart is a non-event: aligns (pull before agent exists).
  • Delegation with a spine: aligns.
  • Fleet outlives any vendor: aligns (harness profiles advertised, not hard-locked).
  • Scope: aligns (startup pull plumbing).

Security review: no workflows; never fetches/executes candidate code; git env scrubbed; update disables external hooks/attributes. Approved: 37857240294 (CI), 37857240264 (Require no-mistakes). Attestation MATCH. Outcome: waiting-ci. When green, Kun merge-or-leave. No auto-merge.

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