Skip to content

feat(bin): give each secondmate its own durable runtime - #1960

Open
eduardstan wants to merge 14 commits into
kunchenguid:mainfrom
eduardstan:fm/secondmate-runtime-per-mate
Open

eduardstan wants to merge 14 commits into
kunchenguid:mainfrom
eduardstan:fm/secondmate-runtime-per-mate

Conversation

@eduardstan

Copy link
Copy Markdown

Intent

Each secondmate gains its own durable runtime: harness, model and effort recorded in its data/secondmates.md registry entry and re-resolved on every spawn, so a per-mate choice survives recovery, /updatefirstmate and any other relaunch instead of reverting to the home-wide config/secondmate-harness pin. That config file stays the fallback default. Per-axis and backward compatible: a mate may pin only a model or only an effort and inherit the rest; a record carrying none of the fields is the pre-existing form and behaves exactly as before; a recorded harness deliberately suppresses the config file's model/effort tokens because those were written against that file's own harness. Precedence per axis, strongest first: explicit per-spawn --harness/--model/--effort flag, then the mate's recorded field, then config/secondmate-harness. Deliberately NO rules engine and NO new config file - the captain ruled out a secondmate-dispatch.json because secondmates are persistent and rarely change. Fail-closed: an unverified harness, an effort outside low|medium|high|xhigh|max, an unusable model (including the reserved '-' and 'default' route sentinels), an unknown or repeated key, or a missing field terminator makes the record malformed and is refused at edit time, at fm-home-seed.sh validate, and again before any launch. Local and remote routes resolve identically and the remote route keeps refusing anything but a verified adapter, now by calling the registry lib's shared allowlist rather than its own copy. bin/fm-home-seed.sh gains a 'runtime' action as the create-time and change-time path, taking the registry lock and validating the whole rewritten registry before replacing it. Captain's explicit decision during review: a re-seed of an already-registered mate PRESERVES its recorded harness/model/effort and must never silently reset it to the home-wide pin; both registry writers emit the runtime segment through the shared secondmate_registry_render_line boundary, with a test that re-seeds a registered mate and asserts the pin survives. bin/fm-fleet-snapshot.sh's independent jq copy of the record regexes was widened to match, so pinned mates no longer show a null home in the fleet and bearings snapshots. The branch also carries a pre-existing flaky-test fix: test_config_reread_serializes_concurrent_pushes bounded a backgrounded config push by 100 x 0.02s spins, which a loaded host misses; it now uses a 60-second wall-clock deadline. That flake was proven to pre-exist on an unmodified checkout of the same base commit (failed 3/3 there). Contributed from a fork per CONTRIBUTING.md: origin is the parent repo and the gate pushes to eduardstan/firstmate, so this is a cross-fork head.

What Changed

  • bin/fm-secondmate-registry-lib.sh now parses an optional per-mate runtime segment (harness:, model:, effort:, each independently optional) between projects: and added on both the local and remote record forms. It fails closed: an unverified adapter, an effort outside low|medium|high|xhigh|max, an unusable model (including the - and default route sentinels), an unknown or repeated key, or a missing field terminator makes the record malformed. A record carrying none of the fields parses exactly as before. The lib also exports the shared adapter allowlist and a secondmate_registry_render_line writer boundary.
  • bin/fm-spawn.sh resolves each axis independently — explicit per-spawn --harness/--model/--effort, then the mate's recorded field, then config/secondmate-harness — and suppresses the config file's model/effort tokens when a harness is recorded. The remote route reads the record in-shell (so a malformed pin surfaces instead of falling through to the local path) and enforces verified adapters via the registry lib's allowlist rather than its own copy.
  • bin/fm-home-seed.sh gains a runtime <id> <harness|model|effort>=<value|-> action that takes the registry lock, validates every value and the whole rewritten registry before replacing it, and - clears an axis; write_registry carries a registered mate's recorded runtime forward across re-seed and both writers emit through the shared render boundary. bin/fm-fleet-snapshot.sh's jq record parser was widened to the same vocabulary so pinned mates no longer report a null home and unreadable pins are flagged. Also bounds the pre-existing flaky test_config_reread_serializes_concurrent_pushes by a 60-second wall-clock deadline instead of 100 × 0.02s spins.

Risk Assessment

✅ Low: The change is well-bounded and matches every source-verifiable acceptance criterion — per-axis precedence resolves identically on both routes, refusal is enforced at edit, validate, and pre-launch, both registry writers preserve the pin through the shared render boundary, legacy records parse unchanged, and a cross-parser check of the fleet-snapshot jq copy found no case where it accepts a record the launcher refuses.

Testing

Ran the seven new per-mate runtime tests in tests/fm-secondmate-harness.test.sh, the two new registry-runtime tests in tests/fm-bearings-snapshot.test.sh, and the previously flaky test_config_reread_serializes_concurrent_pushes three times in a row — all pass. The remote-route e2e assertion in tests/fm-remote-secondmate-lifecycle-e2e.test.sh also passed, though that whole long linear file was still executing when this phase closed, so remote CI owns its full result. Beyond the automated tests I drove the real CLI end to end as a captain would and captured the transcript: two secondmates in one home pinned to different runtimes launch on their own harness/model/effort, a relaunch and a re-seed both preserve the pin, an explicit --model still wins, invalid harness/effort/model values and an unknown record key are refused at edit time, at validate, and before launch with no launch command emitted, and the fleet snapshot keeps pinned mates' homes non-null while flagging only the malformed record. The worktree is clean; no source or test files were modified.

Evidence: Captain CLI walkthrough transcript (per-mate runtime end to end)

$ fm-home-seed.sh runtime infra harness=codex model=gpt-x effort=xhigh - infra - demo charter (home: /tmp/.../infra; scope: demo charter; projects: ; harness: codex; model: gpt-x; effort: xhigh; added 2026-08-08) $ fm-home-seed.sh runtime docs model=cheap-1 # one axis only, rest inherited - docs - demo charter (home: /tmp/.../docs; scope: demo charter; projects: ; model: cheap-1; added 2026-08-08) $ fm-spawn.sh infra --secondmate # launch command handed to the pane ... codex --model 'gpt-x' -c 'model_reasoning_effort="xhigh"' --dangerously-bypass-approvals-and-sandbox ... $ fm-spawn.sh docs --secondmate ... claude --dangerously-skip-permissions --model 'cheap-1' --effort 'high' ... $ fm-spawn.sh infra --secondmate --model sonnet # explicit flag outranks the record ... codex --model 'sonnet' -c 'model_reasoning_effort="xhigh"' ... $ fm-home-seed.sh infra <home> --no-projects # re-seed must NOT reset the pin - infra - demo charter (home: /tmp/.../infra; ...; harness: codex; model: gpt-x; effort: xhigh; added 2026-08-08) $ fm-home-seed.sh runtime infra effort=turbo error: invalid effort for infra: turbo (expected low, medium, high, xhigh, or max) exit=1 $ fm-home-seed.sh runtime infra harness=muse error: unverified harness for infra: muse (verified secondmate adapters: claude, codex, opencode, pi, pi-signed, grok, kimi) exit=1 $ fm-home-seed.sh runtime infra model=default error: unusable model for infra: default exit=1 $ record hand-edited to 'flavor: vanilla;' error: unrecognized runtime field 'flavor' for infra validate exit=1 spawn exit=1 -> error: unrecognized runtime field 'flavor' for infra (no launch command was emitted) $ fm-fleet-snapshot.sh --json {"id":"docs","home":"/tmp/.../docs","registry_error":null} {"id":"infra","home":"/tmp/.../infra","registry_error":"registry entry has an unreadable runtime pin"} # after restoring the valid pin: {"id":"infra","home":"/tmp/.../infra","registry_error":null}


$ fm-home-seed.sh infra ... / docs ...   # register two secondmates in one home
scaffolded: /tmp/fm-runtime-demo.Pg7oFY/home/data/infra/brief.md (secondmate charter)
home=/tmp/fm-runtime-demo.Pg7oFY/infra
scaffolded: /tmp/fm-runtime-demo.Pg7oFY/home/data/docs/brief.md (secondmate charter)
home=/tmp/fm-runtime-demo.Pg7oFY/docs

$ cat config/secondmate-harness   # the home-wide fallback pin
claude opus high

$ fm-home-seed.sh runtime infra harness=codex model=gpt-x effort=xhigh
- infra - demo charter (home: /tmp/fm-runtime-demo.Pg7oFY/infra; scope: demo charter; projects: ; harness: codex; model: gpt-x; effort: xhigh; added 2026-08-08)

$ fm-home-seed.sh runtime docs model=cheap-1   # one axis only, rest inherited
- docs - demo charter (home: /tmp/fm-runtime-demo.Pg7oFY/docs; scope: demo charter; projects: ; model: cheap-1; added 2026-08-08)

$ cat data/secondmates.md
- infra - demo charter (home: /tmp/fm-runtime-demo.Pg7oFY/infra; scope: demo charter; projects: ; harness: codex; model: gpt-x; effort: xhigh; added 2026-08-08)
- docs - demo charter (home: /tmp/fm-runtime-demo.Pg7oFY/docs; scope: demo charter; projects: ; model: cheap-1; added 2026-08-08)

$ fm-spawn.sh infra --secondmate   # launch command handed to the pane
FM_ROOT_OVERRIDE= FM_STATE_OVERRIDE= FM_DATA_OVERRIDE= FM_PROJECTS_OVERRIDE= FM_CONFIG_OVERRIDE= FM_PUBLIC_FOLLOWUP_PRIMARY_HOME='/tmp/fm-runtime-demo.Pg7oFY/home' FM_HOME='/tmp/fm-runtime-demo.Pg7oFY/infra' FM_TRACE_CONTEXT=off FM_SUPERVISION_MODEL=persistent codex --model 'gpt-x' -c 'model_reasoning_effort="xhigh"' --dangerously-bypass-approvals-and-sandbox "$('/home/eduard/.no-mistakes/worktrees/c2387437e145/01KZH07MZVBRCRN0DDNSEQ2FP6/bin/fm-operational-input.sh' encode launch-brief < '/tmp/fm-runtime-demo.Pg7oFY/infra/data/charter.md')"

$ fm-spawn.sh docs --secondmate
FM_ROOT_OVERRIDE= FM_STATE_OVERRIDE= FM_DATA_OVERRIDE= FM_PROJECTS_OVERRIDE= FM_CONFIG_OVERRIDE= FM_PUBLIC_FOLLOWUP_PRIMARY_HOME='/tmp/fm-runtime-demo.Pg7oFY/home' FM_HOME='/tmp/fm-runtime-demo.Pg7oFY/docs' FM_TRACE_CONTEXT=off FM_SUPERVISION_MODEL=autoarm CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions --model 'cheap-1' --effort 'high' "$('/home/eduard/.no-mistakes/worktrees/c2387437e145/01KZH07MZVBRCRN0DDNSEQ2FP6/bin/fm-operational-input.sh' encode launch-brief < '/tmp/fm-runtime-demo.Pg7oFY/docs/data/charter.md')"

$ cat state/infra.meta | grep -E 'harness|model|effort'
harness=codex
model=gpt-x
effort=xhigh

$ fm-spawn.sh infra --secondmate   # relaunch (recovery / updatefirstmate path)
FM_ROOT_OVERRIDE= FM_STATE_OVERRIDE= FM_DATA_OVERRIDE= FM_PROJECTS_OVERRIDE= FM_CONFIG_OVERRIDE= FM_PUBLIC_FOLLOWUP_PRIMARY_HOME='/tmp/fm-runtime-demo.Pg7oFY/home' FM_HOME='/tmp/fm-runtime-demo.Pg7oFY/infra' FM_TRACE_CONTEXT=off FM_SUPERVISION_MODEL=persistent codex --model 'gpt-x' -c 'model_reasoning_effort="xhigh"' --dangerously-bypass-approvals-and-sandbox "$('/home/eduard/.no-mistakes/worktrees/c2387437e145/01KZH07MZVBRCRN0DDNSEQ2FP6/bin/fm-operational-input.sh' encode launch-brief < '/tmp/fm-runtime-demo.Pg7oFY/infra/data/charter.md')"

$ fm-spawn.sh infra --secondmate --model sonnet   # explicit flag outranks the record
FM_ROOT_OVERRIDE= FM_STATE_OVERRIDE= FM_DATA_OVERRIDE= FM_PROJECTS_OVERRIDE= FM_CONFIG_OVERRIDE= FM_PUBLIC_FOLLOWUP_PRIMARY_HOME='/tmp/fm-runtime-demo.Pg7oFY/home' FM_HOME='/tmp/fm-runtime-demo.Pg7oFY/infra' FM_TRACE_CONTEXT=off FM_SUPERVISION_MODEL=persistent codex --model 'sonnet' -c 'model_reasoning_effort="xhigh"' --dangerously-bypass-approvals-and-sandbox "$('/home/eduard/.no-mistakes/worktrees/c2387437e145/01KZH07MZVBRCRN0DDNSEQ2FP6/bin/fm-operational-input.sh' encode launch-brief < '/tmp/fm-runtime-demo.Pg7oFY/infra/data/charter.md')"

$ fm-home-seed.sh infra <home> --no-projects   # re-seed must NOT reset the pin
home=/tmp/fm-runtime-demo.Pg7oFY/infra
- infra - demo charter (home: /tmp/fm-runtime-demo.Pg7oFY/infra; scope: demo charter; projects: ; harness: codex; model: gpt-x; effort: xhigh; added 2026-08-08)

$ fm-home-seed.sh runtime infra effort=turbo   # fail-closed at edit time
error: invalid effort for infra: turbo (expected low, medium, high, xhigh, or max)
exit=1

$ fm-home-seed.sh runtime infra harness=muse   # unverified adapter
error: unverified harness for infra: muse (verified secondmate adapters: claude, codex, opencode, pi, pi-signed, grok, kimi)
exit=1

$ fm-home-seed.sh runtime infra model=default   # reserved route sentinel
error: unusable model for infra: default
exit=1

$ record hand-edited to an unknown key -> refused at validate and before launch
- infra - demo charter (home: /tmp/fm-runtime-demo.Pg7oFY/infra; scope: demo charter; projects: ; flavor: vanilla; model: gpt-x; effort: xhigh; added 2026-08-08)
error: unrecognized runtime field 'flavor' for infra
validate exit=1
spawn exit=1
error: unrecognized runtime field 'flavor' for infra
(no launch command was emitted)

$ fm-fleet-snapshot.sh --json   # the malformed pin is reported, not shown as healthy
{"id":"docs","home":"/tmp/fm-runtime-demo.Pg7oFY/docs","registry_error":null}
{"id":"infra","home":"/tmp/fm-runtime-demo.Pg7oFY/infra","registry_error":"registry entry has an unreadable runtime pin"}

$ fm-fleet-snapshot.sh --json   # pinned mates keep a non-null home in the fleet
{"id":"docs","home":"/tmp/fm-runtime-demo.Pg7oFY/docs","registry_error":null}
{"id":"infra","home":"/tmp/fm-runtime-demo.Pg7oFY/infra","registry_error":null}
Evidence: Reproducible walkthrough script
#!/usr/bin/env bash
# Captain's-eye walkthrough of per-secondmate durable runtime.
# Run from the firstmate worktree root. Writes nothing into the worktree.
set -u

ROOT=${1:?usage: demo-per-mate-runtime.sh <firstmate-repo-root>}
W=$(mktemp -d "${TMPDIR:-/tmp}/fm-runtime-demo.XXXXXX")
trap 'rm -rf "$W"' EXIT
HOME_DIR="$W/home"
mkdir -p "$HOME_DIR/config" "$HOME_DIR/state" "$HOME_DIR/data" "$HOME_DIR/projects"

# Home-wide pin: every secondmate this home launches defaults to claude/opus/high.
printf 'claude opus high\n' > "$HOME_DIR/config/secondmate-harness"

# tmux stub that records the literal launch command the pane would run.
FAKEBIN="$W/fakebin"; mkdir -p "$FAKEBIN"
cat > "$FAKEBIN/tmux" <<'SH'
#!/usr/bin/env bash
set -u
case "$*" in *"#{pane_current_path}"*) printf '%s\n' "${FM_FAKE_PANE_PATH:-}"; exit 0 ;; esac
case "${1:-}" in
  display-message) printf 'firstmate\n'; exit 0 ;;
  send-keys)
    prev=
    for a in "$@"; do
      [ "$prev" = "-l" ] && printf '%s\n' "$a" >> "$FM_FAKE_LAUNCH_LOG"
      prev=$a
    done
    exit 0 ;;
esac
exit 0
SH
chmod +x "$FAKEBIN/tmux"

fm() {
  FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$HOME_DIR" \
    FM_STATE_OVERRIDE="$HOME_DIR/state" FM_DATA_OVERRIDE="$HOME_DIR/data" \
    FM_PROJECTS_OVERRIDE="$HOME_DIR/projects" FM_CONFIG_OVERRIDE="$HOME_DIR/config" \
    FM_SECONDMATE_CHARTER='demo charter' FM_GATE_REFUSE_BYPASS=1 \
    "$@"
}
seed() { fm "$ROOT/bin/fm-home-seed.sh" "$@"; }
spawn() {  # <id>
  local id=$1; shift
  : > "$W/launch-$id.log"
  PATH="$FAKEBIN:$PATH" TMUX='' CLAUDECODE=1 FM_SPAWN_NO_GUARD=1 \
    FM_FAKE_LAUNCH_LOG="$W/launch-$id.log" fm "$ROOT/bin/fm-spawn.sh" "$id" --secondmate "$@" >"$W/spawn.out" 2>&1 \
    || { echo "spawn failed:"; tail -3 "$W/spawn.out"; return 1; }
  tail -1 "$W/launch-$id.log"
}
say() { printf '\n\033[1m$ %s\033[0m\n' "$*"; }

say "fm-home-seed.sh infra ... / docs ...   # register two secondmates in one home"
seed infra "$W/infra" --no-projects
seed docs "$W/docs" --no-projects

say "cat config/secondmate-harness   # the home-wide fallback pin"
cat "$HOME_DIR/config/secondmate-harness"

say "fm-home-seed.sh runtime infra harness=codex model=gpt-x effort=xhigh"
seed runtime infra harness=codex model=gpt-x effort=xhigh

say "fm-home-seed.sh runtime docs model=cheap-1   # one axis only, rest inherited"
seed runtime docs model=cheap-1

say "cat data/secondmates.md"
cat "$HOME_DIR/data/secondmates.md"

say "fm-spawn.sh infra --secondmate   # launch command handed to the pane"
spawn infra
say "fm-spawn.sh docs --secondmate"
spawn docs

say "cat state/infra.meta | grep -E 'harness|model|effort'"
grep -E '^(harness|model|effort)=' "$HOME_DIR/state/infra.meta"

say "fm-spawn.sh infra --secondmate   # relaunch (recovery / updatefirstmate path)"
spawn infra

say "fm-spawn.sh infra --secondmate --model sonnet   # explicit flag outranks the record"
spawn infra --model sonnet

say "fm-home-seed.sh infra <home> --no-projects   # re-seed must NOT reset the pin"
seed infra "$W/infra" --no-projects
grep '^- infra ' "$HOME_DIR/data/secondmates.md"

say "fm-home-seed.sh runtime infra effort=turbo   # fail-closed at edit time"
seed runtime infra effort=turbo; printf 'exit=%s\n' "$?"
say "fm-home-seed.sh runtime infra harness=muse   # unverified adapter"
seed runtime infra harness=muse; printf 'exit=%s\n' "$?"
say "fm-home-seed.sh runtime infra model=default   # reserved route sentinel"
seed runtime infra model=default; printf 'exit=%s\n' "$?"

say "record hand-edited to an unknown key -> refused at validate and before launch"
sed -i 's/harness: codex;/flavor: vanilla;/' "$HOME_DIR/data/secondmates.md"
grep '^- infra ' "$HOME_DIR/data/secondmates.md"
seed validate; printf 'validate exit=%s\n' "$?"
PATH="$FAKEBIN:$PATH" TMUX='' CLAUDECODE=1 FM_SPAWN_NO_GUARD=1 \
  FM_FAKE_LAUNCH_LOG="$W/launch-bad.log" fm "$ROOT/bin/fm-spawn.sh" infra --secondmate >"$W/badspawn.out" 2>&1
printf 'spawn exit=%s\n' "$?"
tail -1 "$W/badspawn.out"
[ -s "$W/launch-bad.log" ] && echo "LEAKED A LAUNCH" || echo "(no launch command was emitted)"

say "fm-fleet-snapshot.sh --json   # the malformed pin is reported, not shown as healthy"
fm "$ROOT/bin/fm-fleet-snapshot.sh" --json 2>/dev/null \
  | jq -c '.secondmate_current.registry.records[] | {id,home,registry_error}'

say "fm-fleet-snapshot.sh --json   # pinned mates keep a non-null home in the fleet"
sed -i 's/flavor: vanilla;/harness: codex;/' "$HOME_DIR/data/secondmates.md"
fm "$ROOT/bin/fm-fleet-snapshot.sh" --json 2>/dev/null \
  | jq -c '.secondmate_current.registry.records[] | {id,home,registry_error}'
Evidence: Targeted test results (per-mate runtime + snapshot + de-flaked push test)
ok - D1 spawn: two mates in one home each launch on their own recorded runtime, per axis
ok - D2 spawn: a relaunch preserves the recorded runtime instead of reverting to the home-wide pin
ok - D3 spawn: a registry written before this change parses, validates, and launches unchanged
ok - D4 spawn: an invalid recorded harness, effort, or model sentinel is refused loudly, at launch and at validation
ok - D5 spawn: a recorded harness suppresses the config tokens and explicit flags beat the record
ok - D6 registry: the runtime edit action sets, clears, and refuses per axis without disturbing the record
ok - D7 registry: a re-seed of an already-registered mate preserves its recorded runtime
ok - Domain Alpha structured state overrides a stale parent Phase 7 event
ok - runtime-bearing registry records keep home and remote placement in the snapshot
ok - registry records the launcher refuses carry a snapshot registry_error

--- test_config_reread_serializes_concurrent_pushes x3 (previously flaky) ---
ok - B21 config reread serializes concurrent propagation and delivery
ok - B21 config reread serializes concurrent propagation and delivery
ok - B21 config reread serializes concurrent propagation and delivery
Evidence: Remote secondmate lifecycle e2e log (partial — run still in progress at phase close)

ok - the remote route resolves each recorded runtime axis and refuses a malformed one

