feat(bin): give each secondmate its own durable runtime - #1960
Open
eduardstan wants to merge 14 commits into
Open
eduardstan wants to merge 14 commits into
eduardstan wants to merge 14 commits into
Conversation
This was referenced Aug 8, 2026
eduardstan
force-pushed
the
fm/secondmate-runtime-per-mate
branch
3 times, most recently
from
August 13, 2026 10:33
b0f03e0 to
c1447db
Compare
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.
…on chain in configuration docs
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.
…o field boundaries
eduardstan
force-pushed
the
fm/secondmate-runtime-per-mate
branch
from
August 14, 2026 11:09
c1447db to
d204dcf
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.shnow parses an optional per-mate runtime segment (harness:,model:,effort:, each independently optional) betweenprojects:andaddedon both the local and remote record forms. It fails closed: an unverified adapter, an effort outsidelow|medium|high|xhigh|max, an unusable model (including the-anddefaultroute 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 asecondmate_registry_render_linewriter boundary.bin/fm-spawn.shresolves each axis independently — explicit per-spawn--harness/--model/--effort, then the mate's recorded field, thenconfig/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.shgains aruntime <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_registrycarries 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 flakytest_config_reread_serializes_concurrent_pushesby 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}Evidence: Reproducible walkthrough script
Evidence: Targeted test results (per-mate runtime + snapshot + de-flaked push test)
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 onePipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
⏭️ **Rebase** - skipped
.agents/skills/afk/SKILL.md- branch carries 52 commit(s) that exist on your local main branch but were never pushed to origin/main; rebasing would bundle this unrelated work (211 file(s)) into the PR:Push main to origin, or rebase your branch onto origin/main, before gating.
bin/fm-fleet-snapshot.sh:873- The jq record regexes were widened fromprojects: [^;)]*;[[:space:]]*addedtoprojects: [^;)]*;.*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:"/h", 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 producedregistry_error: "registry entry has no home"— 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, thegrep -vE "^- $id( |$)"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 owncase, 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 substringharness:/model:/effort:— the repeated-key negative lookaheads are substring-based, not key-position-based, somodel: 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)xis 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.bash tests/fm-secondmate-harness.test.shsubset: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 passbash tests/fm-bearings-snapshot.test.shsubset:test_registry_runtime_records_keep_placement,test_registry_malformed_runtime_is_not_healthy— both passbash 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_pushesrun 3x consecutively (the flaky test the branch rebounds to a 60s wall-clock deadline) — 3/3 passManual CLI walkthrough/tmp/no-mistakes-evidence/01KZH07MZVBRCRN0DDNSEQ2FP6/demo-per-mate-runtime.sh: realfm-home-seed.sh <id> <home> --no-projects,fm-home-seed.sh runtime <id> harness=/model=/effort=,fm-spawn.sh <id> --secondmate(launch command captured off the pane), re-seed, invalid-value refusals, hand-broken record vsfm-home-seed.sh validateand spawn, andfm-fleet-snapshot.sh --jsongit 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.