Skip to content

feat(bin): reconcile the fork main line with current upstream history - #2

Merged
yelenplays merged 80 commits into
mainfrom
fm/firstmate-main-fork-reconciliation
Aug 10, 2026
Merged

yelenplays merged 80 commits into
mainfrom
fm/firstmate-main-fork-reconciliation

Conversation

@yelenplays

Copy link
Copy Markdown
Owner

Intent

Reconcile the captain's main Firstmate line with current upstream history and the captain's fork without touching the dirty primary copy or losing accepted history. Use the current captain fork yelenplays/firstmate as the only write target and upstream kunchenguid/firstmate as read-only. Current fetched heads are origin/main d989773 and upstream/main 85e750a; primary local main is eeb4d41. Preserve every genuinely unique local commit exactly once, including the 32 commits reachable from primary/main but absent from current origin/main, detect equivalent patches rather than duplicating them, and incorporate complete current upstream history. Preserve normalized Matt documentation from merged fork PR #1 and never restore superseded dirty primary sources. The reconciled branch intentionally has a GitHub merge commit with parents origin/main and primary/main, proving origin, upstream, and primary ancestry remain reachable; the final PR must be merged by GitHub's merge-commit method, never squash or rebase. Do not include portable shard refresh branch or commit 889cd89. Validate the final tree with documentation-audience checks, shell lint, and the complete relevant Firstmate tests. Include in PR evidence the remote heads, local commit inventory, duplicate/equivalence decisions, ancestry proof, normalized PR 1 proof, checks run, and exact post-merge path for cleaning the primary copy without losing history. Do not merge the PR.

What Changed

  • Reconciles three histories into one line: merge commit 1a6bdfa joins origin/main (d989773) with the captain's primary line (eeb4d41), carrying all 32 primary-only commits exactly once (28 unique by git cherry, 0 already-equivalent, no duplicate patch-ids), and cedc8f7 merges current upstream/main (85e750ab, 43 further commits) with 11 hand-resolved content conflicts across AGENTS.md, .agents/skills/harness-adapters/SKILL.md, bin/fm-brief.sh, bin/fm-spawn.sh, bin/fm-supervision-lib.sh, bin/fm-teardown.sh, docs/documentation-audiences.json, and the affected docs/tests. Zero files are deleted since base, and PR 1's normalized docs/agents/ content is byte-identical to base.
  • Lands 37 new bin/ entrypoints and libraries plus their docs, skills, and tests: the remote secondmate stack (fm-remote-job-lib.sh with its worker and orphan reaper, fm-remote-doctor.sh, fm-remote-home-provision.sh, fm-remote-entrypoint.sh), deterministic agent lifecycle control (fm-control.sh, fm-on.sh), per-task trace context (fm-trace-context-lib.sh), deferred startup networking (fm-startup-network.sh), the /stow cascade, the Muse Code crewmate adapter, and the Matt Pocock skills bridge behind the shared resolver bin/fm-skill-path.sh.
  • Applies gate fixes on top of the reconciled tree: fm-operational-input.sh sanitization now runs to a fixpoint instead of returning at an 8-iteration cap, fm-matt-skill.sh drops its unreachable fallback resolver while fm-matt-pointers.sh derives plugin identity from --list rather than a hardcoded tdd skill, fm-lock.sh keeps a single ownership predicate on the pre-claim fast path, and the matt loader and portable serial shard docs are refreshed to match the merged lane (103 serial scripts across 4 shards).

Risk Assessment

⚠️ Medium: Every source-verifiable acceptance criterion is now met and I found no defect, but this is a large reconciliation - 73 non-merge commits, two merge commits, ~170 upstream-changed files, and 11 hand-resolved conflicts in load-bearing scripts - whose residual risk sits in the test, lint, and documentation-audience phases that have not run yet.

Testing

I validated the reconciliation as a git-history product: a proof transcript exercises every acceptance constraint (remote heads and write target, ancestry of origin/upstream/primary, both merge commits and their parents, the 32-commit local inventory, duplicate/equivalence decisions via git cherry and a patch-id scan, normalized PR 1 preservation, no superseded dirty-primary sources restored, and the 889cd89 / portable shard refresh exclusion) - all pass. On the resulting tree I ran the documentation-audience check, the test lane coverage guard, and 18 targeted test scripts selected from the actual hand-edited surface (the combined diffs of both merges plus the two gate-fix commits); 16 pass. The two failures - fm-decision-hold-lifecycle (host tasks-axi 0.2.3 below the upstream-supplied 0.2.4 floor) and fm-calm-pi-extension (Pi 0.84.1 renderer API drift inside the installed pi-coding-agent) - reproduce identically against a pristine extract of upstream 85e750a, so both are host toolchain issues rather than defects in this change, and fixing either would require a global package change outside the worktree boundary. No screenshot or rendered-HTML artifact applies: this change touches git history and shell tooling only and has no rendered end-user surface, so the evidence is CLI transcripts. The worktree is clean and the temporary upstream baseline tree was removed.

Evidence: Reconciliation proof: remote heads, ancestry, 32-commit inventory, equivalence decisions, PR 1 preservation, exclusions

=== 2. ANCESTRY PROOF (origin + upstream + primary reachable from HEAD) === REACHABLE origin/main d989773601e09f73ac63771f47b3a5b487c54bb5 REACHABLE upstream/main 85e750ab9b76df275c1f6b9e2bc95b671955bae9 REACHABLE primary/main eeb4d4142b63682701937e019ca8a93a9bdcfbb3 merge commit whose parents are origin/main and primary/main: commit 1a6bdfa42469be306810e48a5979eb1a01bfc4a4 parents d989773601e09f73ac63771f47b3a5b487c54bb5 eeb4d4142b63682701937e019ca8a93a9bdcfbb3 subject Merge remote-tracking branch 'primary/main' into fm/firstmate-main-fork-reconciliation merge commit that incorporates complete current upstream history: commit cedc8f7f1edac31d7d6792d0f7ef556f9a2d7cb4 parents cf0ca426e11b8aca4f4b7b726a1f796a2e6ba90e 85e750ab9b76df275c1f6b9e2bc95b671955bae9 subject merge: incorporate current upstream history 85e750ab === 3. LOCAL COMMIT INVENTORY (in primary/main, absent from origin/main) === count: 32 (intent requires 32) non-merge: 28 merge: 4 === 4. DUPLICATE / EQUIVALENCE DECISIONS === unique(+)=28 already-equivalent(-)=0 (git cherry excludes merge commits) decision: no primary patch was equivalent to an origin patch, so all were preserved via merge (no cherry-pick duplication). each primary-only commit appears EXACTLY ONCE in the reconciled history: OK - all 32 present exactly once, none duplicated patch-id duplication scan across everything HEAD adds over origin/main: OK - no two commits introduce the same patch === 5. NORMALIZED PR 1 DOCUMENTATION PRESERVED === fork PR 1 landed as d989773601e09f73ac63771f47b3a5b487c54bb5 "docs: add installed-skill guides for issues, triage labels, and domain docs (#1)" BYTE-IDENTICAL docs/agents/domain.md (blob 765ae9f91099b9daa7064672fabfdc341dbade32) BYTE-IDENTICAL docs/agents/issue-tracker.md (blob 801f6b3ade3333604734f377adaa791059a3535b) BYTE-IDENTICAL docs/agents/triage-labels.md (blob 142c278f02ae247ee74a1a9716ffaf6d45ededf7) PRESENT .agents/skills/harness-adapters/SKILL.md :: User-installed skills are not part of Firstmate's bundled adapter guarantee. PRESENT AGENTS.md :: issue tracker PRESENT AGENTS.md :: triage labels PRESENT AGENTS.md :: domain documentation PRESENT docs/documentation-audiences.json :: "path": "docs/agents/issue-tracker.md" PRESENT docs/documentation-audiences.json :: "path": "docs/agents/triage-labels.md" PRESENT docs/documentation-audiences.json :: "path": "docs/agents/domain.md" === 6. EXCLUSIONS HONORED === OK commit 889cd89 is absent from this repository entirely refs mentioning "portable" / "shard refresh": (none) branch subjects mentioning portable shard refresh in HEAD history: (none) === 7. WORKING TREE STATE === clean - no stray build or test artifacts left behind

=== 1. REMOTE HEADS / WRITE TARGET ===
origin	https://github.com/yelenplays/firstmate.git (fetch)
origin	https://github.com/yelenplays/firstmate.git (push)

fetched heads under test:
  origin/main    d989773601e09f73ac63771f47b3a5b487c54bb5
  upstream/main  85e750ab9b76df275c1f6b9e2bc95b671955bae9  (read-only)
  primary/main   eeb4d4142b63682701937e019ca8a93a9bdcfbb3  (dirty local copy, read-only)
  branch head    7e3f633e106a8ec925d7e902168f9a2c821b7fcf  fix(lock): keep one ownership predicate on the pre-claim fast path

=== 2. ANCESTRY PROOF (origin + upstream + primary reachable from HEAD) ===
  REACHABLE  origin/main    d989773601e09f73ac63771f47b3a5b487c54bb5
  REACHABLE  upstream/main  85e750ab9b76df275c1f6b9e2bc95b671955bae9
  REACHABLE  primary/main   eeb4d4142b63682701937e019ca8a93a9bdcfbb3

  merge commit whose parents are origin/main and primary/main:
    commit  1a6bdfa42469be306810e48a5979eb1a01bfc4a4
    parents d989773601e09f73ac63771f47b3a5b487c54bb5 eeb4d4142b63682701937e019ca8a93a9bdcfbb3
    subject Merge remote-tracking branch 'primary/main' into fm/firstmate-main-fork-reconciliation

  merge commit that incorporates complete current upstream history:
    commit  cedc8f7f1edac31d7d6792d0f7ef556f9a2d7cb4
    parents cf0ca426e11b8aca4f4b7b726a1f796a2e6ba90e 85e750ab9b76df275c1f6b9e2bc95b671955bae9
    subject merge: incorporate current upstream history 85e750ab

=== 3. LOCAL COMMIT INVENTORY (in primary/main, absent from origin/main) ===
  count: 32 (intent requires 32)
    eeb4d41 merge: reconcile the local line with upstream a83be60
    a83be60 feat: route remote commands through an Aqua job worker (#1660)
    c8edff3 fix: isolate remote secondmates in shared Herdr session (#1659)
    e5e8a67 feat: gate remote second mates on Herdr readiness (#1639)
    733a504 feat(bin): preflight remote runtime tool paths (#1623)
    3d9d12d feat(bin): propagate trace context to remote secondmates (#1609)
    cf95112 fix(bin): harden tmux agent liveness across harnesses (#1577)
    976d97f feat: add per-task trace context propagation (#995)
    7809ab9 feat(bin): support remote secondmate homes (#1576)
    4ee4a0a feat(bin): require an explicit per-task delivery contract (#1563)
    33a4287 fix(bin): harden Claude supervision auto-arm recovery (#1495)
    88b2a94 fix(bin): correct session lock and attached watcher supervision (#1545)
    f5ab708 perf: shard portable serial tests across CI runners (#1544)
    638cc91 merge: land the cross-runtime Matt Pocock pointers on the reconciled line
    ec875a9 merge: land the Matt Pocock skills bridge (Phase 1) on the reconciled line
    4cb82eb fix(tests): stop the pi-signed identity case inheriting the runner's harness markers
    f877ce8 merge: bring upstream 25 commits into the local line, keeping all 12 local
    634d97f feat(skills): reach the Matt Pocock skills from non-Claude runtimes
    ddb5331 docs(verification): date the stale-copy collision evidence
    99679ee feat(skills): bridge Firstmate to the pinned Matt Pocock skills (Phase 1)
    bb1e86c test: make the jobs scheduler refill proof ordered instead of timed
    6ffc69c fix(kimi): reclaim marker-stripped hooks and deliver K3 effort on 0.31
    2a9729d fix(wake): stop the drain dropping distinct check results that share a key
    9201c1f fix(security): sanitize operational provenance on external message ingress
    a44e691 fix(supervision): guard channel-armed idle homes on every primary harness
    18823b0 fix(spawn): merge the Claude turn-end hook into a project's own settings
    7593db5 feat(decisions): add the structured decision record
    40b60f1 fix(brief): name the PR gate before the commit in no-mistakes briefs
    8dab99c no-mistakes(document): note peer-runtime ancestry budgets in session-lock lib header
    22512a2 no-mistakes(review): mint the session, not a shared harness ancestor
    52906a2 docs: record session identity evidence under Claude Code 2.1.220
    a97b49f fix: answer session ownership as identity, not harness category
  non-merge: 28   merge: 4

=== 4. DUPLICATE / EQUIVALENCE DECISIONS ===
  git cherry origin/main primary/main:  "+" = unique patch to keep, "-" = patch already equivalent in origin/main
  unique(+)=28  already-equivalent(-)=0  (git cherry excludes merge commits)
  decision: no primary patch was equivalent to an origin patch, so all were preserved via merge (no cherry-pick duplication).

  each primary-only commit appears EXACTLY ONCE in the reconciled history:
    OK - all 32 present exactly once, none duplicated

  patch-id duplication scan across everything HEAD adds over origin/main:
    OK - no two commits introduce the same patch

=== 5. NORMALIZED PR 1 DOCUMENTATION PRESERVED ===
  fork PR 1 landed as d989773601e09f73ac63771f47b3a5b487c54bb5 "docs: add installed-skill guides for issues, triage labels, and domain docs (#1)"
  BYTE-IDENTICAL  docs/agents/domain.md (blob 765ae9f91099b9daa7064672fabfdc341dbade32)
  BYTE-IDENTICAL  docs/agents/issue-tracker.md (blob 801f6b3ade3333604734f377adaa791059a3535b)
  BYTE-IDENTICAL  docs/agents/triage-labels.md (blob 142c278f02ae247ee74a1a9716ffaf6d45ededf7)

  PR-1 lines added to shared files still present in the reconciled tree:
  PRESENT  .agents/skills/harness-adapters/SKILL.md :: User-installed skills are not part of Firstmate's bundled adapter guaran
  PRESENT  .agents/skills/harness-adapters/SKILL.md :: report the blocker rather than omitting the requirement or substituting 
  PRESENT  AGENTS.md :: [issue tracker](docs/agents/issue-tracker.md)
  PRESENT  AGENTS.md :: [triage labels](docs/agents/triage-labels.md)
  PRESENT  AGENTS.md :: [domain documentation](docs/agents/domain.md)
  PRESENT  docs/documentation-audiences.json :: "target": "docs/agents/issue-tracker.md"
  PRESENT  docs/documentation-audiences.json :: "path": "docs/agents/issue-tracker.md"
  PRESENT  docs/documentation-audiences.json :: "target": "docs/agents/triage-labels.md"
  PRESENT  docs/documentation-audiences.json :: "path": "docs/agents/triage-labels.md"
  PRESENT  docs/documentation-audiences.json :: "target": "docs/agents/domain.md"
  PRESENT  docs/documentation-audiences.json :: "path": "docs/agents/domain.md"

=== 6. EXCLUSIONS HONORED ===
  OK  commit 889cd89 is absent from this repository entirely
  refs mentioning "portable" / "shard refresh":
    (none)
  branch subjects mentioning portable shard refresh in HEAD history:
    (none)

=== 7. WORKING TREE STATE ===
  clean - no stray build or test artifacts left behind
Evidence: No superseded dirty-primary source restored + ref inventory

=== 8. NO SUPERSEDED DIRTY-PRIMARY SOURCE RESTORED === files touched by the accepted local line + PR 1: 152 files where reconciled tree differs from upstream/main: 58 OK - every divergence from upstream is owned by an accepted local commit or fork PR 1; no file was reverted to a superseded dirty-primary version. reverse check - files where the reconciled tree silently dropped an upstream update: OK - no upstream update was reverted to the pre-merge origin content === 9. REF INVENTORY (no portable shard refresh branch) === refs/heads/fm/firstmate-main-fork-reconciliation cf0ca42 refs/remotes/origin/main d989773

=== 8. NO SUPERSEDED DIRTY-PRIMARY SOURCE RESTORED ===
Every file whose reconciled content differs from current upstream must be
explained by a local-only commit (primary/main not in origin/main) or by fork PR 1.

files touched by the accepted local line + PR 1: 152
files where reconciled tree differs from upstream/main: 58

  OK - every divergence from upstream is owned by an accepted local commit or fork PR 1;
       no file was reverted to a superseded dirty-primary version.

  reverse check - files where the reconciled tree silently dropped an upstream update:
    OK - no upstream update was reverted to the pre-merge origin content

=== 9. REF INVENTORY (no portable shard refresh branch) ===
  refs/heads/fm/firstmate-main-fork-reconciliation cf0ca42
  refs/remotes/origin/main d989773
Evidence: Targeted tests set 1 - docs audience, Matt pointers, operational input, sessionstart nudge, session lock ancestry

FM_TEST_END tests/fm-documentation-audiences.test.sh exit=0 duration_ms=1007 FM_TEST_END tests/fm-matt-pointers.test.sh exit=0 duration_ms=9512 FM_TEST_END tests/fm-operational-input.test.sh exit=0 duration_ms=736 FM_TEST_END tests/fm-sessionstart-nudge.test.sh exit=0 duration_ms=11466 FM_TEST_END tests/fm-session-lock-ancestry.test.sh exit=0 duration_ms=4079 FM_TEST_SUMMARY total=5 failed=0 skipped_gate=0 duration_ms=26965

FM_TEST_BEGIN 2026-08-10T14:45:07Z tests/fm-documentation-audiences.test.sh family=pure-contract-unit expected_gate_skip=none
ok - documentation inventory classifies every maintained prose surface exactly once
ok - classification, setup routing, and maintained-prose scope fail safely
ok - required documentation owner pointers cannot silently disappear
ok - local links resolve while dates, versions, commands, and incident prose remain semantically reviewed
FM_TEST_END 2026-08-10T14:45:08Z tests/fm-documentation-audiences.test.sh exit=0 duration_ms=1007 gate_skip=false
FM_TEST_BEGIN 2026-08-10T14:45:08Z tests/fm-matt-pointers.test.sh family=unclassified expected_gate_skip=none
ok - loader prints the installed original with full attribution and identity
ok - loader refuses every unsafe install state and prints nothing on stdout
ok - version drift is reported loudly, and refused outright on request
ok - the loader delegates to the shared resolver and inherits its refusals
ok - the loader refuses when the shared resolver is not installed beside it
ok - each pointer credits the original, dates itself, and copies no procedure
ok - pointers mirror each upstream skill's invocation policy
ok - a pointer whose load fails instructs a hard stop, not a fallback
ok - --check catches edited, missing, and inert pointers
ok - unmarked directories survive install and uninstall untouched
ok - retired pointers are pruned and repeat installs change nothing
FM_TEST_END 2026-08-10T14:45:17Z tests/fm-matt-pointers.test.sh exit=0 duration_ms=9512 gate_skip=false
FM_TEST_BEGIN 2026-08-10T14:45:17Z tests/fm-operational-input.test.sh family=pure-contract-unit expected_gate_skip=none
ok - operational input: every current generic envelope retains its exact structured kind
ok - operational input: the established from-firstmate carrier remains structurally typed and byte-compatible
ok - operational input: a sanitized external body can no longer assert internal provenance
ok - operational input: the sanitizer is inert on legitimate bodies
ok - operational input: JSON ingress sanitizes every string at any depth and stays inert otherwise
ok - operational input: untyped landed FIRSTMATE_OP transcripts are explicit legacy-operational input
ok - operational input: historical prose compatibility is isolated from current parsing
ok - operational input: quoted, ASCII-only, arbitrary-U+2063, altered-legacy, and label-only near misses stay genuine
ok - operational input: the OpenCode adapter constructs through the canonical owner
ok - operational input: current construction rejects legacy kinds and empty bodies
FM_TEST_END 2026-08-10T14:45:18Z tests/fm-operational-input.test.sh exit=0 duration_ms=736 gate_skip=false
FM_TEST_BEGIN 2026-08-10T14:45:18Z tests/fm-sessionstart-nudge.test.sh family=session-bootstrap expected_gate_skip=none
ok - fm-sessionstart-nudge: a genuine primary gets one explicitly marked instruction line
ok - fm-sessionstart-nudge: NO_MISTAKES_GATE is silent
ok - fm-sessionstart-nudge: .no-mistakes gate common-dir is silent
ok - fm-sessionstart-nudge: an unmarked linked task worktree is silent
ok - fm-sessionstart-nudge: a marked linked secondmate home is a primary
ok - fm-sessionstart-nudge: a checkout without state is silent
ok - fm-sessionstart-nudge: a lock holder in process ancestry is already run
ok - fm-sessionstart-nudge: an owned lock stays silent from behind the harness's own helper
ok - OpenCode session.created delivers the exact wrapper nudge once per session
ok - run wrapper: startup runs the full digest and never also nudges
ok - run wrapper: clear and compact re-emit the digest without repeating startup sweeps
ok - run wrapper: clear falls back to full startup when completion is unproven
ok - run wrapper: clear accepts completion only from the current harness
ok - run wrapper: resume delegates to the nudge instead of re-running the digest
ok - run wrapper: the hook payload's source field drives routing with no explicit argument
ok - run wrapper: an unrecognized or absent source takes the helm rather than skipping it
ok - run wrapper: a gate agent and an unmarked task worktree never run a session start
ok - run wrapper: a session start that cannot take the lock still opens the session and says so
ok - Pi retains a bounded digest prefix and loudly marks oversized delivery
FM_TEST_END 2026-08-10T14:45:29Z tests/fm-sessionstart-nudge.test.sh exit=0 duration_ms=11466 gate_skip=false
FM_TEST_BEGIN 2026-08-10T14:45:29Z tests/fm-session-lock-ancestry.test.sh family=watcher-wake-lock expected_gate_skip=none
ok - session-lock: a version-named Claude Code session is identified from its install path and argv[0]
ok - session-lock: ordinary script paths under a harness directory are not harness processes
ok - session-lock: ownership stops at the first non-harness gap above the contiguous run
ok - session-lock: a live version-named session holding the lock is not mistaken for a stale owner
ok - session-lock e2e: a version-named session claims the home and arms supervision
ok - session-lock e2e: a session parented by a harness-named daemon claims the home and arms supervision
ok - session-lock e2e: a version-named session under a harness-named daemon keeps its own lock
FM_TEST_END 2026-08-10T14:45:34Z tests/fm-session-lock-ancestry.test.sh exit=0 duration_ms=4079 gate_skip=false
FM_TEST_SUMMARY total=5 failed=0 skipped_gate=0 duration_ms=26965
FM_TEST_SUMMARY_FAMILY family=pure-contract-unit count=2 duration_ms=1743 failed=0
FM_TEST_SUMMARY_FAMILY family=session-bootstrap count=1 duration_ms=11466 failed=0
FM_TEST_SUMMARY_FAMILY family=unclassified count=1 duration_ms=9512 failed=0
FM_TEST_SUMMARY_FAMILY family=watcher-wake-lock count=1 duration_ms=4079 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-sessionstart-nudge.test.sh duration_ms=11466
FM_TEST_SLOWEST rank=2 script=tests/fm-matt-pointers.test.sh duration_ms=9512
FM_TEST_SLOWEST rank=3 script=tests/fm-session-lock-ancestry.test.sh duration_ms=4079
FM_TEST_SLOWEST rank=4 script=tests/fm-documentation-audiences.test.sh duration_ms=1007
FM_TEST_SLOWEST rank=5 script=tests/fm-operational-input.test.sh duration_ms=736
Evidence: Targeted tests set 2 - merge conflict-resolution surfaces (brief, guard banner, turnend guard, x-mode, test-run, skill path)

FM_TEST_END tests/fm-brief.test.sh exit=0 duration_ms=1527 FM_TEST_END tests/fm-guard-stale-banner.test.sh exit=0 duration_ms=3989 FM_TEST_END tests/fm-turnend-guard.test.sh exit=0 duration_ms=39746 FM_TEST_END tests/fm-x-mode.test.sh exit=0 duration_ms=50380 FM_TEST_END tests/fm-test-run.test.sh exit=0 duration_ms=16228 FM_TEST_END tests/fm-skill-path.test.sh exit=0 duration_ms=2156 FM_TEST_SUMMARY total=6 failed=0 skipped_gate=0 duration_ms=114225

FM_TEST_BEGIN 2026-08-10T14:45:48Z tests/fm-brief.test.sh family=pure-contract-unit expected_gate_skip=none
ok - fm-brief.sh: bash -n succeeds
/var/folders/9d/8w50jhgd79x63rgbq_5cyyvm0000gn/T//fm-brief.rRBSxh/heredoc-in-substitution.sh:2
ok - fm-brief.sh: no heredoc is nested inside a command substitution (Bash 3.2 parse-safe)
ok - fm-brief.sh: --help renders the complete header
ok - fm-brief.sh: no-mistakes/direct-PR/local-only briefs generate cleanly
ok - fm-brief.sh: ship --mode is required and closed-set validated
ok - fm-brief.sh: the explicit ship mode wins over the registered posture
ok - fm-brief.sh: --yolo and scout/secondmate --mode are refused, never silently dropped
ok - fm-brief.sh: faster paths use configured authority without stacked review
ok - fm-brief.sh: no-mistakes DOD keeps its apostrophe prose, now parse-safe
ok - fm-brief.sh: no-mistakes DOD names the PR gate first and the commit as intermediate
ok - fm-brief.sh: ship project-memory wording carries the AGENTS.md authoring bar
ok - fm-brief.sh: --herdr-lab emits the complete hard safety contract
ok - fm-brief.sh: --herdr-lab uses its quoted Firstmate-owned helper path
ok - fm-brief.sh: ship and scout scaffolds make omitted Herdr intent fail-visible
ok - fm-brief.sh: Herdr lab contract covers scouts and rejects secondmate misuse
ok - fm-brief.sh: --no-projects scaffolds a project-less charter and guards misuse
ok - fm-brief.sh: marked requests avoid generic acknowledgements and preserve material reporting
ok - fm-brief.sh: relative directory inputs ignore CDPATH, render stable absolute charter paths, or fail loudly
ok - fm-brief.sh: custom pause verb renders in every scaffold
ok - fm-brief.sh: investigation and visual-review completions load the shared decision policy
ok - fm-brief.sh: generated briefs carry the third-party skill safety rules per task shape
ok - fm-brief: scout and secondmate code paths still scaffold well-formed briefs
FM_TEST_END 2026-08-10T14:45:49Z tests/fm-brief.test.sh exit=0 duration_ms=1527 gate_skip=false
FM_TEST_BEGIN 2026-08-10T14:45:49Z tests/fm-guard-stale-banner.test.sh family=watcher-wake-lock expected_gate_skip=none
ok - fm-guard stale banner: first stale call prints the full actionable banner
ok - fm-guard: a channel-armed idle home is guarded, an unarmed idle home stays silent
ok - fm-guard stale banner: repeated same-episode calls print a concise reminder only
ok - fm-guard stale banner: auto-arm fresh beacon without a live watcher is healthy
ok - fm-guard stale banner: auto-arm stale beacon alarms with the true reason
ok - fm-guard stale banner: auto-arm stale episode stays one episode across calls
ok - fm-guard stale banner: persistent no-watcher banner names the true reason
ok - fm-guard stale banner: a no-watcher episode survives a beacon mtime change
ok - fm-guard stale banner: a fresh beacon without a live watcher remains unhealthy
ok - fm-guard stale banner: X-mode polling without a live watcher remains unhealthy
ok - fm-guard stale banner: healthy recovery rearms the next stale episode
ok - fm-guard stale banner: concurrent same-episode calls claim exactly one full banner
ok - fm-guard stale banner: deduplication is isolated per FM_HOME
ok - fm-guard stale banner: queued-wake warning remains independent
ok - fm-guard stale banner: read-only before writable does not consume full banner
ok - fm-guard stale banner: read-only during episode observes without mutating marker
ok - fm-guard stale banner: healthy read-only does not clear marker
ok - fm-guard stale banner: read-only never mutates stale-banner state files
FM_TEST_END 2026-08-10T14:45:53Z tests/fm-guard-stale-banner.test.sh exit=0 duration_ms=3989 gate_skip=false
FM_TEST_BEGIN 2026-08-10T14:45:53Z tests/fm-turnend-guard.test.sh family=watcher-wake-lock expected_gate_skip=none
ok - fm_supervision_unhealthy: false with no state/*.meta at all
ok - fm_supervision_unhealthy: true with in-flight task and no beacon ever
ok - fm_supervision_unhealthy: true with in-flight task and a beacon far outside the grace window
ok - fm_supervision_unhealthy: false with in-flight task and a fresh beacon
ok - fm_supervision_status: FM_SUP_QUEUE_PENDING tracks state/.wake-queue
ok - fm_supervision_needed: X-mode relay poll needs supervision
ok - fm_supervision_unhealthy: source-only home needs supervision
ok - fm-turnend-guard: silent no-op with nothing in flight
ok - fm-turnend-guard: blocks when a fresh beacon has no live watcher lock
ok - fm-turnend-guard: non-Claude path blocks a source-only home
ok - fm-turnend-guard: blocks on a dead watcher lock even when the beacon is fresh
ok - fm-turnend-guard: silent no-op with a live watcher lock and fresh beacon
ok - fm-turnend-guard: healthy non-Claude harness paths ignore Claude episode contention
ok - fm-turnend-guard: blocks on a live watcher lock with an ancient beacon
ok - fm-turnend-guard: blocks with the exact required reason in the primary when unhealthy
ok - fm-turnend-guard: blocks from active FM_HOME state, not only repo-root state
ok - fm-turnend-guard: X-mode repair reason sources the cadence config
ok - fm-turnend-guard: X-mode-only supervision remains guarded in default mode
ok - fm-turnend-guard: ignores stale repo-root state when FM_HOME is set
ok - fm-turnend-guard: uses FM_STATE_OVERRIDE ahead of FM_HOME/state
ok - fm-turnend-guard: stop_hook_active=true always allows the stop (never blocks twice in one turn)
ok - fm-turnend-guard: blocks a blind turn end in a secondmate's own home (.fm-secondmate-home no longer excludes it)
ok - fm-turnend-guard: idle-by-default - silent in a secondmate home with nothing in flight
ok - fm-turnend-guard: stop_hook_active=true allows the stop in a secondmate home (never blocks twice in one turn)
ok - fm-turnend-guard: secondmate deferred-death recovery - silent while watched, forces re-arm once the watcher exits
ok - fm-turnend-guard: inert in a secondmate's own child worktree (linked git worktree) even when unhealthy
ok - fm-turnend-guard: blocks a blind turn end in a treehouse-leased LINKED secondmate home (marker force-include)
ok - fm-turnend-guard: an invalid (empty) marker cannot spoof inclusion; linked worktree stays exempt
ok - fm-turnend-guard: a non-ASCII marker cannot spoof inclusion; linked worktree stays exempt
ok - fm-turnend-guard: inert in a crewmate/scout task worktree (linked git worktree) even when unhealthy
ok - fm-turnend-guard: fails open (never blocks) when jq is missing
ok - fm-turnend-guard: silent no-op on empty stdin
ok - fm-turnend-guard: runs well under the generous timing margin (0s)
ok - fm-turnend-guard-grok: forces one explicitly marked same-session resume when the shared predicate blocks
ok - fm-turnend-guard-grok: legacy environment loop guard prevents a nested resume loop
ok - fm-turnend-guard-grok: native false delegates blocking feedback with zero resume processes
ok - fm-turnend-guard-grok: native true remains bounded and starts no resume process
ok - fm-turnend-guard-grok: both spellings are typed and camelCase has deterministic precedence
ok - fm-turnend-guard-grok: malformed, invalidly typed, and missing-prerequisite payloads start neither path
ok - fm-turnend-guard-grok: missing jq and no-supervision-needed stops stay silent and bounded
ok - tracked .claude/settings.json entries: 5 inert under grok, the documented subagent exception still armed, all live under Claude
ok - .codex/hooks.json: Stop hook uses hook process root when payload cwd is outside
ok - .codex/hooks.json: Stop hook ignores nested git root guard scripts
ok - .opencode primary plugin: guard path is anchored to worktree, not directory
ok - .pi primary extension: no-tool and multi-tool runs each inject exactly one guard follow-up
ok - .pi primary extension: delivery failure resets the logical-run latch
ok - fm-turnend-guard --claude: re-blocks a loop-guarded stop while unhealthy and unclaimed (incident regression)
ok - fm-turnend-guard --claude: X-mode-only homes re-block when auto-arm recovery is absent
ok - fm-turnend-guard --claude: a live arming epoch advances once and repeated observation is idempotent
ok - fm-turnend-guard --claude: repeated failed-to-arming races make bounded m

... [4788 bytes truncated] ...

- fm-x-reply maps a followup_unavailable follow-up 409 to exit 9
ok - fm-x-reply maps every follow-up 409 to exit 9 even without the marker
ok - fm-x-reply treats answer-endpoint 409 as a generic failure
ok - fm-x-reply --followup --image posts an image object
ok - fm-x-reply --followup is accepted in any position and leaves the answer path default
ok - fm-x-reply --followup dry-run marks the endpoint without changing the answer path
ok - fm-x-reply --followup auto-splits a long follow-up into a marked thread
ok - fm-x-reply followup dry-run keeps endpoint marker and compact image metadata
ok - fm-x-poll records the durable per-request reply context from the relay payload
ok - context registry publishes records only through private guarded artifacts
ok - context registry reads only private single-link artifacts
ok - private artifact publisher is compatible with the system bash path
ok - context registry retention is bounded to the seven-day follow-up window
ok - context registry rewrites preserve the first-seen timestamp
ok - context retention starts only when a live initial answer succeeds
ok - a delayed Discord follow-up stays one message after inbox cleanup via the durable registry
ok - an X follow-up over 280 still splits correctly after inbox cleanup
ok - every unresolved follow-up is refused before posting
ok - a partial registry platform combines with the relay's authoritative budget
ok - concurrent requests each recover their own platform/budget with no cross-overwrite
ok - fm-x-dismiss clears the durable per-request context (a dismissed mention gets no follow-up)
ok - fm-x-dismiss posts a request-bound dismiss and echoes only the request_id
ok - fm-x-dismiss dry-run records the would-be body and never posts
ok - fm-x-dismiss dry-run works without a token
ok - fm-x-dismiss dry-run publishes outbox records only through private guarded artifacts
ok - fm-x-dismiss exits non-zero on a non-2xx relay response
ok - fm-x-dismiss exits non-zero on a transport failure
ok - fm-x-dismiss rejects an unsafe request_id (path-traversal guard)
ok - fm-x-dismiss rejects missing or extra arguments with a usage error
ok - fm-x-link records and refreshes the X-request link without disturbing meta
ok - fm-x-link records Discord platform context so follow-ups keep the Discord budget
ok - fm-x-link resolves the platform by request_id so a post-cleanup link keeps the Discord budget
ok - fm-x-link warns loudly and the follow-up is held (not wrongly split) when the platform is unknown
ok - fm-x-link paired carry flags preserve a prior task's follow-up binding onto a successor
ok - fm-x-link recovery relink preserves Discord platform context after inbox drain
ok - fm-x-link rejects malformed or unpaired carry flags
ok - meta rewrites are independent of TMPDIR
ok - fm-x-link rejects unsafe ids, missing meta, and missing arguments
ok - fm-x-followup --check reports postable / not-linked correctly
ok - fm-x-followup --check prunes a link past the 7-day window
ok - fm-x-followup --check prunes a link that already reached the follow-up cap
ok - fm-x-followup posts a follow-up, increments the counter, and keeps the link under the cap
ok - fm-x-followup --final clears the link after one post regardless of the remaining count
ok - fm-x-followup clears the link once the third follow-up reaches the cap
ok - fm-x-followup --image forwards the attachment through fm-x-reply --followup
ok - fm-x-followup keeps the link and counter when the post fails
ok - fm-x-followup tombstones the link when a post-success counter write fails
ok - fm-x-followup treats a relay cap/window rejection as an already-exhausted link, not a retry
ok - fm-x-followup skips silently and clears the link past the 7-day window
ok - fm-x-followup is a no-op for a task with no X link
ok - fm-x-followup dry-run records the follow-up and increments the counter, keeping the link
ok - fm-x-followup dry-run --final clears the link just as a live post would
ok - fm-x-followup rejects malformed invocations
ok - bootstrap activates X mode from an .env token, idempotently
ok - bootstrap ignores CDPATH when writing absolute FM_HOME into the durable X-mode poll shim
ok - bootstrap reports missing X-mode dependencies before arming
ok - bootstrap does not report X mode on when activation artifacts cannot be written
ok - bootstrap rejects linked X artifacts without touching their targets
ok - bootstrap is inert without a non-empty .env token (non-X users unaffected)
ok - bootstrap cleans up X artifacts on opt-out and is silent once off
ok - bootstrap reports failed X artifact cleanup on opt-out
FM_TEST_END 2026-08-10T14:47:23Z tests/fm-x-mode.test.sh exit=0 duration_ms=50380 gate_skip=false
FM_TEST_BEGIN 2026-08-10T14:47:23Z tests/fm-test-run.test.sh family=pure-contract-unit expected_gate_skip=none
ok - exact suite coverage: --all lists every tests/*.test.sh once
ok - family selection returns a proper subset of the suite
ok - single-script selection lists exactly that path
ok - changed-file selection stays conservative (never silent full suite)
ok - changed selection covers dependents and fails closed for unmapped source
ok - empty changed selection emits deterministic text and JSON summaries
ok - timing markers and JSON artifact are valid
ok - aggregate exit reflects any script failure
ok - gate-skip accounting is honest and non-failing
ok - fail-on-gate-skip converts herdr-not-found into a hard failure
ok - exclude-family drops the named primary family after selection
ok - portable shard union, disjointness, and coverage guard hold
ok - portable serial shards are a deterministic disjoint cover of the serial lane
ok - portable serial shard lanes refuse mismatched, out-of-range, and countless names
ok - --jobs refuses non-proven / stateful selections
ok - jobs scheduler runs proven scripts; failure propagates; non-proven refused
ok - aggregate-json merges lane timing artifacts
FM_TEST_END 2026-08-10T14:47:40Z tests/fm-test-run.test.sh exit=0 duration_ms=16228 gate_skip=false
FM_TEST_BEGIN 2026-08-10T14:47:40Z tests/fm-skill-path.test.sh family=pure-contract-unit expected_gate_skip=none
ok - fm-skill-path.sh: bash -n succeeds
ok - fm-skill-path.sh: resolves a declared skill with its full identity
ok - fm-skill-path.sh: resolved directory carries the skill's support tree
ok - fm-skill-path.sh: an unset CLAUDE_CONFIG_DIR falls back to $HOME/.claude
ok - fm-skill-path.sh: resolves through a CLAUDE_CONFIG_DIR containing spaces
ok - fm-skill-path.sh: refuses a missing registry, plugin, or install directory
ok - fm-skill-path.sh: refuses a disabled or unenabled plugin
ok - fm-skill-path.sh: a local enablement decision wins over the shared one
ok - fm-skill-path.sh: refuses a skill this plugin version does not declare
ok - fm-skill-path.sh: refuses an ambiguous marketplace and accepts an explicit one
ok - fm-skill-path.sh: refuses an ambiguous scope and accepts an explicit one
ok - fm-skill-path.sh: refuses a tampered or inconsistent install
ok - fm-skill-path.sh: refuses symlinked skills and support trees
ok - fm-skill-path.sh: never falls back to a stale user-level skill copy
ok - fm-skill-path.sh: honors expected version and source-commit pins
ok - fm-skill-path.sh: malformed configuration shapes refuse cleanly
ok - fm-skill-path.sh: usage errors are exact and --help works
FM_TEST_END 2026-08-10T14:47:42Z tests/fm-skill-path.test.sh exit=0 duration_ms=2156 gate_skip=false
FM_TEST_SUMMARY total=6 failed=0 skipped_gate=0 duration_ms=114225
FM_TEST_SUMMARY_FAMILY family=pr-forge count=1 duration_ms=50380 failed=0
FM_TEST_SUMMARY_FAMILY family=pure-contract-unit count=3 duration_ms=19911 failed=0
FM_TEST_SUMMARY_FAMILY family=watcher-wake-lock count=2 duration_ms=43735 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-x-mode.test.sh duration_ms=50380
FM_TEST_SLOWEST rank=2 script=tests/fm-turnend-guard.test.sh duration_ms=39746
FM_TEST_SLOWEST rank=3 script=tests/fm-test-run.test.sh duration_ms=16228
FM_TEST_SLOWEST rank=4 script=tests/fm-guard-stale-banner.test.sh duration_ms=3989
FM_TEST_SLOWEST rank=5 script=tests/fm-skill-path.test.sh duration_ms=2156
FM_TEST_SLOWEST rank=6 script=tests/fm-brief.test.sh duration_ms=1527
Evidence: Targeted tests set 3 - remaining conflict surfaces (2 environmental failures)

Source: Targeted tests set 3 - remaining conflict surfaces (2 environmental failures) (local file: /var/folders/9d/8w50jhgd79x63rgbq_5cyyvm0000gn/T/no-mistakes-evidence/01KZNV2HRKAA5FZK36DQ8EQGJR/tests-set3.log)

FM_TEST_END tests/fm-decision-hold-lifecycle.test.sh exit=1 duration_ms=2125
FM_TEST_END tests/fm-wake-queue.test.sh exit=0 duration_ms=27791
FM_TEST_END tests/fm-supervision-instructions.test.sh exit=0 duration_ms=432
FM_TEST_END tests/fm-supervision-events.test.sh exit=0 duration_ms=505
FM_TEST_END tests/fm-spawn-batch.test.sh exit=0 duration_ms=1054
FM_TEST_END tests/fm-spawn-dispatch-profile.test.sh exit=0 duration_ms=57718
FM_TEST_END tests/fm-calm-pi-extension.test.sh exit=1 duration_ms=3107
FM_TEST_SUMMARY total=7 failed=2 skipped_gate=0 duration_ms=92963
Evidence: Baseline: same calm failure on pristine upstream 85e750a (proves environmental, not change-induced)

not ok - Pi calm renderer and lifecycle contract failed: file:///opt/homebrew/lib/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js:2865 TypeError: this.getMarkdownTransformers is not a function (installed pi-coding-agent@0.84.1; .pi/ tree in HEAD is byte-identical to upstream 85e750a)

ok - Pi calm resolves its persistent home independently of Pi's launch directory
ok - Pi calm compatibility evidence never rejects a Pi version for being newer than 0.82.0, and still fails closed on a missing or malformed version
ok - a missing collapsed-thinking presentation API degrades only that Calm adapter with a clear skip reason, while the rest of Calm still registers
ok - missing Pi presentation class exports reach the independent adapter degradation path
ok - Calm registers none of its 7 built-in tool wrappers at load while config/calm is off, and all 7 synchronously at load while config/calm is on
ok - Calm's first same-session /calm activation claims every uncontested built-in, leaves a foreign bash tool fully intact and callable, warns prominently and logs the contested name, and only rows constructed before that activation - the documented bound - fail to retroactively collapse
not ok - Pi calm renderer and lifecycle contract failed: file:///opt/homebrew/lib/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js:2865
                        const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings(), this.outputPad, this.getMarkdownTransformers());
                                                                                                                                              ^

TypeError: this.getMarkdownTransformers is not a function
    at Object.addMessageToChat (file:///opt/homebrew/lib/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js:2865:143)
    at prototype.addMessageToChat (file:///private/var/folders/9d/8w50jhgd79x63rgbq_5cyyvm0000gn/T/fm-calm-pi-extension.ZugPz0/renderer/lib/fm-calm-operational-user-layout.ts:130:32)
    at file:///private/var/folders/9d/8w50jhgd79x63rgbq_5cyyvm0000gn/T/fm-calm-pi-extension.ZugPz0/renderer/[eval1]:150:46
    at process.processTicksAndRejections (node:internal/process/task_queues:104:5)

Node.js v25.9.0
Evidence: Baseline: same decision-hold failure on pristine upstream 85e750a (host tasks-axi 0.2.3 < FM_TASKS_AXI_MIN 0.2.4)

fm-decision-hold: compatible tasks-axi is required not ok - could not register route hold (host tasks-axi 0.2.3; bin/fm-tasks-axi-lib.sh FM_TASKS_AXI_MIN=0.2.4, a file that exists only in upstream 85e750a)

ok - report-only unresolved decision is reproduced and completion refuses before loss
ok - non-forced scout teardown always requires durable inventory verification
fm-decision-hold: compatible tasks-axi is required
not ok - could not register route hold
Evidence: Documentation-audience check and test lane coverage guard
$ bash bin/fm-doc-audience-check.sh
fm-doc-audience-check: ok surfaces=73 local_links=245

$ bash bin/fm-test-run.sh --check-coverage
FM_TEST_COVERAGE ok total=139 parallel=24 serial=103 serial_shards=4 herdr=12
- Outcome: ⚠️ 2 warnings across 1 run (13m53s)

Pipeline

Updates from git push no-mistakes

... (3 earlier update rounds omitted to keep the PR body within GitHub's 65536-char limit; full history is in the run log.)

🔧 **Review** - 5 issues found → auto-fixed (3) ✅

🔧 Fix: fix sanitizer fixpoint, drop dead resolver fallback, derive plugin identity
2 infos still open:

  • ℹ️ bin/fm-matt-pointers.sh:137 - The applied fix does remove the rename dependency it targeted, but the recovery modes it names are still unavailable in the adjacent failure mode: an absent or moved plugin install. SKILLS=$(&#34;$LOADER&#34; --list) runs unconditionally at line 137, before the mode dispatch for --prune and --uninstall at line 304, and under set -eu a failing command substitution aborts the script. Concrete sequence: the operator uninstalls the plugin, or the install directory is moved (the exact scenario docs/verification/matt-pocock-runtime-reach.md exercises under "A missing plugin stops the work instead of degrading it"). bin/fm-matt-skill.sh --list then dies 3 at its own line 150 ("install directory is missing (moved or removed)"), so bin/fm-matt-pointers.sh --uninstall exits 3 having removed nothing, and the generated pointers stay in ~/.agents/skills/matt-* where every non-Claude runtime keeps listing them. The failure stays honest - each pointer body instructs a hard stop and quotes the loader's diagnostic - so this is untidiness, not a correctness or safety hole, and rm -rf ~/.agents/skills/matt-* is a trivial manual workaround. Note also that --uninstall needs neither $SKILLS nor $IDENTITY (it only walks marked directories under $DEST/$PREFIX*), and --prune needs $SKILLS but not $IDENTITY. Dispatching --uninstall before any loader call would close the gap. Flagging rather than fixing because the header's exit table already documents "3-6 the plugin could not be resolved" for the whole script, so whether --uninstall should be exempt from that contract is the author's call.
  • ℹ️ docs/scripts.md:41 - docs/scripts.md is the repo's single bin/ toolbelt index, and this branch's own local work updated it - it added a row for fm-claude-worktree-hook.sh at line 41 and rewrote the fm-decision-hold.sh and fm-session-lock-lib.sh rows. Three sibling entrypoints from the same local series got no row: fm-skill-path.sh, fm-matt-skill.sh, and fm-matt-pointers.sh (grep for each returns 0 hits in docs/scripts.md). These are exactly the kind of surface the table indexes - fm-matt-pointers.sh is an operator-run entrypoint with install/--check/--prune/--uninstall modes, and fm-skill-path.sh is the resolver that .agents/skills/harness-adapters/SKILL.md:177 and .agents/skills/grill-intake/SKILL.md:24 both instruct agents to call by name. Nothing enforces this: docs/documentation-audiences.json tracks audiences and owner pointers, not bin/ coverage, so the pipeline's documentation-audience check will not catch it. Which scripts belong in a curated toolbelt is an editorial call, hence ask-user rather than a mechanical fix.

🔧 Fix: no findings selected; verified prior fixes still hold
1 error still open:

  • 🚨 REQUIRED criterion still unmet, and the fix selected in the previous round has not been applied. The intent states: "Current fetched heads are origin/main d989773 and upstream/main 85e750a" and "Preserve every genuinely unique local commit exactly once ... and incorporate complete current upstream history." Against the current target cf0ca42: git merge-base --is-ancestor 85e750ab9b76df275c1f6b9e2bc95b671955bae9 HEAD exits non-zero and git rev-list --count HEAD..85e750ab reports 43. HEAD is unchanged from the previous round (tree 699810d4, clean working tree, single new commit cf0ca42 touching only the three accepted fixes), so nothing in this round moved the branch toward the named upstream head. Upstream is still reachable only as far as a83be60 (feat: route remote commands through an Aqua job worker kunchenguid/firstmate#1660) via primary/main's own earlier merge. Absent work includes 85e750a fix(cmux): classify borderless Claude composers (fix(cmux): classify borderless Claude composers kunchenguid/firstmate#2029), fffe91e fix(spawn): force regular Pi TUI for crews (fix(spawn): force regular Pi TUI for crews kunchenguid/firstmate#2005), 74230fc docs: add project vision (docs: add project vision kunchenguid/firstmate#1997), 5ade9ef feat(stow): add tiered decaying memory management (feat(stow): add tiered decaying memory management kunchenguid/firstmate#1984), and 2d2be63 feat(bin): add deterministic agent lifecycle control (feat(bin): add deterministic agent lifecycle control kunchenguid/firstmate#1568); git cat-file -e HEAD:docs/vision.md still fails, confirming a content gap and not just a graph-shape difference. New evidence on what the remedy costs: a read-only git merge-tree --write-tree HEAD 85e750ab (merge base a83be60) exits 1 with 11 content conflicts, in AGENTS.md, .agents/skills/harness-adapters/SKILL.md, bin/fm-brief.sh, bin/fm-spawn.sh, bin/fm-supervision-lib.sh, bin/fm-teardown.sh, docs/documentation-audiences.json, docs/sessionstart-nudge.md, docs/turnend-guard.md, docs/verification/runtime-backends.md, and tests/fm-guard-stale-banner.test.sh. Those are the same files the local-only series changed most (the FM_SUP_NEEDED supervision predicate, the brief scaffolds, the Claude worktree-hook wiring in spawn and teardown, and the doc-audience inventory), so resolution is semantic and hand-authored, not mechanical - and the conflict in docs/documentation-audiences.json means the intent's required documentation-audience check cannot pass until that file is resolved deliberately. This stays ask-user: which side wins in each of the 11 conflicts is a product and history decision the author owns, and the intent-conformance rule forbids resolving it inside review. Everything else the intent marks required is satisfied and re-verified on this tree: the merge commit 1a6bdfa with parents origin/main and primary/main is still an ancestor, all 32 primary-only commits are carried with no duplicate patch-ids, PR 1's normalized documentation under docs/agents/ is unchanged since base, there are zero deletions since base, and commit 889cd89 is not a valid object in this repository.

🔧 Fix: merge current upstream history, fix lock ownership seam
✅ Re-checked - no issues remain.

⚠️ **Test** - 2 warnings
  • ⚠️ tests/fm-decision-hold-lifecycle.test.sh - tests/fm-decision-hold-lifecycle.test.sh fails locally with "fm-decision-hold: compatible tasks-axi is required" because the host has tasks-axi 0.2.3 while bin/fm-tasks-axi-lib.sh sets FM_TASKS_AXI_MIN=0.2.4. This is not caused by the change: that floor is upstream-only content (bin/fm-tasks-axi-lib.sh does not exist at base d989773 or primary eeb4d41), and the identical failure reproduces when the test is run from a pristine extract of upstream 85e750a. Fixing it needs a global tasks-axi upgrade, which is outside this worktree's write boundary, so it needs a human decision. Remote CI, which pins the axi toolchain, owns the authoritative run.
  • ⚠️ tests/fm-calm-pi-extension.test.sh - tests/fm-calm-pi-extension.test.sh fails locally on the "Pi calm renderer and lifecycle contract" case with TypeError: this.getMarkdownTransformers is not a function, thrown inside the host-installed @earendil-works/pi-coding-agent@0.84.1 dist/modes/interactive/interactive-mode.js. This is a host Pi version incompatibility, not a defect in the change: the whole .pi/ tree (including .pi/extensions/lib/fm-calm-operational-user-layout.ts) is byte-identical to upstream 85e750a, the only local delta in the test file is one added support-file copy line in a different test function, and the identical failure reproduces from a pristine extract of upstream 85e750a. Fixing it needs a global Pi version change, which is outside this worktree's write boundary. Remote CI owns the pinned-Pi run.
  • bash bin/fm-doc-audience-check.sh - documentation-audience structural check (ok surfaces=73 local_links=245)
  • bash bin/fm-test-run.sh --check-coverage - test lane coverage guard (FM_TEST_COVERAGE ok total=139 parallel=24 serial=103 serial_shards=4 herdr=12)
  • bash bin/fm-test-run.sh tests/fm-documentation-audiences.test.sh tests/fm-matt-pointers.test.sh tests/fm-operational-input.test.sh tests/fm-sessionstart-nudge.test.sh tests/fm-session-lock-ancestry.test.sh - 5/5 pass
  • bash bin/fm-test-run.sh tests/fm-brief.test.sh tests/fm-guard-stale-banner.test.sh tests/fm-turnend-guard.test.sh tests/fm-x-mode.test.sh tests/fm-test-run.test.sh tests/fm-skill-path.test.sh - 6/6 pass
  • bash bin/fm-test-run.sh tests/fm-decision-hold-lifecycle.test.sh tests/fm-wake-queue.test.sh tests/fm-supervision-instructions.test.sh tests/fm-supervision-events.test.sh tests/fm-spawn-batch.test.sh tests/fm-spawn-dispatch-profile.test.sh tests/fm-calm-pi-extension.test.sh - 5/7 pass, 2 environmental failures
  • Test selection derived from the real hand-edited surface: git show --format=&#39;&#39; --name-only cedc8f7 and git show --format=&#39;&#39; --name-only 1a6bdfa (merge conflict resolutions) plus git show --stat cf0ca42 and git show --stat 7e3f633 (gate-fix commits)
  • git merge-base --is-ancestor &lt;sha&gt; HEAD for origin/main d989773, upstream/main 85e750a, primary/main eeb4d41 - ancestry proof, all reachable
  • git log -1 --format=&#39;%H %P&#39; 1a6bdfa and git log -1 --format=&#39;%H %P&#39; cedc8f7 - merge-commit parent proof (origin+primary, and upstream)
  • git rev-list --count eeb4d41 ^d989773 - local commit inventory, 32 commits (28 non-merge + 4 merge)
  • git cherry d989773 eeb4d41 - duplicate/equivalence decision: 28 unique (+), 0 already-equivalent (-)
  • Per-commit occurrence scan of all 32 primary-only commits against git rev-list HEAD - each appears exactly once
  • git patch-id --stable duplication scan across every non-merge commit HEAD adds over origin/main - no two commits introduce the same patch
  • PR 1 preservation: git rev-parse d989773:&lt;path&gt; vs git rev-parse HEAD:&lt;path&gt; for docs/agents/domain.md, issue-tracker.md, triage-labels.md (byte-identical) plus grep of all 11 PR-1 added lines in .agents/skills/harness-adapters/SKILL.md, AGENTS.md, docs/documentation-audiences.json
  • Superseded-source check: every file in git diff --name-only 85e750a HEAD (58 files) matched against the file set touched by the 32 local-only commits plus PR 1 - zero unexplained divergences; reverse check confirms no upstream update reverted to pre-merge origin content
  • Exclusion check: git cat-file -e 889cd89 (absent from object DB) and git for-each-ref (no portable shard refresh ref)
  • Baseline reproduction on pristine upstream: git archive 85e750a | tar -x -C $TMP then bash $TMP/tests/fm-calm-pi-extension.test.sh and bash $TMP/tests/fm-decision-hold-lifecycle.test.sh - both fail identically, proving the two failures are host-environment, not change-induced
  • git status --porcelain - worktree clean, no transient test artifacts left behind
⚠️ **Document** - 3 issues (1 warning, 2 infos)
  • ⚠️ docs/fm-test-portable-shards.md:79 - Portable serial shard weight hints in bin/fm-test-run.sh still cover only the 69 scripts measured on 2026-08-02, while the merged lane now holds 103. The 34 unhinted scripts are packed at PORTABLE_SERIAL_DEFAULT_WEIGHT_MS, raising each shard's estimate from ~4.8 to ~7.6 minutes against the unchanged 15-minute CI timeout. I documented the current partition and the coarser balance, but refreshing the hints is a code change that needs a green CI fm-test-timing-portable-serial artifact, and the intent deliberately excluded the portable shard refresh branch and commit 889cd89. Proposed follow-up: refresh the hint table from the first green run of this branch.
  • ℹ️ docs/scripts.md:8 - docs/scripts.md gained no rows for the three toolbelt entrypoints this change adds (bin/fm-skill-path.sh, bin/fm-matt-skill.sh, bin/fm-matt-pointers.sh). I left the table alone deliberately: it has never been an exhaustive inventory on either line (upstream 85e750a omits 30 of its own scripts, base d989773 omits 19), so adding three of the missing rows would create an arbitrary partial rather than fix a fact this change made wrong. A separate pass that either completes the table or states its selection rule is the right owner for that.
  • ℹ️ docs/verification/matt-pocock-runtime-reach.md:6 - bin/fm-matt-skill.sh pins VALIDATED_VERSION=1.2.0 and both matt verification records are scoped to that version, but the plugin installed on this machine is 1.2.3, so the loader prints its loud PLUGIN CHANGED banner and asks the operator to refresh the validated pin. Code and docs agree with each other, and this drift predates the change, so nothing here is stale; refreshing the pin plus its version-scoped evidence is a separate captain-owned decision.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

yelenplays and others added 30 commits July 27, 2026 10:19
fm_session_lock_owned_by_self() compared the recorded lock pid against the
NEAREST harness-named ancestor, which asks a category question ("is my closest
harness ancestor the owner?") where the lock records an identity ("this session
owns this home"). Claude Code 2.1.220 interposes a daemon, a pty host, and a
bg-spare helper between every hook and the session, so the walk stopped at the
helper and a session stopped recognising its own lock.

Two consequences, both reproduced:

- The Claude Stop auto-arm exited at the identity gate on every firing, before
  writing a byte, so state/.claude-autoarm.lock never appeared and watcher
  continuity silently depended on manual re-arming.
- bin/fm-lock.sh refused a session its own home, naming that session's own pid
  as "another live firstmate session".

Ownership is now chain membership: the recorded pid counts when it appears
anywhere in the current ancestry. The hop budget moves from 8 to 12 because the
session now sits six or more parents above a hook.

fm_harness_ancestry_pid(), which mints the lock value, gets the companion
change: it returns the outermost pid of the uninterrupted harness-named run
rather than the nearest one, so a lock acquired after the helper tree appears
records the session instead of a helper that dies with the next generation. The
run deliberately stops at the first non-harness parent, so a genuinely separate
parent session - reachable only across a multiplexer server or a plain shell -
stays outside this session's identity.

bin/fm-lock.sh now refuses only a lock recorded outside this session's ancestry,
which also keeps a home readable when its lock was minted by a different adapter
or an older firstmate on the same session.

fm-sessionstart-nudge.sh had a second full copy of this contract. It already
asked the identity question but at the old depth, so it is consolidated onto the
shared owner rather than left to drift.

Fail-closed behaviour is unchanged: an unresolvable ancestry, a malformed lock,
and a lock held by an unrelated live session all still refuse.

Deliberately not addressed: the "cycle ended without an actionable reason"
reporting defect. Those reports are currently the loudest signal that continuity
is broken, so the identity gate lands first.
The existing auto-arm evidence was measured on 2.1.219, before the helper
tree existed, so it does not cover the ancestry depth the identity predicate
now has to survive.
Four crewmates in a row (2026-07-26/27) reported `done:` as soon as their
implementation was committed with green project tests, never starting the
pipeline. Each needed a steer, and each pipeline run then found real defects.

They were not ignoring the brief - they were following it. The generated
no-mistakes Definition of done opened with "The task is complete only when
committed on your branch", offered a `done: {summary}` gate right there, and
deferred the pipeline to a later firstmate instruction. The real gate,
`done: PR {url} checks green`, sat fourteen lines below. A worker that stops
reading at its first stop condition stops at the commit.

Invert it: the PR with green checks is the first and only stop condition, the
commit is explicitly an intermediate step, and the worker starts the pipeline
itself instead of waiting to be told. The section gets shorter, not longer -
the defect was the order, not a shortage of text.

Scoped to the no-mistakes branch; direct-PR and local-only genuinely do
complete at the commit and are untouched. The ask-user prohibition, the
`--yes` prohibition, the shared-daemon rule, and the worktree-isolation
assertion are preserved verbatim.

AGENTS.md's validate step is updated to match: firstmate's harness-correct
trigger becomes the fallback for a worker that did not start validation.

Add a regression test pinning the order, the absence of the earlier `done:`
gate, and the preserved safety wording. Nothing checked this before, which is
why it went unnoticed through four repetitions.
A captain decision was one prose field carrying the question, the options,
the recommendation and the evidence at once. Measured on the live backlog on
2026-07-29: 116 open captain decisions, hold reason min 60, median 309, max
1457 characters, against a collapsed notification budget of about 80 and a
decision card question budget of about 52. Nothing could render that, and
nothing should have to parse prose to find the options.

Add seven authored fields alongside the prose, which keeps its exact meaning
as the why disclosure: question, consequence, recommend, options[],
expires_at, sensitivity, and safe_preview.

fm-classify-lib.sh owns the wire form, the field names and the ceilings, and
gains the status-line record block, its parsers and decision_record_validate.
fm-decision-hold.sh accepts the fields on hold, stores them in the hold body,
carries them into a resolved hold, and gains record (fields or JSON) and
status-line (compose and validate). status_line_note strips the block, so
every existing consumer keeps showing the same human summary.

The change is strictly additive. The block sits after the first colon of a
status line, so the verb parser, the key parser, the decision fold, the
watcher and the away-mode daemon are untouched, and a decision registered
without record options behaves exactly as before and is never rewritten.

The ceilings are enforced rather than documented, because a field that
overflows cannot be rendered at all. The first option may not be destructive,
since that is the action an Apple Watch double tap fires with no
confirmation. sensitivity classifies and grants nothing: AGENTS.md section 7
and ask-user-authority keep owning who may answer what, and a regression
asserts no script outside the record owner and its grammar reads it.
safe_preview is the only field that may leave the trusted session, so it is
refused when it carries a link, address, handle, path, identifier, amount,
opaque token, invisible separator, or a phrase lifted out of the why prose.
fm-spawn installed the Claude crewmate turn-end hook by writing
<worktree>/.claude/settings.local.json with `cat >`, replacing whatever the
project kept there. Reproduced end to end: a worktree whose settings file
carried three pre-approved permission entries was left as a single hooks object
with the permissions gone. Some vaults track that file, so a crewmate touching
it or any broad commit could land the deletion in the captain's own repository,
and the diff would read as ordinary agent configuration.

bin/fm-claude-worktree-hook.sh now owns that file's contract for both ends of
the task lifecycle:

- install merges one Stop hook into the existing document, preserving every
  other key and every hook the project already had, including its own Stop
  hooks.
- A missing or empty file has nothing to preserve and is simply written. A
  malformed, symlinked, or structurally surprising file is refused with no
  write at all, so an unreadable file is never silently replaced; fm-spawn
  turns that refusal into a refused spawn naming the file to repair.
- install records what it found, so remove restores the original bytes when
  nothing else changed the file, keeps settings the task added, and deletes
  only a file (and .claude directory) firstmate itself created.
- Only a settings file firstmate created is added to the worktree's
  info/exclude; a project's own file stays in git's view.

fm-teardown removes the hook before it inspects the worktree for uncommitted
work, because a hook merged into a tracked settings file would otherwise read
as the crewmate's own unlanded change and refuse a teardown of a clean
worktree. A refused teardown reinstalls the hook so a still-running crewmate
keeps signalling turn ends.

The other harnesses were checked and do not have this defect: opencode, grok,
and kimi each write only firstmate-reserved names in the worktree
(.opencode/plugins/fm-turn-end.js, .fm-grok-turnend, .fm-kimi-turnend), pi
writes outside the worktree entirely, and codex writes nothing. Kimi's global
config edit already goes through its own marker-region installer.

Tests: tests/fm-claude-worktree-settings.test.sh drives the real fm-spawn and
fm-teardown against real git worktrees, covering pre-existing permissions and
hooks, absent/empty/malformed/symlinked files, byte-exact restoration, and a
tracked settings file surviving a full spawn-to-teardown round trip.
…ness

An X-mode-armed home with no task in flight still needs a live supervision
cycle so an incoming mention can wake it, which AGENTS.md section 14 states as
a contract. Both guards contradicted it.

bin/fm-guard.sh exited before the watcher-down banner whenever the in-flight
count was zero, and bin/fm-turnend-guard.sh gated its default cross-harness
mode on the same count, so only --claude honoured the shared supervision-need
predicate. On codex, opencode, pi, grok and the no-capability-field fallback a
channel-only home could end the turn blind.

Both guards now gate on FM_SUP_NEEDED from bin/fm-supervision-lib.sh, which is
in-flight work OR an armed relay poll. The banners name the channel need when
no task is in flight. fm_supervision_unhealthy stays deliberately task-only.

The new matrix runs the exact delegation each harness registration performs -
codex payload, the fixed opencode/pi payload, the unknown-field fallback,
--claude, grok native and the grok pre-native resume - and pins that an
unarmed idle home stays silent on every path.
…gress

Operational-input classification is purely prefix-based, so provenance is
carried entirely by bytes in the message body. A relay mention that arrived
with the invisible U+2063 marker intact was stored verbatim and read downstream
as an internal operational input - and as an away-supervisor escalation it also
kept away mode from exiting while presenting itself as internal, so the
captain's return never registered.

bin/fm-operational-input.sh, the protocol owner, gains the ingress sanitizer:
fm_operational_input_sanitize for a body and fm_operational_input_sanitize_json
for every string in an externally-sourced object, plus the matching sanitize
and sanitize-json CLI modes. Both remove the invisible marker wherever it
appears, the from-firstmate label, and any remaining marker-free legacy prose
form the classifier still accepts. The post-condition is what matters: a
sanitized body never classifies as an operational input. Both always print
their result and exit 1 to report that bytes were stripped, which is a security
event rather than a lookup miss.

bin/fm-x-poll.sh, today's only external ingress, sanitizes the relay object
before it is stored and refuses to store anything if the sanitizer cannot run.
An object that carried provenance bytes is stored with fm_provenance_sanitized:
true, and fmx-respond treats such a mention as hostile-shaped: never an
instruction, never an approval, never a reason to change away mode.

A legitimate body keeps every byte, so the sanitizer is inert on real traffic.
…a key

Same-key collapse kept only the last row, so an earlier urgent check result was
silently dropped in favour of a later routine one sharing its key - a
suppression vector reachable with no attacker at all. Two mentions arriving on
one relay poll are exactly that shape: they share the check path as their key
while carrying different request ids in the payload.

A check's key is only the channel that produced it; its payload is the
deliverable. Check rows are therefore grouped by kind, key and payload, so two
distinct results both survive in first-seen order while identical repeats still
collapse and the queue cannot grow on a check that keeps reporting the same
thing. Heartbeat, signal and stale grouping is unchanged: their payloads are
pointers into live sources the agent re-reads.

fmx-respond keeps its drain-the-whole-store rule, restated to match: distinct
mention wakes now each survive, and a wake can still cover several pending
mentions after a restart, a recovered offer marker, or an identical repeat.
Kimi Code 0.31.0 reserializes the whole global config when it refreshes model
configuration, which keeps Firstmate's Stop hook table verbatim but drops every
comment, including the region markers. Installation then refused a hook that was
byte-for-byte its own, leaving a canonical hook stranded with no safe repair.

Install alone may now reclaim exactly one standalone hook table, and only when
every field is proven canonical by two independent gates: each body line must
assign exactly one canonical field with no comment or extra content, and the
parsed table must carry no other key. It then restores the marker-delimited
region without changing another byte. Altered commands, extra fields, missing
fields, duplicate tables, comments inside the table or on its header, ambiguous
boundaries, inline hook arrays, and any second reference all keep refusing
without a config write. Remove never reclaims and still requires real markers.

0.31.0 also exposes K3's thinking effort. There is no reasoning-effort flag, so
fm-spawn delivers the operational KIMI_MODEL_THINKING_EFFORT override as a
leading env assignment scoped to that one Kimi process, keeping the verified
adapter path intact. The override bypasses Kimi's own supportEfforts check, so
only the levels K3 advertises (low, high, max) are passed; medium and xhigh stay
in task metadata and never reach the wire.
The fixture proved slot refill by racing a 0.5s sleep against the replacement
worker starting, which false-failed under concurrent test load and reproduced on
a clean base. The long worker now blocks until the replacement signals that it
started, so the ordering is proven rather than timed, and a scheduler that waited
for the oldest worker still fails through the same assertion after a bounded
wait. The later runs in the same case only need a proven-set worker that
succeeds, so the coordination fixture is dropped before them.
…e 1)

Load the original installed skills instead of paraphrasing them, and close
the hazards a globally discoverable skill suite creates inside a worker.

- bin/fm-skill-path.sh resolves one exact installed plugin skill from the
  Claude plugin registry, verifying identity before printing anything:
  the manifest names the plugin and agrees with the registry version, the
  skill is declared by this version, front matter names the skill, and no
  symlink or traversal escapes the install root. Refusals print no path and
  carry distinct exit codes, so a caller can say the honest thing about why
  a workflow is unavailable. Resolution reads only the plugin registry, so a
  stale copy under ~/.agents/skills can never satisfy a request.
- .agents/skills/grill-intake adapts grilling, grill-with-docs and
  domain-modeling for intake sharpening. It restates no upstream procedure:
  it resolves and reads the originals, and states only the Firstmate
  bindings - who interviews, when a fact is looked up instead of asked,
  and that glossary and decision records are captured immediately but
  landed by a crewmate through the project's delivery path.
- Generated briefs gain three defensive rules: a worker never starts an
  interview skill and returns decisions through firstmate; no skill
  displaces the selected delivery path or no-mistakes' sole review
  ownership; and a scout's delegated research lands only in its report,
  with no unsupervised nested agent.
- AGENTS.md gains two lines: the narrow grill-intake trigger, and that
  skill-dependent work must dispatch to a harness that can load the skill
  and fail honestly when none can. harness-adapters records which harness
  discovers what.

Nothing upstream is copied, vendored, symlinked, or paraphrased, so the
workflows improve whenever the plugin does.

Colocated tests cover resolution, support-file resolution, a custom
CLAUDE_CONFIG_DIR, paths with spaces, and refusal of missing, disabled,
ambiguous, tampered, symlinked and malformed installs; brief tests assert
each safety rule lands only in the task shapes whose hazard it addresses.
The Matt-derived copies under ~/.agents/skills were removed after the probe
ran, so state the comparison as a dated observation and make explicit that
the resolver's guarantee never depended on that directory being empty.
The suite is installed once, as the SHA-pinned Claude plugin
mattpocock-skills 1.2.0. Runtimes that discover skills elsewhere had no
route to it, and the twelve vendored copies that used to stand in for one
had gone stale. Copying the bodies again would recreate that failure, so
nothing here copies a skill body.

bin/fm-matt-skill.sh resolves one declared skill from the Claude plugin
registry and prints the installed original, preceded by an identity header
carrying the author, licence, upstream repository, resolved version and
commit, and the SHA-256 of the file it read. It refuses without printing
anything on stdout when the plugin is absent, disabled, moved, swapped,
symlinked, or declares a different skill, so a caller can treat any output
as original bytes. Version drift is reported under a PLUGIN CHANGED banner
rather than refused, because refusing on every upstream bump would turn each
release into an outage and bring back the sync chore this replaces;
--require-validated-pin restores the strict reading.

Resolution delegates to bin/fm-skill-path.sh whenever that script is
present, which is the repo's single owner of plugin-skill resolution. The
inline fallback covers only this one plugin and exists because that owner
has not landed yet. Deleting it once the owner is available changes no
generated pointer, only the resolved_by line.

bin/fm-matt-pointers.sh installs one small pointer per declared skill where
a runtime looks. Each pointer names Matt Pocock, the MIT licence, the
upstream repository, the plugin and the validated version, mirrors the
upstream invocation flags so a user-invoked skill is never promoted, carries
a firstmate-pointer marker and a generation date so an audit can tell it
from a hand-copied directory, and instructs a hard stop rather than any
fallback when the load fails. A destination directory without that marker is
never written to and never removed.

docs/verification/matt-pocock-runtime-reach.md records the live evidence,
including the finding that grok 0.2.114 already reaches the plugin natively
and needs no bridge, while kimi and codex do not.
…local

Merges kunchenguid/firstmate (origin/main, 79e62b8) into the local line
(bb1e86c). Merge rather than rebase so both histories survive intact.

Seven conflict hunks across six files, each resolved on the evidence:

- AGENTS.md state inventory: union. Keeps the local
  <id>.claude-settings-backup entry and takes upstream's .meta field list
  with endpoint_task_id=; both describe real fields written by fm-spawn.

- bin/fm-session-lock-lib.sh, fm_harness_ancestry_pid: keeps the local
  nearest-match walk and raises the hop budget to upstream's 16. The two
  sides drew opposite conclusions from the same Claude helper tree.
  Upstream (kunchenguid#1206) widened the mint to the outermost contiguous
  claude-named ancestor so a Stop hook firing below the lock owner could
  recognise its own session. The local line instead made ownership an
  ancestry-membership question in fm_session_lock_owned_by_self, which
  upstream left as a nearest-match equality test and which the merge
  keeps. Membership already answers upstream's case - the lock owner is
  an ancestor of the hook either way - so widening the mint buys nothing
  and costs the hazard the local evidence records: under Claude Code
  2.1.220's bg-spare shape the outermost contiguous claude-named ancestor
  is the machine-wide daemon, and minting it would record one pid for
  every session at once. Nearest-match is also correct for Pi, whose
  signed wrapper can be the parent of the inner engine pid that holds the
  lock. Both sides' regression tests pass against this resolution.

- bin/fm-session-lock-lib.sh, fm_harness_pid_alive: takes upstream's
  split of the command-name test from the argument test, because
  FM_HARNESS_RE anchors ^pi$ and ^pi-signed$ and neither can match a line
  that also carries the arguments - the local one-line form would have
  made every Pi session look dead. Keeps the local ${comm##*/} expansion
  in place of basename, which is what stops a login shell's leading-dash
  comm being parsed as an option. Keeps the argument fallback ungated
  rather than restricting it to node and python, preserving the
  pre-divergence behaviour and the local intent: this predicate judges an
  unrelated recorded pid, and refusing to evict a possible live owner is
  the safe direction.

- docs/sessionstart-nudge.md: takes the local text, which describes the
  wrapper as it now is - sourcing the shared lib rather than running its
  own hard-coded walk - and folds in upstream's corrected sixteen-parent
  depth and its silent-when-owned sentence.

- docs/turnend-guard.md: union of both coverage inventories, each claim
  checked against the merged test files.

- tests/fm-turnend-guard.test.sh: keeps the local channel-armed idle
  matrix and restores test_grok_hook_invokes_adapter, which upstream
  dropped in favour of adapter-level Grok tests. Those new tests all
  drive bin/fm-turnend-guard-grok.sh directly and none of them assert
  that the tracked .grok registration still points at that adapter, so
  the dropped test covers a real gap; it passes unchanged against
  upstream's registration file.

- tests/fm-kimi-harness.test.sh: union. Keeps upstream's tasktmp and
  GOTMPDIR assertions and the local failure message, which no longer
  calls the effort axis unsupported now that Kimi 0.31 delivers it.
…harness markers

test_pi_signed_detection_and_session_lock_identity drives bin/fm-harness.sh
with a fake ps and PI_CODING_AGENT=true, but never clears the ambient
harness family markers. fm-harness.sh answers CLAUDECODE before
PI_CODING_AGENT, so every one of those cases resolves "claude" and the
first assertion fails whenever the suite is run from inside a Claude Code
session. Verified against an untouched upstream tree: the case fails there
too, so this is a pre-existing environment dependency, not a merge
regression.

Clears CLAUDECODE and GROK_AGENT for the fm-harness.sh invocations, the
same way test_dash_leading_process_names_are_basename_operands already
does immediately below. The session-lock assertions in the same case need
no change; they source the library directly and never read those markers.
… line

Merges fm/firstmate-matt-pocock-crewmate-v1 (ddb5331). The branch was built
on upstream and could not land while the local line had not taken upstream;
that is now resolved, so it merges with one conflict.

bin/fm-brief.sh, twice, in the crewmate rules: the local structured decision
record added the fm-decision-hold.sh composition guidance to rule 6, and this
branch added the caution that no human is available in the pane, so a
grilling or interview skill must never be started to settle a decision. Both
belong in rule 6 and neither contradicts the other, so both are kept: the
caution first, since it follows directly from "append needs-decision and
stop", then the composition guidance for the line itself.
…line

Merges fm/matt-skills-fuer-grok (634d97f), which was also built on upstream
and blocked by the same divergence.

docs/documentation-audiences.json: both this branch and the Phase 1 bridge
add a maintainer-verification entry at the same position in the inventory.
Kept both, in path order.
* perf(ci): shard the portable serial behavior lane across runners

The Behavior portable serial job ran all 69 scripts of the serial
remainder on one runner. The measured serial sum on run 30725985757 was
1143762 ms (19m04s) against a 20-minute timeout, so the job intermittently
reached the cap and was cancelled with every step passing. Setup is only
about 7s, so the cost is entirely test wall time.

Split the lane into four separate-runner shards. Each shard is still
strictly serial, and separate runners mean no two of these stateful
scripts ever share a machine, so the split needs no concurrency isolation
proof. Assignment is longest-processing-time bin packing over measured
per-script duration hints, balancing every shard to 285941 ms (~4m46s) of
expected work, and the timeout tightens from 20 to 15 minutes.

bin/fm-test-run.sh owns the shard count and refuses a lane whose "ofN"
disagrees with it, while ci.yml derives the same count from
strategy.job-total rather than a literal, so changing it in either file
alone fails the lane loudly instead of leaving part of the required suite
unrun. --check-coverage additionally proves the shards are non-empty,
disjoint, and exactly equal to the serial lane. No test is weakened,
skipped, or removed.

Also replace the wall-clock sleeps in the --jobs scheduler test fixture
with an explicit signal handshake between the fixtures. The old
0.5s-versus-0.05s race failed on a loaded machine; the handshake passes
under sustained CPU saturation.

* no-mistakes(review): Correct portable serial shard balance evidence

* no-mistakes(document): Document portable serial shard evidence accurately
…henguid#1545)

* fix(bin): identify harness sessions by path and report delivered wakes

Two supervision faults, both reported by a contributor and both open on the
default branch.

Fault 1: the Stop auto-arm never claims the home. fm_harness_ancestry_pid()
matched only the basename of `ps -o comm=`, and Claude Code's native installer
names the per-session executable by its version (.../share/claude/versions/
2.1.220), so that basename identifies nothing. Three real failure shapes follow:
a version-named session is missed entirely and the hook exits 0 with the epoch
never written (unconditional on Linux, where procps reports the kernel exec name
and ignores argv[0]); a claude-named daemon that directly parents sessions wins
the outermost-contiguous-claude rule ahead of the session itself; and a session
that is both version-named and daemon-parented has its live lock reclaimed as
stale and rewritten to the shared daemon pid, corrupting the home's ownership
record.

Harness identity now also reads whole components of the executable path and of
argv[0], which is what both platforms still carry. Matching whole components
only keeps that widening safe: bin/fm-claude-stop-autoarm.sh and ~/.claude/hooks
scripts have no "claude" component. Ownership is then decided against the
session's whole contiguous harness ancestry rather than one chosen pid, which is
the honest form of the question the library already documents ("does the current
process descend from that same harness?"). That subsumes the outermost-pid rule
for Claude's nested bg-spare worker chain instead of reverting it, and lets a
daemon-parented session recognize its own lock. Lock acquisition still writes the
outermost pid of the run, the only pid that lives as long as the session.

Fault 2: an attached arm reports a delivered cycle as FAILED. The watcher prints
its one reason line to its own stdout, so only the arm that forked it can read
that line; an arm that attached observes nothing but a released lock and called a
completely successful cycle "cycle ended without an actionable reason". No
supervision event was lost - the durable queue held it - but every harness
protocol reads that line as "supervision is down" and directs a manual re-arm.

The arm now resolves an unobservable close against the durable wake queue, which
records every wake before the watcher prints it and whose sequence counter never
rewinds, not even across a drain. A cycle the queue proves delivered a wake
reports that wake and exits 0; a cycle whose records a handling turn already
drained reports the delivery without inventing a reason line; only a cycle that
delivered nothing is still the typed nonzero failure. Fixing it in the arm covers
codex, opencode, pi, grok and kimi, not just the Claude Stop path.

Regressions: tests/fm-session-lock-ancestry.test.sh pins both platforms' ps
semantics behind a deterministic process table and runs the real Stop auto-arm in
version-named, daemon-parented, and combined real process trees, each orphaned so
the walk cannot escape the fixture. tests/fm-watch-arm.test.sh drives a real
watcher and a real attached arm through a real wake. Every fault case fails on
the previous code.

* no-mistakes(review): Bind watcher delivery records to process identity

* no-mistakes(review): Return validated watcher identity atomically

* no-mistakes(review): Track watcher successors by PID and identity

* no-mistakes(document): Consolidate watcher arm-cycle documentation ownership
* fix(supervision): harden Claude auto-arm failure handling

* no-mistakes(review): Guarantee automatic retry after Claude auto-arm failures

* no-mistakes(review): Gate attended fail-open on verified supervision failure

* no-mistakes(document): Document Claude auto-arm retry and guard scope

* no-mistakes: apply CI fixes

* fix(supervision): make Claude fail-open progression monotonic

* no-mistakes(review): Preserve auto-arm failure episodes until verified watcher recovery

* no-mistakes(review): Linearize auto-arm failure progression across existing locks

* no-mistakes(review): Linearize positive recovery across shared failure episode lock

* no-mistakes(review): Scope Claude recovery contention to Claude guard mode

* no-mistakes(document): Align supervision auto-arm documentation

* no-mistakes(review): Preserve actionable wakes despite healthy successors

* no-mistakes(document): Refresh supervision auto-arm documentation
…d#1563)

* feat(bin): require an explicit ship delivery mode in fm-brief

A ship brief's definition of done was shaped by a silent per-project registry
lookup, so an adjusted brief and the task's recorded delivery could disagree and
no one had to decide anything per task.

fm-brief now requires --mode on ship scaffolds, validates it against the closed
set, refuses the conditional no-mistakes-prod-only registry policy as a task
mode, and records the choice as a fixed machine-readable "Delivery contract:
mode=<mode>" line that fm-spawn can check. --mode is refused on scout and
secondmate scaffolds, and --yolo is refused outright because the worker never
owns approval decisions.

* feat(bin): require an explicit ship delivery contract at spawn and promotion

fm-spawn resolved every ship and scout task's mode and yolo from the project
registry, so the delivery posture was never a per-task decision and could
contradict the brief the worker was about to follow.

fm-spawn now requires --mode and --yolo on ship spawns, validates both against
their closed sets, and reads the brief's recorded delivery contract line and
refuses a mismatch before any endpoint exists; a brief scaffolded before that
line existed warns once and launches on the flag. A batch carries one shared
contract that each pair still checks against its own brief. Scout and secondmate
spawns refuse the flags, and a scout now records no mode or yolo at all, which
teardown and the snapshot already tolerate. When the explicit mode carries less
rigor than the project's standing posture, a deviation notice is printed and the
spawn continues, so the registry stays advisory rather than an enforced default.

fm-promote requires the same two flags, because a scout carries no posture to
inherit, and writes them into the task record with the kind flip.

fm-project-mode keeps its one registry parser for the mechanical consumers that
have no task in hand, accepts the conditional no-mistakes-prod-only annotation
and maps it to its most rigorous leg for them, and grows --raw so the deviation
notice can tell a conditional policy apart from a flat mode.

* docs: record the explicit per-task delivery contract

AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at
intake, including the surface classification for a no-mistakes-prod-only project
and the unregistered-project fallback, and the project-management skill defines
that conditional policy as a registration-time posture with its defaults and
initialization consequences. The registry blurb, script table, and architecture
section follow: the registry records the captain's standing posture, and task
delivery is decided per task and passed explicitly.

* test: pass ship delivery flags per call site in the Herdr launcher e2e

The shared spawn helper also launches a secondmate, which refuses the flags, so
the contract belongs at each ship call site rather than inside the helper.

* test: pass the ship delivery contract in the secondmate suites

Both suites scaffold or spawn an ordinary ship task as the control case for a
secondmate assertion, so each needs the explicit contract the ship path now
requires.
* Add generic remote secondmate transport

* Add routed remote secondmate replies

* Add remote outbox backlog handoff

* Integrate remote secondmate lifecycle

* no-mistakes(review): Fix remote snapshot and handoff races

* no-mistakes(review): Serialize remote home provisioning transactions

* no-mistakes(review): Harden remote lifecycle transaction boundaries

* no-mistakes(review): Serialize remote lifecycle mutations and fail closed

* no-mistakes(review): Close remote lifecycle and file race windows

* no-mistakes(review): Serialize remote reply retirement and inheritance

* no-mistakes(review): Harden remote transfer integrity and recovery

* no-mistakes(review): Serialize remote respawn with registry retirement

* no-mistakes(document): Document remote bootstrap convergence accurately

* no-mistakes(document): Clarify skipped remote secondmate mutations

* no-mistakes(lint): Resolve remote script ShellCheck warnings

* no-mistakes: apply CI fixes
* feat(spawn): propagate a native W3C traceparent to spawned agents

Add a default-off capability that resolves one W3C traceparent for a task,
injects it into the agent's pane shell as the TRACEPARENT environment
variable immediately before launch, and records the identical value as
traceparent= in state/<id>.meta, so an external observer that explicitly
reads that env value or meta field can correlate a worker, a Secondmate, and
their nested children into one trace with no collector, storage, UI, or
vendor coupling.

TRACEPARENT as an environment variable is a firstmate convention carrying a
W3C-formatted value: W3C Trace Context standardizes the header, not an env
var, and OpenTelemetry SDKs do not read it automatically, so a downstream
must consume it deliberately; this feature parents no SDK span by itself.

Identity is per task, not per spawn: the carrier is minted with random ids on
the first spawn, adopted as a child (fresh span, same trace) for a nested
spawn whose parent already holds one, and reused verbatim from the meta on
relaunch, so a task keeps one stable logical identity across restarts. A
malformed or all-zero inherited value is treated as absent and roots a fresh
trace. A new root is sampled (01) - a sampling decision a downstream
parent-based sampler honors, not a guarantee that any collector stores a
span, and firstmate emits no spans; a child preserves the inherited flag.

Trust boundary: a firstmate-minted root is random and reads no prompt, path,
task prose, credential, or arbitrary environment key. An inherited
TRACEPARENT is opaque caller-controlled data - up to 24 bytes of id passed
through after syntax validation - so whoever set it controls those bytes, a
bounded fixed-width channel rather than a general content or secret channel.
The feature adds no OTEL_* variable, no tracestate, and no arbitrary
environment injection; it runs no configurable or arbitrary command, only the
fixed local od and tr (resolved from PATH) to read a few bytes of entropy - a
small local pipeline with no network or watchdog and no hard latency
guarantee. Any entropy or validation failure that returns omits the carrier
without aborting the spawn. A default-off spawn leaves the generated meta and
launch environment unchanged.

Enablement is default-off (config/trace-context, or FM_TRACE_CONTEXT where a
non-empty value overrides and unset or empty defers to the file) and is
propagated into secondmate homes, taking effect at each agent's next launch:
a Secondmate launched or relaunched after enablement carries the primary
trace into its nested workers, while an already-running Secondmate roots new
traces for its own workers until relaunched. Injection reuses the existing
GOTMPDIR channel, so all spawn backends and harnesses and the ship, scout,
and secondmate paths are covered.

Covered by a pure-library suite and a spawn-path integration test (fake tmux
plus a real worktree, hermetic against ambient FM_TRACE_CONTEXT) proving the
recorded and injected carriers are identical and sent before launch, that
default-off writes and injects neither, that a relaunch reuses the recorded
carrier, and that an explicit FM_TRACE_CONTEXT overrides the file both ways;
plus a source-owner inheritance test proving trace-context propagates and
absence-mirrors through propagate_inheritable_config.

Documentation follows the repository documentation-audiences contract:
docs/trace-context.md is maintainer-architecture rationale, the configuration
schema lives in docs/configuration.md, and the repeatable test evidence is
separated into docs/verification/trace-context.md (maintainer-verification),
registered in docs/documentation-audiences.json.

* fix(spawn): propagate the effective trace-context decision to secondmates

FM_TRACE_CONTEXT overrode trace context only in the process that read it. A
newly launched secondmate decided enablement from the inherited
config/trace-context file alone, so the override did not cross the
primary-to-secondmate boundary: FM_TRACE_CONTEXT=off with the file present left
the secondmate's nested workers traced (a broken kill switch), and
FM_TRACE_CONTEXT=on with the file absent left them untraced despite the
inherited carrier.

Deliver the primary's effective decision to a newly launched secondmate as a
normalized on/off FM_TRACE_CONTEXT in the launch prefix, so a FM_TRACE_CONTEXT
override governs the nested primary -> secondmate -> worker chain both ways, not
just the copied file. The value is bounded to the literal on/off and does not
broaden environment injection; the already-running secondmate boundary is
unchanged.

Add a genuine two-level spawn regression that drives fm-spawn twice with the
exact environment the primary injects into the secondmate and proves both
divergent directions end to end. Correct the documentation that implied
secondmate coverage on every backend, since orca and cmux reject secondmate
spawns, and refresh the verification evidence for the new assertion count.

* no-mistakes(review): Clarify Secondmate trace-context launch snapshots

* no-mistakes(document): Correct trace-context documentation ownership and relaunch semantics

* fix(spawn): resolve the trace-context decision once for carrier and snapshot

The effective trace-context decision was read twice per spawn: once inside
fm_trace_context_resolve for the recorded carrier, and again for the secondmate
FM_TRACE_CONTEXT launch snapshot. A config-file change between the two reads
could pair a carrier with the opposite enable state - an injected carrier with
an off snapshot, or no carrier with an on snapshot.

Freeze the effective on/off decision once, drive the carrier resolution under
that frozen FM_TRACE_CONTEXT so it cannot independently re-read the file, and
reuse the same frozen decision for the secondmate launch snapshot. Add a
spawn-path regression that drives the file-decided path and proves the recorded
carrier and the delivered snapshot always agree, and refresh the verification
evidence for the new assertion count.

* no-mistakes(review): Preserve legacy Secondmate trace boundary

* no-mistakes(document): Correct trace-context verification comparison base

* no-mistakes(review): Captain, prevent failed trace delivery metadata claims

* no-mistakes(review): Captain, align trace-context tests and verification evidence

* no-mistakes(document): Correct trace-context verification evidence

* no-mistakes(lint): Suppress intentional ShellCheck literal-dollar warnings

* no-mistakes(review): Captain: freeze trace context at session start

* no-mistakes(test): Captain: stabilize scheduler test and document Kimi trace coverage

* no-mistakes(document): Document trace-context safety boundaries

* fix(trace): fail off on stale session snapshots

Publish each home session decision atomically through a same-directory temporary file and bind it to the current session lock. A replacement failure can no longer leave an earlier on decision active in a later session; missing, stale, malformed, or unpublishable state defaults safely to off.

Add regressions for read-only replacement and failed publication, update spawn and session-start fixtures for the lock-bound format, and refresh the architecture and verification records.

* no-mistakes(review): Fix trace spawn failure independence and duplicate safety

* no-mistakes(document): Refresh trace-context documentation and verification

* no-mistakes(review): Clear partial backend input after failed trace submission

* no-mistakes(review): Stop unsafe trace delivery before launch append

* no-mistakes(document): Document unsafe trace delivery handling

* fix(trace): bound each trace to one routed task, never the routing agent

A persistent Secondmate holds its launch-time TRACEPARENT in the process
environment for its whole life, and routed requests never replace it, so
resolving new-task carriers from the ambient environment chained every
routed task into one ever-growing trace per Secondmate with distinct
parent ids. Resolve now reuses the task's recorded carrier or mints a
fresh sampled root, never reading ambient TRACEPARENT, so each routed
task is its own trace boundary while relaunch, recovery, and
scout-to-ship promotion keep one stable per-task identity.

The spawn regression models the reviewed scenario exactly: two unrelated
tasks spawned sequentially through one persistent Secondmate environment
record and inject distinct trace ids, adopt nothing from the Secondmate's
carrier, and a relaunch of the first task reuses its original carrier
verbatim.

* docs(trace): define the per-task trace boundary

The design contract is one task per trace: a persistent Secondmate is
routing infrastructure with its own agent identity, never a shared trace
root for the unrelated tasks routed through it. Root/recovery semantics
replace the removed child-inheritance path, the sampling and safety
sections drop inherited-carrier language because ambient TRACEPARENT is
never read, and the verification page records the refreshed suite
inventories including the two-task Secondmate boundary regression.

* test(trace): adopt the explicit per-task delivery contract in spawn fixtures

Rebasing onto current main brings the explicit per-task delivery contract:
ship spawns now require --mode and --yolo instead of resolving them from the
project registry. The trace spawn fixtures pass the same explicit contract
canonical spawn tests use, preserving the per-task trace boundary coverage
unchanged, and the verification page records the refreshed comparison base.
* fix(bin): classify tmux agent liveness independent of process titles

`fm_backend_tmux_agent_state` attributed a pane solely from
`#{pane_current_command}`, which is a process TITLE a harness can rewrite,
not a structural fact. Claude Code 2.1.220 reports its version string there,
so a live Claude endpoint classified `ambiguous`: the session-start secondmate
liveness sweep could no longer see it, and any consumer that gates on a
positive classification refuses outright.

Read a second, independent name source: the kernel `comm` of every process in
the pane tty's foreground process group. Either source naming a verified
harness yields `alive`, because a false `dead` is the one verdict that can
start a duplicate agent on a live worktree. Scoping to the foreground process
group rather than the pane's descendants keeps a harness-named background
process from faking an agent, and covers multi-process launchers (the Pi
Launcher path) without a special case.

Verified on 2026-08-03 against all seven adapters running for real on tmux
3.6a / macOS 26.5.2 arm64: claude 2.1.220, codex-cli 0.146.0, opencode
1.18.11, pi 0.82.0, pi-signed 0.82.0, grok 0.2.118, kimi 0.31.1 all classify
`alive`, each attributed by a source independent of its title.

Two tests, because they fail for different reasons:
- tests/fm-tmux-agent-liveness.test.sh pins the logic with real processes and
  no harness, so it runs everywhere CI runs tmux. It drives the two name
  sources apart on purpose and asserts the divergence, so no case can go
  quietly vacuous.
- tests/fm-harness-liveness-drift-live-e2e.test.sh relaunches every installed
  harness and fails naming the harness and version when one stops being
  attributed by a title-independent source.

AGENTS.md section 4 carries the resulting standing rule, and
firstmate-coding-guidelines owns how to satisfy it.

* no-mistakes: apply CI fixes

* docs: move the harness-dependent-check policy out of AGENTS.md

The standing rule was stated in AGENTS.md section 4 with the mechanics in
firstmate-coding-guidelines, which split one contract across two owners and
charged every session for a rule that only fires when firstmate's own
harness-dependent code is being changed.

firstmate-coding-guidelines is now the single owner of both the rule and how
to satisfy it: real-harness proof required, that proof authorized to spend
tokens, structural signals preferred over vendor-rendered surfaces, and a
guard that fails loudly naming the harness and version where a surface signal
is unavoidable. No inline stub is left behind, because AGENTS.md already
carries the load trigger for that skill in sections 7 and 13, so it is read
before any change to firstmate's shared tracked material.

Also records the cross-platform lesson the pipeline caught in the portable
regression, and corrects that file's header: the divergence assertion lives
on the version-string case, which diverges on both supported platforms,
rather than on every case.

* no-mistakes(review): Harden tmux liveness identity and drift validation

* no-mistakes(document): Clarify cross-platform tmux liveness documentation
…#1609)

* feat(bin): trace remote secondmate routes and unify the inherit allowlist

Per-task W3C trace context (kunchenguid#995) resolved and injected its carrier only at
the local spawn path. A remote secondmate is routed through
spawn_remote_secondmate, which returns long before that site and wrote its own
metadata block, so a remote secondmate stayed silently untraced even with the
capability enabled.

The parent home still owns that task's identity, because it holds the metadata
an observer reads. It now resolves the carrier against the task's own meta
under its own frozen decision - reused verbatim on relaunch, freshly rooted
otherwise, never adopting the parent process's ambient TRACEPARENT - and hands
it to the configured host through a new fm-spawn --traceparent argument,
accepted only for a secondmate launch and only as a strict W3C value. The
remote host exports it at the same unconditional pre-launch site and reports
back the carrier its endpoint actually holds, which the parent records, so an
already-alive endpoint reports the identity its agent really received rather
than one the parent merely intended. Disabled remains byte-identical and off.

The remote inherit path also carried its own hardcoded copy of the inheritable
config set, already drifted from FM_INHERITABLE_CONFIG by trace-context. Both
remote ends now derive from that one declaration, so a future item cannot be
sent by one side and refused by the other, and session-scoped enablement items
are skipped on live convergence exactly as the local path skips them.

Also fixes a latent stderr leak: an absent session lock printed a raw redirect
failure, which the new remote resolve site made visible.

Adds tests/fm-remote-secondmate-trace-context.test.sh, driving the real
parent -> fm-on -> remote entrypoint -> control -> remote fm-spawn chain over
the deterministic SSH boundary and reading the carrier back from the remote
pane's own log.

* no-mistakes(document): Clarify remote trace and allowlist contracts
* feat(bin): widen the remote runtime PATH and add a remote doctor preflight

The fixed remote entrypoint hard-coded a four-directory PATH, so a remote
account whose tools live under nix or a per-user profile could not run basic
Firstmate work without a login shell. The entrypoint now composes its child
PATH from the code root's bin, the account's ~/.local/bin, the common
package-manager directories that actually exist on the host, and the portable
system tail, deduplicated and in a fixed order, still under env -i with the
same variable allowlist and no shell command string.

fm-remote-doctor.sh reports that exact PATH by inheriting it from its own
entrypoint launch rather than recomposing it, so the ordering keeps one owner.
It is read-only, reports where each required and optional tool resolved, and
exits non-zero naming every required tool that did not. Remote seeding runs it
as a preflight before anything is created on the host and restores the registry
when it fails.

* no-mistakes(review): Harden remote git authorization and missing-tool diagnostics

* no-mistakes(document): Document remote PATH doctor and safe shims

* no-mistakes(lint): Fix ShellCheck findings in remote path tests

* no-mistakes(lint): Suppress exported fixture's false-positive ShellCheck warning
* feat(bin): gate remote second mates on herdr readiness

A remote second mate now always runs on the Herdr backend, whose server
belongs to the host's GUI login session and therefore outlives the SSH
connections that supervise it. fm-spawn's remote route forces that backend
and the host-local control script refuses any other, so the requirement
cannot be dropped from either side.

fm-remote-doctor.sh becomes the single owner of what "ready" means. It keeps
its PATH and tool reporting from kunchenguid#1623 and adds the Herdr, Aqua LaunchAgent,
GUI-session, server-reachability, and entrypoint-symlink checks, tagging each
gap fixable: or human: with the exact operator step. --fix closes only the
automatable gaps - writing and loading the Aqua-scoped dev.firstmate.herdr
launch agent, starting the server where no launch agent applies, and
recreating the entrypoint symlink - then re-derives every check from the host,
so a human gap is never presented as fixed. It never creates a login session,
writes an auto-login password, or touches FileVault.

Remote seed, remote spawn, and the startup liveness relaunch all run the same
check, repair, re-check sequence through one shared library and fail closed
with the doctor's own gap text. Recovery inherits the gate because it respawns
through the same route.

Tests drive the real doctor against a controlled account fixture with a
private HOME, a state-backed launchctl, and a fake herdr, and prove the
dangerous actions are never attempted. The remote lifecycle suites gain a
stateful Herdr CLI fixture and answer the readiness gate at the SSH boundary,
so they never inspect or repair the runner's own account.

* no-mistakes(review): Validate launch-agent contract and confirm Herdr startup

* no-mistakes(review): Validate loaded launch-agent contract before readiness

* no-mistakes(review): Refuse legacy remote backends without altering routes

* no-mistakes(review): Clarify conditional remote readiness repair sequence

* no-mistakes(review): Repair remote readiness before liveness probing

* no-mistakes(review): Preserve unknown seeds and reject legacy liveness

* no-mistakes(document): docs: clarify remote Herdr backend ownership
…1659)

* Pin remote secondmates to fm-remote

* no-mistakes(review): Fail closed on legacy remote Herdr endpoints

* no-mistakes(review): Isolate fm-remote launch agent from interactive default

* no-mistakes(document): Document shared remote Herdr retirement safety
kunchenguid and others added 29 commits August 6, 2026 01:52
* fix(muse): trust the settled session log as idle

The credentialed multi-step smoke on Muse Code 0.1.0-R708.1 answered the one
question the idle half was held back for: one real 75-second tool-loop turn with
23 tool batches stays inside exactly one run started/terminal pair, and an
Escape mid tool loop closes that run as cancelled rather than leaving the turn to
continue in another run. A settled log is therefore a finished turn, not a pause
between the runs of one turn.

Remove fm_busy_muse_idle_verified and FM_BUSY_MUSE_IDLE_VERIFIED_VERSIONS
outright rather than pinning them to a version: the session log's own metadata
carries only semver 0.1.0 and a build sha, so a version allowlist could not
actually match the running build and would be false precision. A settled log now
classifies idle, an open run still classifies busy, and only a resolution
failure - no binding, no matching log, an unreadable or run-free log - stays
unknown.

Record the evidence in docs/verification/muse.md, including the run-scoped grep
the counts must use, and keep the post-upgrade re-check guidance.

* no-mistakes(review): Document Muse idle trust and remove stale gate reference

* no-mistakes(document): Clarify Muse idle verification ownership
…guid#1798)

* feat(session-start): order the startup digest for truncation safety and bound its bulk

The digest is delivered through a harness that truncates an oversized payload
from the tail, and it really has been truncated: a 70KB digest arrived as lines
1-435 of 578, cutting off eight lines before the live-task inventory. That
session took the helm without ever seeing which tasks were live or where their
endpoints were.

Three changes, one file's worth of composition:

- FLEET STATE is emitted before CONTEXT, so a truncated tail drops curated
  memory - stable session to session, already governed by a captain-set budget,
  recoverable with one targeted read - instead of live fleet identity. The
  LOCK/BOOTSTRAP/WAKE-QUEUE safety preamble keeps its order. The read-once
  contract moves out of the closing reminder into its own section ahead of both,
  and now names the condition that voids it: a stage the truncation banner
  reports as never emitted.

- Status-tail lines are capped per line, reusing the cut the wake digest's OPEN
  DECISIONS section already applies. An observed tail line ran 865 characters
  and nothing bounded it. The cut and its marker now live in one place,
  bin/fm-line-cap-lib.sh, so the two digests cannot drift apart; each task's
  full status log path is still printed beside its tail.

- The backlog listing is composed as a recovery input: done rows are never
  listed, every in-flight, held, and blocked row is shown in full with its hold
  and blocked-by metadata, and only the dispatchable-now listing is bounded -
  with an exact remainder count and the command that shows the rest.
  FM_SESSION_START_QUEUED_LIMIT (default 20) replaces
  FM_SESSION_START_BACKLOG_LIMIT, which bounded the whole listing
  indiscriminately and so could drop a held or blocked row.

Tests exercise the real digest output: section ordering with the preamble
pinned, the per-line cap and its marker, and the backlog composition including
the remainder counters on both the tasks-axi and manual paths.

* no-mistakes(document): Clarify digest source recovery comments
…kunchenguid#1842)

A captain decision opened by a keyed needs-decision:/blocked: status line
orphaned as permanently open whenever the answer kicked off work: the
worker's next event is working [key=<workstream>] in a different key
namespace, so no resolved [key=<decision>] ever landed and the OPEN
DECISIONS fold kept listing the answered decision forever.

Remove the writer-dependency at its source: the answering firstmate
already holds the decision key when it sends the answer, so fm-send's new
--resolve-key flag (repeatable) appends the closing resolved line to this
home's own state/<id>.status after the submit is confirmed. The close is
a local ledger append for crewmates, local secondmates, and remote
secondmates alike - a remote mate's escalations reach this ledger through
the parent-replies ingest, so only the answer message crosses the
transport.

Safety: each named key must currently be open per the authoritative
status_open_decisions fold or fm-send refuses before sending; a failed or
unconfirmed send never closes a key; an append failure after a delivered
answer exits nonzero with the manual close command so the decision
re-surfaces instead of silently vanishing; a send without the flag closes
nothing, and working:/done: still never clear a captain decision.

Complementary fixes: the wake-drain OPEN DECISIONS section prints the
answer-with-close command hint at the moment of use; brief scaffolds
separate resolved's two duties (keyed-phase end vs decision closure) and
state that a done:/working: line never closes a decision even when the
answer started that work, keeping worker self-close for blockers that
clear without a firstmate reply; AGENTS.md and docs/architecture.md carry
the one-line pointers to the fm-send contract.
…1836)

* feat(secondmate): seed a remote home from a supplied project origin

Remote seeding required a local projects/<name> clone purely to read
`git remote get-url origin` into the provisioning manifest, so setting up
a remote second mate forced disposable clones and no-mistakes inits in the
primary home for projects that home has no reason to hold.

Firstmate now resolves the origin itself and names it as <project>=<origin-url>.
The seed validates and transports what it is given, and the receiving host
re-validates it rather than trusting the sender; bin/fm-project-origin-lib.sh
is the single owner of which URLs are accepted, refusing executable remote-helper
transports, option-shaped values, and unusable spellings at both ends. A bare
<project> still reads an already-present clone's origin, so nothing that works
today has to change. Registry consistency is unchanged: an unregistered or
local-only project is still refused.

A remote seed therefore creates nothing in the primary home beyond the route,
the charter, and its launch record.

The lifecycle test now seeds a registered project the primary has never cloned
and asserts the primary project tree is byte-identical afterwards, alongside
refusals for a missing origin, an unsafe origin, a local-only project, and an
unregistered project.

* no-mistakes(review): Clarify project origin documentation ownership

* no-mistakes(document): Document supplied-origin remote seeding contract

* feat(secondmate): accept project origins from any host or forge

Firstmate is a shared template, so a project origin must be able to name any
host: GitHub Enterprise on a private domain, GitLab hosted or self-hosted,
Bitbucket, Gitea, Codeberg, sr.ht, a bare IP, an SSH config alias, or a plain
server nobody else has heard of. The validator already decided on structure
rather than on a forge allowlist, and this makes that guarantee explicit and
closes the two gaps that a host-agnostic rule exposed:

- a bracketed IPv6 literal in the scp-like form is now accepted, so a host
  reachable only by address is not excluded
- a "/../" traversal inside a local or file: origin is now refused, because
  that names a path on the cloning host's own filesystem

The library is the single owner of the accepted forms, and its header says
plainly that there is no host, domain, or forge allowlist and there must never
be one. The skill keeps its distinct agent-operating lines (the agent resolves
and supplies the origin; a remote seed creates nothing in the primary home
beyond the route, the charter, and its launch record) and points at the library
for URL acceptance and at the operator doc for the rest.

The lifecycle test now drives Bitbucket, a self-hosted enterprise domain, a
self-hosted GitLab over ssh with a port, and a bare scp-like custom host through
the real seed, manifest, transport, and remote provisioning path in one seed,
asserting each URL reaches git unchanged and each clone carries its own origin's
content. The unit matrix leads with non-GitHub hosts for the same reason.

* no-mistakes(review): Validate project origin authorities safely

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes
…guid#1851)

* fix(tests): copy the whole bin/ tree into the old-vs-new conformance shim

main went red on tests/fm-backend.test.sh's "fm-send --key: old vs new
exit code" assertion, which reads as an fm-send fail-closed regression from

build_old_bin enumerated by hand the sibling scripts it copied into the
synthetic pre-refactor tree. kunchenguid#1842 made bin/fm-send.sh source
bin/fm-line-cap-lib.sh (added by kunchenguid#1798) and the list never learned about it,
so the pinned old fm-send.sh aborted at `. "$SCRIPT_DIR/fm-line-cap-lib.sh"`
under set -eu and exited 1 before parsing a single argument, while the
current one delivered the key and exited 0. The parity check compared a
crashed process against a working one and reported a behavior divergence
that never happened - the more so because BASE_REF collapses to HEAD on
main, where both sides run byte-identical source and a genuine divergence is
impossible. fm-send's --key exit path is unchanged and its fail-closed
contract is intact.

Copy the tree whole instead of enumerating it. An enumerated list has to be
extended by hand every time an entrypoint gains a dependency and is the only
thing that knows; it has been patched a dozen times for exactly that. A
whole-tree copy has nothing to forget. Extracting a refactored entrypoint the
baseline does not have now fails loudly instead of writing an empty file.

Only old-vs-new parity covered that exit contract, and parity is near-vacuous
on main. Pin it directly: tests/fm-send-strict.test.sh drives delivery both
ways from one stub and asserts an undelivered key exits nonzero naming the
key, so swallowing that error fails the suite.

* no-mistakes(review): Materialize historical fixture dependencies from baseline

* no-mistakes(document): Clarify fm-send key regression scope
* fix(bin): mirror the whole remote secondmate status stream

A remote secondmate's reply channel required corr=<16hex> on every line and
failed the entire delta when one line lacked it, so the cursor could never
advance past that line and the channel wedged permanently.

The charter tells a secondmate to report its own progress phases and to raise
new decisions with no correlation token, because correlation only answers a
marked parent request. Those lines were therefore unrepresentable on the remote
channel, while a local secondmate writes them straight into the parent's status
file.

Treat the channel as what it is: a mirror of the mate's status stream. A remote
mate now presents the same status and decision model as a local one, so a newly
raised needs-decision reaches the parent's open-decision fold identically, and
correlation goes back to being a per-line property that settles a pending
request rather than a gate on the stream.

Only what crossing a machine boundary genuinely adds stays behind: cursor
continuity, confined document fetch and rewrite, at-most-once append, and
control-byte normalization that rewrites bytes without ever dropping a line.
Line framing and size bounding already belong to fm-remote-delta-read.sh. A
document the remote reader refuses is named in one escalation instead of
stalling the stream, while an unavailable transport still leaves the delta for
the existing retry.

* refactor(bin): give the remote reply stream one append owner

Every line entering the parent status stream - a mirrored line, the continuity
escalation, and the undelivered-document escalation - now goes through one
at-most-once append, so the idempotence a replayed generation depends on is
stated once instead of copied at three call sites.

* no-mistakes(review): Keep local document transfer failures retryable

* no-mistakes(review): Isolate reply headers and normalize payload bytes

* no-mistakes(review): Correct remote reply mirror contract wording

* no-mistakes(review): Update remote reply script catalog description

* no-mistakes(document): Document remote status-stream mirroring
…chenguid#1847)

fm_secondmate_parent_record_parse read the .fm-secondmate-parent record
with bash's read, which drops NUL bytes - and different bash generations
disagree on the result: 3.2 truncates the value at the NUL while 5.x
splices the surrounding bytes together. A NUL-bearing parent_home could
therefore resolve to a home the record's bytes never name contiguously,
and which home fm-teardown.sh's promised-public-reply resolution read
(registration, registry, relay state) - or whether that protection
engaged at all - depended on which interpreter ran the cleanup.
Reproduced end to end: the same NUL-bearing record cleaned up under bash
5.x by resolving the spliced-together registered parent, while bash 3.2
refused it as unresolved, and a literal truncated path refused under
both.

Reject any NUL byte in the record before field parsing, putting corrupt
records in the same fail-closed bucket as duplicate fields, malformed
local bindings, unsupported routes, and symlinked records. The
regression test drives the real bin/fm-teardown.sh over the proven
clean-cleanup fixture with a NUL spliced mid-path into the recorded
parent_home, so before the fix it reproduced the wrong-home cleanup and
now it must refuse with the explicit binding refusal.
…unchenguid#1853)

* docs(secondmate-provisioning): require record intake for an inherited domain

A new mate seeded for an existing or inherited domain previously pulled in
charter, inherited config, captain-shared preferences, project clones, and
queued backlog rows with zero instruction about the domain's shipped history,
so it assumed a greenfield domain. A live backlog keeps only the configured
recent Done entries, so an inherited queue structurally over-represents plans
and under-represents deliveries, and already-delivered work resurfaced as open.

Add a record-intake step to the creation/seed path: classify greenfield versus
existing or inherited, and for the latter reconcile every inherited plan
against origin/main plus the live deployment, take only genuinely open work
and still-live durable knowledge, never carry a plan row for shipped work, and
record what could not be reconciled. Greenfield domains are untouched.

The skill owns the procedure; the backlog handoff section carries a one-line
reinforcement at the point where plan rows actually move.

* no-mistakes(document): Clarify secondmate record-intake scope
…guid#1860)

* perf(session-start): run every network check off the blocking path

The session-start digest runs on a session-open hook that blocks session
initialization, and every external-network call it made was individually
unbounded: `gh auth status`, secondmate liveness, secondmate convergence,
pending remote handoff delivery, and the fleet-sync fetch. One unreachable
remote secondmate could consume the whole FM_SESSION_START_TIMEOUT and
truncate the digest, so a slow network could cost the work queue itself.
Measured against a host hanging 25s per SSH connection, that startup took
1m18s.

The digest is now composed from local reads alone. bin/fm-startup-network.sh
runs the same checks concurrently in a bounded detached worker and the digest
harvests whatever finished, without ever waiting. Same fixture: 0.84s.

Nothing is dropped. fm-bootstrap.sh stays the single owner of every sweep and
still runs all of them, through a new FM_BOOTSTRAP_NETWORK phase split whose
`skip` and `only` halves are a partition of the unsplit run. Deferral is safe
because the sweeps are idempotent detectors, the result is durable and always
surfaces (inline, or as a `check: startup-network` wake), and the worker
re-verifies that the fleet lock still names the session that asked before it
mutates anything. While the worker is still running the digest names exactly
what is unconfirmed rather than implying it passed.

A relaunch performed by the deferred pass is now always reported, because the
digest that printed the superseded endpoint record is already out.

Also collapses the duplicate tasks-axi compatibility probe: the verdict is
computed once and handed to the bootstrap child for one process hop, then
consumed so it never reaches a spawned agent's environment. 10 tasks-axi
invocations per startup become 7.

Verified on Claude Code 2.1.222 that a worker detached by the session-open
hook survives the hook returning, the one vendor behavior this design needs
and no portable test can see.

Re-landed on current main, superseding PR kunchenguid#1845, which was cut from a
pre-kunchenguid#1842 base. The digest's section numbering in AGENTS.md section 3 now
states the emission order directly - supervision block and its read-once
contract, fleet state, network checks, then context - which keeps kunchenguid#1826's
fleet-state-before-context ordering. The old-bin test shim keeps main's
git-archive baseline from kunchenguid#1851, which already subsumes this branch's reason
for widening that shim.

* docs(verification): re-measure the deferred startup stage on the current base

Re-runs the unreachable-remote latency fixture against default-branch tip
8398d31 rather than the now-historical 345de4e, and records the sweep-result
comparison the deferral's safety argument rests on: the deferred worker's
published report is byte-identical to the three sweep lines the blocking
baseline printed, with the unreachable route preserved in both.

* no-mistakes(review): Fail deferred startup when report publication fails

* no-mistakes(document): Document deferred startup network behavior accurately
* fix(procevent): apply a captured adapter result in code, not by instruction

A remote secondmate's reply was captured and announced, but never applied.
Nothing dispatched the reply adapter's `handle` on a `procevent remote-reply`
wake, and the handling instruction named only the generic acknowledgement, so
the wake was retired while everything it carried was dropped: the reply never
reached the secondmate's local status mirror, the request it answered kept
escalating as a missed report, and the relay - whose registration each capture
retires, and which only that same handling re-arms - was left dead until the
next session start armed it again.

Applying such a result carries no judgement, so it belongs in code. After
publishing, the runner now calls
`bin/fm-procevent-<adapter>.sh autohandle <source-id> <sequence> <result-file>`
and lets the adapter apply and acknowledge its own result, through the same kind
of seam that already owns the terminal verdict. It runs strictly after terminal
retirement, because a handling adapter re-arms its own next source and retiring
afterwards would drop that fresh registration. An adapter with no such command,
or one whose pass does not complete, leaves the result unacknowledged and
therefore still announced, so a handler receives it exactly as before.

Resolving the request was not enough on its own either. An escalation opens a
durable keyed decision in the parent status log, and nothing ever closed it, so
a request the remote had answered kept surfacing in every later open-decisions
fold. The pending-reply library now owns both ends of that decision: it opens
one under a per-request key rather than the shared default key, and closes it
once the record resolves, appending the closing line only while that exact
decision is still open in the fold so it can neither double-close nor clear an
unrelated decision that has since taken the same key.

The handling instruction still routes a wake to its adapter, now as the
idempotent confirmation of what the runner already did rather than as the
guarantee.

Verified end to end in a throwaway isolated home driving the real armed source,
blocking delta reader, runner, and wake queue, with the handler doing only the
generic acknowledgement and no part of the ingest stubbed: before, seven failed
observations reproducing the incident; after, none. Each half is independently
load-bearing - without the runner change the reply never reaches the mirror,
without the escalation close the settled request still surfaces as an open
decision.

* no-mistakes(review): Prevent legacy reply closure from masking decisions

* no-mistakes(review): Serialize pending reply resolution and escalation closure

* no-mistakes(review): Serialize pending reply escalation with resolution

* no-mistakes(review): Clarify guarded legacy escalation closure behavior

* no-mistakes(review): Guard legacy closure and reserve pending reply keys

* no-mistakes(review): Match pending reply escalations by construction

* no-mistakes(document): Document automatic remote reply resolution

* no-mistakes(lint): Fix unused concurrent escalation loop variable

* no-mistakes(lint): Fix unused concurrent resolution loop binding

* no-mistakes(review): Version fold cache and gate autohandle on publication

* no-mistakes(document): Clarify remote reply relay documentation
…kill (kunchenguid#1841)

Bring the public installer-facing stow skill up to the internal skill's
current curation behavior while keeping it fully standalone:

- Replace the total-capture thesis with the compact-operating-map framing.
- Add read-the-destination-before-writing with the inspect-then-update
  triad (supersedes what, one-sentence rewrite, delete stale now).
- Add the concrete prune list together with its unique-fact guard, as an
  accuracy discipline with no size-budget machinery.
- Curate every memory file the pass has open, not only the routed one.
- Add the standing-decisions sweep category.
- Add the stronger-owner pointer-over-copy test before filing.
- Add tool-agnostic task-note discipline (inspect, classify, considered
  replacement body, never blind-append) and blocked-on recording.
- Give .stow-notes.md a closed set of three exits.
- Forbid storing, creating, or editing a skill as a stow destination.
- Report per-file action verbs in the completion receipt.
- Consolidate the repeated local-vs-external and .gitignore prose and fix
  the second-person voice slip, so the file does not grow (11334 -> 11276
  bytes).
…nchenguid#1917)

* fix(hooks): keep tracked Claude entries inert under grok 1.0.0 hooks

Grok loads Claude-compatible settings, so the tracked `.claude/settings.json`
hook entries also fire under Grok. They were meant to be inert there, guarded
by `[ -z "${GROK_AGENT:-}" ] || exit 0`. That guard silently stopped working.

Verified from the live process environment of a wedged grok 1.0.0 Stop hook on
2026-08-07: a grok 1.0.0 HOOK process carries GROK_HOOK_EVENT, GROK_HOOK_NAME,
GROK_SESSION_ID, and GROK_WORKSPACE_ROOT, but no GROK_AGENT. The observed hook
process was labelled `GROK_HOOK_NAME=project/settings:stop[0].hooks[1]`, which
is the Claude-only auto-arm entry.

Consequence: Grok ran `bin/fm-claude-stop-autoarm.sh` synchronously. Grok has
no `asyncRewake`, so it waited on the foregrounded watcher for that entry's
declared 28800-second timeout and the Grok turn never ended - the operator saw
an infinite "Responding".

Widen the guard to `[ -z "${GROK_AGENT:-}${GROK_HOOK_EVENT:-}" ] || exit 0` on
the five entries that have a `.grok/hooks/` counterpart: both Stop entries, the
SessionStart entry, and the two PreToolUse Bash entries.

Two deliberate limits:

- The guard is NOT widened to GROK_SESSION_ID. Grok injects it into every child
  process, so it can survive into a Claude session that Grok launched and would
  silently disable Claude's own watcher continuity. GROK_HOOK_EVENT is
  per-hook-invocation and does not leak that way.
- `bin/fm-subagent-pretool-check.sh` stays unguarded on purpose. It is the one
  tracked entry with no `.grok/hooks/` counterpart, so guarding it would remove
  the guard from Grok entirely rather than deduplicate it. The new test asserts
  it stays unguarded so the exception cannot be closed silently, and
  docs/subagent-guard.md is honest that the coverage it leaves is partial.

`bin/fm-harness.sh` corrects a comment that presented GROK_AGENT as reliably
present; it is a fast path only, and the ancestry walk is what actually
guarantees grok identification.

tests/fm-turnend-guard.test.sh adds test_tracked_claude_entries_inert_under_grok,
which runs every tracked entry under a real grok 1.0.0 hook environment, a
legacy GROK_AGENT environment, and a native Claude environment.

* no-mistakes(document): docs: sync grok hook-marker guard facts to owners

* no-mistakes(review): docs: state grok guard criterion by event coverage
… stage (kunchenguid#1918)

The deferred network stage published one aggregate started/finished pair, so
a run that took a minute could not be attributed to a phase, a host, or a
clone without re-running it by hand under manual tracing.

Add bin/fm-timing-lib.sh as the single owner of elapsed-time records, and
bracket each network owner with one: the gh auth probe, the secondmate
liveness sweep, secondmate convergence, pending handoff delivery, and the
project clone refresh, plus one record per secondmate for the remote-touching
steps (id and host) and one per project clone. Each record carries a start
offset from one shared origin, so the artifact reads as a timeline.

The stage publishes them beside its report as state/.startup-network.timings,
for a timed-out or failed run too, where the partial record is the answer.
Only the on-demand `report` command prints them: `harvest` composes the
session-start digest, so its output, the wake cadence, and every other part
of a normal session start are unchanged.

Recording is inert unless a run asks for it, so nothing else that sources
these scripts pays for it. Details are identities only - a detail carrying
whitespace is refused rather than cleaned up, which is what keeps a command
line, an environment dump, or a captured error out of the file.

Split two per-item loop bodies into their own functions so each iteration can
be timed; every `continue` became a `return 0` with the same meaning, and the
sweeps still run directly, in the same order, returning the same results.
…kunchenguid#1928)

* feat(stow): cascade the internal /stow to every registered secondmate

Invoked in a primary home, /stow now sweeps every registered secondmate
after the primary's own required pass, enforcing the same startup-memory
threshold in each home against that home's own allowance rather than a
fleet total.

bin/fm-stow-cascade.sh owns the mechanical inputs: it enumerates each
registered secondmate exactly once from data/secondmates.md, reports that
home's own budget accounting, and resolves how the sweep reaches it. A
live agent sweeps its own home so its uncaptured session knowledge is
captured too; a local home without one is curated in place; a remote home
without one is accounted read-only and deferred, because there is no
generic remote write path for a home's own memory files. Every host-
crossing step and each home's accounting runs under one hard bound, so a
slow or unreachable home reports an exception and the sweep continues.

Nothing changes until /stow is invoked: no new notification, digest
section, or background work. The public skills/stow skill is untouched.

* no-mistakes(review): fix(stow): extend cascade --help range to include full exit-code contract
…nguid#1927)

29 fm-remote-job-worker.sh processes were found running at ppid 1, 1-2 days
old, each still polling and appending to a log inside a no-mistakes gate
worktree that had already been returned.

Three things combined to make that possible:

- The recorded worker.pid is the serving child, not the restart supervisor
  above it, so a teardown that stops that one pid only makes the supervisor
  respawn. The Linux start path also left the worker tree in the launching
  command's process group, so there was no group to signal instead.
- Neither the serving loop nor the supervisor ever rechecked whether its
  configured FM_ROOT still existed, so a worker launched from a worktree
  outlived that worktree indefinitely.
- The supervisor restarted a failing child with a fixed 0.1s delay and no
  bound, which is what grew the logs (~66MB/day measured).

The Linux start path now puts the worker tree in its own process group, and
fm_remote_job_stop_worker_tree signals that whole group - refusing any group
whose leader is not itself a worker, so a worker from an older build or from
launchd's own session is still stopped safely as a single process. The worker
stops itself once its code root stops being a Firstmate checkout, confirmed
across a grace window so an ordinary transient cannot stop a healthy worker.
The supervisor backs off and gives up rather than restarting forever.

bin/fm-remote-job-reap-orphans.sh is the belt-and-suspenders sweep for workers
already orphaned that way, wired into fm-teardown.sh. Its reap condition is
exactly "the code root named in the worker's own command line is gone", which
is why the account's healthy LaunchAgent worker and every live remote
secondmate worker are never candidates.

The two suites that leaked these in the first place now stop the worker tree
rather than the recorded pid alone.
…henguid#1925)

* fix(bin): lint only the changed shard locally, full lint in CI

Two ships hitting fm-lint.sh at once could spike CPU to 190% and load
to 8.58 on a captain's Mac, even though each run finishes quickly.
fm-lint.sh now defaults to linting only the canonical-set files
changed since the merge-base with origin/main (including uncommitted
edits) on an ordinary local branch, using plain local git with no
network calls. It still lints the full canonical set in CI
(GITHUB_ACTIONS=true or CI=true), on the main branch, or whenever no
merge-base can be found, so CI coverage never depends on a local diff.
Explicit paths keep bypassing this selection entirely.

* no-mistakes: apply CI fixes
* feat(bin): add deterministic agent lifecycle control

Separate firstmate's data plane from its control plane.

bin/fm-send.sh is the data plane: conversational text, always
routing-marked for a kind=secondmate target. That marking is right for a
message and wrong for a lifecycle command - a marked "/quit" arrives as
ordinary chat the agent reasons about instead of executing.

bin/fm-control.sh is the control plane: allowlisted interrupt, exit, and
transactional relaunch verbs addressed to an exact task id, with
per-harness mechanics owned by the executable bin/fm-control-lib.sh
rather than improvised in agent prose, and a verified postcondition for
every action. There is no arbitrary-text and no raw-key entry point.

relaunch runs as a transaction with a durable journal: it resolves the
profile, proves the work it must preserve is recoverable, records the
required progress note, stops the old agent, then delegates the launch
to its single owner, bin/fm-spawn.sh --relaunch, which adopts the
recorded endpoint and worktree instead of creating either. A refusal
before the stop leaves the record and instructions byte-identical; a
failure after it reports the concrete state rather than claiming an
agent that is not running. Teardown and discard stay separate and
explicit.

exit and relaunch require a backend with a recovery-grade agent-state
classifier, so zellij, orca, and cmux are refused rather than reported
as successful blind. A remotely placed secondmate is refused by name,
because its agent runs on a host where none of these postconditions can
be read.

* fix(control): resolve a recorded harness to its adapter before retiring wiring

fm-spawn arms per-task harness wiring on prefixes, because a task
launched from a raw command records that command's basename rather than
the exact adapter name. The control plane's retirement tables are keyed
by the exact adapter, so a task recorded as `grok-2` had its turn-end
token, private registry entry, and worktree hook pointer armed and never
retired - leaving a registry entry that outlived the agent that owned
it.

State the prefix rule once, in the capability owner, and resolve the
recorded value through it before every table lookup. bin/fm-send.sh's
composer-clear lookup reads the same owner instead of keeping its own
copy of which adapters need one.

* test(control): pin muse session-binding retirement across a harness switch

* no-mistakes(review): Resolve prefixed harnesses across lifecycle control verbs

* no-mistakes(review): Report interrupt delivery without fabricating cancellation state

* no-mistakes(review): Clear disabled relaunch trace context atomically

* no-mistakes(review): Clarify control interrupts and restore legacy send state

* no-mistakes(review): Refuse ambiguous relaunches and report exit delivery

* no-mistakes(review): Revalidate interrupts and accept interrupt-stopped exits

* no-mistakes(review): Lock descendant tasks before forced recursive teardown

* no-mistakes(document): Align lifecycle adapter documentation with control plane

* no-mistakes: apply CI fixes

* fix(bin): serialize fresh task publication with forced teardown

Forced secondmate teardown enumerated a home's task set, locked what it
found, then re-enumerated while removing. A fresh spawn takes only its
own per-task lock, so a record published inside that window was
invisible to the preflight and visible to the cleanup: it was
destructively processed while never lifecycle-locked.

Reproduced with real agents. A record published 0.249s after teardown
began was removed, its window closed, and its worktree returned to the
pool - while both commands reported success. A per-task lock cannot
protect a task that does not exist yet.

Add a per-home task-set lock guarding WHICH tasks a home has, as opposed
to the metadata lock guarding one task's record. Teardown takes it per
home, parent before child, before enumerating and holds it through
cleanup. A fresh spawn takes it before its own per-task locks and holds
it through publication; a relaunch is exempt, because it republishes an
existing task already covered by that task's control lock.

Either the spawn publishes first and the teardown's preflight covers it,
or the teardown owns the set and the spawn refuses. Both directions fail
closed, and both are pinned by tests that hold the lock rather than
racing on timing.

* no-mistakes(review): Serialize remote secondmate publication with forced teardown

* no-mistakes(review): Preserve remote spawn routing and state initialization

* no-mistakes(review): Serialize teardown when descendant state is absent

* no-mistakes(review): Cover symlinked descendant state refusal

* no-mistakes(document): Document task-set serialization safeguards

* no-mistakes(lint): Isolate task-set lock path resolution

* no-mistakes: apply CI fixes
* feat(stow): tiered decaying memory with captain-gated offload to local excluded skills

Implement the captain-adopted /stow redesign from the v2 tiering report as
amended by the adoption decision:

- Per-entry trailing HTML-comment markers with three tiers named for their
  handling: pinned (no clock, no eviction), aging (stale after 30 days),
  perishable (stale after 7 days, mandatory checkable expiry condition).
- File-scoped defaults (captain.md and captain-shared.md pinned,
  learnings.md aging) with a self-describing legend line per file header.
- Reinforcement requires session evidence; re-reading memory never counts.
- Archive-not-delete: stale and budget-evicted entries move with provenance
  to the never-injected data/memory-archive.md; prune always means the cold
  tier, and a stale unique fact is never deleted.
- Captain-gated over-budget offload: staleness evaluated before scope, the
  sweep runs only when still over budget after decay and consolidation,
  proposals go through the receipt plus one durable captain-held backlog
  item, migration runs through the destination's normal path, and the
  memory entry leaves only once the destination is live.
- Offload destination per the adoption decision: a user-owned skill under
  .agents/skills/<freeform-name>/ excluded via the local .git/info/exclude,
  with the hard rule that stow never creates or writes a tracked skill.
- Five graduation moves, receipt verbs archived and proposed-offload, and
  the one-time non-destructive migration of unmarked legacy entries.

The public skills/stow/SKILL.md mirrors the generic parts (markers, decay,
archive exit, user-approved on-demand offload exit, migration) with no
firstmate-specific paths.

The load-bearing assumption that a git-excluded skill is still discovered
was verified empirically against Claude Code 2.1.226 (direct
.git/info/exclude scratch-repo test plus an in-repo ignored-probe test);
the dated evidence is recorded in docs/verification/stow-memory.md.

The graduation list's deletion move is deliberately narrowed to duplicates
already preserved by a stronger owner, reconciling the v2 report's retained
'deletion of a stale entry' wording with its own prune-always-archives
rule.

* no-mistakes(review): Persist legacy migration grace across stow passes

* no-mistakes(review): Enforce archival invariants and exempt default-pinned legacy entries

* no-mistakes(review): Clarify offload scope, archive placement, and marker boundaries

* no-mistakes(review): Enforce aging fallback and verify excluded skill loading

* no-mistakes(review): Fix stow decay, pinned offload, and archival safeguards

* no-mistakes(review): Preserve pinned entries, approvals, and archive provenance

* no-mistakes(review): Restrict stow mutations to editable memory files

* no-mistakes(review): Clarify skill destinations, collision checks, and migration legends

* no-mistakes(review): Resolve exclude paths for linked worktrees

* no-mistakes(review): Secure per-home excluded skill migration

* no-mistakes(test): Require explicit tier markers on new stow entries

* no-mistakes(test): Route missing shared legends to primary owner

* no-mistakes(document): Align stow documentation with tiered memory

* fix(stow): converge the pass on an over-budget home (dogfood D1-D3)

The dogfood run against a copy of the real over-budget home showed the
pass increasing the deficit from 624 to 1,107 estimated tokens and the
relief ladder provably unable to reach budget. Three skill-text fixes:

- D1: markers become single-token spellings (<!--a:DATE-->, <!--p:DATE-->,
  <!--P-->, <!--g-->), entries matching a pinned file default carry no
  marker, the per-file policy legend collapses to a one-line pointer
  naming the stow skill as the scheme owner, and marker/pointer bytes are
  explicitly counted content - roughly 76% less metadata cost on the
  dogfooded home's first installment.
- D2: the eviction rung gains a convergence precondition - total the
  eligible pool first, and when archiving all of it cannot reach budget,
  skip eviction entirely, archive nothing for budget reasons, and report
  the exempt pinned floor as the concrete inability in the final step.
- D3: budget eviction considers only dated aging entries; <!--g-->
  legacy-grace entries are ineligible until their grace cycle resolves,
  so eviction cannot cancel promised grace or invert against validation.

Public skill mirrors the D1 marker/pointer changes; D2/D3 are internal
because the public skill has no budget ladder.

* no-mistakes(test): Enforce evidence-only reinforcement during stow migration

* no-mistakes(document): Clarify stow receipt marker actions
* docs: add firstmate vision

* no-mistakes(test): Classify VISION.md as public product documentation

* no-mistakes(document): Restore approved one-file vision diff

* no-mistakes: apply CI fixes
* fix(spawn): force regular Pi TUI for crews

* no-mistakes(document): Documented Pi regular TUI launch mode
* fix(cmux): classify borderless Claude composer

* no-mistakes(review): Normalize cmux NBSP prompts across locales

* no-mistakes(document): Document cmux borderless Claude composer classification
Brings kunchenguid/firstmate main (85e750a, 43 commits since a83be60) into
the reconciliation branch as a second parent, so origin, primary, and current
upstream ancestry are all reachable from one line.

Eleven files conflicted and were resolved to keep both sides' work:

- AGENTS.md, .agents/skills/harness-adapters/SKILL.md: keep the local
  claude-settings-backup and Kimi effort rows alongside upstream's
  muse-session row and muse adapter row; take upstream's Relay rename.
- bin/fm-brief.sh: keep the local no-human/decision-hold guidance, take
  upstream's resolved-line rule, which matches the new --resolve-key
  close-at-answer-time behavior.
- bin/fm-spawn.sh: keep the local Kimi effort env prefix and add upstream's
  muse launch template and muse effort mapping; keep the local Claude
  settings-hook path and drop upstream's mkdir -p "$WT/.claude", which would
  defeat fm-claude-worktree-hook.sh's dir_created bookkeeping and leave an
  empty .claude behind at teardown.
- bin/fm-supervision-lib.sh, docs/turnend-guard.md: keep the local
  FM_SUP_NEEDED gating and take upstream's model-aware health description,
  which is what the merged fm-guard.sh and fm-turnend-guard.sh actually do.
- bin/fm-teardown.sh: union of the removed state files, keeping upstream's
  meta-lock release.
- docs/documentation-audiences.json: both sides' surfaces, sort order kept.
- docs/sessionstart-nudge.md: keep the local shared session-lock predicate,
  which is what both merged wrappers call, plus upstream's payload and
  run-wrapper paragraphs.
- docs/verification/runtime-backends.md, tests/fm-guard-stale-banner.test.sh:
  both sides' sections and test cases.
Upstream's pre-claim fast path in bin/fm-lock.sh decided self-ownership by
strict pid equality, which contradicts this line's single ownership owner:
a session whose lock records another pid of its own contiguous harness run
was refused the home it already holds. It now asks
fm_session_lock_owned_by_self, exactly as the claimed path below it does.

tests/fm-sessionstart-nudge.test.sh: bin/fm-lock.sh records a harness pid
resolved from its own ancestry, never a plain shell pid, so the owned-lock
fixture writes the harness pid instead of the test shell's. The fixture had
been unreachable state since the nudge moved onto the shared predicate.
@yelenplays
yelenplays merged commit 31b728d into main Aug 10, 2026
13 checks passed
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.

7 participants