Skip to content

fix: preserve fleet state in truncated session-start digests - #1798

Merged
kunchenguid merged 2 commits into
mainfrom
fm/fm-sessionstart-digest-reorder-trim-r1
Aug 6, 2026
Merged

kunchenguid merged 2 commits into
mainfrom
fm/fm-sessionstart-digest-reorder-trim-r1

Conversation

@kunchenguid

Copy link
Copy Markdown
Owner

Intent

Promote a digest-composition investigation's recommendations (its sections 5.1-5.3) into one coherent ship in bin/fm-session-start.sh, the firstmate session-start digest. Captain authorized. This is firstmate shared, tracked material, so firstmate-coding-guidelines applies: one-owner rule for contracts, colocated tests in tests/ that exercise behavior through the executable interface rather than implementation source text, one sentence per line in Markdown, plain dash never em dash, no agent co-author, and shellcheck-clean via bin/fm-lint.sh.

Motivating evidence from the investigation: the digest is delivered through a harness that truncates an oversized payload from the TAIL. A measured 70KB digest was really delivered as lines 1-435 of 578, cutting off eight lines before the live-task inventory, so that session took the helm without ever seeing which tasks were live or where their endpoints were. The CONTEXT block (five curated memory files) was 50.4% of the digest and sat in front of the recovery-critical fleet state.

Three required changes:

  1. REORDER (highest value; a safety change, zero bytes saved). Emit the FLEET STATE section BEFORE the bulky CONTEXT memory files, so tail truncation drops recoverable curated memory instead of recovery-critical live fleet identity (live-task inventory, window/worktree/backend/endpoint-liveness). Use the existing SESSION_START_STAGES ordering contract. Explicitly do NOT reorder the LOCK/BOOTSTRAP/WAKE-QUEUE safety preamble. Move or duplicate the NEXT STEP read-once contract near the top so it too survives truncation.

  2. CAP STATUS-TAIL LINE LENGTH. print_status_tail had no per-line bound; an observed status line ran 865 characters. Cap each tail line at 220 reusing the OPEN DECISIONS cap shape already in bin/fm-wake-drain.sh (a ' [truncated]' marker). This is safe because the section already prints each task's full status log path for targeted follow-up, and AGENTS.md section 8 already treats a status line as a wake event rather than current state (bin/fm-crew-state.sh owns current state).

  3. BACKLOG LISTING. Drop done rows at startup (tasks-axi supports --state; retained completion history is a reporting surface, not a recovery input). ALWAYS keep every in_flight and held row with its hold_kind/hold_reason. Bound the plain queued listing, preferring the ready set plus all held/blocked rows in full plus an accurate '(N more queued)' remainder counter. Never silently truncate blocked or held items.

Scope boundaries the captain set: this is complementary to, and must NOT duplicate, a separate already-merged full-read safety-net change. It does NOT touch AGENTS.md. It must not change the startup memory budget scope or its estimator (separately deferred).

Required tests: colocated tests for the reorder ordering (FLEET STATE precedes CONTEXT, safety preamble unchanged), the status-tail per-line cap, and the backlog-state filtering (done dropped, in_flight/held always kept in full, queued bounded with an accurate '(N more)' counter), exercising real script output rather than source text.

Decisions and tradeoffs I made while implementing, which a reviewer reading only the diff would not know:

  • I chose the section order LOCK, BOOTSTRAP, WAKE QUEUE, SUPERVISION, READ-ONCE CONTRACT, FLEET STATE, CONTEXT, NEXT STEP. Supervision stays between the wake queue and context exactly as AGENTS.md section 3 already states in prose.
  • I MOVED the read-once contract rather than duplicating it, because the one-owner rule forbids two copies of the same contract. Because it now precedes the payload it governs, I added the condition that voids it: a STARTUP TRUNCATED banner naming a stage that never ran. The closing NEXT STEP reminder now points back at it instead of restating it.
  • I added a new stage name 'read-once' to SESSION_START_STAGES so the truncation banner still names exactly which stages never ran; the existing banner test was updated for the new order.
  • I did NOT reorder content WITHIN the FLEET STATE section (backlog listing still precedes the work-under-way inventory). AGENTS.md section 3 item 5 enumerates that internal order and AGENTS.md is out of scope; change 3 shrinks the backlog listing enough that the intra-section risk is much reduced. This was a deliberate scope decision, not an oversight.
  • For the 220 cap I extracted the cut and its marker into a new shared owner, bin/fm-line-cap-lib.sh, and refactored bin/fm-wake-drain.sh's OPEN DECISIONS section onto it, rather than copying the logic into a second place. fm-wake-drain.sh keeps its own composite policy (the 4000-byte global cap and its 'N more omitted' disclosure); the lib owns only the per-line cut. The drain passes a per-item allowance one short of the cap because its global budget also charges for the trailing newline, preserving its existing byte math exactly. The lib exposes an assigning form (fm_cap_line_var) and a printing form (fm_cap_line) built on it, one rule with two call shapes, so the drain - which runs at the top of every wake-handling turn - pays no command substitution per item.
  • The cap counts characters rather than bytes. Status lines are plain ASCII in practice so the two coincide, and character semantics can never cut a multibyte character in half into an invalid sequence. This matches the existing OPEN DECISIONS shape it reuses.
  • I added a regression test for the OPEN DECISIONS per-item cap, which previously had none, since I refactored that code path.
  • For the backlog I composed four tasks-axi group filters (--state in_flight, --state held, --state queued --blocked, and tasks-axi ready) rather than post-processing one listing, so this script never reimplements task state. The groups can overlap: an in-flight item that is also held appears in both, which I disclose in the group label. Only the ready group is bounded, so the remainder counter is exact.
  • I renamed FM_SESSION_START_BACKLOG_LIMIT to FM_SESSION_START_QUEUED_LIMIT and changed its default from 80 to 20. This is deliberate: the knob's meaning genuinely changed (it no longer bounds the whole listing, and bounding the whole listing was unsafe because it could drop a held or blocked row), so keeping the old name would have been misleading. The old name is referenced nowhere else in the repo. docs/configuration.md's env inventory records the new knob.
  • The manual (non-tasks-axi) backend path got the same rules: done rows dropped, every in-flight title line kept, every queued title line whose own text carries a hold or blocked-by marker kept, the rest bounded with an exact remainder. Its keep-pattern uses bracket expressions rather than backslashes because awk's -v applies escape processing before the regex is compiled.
  • I dropped tasks-axi's per-group help block from each of the four composed listings. Keeping them would have repeated the same two pointers four times, each carrying this home's full backlog path, giving back much of the saving; the section already prints one equivalent pointer of its own.
  • I tightened one pre-existing assertion that checked for the bare string 'STARTUP TRUNCATED', because the read-once contract now legitimately names that banner as the condition that voids it; the test now pins the actual banner line.

Verification performed: bin/fm-lint.sh clean, bin/fm-doc-audience-check.sh clean, bin/fm-test-run.sh --check-coverage clean, and bin/fm-test-run.sh --changed ran 63 test scripts with zero failures. I also ran the real digest end to end against a throwaway home with the real tasks-axi installed to inspect the rendered output.

What Changed

  • Move the read-once contract and FLEET STATE ahead of bulky CONTEXT content while preserving the lock, bootstrap, and wake-queue safety preamble.
  • Add a shared 220-character line cap with a [truncated] marker for session status tails and wake-drain open decisions.
  • Omit completed backlog rows, retain all in-flight, held, and blocked rows, and bound ready queued rows with an exact remainder count and documented configuration.

Risk Assessment

✅ Low: The change is well-bounded, conforms to the stated digest ordering and truncation requirements, preserves existing wake-drain byte accounting, and adds behavioral coverage through executable output.

Testing

The author-reported baseline had already covered lint, documentation checks, coverage registration, and 63 changed test scripts; this phase reran the focused session-start and shared OPEN DECISIONS behavior suites and captured an actual CLI digest proving the safety preamble remains first, FLEET STATE precedes CONTEXT, the long status event is capped at 220 characters with its recovery path retained, done work is absent, held/blocked/in-flight work remains visible, and seven ready items bounded to three disclose an exact four-item remainder.

Evidence: End-to-end session-start digest

Actual CLI digest shows LOCK -> BOOTSTRAP -> WAKE QUEUE -> SUPERVISION -> READ-ONCE CONTRACT -> FLEET STATE -> CONTEXT -> NEXT STEP. It retains in-flight, held, and blocked rows, shows 3 of 7 ready rows with “(4 more queued)”, omits done rows, and caps the long status line at exactly 220 characters with “ [truncated]”.


================================================================================
SESSION START - /var/folders/0k/bf8mwt2n5qddzk24r20gfk0c0000gn/T//fm-session-start-tests.gz4N0P/reviewer-evidence/home
================================================================================

LOCK
--------------------------------------------------------------------------------
lock acquired: harness pid 66699

BOOTSTRAP
--------------------------------------------------------------------------------
MISSING: quota-axi (install: npm install -g quota-axi)

WAKE QUEUE
--------------------------------------------------------------------------------
OPEN DECISIONS (still open, folded from the durable status logs - not just the latest line):
evidence-task needs-decision: [key=visual-proof] a very long status event padding padding padding padding padding padding padding padding padding padding padding padding padding padding padding padding paddi [truncated]
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  watcher supervision needs Stop-owned automatic recovery; inspect the hook registration and startup status before ending the turn.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
================================================================================
SUPERVISION OPERATING INSTRUCTIONS - primary harness: claude
================================================================================
Current state:
- Lock: held by this session; this session owns normal supervision unless away mode says otherwise.
- Away mode: inactive.
- X mode: inactive; use the default watcher cadence.
- Ordinary wake: the Stop-owned auto-arm (bin/fm-claude-stop-autoarm.sh) already owns watcher continuity; drain and handle the wake, and do not arm another cycle yourself.

Mode: Claude Stop-hook-owned supervision.

When this session owns supervision and away mode is not active:
1. Drain first with `bin/fm-wake-drain.sh`.
2. Routine watcher arm and re-arm are owned by the Stop `asyncRewake` hook (`bin/fm-claude-stop-autoarm.sh`), never by you.
   Every turn end while supervision is needed launches or attaches one home-scoped watcher cycle with no model command and no model tokens.
   An actionable close wakes you through the hook's exit-2 rewake, delivered as a `Stop hook feedback` message.
3. On a `Stop hook feedback` wake (`signal:`, `stale:`, `check:`, or `heartbeat`), run `bin/fm-wake-drain.sh` first and handle the wake.
   Do not run `bin/fm-watch-arm.sh` after an ordinary wake; the next turn end re-arms automatically when supervision is still needed.
   Do not invent a wake from an attach-status line alone; drain and act only on real wake records, the drain's `OPEN DECISIONS` entries, or a real watcher reason line.
4. On the one `Stop hook feedback` automatic-mechanism failure notice (`firstmate watcher auto-arm FAILED ...`), drain, inspect the automatic mechanism failure, and do not turn the notice into a repeating manual-arm loop.
5. If the Stop hook does not claim the home or reports an exhausted failure, inspect its registration and watcher startup path before ending blind.
   Keep the Stop-owned automatic mechanism as the only Claude arm owner.
6. Treat `watcher: started ...` and `watcher: attached ...` inside automatic arm output as proof that one live cycle exists.
   On attach, the arm follows verified identity-matched successors instead of exiting when the first cycle ends.
7. The durable wake queue preserves actionable events between a rewake and the next Stop-launched arm, while the bounded turn-end guard prevents a blind Stop when recovery did not start.
   No PreToolUse hook denies fleet commands based on watcher status.
   [`watcher-continuity.md`](../watcher-continuity.md) owns the exact session-lock recovery boundary.
8. The turn-end guard (`bin/fm-turnend-guard.sh --claude`) remains the final backstop.
   It requires the PID-strict live-watcher and fresh-beacon predicate at the Stop boundary, while the mid-turn pull guard accepts a fresh beacon without a live process under Claude's between-turns auto-arm model.
   It allows the stop when a watcher is healthy or the role-verified auto-arm owns recovery, while fresh failure epochs advance the bounded one-time attended fail-open progression described in [`turnend-guard.md`](../turnend-guard.md).
9. Waiting on the hook-owned cycle is silent: do not send idle progress while the watcher is parked.

The watcher itself remains `bin/fm-watch.sh`, and `bin/fm-watch-arm.sh` remains the verified arm wrapper that the Stop hook foregrounds.
Re-arm attaches to an existing healthy cycle when one is already present and follows its verified successor chain.
See [`watcher-continuity.md`](../watcher-continuity.md) for the arm-layer successor and clean-close failure contract and the Claude ownership model.