ok - overlapping remote home provisioning serializes through publication and rollback
ok - remote seed rollback preserves serialized competing routes
ok - unknown readiness preserves its route and brief for reconciliation
ok - remote seeding checks, repairs, and re-checks readiness, then stops on a remaining gap
ok - remote seeding proceeds once the repair closes every gap
ok - remote seeding provisions a supplied origin without touching the primary project tree
ok - remote provisioning re-validates a supplied origin at the receiving host
ok - seeding carries bitbucket, self-hosted, and scp-like origins through to the remote clone
ok - remote seed registers the route and provisions the whole home and project clone on that host
ok - remote inheritance rejects incomplete and superseded payload generations
ok - mixed local and remote routes validate without migration
ok - remote spawn launches on the remote-local backend and records a host-qualified route
ok - legacy and mismatched remote endpoints fail closed before backend access
ok - non-herdr remote endpoints are refused without changing either route
ok - the remote route resolves each recorded runtime axis and refuses a malformed one
ok - remote spawn serializes inheritance through launch publication
ok - marked send and routed reply complete through the existing parent correlation owner
PR_CHECK_MIGRATION: watcher ownership is ambiguous; review state/.watch.lock before rearming polls
ok - partial remote inheritance retains reread intent through bootstrap convergence
ok - config push and bootstrap serialize remote inheritance convergence
WARNING: queued wakes pending - drain them with bin/fm-wake-drain.sh before anything else.
ok - remote inherited config retains and retries a failed live reread nudge
Terminated
- Outcome: ⚠️ 1 info across 1 run (7m41s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

⏭️ **Rebase** - skipped

Push main to origin, or rebase your branch onto origin/main, before gating.

⚠️ **Review** - 2 infos
  • ⚠️ bin/fm-fleet-snapshot.sh:873 - The jq record regexes were widened from projects: [^;)]*;[[:space:]]*added to projects: [^;)]*;.*added, which accepts ANY text in the runtime segment, not just the vocabulary bin/fm-secondmate-registry-lib.sh honors. A record the launcher refuses now reads as healthy in the fleet and bearings snapshots. Concretely: - ios - x (home: /h; scope: s; projects: p; effort: turbo; added 2026-01-01) yields {home:&#34;/h&#34;, registry_error:null} from registry_secondmates_json, while fm-spawn.sh (both routes) and fm-home-seed.sh validate refuse it with "invalid recorded effort for ios: turbo". Before this diff that same record produced registry_error: &#34;registry entry has no home&#34; — a wrong reason, but at least visible. Suggest constraining the segment to the recognized keys and effort vocabulary instead of .*, e.g. (?:[[:space:]]*(?:harness|model):[^;)[:space:]]+;|[[:space:]]*effort:[[:space:]]*(?:low|medium|high|xhigh|max);)*[[:space:]]*added, so the second parser refuses what the owning parser refuses.
  • ⚠️ bin/fm-home-seed.sh:844 - write_registry carries the recorded runtime forward only when secondmate_registry_line_for_id succeeds. When the mate's existing record does not parse, kept_harness/kept_model/kept_effort stay empty, the grep -vE &#34;^- $id( |$)&#34; at line 849 strips the old line, and the mate is rewritten with no runtime segment — validate_registry then passes, so the re-seed succeeds silently and the mate reverts to config/secondmate-harness. This is the exact outcome the captain's decision forbids ("a re-seed ... PRESERVES its recorded harness/model/effort and must never silently reset it to the home-wide pin"), reached whenever the record was hand-edited into a refused state (e.g. effort: hihg;): every spawn fails loudly, then one re-seed to add a project quietly erases the pin and launches on the home-wide config. bin/fm-remote-home-seed.sh:217 has the identical shape. Fail-closed would be to refuse the re-seed when the id's existing line is present but unparsable, rather than repairing it by dropping the pin — that is a behavior choice, so flagging rather than fixing.
  • ℹ️ bin/fm-remote-secondmate-control.sh:141 - fm-spawn.sh's spawn_remote_secondmate now calls secondmate_registry_runtime_harness_ok instead of its own case, as intended. The remote-host boundary still keeps a literal third copy of the same seven-adapter list. Not a defect today (the lists are identical), and this script does not currently source the registry lib, so deduplicating it means adding a source line. Noting only because the registry now validates recorded harnesses against the lib's list at edit time: if the two ever diverge, a harness accepted and stored by fm-home-seed.sh runtime would pass parent-side validation and then die at the SSH boundary.

🔧 Fix: reject malformed runtime pins in fleet-snapshot registry parser
2 infos still open:

  • ℹ️ bin/fm-fleet-snapshot.sh:874 - The new $runtime_re is fail-safe but not exactly equivalent to bin/fm-secondmate-registry-lib.sh, so the comment at line 870 ("refuses exactly what the owning parser refuses") is slightly overstated. Two model values the lib accepts and both writers will store are refused by jq: (1) a model containing ')' — [^;)[:space:][:cntrl:]]+ excludes it while secondmate_registry_runtime_model_ok does not; (2) a model whose value contains the literal substring harness:/model:/effort: — the repeated-key negative lookaheads are substring-based, not key-position-based, so model: gpt-harness:x; harness: codex; trips (?!.*harness:.*harness:). Verified by running both parsers over the same inputs: fm-home-seed.sh runtime sm model=gpt)x is accepted, validates, and launches, but the record then reports registry_error "registry entry has an unreadable runtime pin" in the fleet and bearings snapshots. The direction is safe (snapshot stricter than launcher) and the values are unrealistic, so this is noted rather than a defect; no unsafe-direction divergence exists.
  • ℹ️ bin/fm-remote-home-seed.sh:216 - The captain's re-seed-preserves-runtime decision is proven for the local writer only (tests/fm-secondmate-harness.test.sh D7 test_registry_runtime_survives_reseed). The remote writer's KEPT_HARNESS/KEPT_MODEL/KEPT_EFFORT carry-forward at fm-remote-home-seed.sh:216-235 has no test; tests/fm-remote-secondmate-lifecycle-e2e.test.sh covers only remote runtime resolution at spawn, not a remote re-seed. The code is correct by inspection (it reads the record before the grep -vE strip and re-emits through secondmate_registry_render_line), so this is a coverage gap on a required behavior, not a defect.
⚠️ **Test** - 1 info
  • ℹ️ tests/fm-remote-secondmate-lifecycle-e2e.test.sh is a long linear e2e script with no per-test selector; the new remote-route runtime assertion inside it passed ("ok - the remote route resolves each recorded runtime axis and refuses a malformed one"), but the remainder of that file was still executing when this phase closed. Remote CI owns the full run.
  • bash tests/fm-secondmate-harness.test.sh subset: test_registry_runtime_per_mate, test_registry_runtime_survives_relaunch, test_registry_pre_change_record_unchanged, test_registry_runtime_invalid_values_refused, test_registry_runtime_precedence_edges, test_registry_runtime_edit_action, test_registry_runtime_survives_reseed — all 7 pass
  • bash tests/fm-bearings-snapshot.test.sh subset: test_registry_runtime_records_keep_placement, test_registry_malformed_runtime_is_not_healthy — both pass
  • bash tests/fm-remote-secondmate-lifecycle-e2e.test.sh — the new remote-route assertion passed: "ok - the remote route resolves each recorded runtime axis and refuses a malformed one"
  • test_config_reread_serializes_concurrent_pushes run 3x consecutively (the flaky test the branch rebounds to a 60s wall-clock deadline) — 3/3 pass
  • Manual CLI walkthrough /tmp/no-mistakes-evidence/01KZH07MZVBRCRN0DDNSEQ2FP6/demo-per-mate-runtime.sh: real fm-home-seed.sh &lt;id&gt; &lt;home&gt; --no-projects, fm-home-seed.sh runtime &lt;id&gt; harness=/model=/effort=, fm-spawn.sh &lt;id&gt; --secondmate (launch command captured off the pane), re-seed, invalid-value refusals, hand-broken record vs fm-home-seed.sh validate and spawn, and fm-fleet-snapshot.sh --json
  • git status --porcelain — worktree clean, no transient test files left behind
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

config/secondmate-harness pins one harness, model, and effort for every
secondmate a home launches, and fm-spawn re-resolves it on every --secondmate
spawn. A per-spawn --model/--effort therefore never survived a respawn:
recovery, /updatefirstmate, and any other relaunch silently reverted the choice
to the home-wide pin.

Record the runtime on the secondmate itself instead. A registry entry may now
carry optional harness:, model:, and effort: fields between projects: and added,
each axis independent, re-resolved on every spawn exactly the way home:, host:,
and root: already are - so the choice is durable with no new config file and no
rules engine.

Precedence per axis, strongest first: an explicit per-spawn flag, the mate's own
recorded field, then config/secondmate-harness - whose tokens a recorded harness
suppresses, because they were written against the config's own harness. Local
and remote routes resolve identically, and the remote route keeps refusing
anything but a verified adapter.

The segment fails closed: an unverified harness, an effort outside
low|medium|high|xhigh|max, an unusable model, an unknown or repeated key, or a
missing terminator makes the record malformed and refuses at edit time, at
fm-home-seed.sh validate, and again before any launch. A record with none of the
fields is the pre-existing form and behaves exactly as before.

fm-home-seed.sh runtime <id> <axis>=<value|-> is the create-time and change-time
path: it validates each value, takes the registry lock, validates the whole
rewritten registry, and leaves the record untouched if anything refuses.
test_config_reread_serializes_concurrent_pushes gave the backgrounded
fm-config-push.sh two seconds (100 x 0.02s) to start, discover the home,
converge its config, and publish a generation before the test declared "first
config push did not reach pointer delivery". On a loaded multi-lane or CI host
that push routinely needs longer, so a slow machine reported as a serialization
defect.

Wait on a 60-second wall-clock deadline instead. The assertion still fails
loudly when delivery genuinely never happens; it just stops treating scheduler
latency as a product failure.

Verified: the single-test driver failed on the first run before this change and
failed three times in a row on an unmodified checkout of the same commit, so the
flake pre-dated the per-secondmate runtime work; after the change it passed five
consecutive driver runs and the full suite passed with 53 assertions and no
failures.
The stock macOS Bash job pins the Bearings suite's assertion count, and this
branch adds two Bearings cases for the per-secondmate runtime, taking the suite
from 41 to 43. The guard tripped even though every test passed.

Measured on this branch: tests/fm-bearings-snapshot.test.sh emits 43 `ok - `
lines and tests/fm-fleet-snapshot-view.test.sh emits 15, so the sibling
snapshot_count guard is already correct and is left untouched.
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