================================================================================
READ-ONCE CONTRACT
================================================================================
Everything below is printed in full for this session start: every state/*.meta,
a compact data/backlog.md listing, a bounded tail of every state/*.status,
data/projects.md, data/secondmates.md, data/captain.md, data/captain-shared.md,
and data/learnings.md.
Do NOT re-read any of them after reading this digest, and do NOT bulk-read
data/backlog.md or state/*.status: re-reading everything defeats the entire
point of this command.

Go to a source directly only when:
  - this digest flagged it ABSENT (then rebuild or create it per AGENTS.md),
  - its contents looked unparseable or corrupt,
  - an individual full status log is needed for older wake-event history, or a
    status line was capped and its tail matters (each task's full log path is
    printed with its tail),
  - a full task body is needed (tasks-axi show <id> --full, or data/backlog.md),
  - the backlog listing disclosed omitted queued items and this turn needs them,
  - or a STARTUP TRUNCATED banner named the stage that would have printed it, in
    which case that stage's sources were never emitted and must be reconciled.

================================================================================
FLEET STATE
================================================================================

data/backlog.md
--------------------------------------------------------------------------------
compact backlog listing (tasks-axi; done rows omitted; every in-flight, held, and blocked row shown in full; ready queued bounded to 3; task bodies omitted)

in flight:
count: 1
tasks[1]{id,state,kind,repo,title,blocked_by,hold_kind,hold_reason}:
  compact-startup,in_flight,ship,firstmate,Compact startup digest,none,captain,captain choice pending

held (captain- or time-gated; an in-flight item that is also held appears in both groups):
count: 1
tasks[1]{id,state,kind,repo,title,blocked_by,hold_kind,hold_reason}:
  held-queued,queued,ship,firstmate,Held queued work,none,captain,captain choice pending

blocked queued:
count: 1
tasks[1]{id,state,kind,repo,title,blocked_by,hold_kind,hold_reason}:
  blocked-followup,queued,scout,firstmate,Follow compact startup,compact-startup,"-","-"

ready queued (dispatchable now):
count: 7
ready[7]{id,state,kind,repo,title}:
  ready-1,queued,ship,firstmate,Ready item 1
  ready-2,queued,ship,firstmate,Ready item 2
  ready-3,queued,ship,firstmate,Ready item 3
ready_public_followups: 0 delivery-ready obligations
(shown 3 of 7 ready queued item(s))
(4 more queued - tasks-axi ready --file /var/folders/0k/bf8mwt2n5qddzk24r20gfk0c0000gn/T//fm-session-start-tests.gz4N0P/reviewer-evidence/home/data/backlog.md)
Full task bodies remain available on demand: tasks-axi show <id> --full when compatible tasks-axi is available, or data/backlog.md.

Work under way (state/*.meta)
--------------------------------------------------------------------------------

--- evidence-task ---
window=fm-evidence:live
worktree=/var/folders/0k/bf8mwt2n5qddzk24r20gfk0c0000gn/T//fm-session-start-tests.gz4N0P/reviewer-evidence/root
backend=codex
kind=ship
endpoint: dead (backend=codex window=fm-evidence:live)
status tail (last 5 line(s), each capped at 220 characters, wake-EVENT history, not current state; full log: /var/folders/0k/bf8mwt2n5qddzk24r20gfk0c0000gn/T//fm-session-start-tests.gz4N0P/reviewer-evidence/home/state/evidence-task.status):
needs-decision: [key=visual-proof] a very long status event padding padding padding padding padding padding padding padding padding padding padding padding padding padding padding padding padding padding padd [truncated]
working: short status event remains whole

Orphan status logs (state/*.status without matching .meta)
--------------------------------------------------------------------------------
(none)

AFK
--------------------------------------------------------------------------------
absent

================================================================================
CONTEXT
================================================================================

data/projects.md
--------------------------------------------------------------------------------
ABSENT

data/secondmates.md
--------------------------------------------------------------------------------
ABSENT

data/captain.md
--------------------------------------------------------------------------------
ABSENT

data/captain-shared.md (shared, main-authoritative, read-only in secondmate homes)
--------------------------------------------------------------------------------
ABSENT

data/learnings.md
--------------------------------------------------------------------------------
ABSENT

================================================================================
NEXT STEP
================================================================================
Follow the supervision operating instructions block above for harness 'claude'.
This script never starts supervision itself.

The digest above is complete for this session start. The READ-ONCE CONTRACT
section near the top of it governs what may still be read from disk.

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-session-start.test.sh
  • bash tests/fm-wake-drain-open-decisions.test.sh
  • Ran bin/fm-session-start.sh end-to-end through the test fixture with FM_SESSION_START_QUEUED_LIMIT=3, seven ready tasks, held/blocked/in-flight tasks, a done task, and an overlong status line; captured the complete emitted digest.
  • Inspected the captured section positions and backlog rows with rg; verified the emitted capped status line length with awk and confirmed the working tree remained clean.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…nd 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.
@kunchenguid
kunchenguid merged commit 345de4e into main Aug 6, 2026
13 checks passed
@kunchenguid
kunchenguid deleted the fm/fm-sessionstart-digest-reorder-trim-r1 branch August 6, 2026 17:29
kunchenguid added a commit that referenced this pull request Aug 7, 2026
…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. #1842 made bin/fm-send.sh source
bin/fm-line-cap-lib.sh (added by #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.
kunchenguid added a commit that referenced this pull request Aug 7, 2026
* 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. #1842 made bin/fm-send.sh source
bin/fm-line-cap-lib.sh (added by #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
kaku-san added a commit to kaku-san/firstmate that referenced this pull request Aug 7, 2026
…eltas (#12)

* feat: gate remote second mates on Herdr readiness (#1639)

* 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 #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

* fix: isolate remote secondmates in shared Herdr session (#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

* feat: route remote commands through an Aqua job worker (#1660)

* feat: run remote commands through Aqua job worker

* no-mistakes(review): Enforce remote job deadlines and safe worker shutdown

* no-mistakes(review): Refresh stale workers and harden dependency-free supervision

* no-mistakes(review): Harden worker ownership recovery and shutdown quarantine

* no-mistakes(review): Fix doctor bootstrap, harness repair, and output draining

* no-mistakes(review): Probe doctor tools through authenticated worker bootstrap

* no-mistakes(review): Refresh stale workers before doctor tool probes

* no-mistakes(review): Recover stopped quarantines and extend job deadlines

* no-mistakes(review): Separate queue and execution timeout windows

* no-mistakes(review): Supervise Linux worker crashes and bind root identity

* no-mistakes(review): Resolve authorized Nix profile bin links

* no-mistakes(review): Clarify Nix path resolution documentation

* no-mistakes(review): Harden PATH safety and nvm selection

* no-mistakes(review): Honor nvm system defaults and refresh doctor digest

* no-mistakes(review): Keep workers ready during active jobs

* no-mistakes(review): Bound pre-execution validation by job timeout

* no-mistakes(document): Clarify remote worker documentation

* no-mistakes(lint): Fix remote worker ShellCheck diagnostics

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* fix: clarify remote doctor bootstrap path (#1691)

* fix(bin): bound remote SSH dead-peer detection (#1699)

* fix(remote): arm SSH dead-peer detection in fm-on.sh

A vanished remote host mid-poll (a reboot, a dropped link) left ssh
blocked indefinitely on a half-open TCP connection, because fm-on.sh's
ssh invocation had no ServerAliveInterval/ServerAliveCountMax. This
wedged the remote-reply ferry: fm-procevent.sh's runner blocked inside
the ssh child and never reached its own no-result -> claim-release ->
reconcile re-arm self-healing path, which otherwise already handles a
nonzero exit with empty output correctly. Recovery required a manual
retire and re-arm.

Arm ServerAliveInterval=15 and ServerAliveCountMax=3 by default
(bounded ~45s detection window), both overridable via
FM_SSH_ALIVE_INTERVAL and FM_SSH_ALIVE_COUNT_MAX. This is a transport-
level fix in fm-on.sh, so it covers every remote command routed
through it, not just the reply ferry. The remote sshd answers
keepalive probes independently of whatever the remote command is
doing, so a legitimately long-but-alive command (a 55s poll, a clone,
the doctor) is never falsely killed - only a truly vanished peer trips
it, turning that case into a bounded, detectable ssh failure (exit
255) instead of an indefinite hang.

Extends tests/fm-on.test.sh with a behavioral regression asserting a
bounded, positive ServerAliveInterval/ServerAliveCountMax on the real
ssh argv captured through the FM_SSH_BIN process seam, plus coverage
that both are env-overridable.

* no-mistakes(document): Document SSH dead-peer detection ownership

* fix: report stale AXI tools during bootstrap (#1701)

* feat(bootstrap): gate stale axi CLIs at the floors firstmate actually uses

Add gh-axi 0.1.29 floor so bare --squash PR merges stop failing quietly on
older builds. Raise tasks-axi to FM_TASKS_AXI_MIN=0.2.2 (multi-id mv) while
keeping feature probes. Keep quota-axi at 0.1.16 after verifying schema 3
and per-model availability already ship there; runway remains optional.

* no-mistakes(document): Clarify AXI compatibility documentation ownership

* fix: prevent false watcher-down alarms in Claude sessions (#1661)

* fix(guard): stop false watcher-down alarm mid-turn under Claude auto-arm

bin/fm-guard.sh derived its watcher-health verdict from fm_watcher_healthy,
which requires a live watcher process holding the home lock. Under the Claude
Stop-hook auto-arm supervision model the watcher is armed at each turn end and
exits on its wake, so it runs only between turns. Every guarded command run
mid-turn therefore found no live watcher and printed the "WATCHER DOWN -
SUPERVISION IS OFF" banner even though supervision was healthy. Because the
episode key was derived from the beacon mtime (which the between-turns watcher
advances every poll), the full banner re-printed on essentially every command,
and the message always blamed a "fresh beacon" that was in fact fresh.

Make the pull guard's health check model-aware via a new
fm_watcher_supervision_verdict in bin/fm-wake-lib.sh:

- Under the auto-arm model a beacon fresh within FM_GUARD_GRACE is healthy even
  with no live watcher process; only a beacon stale beyond grace (or absent) is
  a genuine lapse and alarms.
- Under every persistent-watcher harness (codex foreground checkpoint,
  opencode/pi/grok background arm, tmux, unknown) a live identity-matched
  watcher with a fresh beacon is still required, unchanged.

The banner now names the true failing condition, a missing live watcher process
versus a genuinely stale beacon, instead of always blaming the beacon, and the
once-per-episode dedup keys on that condition rather than the beacon mtime so a
genuine lapse announces once and does not re-print each turn.

The turn-end guard keeps the strict fm_watcher_healthy check because it fires at
the turn boundary, where the auto-arm brings a fresh watcher up and it
cooperates with that arm. fm_watcher_healthy itself is unchanged, so the arm
layer's start/attach/replace decisions are unaffected.

Tests in tests/fm-guard-stale-banner.test.sh cover the auto-arm healthy
fresh-beacon-without-a-watcher case, the auto-arm stale-beacon alarm and its
stable episode, the true-reason banner wording, and the reason-keyed episode
surviving a beacon mtime change; existing persistent-model cases are pinned to
that model.

* no-mistakes(review): Pin secondmate supervision model to launched harness

* no-mistakes(document): Align watcher documentation with model-aware supervision health

* test: prevent fixture temporary directory leaks (#1704)

* fix(tests): stop fixture-tempdir helper from self-deleting under command substitution

fm_test_tmproot is almost always called as `TMP_ROOT=$(fm_test_tmproot prefix)`,
which forks a subshell to capture its stdout. The old implementation set its
EXIT cleanup trap inside that call, so the trap fired - and deleted the fixture
root - the instant the subshell exited, before the real caller's own EXIT trap
was ever installed. Every test using the documented call pattern leaked its
fixture root on every run; two suites had already independently discovered and
worked around this with ad-hoc mktemp calls.

Registration now goes through a $$-keyed registry file instead of in-process
state, since $$ resolves to the invoking shell's PID even inside the
subshell. The real cleanup trap is armed once at source time (always the real
caller, never a subshell) for EXIT, INT, and TERM. A best-effort orphan sweep
on next source reaps marked fixture roots old enough to be from a killed prior
run.

Simplifies the two existing ad-hoc workarounds (fm-procevent.test.sh,
wake-helpers.sh) back onto the shared helper now that it works correctly.

* no-mistakes(review): Preserve live fixtures during orphan reaping

* no-mistakes(review): Harden fixture ownership against PID reuse

* no-mistakes(review): Secure cleanup registry against path precreation

* no-mistakes(review): Make fixture registration transactional

* no-mistakes(document): Documentation already matches fixture cleanup behavior

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* feat(herdr): enable presentation spaces by default (#1708)

* feat(herdr): default presentation spaces on with an explicit opt-out

Herdr's disposable one-task presentation workspace was opt-in through the
presence of local config/herdr-presentation-spaces. It is now on by default,
and a home opts out by writing "off" into that same file.

Values are read with the whole-file whitespace-stripped convention the other
scalar config items already use, plus case folding. An absent file, an empty
file, and "on" all resolve on; only "off" opts out; an unrecognized value warns
and keeps the default rather than failing a spawn over a purely visual setting.
The empty file is exactly the historical opt-in form, so every home that had
already enabled the projection stays enabled with no migration step, and no
previously enabled home can be turned off by the flip.

Because absence now means on at both ends, secondmate inheritance needs no
item-specific convergence: mirroring an absent primary file converges a
secondmate to the same default-on rather than turning its projection off, and
only an explicit primary opt-out propagates the opt-out.

The gate itself moves into fm_backend_herdr_presentation_enabled in the Herdr
adapter so the semantics have one owner that regressions can exercise directly.

* no-mistakes(document): Document Herdr default-on presentation safety

---------

Co-authored-by: kunchenguid <kun-1@kunchenguid.com>

* fix(bin): surface fleet-wide open decisions on every wake drain (#1711)

* fix: surface consolidated open decisions on every wake-drain

A needs-decision or blocked event buried under later, unrelated status
appends was only ever shown via the last-line wake annotation, so a
still-open captain decision could go silently missed even though
status_open_decisions (fm-classify-lib.sh) already folds the whole
status stream correctly and fleet-snapshot/bearings already reuse it.

Wire that same fold into bin/fm-wake-drain.sh: a new fleet-wide
scan_open_decisions wrapper scans every state/<id>.status, and
fm-wake-drain.sh prints a separate, bounded OPEN DECISIONS section on
every drain (including the empty-queue fast path), so session-start
and every wake-handling turn surface it for free without duplicating
the open/resolved fold itself. Heartbeat wakes drain through the same
script, so this covers that surface too.

Also tighten status_open_decisions' file guard to skip an unreadable
status file instead of leaking a bash redirection error, now that a
fleet-wide directory scan can reach files a single targeted read
would not.

* no-mistakes(review): Prevent status symlinks leaking open decisions

* fix: drop unbounded perl subprocess from status symlink guard

The review step's own symlink-safety auto-fix (O_NOFOLLOW read via a
perl subprocess) forked one perl process per status file scanned by
the new fleet-wide open-decisions scan, with no cap - inflating
fm-wake-drain.sh's total external-read cost from 8 (the existing
annotation read_cap) to 18 in the enrichment-caps regression test.

The plain [ -L "$f" ] check already rejects any status file that is
itself a symlink before any read happens, which is exactly what the
new regression test exercises and is the same defense level the
sibling scan_captain_relevant_statuses/last_status_line already rely
on elsewhere in this file (no O_NOFOLLOW). Drop the subprocess-based
nofollow read and keep the cheap builtin guard.

* no-mistakes(document): Document actionable fleet-wide open decision drains

* fix(bin): abort parked runs and reap leaked processes before teardown (#1710)

* fix(bin): abort orphaned no-mistakes runs and reap leaked processes at teardown

Teardown could remove a task's worker while its no-mistakes pipeline run was
still parked at a gate, leaving an orphaned run holding a fleet slot
indefinitely (observed 2026-08-03: runs parked 7h39m and parked at a
post-CI approval gate). It could also leave backgrounded/disowned
descendant processes rooted under the worktree or tasktmp surviving
reparented to init (observed: two `go test` binaries pinning CPU for
hours with no live task meta to attribute them to).

Add two coupled pre-teardown steps, both scoped to this task's exact
branch/head or worktree/tasktmp so they can never touch another task's
run or processes:
- conclude_task_no_mistakes_run aborts a run parked at a gate via
  `no-mistakes axi abort`, cd'd into the exact worktree so the daemon
  resolves the run itself rather than teardown naming a --run id.
- reap_task_worktree_processes sweeps for processes whose cwd is under
  the worktree or tasktmp (via `lsof -a -d cwd`) and TERM/KILLs them.

Both run before any worktree return, branch delete, or backend kill,
and are idempotent on a retried teardown. The branch+head attribution
logic is factored out of bin/fm-crew-state.sh into the new shared
bin/fm-nm-run-lib.sh so both scripts use the same ownership contract.

* no-mistakes(review): Fail closed on incomplete teardown cleanup

* no-mistakes(review): Bind teardown cleanup to verified run and process identities

* no-mistakes(review): Require confirmed aborts and convergent identity-safe process reaping

* no-mistakes(review): Handle process exits during teardown identity checks

* no-mistakes(review): Restore teardown library in hermetic gotmp fixtures

* no-mistakes(document): Document teardown run attribution and timeout

* no-mistakes(lint): Rename shell variable conflicting with done keyword

* no-mistakes: apply CI fixes

* fix: resolve fm-remote-entrypoint.sh SCRIPT_DIR through a PATH symlink (#1709)

The script installs as a symlink under ~/.local/bin. Taking dirname of
the symlink itself (instead of its real target) pointed SCRIPT_DIR at
~/.local/bin, breaking sourcing of the sibling fm-remote-job-lib.sh.
Resolve the real path first, preferring python3's os.path.realpath,
then realpath, falling back to the raw BASH_SOURCE on hosts with
neither.

* fix(pi): gate Calm built-in overrides by activation state (#1724)

* fix(pi): stop Calm claiming a built-in tool name another extension owns

fm-calm.ts claimed bash/read/edit/write/grep/find/ls unconditionally at
extension load, regardless of whether Calm was on. Pi resolves two
extensions registering the same built-in name by first-registered-wins
with no merge and no unregister call, and Calm's project-local
.pi/extensions/ position beats any global or CLI-configured extension,
so a user who never even enabled Calm could have their own bash/read/etc
override silently replaced.

Captain-approved plan implemented:

- Registration is now gated on config/calm already being "on" at load
  time. A Calm-off session or reload registers nothing, so a non-Calm
  user never contests a name. This stays synchronous during the
  factory's own load, not deferred to session_start: /reload (and
  ctx.newSession/fork/switchSession) render the restored transcript from
  a pre-session_start snapshot of the tool registry, so a deferred claim
  would miss that render - confirmed by tests/fm-calm-pi-extension
  .test.sh's hidden-block-geometry E2E when trialed.
- The first time Calm turns on in a session that started off
  (activateBuiltInsIfNeeded, from the /calm command handler), Calm calls
  pi.getAllTools() - safe only once every extension has finished loading,
  unlike the load-time path above - to see whether a different extension
  already owns a name, and skips claiming only that one, leaving it and
  its owning extension fully intact and callable.
- A contested name found this way prints a prominent ctx.ui.notify()
  warning naming the tool, plus a console diagnostic.
- reportBuiltInLosses() remains the backstop for the one case neither of
  the above can reach: a session that starts or reloads with Calm already
  on, where the registry snapshot is taken before Calm gets any chance to
  check ownership. A symlink-safe realpath comparison avoids misreporting
  Calm's own registration as foreign when its path crosses a symlink
  (macOS /tmp, /var).

Confirmed, bounded trade-off: the very first time a session that started
Calm-off turns Calm on, tool-call rows already on screen from before that
toggle do not retroactively collapse, because Pi never lets an extension
re-point an already-rendered row at a definition registered later. Every
session after that first toggle starts with the preference already on and
takes the synchronous load-time path, so the guarantee is intact from
then on. docs/calm.md and the file's own header document this in full.

tests/fm-calm-pi-extension.test.sh gains test_builtin_gate_load_time
(config/calm off registers nothing, on registers all 7 synchronously at
load) and test_calm_activation_collision_and_regression_bound (first
activation claims every uncontested built-in, leaves a foreign bash tool
fully intact and callable, warns and logs the contested name, and locks
in the documented pre-activation bound against real ToolExecutionComponent
rendering). test_rendering_and_session_lifecycle and the live interactive
E2E are updated for the new gate-at-load and first-activation-bound
contract.

* no-mistakes(document): Document Calm tool collision boundaries

* no-mistakes: apply CI fixes

* fix(bin): persist secondmate parent bindings for cleanup (#1727)

* fix(bin): give secondmate homes a durable parent binding record

Finished-worker cleanup on a remote second mate refused forever with
"cannot resolve the primary home ... durable parent binding". The
remote launch hands the child the remote code checkout as its parent
home (fm-spawn.sh's sole writer of FM_PUBLIC_FOLLOWUP_PRIMARY_HOME
receives FM_HOME=$FM_ROOT from fm-remote-secondmate-control.sh's
host-local launch), and that path can never carry the parent's real
records, so the guard refused unconditionally once relay looked active
anywhere on that host.

fm-home-seed.sh and fm-remote-home-provision.sh now write a durable
.fm-secondmate-parent record next to the .fm-secondmate-home identity
marker, naming the home's route to its parent as local (with the real
parent path) or remote (with the parent's SSH alias for diagnostics
only). fm-teardown.sh's cleanup gate reads it: a remote parent is out
of scope for the delegated-promise check (the whole promised-public-
reply subsystem is same-filesystem by construction, so a remote parent
can never hold one), while a token committed directly to the child's
own .env file - never the process environment - still refuses, so an
unrelated export in the remote host's login shell can no longer mask
in. For a local secondmate, the durable parent_home now also backs up
the launch-time env var, closing a silent fail-open where a restart
that dropped the launch prefix made the guard treat a genuinely active
parent relay as off.

Regression coverage drives the real remote route (SSH boundary + Herdr
fixture) and real fm-home-seed.sh seeding rather than hand-crafted
markers.

* no-mistakes(review): Captain: fail closed on unsafe durable parent records

* no-mistakes(review): Captain: enforce durable parent binding commit protocol

* no-mistakes(review): Captain: publish local parent binding before identity

* no-mistakes(review): Captain: refuse conflicting local parent bindings

* no-mistakes(review): Captain: reject non-regular secondmate seed leaves

* no-mistakes(review): Captain: enforce unique durable parent bindings

* no-mistakes(review): Captain: reject route-incompatible durable parent fields

* no-mistakes(document): Document durable secondmate parent bindings

* no-mistakes(lint): Fix secondmate parent parser ShellCheck warnings

* no-mistakes: apply CI fixes

* feat(bin): enforce latest AXI-family tool floors (#1733)

* feat(bin): gate lavish-axi at its session_ended floor in bootstrap

bin/fm-procevent-lavish.sh decides that a human "Send & End" review is
terminal by reading session_ended from the poll response's leading session
block. That field first shipped in lavish-axi 0.1.35, so an older installed
build silently leaves every ended review source armed forever and captures
an empty ended result on each later cycle. The same release is what makes a
plain reopen refuse a session the human deliberately ended.

Add LAVISH_AXI_MIN=0.1.35 to the existing axi-family floor structure in
bin/fm-bootstrap.sh, reusing tool_version_at_least and the same MISSING
diagnostic gh-axi already emits, so an incompatible build is reported as an
upgrade request before any review surface is armed. Later lavish-axi
releases only add artifact-authoring surface the adapter never reads, so
the floor is the feature-introduction point rather than latest.

Fixtures that stubbed lavish-axi as a bare exit-0 tool would now be read as
unparseable builds, so tests/lib.sh gains fm_fake_version_tool and every
bootstrap-running suite uses it for lavish-axi.

* no-mistakes(review): Clarify lavish-axi version floor rationale

* no-mistakes: apply CI fixes

* feat(bin): set axi-family floors to current latest under the bump policy

The axi-family bootstrap floors are the CURRENT LATEST published version of
each tool, captain-bumped periodically to move the whole fleet onto the
newest axi tools. They are not the minimum feature-introduced version. The
earlier lavish-axi work set a feature-minimum floor, which is the opposite
of this policy, so replace it along with the older feature-minimum rationale
carried by tasks-axi and quota-axi.

State the policy explicitly in bin/fm-bootstrap.sh's header, which owns it,
and in each per-tool floor owner, so no future change argues a floor back
down to the earliest release that happens to satisfy some behavior. Remove
the lavish-axi session_ended and upstream-PR citation, the tasks-axi
multi-ID-mv minimum argument, and the quota-axi credential-source argument
as floor rationale; the tasks-axi feature probes remain as a separate
defense-in-depth concern.

Floors: lavish-axi 0.1.45 (was 0.1.35), tasks-axi 0.2.4 (was 0.2.2),
quota-axi 0.1.17 (was 0.1.16), gh-axi 0.1.29 unchanged and already latest.
Each was verified against the tool's current published version.

The mechanism is unchanged: the same shared version helper and the same
MISSING diagnostic path. The below-fires and at-or-above-silent regression
rows move to the new floors, keeping each boundary genuine by pinning the
patch immediately below each floor rather than a version that was only
below the old one. Fleet fixtures move to the new floors so a bootstrap-
running suite is not reported as an out-of-date build.

Three operator-facing backlog handoff and receipt errors named "0.2.2+"
while the enforced floor moved, so they now point at the floor's owner
instead of duplicating a version number that drifts.

* no-mistakes(review): Centralize AXI floor policy beside constants

* no-mistakes(review): Clarify bootstrap boundary test comment

* no-mistakes(document): Centralize AXI floor policy rationale

* fix(bin): bound open decision scans with incremental cursors (#1737)

* fix(bin): bound OPEN DECISIONS scan cost with a per-status-file cursor

The fleet-wide OPEN DECISIONS scan added in #1711 re-reads and refolds
every task's entire lifetime status log on every drain, so its cost
grows unbounded with total log size. Add status_open_decisions_incremental
and scan_open_decisions_incremental to fm-classify-lib.sh: they persist a
per-status-file byte cursor plus the folded open-decision set, and fold
only newly appended bytes on each call, reusing status_open_decisions'
exact fold-line rule (extracted into _fm_decision_fold_line) so the two
strategies can never disagree on what is open. A missing or invalidated
cursor (new task, truncated/rewritten/shrunk log) falls back to a full
re-fold. bin/fm-wake-drain.sh now calls the incremental wrapper instead
of the whole-file scan.

* fix(bin): add O(1) rotation detection and read-failure guarding to the cursor fold

Add the two pieces the incremental open-decisions cursor was missing,
scoped to this repo's actual status-file usage (create-once, append-only,
never replaced or rewritten in place):

- An O(1) device+inode identity check (one stat call) alongside the
  existing size-shrink check, so a status file replaced/rotated/recreated
  at the same path is detected and falls back to a full re-fold, even
  when the replacement is the same size. A same-inode, same-size,
  in-place byte edit is a deliberately accepted gap: no code path in
  this repo ever does that to a status file.
- Checked reads: a stat/wc/tail failure is a genuine I/O error, not
  "the file is empty" - it now reports the already-trusted persisted
  open set unchanged instead of risking a silent invalidation.

Both stay O(1) plus new bytes per call, matching the cursor's bounded-
cost design; no content hashing or pending-fragment machinery.

* no-mistakes(review): Preserve cursor state across failed incremental reads

* no-mistakes(review): Refold status when cursor cache reads fail

* no-mistakes(document): Document cursor-backed open-decision scanning

* no-mistakes: apply CI fixes

* no-mistakes(review): Harden teardown and remote runtime edge cases

* fix(bin): prevent remote polls from blocking session startup (#1754)

* fix(bin): preempt remote reply long-polls for queued short jobs

Session start on a home with live remote second mates could stall silently
for many minutes: the single serial remote job worker ran each armed
fm-remote-delta-read.sh reply poll to its full 55s window while bootstrap's
short sync, inherit, state, and route commands sat queued behind it, and
non-FIFO queue pickup let re-armed polls keep winning the lane. Measured
end to end, a trivial short job took 31s behind one 30s poll window.

The worker now preempts a running preemptible job (the read-only, cursor-
anchored delta read is the only member of that class) as soon as a
non-preemptible job is queued, publishing exit 75 with emptied output -
byte-identical to the poll's own elapsed-window-with-no-data result - so
the parent runner takes its existing no-result path and the watcher re-arms
from the same cursor with nothing lost. The delta read translates SIGTERM
into that same exit after removing its staging directory. Sibling polls
never preempt each other, so two armed monitors cannot churn. The same
measured scenario now completes in 1s.

* no-mistakes(document): Clarify remote poll preemption documentation

* docs: present X mode as the X and Discord public surface (#1778)

Discord mentions already ride the same pairing-token opt-in, relay poll,
and platform-aware reply path as X mentions, but the docs still read as
X-only, so a stranger could not self-serve the Discord path.

Add the numbered turn-on steps to the X mode configuration reference,
pointing at the myfirstmate dashboard for account creation, bot install,
and token issuance rather than duplicating operator setup here, and drop
the X-only framing from the README bullet, the documentation index, and
the architecture overview.

* fix(bin): run session start deterministically from hooks (#1781)

* feat(bin): run session start deterministically on hook-capable harnesses

Session start relied on a native nudge that only asked the agent to run
bin/fm-session-start.sh, and an agent can defer that. Observed 2026-08-01:
an /ahoy-first session followed the recap path and did not take the helm
until a later request forced it.

Claude, Codex, and Pi now RUN the digest in their session-open hook through
the new bin/fm-sessionstart-run.sh, so the full ordered digest is in model
context before the first turn. That wrapper is the single owner of what a
session-open source means: startup and Pi's "new" take the helm, clear and
compact re-emit, resume/reload/fork delegate to the nudge, and an unreadable
source takes the helm because doing that redundantly is idempotent while
skipping it is the bug. Grok and OpenCode keep the nudge as the floor, since
neither can carry hook stdout into a model turn.

Because the hook now blocks session initialization, fm-session-start.sh
bounds itself first. Its steps are not all individually bounded - bootstrap's
gh auth probe, tool version probes, the backlog listing and per-task endpoint
reads are unbounded - so the whole digest runs as one bounded child (default
120s). Whatever it emitted before the bound survives, and the parent adds a
loud STARTUP TRUNCATED banner naming the stage that stalled and every stage
that never ran, still exiting 0.

--reemit skips only the sweeps startup already reconciled. It still re-verifies
lock ownership and still drains queued wakes, which arrived after startup and
are the turn's work. fm-bootstrap.sh gains FM_BOOTSTRAP_LOCKED so a re-emit
keeps repair ownership instead of deferring to a lock holder that is itself.

Also adds bin/fm-timeout-lib.sh as the single owner of bounded execution,
replacing three near-identical copies, and gives the ahoy skill a helm check
so a nudge-tier harness cannot recap before taking the helm.

Verified live on 2026-08-05 against Claude 2.1.222, Codex 0.146.0, and Pi
0.82.0; docs/verification/supervision.md records the per-harness source
vocabulary, the two named gaps, and the refresh command.

* no-mistakes(review): Harden session-start completion, timeout, and Pi delivery

* no-mistakes(review): Harden completion ownership and portable timeout escalation

* no-mistakes(review): Normalize watchdog KILL exits without masking command status

* no-mistakes(review): Guarantee startup bounds and align harness delivery tiers

* no-mistakes(test): Fix Pi session-start live verification fixture

* no-mistakes(document): Align session-start documentation with deterministic hooks

* no-mistakes(lint): Silence intentional child-shell expansion lint warning

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* fix: rename X mode to Relay in user-facing docs (#1784)

* docs: rename the user-facing product name to Relay

The public-mention integration gated by the `.env` pairing token is now
called Relay across user-facing prose, covering X and Discord alike
instead of implying a single network.

Renames the product-name strings only: README, docs, the captain-facing
skill descriptions, and the AGENTS.md operating prose, including the
`X mode (.env)` and `Optional X mode` headings and every link anchor
that pointed at them. AGENTS.md section 14 carries a one-line bridge
note so the older name and the unchanged identifier spellings stay
discoverable.

Internal identifiers are untouched: `FMX_*`, `config/x-mode.env`,
`state/x-*`, `bin/fm-x-*`, the `fmx-respond` skill path,
`__FM_X_MODE_ENV__`, and `x-mode-error`. Platform references to X and
Discord as networks stay as they are, and the bootstrap-diagnostics
entry still quotes bootstrap's emitted `FMX: X mode on/off` line
verbatim because `bin/` output is out of scope for this pass.

* no-mistakes(review): Complete Relay prose rename in maintained docs

* no-mistakes: apply CI fixes

* no-mistakes(review): Honor --force in lsof-free teardown refusals; diagnose parent bindings

* feat: add Muse Code crewmate adapter (#1786)

* feat(harness): add a verified muse crewmate adapter

Muse Code joins the fleet as a crewmate/scout adapter, verified live against
Muse Code 0.1.0-R708.1 in an isolated lab.

Detection matches the anchored prefix muse-bin*, because the installed launcher
execs a version-suffixed binary whose name changes on every auto-update and
whose install path carries no muse component to fall back on. The same identity
is taught to the tmux liveness classifier, without which a healthy muse pane
would have read as a dead endpoint.

Busy state folds muse's own durable session event log, bound per task by a
sessions-root/worktree sidecar. It is a pull source with no writer, so nothing
is armed and no record is ever seeded. The fold is anchored on the full run
lifecycle prefix so muse's nested cleanup "terminal" payloads cannot settle an
in-flight run, and it is depth-bounded so muse's native sub-agent logs cannot be
mistaken for the parent's. The idle half stays gated: an open run proves busy,
but a settled log reads unknown until a credentialed multi-step run proves one
turn stays inside one run.

Two findings corrected the scout report. The exec-only
--no-foreign-personal-context flag is rejected by the interactive TUI, so the
privacy control that actually reaches a pane worker is
MUSE_EXPERIMENTAL_FOREIGN_PERSONAL_CONTEXT_KILL, verified to drop the operator's
foreign personal rules while keeping the project's own AGENTS.md. And an
unauthenticated muse pane never exits, it waits on a device-code prompt, so
credentials are a spawn preflight rather than a screen check.

muse is refused for secondmates: it has no primary supervision protocol and its
hook dialect rejects the reawakening handlers that protocol needs.

Per the captain's decision, auto-update is not pinned, and the credentialed
multi-step smoke is deferred with an explicit checklist in
docs/verification/muse.md.

* no-mistakes(review): Accept Muse dispatch profiles and shared efforts

* no-mistakes(review): Bind Muse busy state to current session

* no-mistakes(review): Compare Muse workspace bindings literally

* no-mistakes(review): Harden Muse worker credentials and live signal verification

* no-mistakes(review): Cache Muse session bindings and clarify worker credentials

* no-mistakes(review): Clear Muse marker inheritance and normalize interrupt aliases

* no-mistakes(review): Verify Muse glyph effective foreground color

* no-mistakes(review): Harden Muse XDG paths, session cache, and glyph parsing

* no-mistakes(document): Document Muse adapter boundaries

* fix(herdr): require 0.8.0 for default presentation spaces (#1787)

* fix(herdr): floor default-on presentation spaces at Herdr 0.8.0

Default-on presentation projection turns every crewmate teardown into a
workspace-emptying removal. The focus-safe removal plan avoids Herdr's
focus-stealing explicit close only while the doomed pane's shell can be proved
lone, childless, and idle; a persistent child of that shell (gitstatusd, a
zsh-async worker, direnv) fails that proof permanently and forces the plain
close, which on every release before Herdr 0.8.0 moves the captain's active
workspace for ~140ms on each teardown.

Gate the unconfigured default behind a Herdr 0.8.0 floor. At or above it,
project as before; below it, fall back to the flat per-home layout with one
warning per home per detected release naming the version and the upgrade. An
explicit "on" - including the historical empty opt-in file - is still honored
below the floor, so a deliberate opt-in is never silently downgraded.

The floor reads two independent signals from the client's own status, either of
which can establish a supported release: the protocol number and the release
core of the version string. Measured against the real release binaries, no build
lacking both upstream focus fixes reaches protocol 19 and every pre-fix build
tops out at 17, so protocol 19 is a safe structural expression of the floor. A
release that reports neither signal readably is treated as unsupported rather
than guessed at.

Also:
- Correct the adapter comment claiming the mitigation "stays safe without any
  version gate". That holds for the pane-death route only; the plain-close
  fallback is reachable precisely on the releases where it is unsafe.
- Stop discarding the projected-close helper's stderr at teardown, so a refused
  or failed focus restore is visible instead of silent. The close stays
  non-fatal; the presence gate still decides record removal.
- Add Part C to the focus-flash regression: a doomed pane whose shell holds a
  persistent child, in the geometry where the closing workspace's right
  neighbour is not the anchor. That is the fallback branch the suite could not
  structurally reach. On 0.7.5 it observes a bounded four-sample wrong-focus
  window restored exactly; on 0.8.0 it observes none. It also cross-checks its
  own measurement against the floor classifier, so a drifted protocol mapping
  fails loudly.
- Make the projection suite's unconfigured-home case release-aware, so the whole
  real-Herdr lane passes on both the CI-pinned 0.7.4 and 0.8.0.
- Add an opt-in live guard that re-measures the release-to-protocol mapping
  against the pinned upstream binaries.

The immediate no-code mitigation for a home that cannot upgrade remains writing
"off" into config/herdr-presentation-spaces.

* no-mistakes(review): Pin Herdr live-guard digests across supported platforms

* no-mistakes(review): Document authorized Herdr cleanup containment

* no-mistakes(review): Harden Herdr warning marker publication

* no-mistakes(review): Honor running Herdr server presentation floor

* no-mistakes(review): Recheck Herdr floor after server ensure

* no-mistakes(review): Refresh 0.7.5 and 0.8.0 focus transcripts

* no-mistakes(review): Route Herdr floor probe through lab session

* no-mistakes(document): Align Herdr floor documentation and comments

* no-mistakes(lint): Document Herdr presentation out-parameter consumer

* fix(bin): classify settled Muse session logs as idle (#1788)

* 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

* docs(agents): read the persisted digest when only a preview is shown (#1794)

* fix: preserve fleet state in truncated session-start digests (#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

* no-mistakes(review): Restore newest-node fallback for unresolvable nvm LTS aliases

Captain decision (key=nvm-lts-unresolvable, option a): an lts/* default
selector whose alias file is missing can no longer be resolved offline, so
compose the operator PATH from the newest installed nvm version instead of
dropping nvm entirely. Resolvable LTS aliases still resolve explicitly
through the alias chain; a 'system' default still yields no nvm path.

* no-mistakes(review): Downgrade no-evidence lsof reap refusals under --force

Route the lsof-scan-failure and unverifiable-process-identity refusals in
reap_task_worktree_processes through refuse_backend_reap, matching the
lsof-absent path per the captain-approved no-evidence vs positive-proof
asymmetry: fail-closed REFUSED by default, loud warning and proceed under
--force. Positive-proof refusals (processes remain after bounded reap
attempts) stay hard even under --force.

* feat(send): close answered decisions at answer time via --resolve-key (#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.

* fix(bin): seed remote secondmates from supplied origins (#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

* fix(tests): restore reliable fm-send backend parity coverage (#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. #1842 made bin/fm-send.sh source
bin/fm-line-cap-lib.sh (added by #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 remote secondmate status streams (#1846)

* 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

* docs(agents): describe the digest's fleet-state-before-context order (#1826)

* fix(bin): fail closed on NUL bytes in the durable parent binding (#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.

* fix(skills): reconcile inherited secondmate plans with shipped state (#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

* no-mistakes(review): add regression test pinning hard persistent-leak refusal under --force

* no-mistakes(test): wait for async runner claims instead of fixed sleeps

The detached procevent runner publishes its claim file atomically but
asynchronously after reconcile returns. Two assertions sampled that state
after a fixed 0.5s sleep, which races runner startup on a slow machine:
the orphan-runner test read the claim pid, and the crashed-leader test
checked claim existence plus the replacement's started line. Replace the
fixed sleeps with bounded waits on the observable state itself. Verified
against unchanged bin/fm-procevent machinery: the runner claim appears at
~500ms here, right at the old sleep's edge.

* no-mistakes(document): document --force no-evidence reap downgrade in teardown usage header

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
Co-authored-by: Christopher McKay <101884182+karotkriss@users.noreply.github.com>
Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
mjskinner82 added a commit to mjskinner82/firstmate that referenced this pull request Aug 7, 2026
* feat(send): close answered decisions at answer time via --resolve-key (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.

* fix(bin): seed remote secondmates from supplied origins (kunchenguid#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

* fix(tests): restore reliable fm-send backend parity coverage (kunchenguid#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 remote secondmate status streams (kunchenguid#1846)

* 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

* docs(agents): describe the digest's fleet-state-before-context order (kunchenguid#1826)

* fix(bin): fail closed on NUL bytes in the durable parent binding (kunchenguid#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.

* fix(skills): reconcile inherited secondmate plans with shipped state (kunchenguid#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

* fix: move network checks off the session-start blocking path (kunchenguid#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 remote replies during capture (kunchenguid#1831)

* 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

* feat(skills): port internal stow curation disciplines to the public skill (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).

* chore(bootstrap): raise lavish-axi version floor to 0.1.46 (kunchenguid#1865)

* feat(bin): add maintenance agent grading loop

* no-mistakes(review): Captain, serialize ledger writes and reject duplicate keys

* no-mistakes(document): Document grading helper in toolbelt index

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
huynhtandat223 pushed a commit to huynhtandat223/firstmate that referenced this pull request Aug 7, 2026
…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
huynhtandat223 pushed a commit to huynhtandat223/firstmate that referenced this pull request Aug 7, 2026
…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
cipherholdingsllc added a commit to cipherlab-ai/firstmate that referenced this pull request Aug 7, 2026
* fix(bin): prevent remote polls from blocking session startup (kunchenguid#1754)

* fix(bin): preempt remote reply long-polls for queued short jobs

Session start on a home with live remote second mates could stall silently
for many minutes: the single serial remote job worker ran each armed
fm-remote-delta-read.sh reply poll to its full 55s window while bootstrap's
short sync, inherit, state, and route commands sat queued behind it, and
non-FIFO queue pickup let re-armed polls keep winning the lane. Measured
end to end, a trivial short job took 31s behind one 30s poll window.

The worker now preempts a running preemptible job (the read-only, cursor-
anchored delta read is the only member of that class) as soon as a
non-preemptible job is queued, publishing exit 75 with emptied output -
byte-identical to the poll's own elapsed-window-with-no-data result - so
the parent runner takes its existing no-result path and the watcher re-arms
from the same cursor with nothing lost. The delta read translates SIGTERM
into that same exit after removing its staging directory. Sibling polls
never preempt each other, so two armed monitors cannot churn. The same
measured scenario now completes in 1s.

* no-mistakes(document): Clarify remote poll preemption documentation

* docs: present X mode as the X and Discord public surface (kunchenguid#1778)

Discord mentions already ride the same pairing-token opt-in, relay poll,
and platform-aware reply path as X mentions, but the docs still read as
X-only, so a stranger could not self-serve the Discord path.

Add the numbered turn-on steps to the X mode configuration reference,
pointing at the myfirstmate dashboard for account creation, bot install,
and token issuance rather than duplicating operator setup here, and drop
the X-only framing from the README bullet, the documentation index, and
the architecture overview.

* fix(bin): run session start deterministically from hooks (kunchenguid#1781)

* feat(bin): run session start deterministically on hook-capable harnesses

Session start relied on a native nudge that only asked the agent to run
bin/fm-session-start.sh, and an agent can defer that. Observed 2026-08-01:
an /ahoy-first session followed the recap path and did not take the helm
until a later request forced it.

Claude, Codex, and Pi now RUN the digest in their session-open hook through
the new bin/fm-sessionstart-run.sh, so the full ordered digest is in model
context before the first turn. That wrapper is the single owner of what a
session-open source means: startup and Pi's "new" take the helm, clear and
compact re-emit, resume/reload/fork delegate to the nudge, and an unreadable
source takes the helm because doing that redundantly is idempotent while
skipping it is the bug. Grok and OpenCode keep the nudge as the floor, since
neither can carry hook stdout into a model turn.

Because the hook now blocks session initialization, fm-session-start.sh
bounds itself first. Its steps are not all individually bounded - bootstrap's
gh auth probe, tool version probes, the backlog listing and per-task endpoint
reads are unbounded - so the whole digest runs as one bounded child (default
120s). Whatever it emitted before the bound survives, and the parent adds a
loud STARTUP TRUNCATED banner naming the stage that stalled and every stage
that never ran, still exiting 0.

--reemit skips only the sweeps startup already reconciled. It still re-verifies
lock ownership and still drains queued wakes, which arrived after startup and
are the turn's work. fm-bootstrap.sh gains FM_BOOTSTRAP_LOCKED so a re-emit
keeps repair ownership instead of deferring to a lock holder that is itself.

Also adds bin/fm-timeout-lib.sh as the single owner of bounded execution,
replacing three near-identical copies, and gives the ahoy skill a helm check
so a nudge-tier harness cannot recap before taking the helm.

Verified live on 2026-08-05 against Claude 2.1.222, Codex 0.146.0, and Pi
0.82.0; docs/verification/supervision.md records the per-harness source
vocabulary, the two named gaps, and the refresh command.

* no-mistakes(review): Harden session-start completion, timeout, and Pi delivery

* no-mistakes(review): Harden completion ownership and portable timeout escalation

* no-mistakes(review): Normalize watchdog KILL exits without masking command status

* no-mistakes(review): Guarantee startup bounds and align harness delivery tiers

* no-mistakes(test): Fix Pi session-start live verification fixture

* no-mistakes(document): Align session-start documentation with deterministic hooks

* no-mistakes(lint): Silence intentional child-shell expansion lint warning

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* fix: rename X mode to Relay in user-facing docs (kunchenguid#1784)

* docs: rename the user-facing product name to Relay

The public-mention integration gated by the `.env` pairing token is now
called Relay across user-facing prose, covering X and Discord alike
instead of implying a single network.

Renames the product-name strings only: README, docs, the captain-facing
skill descriptions, and the AGENTS.md operating prose, including the
`X mode (.env)` and `Optional X mode` headings and every link anchor
that pointed at them. AGENTS.md section 14 carries a one-line bridge
note so the older name and the unchanged identifier spellings stay
discoverable.

Internal identifiers are untouched: `FMX_*`, `config/x-mode.env`,
`state/x-*`, `bin/fm-x-*`, the `fmx-respond` skill path,
`__FM_X_MODE_ENV__`, and `x-mode-error`. Platform references to X and
Discord as networks stay as they are, and the bootstrap-diagnostics
entry still quotes bootstrap's emitted `FMX: X mode on/off` line
verbatim because `bin/` output is out of scope for this pass.

* no-mistakes(review): Complete Relay prose rename in maintained docs

* no-mistakes: apply CI fixes

* feat: add Muse Code crewmate adapter (kunchenguid#1786)

* feat(harness): add a verified muse crewmate adapter

Muse Code joins the fleet as a crewmate/scout adapter, verified live against
Muse Code 0.1.0-R708.1 in an isolated lab.

Detection matches the anchored prefix muse-bin*, because the installed launcher
execs a version-suffixed binary whose name changes on every auto-update and
whose install path carries no muse component to fall back on. The same identity
is taught to the tmux liveness classifier, without which a healthy muse pane
would have read as a dead endpoint.

Busy state folds muse's own durable session event log, bound per task by a
sessions-root/worktree sidecar. It is a pull source with no writer, so nothing
is armed and no record is ever seeded. The fold is anchored on the full run
lifecycle prefix so muse's nested cleanup "terminal" payloads cannot settle an
in-flight run, and it is depth-bounded so muse's native sub-agent logs cannot be
mistaken for the parent's. The idle half stays gated: an open run proves busy,
but a settled log reads unknown until a credentialed multi-step run proves one
turn stays inside one run.

Two findings corrected the scout report. The exec-only
--no-foreign-personal-context flag is rejected by the interactive TUI, so the
privacy control that actually reaches a pane worker is
MUSE_EXPERIMENTAL_FOREIGN_PERSONAL_CONTEXT_KILL, verified to drop the operator's
foreign personal rules while keeping the project's own AGENTS.md. And an
unauthenticated muse pane never exits, it waits on a device-code prompt, so
credentials are a spawn preflight rather than a screen check.

muse is refused for secondmates: it has no primary supervision protocol and its
hook dialect rejects the reawakening handlers that protocol needs.

Per the captain's decision, auto-update is not pinned, and the credentialed
multi-step smoke is deferred with an explicit checklist in
docs/verification/muse.md.

* no-mistakes(review): Accept Muse dispatch profiles and shared efforts

* no-mistakes(review): Bind Muse busy state to current session

* no-mistakes(review): Compare Muse workspace bindings literally

* no-mistakes(review): Harden Muse worker credentials and live signal verification

* no-mistakes(review): Cache Muse session bindings and clarify worker credentials

* no-mistakes(review): Clear Muse marker inheritance and normalize interrupt aliases

* no-mistakes(review): Verify Muse glyph effective foreground color

* no-mistakes(review): Harden Muse XDG paths, session cache, and glyph parsing

* no-mistakes(document): Document Muse adapter boundaries

* fix(herdr): require 0.8.0 for default presentation spaces (kunchenguid#1787)

* fix(herdr): floor default-on presentation spaces at Herdr 0.8.0

Default-on presentation projection turns every crewmate teardown into a
workspace-emptying removal. The focus-safe removal plan avoids Herdr's
focus-stealing explicit close only while the doomed pane's shell can be proved
lone, childless, and idle; a persistent child of that shell (gitstatusd, a
zsh-async worker, direnv) fails that proof permanently and forces the plain
close, which on every release before Herdr 0.8.0 moves the captain's active
workspace for ~140ms on each teardown.

Gate the unconfigured default behind a Herdr 0.8.0 floor. At or above it,
project as before; below it, fall back to the flat per-home layout with one
warning per home per detected release naming the version and the upgrade. An
explicit "on" - including the historical empty opt-in file - is still honored
below the floor, so a deliberate opt-in is never silently downgraded.

The floor reads two independent signals from the client's own status, either of
which can establish a supported release: the protocol number and the release
core of the version string. Measured against the real release binaries, no build
lacking both upstream focus fixes reaches protocol 19 and every pre-fix build
tops out at 17, so protocol 19 is a safe structural expression of the floor. A
release that reports neither signal readably is treated as unsupported rather
than guessed at.

Also:
- Correct the adapter comment claiming the mitigation "stays safe without any
  version gate". That holds for the pane-death route only; the plain-close
  fallback is reachable precisely on the releases where it is unsafe.
- Stop discarding the projected-close helper's stderr at teardown, so a refused
  or failed focus restore is visible instead of silent. The close stays
  non-fatal; the presence gate still decides record removal.
- Add Part C to the focus-flash regression: a doomed pane whose shell holds a
  persistent child, in the geometry where the closing workspace's right
  neighbour is not the anchor. That is the fallback branch the suite could not
  structurally reach. On 0.7.5 it observes a bounded four-sample wrong-focus
  window restored exactly; on 0.8.0 it observes none. It also cross-checks its
  own measurement against the floor classifier, so a drifted protocol mapping
  fails loudly.
- Make the projection suite's unconfigured-home case release-aware, so the whole
  real-Herdr lane passes on both the CI-pinned 0.7.4 and 0.8.0.
- Add an opt-in live guard that re-measures the release-to-protocol mapping
  against the pinned upstream binaries.

The immediate no-code mitigation for a home that cannot upgrade remains writing
"off" into config/herdr-presentation-spaces.

* no-mistakes(review): Pin Herdr live-guard digests across supported platforms

* no-mistakes(review): Document authorized Herdr cleanup containment

* no-mistakes(review): Harden Herdr warning marker publication

* no-mistakes(review): Honor running Herdr server presentation floor

* no-mistakes(review): Recheck Herdr floor after server ensure

* no-mistakes(review): Refresh 0.7.5 and 0.8.0 focus transcripts

* no-mistakes(review): Route Herdr floor probe through lab session

* no-mistakes(document): Align Herdr floor documentation and comments

* no-mistakes(lint): Document Herdr presentation out-parameter consumer

* fix(bin): classify settled Muse session logs as idle (kunchenguid#1788)

* 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

* docs(agents): read the persisted digest when only a preview is shown (kunchenguid#1794)

* fix: preserve fleet state in truncated session-start digests (kunchenguid#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

* feat(send): close answered decisions at answer time via --resolve-key (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.

* fix(bin): seed remote secondmates from supplied origins (kunchenguid#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

* fix(tests): restore reliable fm-send backend parity coverage (kunchenguid#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 remote secondmate status streams (kunchenguid#1846)

* 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

* docs(agents): describe the digest's fleet-state-before-context order (kunchenguid#1826)

* fix(bin): fail closed on NUL bytes in the durable parent binding (kunchenguid#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.

* fix(skills): reconcile inherited secondmate plans with shipped state (kunchenguid#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

* fix: move network checks off the session-start blocking path (kunchenguid#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 remote replies during capture (kunchenguid#1831)

* 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

* feat(skills): port internal stow curation disciplines to the public skill (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).

* chore(bootstrap): raise lavish-axi version floor to 0.1.46 (kunchenguid#1865)

* docs(cmux): add operator war-room recipe and thin helper (CMUX-001)

Publishes concrete cmux CLI recipes for a labeled, multi-pane war-room
view (new-workspace/layout, new-split, set-color, capture UUIDs,
read-screen, scoped close-surface before close-workspace), backed by a
thin bin/fm-cmux-war-room.sh helper (banner, color-for-harness,
teardown-surfaces). The 1:1 task-workspace backend invariant in
cmux-backend.md is unchanged; this is an operator recipe sheet plus a
standalone helper, not a spawn-path change.

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
TFBizzle added a commit to TFBizzle/firstmate that referenced this pull request Aug 8, 2026
* feat(send): close answered decisions at answer time via --resolve-key (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.

* fix(bin): seed remote secondmates from supplied origins (kunchenguid#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

* fix(tests): restore reliable fm-send backend parity coverage (kunchenguid#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 remote secondmate status streams (kunchenguid#1846)

* 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

* docs(agents): describe the digest's fleet-state-before-context order (kunchenguid#1826)

* fix(bin): fail closed on NUL bytes in the durable parent binding (kunchenguid#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.

* fix(skills): reconcile inherited secondmate plans with shipped state (kunchenguid#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

* fix: move network checks off the session-start blocking path (kunchenguid#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 remote replies during capture (kunchenguid#1831)

* 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

* feat(skills): port internal stow curation disciplines to the public skill (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).

* chore(bootstrap): raise lavish-axi version floor to 0.1.46 (kunchenguid#1865)

* fix(bin): keep tracked Claude hook entries inert under grok 1.0.0 (kunchenguid#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

* feat(startup-network): record per-step elapsed times for the deferred 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.

* feat(stow): cascade the internal /stow to every registered secondmate (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

* fix(remote-job): stop workers abandoned by a pruned code root (kunchenguid#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.

* feat(bin): lint only the changed shard locally, full lint in CI (kunchenguid#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 fm-grant, the captain-authorized grant vault for bounded promptless secret access

One script, the macOS Keychain, per-secret state files, and one log.
grant imports a key through a single av approval shaped as
'av inject +KEY -- fm-grant.sh _store KEY' (env -> child -> security, no
plaintext on any fd), then exec injects values into the child command's
environment only - no public get, values never printed, no-grant calls
exec 'av inject' directly after one loud stderr line so nothing wedges
silently and the value never transits fm-grant.

Honest identity, stated in the doc and in status: a convenience window
with receipts, NOT a security control - an imported key is silently
readable by any same-user process until forget; counts and expiry are
cooperative bookkeeping and audit. Never bless fm-grant.sh itself.

Bounds (--uses/--until/--permanent) combine, first limit wins, lazy
expiry; a mandatory --reason quotes the captain's instruction; shape
warnings (--permanent, uses>100, until>30d, consequence-listed names)
warn without blocking. Per-secret 0600 files in a 0700 grants dir with
atomic tmp+mv writes and an mkdir spinlock around read-decrement-write.

Tests were written first: fakebin security/av shims and an isolated
FM_GRANT_STATE_DIR pin the contract, including the concurrent-decrement
lock proof and a no-value-in-any-output sweep; suite passes under stock
Bash 3.2 and pinned ShellCheck 0.11.0.

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
Co-authored-by: Jay Park <jay.jongcheol.park@gmail.com>
Co-authored-by: Max <max@Maxs-Mac-mini.fritz.box>
blazingbunny pushed a commit to blazingbunny/firstmate that referenced this pull request Aug 9, 2026
…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
blazingbunny pushed a commit to blazingbunny/firstmate that referenced this pull request Aug 9, 2026
…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
4mb1t10n pushed a commit to 4mb1t10n/firstmate that referenced this pull request Aug 11, 2026
…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
4mb1t10n pushed a commit to 4mb1t10n/firstmate that referenced this pull request Aug 11, 2026
…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
spirispark added a commit to spirispark/firstmate that referenced this pull request Aug 13, 2026
* fix: preserve fleet state in truncated session-start digests (#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

* feat(send): close answered decisions at answer time via --resolve-key (#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.

* fix(bin): seed remote secondmates from supplied origins (#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

* fix(tests): restore reliable fm-send backend parity coverage (#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. #1842 made bin/fm-send.sh source
bin/fm-line-cap-lib.sh (added by #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 remote secondmate status streams (#1846)

* 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

* docs(agents): describe the digest's fleet-state-before-context order (#1826)

* fix(bin): fail closed on NUL bytes in the durable parent binding (#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.

* fix(skills): reconcile inherited secondmate plans with shipped state (#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

* fix: move network checks off the session-start blocking path (#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 #1845, which was cut from a
pre-#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 #1826's
fleet-state-before-context ordering. The old-bin test shim keeps main's
git-archive baseline from #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 remote replies during capture (#1831)

* 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

* feat(skills): port internal stow curation disciplines to the public skill (#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).

* chore(bootstrap): raise lavish-axi version floor to 0.1.46 (#1865)

* fix(bin): keep tracked Claude hook entries inert under grok 1.0.0 (#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

* feat(startup-network): record per-step elapsed times for the deferred stage (#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.

* feat(stow): cascade the internal /stow to every registered secondmate (#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

* fix(remote-job): stop workers abandoned by a pruned code root (#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.

* feat(bin): lint only the changed shard locally, full lint in CI (#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 (#1568)

* 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): add tiered decaying memory management (#1984)

* 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 project vision (#1997)

* 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 (#2005)

* fix(spawn): force regular Pi TUI for crews

* no-mistakes(document): Documented Pi regular TUI launch mode

* fix(cmux): classify borderless Claude composers (#2029)

* fix(cmux): classify borderless Claude composer

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

* no-mistakes(document): Document cmux borderless Claude composer classification

* docs(stow): generalize read-before-write in the public stow skill (#2091)

The public installer-facing stow skill scoped its classify-then-replace
discipline to TODO/BACKLOG items only, so findings routed to a memory file
had no stated rule against a blind append or a wholesale overwrite.

Step 6 now classifies every finding against the destination's current
contents as new, duplicate, superseding, or obsolete, and states the
considered replacement each classification implies. The outcomes follow the
tiered-memory contract already in the file: an obsolete entry is refreshed,
archived, or replaced in a way that preserves its fact, a duplicate folds
into the entry that already carries it, and a superseded body worth keeping
leaves through step 7's existing exits rather than a second recovery
mechanism.

* fix: resurface durable supervision work after re-arm (#2065)

* fix(watcher): resurface durable work after downtime

* no-mistakes(review): Make watcher rearm recovery durable and cursor-safe

* no-mistakes(review): Persist safe recovery markers across migration lock recovery

* no-mistakes(review): Retain stale lock when recovery marker publication fails

* no-mistakes(review): Preserve delivery-gap recovery and quarantine malformed markers

* no-mistakes(review): Serialize recovery consumption and report acknowledgment failures

* no-mistakes(review): Centralize recovery publication before clearing watcher evidence

* no-mistakes(review): Guarantee recovery evidence across queue and lock handoffs

* no-mistakes(review): Publish recovery evidence before durable wake commits

* no-mistakes(review): Replace recovery marker Perl dependency with Node

* no-mistakes(review): Keep interrupted wakes durable until handling acknowledgment

* no-mistakes(review): Add post-handling durable wake acknowledgements

* no-mistakes(review): Enforce post-handling acknowledgement across recovery and AFK return

* no-mistakes(review): Bind wake acknowledgements to recovery generations

* no-mistakes(review): Align wake regressions with generation-bound acknowledgements

* no-mistakes(document): Document durable re-arm recovery semantics

* no-mistakes(lint): Resolve ShellCheck warnings in recovery and watcher tests

* no-mistakes: apply CI fixes

* test(watcher): assert post-handling wake replay

* no-mistakes(review): Prevent successor loops and adopt legacy wake generations

* no-mistakes(review): Rearm durable wakes without recursive successor recovery

* no-mistakes(review): Align recovery tests with handling marker state

* no-mistakes(review): Delay handling transition until successor launch is established

* no-mistakes(review): Confirm wake handling only after successful prompt delivery

* no-mistakes(review): Acknowledge AFK wakes only after evidence publication

* no-mistakes(review): Prevent AFK wake loss before post-handling acknowledgement

* no-mistakes(document): Document durable wake acknowledgement semantics

* no-mistakes(lint): Suppress false positive for recovery action output

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* ci: measure Herdr automation on Windows runners (#2100)

* ci: add Windows Herdr automation spike

* ci: run Windows spike on its pull request

* fix: wait for Windows Herdr command output

* fix: run ANSI probe in pane shell

* ci: keep Windows Herdr spike manually triggered

* docs: clarify Windows Herdr spike verdict

* feat(ahoy): guide captains through open decisions (#2099)

* Add guided ahoy decision flow

* no-mistakes(document): Document guided Ahoy decision flow

* fix(stow): enforce startup-memory budget decisions (#2110)

* Harden stow memory budget policy

* Refine internal stow offload policy

* no-mistakes(review): Enforce shared-budget decisions and autonomous offload

* fix(spawn): refresh pooled worktrees from origin before launch (#2116)

* fix(spawn): refresh pooled worktree base

* no-mistakes(document): Document spawn base-freshness invariant

* no-mistakes: apply CI fixes

* fix(composer): unify safe classification across backends (#2102)

* refactor(composer): one shape owner behind thin capture adapters, whole matrix fixed

Consolidate every composer shape - bordered boxes (all families, geometry,
titled bottom borders), bare agent-glyph rows and their wrap regions,
opencode's left bar, and pi's identity-gated separator pair - into
fm_composer_classify_screen in bin/fm-composer-lib.sh. Adapters now
contribute only a capture and a declarative capability descriptor
(styled/cursor/identity/rows); capability differences change how confidently
a shape is judged, never what the shapes are, so a new harness shape is
teachable in exactly one place.

Correctness fixes landed as part of the consolidation (audit
data/fm-composer-consolidation-audit-s1):
- locale-safe Unicode-space normalization in the shared owner (closes the
  fleet-wide half of #1988; cmux's local byte-exact NBSP case deleted;
  naming converges with PR #1995's normalization primitive)
- muse's bare glyph joins the shared set, unbreaking muse on herdr/cmux/orca
- orca learns the borderless bare shape, drops its backward-paged composer
  window, and can no longer classify a stale startup banner as the composer
- tmux tolerates a titled bottom border, unbreaking grok steering
- the left-bar shape makes opencode readable on every backend
- zellij gets a real classifier through dump-screen --ansi, replacing the
  content-diff submit heuristic that could confirm an undelivered message
  and close a --resolve-key decision (the fleet's only false positive)
- fm-spawn's kimi launch-readiness regex (the fourth shape copy) now routes
  through the shared classifier

The strict blank-row posture applies fleet-wide (captain decision
blank-row-injection-posture): no positive container proof = unknown = defer,
replacing tmux's permissive blank-cursor-row rule. Away-mode injection was
re-validated end to end on real tmux (defer on partial input and unproven
rows, clean delivery with swallowed-Enter retry into proven-empty
composers). The tmux submit core gains a baseline-idle turn-started
conversion so pi steering stays confirmed while its working screen hides
the composer; busy conversion without that baseline remains forbidden.

Plain-capture backends now degrade a glyph row carrying trailing text to
unknown instead of a false pending, per the approved capability rule.

Portable regressions pin the full byte-capture matrix from the audit under
a UTF-8 locale and LC_ALL=C, the strict-vs-permissive divergence, and
deliberate signal separation; the opt-in live guard
(tests/fm-composer-matrix-live-e2e.test.sh) verified every installed
harness against the real classifier, recorded in
docs/verification/runtime-backends.md.

* no-mistakes(review): Fix Pi glyph ambiguity and complete profile matrix

* no-mistakes(review): Preserve bare verdict when Pi identity probe is absent

* no-mistakes(review): Harden composer structure and titled-border geometry

* no-mistakes(review): Require proven idle baseline and strict Zellij guard

* no-mistakes(review): Reject box bottom borders as composer input rows

* no-mistakes(review): Prove Zellij probe typing before classifier retries

* no-mistakes(review): Preserve Pi identity uncertainty and scan full left-bar drafts

* no-mistakes(review): Verify Zellij text lands before submitting

* no-mistakes(review): Scope Zellij typing verification to selected composer content

* no-mistakes(review): Verify Zellij pastes through composer-scoped content deltas

* no-mistakes(review): Prove wrapped bare Zellij pastes through composer extraction

* no-mistakes(review): Invalidate stale cursorless composers below dead shell prompts

* no-mistakes(review): Handle shell prompt placeholders in composer extraction

* no-mistakes(review): Classify cursorless bare continuation regions safely

* no-mistakes(review): Reject stale cursorless containers below live activity

* no-mistakes(review): Preserve prompt glyphs in wrapped Zellij pastes

* no-mistakes(review): Reject live shell rows during composer extraction

* no-mistakes(review): Preserve wrapped glyph continuations through submit retries

* no-mistakes(review): Scope idle placeholders to proven positions

* no-mistakes(review): Restore boxed placeholders and live prompt reanchoring

* no-mistakes(review): Fix Zellij placeholder and wrapped glyph paste proof

* no-mistakes(document): Align composer architecture documentation

* no-mistakes(lint): Fix ShellCheck warnings in composer refactor

* no-mistakes: apply CI fixes

* docs(verification): record the trusted-checkout live matrix rerun

The pipeline's isolated gate worktree is untrusted, so claude, grok, and
muse stopped at first-launch trust dialogs there (the guard refuses to
confirm them by design). This rerun from the trusted checkout at the final
validated head verified all six installed harnesses, the strict blank-row
deferral, and the hardened zellij false-positive probe live.

* no-mistakes(document): Align composer verification evidence

* no-mistakes: apply CI fixes

* no-mistakes(review): Restore proven box bottom-cursor classification

* no-mistakes(review): Preserve styled placeholder-like drafts as pending

* no-mistakes(document): Align composer safety and Zellij delivery documentation

* no-mistakes: apply CI fixes

* docs(verification): refresh the live matrix with the final-head trusted rerun

The post-validation rerun from the trusted checkout verified all six
installed harnesses at the branch's final head, including Claude 2.1.227
(auto-updated since the audit's captures) and Grok, which the untrusted
gate worktree could not verify past their first-launch trust dialogs.

* fix(spawn): gate Pi TUI mode by CLI capability (#2117)

* fix(spawn): gate Pi regular TUI flag by capability

* no-mistakes(review): Document conditional Pi TUI capability detection

* no-mistakes(review): Pin Pi probing and launch to one executable

* no-mistakes(review): Preserve literal pinned Pi paths and update documentation

* no-mistakes(review): Defer pinned Pi path insertion until final substitution

* no-mistakes(document): Document version-safe Pi launch probing

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* docs(vision): elevate experience, pain narrative, and distro virtues (#2147)

* docs(vision): elevate experience, pain narrative, and distro virtues

Fold the captain's public vision framing into VISION.md: peace of mind as a
primary goal, multi-session context-switch pain as the problem one interface
solves, clone-and-run setup ease, self-evolution including community, and
explicit harness/backend orthogonality. Reconcile experience-as-garnish into
experience-as-purpose and update aligns/resists accordingly.

* docs(vision): state the experience goal positively

Drop the negative "not a smart workflow / useful tool / impressive technology"
pretext. Lead straight into the positive experience north star.

* feat(bin): reconcile inactive terminal crew outcomes (#2167)

* fix: reconcile inactive terminal outcomes

* fix: stream secondmate summary inputs

* no-mistakes(review): Fix reconciliation locking and request delivery retries

* no-mistakes(review): Prevent retries after unknown request delivery

* no-mistakes(document): Clarify inactive reconciliation cadence and receipts

* no-mistakes(lint): Quote terminal status arguments in reconciliation tests

* refactor: simplify inactive outcome reconciliation

* no-mistakes(review): Bound inactive reconciliation scans with durable progress

* no-mistakes(review): Bound reconciliation and deduplicate recovery notices

* no-mistakes(document): Document inactive outcome reconciliation contracts

* no-mistakes(review): Reject relative local secondmate parent routes

* no-mistakes(review): Key terminal receipts by spawn incarnation

* no-mistakes(review): Stabilize legacy receipts and lock reconciliation snapshots

* no-mistakes(review): Fail closed on invalid secondmate identity markers

* no-mistakes(document): Document durable inactive-outcome reconciliation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* ci: raise Herdr test timeout (#2191)

* fix: refresh stale Pi instructions after compaction (#2163)

* fix(session-start): refresh drifted instructions on stale rebuilds

* test(session-start): prove Pi instruction refresh end to end

* no-mistakes(review): Fix stale instruction refresh and baseline integrity

* no-mistakes(review): Preserve true-start baselines across Pi continuations

* no-mistakes(review): Correct Pi continuation classification and live expectation

* no-mistakes(review): Correct Pi continuation coverage documentation

* no-mistakes(review): Fix read-only refresh and exact Pi session restores

* no-mistakes(review): Classify Pi create-if-missing sessions correctly

* no-mistakes(review): Classify named Pi sessions using immutable headers

* no-mistakes(review): Correct Codex interactive coverage diagnostic

* no-mistakes(document): Document immutable Pi compaction instruction refresh

* no-mistakes(document): Correct Pi refresh documentation and validation claims

* feat: add deterministic condition-to-action watcher (#2200)

* feat(bin): add deterministic condition->action watch adapter on the process-event channel

Register a (condition, action) pair once with bin/fm-procevent-when.sh and the
existing process-to-event runner polls the condition tokenlessly, fires the
action at most once on a stable true, and wakes firstmate exactly once with the
captured outcome - instead of burning an agent turn per re-check.

The pair is stored privately under state/when/ and hash-bound by a trust record
the same way fm-check-register.sh binds a custom check, so a mutated spec is
refused without executing anything. A durable exclusive fired marker claimed
before the action makes restarts and re-polls unable to double-fire; every
failure path (mutated spec, condition error past budget, expired deadline,
failed action, uncaptured earlier fire) ends in a terminal captured outcome
that wakes firstmate rather than a silent retry. Eligibility stays a firstmate
judgment: only exact, safe, reversible actions may be bound, and judgment-
needing or destructive actions keep the wake-and-decide flow.

* no-mistakes(review): Harden when watcher concurrency, deadlines, timeouts, and output

* no-mistakes(test): Bind watcher actions to registered executable bytes

* no-mistakes(document): Correct condition-action watcher documentation

* no-mistakes(document): Clarify outcome wake re-announcement

* no-mistakes: apply CI fixes

* fix(bin): honor a decision key stated after the verb colon (#2202)

The open-decisions fold only recognized a [key=<slug>] token between the
verb and the colon (needs-decision [key=x]: note). The common worker
shape with the colon first (needs-decision: [key=x] note) silently
folded its stated key into the shared "default" bucket, so two open
decisions could collapse into one record and fm-send --resolve-key <x>
refused to close the decision it plainly named.

A complete token at the head of the note is now an equivalent stated-key
position for every keyed verb, shared by the whole-file and incremental
folds through the one _fm_decision_key owner. The documented
before-colon position wins when both are present, a token deeper in the
note stays prose, a bare keyless line still folds to "default", and a
stated-but-malformed slug is rejected rather than rewritten to
"default". A consumed note-head token is stripped from the note so both
positions yield identical records, and the incremental fold version is
bumped so persisted cursors folded under the old interpretation are
rebuilt from the authoritative log.

Fixes #2109

* fix(bin): prevent watcher recovery acknowledgement livelock (#2212)

* fix(bin): keep a recovery acknowledgement valid across republication

A watcher cycle that opened and closed while the model handled its drained
wakes minted a fresh recovery generation, which invalidated the exact
acknowledgement the drain had just printed. That acknowledgement then consumed
nothing, so the marker stayed pending and every later arm spent its whole cycle
re-announcing the same recovery instead of supervising - a livelock the home
could not leave on its own.

A downtime publication now reuses the generation of an outstanding handling
episode, so a close during the handling window cannot orphan the printed
acknowledgement. The acknowledgement itself separates its two facts: queue-row
consumption is bound to the monotonic --ack-through sequence and always
happens, while only retiring the episode is bound to --recovery-generation. A
generation that moved on is a non-fatal result that names its own remedy
instead of a refusal that consumes nothing.

* no-mistakes(review): Preserve recovery generations and consume stale acknowledgements safely

* no-mistakes(document): Document sequence-bound recovery acknowledgements

* feat(fmx-respond): consume Relay conversation chains (#2206)

* feat(fmx-respond): consume in_reply_to_chain conversation context

The relay's poll payload can carry in_reply_to_chain, an oldest-first
transcript of the surrounding conversation, but the mention-handling
procedure only ever read the immediate in_reply_to parent, so referents
like "this" in a standalone mention stayed unresolvable even when
context was delivered.

Teach fmx-respond to read the chain when present (optional and
backward-compatible: often absent today, kind label not required),
resolve referents against the whole transcript, and extend the
untrusted-content framing to every chain entry including the upcoming
kind=history entries. Document the field's wire shape in
docs/configuration.md as the firstmate-side owner.

* no-mistakes(document): Document Relay chain context ownership

* fix: parse decision verbs before status metadata tags (#2280)

* fix(bin): strip every bracket tag, not just [key=...], from a status verb

status_line_verb only stripped a leading "[key=...]" token before the
colon, so a remote secondmate reply's leading "[corr=...]" correlation
tag stayed glued onto the returned verb word ("needs-decision
[corr=...]" instead of "needs-decision"). The open-decisions fold's
verb match then silently failed to recognize the line at all, so
fm-send --resolve-key refused to close a decision that was plainly
open on the status line.

Generalize the parser to strip every "[name=value]" tag before the
colon, in any order and count, so local and remote replies fold
identically.

* no-mistakes(review): Invalidate stale decision cursors after parser fix

* no-mistakes(document): Clarify status metadata verb parsing

* fix(bin): collapse duplicate supervision wakes (#2287)

* fix: collapse duplicate supervision wakes without losing legitimate updates

One remote-secondmate note produced two handling turns (a procevent check
wake published before autohandle, then a signal wake for the same mirrored
bytes), already-ingested replays such as a cursor-loss whole-log recapture
still woke with nothing to do, this home's own bookkeeping closes (fm-send
--resolve-key, the pending-reply escalation close, the captain-held
transfer) re-woke the session that wrote them, and turn-ended-only wakes
were annotated with already-announced status lines that looked like fresh
progress.

Dedup rules, each at its layer's one owner:
- fm-procevent.sh: an adapter may declare 'self-announcing'; the runner
  then applies first and publishes a check wake only for what remains
  unhandled. fm-procevent-remote-reply.sh declares it: the mirrored status
  append is the single announcement, so a fully applied capture publishes
  nothing and a byte-identical replay stays completely quiet. All other
  adapters keep strict publish-before-apply.
- fm-wake-lib.sh: fm_wake_signal_sig/seen_path/seen_current now own the
  watcher's signal signature and .seen-* marker format, plus
  fm_wake_status_append_self_announced, the guarded bookkeeping append
  that advances the marker only over exactly its own bytes and fails
  toward waking on any pending or interleaved foreign write.
- fm-send.sh, fm-pending-reply-lib.sh, fm-decision-hold.sh: bookkeeping
  closes go through that guarded append; escalation opens stay plain
  appends because a new blocker must wake.
- fm-wake-lib.sh annotations: a historical (turn-ended-only) row skips its
  status annotation only when the file's signature provably matches the
  seen marker; anything unannounced keeps annotating.
- fm-classify-lib.sh: a kind=secondmate task's status signal is never
  absorbed as provably-working, because that stream is the routed-reply
  channel the parent must read.

Also fixes a pre-existing exit-path deadlock the regression run reproduced:
a TERM inside a recovery-marker critical section left fm_lock_try_acquire
spinning against this same process's abandoned hold; a self-held lock is
now reclaimed (a subshell still waits on its parent's live hold).

Regression tests drive the real wake functions and executables in both
directions: each duplicate case collapses, while a new remote reply, new
decision, new blocker, merge result, failure, first status change, and a
later different note on the same task all still wake.

* no-mistakes(document): Document wake deduplication contracts

* feat: add Cursor CLI crew harness (#2238)

* feat(harness): add Cursor Agent CLI adapter

# Conflicts:
#	bin/fm-spawn.sh

* fix(composer): read cursor-agent's reverse-video placeholder as idle

cursor-agent renders its idle composer placeholder dim (SGR 2) but paints the
cell under the terminal cursor in reverse video (SGR 0;7). Reverse video is
neither dim nor a dark truecolor foreground, so the shared ghost stripper keeps
that one character and an idle composer reduces to a lone `P`. Judged on its
own, that remnant reads `pending` on a genuinely idle pane, which defers
away-mode escalation indefinitely on the styled cursorless backends.

Teach the ONE fleet-wide classifier the shape instead of adding an adapter-local
copy: register `→` as an agent prompt glyph so the composer row is structurally
findable at all (without it the bottom-most shape is a stale shell prompt echo
in the scrollback), add both verified placeholders to the idle set, and consult
the styling-independent plain row when the styled row is only a remnant.

The plain-row branch demands the remnant be a proper, strictly shorter substring
of a plain row matching a fully anchored placeholder. Real typed text is
uniformly bright, so stripping leaves it equal to the plain row and it stays
`pending` - verified live against a pane where the typed text was exactly the
placeholder string.

Verified live on cursor-agent 2026.08.11-e8db854; the regression pins the real
captured bytes and asserts the remnant survives stripping, so the case cannot go
vacuous if the stripper later learns SGR 7.

Co-authored-by: Amplify Logic AI <lars@sockinator.co>

* feat(cursor): narrow cursor identity and order its marker before CLAUDECODE

Cursor ships two executable names - `cursor-agent` and the legacy alias `agent`
- and runs as a bundled node script, so tmux reports the pane command as a bare
`node`. Neither `agent` nor `node` can be trusted by name, so identity gets one
owner in bin/fm-cursor-lib.sh that demands cursor's own name or install tree in
the path or argv[0], from the structural signal only. Probing an arbitrary pid's
executable during a liveness poll would execute a stranger's binary, which is
the hazard that rule exists to close.

Two consequences wired up:

Detection. cursor-agent does NOT clear an inherited CLAUDECODE, so a cursor
worker launched under a claude primary carries both markers and whichever is
tested first wins. The cursor markers are ordered ahead of the CLAUDECODE check;
fm-spawn additionally clears foreign markers at the launch boundary. Both are
kept deliberately - launch sanitization only covers sessions fm-spawn started,
while the ordering also covers a cursor session started by hand. Verified live
that CURSOR_INVOKED_AS is set on the agent process and CURSOR_AGENT=1 on the
child/tool processes fm-harness.sh actually runs as.

Pane liveness. A cursor pane now classifies `agent`. An unrelated node or agent
stays `other`, which the liveness callers already fold into `ambiguous` rather
than `dead`, so a stranger's node pane is never reported agent-free.

Resolution prints the STABLE launcher rather than the canonical target: identity
is proven through canonicalization, but cursor's canonical path carries a
version its own auto-update replaces, and pinning that would strand a task on a
version that can vanish.

The regression drives the two identity signals apart - a cursor-named executable
outside any cursor tree, and a non-cursor-named alias inside one - and asserts
each carries a verdict alone, so no single vendor string is load-bearing. Its
negative controls are real spawned processes, not fixtures.

Verified live on cursor-agent 2026.08.11-e8db854.

Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* feat(cursor): classify cursor busy state from its own turn transcript

Cursor shipped as "unknown cursor-unverified" on the premise that it exposes no
semantic turn lifecycle, only a rendered "Working" footer. That premise is
wrong: cursor-agent persists an append-only JSONL transcript per conversation
and brackets every submitted turn with a ro…
levelupself added a commit to levelupself/firstmate that referenced this pull request Aug 13, 2026
* feat(bin): require an explicit per-task delivery contract (#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.

* feat(bin): support remote secondmate homes (#1576)

* 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: add per-task trace context propagation (#995)

* 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): harden tmux agent liveness across harnesses (#1577)

* 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

* feat(bin): propagate trace context to remote secondmates (#1609)

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

Per-task W3C trace context (#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): preflight remote runtime tool paths (#1623)

* 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: gate remote second mates on Herdr readiness (#1639)

* 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 #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

* fix: isolate remote secondmates in shared Herdr session (#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

* feat: route remote commands through an Aqua job worker (#1660)

* feat: run remote commands through Aqua job worker

* no-mistakes(review): Enforce remote job deadlines and safe worker shutdown

* no-mistakes(review): Refresh stale workers and harden dependency-free supervision

* no-mistakes(review): Harden worker ownership recovery and shutdown quarantine

* no-mistakes(review): Fix doctor bootstrap, harness repair, and output draining

* no-mistakes(review): Probe doctor tools through authenticated worker bootstrap

* no-mistakes(review): Refresh stale workers before doctor tool probes

* no-mistakes(review): Recover stopped quarantines and extend job deadlines

* no-mistakes(review): Separate queue and execution timeout windows

* no-mistakes(review): Supervise Linux worker crashes and bind root identity

* no-mistakes(review): Resolve authorized Nix profile bin links

* no-mistakes(review): Clarify Nix path resolution documentation

* no-mistakes(review): Harden PATH safety and nvm selection

* no-mistakes(review): Honor nvm system defaults and refresh doctor digest

* no-mistakes(review): Keep workers ready during active jobs

* no-mistakes(review): Bound pre-execution validation by job timeout

* no-mistakes(document): Clarify remote worker documentation

* no-mistakes(lint): Fix remote worker ShellCheck diagnostics

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* fix: clarify remote doctor bootstrap path (#1691)

* fix(bin): bound remote SSH dead-peer detection (#1699)

* fix(remote): arm SSH dead-peer detection in fm-on.sh

A vanished remote host mid-poll (a reboot, a dropped link) left ssh
blocked indefinitely on a half-open TCP connection, because fm-on.sh's
ssh invocation had no ServerAliveInterval/ServerAliveCountMax. This
wedged the remote-reply ferry: fm-procevent.sh's runner blocked inside
the ssh child and never reached its own no-result -> claim-release ->
reconcile re-arm self-healing path, which otherwise already handles a
nonzero exit with empty output correctly. Recovery required a manual
retire and re-arm.

Arm ServerAliveInterval=15 and ServerAliveCountMax=3 by default
(bounded ~45s detection window), both overridable via
FM_SSH_ALIVE_INTERVAL and FM_SSH_ALIVE_COUNT_MAX. This is a transport-
level fix in fm-on.sh, so it covers every remote command routed
through it, not just the reply ferry. The remote sshd answers
keepalive probes independently of whatever the remote command is
doing, so a legitimately long-but-alive command (a 55s poll, a clone,
the doctor) is never falsely killed - only a truly vanished peer trips
it, turning that case into a bounded, detectable ssh failure (exit
255) instead of an indefinite hang.

Extends tests/fm-on.test.sh with a behavioral regression asserting a
bounded, positive ServerAliveInterval/ServerAliveCountMax on the real
ssh argv captured through the FM_SSH_BIN process seam, plus coverage
that both are env-overridable.

* no-mistakes(document): Document SSH dead-peer detection ownership

* fix: report stale AXI tools during bootstrap (#1701)

* feat(bootstrap): gate stale axi CLIs at the floors firstmate actually uses

Add gh-axi 0.1.29 floor so bare --squash PR merges stop failing quietly on
older builds. Raise tasks-axi to FM_TASKS_AXI_MIN=0.2.2 (multi-id mv) while
keeping feature probes. Keep quota-axi at 0.1.16 after verifying schema 3
and per-model availability already ship there; runway remains optional.

* no-mistakes(document): Clarify AXI compatibility documentation ownership

* fix: prevent false watcher-down alarms in Claude sessions (#1661)

* fix(guard): stop false watcher-down alarm mid-turn under Claude auto-arm

bin/fm-guard.sh derived its watcher-health verdict from fm_watcher_healthy,
which requires a live watcher process holding the home lock. Under the Claude
Stop-hook auto-arm supervision model the watcher is armed at each turn end and
exits on its wake, so it runs only between turns. Every guarded command run
mid-turn therefore found no live watcher and printed the "WATCHER DOWN -
SUPERVISION IS OFF" banner even though supervision was healthy. Because the
episode key was derived from the beacon mtime (which the between-turns watcher
advances every poll), the full banner re-printed on essentially every command,
and the message always blamed a "fresh beacon" that was in fact fresh.

Make the pull guard's health check model-aware via a new
fm_watcher_supervision_verdict in bin/fm-wake-lib.sh:

- Under the auto-arm model a beacon fresh within FM_GUARD_GRACE is healthy even
  with no live watcher process; only a beacon stale beyond grace (or absent) is
  a genuine lapse and alarms.
- Under every persistent-watcher harness (codex foreground checkpoint,
  opencode/pi/grok background arm, tmux, unknown) a live identity-matched
  watcher with a fresh beacon is still required, unchanged.

The banner now names the true failing condition, a missing live watcher process
versus a genuinely stale beacon, instead of always blaming the beacon, and the
once-per-episode dedup keys on that condition rather than the beacon mtime so a
genuine lapse announces once and does not re-print each turn.

The turn-end guard keeps the strict fm_watcher_healthy check because it fires at
the turn boundary, where the auto-arm brings a fresh watcher up and it
cooperates with that arm. fm_watcher_healthy itself is unchanged, so the arm
layer's start/attach/replace decisions are unaffected.

Tests in tests/fm-guard-stale-banner.test.sh cover the auto-arm healthy
fresh-beacon-without-a-watcher case, the auto-arm stale-beacon alarm and its
stable episode, the true-reason banner wording, and the reason-keyed episode
surviving a beacon mtime change; existing persistent-model cases are pinned to
that model.

* no-mistakes(review): Pin secondmate supervision model to launched harness

* no-mistakes(document): Align watcher documentation with model-aware supervision health

* test: prevent fixture temporary directory leaks (#1704)

* fix(tests): stop fixture-tempdir helper from self-deleting under command substitution

fm_test_tmproot is almost always called as `TMP_ROOT=$(fm_test_tmproot prefix)`,
which forks a subshell to capture its stdout. The old implementation set its
EXIT cleanup trap inside that call, so the trap fired - and deleted the fixture
root - the instant the subshell exited, before the real caller's own EXIT trap
was ever installed. Every test using the documented call pattern leaked its
fixture root on every run; two suites had already independently discovered and
worked around this with ad-hoc mktemp calls.

Registration now goes through a $$-keyed registry file instead of in-process
state, since $$ resolves to the invoking shell's PID even inside the
subshell. The real cleanup trap is armed once at source time (always the real
caller, never a subshell) for EXIT, INT, and TERM. A best-effort orphan sweep
on next source reaps marked fixture roots old enough to be from a killed prior
run.

Simplifies the two existing ad-hoc workarounds (fm-procevent.test.sh,
wake-helpers.sh) back onto the shared helper now that it works correctly.

* no-mistakes(review): Preserve live fixtures during orphan reaping

* no-mistakes(review): Harden fixture ownership against PID reuse

* no-mistakes(review): Secure cleanup registry against path precreation

* no-mistakes(review): Make fixture registration transactional

* no-mistakes(document): Documentation already matches fixture cleanup behavior

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* feat(herdr): enable presentation spaces by default (#1708)

* feat(herdr): default presentation spaces on with an explicit opt-out

Herdr's disposable one-task presentation workspace was opt-in through the
presence of local config/herdr-presentation-spaces. It is now on by default,
and a home opts out by writing "off" into that same file.

Values are read with the whole-file whitespace-stripped convention the other
scalar config items already use, plus case folding. An absent file, an empty
file, and "on" all resolve on; only "off" opts out; an unrecognized value warns
and keeps the default rather than failing a spawn over a purely visual setting.
The empty file is exactly the historical opt-in form, so every home that had
already enabled the projection stays enabled with no migration step, and no
previously enabled home can be turned off by the flip.

Because absence now means on at both ends, secondmate inheritance needs no
item-specific convergence: mirroring an absent primary file converges a
secondmate to the same default-on rather than turning its projection off, and
only an explicit primary opt-out propagates the opt-out.

The gate itself moves into fm_backend_herdr_presentation_enabled in the Herdr
adapter so the semantics have one owner that regressions can exercise directly.

* no-mistakes(document): Document Herdr default-on presentation safety

---------

Co-authored-by: kunchenguid <kun-1@kunchenguid.com>

* fix(bin): surface fleet-wide open decisions on every wake drain (#1711)

* fix: surface consolidated open decisions on every wake-drain

A needs-decision or blocked event buried under later, unrelated status
appends was only ever shown via the last-line wake annotation, so a
still-open captain decision could go silently missed even though
status_open_decisions (fm-classify-lib.sh) already folds the whole
status stream correctly and fleet-snapshot/bearings already reuse it.

Wire that same fold into bin/fm-wake-drain.sh: a new fleet-wide
scan_open_decisions wrapper scans every state/<id>.status, and
fm-wake-drain.sh prints a separate, bounded OPEN DECISIONS section on
every drain (including the empty-queue fast path), so session-start
and every wake-handling turn surface it for free without duplicating
the open/resolved fold itself. Heartbeat wakes drain through the same
script, so this covers that surface too.

Also tighten status_open_decisions' file guard to skip an unreadable
status file instead of leaking a bash redirection error, now that a
fleet-wide directory scan can reach files a single targeted read
would not.

* no-mistakes(review): Prevent status symlinks leaking open decisions

* fix: drop unbounded perl subprocess from status symlink guard

The review step's own symlink-safety auto-fix (O_NOFOLLOW read via a
perl subprocess) forked one perl process per status file scanned by
the new fleet-wide open-decisions scan, with no cap - inflating
fm-wake-drain.sh's total external-read cost from 8 (the existing
annotation read_cap) to 18 in the enrichment-caps regression test.

The plain [ -L "$f" ] check already rejects any status file that is
itself a symlink before any read happens, which is exactly what the
new regression test exercises and is the same defense level the
sibling scan_captain_relevant_statuses/last_status_line already rely
on elsewhere in this file (no O_NOFOLLOW). Drop the subprocess-based
nofollow read and keep the cheap builtin guard.

* no-mistakes(document): Document actionable fleet-wide open decision drains

* fix(bin): abort parked runs and reap leaked processes before teardown (#1710)

* fix(bin): abort orphaned no-mistakes runs and reap leaked processes at teardown

Teardown could remove a task's worker while its no-mistakes pipeline run was
still parked at a gate, leaving an orphaned run holding a fleet slot
indefinitely (observed 2026-08-03: runs parked 7h39m and parked at a
post-CI approval gate). It could also leave backgrounded/disowned
descendant processes rooted under the worktree or tasktmp surviving
reparented to init (observed: two `go test` binaries pinning CPU for
hours with no live task meta to attribute them to).

Add two coupled pre-teardown steps, both scoped to this task's exact
branch/head or worktree/tasktmp so they can never touch another task's
run or processes:
- conclude_task_no_mistakes_run aborts a run parked at a gate via
  `no-mistakes axi abort`, cd'd into the exact worktree so the daemon
  resolves the run itself rather than teardown naming a --run id.
- reap_task_worktree_processes sweeps for processes whose cwd is under
  the worktree or tasktmp (via `lsof -a -d cwd`) and TERM/KILLs them.

Both run before any worktree return, branch delete, or backend kill,
and are idempotent on a retried teardown. The branch+head attribution
logic is factored out of bin/fm-crew-state.sh into the new shared
bin/fm-nm-run-lib.sh so both scripts use the same ownership contract.

* no-mistakes(review): Fail closed on incomplete teardown cleanup

* no-mistakes(review): Bind teardown cleanup to verified run and process identities

* no-mistakes(review): Require confirmed aborts and convergent identity-safe process reaping

* no-mistakes(review): Handle process exits during teardown identity checks

* no-mistakes(review): Restore teardown library in hermetic gotmp fixtures

* no-mistakes(document): Document teardown run attribution and timeout

* no-mistakes(lint): Rename shell variable conflicting with done keyword

* no-mistakes: apply CI fixes

* fix: resolve fm-remote-entrypoint.sh SCRIPT_DIR through a PATH symlink (#1709)

The script installs as a symlink under ~/.local/bin. Taking dirname of
the symlink itself (instead of its real target) pointed SCRIPT_DIR at
~/.local/bin, breaking sourcing of the sibling fm-remote-job-lib.sh.
Resolve the real path first, preferring python3's os.path.realpath,
then realpath, falling back to the raw BASH_SOURCE on hosts with
neither.

* fix(pi): gate Calm built-in overrides by activation state (#1724)

* fix(pi): stop Calm claiming a built-in tool name another extension owns

fm-calm.ts claimed bash/read/edit/write/grep/find/ls unconditionally at
extension load, regardless of whether Calm was on. Pi resolves two
extensions registering the same built-in name by first-registered-wins
with no merge and no unregister call, and Calm's project-local
.pi/extensions/ position beats any global or CLI-configured extension,
so a user who never even enabled Calm could have their own bash/read/etc
override silently replaced.

Captain-approved plan implemented:

- Registration is now gated on config/calm already being "on" at load
  time. A Calm-off session or reload registers nothing, so a non-Calm
  user never contests a name. This stays synchronous during the
  factory's own load, not deferred to session_start: /reload (and
  ctx.newSession/fork/switchSession) render the restored transcript from
  a pre-session_start snapshot of the tool registry, so a deferred claim
  would miss that render - confirmed by tests/fm-calm-pi-extension
  .test.sh's hidden-block-geometry E2E when trialed.
- The first time Calm turns on in a session that started off
  (activateBuiltInsIfNeeded, from the /calm command handler), Calm calls
  pi.getAllTools() - safe only once every extension has finished loading,
  unlike the load-time path above - to see whether a different extension
  already owns a name, and skips claiming only that one, leaving it and
  its owning extension fully intact and callable.
- A contested name found this way prints a prominent ctx.ui.notify()
  warning naming the tool, plus a console diagnostic.
- reportBuiltInLosses() remains the backstop for the one case neither of
  the above can reach: a session that starts or reloads with Calm already
  on, where the registry snapshot is taken before Calm gets any chance to
  check ownership. A symlink-safe realpath comparison avoids misreporting
  Calm's own registration as foreign when its path crosses a symlink
  (macOS /tmp, /var).

Confirmed, bounded trade-off: the very first time a session that started
Calm-off turns Calm on, tool-call rows already on screen from before that
toggle do not retroactively collapse, because Pi never lets an extension
re-point an already-rendered row at a definition registered later. Every
session after that first toggle starts with the preference already on and
takes the synchronous load-time path, so the guarantee is intact from
then on. docs/calm.md and the file's own header document this in full.

tests/fm-calm-pi-extension.test.sh gains test_builtin_gate_load_time
(config/calm off registers nothing, on registers all 7 synchronously at
load) and test_calm_activation_collision_and_regression_bound (first
activation claims every uncontested built-in, leaves a foreign bash tool
fully intact and callable, warns and logs the contested name, and locks
in the documented pre-activation bound against real ToolExecutionComponent
rendering). test_rendering_and_session_lifecycle and the live interactive
E2E are updated for the new gate-at-load and first-activation-bound
contract.

* no-mistakes(document): Document Calm tool collision boundaries

* no-mistakes: apply CI fixes

* fix(bin): persist secondmate parent bindings for cleanup (#1727)

* fix(bin): give secondmate homes a durable parent binding record

Finished-worker cleanup on a remote second mate refused forever with
"cannot resolve the primary home ... durable parent binding". The
remote launch hands the child the remote code checkout as its parent
home (fm-spawn.sh's sole writer of FM_PUBLIC_FOLLOWUP_PRIMARY_HOME
receives FM_HOME=$FM_ROOT from fm-remote-secondmate-control.sh's
host-local launch), and that path can never carry the parent's real
records, so the guard refused unconditionally once relay looked active
anywhere on that host.

fm-home-seed.sh and fm-remote-home-provision.sh now write a durable
.fm-secondmate-parent record next to the .fm-secondmate-home identity
marker, naming the home's route to its parent as local (with the real
parent path) or remote (with the parent's SSH alias for diagnostics
only). fm-teardown.sh's cleanup gate reads it: a remote parent is out
of scope for the delegated-promise check (the whole promised-public-
reply subsystem is same-filesystem by construction, so a remote parent
can never hold one), while a token committed directly to the child's
own .env file - never the process environment - still refuses, so an
unrelated export in the remote host's login shell can no longer mask
in. For a local secondmate, the durable parent_home now also backs up
the launch-time env var, closing a silent fail-open where a restart
that dropped the launch prefix made the guard treat a genuinely active
parent relay as off.

Regression coverage drives the real remote route (SSH boundary + Herdr
fixture) and real fm-home-seed.sh seeding rather than hand-crafted
markers.

* no-mistakes(review): Captain: fail closed on unsafe durable parent records

* no-mistakes(review): Captain: enforce durable parent binding commit protocol

* no-mistakes(review): Captain: publish local parent binding before identity

* no-mistakes(review): Captain: refuse conflicting local parent bindings

* no-mistakes(review): Captain: reject non-regular secondmate seed leaves

* no-mistakes(review): Captain: enforce unique durable parent bindings

* no-mistakes(review): Captain: reject route-incompatible durable parent fields

* no-mistakes(document): Document durable secondmate parent bindings

* no-mistakes(lint): Fix secondmate parent parser ShellCheck warnings

* no-mistakes: apply CI fixes

* feat(bin): enforce latest AXI-family tool floors (#1733)

* feat(bin): gate lavish-axi at its session_ended floor in bootstrap

bin/fm-procevent-lavish.sh decides that a human "Send & End" review is
terminal by reading session_ended from the poll response's leading session
block. That field first shipped in lavish-axi 0.1.35, so an older installed
build silently leaves every ended review source armed forever and captures
an empty ended result on each later cycle. The same release is what makes a
plain reopen refuse a session the human deliberately ended.

Add LAVISH_AXI_MIN=0.1.35 to the existing axi-family floor structure in
bin/fm-bootstrap.sh, reusing tool_version_at_least and the same MISSING
diagnostic gh-axi already emits, so an incompatible build is reported as an
upgrade request before any review surface is armed. Later lavish-axi
releases only add artifact-authoring surface the adapter never reads, so
the floor is the feature-introduction point rather than latest.

Fixtures that stubbed lavish-axi as a bare exit-0 tool would now be read as
unparseable builds, so tests/lib.sh gains fm_fake_version_tool and every
bootstrap-running suite uses it for lavish-axi.

* no-mistakes(review): Clarify lavish-axi version floor rationale

* no-mistakes: apply CI fixes

* feat(bin): set axi-family floors to current latest under the bump policy

The axi-family bootstrap floors are the CURRENT LATEST published version of
each tool, captain-bumped periodically to move the whole fleet onto the
newest axi tools. They are not the minimum feature-introduced version. The
earlier lavish-axi work set a feature-minimum floor, which is the opposite
of this policy, so replace it along with the older feature-minimum rationale
carried by tasks-axi and quota-axi.

State the policy explicitly in bin/fm-bootstrap.sh's header, which owns it,
and in each per-tool floor owner, so no future change argues a floor back
down to the earliest release that happens to satisfy some behavior. Remove
the lavish-axi session_ended and upstream-PR citation, the tasks-axi
multi-ID-mv minimum argument, and the quota-axi credential-source argument
as floor rationale; the tasks-axi feature probes remain as a separate
defense-in-depth concern.

Floors: lavish-axi 0.1.45 (was 0.1.35), tasks-axi 0.2.4 (was 0.2.2),
quota-axi 0.1.17 (was 0.1.16), gh-axi 0.1.29 unchanged and already latest.
Each was verified against the tool's current published version.

The mechanism is unchanged: the same shared version helper and the same
MISSING diagnostic path. The below-fires and at-or-above-silent regression
rows move to the new floors, keeping each boundary genuine by pinning the
patch immediately below each floor rather than a version that was only
below the old one. Fleet fixtures move to the new floors so a bootstrap-
running suite is not reported as an out-of-date build.

Three operator-facing backlog handoff and receipt errors named "0.2.2+"
while the enforced floor moved, so they now point at the floor's owner
instead of duplicating a version number that drifts.

* no-mistakes(review): Centralize AXI floor policy beside constants

* no-mistakes(review): Clarify bootstrap boundary test comment

* no-mistakes(document): Centralize AXI floor policy rationale

* fix(bin): bound open decision scans with incremental cursors (#1737)

* fix(bin): bound OPEN DECISIONS scan cost with a per-status-file cursor

The fleet-wide OPEN DECISIONS scan added in #1711 re-reads and refolds
every task's entire lifetime status log on every drain, so its cost
grows unbounded with total log size. Add status_open_decisions_incremental
and scan_open_decisions_incremental to fm-classify-lib.sh: they persist a
per-status-file byte cursor plus the folded open-decision set, and fold
only newly appended bytes on each call, reusing status_open_decisions'
exact fold-line rule (extracted into _fm_decision_fold_line) so the two
strategies can never disagree on what is open. A missing or invalidated
cursor (new task, truncated/rewritten/shrunk log) falls back to a full
re-fold. bin/fm-wake-drain.sh now calls the incremental wrapper instead
of the whole-file scan.

* fix(bin): add O(1) rotation detection and read-failure guarding to the cursor fold

Add the two pieces the incremental open-decisions cursor was missing,
scoped to this repo's actual status-file usage (create-once, append-only,
never replaced or rewritten in place):

- An O(1) device+inode identity check (one stat call) alongside the
  existing size-shrink check, so a status file replaced/rotated/recreated
  at the same path is detected and falls back to a full re-fold, even
  when the replacement is the same size. A same-inode, same-size,
  in-place byte edit is a deliberately accepted gap: no code path in
  this repo ever does that to a status file.
- Checked reads: a stat/wc/tail failure is a genuine I/O error, not
  "the file is empty" - it now reports the already-trusted persisted
  open set unchanged instead of risking a silent invalidation.

Both stay O(1) plus new bytes per call, matching the cursor's bounded-
cost design; no content hashing or pending-fragment machinery.

* no-mistakes(review): Preserve cursor state across failed incremental reads

* no-mistakes(review): Refold status when cursor cache reads fail

* no-mistakes(document): Document cursor-backed open-decision scanning

* no-mistakes: apply CI fixes

* fix(bin): prevent remote polls from blocking session startup (#1754)

* fix(bin): preempt remote reply long-polls for queued short jobs

Session start on a home with live remote second mates could stall silently
for many minutes: the single serial remote job worker ran each armed
fm-remote-delta-read.sh reply poll to its full 55s window while bootstrap's
short sync, inherit, state, and route commands sat queued behind it, and
non-FIFO queue pickup let re-armed polls keep winning the lane. Measured
end to end, a trivial short job took 31s behind one 30s poll window.

The worker now preempts a running preemptible job (the read-only, cursor-
anchored delta read is the only member of that class) as soon as a
non-preemptible job is queued, publishing exit 75 with emptied output -
byte-identical to the poll's own elapsed-window-with-no-data result - so
the parent runner takes its existing no-result path and the watcher re-arms
from the same cursor with nothing lost. The delta read translates SIGTERM
into that same exit after removing its staging directory. Sibling polls
never preempt each other, so two armed monitors cannot churn. The same
measured scenario now completes in 1s.

* no-mistakes(document): Clarify remote poll preemption documentation

* docs: present X mode as the X and Discord public surface (#1778)

Discord mentions already ride the same pairing-token opt-in, relay poll,
and platform-aware reply path as X mentions, but the docs still read as
X-only, so a stranger could not self-serve the Discord path.

Add the numbered turn-on steps to the X mode configuration reference,
pointing at the myfirstmate dashboard for account creation, bot install,
and token issuance rather than duplicating operator setup here, and drop
the X-only framing from the README bullet, the documentation index, and
the architecture overview.

* fix(bin): run session start deterministically from hooks (#1781)

* feat(bin): run session start deterministically on hook-capable harnesses

Session start relied on a native nudge that only asked the agent to run
bin/fm-session-start.sh, and an agent can defer that. Observed 2026-08-01:
an /ahoy-first session followed the recap path and did not take the helm
until a later request forced it.

Claude, Codex, and Pi now RUN the digest in their session-open hook through
the new bin/fm-sessionstart-run.sh, so the full ordered digest is in model
context before the first turn. That wrapper is the single owner of what a
session-open source means: startup and Pi's "new" take the helm, clear and
compact re-emit, resume/reload/fork delegate to the nudge, and an unreadable
source takes the helm because doing that redundantly is idempotent while
skipping it is the bug. Grok and OpenCode keep the nudge as the floor, since
neither can carry hook stdout into a model turn.

Because the hook now blocks session initialization, fm-session-start.sh
bounds itself first. Its steps are not all individually bounded - bootstrap's
gh auth probe, tool version probes, the backlog listing and per-task endpoint
reads are unbounded - so the whole digest runs as one bounded child (default
120s). Whatever it emitted before the bound survives, and the parent adds a
loud STARTUP TRUNCATED banner naming the stage that stalled and every stage
that never ran, still exiting 0.

--reemit skips only the sweeps startup already reconciled. It still re-verifies
lock ownership and still drains queued wakes, which arrived after startup and
are the turn's work. fm-bootstrap.sh gains FM_BOOTSTRAP_LOCKED so a re-emit
keeps repair ownership instead of deferring to a lock holder that is itself.

Also adds bin/fm-timeout-lib.sh as the single owner of bounded execution,
replacing three near-identical copies, and gives the ahoy skill a helm check
so a nudge-tier harness cannot recap before taking the helm.

Verified live on 2026-08-05 against Claude 2.1.222, Codex 0.146.0, and Pi
0.82.0; docs/verification/supervision.md records the per-harness source
vocabulary, the two named gaps, and the refresh command.

* no-mistakes(review): Harden session-start completion, timeout, and Pi delivery

* no-mistakes(review): Harden completion ownership and portable timeout escalation

* no-mistakes(review): Normalize watchdog KILL exits without masking command status

* no-mistakes(review): Guarantee startup bounds and align harness delivery tiers

* no-mistakes(test): Fix Pi session-start live verification fixture

* no-mistakes(document): Align session-start documentation with deterministic hooks

* no-mistakes(lint): Silence intentional child-shell expansion lint warning

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* fix: rename X mode to Relay in user-facing docs (#1784)

* docs: rename the user-facing product name to Relay

The public-mention integration gated by the `.env` pairing token is now
called Relay across user-facing prose, covering X and Discord alike
instead of implying a single network.

Renames the product-name strings only: README, docs, the captain-facing
skill descriptions, and the AGENTS.md operating prose, including the
`X mode (.env)` and `Optional X mode` headings and every link anchor
that pointed at them. AGENTS.md section 14 carries a one-line bridge
note so the older name and the unchanged identifier spellings stay
discoverable.

Internal identifiers are untouched: `FMX_*`, `config/x-mode.env`,
`state/x-*`, `bin/fm-x-*`, the `fmx-respond` skill path,
`__FM_X_MODE_ENV__`, and `x-mode-error`. Platform references to X and
Discord as networks stay as they are, and the bootstrap-diagnostics
entry still quotes bootstrap's emitted `FMX: X mode on/off` line
verbatim because `bin/` output is out of scope for this pass.

* no-mistakes(review): Complete Relay prose rename in maintained docs

* no-mistakes: apply CI fixes

* feat: add Muse Code crewmate adapter (#1786)

* feat(harness): add a verified muse crewmate adapter

Muse Code joins the fleet as a crewmate/scout adapter, verified live against
Muse Code 0.1.0-R708.1 in an isolated lab.

Detection matches the anchored prefix muse-bin*, because the installed launcher
execs a version-suffixed binary whose name changes on every auto-update and
whose install path carries no muse component to fall back on. The same identity
is taught to the tmux liveness classifier, without which a healthy muse pane
would have read as a dead endpoint.

Busy state folds muse's own durable session event log, bound per task by a
sessions-root/worktree sidecar. It is a pull source with no writer, so nothing
is armed and no record is ever seeded. The fold is anchored on the full run
lifecycle prefix so muse's nested cleanup "terminal" payloads cannot settle an
in-flight run, and it is depth-bounded so muse's native sub-agent logs cannot be
mistaken for the parent's. The idle half stays gated: an open run proves busy,
but a settled log reads unknown until a credentialed multi-step run proves one
turn stays inside one run.

Two findings corrected the scout report. The exec-only
--no-foreign-personal-context flag is rejected by the interactive TUI, so the
privacy control that actually reaches a pane worker is
MUSE_EXPERIMENTAL_FOREIGN_PERSONAL_CONTEXT_KILL, verified to drop the operator's
foreign personal rules while keeping the project's own AGENTS.md. And an
unauthenticated muse pane never exits, it waits on a device-code prompt, so
credentials are a spawn preflight rather than a screen check.

muse is refused for secondmates: it has no primary supervision protocol and its
hook dialect rejects the reawakening handlers that protocol needs.

Per the captain's decision, auto-update is not pinned, and the credentialed
multi-step smoke is deferred with an explicit checklist in
docs/verification/muse.md.

* no-mistakes(review): Accept Muse dispatch profiles and shared efforts

* no-mistakes(review): Bind Muse busy state to current session

* no-mistakes(review): Compare Muse workspace bindings literally

* no-mistakes(review): Harden Muse worker credentials and live signal verification

* no-mistakes(review): Cache Muse session bindings and clarify worker credentials

* no-mistakes(review): Clear Muse marker inheritance and normalize interrupt aliases

* no-mistakes(review): Verify Muse glyph effective foreground color

* no-mistakes(review): Harden Muse XDG paths, session cache, and glyph parsing

* no-mistakes(document): Document Muse adapter boundaries

* fix(herdr): require 0.8.0 for default presentation spaces (#1787)

* fix(herdr): floor default-on presentation spaces at Herdr 0.8.0

Default-on presentation projection turns every crewmate teardown into a
workspace-emptying removal. The focus-safe removal plan avoids Herdr's
focus-stealing explicit close only while the doomed pane's shell can be proved
lone, childless, and idle; a persistent child of that shell (gitstatusd, a
zsh-async worker, direnv) fails that proof permanently and forces the plain
close, which on every release before Herdr 0.8.0 moves the captain's active
workspace for ~140ms on each teardown.

Gate the unconfigured default behind a Herdr 0.8.0 floor. At or above it,
project as before; below it, fall back to the flat per-home layout with one
warning per home per detected release naming the version and the upgrade. An
explicit "on" - including the historical empty opt-in file - is still honored
below the floor, so a deliberate opt-in is never silently downgraded.

The floor reads two independent signals from the client's own status, either of
which can establish a supported release: the protocol number and the release
core of the version string. Measured against the real release binaries, no build
lacking both upstream focus fixes reaches protocol 19 and every pre-fix build
tops out at 17, so protocol 19 is a safe structural expression of the floor. A
release that reports neither signal readably is treated as unsupported rather
than guessed at.

Also:
- Correct the adapter comment claiming the mitigation "stays safe without any
  version gate". That holds for the pane-death route only; the plain-close
  fallback is reachable precisely on the releases where it is unsafe.
- Stop discarding the projected-close helper's stderr at teardown, so a refused
  or failed focus restore is visible instead of silent. The close stays
  non-fatal; the presence gate still decides record removal.
- Add Part C to the focus-flash regression: a doomed pane whose shell holds a
  persistent child, in the geometry where the closing workspace's right
  neighbour is not the anchor. That is the fallback branch the suite could not
  structurally reach. On 0.7.5 it observes a bounded four-sample wrong-focus
  window restored exactly; on 0.8.0 it observes none. It also cross-checks its
  own measurement against the floor classifier, so a drifted protocol mapping
  fails loudly.
- Make the projection suite's unconfigured-home case release-aware, so the whole
  real-Herdr lane passes on both the CI-pinned 0.7.4 and 0.8.0.
- Add an opt-in live guard that re-measures the release-to-protocol mapping
  against the pinned upstream binaries.

The immediate no-code mitigation for a home that cannot upgrade remains writing
"off" into config/herdr-presentation-spaces.

* no-mistakes(review): Pin Herdr live-guard digests across supported platforms

* no-mistakes(review): Document authorized Herdr cleanup containment

* no-mistakes(review): Harden Herdr warning marker publication

* no-mistakes(review): Honor running Herdr server presentation floor

* no-mistakes(review): Recheck Herdr floor after server ensure

* no-mistakes(review): Refresh 0.7.5 and 0.8.0 focus transcripts

* no-mistakes(review): Route Herdr floor probe through lab session

* no-mistakes(document): Align Herdr floor documentation and comments

* no-mistakes(lint): Document Herdr presentation out-parameter consumer

* fix(bin): classify settled Muse session logs as idle (#1788)

* 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

* docs(agents): read the persisted digest when only a preview is shown (#1794)

* fix: preserve fleet state in truncated session-start digests (#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

* feat(send): close answered decisions at answer time via --resolve-key (#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.

* fix(bin): seed remote secondmates from supplied origins (#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
…
erxie1259 added a commit to erxie1259/firstmate that referenced this pull request Aug 13, 2026
* fix(pi): gate Calm built-in overrides by activation state (#1724)

* fix(pi): stop Calm claiming a built-in tool name another extension owns

fm-calm.ts claimed bash/read/edit/write/grep/find/ls unconditionally at
extension load, regardless of whether Calm was on. Pi resolves two
extensions registering the same built-in name by first-registered-wins
with no merge and no unregister call, and Calm's project-local
.pi/extensions/ position beats any global or CLI-configured extension,
so a user who never even enabled Calm could have their own bash/read/etc
override silently replaced.

Captain-approved plan implemented:

- Registration is now gated on config/calm already being "on" at load
  time. A Calm-off session or reload registers nothing, so a non-Calm
  user never contests a name. This stays synchronous during the
  factory's own load, not deferred to session_start: /reload (and
  ctx.newSession/fork/switchSession) render the restored transcript from
  a pre-session_start snapshot of the tool registry, so a deferred claim
  would miss that render - confirmed by tests/fm-calm-pi-extension
  .test.sh's hidden-block-geometry E2E when trialed.
- The first time Calm turns on in a session that started off
  (activateBuiltInsIfNeeded, from the /calm command handler), Calm calls
  pi.getAllTools() - safe only once every extension has finished loading,
  unlike the load-time path above - to see whether a different extension
  already owns a name, and skips claiming only that one, leaving it and
  its owning extension fully intact and callable.
- A contested name found this way prints a prominent ctx.ui.notify()
  warning naming the tool, plus a console diagnostic.
- reportBuiltInLosses() remains the backstop for the one case neither of
  the above can reach: a session that starts or reloads with Calm already
  on, where the registry snapshot is taken before Calm gets any chance to
  check ownership. A symlink-safe realpath comparison avoids misreporting
  Calm's own registration as foreign when its path crosses a symlink
  (macOS /tmp, /var).

Confirmed, bounded trade-off: the very first time a session that started
Calm-off turns Calm on, tool-call rows already on screen from before that
toggle do not retroactively collapse, because Pi never lets an extension
re-point an already-rendered row at a definition registered later. Every
session after that first toggle starts with the preference already on and
takes the synchronous load-time path, so the guarantee is intact from
then on. docs/calm.md and the file's own header document this in full.

tests/fm-calm-pi-extension.test.sh gains test_builtin_gate_load_time
(config/calm off registers nothing, on registers all 7 synchronously at
load) and test_calm_activation_collision_and_regression_bound (first
activation claims every uncontested built-in, leaves a foreign bash tool
fully intact and callable, warns and logs the contested name, and locks
in the documented pre-activation bound against real ToolExecutionComponent
rendering). test_rendering_and_session_lifecycle and the live interactive
E2E are updated for the new gate-at-load and first-activation-bound
contract.

* no-mistakes(document): Document Calm tool collision boundaries

* no-mistakes: apply CI fixes

* fix(bin): persist secondmate parent bindings for cleanup (#1727)

* fix(bin): give secondmate homes a durable parent binding record

Finished-worker cleanup on a remote second mate refused forever with
"cannot resolve the primary home ... durable parent binding". The
remote launch hands the child the remote code checkout as its parent
home (fm-spawn.sh's sole writer of FM_PUBLIC_FOLLOWUP_PRIMARY_HOME
receives FM_HOME=$FM_ROOT from fm-remote-secondmate-control.sh's
host-local launch), and that path can never carry the parent's real
records, so the guard refused unconditionally once relay looked active
anywhere on that host.

fm-home-seed.sh and fm-remote-home-provision.sh now write a durable
.fm-secondmate-parent record next to the .fm-secondmate-home identity
marker, naming the home's route to its parent as local (with the real
parent path) or remote (with the parent's SSH alias for diagnostics
only). fm-teardown.sh's cleanup gate reads it: a remote parent is out
of scope for the delegated-promise check (the whole promised-public-
reply subsystem is same-filesystem by construction, so a remote parent
can never hold one), while a token committed directly to the child's
own .env file - never the process environment - still refuses, so an
unrelated export in the remote host's login shell can no longer mask
in. For a local secondmate, the durable parent_home now also backs up
the launch-time env var, closing a silent fail-open where a restart
that dropped the launch prefix made the guard treat a genuinely active
parent relay as off.

Regression coverage drives the real remote route (SSH boundary + Herdr
fixture) and real fm-home-seed.sh seeding rather than hand-crafted
markers.

* no-mistakes(review): Captain: fail closed on unsafe durable parent records

* no-mistakes(review): Captain: enforce durable parent binding commit protocol

* no-mistakes(review): Captain: publish local parent binding before identity

* no-mistakes(review): Captain: refuse conflicting local parent bindings

* no-mistakes(review): Captain: reject non-regular secondmate seed leaves

* no-mistakes(review): Captain: enforce unique durable parent bindings

* no-mistakes(review): Captain: reject route-incompatible durable parent fields

* no-mistakes(document): Document durable secondmate parent bindings

* no-mistakes(lint): Fix secondmate parent parser ShellCheck warnings

* no-mistakes: apply CI fixes

* feat(bin): enforce latest AXI-family tool floors (#1733)

* feat(bin): gate lavish-axi at its session_ended floor in bootstrap

bin/fm-procevent-lavish.sh decides that a human "Send & End" review is
terminal by reading session_ended from the poll response's leading session
block. That field first shipped in lavish-axi 0.1.35, so an older installed
build silently leaves every ended review source armed forever and captures
an empty ended result on each later cycle. The same release is what makes a
plain reopen refuse a session the human deliberately ended.

Add LAVISH_AXI_MIN=0.1.35 to the existing axi-family floor structure in
bin/fm-bootstrap.sh, reusing tool_version_at_least and the same MISSING
diagnostic gh-axi already emits, so an incompatible build is reported as an
upgrade request before any review surface is armed. Later lavish-axi
releases only add artifact-authoring surface the adapter never reads, so
the floor is the feature-introduction point rather than latest.

Fixtures that stubbed lavish-axi as a bare exit-0 tool would now be read as
unparseable builds, so tests/lib.sh gains fm_fake_version_tool and every
bootstrap-running suite uses it for lavish-axi.

* no-mistakes(review): Clarify lavish-axi version floor rationale

* no-mistakes: apply CI fixes

* feat(bin): set axi-family floors to current latest under the bump policy

The axi-family bootstrap floors are the CURRENT LATEST published version of
each tool, captain-bumped periodically to move the whole fleet onto the
newest axi tools. They are not the minimum feature-introduced version. The
earlier lavish-axi work set a feature-minimum floor, which is the opposite
of this policy, so replace it along with the older feature-minimum rationale
carried by tasks-axi and quota-axi.

State the policy explicitly in bin/fm-bootstrap.sh's header, which owns it,
and in each per-tool floor owner, so no future change argues a floor back
down to the earliest release that happens to satisfy some behavior. Remove
the lavish-axi session_ended and upstream-PR citation, the tasks-axi
multi-ID-mv minimum argument, and the quota-axi credential-source argument
as floor rationale; the tasks-axi feature probes remain as a separate
defense-in-depth concern.

Floors: lavish-axi 0.1.45 (was 0.1.35), tasks-axi 0.2.4 (was 0.2.2),
quota-axi 0.1.17 (was 0.1.16), gh-axi 0.1.29 unchanged and already latest.
Each was verified against the tool's current published version.

The mechanism is unchanged: the same shared version helper and the same
MISSING diagnostic path. The below-fires and at-or-above-silent regression
rows move to the new floors, keeping each boundary genuine by pinning the
patch immediately below each floor rather than a version that was only
below the old one. Fleet fixtures move to the new floors so a bootstrap-
running suite is not reported as an out-of-date build.

Three operator-facing backlog handoff and receipt errors named "0.2.2+"
while the enforced floor moved, so they now point at the floor's owner
instead of duplicating a version number that drifts.

* no-mistakes(review): Centralize AXI floor policy beside constants

* no-mistakes(review): Clarify bootstrap boundary test comment

* no-mistakes(document): Centralize AXI floor policy rationale

* fix(bin): bound open decision scans with incremental cursors (#1737)

* fix(bin): bound OPEN DECISIONS scan cost with a per-status-file cursor

The fleet-wide OPEN DECISIONS scan added in #1711 re-reads and refolds
every task's entire lifetime status log on every drain, so its cost
grows unbounded with total log size. Add status_open_decisions_incremental
and scan_open_decisions_incremental to fm-classify-lib.sh: they persist a
per-status-file byte cursor plus the folded open-decision set, and fold
only newly appended bytes on each call, reusing status_open_decisions'
exact fold-line rule (extracted into _fm_decision_fold_line) so the two
strategies can never disagree on what is open. A missing or invalidated
cursor (new task, truncated/rewritten/shrunk log) falls back to a full
re-fold. bin/fm-wake-drain.sh now calls the incremental wrapper instead
of the whole-file scan.

* fix(bin): add O(1) rotation detection and read-failure guarding to the cursor fold

Add the two pieces the incremental open-decisions cursor was missing,
scoped to this repo's actual status-file usage (create-once, append-only,
never replaced or rewritten in place):

- An O(1) device+inode identity check (one stat call) alongside the
  existing size-shrink check, so a status file replaced/rotated/recreated
  at the same path is detected and falls back to a full re-fold, even
  when the replacement is the same size. A same-inode, same-size,
  in-place byte edit is a deliberately accepted gap: no code path in
  this repo ever does that to a status file.
- Checked reads: a stat/wc/tail failure is a genuine I/O error, not
  "the file is empty" - it now reports the already-trusted persisted
  open set unchanged instead of risking a silent invalidation.

Both stay O(1) plus new bytes per call, matching the cursor's bounded-
cost design; no content hashing or pending-fragment machinery.

* no-mistakes(review): Preserve cursor state across failed incremental reads

* no-mistakes(review): Refold status when cursor cache reads fail

* no-mistakes(document): Document cursor-backed open-decision scanning

* no-mistakes: apply CI fixes

* fix(bin): prevent remote polls from blocking session startup (#1754)

* fix(bin): preempt remote reply long-polls for queued short jobs

Session start on a home with live remote second mates could stall silently
for many minutes: the single serial remote job worker ran each armed
fm-remote-delta-read.sh reply poll to its full 55s window while bootstrap's
short sync, inherit, state, and route commands sat queued behind it, and
non-FIFO queue pickup let re-armed polls keep winning the lane. Measured
end to end, a trivial short job took 31s behind one 30s poll window.

The worker now preempts a running preemptible job (the read-only, cursor-
anchored delta read is the only member of that class) as soon as a
non-preemptible job is queued, publishing exit 75 with emptied output -
byte-identical to the poll's own elapsed-window-with-no-data result - so
the parent runner takes its existing no-result path and the watcher re-arms
from the same cursor with nothing lost. The delta read translates SIGTERM
into that same exit after removing its staging directory. Sibling polls
never preempt each other, so two armed monitors cannot churn. The same
measured scenario now completes in 1s.

* no-mistakes(document): Clarify remote poll preemption documentation

* docs: present X mode as the X and Discord public surface (#1778)

Discord mentions already ride the same pairing-token opt-in, relay poll,
and platform-aware reply path as X mentions, but the docs still read as
X-only, so a stranger could not self-serve the Discord path.

Add the numbered turn-on steps to the X mode configuration reference,
pointing at the myfirstmate dashboard for account creation, bot install,
and token issuance rather than duplicating operator setup here, and drop
the X-only framing from the README bullet, the documentation index, and
the architecture overview.

* fix(bin): run session start deterministically from hooks (#1781)

* feat(bin): run session start deterministically on hook-capable harnesses

Session start relied on a native nudge that only asked the agent to run
bin/fm-session-start.sh, and an agent can defer that. Observed 2026-08-01:
an /ahoy-first session followed the recap path and did not take the helm
until a later request forced it.

Claude, Codex, and Pi now RUN the digest in their session-open hook through
the new bin/fm-sessionstart-run.sh, so the full ordered digest is in model
context before the first turn. That wrapper is the single owner of what a
session-open source means: startup and Pi's "new" take the helm, clear and
compact re-emit, resume/reload/fork delegate to the nudge, and an unreadable
source takes the helm because doing that redundantly is idempotent while
skipping it is the bug. Grok and OpenCode keep the nudge as the floor, since
neither can carry hook stdout into a model turn.

Because the hook now blocks session initialization, fm-session-start.sh
bounds itself first. Its steps are not all individually bounded - bootstrap's
gh auth probe, tool version probes, the backlog listing and per-task endpoint
reads are unbounded - so the whole digest runs as one bounded child (default
120s). Whatever it emitted before the bound survives, and the parent adds a
loud STARTUP TRUNCATED banner naming the stage that stalled and every stage
that never ran, still exiting 0.

--reemit skips only the sweeps startup already reconciled. It still re-verifies
lock ownership and still drains queued wakes, which arrived after startup and
are the turn's work. fm-bootstrap.sh gains FM_BOOTSTRAP_LOCKED so a re-emit
keeps repair ownership instead of deferring to a lock holder that is itself.

Also adds bin/fm-timeout-lib.sh as the single owner of bounded execution,
replacing three near-identical copies, and gives the ahoy skill a helm check
so a nudge-tier harness cannot recap before taking the helm.

Verified live on 2026-08-05 against Claude 2.1.222, Codex 0.146.0, and Pi
0.82.0; docs/verification/supervision.md records the per-harness source
vocabulary, the two named gaps, and the refresh command.

* no-mistakes(review): Harden session-start completion, timeout, and Pi delivery

* no-mistakes(review): Harden completion ownership and portable timeout escalation

* no-mistakes(review): Normalize watchdog KILL exits without masking command status

* no-mistakes(review): Guarantee startup bounds and align harness delivery tiers

* no-mistakes(test): Fix Pi session-start live verification fixture

* no-mistakes(document): Align session-start documentation with deterministic hooks

* no-mistakes(lint): Silence intentional child-shell expansion lint warning

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* fix: rename X mode to Relay in user-facing docs (#1784)

* docs: rename the user-facing product name to Relay

The public-mention integration gated by the `.env` pairing token is now
called Relay across user-facing prose, covering X and Discord alike
instead of implying a single network.

Renames the product-name strings only: README, docs, the captain-facing
skill descriptions, and the AGENTS.md operating prose, including the
`X mode (.env)` and `Optional X mode` headings and every link anchor
that pointed at them. AGENTS.md section 14 carries a one-line bridge
note so the older name and the unchanged identifier spellings stay
discoverable.

Internal identifiers are untouched: `FMX_*`, `config/x-mode.env`,
`state/x-*`, `bin/fm-x-*`, the `fmx-respond` skill path,
`__FM_X_MODE_ENV__`, and `x-mode-error`. Platform references to X and
Discord as networks stay as they are, and the bootstrap-diagnostics
entry still quotes bootstrap's emitted `FMX: X mode on/off` line
verbatim because `bin/` output is out of scope for this pass.

* no-mistakes(review): Complete Relay prose rename in maintained docs

* no-mistakes: apply CI fixes

* feat: add Muse Code crewmate adapter (#1786)

* feat(harness): add a verified muse crewmate adapter

Muse Code joins the fleet as a crewmate/scout adapter, verified live against
Muse Code 0.1.0-R708.1 in an isolated lab.

Detection matches the anchored prefix muse-bin*, because the installed launcher
execs a version-suffixed binary whose name changes on every auto-update and
whose install path carries no muse component to fall back on. The same identity
is taught to the tmux liveness classifier, without which a healthy muse pane
would have read as a dead endpoint.

Busy state folds muse's own durable session event log, bound per task by a
sessions-root/worktree sidecar. It is a pull source with no writer, so nothing
is armed and no record is ever seeded. The fold is anchored on the full run
lifecycle prefix so muse's nested cleanup "terminal" payloads cannot settle an
in-flight run, and it is depth-bounded so muse's native sub-agent logs cannot be
mistaken for the parent's. The idle half stays gated: an open run proves busy,
but a settled log reads unknown until a credentialed multi-step run proves one
turn stays inside one run.

Two findings corrected the scout report. The exec-only
--no-foreign-personal-context flag is rejected by the interactive TUI, so the
privacy control that actually reaches a pane worker is
MUSE_EXPERIMENTAL_FOREIGN_PERSONAL_CONTEXT_KILL, verified to drop the operator's
foreign personal rules while keeping the project's own AGENTS.md. And an
unauthenticated muse pane never exits, it waits on a device-code prompt, so
credentials are a spawn preflight rather than a screen check.

muse is refused for secondmates: it has no primary supervision protocol and its
hook dialect rejects the reawakening handlers that protocol needs.

Per the captain's decision, auto-update is not pinned, and the credentialed
multi-step smoke is deferred with an explicit checklist in
docs/verification/muse.md.

* no-mistakes(review): Accept Muse dispatch profiles and shared efforts

* no-mistakes(review): Bind Muse busy state to current session

* no-mistakes(review): Compare Muse workspace bindings literally

* no-mistakes(review): Harden Muse worker credentials and live signal verification

* no-mistakes(review): Cache Muse session bindings and clarify worker credentials

* no-mistakes(review): Clear Muse marker inheritance and normalize interrupt aliases

* no-mistakes(review): Verify Muse glyph effective foreground color

* no-mistakes(review): Harden Muse XDG paths, session cache, and glyph parsing

* no-mistakes(document): Document Muse adapter boundaries

* fix(herdr): require 0.8.0 for default presentation spaces (#1787)

* fix(herdr): floor default-on presentation spaces at Herdr 0.8.0

Default-on presentation projection turns every crewmate teardown into a
workspace-emptying removal. The focus-safe removal plan avoids Herdr's
focus-stealing explicit close only while the doomed pane's shell can be proved
lone, childless, and idle; a persistent child of that shell (gitstatusd, a
zsh-async worker, direnv) fails that proof permanently and forces the plain
close, which on every release before Herdr 0.8.0 moves the captain's active
workspace for ~140ms on each teardown.

Gate the unconfigured default behind a Herdr 0.8.0 floor. At or above it,
project as before; below it, fall back to the flat per-home layout with one
warning per home per detected release naming the version and the upgrade. An
explicit "on" - including the historical empty opt-in file - is still honored
below the floor, so a deliberate opt-in is never silently downgraded.

The floor reads two independent signals from the client's own status, either of
which can establish a supported release: the protocol number and the release
core of the version string. Measured against the real release binaries, no build
lacking both upstream focus fixes reaches protocol 19 and every pre-fix build
tops out at 17, so protocol 19 is a safe structural expression of the floor. A
release that reports neither signal readably is treated as unsupported rather
than guessed at.

Also:
- Correct the adapter comment claiming the mitigation "stays safe without any
  version gate". That holds for the pane-death route only; the plain-close
  fallback is reachable precisely on the releases where it is unsafe.
- Stop discarding the projected-close helper's stderr at teardown, so a refused
  or failed focus restore is visible instead of silent. The close stays
  non-fatal; the presence gate still decides record removal.
- Add Part C to the focus-flash regression: a doomed pane whose shell holds a
  persistent child, in the geometry where the closing workspace's right
  neighbour is not the anchor. That is the fallback branch the suite could not
  structurally reach. On 0.7.5 it observes a bounded four-sample wrong-focus
  window restored exactly; on 0.8.0 it observes none. It also cross-checks its
  own measurement against the floor classifier, so a drifted protocol mapping
  fails loudly.
- Make the projection suite's unconfigured-home case release-aware, so the whole
  real-Herdr lane passes on both the CI-pinned 0.7.4 and 0.8.0.
- Add an opt-in live guard that re-measures the release-to-protocol mapping
  against the pinned upstream binaries.

The immediate no-code mitigation for a home that cannot upgrade remains writing
"off" into config/herdr-presentation-spaces.

* no-mistakes(review): Pin Herdr live-guard digests across supported platforms

* no-mistakes(review): Document authorized Herdr cleanup containment

* no-mistakes(review): Harden Herdr warning marker publication

* no-mistakes(review): Honor running Herdr server presentation floor

* no-mistakes(review): Recheck Herdr floor after server ensure

* no-mistakes(review): Refresh 0.7.5 and 0.8.0 focus transcripts

* no-mistakes(review): Route Herdr floor probe through lab session

* no-mistakes(document): Align Herdr floor documentation and comments

* no-mistakes(lint): Document Herdr presentation out-parameter consumer

* fix(bin): classify settled Muse session logs as idle (#1788)

* 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

* docs(agents): read the persisted digest when only a preview is shown (#1794)

* fix: preserve fleet state in truncated session-start digests (#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

* feat(send): close answered decisions at answer time via --resolve-key (#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.

* fix(bin): seed remote secondmates from supplied origins (#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

* fix(tests): restore reliable fm-send backend parity coverage (#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. #1842 made bin/fm-send.sh source
bin/fm-line-cap-lib.sh (added by #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 remote secondmate status streams (#1846)

* 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

* docs(agents): describe the digest's fleet-state-before-context order (#1826)

* fix(bin): fail closed on NUL bytes in the durable parent binding (#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.

* fix(skills): reconcile inherited secondmate plans with shipped state (#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

* fix: move network checks off the session-start blocking path (#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 #1845, which was cut from a
pre-#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 #1826's
fleet-state-before-context ordering. The old-bin test shim keeps main's
git-archive baseline from #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 remote replies during capture (#1831)

* 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

* feat(skills): port internal stow curation disciplines to the public skill (#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).

* chore(bootstrap): raise lavish-axi version floor to 0.1.46 (#1865)

* fix(bin): keep tracked Claude hook entries inert under grok 1.0.0 (#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

* feat(startup-network): record per-step elapsed times for the deferred stage (#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.

* feat(stow): cascade the internal /stow to every registered secondmate (#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

* fix(remote-job): stop workers abandoned by a pruned code root (#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.

* feat(bin): lint only the changed shard locally, full lint in CI (#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 (#1568)

* 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): add tiered decaying memory management (#1984)

* 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 project vision (#1997)

* 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 (#2005)

* fix(spawn): force regular Pi TUI for crews

* no-mistakes(document): Documented Pi regular TUI launch mode

* fix(cmux): classify borderless Claude composers (#2029)

* fix(cmux): classify borderless Claude composer

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

* no-mistakes(document): Document cmux borderless Claude composer classification

* docs(stow): generalize read-before-write in the public stow skill (#2091)

The public installer-facing stow skill scoped its classify-then-replace
discipline to TODO/BACKLOG items only, so findings routed to a memory file
had no stated rule against a blind append or a wholesale overwrite.

Step 6 now classifies every finding against the destination's current
contents as new, duplicate, superseding, or obsolete, and states the
considered replacement each classification implies. The outcomes follow the
tiered-memory contract already in the file: an obsolete entry is refreshed,
archived, or replaced in a way that preserves its fact, a duplicate folds
into the entry that already carries it, and a superseded body worth keeping
leaves through step 7's existing exits rather than a second recovery
mechanism.

* fix: resurface durable supervision work after re-arm (#2065)

* fix(watcher): resurface durable work after downtime

* no-mistakes(review): Make watcher rearm recovery durable and cursor-safe

* no-mistakes(review): Persist safe recovery markers across migration lock recovery

* no-mistakes(review): Retain stale lock when recovery marker publication f…
justinmaxw added a commit to justinmaxw/firstmate-archive that referenced this pull request Aug 14, 2026
* docs: present X mode as the X and Discord public surface (#1778)

Discord mentions already ride the same pairing-token opt-in, relay poll,
and platform-aware reply path as X mentions, but the docs still read as
X-only, so a stranger could not self-serve the Discord path.

Add the numbered turn-on steps to the X mode configuration reference,
pointing at the myfirstmate dashboard for account creation, bot install,
and token issuance rather than duplicating operator setup here, and drop
the X-only framing from the README bullet, the documentation index, and
the architecture overview.

* fix(bin): run session start deterministically from hooks (#1781)

* feat(bin): run session start deterministically on hook-capable harnesses

Session start relied on a native nudge that only asked the agent to run
bin/fm-session-start.sh, and an agent can defer that. Observed 2026-08-01:
an /ahoy-first session followed the recap path and did not take the helm
until a later request forced it.

Claude, Codex, and Pi now RUN the digest in their session-open hook through
the new bin/fm-sessionstart-run.sh, so the full ordered digest is in model
context before the first turn. That wrapper is the single owner of what a
session-open source means: startup and Pi's "new" take the helm, clear and
compact re-emit, resume/reload/fork delegate to the nudge, and an unreadable
source takes the helm because doing that redundantly is idempotent while
skipping it is the bug. Grok and OpenCode keep the nudge as the floor, since
neither can carry hook stdout into a model turn.

Because the hook now blocks session initialization, fm-session-start.sh
bounds itself first. Its steps are not all individually bounded - bootstrap's
gh auth probe, tool version probes, the backlog listing and per-task endpoint
reads are unbounded - so the whole digest runs as one bounded child (default
120s). Whatever it emitted before the bound survives, and the parent adds a
loud STARTUP TRUNCATED banner naming the stage that stalled and every stage
that never ran, still exiting 0.

--reemit skips only the sweeps startup already reconciled. It still re-verifies
lock ownership and still drains queued wakes, which arrived after startup and
are the turn's work. fm-bootstrap.sh gains FM_BOOTSTRAP_LOCKED so a re-emit
keeps repair ownership instead of deferring to a lock holder that is itself.

Also adds bin/fm-timeout-lib.sh as the single owner of bounded execution,
replacing three near-identical copies, and gives the ahoy skill a helm check
so a nudge-tier harness cannot recap before taking the helm.

Verified live on 2026-08-05 against Claude 2.1.222, Codex 0.146.0, and Pi
0.82.0; docs/verification/supervision.md records the per-harness source
vocabulary, the two named gaps, and the refresh command.

* no-mistakes(review): Harden session-start completion, timeout, and Pi delivery

* no-mistakes(review): Harden completion ownership and portable timeout escalation

* no-mistakes(review): Normalize watchdog KILL exits without masking command status

* no-mistakes(review): Guarantee startup bounds and align harness delivery tiers

* no-mistakes(test): Fix Pi session-start live verification fixture

* no-mistakes(document): Align session-start documentation with deterministic hooks

* no-mistakes(lint): Silence intentional child-shell expansion lint warning

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* fix: rename X mode to Relay in user-facing docs (#1784)

* docs: rename the user-facing product name to Relay

The public-mention integration gated by the `.env` pairing token is now
called Relay across user-facing prose, covering X and Discord alike
instead of implying a single network.

Renames the product-name strings only: README, docs, the captain-facing
skill descriptions, and the AGENTS.md operating prose, including the
`X mode (.env)` and `Optional X mode` headings and every link anchor
that pointed at them. AGENTS.md section 14 carries a one-line bridge
note so the older name and the unchanged identifier spellings stay
discoverable.

Internal identifiers are untouched: `FMX_*`, `config/x-mode.env`,
`state/x-*`, `bin/fm-x-*`, the `fmx-respond` skill path,
`__FM_X_MODE_ENV__`, and `x-mode-error`. Platform references to X and
Discord as networks stay as they are, and the bootstrap-diagnostics
entry still quotes bootstrap's emitted `FMX: X mode on/off` line
verbatim because `bin/` output is out of scope for this pass.

* no-mistakes(review): Complete Relay prose rename in maintained docs

* no-mistakes: apply CI fixes

* feat: add Muse Code crewmate adapter (#1786)

* feat(harness): add a verified muse crewmate adapter

Muse Code joins the fleet as a crewmate/scout adapter, verified live against
Muse Code 0.1.0-R708.1 in an isolated lab.

Detection matches the anchored prefix muse-bin*, because the installed launcher
execs a version-suffixed binary whose name changes on every auto-update and
whose install path carries no muse component to fall back on. The same identity
is taught to the tmux liveness classifier, without which a healthy muse pane
would have read as a dead endpoint.

Busy state folds muse's own durable session event log, bound per task by a
sessions-root/worktree sidecar. It is a pull source with no writer, so nothing
is armed and no record is ever seeded. The fold is anchored on the full run
lifecycle prefix so muse's nested cleanup "terminal" payloads cannot settle an
in-flight run, and it is depth-bounded so muse's native sub-agent logs cannot be
mistaken for the parent's. The idle half stays gated: an open run proves busy,
but a settled log reads unknown until a credentialed multi-step run proves one
turn stays inside one run.

Two findings corrected the scout report. The exec-only
--no-foreign-personal-context flag is rejected by the interactive TUI, so the
privacy control that actually reaches a pane worker is
MUSE_EXPERIMENTAL_FOREIGN_PERSONAL_CONTEXT_KILL, verified to drop the operator's
foreign personal rules while keeping the project's own AGENTS.md. And an
unauthenticated muse pane never exits, it waits on a device-code prompt, so
credentials are a spawn preflight rather than a screen check.

muse is refused for secondmates: it has no primary supervision protocol and its
hook dialect rejects the reawakening handlers that protocol needs.

Per the captain's decision, auto-update is not pinned, and the credentialed
multi-step smoke is deferred with an explicit checklist in
docs/verification/muse.md.

* no-mistakes(review): Accept Muse dispatch profiles and shared efforts

* no-mistakes(review): Bind Muse busy state to current session

* no-mistakes(review): Compare Muse workspace bindings literally

* no-mistakes(review): Harden Muse worker credentials and live signal verification

* no-mistakes(review): Cache Muse session bindings and clarify worker credentials

* no-mistakes(review): Clear Muse marker inheritance and normalize interrupt aliases

* no-mistakes(review): Verify Muse glyph effective foreground color

* no-mistakes(review): Harden Muse XDG paths, session cache, and glyph parsing

* no-mistakes(document): Document Muse adapter boundaries

* fix(herdr): require 0.8.0 for default presentation spaces (#1787)

* fix(herdr): floor default-on presentation spaces at Herdr 0.8.0

Default-on presentation projection turns every crewmate teardown into a
workspace-emptying removal. The focus-safe removal plan avoids Herdr's
focus-stealing explicit close only while the doomed pane's shell can be proved
lone, childless, and idle; a persistent child of that shell (gitstatusd, a
zsh-async worker, direnv) fails that proof permanently and forces the plain
close, which on every release before Herdr 0.8.0 moves the captain's active
workspace for ~140ms on each teardown.

Gate the unconfigured default behind a Herdr 0.8.0 floor. At or above it,
project as before; below it, fall back to the flat per-home layout with one
warning per home per detected release naming the version and the upgrade. An
explicit "on" - including the historical empty opt-in file - is still honored
below the floor, so a deliberate opt-in is never silently downgraded.

The floor reads two independent signals from the client's own status, either of
which can establish a supported release: the protocol number and the release
core of the version string. Measured against the real release binaries, no build
lacking both upstream focus fixes reaches protocol 19 and every pre-fix build
tops out at 17, so protocol 19 is a safe structural expression of the floor. A
release that reports neither signal readably is treated as unsupported rather
than guessed at.

Also:
- Correct the adapter comment claiming the mitigation "stays safe without any
  version gate". That holds for the pane-death route only; the plain-close
  fallback is reachable precisely on the releases where it is unsafe.
- Stop discarding the projected-close helper's stderr at teardown, so a refused
  or failed focus restore is visible instead of silent. The close stays
  non-fatal; the presence gate still decides record removal.
- Add Part C to the focus-flash regression: a doomed pane whose shell holds a
  persistent child, in the geometry where the closing workspace's right
  neighbour is not the anchor. That is the fallback branch the suite could not
  structurally reach. On 0.7.5 it observes a bounded four-sample wrong-focus
  window restored exactly; on 0.8.0 it observes none. It also cross-checks its
  own measurement against the floor classifier, so a drifted protocol mapping
  fails loudly.
- Make the projection suite's unconfigured-home case release-aware, so the whole
  real-Herdr lane passes on both the CI-pinned 0.7.4 and 0.8.0.
- Add an opt-in live guard that re-measures the release-to-protocol mapping
  against the pinned upstream binaries.

The immediate no-code mitigation for a home that cannot upgrade remains writing
"off" into config/herdr-presentation-spaces.

* no-mistakes(review): Pin Herdr live-guard digests across supported platforms

* no-mistakes(review): Document authorized Herdr cleanup containment

* no-mistakes(review): Harden Herdr warning marker publication

* no-mistakes(review): Honor running Herdr server presentation floor

* no-mistakes(review): Recheck Herdr floor after server ensure

* no-mistakes(review): Refresh 0.7.5 and 0.8.0 focus transcripts

* no-mistakes(review): Route Herdr floor probe through lab session

* no-mistakes(document): Align Herdr floor documentation and comments

* no-mistakes(lint): Document Herdr presentation out-parameter consumer

* fix(bin): classify settled Muse session logs as idle (#1788)

* 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

* docs(agents): read the persisted digest when only a preview is shown (#1794)

* fix: preserve fleet state in truncated session-start digests (#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

* feat(send): close answered decisions at answer time via --resolve-key (#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.

* fix(bin): seed remote secondmates from supplied origins (#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

* fix(tests): restore reliable fm-send backend parity coverage (#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. #1842 made bin/fm-send.sh source
bin/fm-line-cap-lib.sh (added by #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 remote secondmate status streams (#1846)

* 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

* docs(agents): describe the digest's fleet-state-before-context order (#1826)

* fix(bin): fail closed on NUL bytes in the durable parent binding (#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.

* fix(skills): reconcile inherited secondmate plans with shipped state (#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

* fix: move network checks off the session-start blocking path (#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 #1845, which was cut from a
pre-#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 #1826's
fleet-state-before-context ordering. The old-bin test shim keeps main's
git-archive baseline from #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 remote replies during capture (#1831)

* 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

* feat(skills): port internal stow curation disciplines to the public skill (#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).

* chore(bootstrap): raise lavish-axi version floor to 0.1.46 (#1865)

* fix(bin): keep tracked Claude hook entries inert under grok 1.0.0 (#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

* feat(startup-network): record per-step elapsed times for the deferred stage (#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.

* feat(stow): cascade the internal /stow to every registered secondmate (#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

* fix(remote-job): stop workers abandoned by a pruned code root (#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.

* feat(bin): lint only the changed shard locally, full lint in CI (#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 (#1568)

* 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): add tiered decaying memory management (#1984)

* 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 project vision (#1997)

* 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 (#2005)

* fix(spawn): force regular Pi TUI for crews

* no-mistakes(document): Documented Pi regular TUI launch mode

* fix(cmux): classify borderless Claude composers (#2029)

* fix(cmux): classify borderless Claude composer

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

* no-mistakes(document): Document cmux borderless Claude composer classification

* docs(stow): generalize read-before-write in the public stow skill (#2091)

The public installer-facing stow skill scoped its classify-then-replace
discipline to TODO/BACKLOG items only, so findings routed to a memory file
had no stated rule against a blind append or a wholesale overwrite.

Step 6 now classifies every finding against the destination's current
contents as new, duplicate, superseding, or obsolete, and states the
considered replacement each classification implies. The outcomes follow the
tiered-memory contract already in the file: an obsolete entry is refreshed,
archived, or replaced in a way that preserves its fact, a duplicate folds
into the entry that already carries it, and a superseded body worth keeping
leaves through step 7's existing exits rather than a second recovery
mechanism.

* fix: resurface durable supervision work after re-arm (#2065)

* fix(watcher): resurface durable work after downtime

* no-mistakes(review): Make watcher rearm recovery durable and cursor-safe

* no-mistakes(review): Persist safe recovery markers across migration lock recovery

* no-mistakes(review): Retain stale lock when recovery marker publication fails

* no-mistakes(review): Preserve delivery-gap recovery and quarantine malformed markers

* no-mistakes(review): Serialize recovery consumption and report acknowledgment failures

* no-mistakes(review): Centralize recovery publication before clearing watcher evidence

* no-mistakes(review): Guarantee recovery evidence across queue and lock handoffs

* no-mistakes(review): Publish recovery evidence before durable wake commits

* no-mistakes(review): Replace recovery marker Perl dependency with Node

* no-mistakes(review): Keep interrupted wakes durable until handling acknowledgment

* no-mistakes(review): Add post-handling durable wake acknowledgements

* no-mistakes(review): Enforce post-handling acknowledgement across recovery and AFK return

* no-mistakes(review): Bind wake acknowledgements to recovery generations

* no-mistakes(review): Align wake regressions with generation-bound acknowledgements

* no-mistakes(document): Document durable re-arm recovery semantics

* no-mistakes(lint): Resolve ShellCheck warnings in recovery and watcher tests

* no-mistakes: apply CI fixes

* test(watcher): assert post-handling wake replay

* no-mistakes(review): Prevent successor loops and adopt legacy wake generations

* no-mistakes(review): Rearm durable wakes without recursive successor recovery

* no-mistakes(review): Align recovery tests with handling marker state

* no-mistakes(review): Delay handling transition until successor launch is established

* no-mistakes(review): Confirm wake handling only after successful prompt delivery

* no-mistakes(review): Acknowledge AFK wakes only after evidence publication

* no-mistakes(review): Prevent AFK wake loss before post-handling acknowledgement

* no-mistakes(document): Document durable wake acknowledgement semantics

* no-mistakes(lint): Suppress false positive for recovery action output

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* ci: measure Herdr automation on Windows runners (#2100)

* ci: add Windows Herdr automation spike

* ci: run Windows spike on its pull request

* fix: wait for Windows Herdr command output

* fix: run ANSI probe in pane shell

* ci: keep Windows Herdr spike manually triggered

* docs: clarify Windows Herdr spike verdict

* feat(ahoy): guide captains through open decisions (#2099)

* Add guided ahoy decision flow

* no-mistakes(document): Document guided Ahoy decision flow

* fix(stow): enforce startup-memory budget decisions (#2110)

* Harden stow memory budget policy

* Refine internal stow offload policy

* no-mistakes(review): Enforce shared-budget decisions and autonomous offload

* fix(spawn): refresh pooled worktrees from origin before launch (#2116)

* fix(spawn): refresh pooled worktree base

* no-mistakes(document): Document spawn base-freshness invariant

* no-mistakes: apply CI fixes

* fix(composer): unify safe classification across backends (#2102)

* refactor(composer): one shape owner behind thin capture adapters, whole matrix fixed

Consolidate every composer shape - bordered boxes (all families, geometry,
titled bottom borders), bare agent-glyph rows and their wrap regions,
opencode's left bar, and pi's identity-gated separator pair - into
fm_composer_classify_screen in bin/fm-composer-lib.sh. Adapters now
contribute only a capture and a declarative capability descriptor
(styled/cursor/identity/rows); capability differences change how confidently
a shape is judged, never what the shapes are, so a new harness shape is
teachable in exactly one place.

Correctness fixes landed as part of the consolidation (audit
data/fm-composer-consolidation-audit-s1):
- locale-safe Unicode-space normalization in the shared owner (closes the
  fleet-wide half of #1988; cmux's local byte-exact NBSP case deleted;
  naming converges with PR #1995's normalization primitive)
- muse's bare glyph joins the shared set, unbreaking muse on herdr/cmux/orca
- orca learns the borderless bare shape, drops its backward-paged composer
  window, and can no longer classify a stale startup banner as the composer
- tmux tolerates a titled bottom border, unbreaking grok steering
- the left-bar shape makes opencode readable on every backend
- zellij gets a real classifier through dump-screen --ansi, replacing the
  content-diff submit heuristic that could confirm an undelivered message
  and close a --resolve-key decision (the fleet's only false positive)
- fm-spawn's kimi launch-readiness regex (the fourth shape copy) now routes
  through the shared classifier

The strict blank-row posture applies fleet-wide (captain decision
blank-row-injection-posture): no positive container proof = unknown = defer,
replacing tmux's permissive blank-cursor-row rule. Away-mode injection was
re-validated end to end on real tmux (defer on partial input and unproven
rows, clean delivery with swallowed-Enter retry into proven-empty
composers). The tmux submit core gains a baseline-idle turn-started
conversion so pi steering stays confirmed while its working screen hides
the composer; busy conversion without that baseline remains forbidden.

Plain-capture backends now degrade a glyph row carrying trailing text to
unknown instead of a false pending, per the approved capability rule.

Portable regressions pin the full byte-capture matrix from the audit under
a UTF-8 locale and LC_ALL=C, the strict-vs-permissive divergence, and
deliberate signal separation; the opt-in live guard
(tests/fm-composer-matrix-live-e2e.test.sh) verified every installed
harness against the real classifier, recorded in
docs/verification/runtime-backends.md.

* no-mistakes(review): Fix Pi glyph ambiguity and complete profile matrix

* no-mistakes(review): Preserve bare verdict when Pi identity probe is absent

* no-mistakes(review): Harden composer structure and titled-border geometry

* no-mistakes(review): Require proven idle baseline and strict Zellij guard

* no-mistakes(review): Reject box bottom borders as composer input rows

* no-mistakes(review): Prove Zellij probe typing before classifier retries

* no-mistakes(review): Preserve Pi identity uncertainty and scan full left-bar drafts

* no-mistakes(review): Verify Zellij text lands before submitting

* no-mistakes(review): Scope Zellij typing verification to selected composer content

* no-mistakes(review): Verify Zellij pastes through composer-scoped content deltas

* no-mistakes(review): Prove wrapped bare Zellij pastes through composer extraction

* no-mistakes(review): Invalidate stale cursorless composers below dead shell prompts

* no-mistakes(review): Handle shell prompt placeholders in composer extraction

* no-mistakes(review): Classify cursorless bare continuation regions safely

* no-mistakes(review): Reject stale cursorless containers below live activity

* no-mistakes(review): Preserve prompt glyphs in wrapped Zellij pastes

* no-mistakes(review): Reject live shell rows during composer extraction

* no-mistakes(review): Preserve wrapped glyph continuations through submit retries

* no-mistakes(review): Scope idle placeholders to proven positions

* no-mistakes(review): Restore boxed placeholders and live prompt reanchoring

* no-mistakes(review): Fix Zellij placeholder and wrapped glyph paste proof

* no-mistakes(document): Align composer architecture documentation

* no-mistakes(lint): Fix ShellCheck warnings in composer refactor

* no-mistakes: apply CI fixes

* docs(verification): record the trusted-checkout live matrix rerun

The pipeline's isolated gate worktree is untrusted, so claude, grok, and
muse stopped at first-launch trust dialogs there (the guard refuses to
confirm them by design). This rerun from the trusted checkout at the final
validated head verified all six installed harnesses, the strict blank-row
deferral, and the hardened zellij false-positive probe live.

* no-mistakes(document): Align composer verification evidence

* no-mistakes: apply CI fixes

* no-mistakes(review): Restore proven box bottom-cursor classification

* no-mistakes(review): Preserve styled placeholder-like drafts as pending

* no-mistakes(document): Align composer safety and Zellij delivery documentation

* no-mistakes: apply CI fixes

* docs(verification): refresh the live matrix with the final-head trusted rerun

The post-validation rerun from the trusted checkout verified all six
installed harnesses at the branch's final head, including Claude 2.1.227
(auto-updated since the audit's captures) and Grok, which the untrusted
gate worktree could not verify past their first-launch trust dialogs.

* fix(spawn): gate Pi TUI mode by CLI capability (#2117)

* fix(spawn): gate Pi regular TUI flag by capability

* no-mistakes(review): Document conditional Pi TUI capability detection

* no-mistakes(review): Pin Pi probing and launch to one executable

* no-mistakes(review): Preserve literal pinned Pi paths and update documentation

* no-mistakes(review): Defer pinned Pi path insertion until final substitution

* no-mistakes(document): Document version-safe Pi launch probing

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* docs(vision): elevate experience, pain narrative, and distro virtues (#2147)

* docs(vision): elevate experience, pain narrative, and distro virtues

Fold the captain's public vision framing into VISION.md: peace of mind as a
primary goal, multi-session context-switch pain as the problem one interface
solves, clone-and-run setup ease, self-evolution including community, and
explicit harness/backend orthogonality. Reconcile experience-as-garnish into
experience-as-purpose and update aligns/resists accordingly.

* docs(vision): state the experience goal positively

Drop the negative "not a smart workflow / useful tool / impressive technology"
pretext. Lead straight into the positive experience north star.

* feat(bin): reconcile inactive terminal crew outcomes (#2167)

* fix: reconcile inactive terminal outcomes

* fix: stream secondmate summary inputs

* no-mistakes(review): Fix reconciliation locking and request delivery retries

* no-mistakes(review): Prevent retries after unknown request delivery

* no-mistakes(document): Clarify inactive reconciliation cadence and receipts

* no-mistakes(lint): Quote terminal status arguments in reconciliation tests

* refactor: simplify inactive outcome reconciliation

* no-mistakes(review): Bound inactive reconciliation scans with durable progress

* no-mistakes(review): Bound reconciliation and deduplicate recovery notices

* no-mistakes(document): Document inactive outcome reconciliation contracts

* no-mistakes(review): Reject relative local secondmate parent routes

* no-mistakes(review): Key terminal receipts by spawn incarnation

* no-mistakes(review): Stabilize legacy receipts and lock reconciliation snapshots

* no-mistakes(review): Fail closed on invalid secondmate identity markers

* no-mistakes(document): Document durable inactive-outcome reconciliation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* ci: raise Herdr test timeout (#2191)

* fix: refresh stale Pi instructions after compaction (#2163)

* fix(session-start): refresh drifted instructions on stale rebuilds

* test(session-start): prove Pi instruction refresh end to end

* no-mistakes(review): Fix stale instruction refresh and baseline integrity

* no-mistakes(review): Preserve true-start baselines across Pi continuations

* no-mistakes(review): Correct Pi continuation classification and live expectation

* no-mistakes(review): Correct Pi continuation coverage documentation

* no-mistakes(review): Fix read-only refresh and exact Pi session restores

* no-mistakes(review): Classify Pi create-if-missing sessions correctly

* no-mistakes(review): Classify named Pi sessions using immutable headers

* no-mistakes(review): Correct Codex interactive coverage diagnostic

* no-mistakes(document): Document immutable Pi compaction instruction refresh

* no-mistakes(document): Correct Pi refresh documentation and validation claims

* feat: add deterministic condition-to-action watcher (#2200)

* feat(bin): add deterministic condition->action watch adapter on the process-event channel

Register a (condition, action) pair once with bin/fm-procevent-when.sh and the
existing process-to-event runner polls the condition tokenlessly, fires the
a…
levelupself added a commit to levelupself/firstmate that referenced this pull request Aug 16, 2026
…ts preserved) (#58)

* feat(bin): require an explicit per-task delivery contract (#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.

* feat(bin): support remote secondmate homes (#1576)

* 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: add per-task trace context propagation (#995)

* 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): harden tmux agent liveness across harnesses (#1577)

* 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

* feat(bin): propagate trace context to remote secondmates (#1609)

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

Per-task W3C trace context (#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): preflight remote runtime tool paths (#1623)

* 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: gate remote second mates on Herdr readiness (#1639)

* 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 #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

* fix: isolate remote secondmates in shared Herdr session (#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

* feat: route remote commands through an Aqua job worker (#1660)

* feat: run remote commands through Aqua job worker

* no-mistakes(review): Enforce remote job deadlines and safe worker shutdown

* no-mistakes(review): Refresh stale workers and harden dependency-free supervision

* no-mistakes(review): Harden worker ownership recovery and shutdown quarantine

* no-mistakes(review): Fix doctor bootstrap, harness repair, and output draining

* no-mistakes(review): Probe doctor tools through authenticated worker bootstrap

* no-mistakes(review): Refresh stale workers before doctor tool probes

* no-mistakes(review): Recover stopped quarantines and extend job deadlines

* no-mistakes(review): Separate queue and execution timeout windows

* no-mistakes(review): Supervise Linux worker crashes and bind root identity

* no-mistakes(review): Resolve authorized Nix profile bin links

* no-mistakes(review): Clarify Nix path resolution documentation

* no-mistakes(review): Harden PATH safety and nvm selection

* no-mistakes(review): Honor nvm system defaults and refresh doctor digest

* no-mistakes(review): Keep workers ready during active jobs

* no-mistakes(review): Bound pre-execution validation by job timeout

* no-mistakes(document): Clarify remote worker documentation

* no-mistakes(lint): Fix remote worker ShellCheck diagnostics

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* fix: clarify remote doctor bootstrap path (#1691)

* fix(bin): bound remote SSH dead-peer detection (#1699)

* fix(remote): arm SSH dead-peer detection in fm-on.sh

A vanished remote host mid-poll (a reboot, a dropped link) left ssh
blocked indefinitely on a half-open TCP connection, because fm-on.sh's
ssh invocation had no ServerAliveInterval/ServerAliveCountMax. This
wedged the remote-reply ferry: fm-procevent.sh's runner blocked inside
the ssh child and never reached its own no-result -> claim-release ->
reconcile re-arm self-healing path, which otherwise already handles a
nonzero exit with empty output correctly. Recovery required a manual
retire and re-arm.

Arm ServerAliveInterval=15 and ServerAliveCountMax=3 by default
(bounded ~45s detection window), both overridable via
FM_SSH_ALIVE_INTERVAL and FM_SSH_ALIVE_COUNT_MAX. This is a transport-
level fix in fm-on.sh, so it covers every remote command routed
through it, not just the reply ferry. The remote sshd answers
keepalive probes independently of whatever the remote command is
doing, so a legitimately long-but-alive command (a 55s poll, a clone,
the doctor) is never falsely killed - only a truly vanished peer trips
it, turning that case into a bounded, detectable ssh failure (exit
255) instead of an indefinite hang.

Extends tests/fm-on.test.sh with a behavioral regression asserting a
bounded, positive ServerAliveInterval/ServerAliveCountMax on the real
ssh argv captured through the FM_SSH_BIN process seam, plus coverage
that both are env-overridable.

* no-mistakes(document): Document SSH dead-peer detection ownership

* fix: report stale AXI tools during bootstrap (#1701)

* feat(bootstrap): gate stale axi CLIs at the floors firstmate actually uses

Add gh-axi 0.1.29 floor so bare --squash PR merges stop failing quietly on
older builds. Raise tasks-axi to FM_TASKS_AXI_MIN=0.2.2 (multi-id mv) while
keeping feature probes. Keep quota-axi at 0.1.16 after verifying schema 3
and per-model availability already ship there; runway remains optional.

* no-mistakes(document): Clarify AXI compatibility documentation ownership

* fix: prevent false watcher-down alarms in Claude sessions (#1661)

* fix(guard): stop false watcher-down alarm mid-turn under Claude auto-arm

bin/fm-guard.sh derived its watcher-health verdict from fm_watcher_healthy,
which requires a live watcher process holding the home lock. Under the Claude
Stop-hook auto-arm supervision model the watcher is armed at each turn end and
exits on its wake, so it runs only between turns. Every guarded command run
mid-turn therefore found no live watcher and printed the "WATCHER DOWN -
SUPERVISION IS OFF" banner even though supervision was healthy. Because the
episode key was derived from the beacon mtime (which the between-turns watcher
advances every poll), the full banner re-printed on essentially every command,
and the message always blamed a "fresh beacon" that was in fact fresh.

Make the pull guard's health check model-aware via a new
fm_watcher_supervision_verdict in bin/fm-wake-lib.sh:

- Under the auto-arm model a beacon fresh within FM_GUARD_GRACE is healthy even
  with no live watcher process; only a beacon stale beyond grace (or absent) is
  a genuine lapse and alarms.
- Under every persistent-watcher harness (codex foreground checkpoint,
  opencode/pi/grok background arm, tmux, unknown) a live identity-matched
  watcher with a fresh beacon is still required, unchanged.

The banner now names the true failing condition, a missing live watcher process
versus a genuinely stale beacon, instead of always blaming the beacon, and the
once-per-episode dedup keys on that condition rather than the beacon mtime so a
genuine lapse announces once and does not re-print each turn.

The turn-end guard keeps the strict fm_watcher_healthy check because it fires at
the turn boundary, where the auto-arm brings a fresh watcher up and it
cooperates with that arm. fm_watcher_healthy itself is unchanged, so the arm
layer's start/attach/replace decisions are unaffected.

Tests in tests/fm-guard-stale-banner.test.sh cover the auto-arm healthy
fresh-beacon-without-a-watcher case, the auto-arm stale-beacon alarm and its
stable episode, the true-reason banner wording, and the reason-keyed episode
surviving a beacon mtime change; existing persistent-model cases are pinned to
that model.

* no-mistakes(review): Pin secondmate supervision model to launched harness

* no-mistakes(document): Align watcher documentation with model-aware supervision health

* test: prevent fixture temporary directory leaks (#1704)

* fix(tests): stop fixture-tempdir helper from self-deleting under command substitution

fm_test_tmproot is almost always called as `TMP_ROOT=$(fm_test_tmproot prefix)`,
which forks a subshell to capture its stdout. The old implementation set its
EXIT cleanup trap inside that call, so the trap fired - and deleted the fixture
root - the instant the subshell exited, before the real caller's own EXIT trap
was ever installed. Every test using the documented call pattern leaked its
fixture root on every run; two suites had already independently discovered and
worked around this with ad-hoc mktemp calls.

Registration now goes through a $$-keyed registry file instead of in-process
state, since $$ resolves to the invoking shell's PID even inside the
subshell. The real cleanup trap is armed once at source time (always the real
caller, never a subshell) for EXIT, INT, and TERM. A best-effort orphan sweep
on next source reaps marked fixture roots old enough to be from a killed prior
run.

Simplifies the two existing ad-hoc workarounds (fm-procevent.test.sh,
wake-helpers.sh) back onto the shared helper now that it works correctly.

* no-mistakes(review): Preserve live fixtures during orphan reaping

* no-mistakes(review): Harden fixture ownership against PID reuse

* no-mistakes(review): Secure cleanup registry against path precreation

* no-mistakes(review): Make fixture registration transactional

* no-mistakes(document): Documentation already matches fixture cleanup behavior

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* feat(herdr): enable presentation spaces by default (#1708)

* feat(herdr): default presentation spaces on with an explicit opt-out

Herdr's disposable one-task presentation workspace was opt-in through the
presence of local config/herdr-presentation-spaces. It is now on by default,
and a home opts out by writing "off" into that same file.

Values are read with the whole-file whitespace-stripped convention the other
scalar config items already use, plus case folding. An absent file, an empty
file, and "on" all resolve on; only "off" opts out; an unrecognized value warns
and keeps the default rather than failing a spawn over a purely visual setting.
The empty file is exactly the historical opt-in form, so every home that had
already enabled the projection stays enabled with no migration step, and no
previously enabled home can be turned off by the flip.

Because absence now means on at both ends, secondmate inheritance needs no
item-specific convergence: mirroring an absent primary file converges a
secondmate to the same default-on rather than turning its projection off, and
only an explicit primary opt-out propagates the opt-out.

The gate itself moves into fm_backend_herdr_presentation_enabled in the Herdr
adapter so the semantics have one owner that regressions can exercise directly.

* no-mistakes(document): Document Herdr default-on presentation safety

---------

Co-authored-by: kunchenguid <kun-1@kunchenguid.com>

* fix(bin): surface fleet-wide open decisions on every wake drain (#1711)

* fix: surface consolidated open decisions on every wake-drain

A needs-decision or blocked event buried under later, unrelated status
appends was only ever shown via the last-line wake annotation, so a
still-open captain decision could go silently missed even though
status_open_decisions (fm-classify-lib.sh) already folds the whole
status stream correctly and fleet-snapshot/bearings already reuse it.

Wire that same fold into bin/fm-wake-drain.sh: a new fleet-wide
scan_open_decisions wrapper scans every state/<id>.status, and
fm-wake-drain.sh prints a separate, bounded OPEN DECISIONS section on
every drain (including the empty-queue fast path), so session-start
and every wake-handling turn surface it for free without duplicating
the open/resolved fold itself. Heartbeat wakes drain through the same
script, so this covers that surface too.

Also tighten status_open_decisions' file guard to skip an unreadable
status file instead of leaking a bash redirection error, now that a
fleet-wide directory scan can reach files a single targeted read
would not.

* no-mistakes(review): Prevent status symlinks leaking open decisions

* fix: drop unbounded perl subprocess from status symlink guard

The review step's own symlink-safety auto-fix (O_NOFOLLOW read via a
perl subprocess) forked one perl process per status file scanned by
the new fleet-wide open-decisions scan, with no cap - inflating
fm-wake-drain.sh's total external-read cost from 8 (the existing
annotation read_cap) to 18 in the enrichment-caps regression test.

The plain [ -L "$f" ] check already rejects any status file that is
itself a symlink before any read happens, which is exactly what the
new regression test exercises and is the same defense level the
sibling scan_captain_relevant_statuses/last_status_line already rely
on elsewhere in this file (no O_NOFOLLOW). Drop the subprocess-based
nofollow read and keep the cheap builtin guard.

* no-mistakes(document): Document actionable fleet-wide open decision drains

* fix(bin): abort parked runs and reap leaked processes before teardown (#1710)

* fix(bin): abort orphaned no-mistakes runs and reap leaked processes at teardown

Teardown could remove a task's worker while its no-mistakes pipeline run was
still parked at a gate, leaving an orphaned run holding a fleet slot
indefinitely (observed 2026-08-03: runs parked 7h39m and parked at a
post-CI approval gate). It could also leave backgrounded/disowned
descendant processes rooted under the worktree or tasktmp surviving
reparented to init (observed: two `go test` binaries pinning CPU for
hours with no live task meta to attribute them to).

Add two coupled pre-teardown steps, both scoped to this task's exact
branch/head or worktree/tasktmp so they can never touch another task's
run or processes:
- conclude_task_no_mistakes_run aborts a run parked at a gate via
  `no-mistakes axi abort`, cd'd into the exact worktree so the daemon
  resolves the run itself rather than teardown naming a --run id.
- reap_task_worktree_processes sweeps for processes whose cwd is under
  the worktree or tasktmp (via `lsof -a -d cwd`) and TERM/KILLs them.

Both run before any worktree return, branch delete, or backend kill,
and are idempotent on a retried teardown. The branch+head attribution
logic is factored out of bin/fm-crew-state.sh into the new shared
bin/fm-nm-run-lib.sh so both scripts use the same ownership contract.

* no-mistakes(review): Fail closed on incomplete teardown cleanup

* no-mistakes(review): Bind teardown cleanup to verified run and process identities

* no-mistakes(review): Require confirmed aborts and convergent identity-safe process reaping

* no-mistakes(review): Handle process exits during teardown identity checks

* no-mistakes(review): Restore teardown library in hermetic gotmp fixtures

* no-mistakes(document): Document teardown run attribution and timeout

* no-mistakes(lint): Rename shell variable conflicting with done keyword

* no-mistakes: apply CI fixes

* fix: resolve fm-remote-entrypoint.sh SCRIPT_DIR through a PATH symlink (#1709)

The script installs as a symlink under ~/.local/bin. Taking dirname of
the symlink itself (instead of its real target) pointed SCRIPT_DIR at
~/.local/bin, breaking sourcing of the sibling fm-remote-job-lib.sh.
Resolve the real path first, preferring python3's os.path.realpath,
then realpath, falling back to the raw BASH_SOURCE on hosts with
neither.

* fix(pi): gate Calm built-in overrides by activation state (#1724)

* fix(pi): stop Calm claiming a built-in tool name another extension owns

fm-calm.ts claimed bash/read/edit/write/grep/find/ls unconditionally at
extension load, regardless of whether Calm was on. Pi resolves two
extensions registering the same built-in name by first-registered-wins
with no merge and no unregister call, and Calm's project-local
.pi/extensions/ position beats any global or CLI-configured extension,
so a user who never even enabled Calm could have their own bash/read/etc
override silently replaced.

Captain-approved plan implemented:

- Registration is now gated on config/calm already being "on" at load
  time. A Calm-off session or reload registers nothing, so a non-Calm
  user never contests a name. This stays synchronous during the
  factory's own load, not deferred to session_start: /reload (and
  ctx.newSession/fork/switchSession) render the restored transcript from
  a pre-session_start snapshot of the tool registry, so a deferred claim
  would miss that render - confirmed by tests/fm-calm-pi-extension
  .test.sh's hidden-block-geometry E2E when trialed.
- The first time Calm turns on in a session that started off
  (activateBuiltInsIfNeeded, from the /calm command handler), Calm calls
  pi.getAllTools() - safe only once every extension has finished loading,
  unlike the load-time path above - to see whether a different extension
  already owns a name, and skips claiming only that one, leaving it and
  its owning extension fully intact and callable.
- A contested name found this way prints a prominent ctx.ui.notify()
  warning naming the tool, plus a console diagnostic.
- reportBuiltInLosses() remains the backstop for the one case neither of
  the above can reach: a session that starts or reloads with Calm already
  on, where the registry snapshot is taken before Calm gets any chance to
  check ownership. A symlink-safe realpath comparison avoids misreporting
  Calm's own registration as foreign when its path crosses a symlink
  (macOS /tmp, /var).

Confirmed, bounded trade-off: the very first time a session that started
Calm-off turns Calm on, tool-call rows already on screen from before that
toggle do not retroactively collapse, because Pi never lets an extension
re-point an already-rendered row at a definition registered later. Every
session after that first toggle starts with the preference already on and
takes the synchronous load-time path, so the guarantee is intact from
then on. docs/calm.md and the file's own header document this in full.

tests/fm-calm-pi-extension.test.sh gains test_builtin_gate_load_time
(config/calm off registers nothing, on registers all 7 synchronously at
load) and test_calm_activation_collision_and_regression_bound (first
activation claims every uncontested built-in, leaves a foreign bash tool
fully intact and callable, warns and logs the contested name, and locks
in the documented pre-activation bound against real ToolExecutionComponent
rendering). test_rendering_and_session_lifecycle and the live interactive
E2E are updated for the new gate-at-load and first-activation-bound
contract.

* no-mistakes(document): Document Calm tool collision boundaries

* no-mistakes: apply CI fixes

* fix(bin): persist secondmate parent bindings for cleanup (#1727)

* fix(bin): give secondmate homes a durable parent binding record

Finished-worker cleanup on a remote second mate refused forever with
"cannot resolve the primary home ... durable parent binding". The
remote launch hands the child the remote code checkout as its parent
home (fm-spawn.sh's sole writer of FM_PUBLIC_FOLLOWUP_PRIMARY_HOME
receives FM_HOME=$FM_ROOT from fm-remote-secondmate-control.sh's
host-local launch), and that path can never carry the parent's real
records, so the guard refused unconditionally once relay looked active
anywhere on that host.

fm-home-seed.sh and fm-remote-home-provision.sh now write a durable
.fm-secondmate-parent record next to the .fm-secondmate-home identity
marker, naming the home's route to its parent as local (with the real
parent path) or remote (with the parent's SSH alias for diagnostics
only). fm-teardown.sh's cleanup gate reads it: a remote parent is out
of scope for the delegated-promise check (the whole promised-public-
reply subsystem is same-filesystem by construction, so a remote parent
can never hold one), while a token committed directly to the child's
own .env file - never the process environment - still refuses, so an
unrelated export in the remote host's login shell can no longer mask
in. For a local secondmate, the durable parent_home now also backs up
the launch-time env var, closing a silent fail-open where a restart
that dropped the launch prefix made the guard treat a genuinely active
parent relay as off.

Regression coverage drives the real remote route (SSH boundary + Herdr
fixture) and real fm-home-seed.sh seeding rather than hand-crafted
markers.

* no-mistakes(review): Captain: fail closed on unsafe durable parent records

* no-mistakes(review): Captain: enforce durable parent binding commit protocol

* no-mistakes(review): Captain: publish local parent binding before identity

* no-mistakes(review): Captain: refuse conflicting local parent bindings

* no-mistakes(review): Captain: reject non-regular secondmate seed leaves

* no-mistakes(review): Captain: enforce unique durable parent bindings

* no-mistakes(review): Captain: reject route-incompatible durable parent fields

* no-mistakes(document): Document durable secondmate parent bindings

* no-mistakes(lint): Fix secondmate parent parser ShellCheck warnings

* no-mistakes: apply CI fixes

* feat(bin): enforce latest AXI-family tool floors (#1733)

* feat(bin): gate lavish-axi at its session_ended floor in bootstrap

bin/fm-procevent-lavish.sh decides that a human "Send & End" review is
terminal by reading session_ended from the poll response's leading session
block. That field first shipped in lavish-axi 0.1.35, so an older installed
build silently leaves every ended review source armed forever and captures
an empty ended result on each later cycle. The same release is what makes a
plain reopen refuse a session the human deliberately ended.

Add LAVISH_AXI_MIN=0.1.35 to the existing axi-family floor structure in
bin/fm-bootstrap.sh, reusing tool_version_at_least and the same MISSING
diagnostic gh-axi already emits, so an incompatible build is reported as an
upgrade request before any review surface is armed. Later lavish-axi
releases only add artifact-authoring surface the adapter never reads, so
the floor is the feature-introduction point rather than latest.

Fixtures that stubbed lavish-axi as a bare exit-0 tool would now be read as
unparseable builds, so tests/lib.sh gains fm_fake_version_tool and every
bootstrap-running suite uses it for lavish-axi.

* no-mistakes(review): Clarify lavish-axi version floor rationale

* no-mistakes: apply CI fixes

* feat(bin): set axi-family floors to current latest under the bump policy

The axi-family bootstrap floors are the CURRENT LATEST published version of
each tool, captain-bumped periodically to move the whole fleet onto the
newest axi tools. They are not the minimum feature-introduced version. The
earlier lavish-axi work set a feature-minimum floor, which is the opposite
of this policy, so replace it along with the older feature-minimum rationale
carried by tasks-axi and quota-axi.

State the policy explicitly in bin/fm-bootstrap.sh's header, which owns it,
and in each per-tool floor owner, so no future change argues a floor back
down to the earliest release that happens to satisfy some behavior. Remove
the lavish-axi session_ended and upstream-PR citation, the tasks-axi
multi-ID-mv minimum argument, and the quota-axi credential-source argument
as floor rationale; the tasks-axi feature probes remain as a separate
defense-in-depth concern.

Floors: lavish-axi 0.1.45 (was 0.1.35), tasks-axi 0.2.4 (was 0.2.2),
quota-axi 0.1.17 (was 0.1.16), gh-axi 0.1.29 unchanged and already latest.
Each was verified against the tool's current published version.

The mechanism is unchanged: the same shared version helper and the same
MISSING diagnostic path. The below-fires and at-or-above-silent regression
rows move to the new floors, keeping each boundary genuine by pinning the
patch immediately below each floor rather than a version that was only
below the old one. Fleet fixtures move to the new floors so a bootstrap-
running suite is not reported as an out-of-date build.

Three operator-facing backlog handoff and receipt errors named "0.2.2+"
while the enforced floor moved, so they now point at the floor's owner
instead of duplicating a version number that drifts.

* no-mistakes(review): Centralize AXI floor policy beside constants

* no-mistakes(review): Clarify bootstrap boundary test comment

* no-mistakes(document): Centralize AXI floor policy rationale

* fix(bin): bound open decision scans with incremental cursors (#1737)

* fix(bin): bound OPEN DECISIONS scan cost with a per-status-file cursor

The fleet-wide OPEN DECISIONS scan added in #1711 re-reads and refolds
every task's entire lifetime status log on every drain, so its cost
grows unbounded with total log size. Add status_open_decisions_incremental
and scan_open_decisions_incremental to fm-classify-lib.sh: they persist a
per-status-file byte cursor plus the folded open-decision set, and fold
only newly appended bytes on each call, reusing status_open_decisions'
exact fold-line rule (extracted into _fm_decision_fold_line) so the two
strategies can never disagree on what is open. A missing or invalidated
cursor (new task, truncated/rewritten/shrunk log) falls back to a full
re-fold. bin/fm-wake-drain.sh now calls the incremental wrapper instead
of the whole-file scan.

* fix(bin): add O(1) rotation detection and read-failure guarding to the cursor fold

Add the two pieces the incremental open-decisions cursor was missing,
scoped to this repo's actual status-file usage (create-once, append-only,
never replaced or rewritten in place):

- An O(1) device+inode identity check (one stat call) alongside the
  existing size-shrink check, so a status file replaced/rotated/recreated
  at the same path is detected and falls back to a full re-fold, even
  when the replacement is the same size. A same-inode, same-size,
  in-place byte edit is a deliberately accepted gap: no code path in
  this repo ever does that to a status file.
- Checked reads: a stat/wc/tail failure is a genuine I/O error, not
  "the file is empty" - it now reports the already-trusted persisted
  open set unchanged instead of risking a silent invalidation.

Both stay O(1) plus new bytes per call, matching the cursor's bounded-
cost design; no content hashing or pending-fragment machinery.

* no-mistakes(review): Preserve cursor state across failed incremental reads

* no-mistakes(review): Refold status when cursor cache reads fail

* no-mistakes(document): Document cursor-backed open-decision scanning

* no-mistakes: apply CI fixes

* fix(bin): prevent remote polls from blocking session startup (#1754)

* fix(bin): preempt remote reply long-polls for queued short jobs

Session start on a home with live remote second mates could stall silently
for many minutes: the single serial remote job worker ran each armed
fm-remote-delta-read.sh reply poll to its full 55s window while bootstrap's
short sync, inherit, state, and route commands sat queued behind it, and
non-FIFO queue pickup let re-armed polls keep winning the lane. Measured
end to end, a trivial short job took 31s behind one 30s poll window.

The worker now preempts a running preemptible job (the read-only, cursor-
anchored delta read is the only member of that class) as soon as a
non-preemptible job is queued, publishing exit 75 with emptied output -
byte-identical to the poll's own elapsed-window-with-no-data result - so
the parent runner takes its existing no-result path and the watcher re-arms
from the same cursor with nothing lost. The delta read translates SIGTERM
into that same exit after removing its staging directory. Sibling polls
never preempt each other, so two armed monitors cannot churn. The same
measured scenario now completes in 1s.

* no-mistakes(document): Clarify remote poll preemption documentation

* docs: present X mode as the X and Discord public surface (#1778)

Discord mentions already ride the same pairing-token opt-in, relay poll,
and platform-aware reply path as X mentions, but the docs still read as
X-only, so a stranger could not self-serve the Discord path.

Add the numbered turn-on steps to the X mode configuration reference,
pointing at the myfirstmate dashboard for account creation, bot install,
and token issuance rather than duplicating operator setup here, and drop
the X-only framing from the README bullet, the documentation index, and
the architecture overview.

* fix(bin): run session start deterministically from hooks (#1781)

* feat(bin): run session start deterministically on hook-capable harnesses

Session start relied on a native nudge that only asked the agent to run
bin/fm-session-start.sh, and an agent can defer that. Observed 2026-08-01:
an /ahoy-first session followed the recap path and did not take the helm
until a later request forced it.

Claude, Codex, and Pi now RUN the digest in their session-open hook through
the new bin/fm-sessionstart-run.sh, so the full ordered digest is in model
context before the first turn. That wrapper is the single owner of what a
session-open source means: startup and Pi's "new" take the helm, clear and
compact re-emit, resume/reload/fork delegate to the nudge, and an unreadable
source takes the helm because doing that redundantly is idempotent while
skipping it is the bug. Grok and OpenCode keep the nudge as the floor, since
neither can carry hook stdout into a model turn.

Because the hook now blocks session initialization, fm-session-start.sh
bounds itself first. Its steps are not all individually bounded - bootstrap's
gh auth probe, tool version probes, the backlog listing and per-task endpoint
reads are unbounded - so the whole digest runs as one bounded child (default
120s). Whatever it emitted before the bound survives, and the parent adds a
loud STARTUP TRUNCATED banner naming the stage that stalled and every stage
that never ran, still exiting 0.

--reemit skips only the sweeps startup already reconciled. It still re-verifies
lock ownership and still drains queued wakes, which arrived after startup and
are the turn's work. fm-bootstrap.sh gains FM_BOOTSTRAP_LOCKED so a re-emit
keeps repair ownership instead of deferring to a lock holder that is itself.

Also adds bin/fm-timeout-lib.sh as the single owner of bounded execution,
replacing three near-identical copies, and gives the ahoy skill a helm check
so a nudge-tier harness cannot recap before taking the helm.

Verified live on 2026-08-05 against Claude 2.1.222, Codex 0.146.0, and Pi
0.82.0; docs/verification/supervision.md records the per-harness source
vocabulary, the two named gaps, and the refresh command.

* no-mistakes(review): Harden session-start completion, timeout, and Pi delivery

* no-mistakes(review): Harden completion ownership and portable timeout escalation

* no-mistakes(review): Normalize watchdog KILL exits without masking command status

* no-mistakes(review): Guarantee startup bounds and align harness delivery tiers

* no-mistakes(test): Fix Pi session-start live verification fixture

* no-mistakes(document): Align session-start documentation with deterministic hooks

* no-mistakes(lint): Silence intentional child-shell expansion lint warning

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* fix: rename X mode to Relay in user-facing docs (#1784)

* docs: rename the user-facing product name to Relay

The public-mention integration gated by the `.env` pairing token is now
called Relay across user-facing prose, covering X and Discord alike
instead of implying a single network.

Renames the product-name strings only: README, docs, the captain-facing
skill descriptions, and the AGENTS.md operating prose, including the
`X mode (.env)` and `Optional X mode` headings and every link anchor
that pointed at them. AGENTS.md section 14 carries a one-line bridge
note so the older name and the unchanged identifier spellings stay
discoverable.

Internal identifiers are untouched: `FMX_*`, `config/x-mode.env`,
`state/x-*`, `bin/fm-x-*`, the `fmx-respond` skill path,
`__FM_X_MODE_ENV__`, and `x-mode-error`. Platform references to X and
Discord as networks stay as they are, and the bootstrap-diagnostics
entry still quotes bootstrap's emitted `FMX: X mode on/off` line
verbatim because `bin/` output is out of scope for this pass.

* no-mistakes(review): Complete Relay prose rename in maintained docs

* no-mistakes: apply CI fixes

* feat: add Muse Code crewmate adapter (#1786)

* feat(harness): add a verified muse crewmate adapter

Muse Code joins the fleet as a crewmate/scout adapter, verified live against
Muse Code 0.1.0-R708.1 in an isolated lab.

Detection matches the anchored prefix muse-bin*, because the installed launcher
execs a version-suffixed binary whose name changes on every auto-update and
whose install path carries no muse component to fall back on. The same identity
is taught to the tmux liveness classifier, without which a healthy muse pane
would have read as a dead endpoint.

Busy state folds muse's own durable session event log, bound per task by a
sessions-root/worktree sidecar. It is a pull source with no writer, so nothing
is armed and no record is ever seeded. The fold is anchored on the full run
lifecycle prefix so muse's nested cleanup "terminal" payloads cannot settle an
in-flight run, and it is depth-bounded so muse's native sub-agent logs cannot be
mistaken for the parent's. The idle half stays gated: an open run proves busy,
but a settled log reads unknown until a credentialed multi-step run proves one
turn stays inside one run.

Two findings corrected the scout report. The exec-only
--no-foreign-personal-context flag is rejected by the interactive TUI, so the
privacy control that actually reaches a pane worker is
MUSE_EXPERIMENTAL_FOREIGN_PERSONAL_CONTEXT_KILL, verified to drop the operator's
foreign personal rules while keeping the project's own AGENTS.md. And an
unauthenticated muse pane never exits, it waits on a device-code prompt, so
credentials are a spawn preflight rather than a screen check.

muse is refused for secondmates: it has no primary supervision protocol and its
hook dialect rejects the reawakening handlers that protocol needs.

Per the captain's decision, auto-update is not pinned, and the credentialed
multi-step smoke is deferred with an explicit checklist in
docs/verification/muse.md.

* no-mistakes(review): Accept Muse dispatch profiles and shared efforts

* no-mistakes(review): Bind Muse busy state to current session

* no-mistakes(review): Compare Muse workspace bindings literally

* no-mistakes(review): Harden Muse worker credentials and live signal verification

* no-mistakes(review): Cache Muse session bindings and clarify worker credentials

* no-mistakes(review): Clear Muse marker inheritance and normalize interrupt aliases

* no-mistakes(review): Verify Muse glyph effective foreground color

* no-mistakes(review): Harden Muse XDG paths, session cache, and glyph parsing

* no-mistakes(document): Document Muse adapter boundaries

* fix(herdr): require 0.8.0 for default presentation spaces (#1787)

* fix(herdr): floor default-on presentation spaces at Herdr 0.8.0

Default-on presentation projection turns every crewmate teardown into a
workspace-emptying removal. The focus-safe removal plan avoids Herdr's
focus-stealing explicit close only while the doomed pane's shell can be proved
lone, childless, and idle; a persistent child of that shell (gitstatusd, a
zsh-async worker, direnv) fails that proof permanently and forces the plain
close, which on every release before Herdr 0.8.0 moves the captain's active
workspace for ~140ms on each teardown.

Gate the unconfigured default behind a Herdr 0.8.0 floor. At or above it,
project as before; below it, fall back to the flat per-home layout with one
warning per home per detected release naming the version and the upgrade. An
explicit "on" - including the historical empty opt-in file - is still honored
below the floor, so a deliberate opt-in is never silently downgraded.

The floor reads two independent signals from the client's own status, either of
which can establish a supported release: the protocol number and the release
core of the version string. Measured against the real release binaries, no build
lacking both upstream focus fixes reaches protocol 19 and every pre-fix build
tops out at 17, so protocol 19 is a safe structural expression of the floor. A
release that reports neither signal readably is treated as unsupported rather
than guessed at.

Also:
- Correct the adapter comment claiming the mitigation "stays safe without any
  version gate". That holds for the pane-death route only; the plain-close
  fallback is reachable precisely on the releases where it is unsafe.
- Stop discarding the projected-close helper's stderr at teardown, so a refused
  or failed focus restore is visible instead of silent. The close stays
  non-fatal; the presence gate still decides record removal.
- Add Part C to the focus-flash regression: a doomed pane whose shell holds a
  persistent child, in the geometry where the closing workspace's right
  neighbour is not the anchor. That is the fallback branch the suite could not
  structurally reach. On 0.7.5 it observes a bounded four-sample wrong-focus
  window restored exactly; on 0.8.0 it observes none. It also cross-checks its
  own measurement against the floor classifier, so a drifted protocol mapping
  fails loudly.
- Make the projection suite's unconfigured-home case release-aware, so the whole
  real-Herdr lane passes on both the CI-pinned 0.7.4 and 0.8.0.
- Add an opt-in live guard that re-measures the release-to-protocol mapping
  against the pinned upstream binaries.

The immediate no-code mitigation for a home that cannot upgrade remains writing
"off" into config/herdr-presentation-spaces.

* no-mistakes(review): Pin Herdr live-guard digests across supported platforms

* no-mistakes(review): Document authorized Herdr cleanup containment

* no-mistakes(review): Harden Herdr warning marker publication

* no-mistakes(review): Honor running Herdr server presentation floor

* no-mistakes(review): Recheck Herdr floor after server ensure

* no-mistakes(review): Refresh 0.7.5 and 0.8.0 focus transcripts

* no-mistakes(review): Route Herdr floor probe through lab session

* no-mistakes(document): Align Herdr floor documentation and comments

* no-mistakes(lint): Document Herdr presentation out-parameter consumer

* fix(bin): classify settled Muse session logs as idle (#1788)

* 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

* docs(agents): read the persisted digest when only a preview is shown (#1794)

* fix: preserve fleet state in truncated session-start digests (#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

* feat(send): close answered decisions at answer time via --resolve-key (#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.

* fix(bin): seed remote secondmates from supplied origins (#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 give…
davidx-secco added a commit to davidx-secco/firstmate that referenced this pull request Aug 18, 2026
…m's (#6)

* fix(bin): bound open decision scans with incremental cursors (#1737)

* fix(bin): bound OPEN DECISIONS scan cost with a per-status-file cursor

The fleet-wide OPEN DECISIONS scan added in #1711 re-reads and refolds
every task's entire lifetime status log on every drain, so its cost
grows unbounded with total log size. Add status_open_decisions_incremental
and scan_open_decisions_incremental to fm-classify-lib.sh: they persist a
per-status-file byte cursor plus the folded open-decision set, and fold
only newly appended bytes on each call, reusing status_open_decisions'
exact fold-line rule (extracted into _fm_decision_fold_line) so the two
strategies can never disagree on what is open. A missing or invalidated
cursor (new task, truncated/rewritten/shrunk log) falls back to a full
re-fold. bin/fm-wake-drain.sh now calls the incremental wrapper instead
of the whole-file scan.

* fix(bin): add O(1) rotation detection and read-failure guarding to the cursor fold

Add the two pieces the incremental open-decisions cursor was missing,
scoped to this repo's actual status-file usage (create-once, append-only,
never replaced or rewritten in place):

- An O(1) device+inode identity check (one stat call) alongside the
  existing size-shrink check, so a status file replaced/rotated/recreated
  at the same path is detected and falls back to a full re-fold, even
  when the replacement is the same size. A same-inode, same-size,
  in-place byte edit is a deliberately accepted gap: no code path in
  this repo ever does that to a status file.
- Checked reads: a stat/wc/tail failure is a genuine I/O error, not
  "the file is empty" - it now reports the already-trusted persisted
  open set unchanged instead of risking a silent invalidation.

Both stay O(1) plus new bytes per call, matching the cursor's bounded-
cost design; no content hashing or pending-fragment machinery.

* no-mistakes(review): Preserve cursor state across failed incremental reads

* no-mistakes(review): Refold status when cursor cache reads fail

* no-mistakes(document): Document cursor-backed open-decision scanning

* no-mistakes: apply CI fixes

* fix(bin): prevent remote polls from blocking session startup (#1754)

* fix(bin): preempt remote reply long-polls for queued short jobs

Session start on a home with live remote second mates could stall silently
for many minutes: the single serial remote job worker ran each armed
fm-remote-delta-read.sh reply poll to its full 55s window while bootstrap's
short sync, inherit, state, and route commands sat queued behind it, and
non-FIFO queue pickup let re-armed polls keep winning the lane. Measured
end to end, a trivial short job took 31s behind one 30s poll window.

The worker now preempts a running preemptible job (the read-only, cursor-
anchored delta read is the only member of that class) as soon as a
non-preemptible job is queued, publishing exit 75 with emptied output -
byte-identical to the poll's own elapsed-window-with-no-data result - so
the parent runner takes its existing no-result path and the watcher re-arms
from the same cursor with nothing lost. The delta read translates SIGTERM
into that same exit after removing its staging directory. Sibling polls
never preempt each other, so two armed monitors cannot churn. The same
measured scenario now completes in 1s.

* no-mistakes(document): Clarify remote poll preemption documentation

* docs: present X mode as the X and Discord public surface (#1778)

Discord mentions already ride the same pairing-token opt-in, relay poll,
and platform-aware reply path as X mentions, but the docs still read as
X-only, so a stranger could not self-serve the Discord path.

Add the numbered turn-on steps to the X mode configuration reference,
pointing at the myfirstmate dashboard for account creation, bot install,
and token issuance rather than duplicating operator setup here, and drop
the X-only framing from the README bullet, the documentation index, and
the architecture overview.

* fix(bin): run session start deterministically from hooks (#1781)

* feat(bin): run session start deterministically on hook-capable harnesses

Session start relied on a native nudge that only asked the agent to run
bin/fm-session-start.sh, and an agent can defer that. Observed 2026-08-01:
an /ahoy-first session followed the recap path and did not take the helm
until a later request forced it.

Claude, Codex, and Pi now RUN the digest in their session-open hook through
the new bin/fm-sessionstart-run.sh, so the full ordered digest is in model
context before the first turn. That wrapper is the single owner of what a
session-open source means: startup and Pi's "new" take the helm, clear and
compact re-emit, resume/reload/fork delegate to the nudge, and an unreadable
source takes the helm because doing that redundantly is idempotent while
skipping it is the bug. Grok and OpenCode keep the nudge as the floor, since
neither can carry hook stdout into a model turn.

Because the hook now blocks session initialization, fm-session-start.sh
bounds itself first. Its steps are not all individually bounded - bootstrap's
gh auth probe, tool version probes, the backlog listing and per-task endpoint
reads are unbounded - so the whole digest runs as one bounded child (default
120s). Whatever it emitted before the bound survives, and the parent adds a
loud STARTUP TRUNCATED banner naming the stage that stalled and every stage
that never ran, still exiting 0.

--reemit skips only the sweeps startup already reconciled. It still re-verifies
lock ownership and still drains queued wakes, which arrived after startup and
are the turn's work. fm-bootstrap.sh gains FM_BOOTSTRAP_LOCKED so a re-emit
keeps repair ownership instead of deferring to a lock holder that is itself.

Also adds bin/fm-timeout-lib.sh as the single owner of bounded execution,
replacing three near-identical copies, and gives the ahoy skill a helm check
so a nudge-tier harness cannot recap before taking the helm.

Verified live on 2026-08-05 against Claude 2.1.222, Codex 0.146.0, and Pi
0.82.0; docs/verification/supervision.md records the per-harness source
vocabulary, the two named gaps, and the refresh command.

* no-mistakes(review): Harden session-start completion, timeout, and Pi delivery

* no-mistakes(review): Harden completion ownership and portable timeout escalation

* no-mistakes(review): Normalize watchdog KILL exits without masking command status

* no-mistakes(review): Guarantee startup bounds and align harness delivery tiers

* no-mistakes(test): Fix Pi session-start live verification fixture

* no-mistakes(document): Align session-start documentation with deterministic hooks

* no-mistakes(lint): Silence intentional child-shell expansion lint warning

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* fix: rename X mode to Relay in user-facing docs (#1784)

* docs: rename the user-facing product name to Relay

The public-mention integration gated by the `.env` pairing token is now
called Relay across user-facing prose, covering X and Discord alike
instead of implying a single network.

Renames the product-name strings only: README, docs, the captain-facing
skill descriptions, and the AGENTS.md operating prose, including the
`X mode (.env)` and `Optional X mode` headings and every link anchor
that pointed at them. AGENTS.md section 14 carries a one-line bridge
note so the older name and the unchanged identifier spellings stay
discoverable.

Internal identifiers are untouched: `FMX_*`, `config/x-mode.env`,
`state/x-*`, `bin/fm-x-*`, the `fmx-respond` skill path,
`__FM_X_MODE_ENV__`, and `x-mode-error`. Platform references to X and
Discord as networks stay as they are, and the bootstrap-diagnostics
entry still quotes bootstrap's emitted `FMX: X mode on/off` line
verbatim because `bin/` output is out of scope for this pass.

* no-mistakes(review): Complete Relay prose rename in maintained docs

* no-mistakes: apply CI fixes

* feat: add Muse Code crewmate adapter (#1786)

* feat(harness): add a verified muse crewmate adapter

Muse Code joins the fleet as a crewmate/scout adapter, verified live against
Muse Code 0.1.0-R708.1 in an isolated lab.

Detection matches the anchored prefix muse-bin*, because the installed launcher
execs a version-suffixed binary whose name changes on every auto-update and
whose install path carries no muse component to fall back on. The same identity
is taught to the tmux liveness classifier, without which a healthy muse pane
would have read as a dead endpoint.

Busy state folds muse's own durable session event log, bound per task by a
sessions-root/worktree sidecar. It is a pull source with no writer, so nothing
is armed and no record is ever seeded. The fold is anchored on the full run
lifecycle prefix so muse's nested cleanup "terminal" payloads cannot settle an
in-flight run, and it is depth-bounded so muse's native sub-agent logs cannot be
mistaken for the parent's. The idle half stays gated: an open run proves busy,
but a settled log reads unknown until a credentialed multi-step run proves one
turn stays inside one run.

Two findings corrected the scout report. The exec-only
--no-foreign-personal-context flag is rejected by the interactive TUI, so the
privacy control that actually reaches a pane worker is
MUSE_EXPERIMENTAL_FOREIGN_PERSONAL_CONTEXT_KILL, verified to drop the operator's
foreign personal rules while keeping the project's own AGENTS.md. And an
unauthenticated muse pane never exits, it waits on a device-code prompt, so
credentials are a spawn preflight rather than a screen check.

muse is refused for secondmates: it has no primary supervision protocol and its
hook dialect rejects the reawakening handlers that protocol needs.

Per the captain's decision, auto-update is not pinned, and the credentialed
multi-step smoke is deferred with an explicit checklist in
docs/verification/muse.md.

* no-mistakes(review): Accept Muse dispatch profiles and shared efforts

* no-mistakes(review): Bind Muse busy state to current session

* no-mistakes(review): Compare Muse workspace bindings literally

* no-mistakes(review): Harden Muse worker credentials and live signal verification

* no-mistakes(review): Cache Muse session bindings and clarify worker credentials

* no-mistakes(review): Clear Muse marker inheritance and normalize interrupt aliases

* no-mistakes(review): Verify Muse glyph effective foreground color

* no-mistakes(review): Harden Muse XDG paths, session cache, and glyph parsing

* no-mistakes(document): Document Muse adapter boundaries

* fix(herdr): require 0.8.0 for default presentation spaces (#1787)

* fix(herdr): floor default-on presentation spaces at Herdr 0.8.0

Default-on presentation projection turns every crewmate teardown into a
workspace-emptying removal. The focus-safe removal plan avoids Herdr's
focus-stealing explicit close only while the doomed pane's shell can be proved
lone, childless, and idle; a persistent child of that shell (gitstatusd, a
zsh-async worker, direnv) fails that proof permanently and forces the plain
close, which on every release before Herdr 0.8.0 moves the captain's active
workspace for ~140ms on each teardown.

Gate the unconfigured default behind a Herdr 0.8.0 floor. At or above it,
project as before; below it, fall back to the flat per-home layout with one
warning per home per detected release naming the version and the upgrade. An
explicit "on" - including the historical empty opt-in file - is still honored
below the floor, so a deliberate opt-in is never silently downgraded.

The floor reads two independent signals from the client's own status, either of
which can establish a supported release: the protocol number and the release
core of the version string. Measured against the real release binaries, no build
lacking both upstream focus fixes reaches protocol 19 and every pre-fix build
tops out at 17, so protocol 19 is a safe structural expression of the floor. A
release that reports neither signal readably is treated as unsupported rather
than guessed at.

Also:
- Correct the adapter comment claiming the mitigation "stays safe without any
  version gate". That holds for the pane-death route only; the plain-close
  fallback is reachable precisely on the releases where it is unsafe.
- Stop discarding the projected-close helper's stderr at teardown, so a refused
  or failed focus restore is visible instead of silent. The close stays
  non-fatal; the presence gate still decides record removal.
- Add Part C to the focus-flash regression: a doomed pane whose shell holds a
  persistent child, in the geometry where the closing workspace's right
  neighbour is not the anchor. That is the fallback branch the suite could not
  structurally reach. On 0.7.5 it observes a bounded four-sample wrong-focus
  window restored exactly; on 0.8.0 it observes none. It also cross-checks its
  own measurement against the floor classifier, so a drifted protocol mapping
  fails loudly.
- Make the projection suite's unconfigured-home case release-aware, so the whole
  real-Herdr lane passes on both the CI-pinned 0.7.4 and 0.8.0.
- Add an opt-in live guard that re-measures the release-to-protocol mapping
  against the pinned upstream binaries.

The immediate no-code mitigation for a home that cannot upgrade remains writing
"off" into config/herdr-presentation-spaces.

* no-mistakes(review): Pin Herdr live-guard digests across supported platforms

* no-mistakes(review): Document authorized Herdr cleanup containment

* no-mistakes(review): Harden Herdr warning marker publication

* no-mistakes(review): Honor running Herdr server presentation floor

* no-mistakes(review): Recheck Herdr floor after server ensure

* no-mistakes(review): Refresh 0.7.5 and 0.8.0 focus transcripts

* no-mistakes(review): Route Herdr floor probe through lab session

* no-mistakes(document): Align Herdr floor documentation and comments

* no-mistakes(lint): Document Herdr presentation out-parameter consumer

* fix(bin): classify settled Muse session logs as idle (#1788)

* 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

* docs(agents): read the persisted digest when only a preview is shown (#1794)

* fix: preserve fleet state in truncated session-start digests (#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

* feat(send): close answered decisions at answer time via --resolve-key (#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.

* fix(bin): seed remote secondmates from supplied origins (#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

* fix(tests): restore reliable fm-send backend parity coverage (#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. #1842 made bin/fm-send.sh source
bin/fm-line-cap-lib.sh (added by #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 remote secondmate status streams (#1846)

* 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

* docs(agents): describe the digest's fleet-state-before-context order (#1826)

* fix(bin): fail closed on NUL bytes in the durable parent binding (#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.

* fix(skills): reconcile inherited secondmate plans with shipped state (#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

* fix: move network checks off the session-start blocking path (#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 #1845, which was cut from a
pre-#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 #1826's
fleet-state-before-context ordering. The old-bin test shim keeps main's
git-archive baseline from #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 remote replies during capture (#1831)

* 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

* feat(skills): port internal stow curation disciplines to the public skill (#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).

* chore(bootstrap): raise lavish-axi version floor to 0.1.46 (#1865)

* fix(bin): keep tracked Claude hook entries inert under grok 1.0.0 (#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

* feat(startup-network): record per-step elapsed times for the deferred stage (#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.

* feat(stow): cascade the internal /stow to every registered secondmate (#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

* fix(remote-job): stop workers abandoned by a pruned code root (#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.

* feat(bin): lint only the changed shard locally, full lint in CI (#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 (#1568)

* 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): add tiered decaying memory management (#1984)

* 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 project vision (#1997)

* 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 (#2005)

* fix(spawn): force regular Pi TUI for crews

* no-mistakes(document): Documented Pi regular TUI launch mode

* fix(cmux): classify borderless Claude composers (#2029)

* fix(cmux): classify borderless Claude composer

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

* no-mistakes(document): Document cmux borderless Claude composer classification

* docs(stow): generalize read-before-write in the public stow skill (#2091)

The public installer-facing stow skill scoped its classify-then-replace
discipline to TODO/BACKLOG items only, so findings routed to a memory file
had no stated rule against a blind append or a wholesale overwrite.

Step 6 now classifies every finding against the destination's current
contents as new, duplicate, superseding, or obsolete, and states the
considered replacement each classification implies. The outcomes follow the
tiered-memory contract already in the file: an obsolete entry is refreshed,
archived, or replaced in a way that preserves its fact, a duplicate folds
into the entry that already carries it, and a superseded body worth keeping
leaves through step 7's existing exits rather than a second recovery
mechanism.

* fix: resurface durable supervision work after re-arm (#2065)

* fix(watcher): resurface durable work after downtime

* no-mistakes(review): Make watcher rearm recovery durable and cursor-safe

* no-mistakes(review): Persist safe recovery markers across migration lock recovery

* no-mistakes(review): Retain stale lock when recovery marker publication fails

* no-mistakes(review): Preserve delivery-gap recovery and quarantine malformed markers

* no-mistakes(review): Serialize recovery consumption and report acknowledgment failures

* no-mistakes(review): Centralize recovery publication before clearing watcher evidence

* no-mistakes(review): Guarantee recovery evidence across queue and lock handoffs

* no-mistakes(review): Publish recovery evidence before durable wake commits

* no-mistakes(review): Replace recovery marker Perl dependency with Node

* no-mistakes(review): Keep interrupted wakes durable until handling acknowledgment

* no-mistakes(review): Add post-handling durable wake acknowledgements

* no-mistakes(review): Enforce post-handling acknowledgement across recovery and AFK return

* no-mistakes(review): Bind wake acknowledgements to recovery generations

* no-mistakes(review): Align wake regressions with generation-bound acknowledgements

* no-mistakes(document): Document durable re-arm recovery semantics

* no-mistakes(lint): Resolve ShellCheck warnings in recovery and watcher tests

* no-mistakes: apply CI fixes

* test(watcher): assert post-handling wake replay

* no-mistakes(review): Prevent successor loops and adopt legacy wake generations

* no-mistakes(review): Rearm durable wakes without recursive successor recovery

* no-mistakes(review): Align recovery tests with handling marker state

* no-mistakes(review): Delay handling transition until successor launch is established

* no-mistakes(review): Confirm wake handling only after successful prompt delivery

* no-mistakes(review): Acknowledge AFK wakes only after evidence publication

* no-mistakes(review): Prevent AFK wake loss before post-handling acknowledgement

* no-mistakes(document): Document durable wake acknowledgement semantics

* no-mistakes(lint): Suppress false positive for recovery action output

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* ci: measure Herdr automation on Windows runners (#2100)

* ci: add Windows Herdr automation spike

* ci: run Windows spike on its pull request

* fix: wait for Windows Herdr command output

* fix: run ANSI probe in pane shell

* ci: keep Windows Herdr spike manually triggered

* docs: clarify Windows Herdr spike verdict

* feat(ahoy): guide captains through open decisions (#2099)

* Add guided ahoy decision flow

* no-mistakes(document): Document guided Ahoy decision flow

* fix(stow): enforce startup-memory budget decisions (#2110)

* Harden stow memory budget policy

* Refine internal stow offload policy

* no-mistakes(review): Enforce shared-budget decisions and autonomous offload

* fix(spawn): refresh pooled worktrees from origin before launch (#2116)

* fix(spawn): refresh pooled worktree base

* no-mistakes(document): Document spawn base-freshness invariant

* no-mistakes: apply CI fixes

* fix(composer): unify safe classification across backends (#2102)

* refactor(composer): one shape owner behind thin capture adapters, whole matrix fixed

Consolidate every composer shape - bordered boxes (all families, geometry,
titled bottom borders), bare agent-glyph rows and their wrap regions,
opencode's left bar, and pi's identity-gated separator pair - into
fm_composer_classify_screen in bin/fm-composer-lib.sh. Adapters now
contribute only a capture and a declarative capability descriptor
(styled/cursor/identity/rows); capability differences change how confidently
a shape is judged, never what the shapes are, so a new harness shape is
teachable in exactly one place.

Correctness fixes landed as part of the consolidation (audit
data/fm-composer-consolidation-audit-s1):
- locale-safe Unicode-space normalization in the shared owner (closes the
  fleet-wide half of #1988; cmux's local byte-exact NBSP case deleted;
  naming converges with PR #1995's normalization primitive)
- muse's bare glyph joins the shared set, unbreaking muse on herdr/cmux/orca
- orca learns the borderless bare shape, drops its backward-paged composer
  window, and can no longer classify a stale startup banner as the composer
- tmux tolerates a titled bottom border, unbreaking grok steering
- the left-bar shape makes opencode readable on every backend
- zellij gets a real classifier through dump-screen --ansi, replacing the
  content-diff submit heuristic that could confirm an undelivered message
  and close a --resolve-key decision (the fleet's only false positive)
- fm-spawn's kimi launch-readiness regex (the fourth shape copy) now routes
  through the shared classifier

The strict blank-row posture applies fleet-wide (captain decision
blank-row-injection-posture): no positive container proof = unknown = defer,
replacing tmux's permissive blank-cursor-row rule. Away-mode injection was
re-validated end to end on real tmux (defer on partial input and unproven
rows, clean delivery with swallowed-Enter retry into proven-empty
composers). The tmux submit core gains a baseline-idle turn-started
conversion so pi steering stays confirmed while its working screen hides
the composer; busy conversion without that baseline remains forbidden.

Plain-capture backends now degrade a glyph row carrying trailing text to
unknown instead of a false pending, per the approved capability rule.

Portable regressions pin the full byte-capture matrix from the audit under
a UTF-8 locale and LC_ALL=C, the strict-vs-permissive divergence, and
deliberate signal separation; the opt-in live guard
(tests/fm-composer-matrix-live-e2e.test.sh) verified every installed
harness against the real classifier, recorded in
docs/verification/runtime-backends.md.

* no-mistakes(review): Fix Pi glyph ambiguity and complete profile matrix

* no-mistakes(review): Preserve bare verdict when Pi identity probe is absent

* no-mistakes(review): Harden composer structure and titled-border geometry

* no-mistakes(review): Require proven idle baseline and strict Zellij guard

* no-mistakes(review): Reject box bottom borders as composer input rows

* no-mistakes(review): Prove Zellij probe typing before classifier retries

* no-mistakes(review): Preserve Pi identity uncertainty and scan full left-bar drafts

* no-mistakes(review): Verify Zellij text lands before submitting

* no-mistakes(review): Scope Zellij typing verification to selected composer content

* no-mistakes(review): Verify Zellij pastes through composer-scoped content deltas

* no-mistakes(review): Prove wrapped bare Zellij pastes through composer extraction

* no-mistakes(review): Invalidate stale cursorless composers below dead shell prompts

* no-mistakes(review): Handle shell prompt placeholders in composer extraction

* no-mistakes(review): Classify cursorless bare continuation regions safely

* no-mistakes(review): Reject stale cursorless containers below live activity

* no-mistakes(review): Preserve prompt glyphs in wrapped Zellij pastes

* no-mistakes(review): Reject live shell rows during composer extraction

* no-mistakes(review): Preserve wrapped glyph continuations through submit retries

* no-mistakes(review): Scope idle placeholders to proven positions

* no-mistakes(review): Restore boxed placeholders and live prompt reanchoring

* no-mistakes(review): Fix Zellij placeholder and wrapped glyph paste proof

* no-mistakes(document): Align composer architecture documentation

* no-mistakes(lint): Fix ShellCheck warnings in composer refactor

* no-mistakes: apply CI fixes

* docs(verification): record the trusted-checkout live matrix rerun

The pipeline's isolated gate worktree is untrusted, so claude, grok, and
muse stopped at first-launch trust dialogs there (the guard refuses to
confirm them by design). This rerun from the trusted checkout at the final
validated head verified all six installed harnesses, the strict blank-row
deferral, and the hardened zellij false-positive probe live.

* no-mistakes(document): Align composer verification evidence

* no-mistakes: apply CI fixes

* no-mistakes(review): Restore proven box bottom-cursor classification

* no-mistakes(review): Preserve styled placeholder-like drafts as pending

* no-mistakes(document): Align composer safety and Zellij delivery documentation

* no-mistakes: apply CI fixes

* docs(verification): refresh the live matrix with the final-head trusted rerun

The post-validation rerun from the trusted checkout verified all six
installed harnesses at the branch's final head, including Claude 2.1.227
(auto-updated since the audit's captures) and Grok, which the untrusted
gate worktree could not verify past their first-launch trust dialogs.

* fix(spawn): gate Pi TUI mode by CLI capability (#2117)

* fix(spawn): gate Pi regular TUI flag by capability

* no-mistakes(review): Document conditional Pi TUI capability detection

* no-mistakes(review): Pin Pi probing and launch to one executable

* no-mistakes(review): Preserve literal pinned Pi paths and update documentation

* no-mistakes(review): Defer pinned P…
lytv pushed a commit to lytv/mymate that referenced this pull request Sep 8, 2026
…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
lytv pushed a commit to lytv/mymate that referenced this pull request Sep 8, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant