Skip to content

fix(afk): support herdr away-mode injection - #251

Merged
kunchenguid merged 5 commits into
mainfrom
fm/afk-herdr-inject-h6
Jul 4, 2026
Merged

kunchenguid merged 5 commits into
mainfrom
fm/afk-herdr-inject-h6

Conversation

@kunchenguid

Copy link
Copy Markdown
Owner

Intent

Make the away-mode supervise-daemon (bin/fm-supervise-daemon.sh) work on the herdr runtime backend, not just tmux. Live failure: on a herdr-based fleet (firstmate itself running with HERDR_ENV=1, no TMUX_PANE), /afk's daemon errored out at startup because supervisor-pane discovery and every injection primitive (busy-guard, composer-guard, verified submit) were tmux-only raw calls.

Scope, deliberately kept to a transport-layer fix (classification policy, batching, max-defer, the FM_INJECT_MARK contract, locks, and wake-queue handling are explicitly NOT touched):

  • Extended supervisor-target/backend discovery: FM_SUPERVISOR_TARGET stays the explicit override (now also accepting a herdr ':' target), and a NEW FM_SUPERVISOR_BACKEND override resolves independently, mirroring bin/fm-backend.sh's own fm_backend_detect precedence (TMUX_PANE wins over HERDR_ENV=1, matching the innermost-first tmux-nested-in-herdr rule; falls back to tmux/firstmate:0 byte-identical to before when nothing is configured or detected).
  • zellij and orca are explicitly out of scope for this daemon and refuse loudly at startup with a clear message (FM_SUPERVISOR_SUPPORTED_BACKENDS) rather than being silently misapplied to tmux primitives - this is a deliberate scope decision, not an oversight.
  • inject_msg's pane-exists probe, busy-guard (pane_is_busy), composer-guard (pane_input_pending), and verified submit now take an optional backend argument (default tmux, so every existing caller/test is unaffected) and dispatch through bin/fm-backend.sh's generic primitives (fm_backend_target_exists, fm_backend_busy_state, fm_backend_capture, fm_backend_send_text_submit) instead of raw tmux calls. Added a new generic fm_backend_composer_state dispatcher to bin/fm-backend.sh (tmux/herdr/orca arms; zellij/unknown fall back to 'unknown') since no such generic dispatcher existed before, mirroring the existing fm_backend_busy_state pattern.
  • For backend=tmux every dispatch resolves to the EXACT same underlying call as before (verified: the full pre-existing test suite, including a real-tmux private-socket e2e test, passes unchanged) - this is intentionally byte-identical, not a behavior change.
  • Found and fixed a PRE-EXISTING, unrelated bug while doing empirical real-herdr verification: fm_backend_target_exists's herdr arm called WITHOUT the required --session flag, which docs/herdr-backend.md already documented as unreliable once more than one herdr server is running (it silently falls back to whichever server IS running). This is a real bug affecting bin/fm-session-start.sh's endpoint-liveness digest too, not something I introduced, but it directly blocked the daemon's own herdr startup validation and I fixed it in passing (route through fm_backend_herdr_cli, which appends --session). This is a deliberate fix, not scope creep - it was empirically reproduced (an isolated test session was misrouted to the ambient default session) before being fixed.
  • Empirical verification against THIS machine's real herdr, in strict isolation (never the live default session or any live pane): read-only discovery check against my own real pane first (confirmed target/backend resolution), then a full new e2e test file tests/fm-afk-inject-herdr-e2e.test.sh (mirroring tests/fm-afk-inject-e2e.test.sh's tmux e2e) on a throwaway, never-default HERDR_SESSION with guarded teardown (herdr_safe_stop_and_delete), covering: partial-input deferral, swallowed-Enter retry (via a thin herdr CLI shim that swallows exactly one Enter, since herdr's real CLI has no built-in way to drop a keystroke), a normal single digest, and a NEW max-defer scenario (a persistently pending composer must alarm via state/.subsuper-inject-wedged, preserve the buffer, and never crash the daemon).
  • Along the way, discovered and documented (docs/herdr-backend.md) that 80 is unreliable when called from within a script launched into a herdr pane (reports a stale default width instead of the pane's real width) - worked around in the test's own composer-drawing script with a hardcoded conservative width; this is a test-harness-only finding, not a change to the herdr adapter itself.
  • Also added an FM_SUPERVISOR_BACKEND=tmux pin to the existing tests/fm-afk-inject-e2e.test.sh's daemon-subprocess launch, because that e2e spawns the real daemon process and, without an explicit pin, would inherit whatever HERDR_ENV the invoking shell happens to carry (e.g. running this validation itself from inside a herdr pane) and misdetect backend=herdr against what is actually a tmux pane on a private socket - this is a necessary correctness fix given the new auto-detection behavior, not incidental to the task.
  • Documentation: docs/herdr-backend.md gets a new 'Away-mode daemon: herdr supervisor-pane support' section; .agents/skills/afk/SKILL.md's tmux-specific mechanics (busy-guard/composer-guard, submit model, auto-discovered supervisor pane) are updated to describe both backends. AGENTS.md's own afk stub was deliberately NOT touched - its inline facts (marker contract, watcher ownership, approval semantics) are backend-independent and did not change.
  • Out of scope, left alone: multi-harness (codex/opencode/pi) pane-mechanics verification, the primary turn-end guard, and any watcher-policy change.

Full local test suite (tests/*.test.sh) and shellcheck both pass clean.

What Changed

  • Captain, made fm-supervise-daemon.sh backend-aware so away-mode injection can discover tmux or Herdr supervisor targets, refuse unsupported supervisor backends, and route target, busy, composer, and submit checks through backend primitives.
  • Fixed Herdr backend helpers used by daemon startup and session liveness by adding generic composer-state dispatch and using session-scoped Herdr target existence checks.
  • Updated AFK/Herdr documentation and added tmux/Herdr daemon coverage for startup, pending input deferral, swallowed-submit retry, and wedged max-defer behavior.

Risk Assessment

⚠️ Medium: Captain, the review found no material defects, but the change touches the daemon's live injection transport and backend discovery path, so residual integration risk is moderate.

Testing

Captain, the already-successful baseline was accounted for; I reran focused daemon, herdr backend, tmux E2E, and real-herdr E2E coverage, then performed a preserved manual real-herdr run showing pending-input deferral, sentinel-prefixed injection after idle, max-defer wedge alarm with buffer preservation, and loud unsupported-backend refusal. No tests failed, no source files were modified, and no UI surface changed, so no screenshot was applicable.

Evidence: Manual herdr observed behavior
Manual herdr AFK evidence session: fm-afk-herdr-evidence-54136
Supervisor target: fm-afk-herdr-evidence-54136:w1:p2

[Scenario A raw submitted lines]
68756d616e2064726166742074657874	human draft text	user
1f53757065727669736f7220657363616c61746520282020202020202031206576656e74287329293a2066616b652d63312e7374617475733a20646f6e653a2050522068747470733a2f2f6578616d706c652e746573742f70722f6d616e75616c2d61202863617463682d616c6c207363616e2920287072652d726561643b2072652d61726d206e6f74206e656564656420e280942077617463686572206461656d6f6e2d6d616e6167656429	�Supervisor escalate (       1 event(s)): fake-c1.status: done: PR https://example.test/pr/manual-a (catch-all scan) (pre-read; re-arm not needed — watcher daemon-managed)	injection

[Scenario C raw submitted lines]
1f53757065727669736f7220657363616c61746520282020202020202031206576656e74287329293a2066616b652d63312e7374617475733a20646f6e653a2050522068747470733a2f2f6578616d706c652e746573742f70722f6d616e75616c2d63202863617463682d616c6c207363616e2920287072652d726561643b2072652d61726d206e6f74206e656564656420e280942077617463686572206461656d6f6e2d6d616e6167656429	�Supervisor escalate (       1 event(s)): fake-c1.status: done: PR https://example.test/pr/manual-c (catch-all scan) (pre-read; re-arm not needed — watcher daemon-managed)	injection

[Scenario D wedge marker]
fm away-mode inject WEDGED: 11s undelivered as of 2026-07-04T12:52:24-0700
The supervisor pane could not accept an escalation. Buffered items:
fake-c1.status: needs-decision: pick A or B (catch-all scan)

[Scenario D buffered escalations]
fake-c1.status: needs-decision: pick A or B (catch-all scan)
Manual evidence passed
Evidence: Manual herdr submitted lines, readable
line 1: hex_prefix=68756d616e206472 classification=user text=human draft text
line 2: hex_prefix=1f53757065727669 classification=injection text=�Supervisor escalate (       1 event(s)): fake-c1.status: done: PR https://example.test/pr/manual-a (catch-all scan) (pre-read; re-arm not needed — watcher daemon-managed)
line 3: hex_prefix=1f53757065727669 classification=injection text=�Supervisor escalate (       1 event(s)): fake-c1.status: done: PR https://example.test/pr/manual-c (catch-all scan) (pre-read; re-arm not needed — watcher daemon-managed)
Evidence: Manual herdr daemon log
[2026-07-04T12:51:43-0700] daemon starting (pid 54511); target=fm-afk-herdr-evidence-54136:w1:p2; target_source=FM_SUPERVISOR_TARGET; backend=herdr; backend_source=FM_SUPERVISOR_BACKEND; afk=on; inject_skip='heartbeat'; stale_escalate=999999s; batch=0s
[2026-07-04T12:51:45-0700] inject deferred: supervisor pane has pending input (non-empty composer)
[2026-07-04T12:51:47-0700] inject deferred: supervisor pane has pending input (non-empty composer)
[2026-07-04T12:51:47-0700] wake: signal: /var/folders/5x/4nqprlbx0518k3ybcb1sz6gr0000gn/T/no-mistakes-evidence/01KWQ882JBBTJ3F3S9J5SJ75VV/manual-herdr-state/fake-c1.status
[2026-07-04T12:51:47-0700] self-handle: signal: /var/folders/5x/4nqprlbx0518k3ybcb1sz6gr0000gn/T/no-mistakes-evidence/01KWQ882JBBTJ3F3S9J5SJ75VV/manual-herdr-state/fake-c1.status -> signal already escalated (catch-all scan): fake-c1.status: done: PR https://example.test/pr/manual-a
[2026-07-04T12:51:48-0700] inject deferred: supervisor pane has pending input (non-empty composer)
[2026-07-04T12:51:49-0700] inject deferred: supervisor pane has pending input (non-empty composer)
[2026-07-04T12:51:50-0700] inject deferred: supervisor pane has pending input (non-empty composer)
[2026-07-04T12:51:51-0700] inject deferred: supervisor pane has pending input (non-empty composer)
[2026-07-04T12:52:00-0700] daemon shutting down
[2026-07-04T12:52:01-0700] daemon starting (pid 57322); target=fm-afk-herdr-evidence-54136:w1:p2; target_source=FM_SUPERVISOR_TARGET; backend=herdr; backend_source=FM_SUPERVISOR_BACKEND; afk=on; inject_skip='heartbeat'; stale_escalate=999999s; batch=0s
[2026-07-04T12:52:05-0700] wake: signal: /var/folders/5x/4nqprlbx0518k3ybcb1sz6gr0000gn/T/no-mistakes-evidence/01KWQ882JBBTJ3F3S9J5SJ75VV/manual-herdr-state/fake-c1.status
[2026-07-04T12:52:05-0700] self-handle: signal: /var/folders/5x/4nqprlbx0518k3ybcb1sz6gr0000gn/T/no-mistakes-evidence/01KWQ882JBBTJ3F3S9J5SJ75VV/manual-herdr-state/fake-c1.status -> signal already escalated (catch-all scan): fake-c1.status: done: PR https://example.test/pr/manual-c
[2026-07-04T12:52:10-0700] daemon shutting down
[2026-07-04T12:52:12-0700] daemon starting (pid 58656); target=fm-afk-herdr-evidence-54136:w1:p2; target_source=FM_SUPERVISOR_TARGET; backend=herdr; backend_source=FM_SUPERVISOR_BACKEND; afk=on; inject_skip='heartbeat'; stale_escalate=999999s; batch=99999s
[2026-07-04T12:52:15-0700] wake: signal: /var/folders/5x/4nqprlbx0518k3ybcb1sz6gr0000gn/T/no-mistakes-evidence/01KWQ882JBBTJ3F3S9J5SJ75VV/manual-herdr-state/fake-c1.status
[2026-07-04T12:52:15-0700] self-handle: signal: /var/folders/5x/4nqprlbx0518k3ybcb1sz6gr0000gn/T/no-mistakes-evidence/01KWQ882JBBTJ3F3S9J5SJ75VV/manual-herdr-state/fake-c1.status -> signal already escalated (catch-all scan): fake-c1.status: needs-decision: pick A or B
[2026-07-04T12:52:17-0700] inject deferred: supervisor pane has pending input (non-empty composer)
[2026-07-04T12:52:17-0700] ERROR: away-mode escalation undelivered 4s; inject could not confirm a submit (supervisor pane busy or wedged). Buffer + wake-queue preserved; alarm marker written.
[2026-07-04T12:52:20-0700] inject deferred: supervisor pane has pending input (non-empty composer)
[2026-07-04T12:52:20-0700] ERROR: away-mode escalation undelivered 7s; inject could not confirm a submit (supervisor pane busy or wedged). Buffer + wake-queue preserved; alarm marker written.
[2026-07-04T12:52:24-0700] inject deferred: supervisor pane has pending input (non-empty composer)
[2026-07-04T12:52:24-0700] ERROR: away-mode escalation undelivered 11s; inject could not confirm a submit (supervisor pane busy or wedged). Buffer + wake-queue preserved; alarm marker written.
[2026-07-04T12:52:25-0700] inject deferred: afk inactive
[2026-07-04T12:52:25-0700] daemon shutting down
Evidence: Manual herdr wedge marker
fm away-mode inject WEDGED: 11s undelivered as of 2026-07-04T12:52:24-0700
The supervisor pane could not accept an escalation. Buffered items:
fake-c1.status: needs-decision: pick A or B (catch-all scan)
Evidence: Real herdr AFK injection E2E transcript
ok - real herdr Scenario A: partial input defers injection; digest arrives clean after idle
ok - real herdr Scenario B: swallowed Enter (via the herdr shim) produces exactly one clean digest
ok - real herdr Scenario C: a normal captain status injects exactly one clean single-line sentinel digest
ok - real herdr Scenario D: a persistently pending composer raises the max-defer wedge alarm, preserves the buffer, and never crashes the daemon
all real-herdr afk injection e2e tests passed
Evidence: Tmux AFK injection regression E2E transcript
ok - Scenario A: partial input defers injection; digest arrives clean after idle
ok - Scenario B: swallowed Enter produces exactly one clean digest
ok - Scenario C: a normal captain status injects exactly one clean single-line sentinel digest
all e2e injection tests passed
Evidence: Daemon unit test transcript
ok - supervise daemon state root is scoped by FM_HOME
ok - routine signal self-handles
ok - captain-relevant status verbs escalate
ok - check + unknown escalate; heartbeat self-handles
ok - transient stale self-handles and records a persistence marker
ok - stale + terminal status escalates immediately
ok - persistent stale escalates after threshold and clears its marker
ok - resumed (busy) stale clears its marker without escalating
ok - persistent herdr stale resolves the target from metadata and escalates
ok - herdr idle busy-footer stale clears through capture corroboration
ok - resumed herdr stale clears through backend-aware busy state
ok - persistent Orca stale resolves the terminal from metadata
ok - multiple escalations flush as a single batched digest
ok - batch flush measures max-delay from the first append, not the last
ok - catch-all scan escalates a missed terminal once, not twice
ok - handle_wake routes routine->self and captain->escalate
ok - INJECT_SKIP forces self-handle, bypassing captain-relevant classification
ok - is_wake_reason distinguishes watcher wake reasons from singleton-status stdout
ok - terminal-stale escalate removes its marker so housekeeping does not re-escalate
ok - captain signal escalate marks seen so the catch-all scan does not re-fire
ok - _collapse_newlines replaces newlines with literal separator
ok - afk flag absent: daemon does not inject, buffer preserved
ok - busy-guard defers injection when supervisor pane is busy
ok - marker detection: marker -> stay afk, no marker -> exit afk
ok - /afk invocation is exempt from afk exit (no self-cancel)
ok - should_exit_afk returns false when afk is not active
ok - strip_injection_marker removes the sentinel marker cleanly
ok - pane_input_pending detects partial input on the cursor line
ok - pane_input_pending: blank cursor line is not pending
ok - pane_input_pending: bare prompts are not pending (idle)
ok - pane_input_pending honors FM_COMPOSER_IDLE_RE after border stripping
ok - classify_signal dedupes against the catch-all scan seen marker
ok - classify_stale dedupes against the signal path seen marker
ok - pane_input_pending: an idle bordered composer is NOT pending (afk-invx-i5)
ok - pane_input_pending: text inside a bordered composer is still pending
ok - submit-ACK confirms a submit when the composer returns to a bordered-empty box
ok - submit-ACK reports pending on a persistently swallowed Enter (type-once)
ok - max-defer on an empty stuck pane types once, alarms, and preserves the buffer
ok - max-defer flushes and clears the buffer on an empty bordered pane
ok - max-defer on a pending composer alarms without typing
ok - normal flush clears a stale wedge marker
ok - below MAX_DEFER: no inject, no alarm, buffer preserved
ok - max-defer does not flush or alarm while afk is inactive
ok - fm-send exits non-zero on a confirmed swallow, zero on a clean submit
ok - fm-send exits non-zero when initial text send fails
ok - discover_supervisor_backend: override > TMUX_PANE > HERDR_ENV+HERDR_PANE_ID > tmux fallback
ok - discover_supervisor_target: override > TMUX_PANE > herdr '<session>:<pane-id>' composition > firstmate:0 fallback
ok - pane_is_busy: herdr native busy_state='busy' short-circuits without a capture fallback
ok - pane_is_busy: herdr falls back to the shared regex-over-capture reader when native busy_state is unknown
ok - pane_is_busy: herdr corroborates native idle with the shared regex-over-capture reader
ok - pane_is_busy: omitted backend arg defaults to tmux (pre-existing callers unaffected)
ok - pane_input_pending: dispatches through fm_backend_composer_state for backend=herdr
ok - inject_msg: herdr busy-guard defers before ever attempting a submit
ok - inject_msg: herdr composer-guard defers before ever attempting a submit
ok - inject_msg: herdr pane-gone check defers before any busy/composer/submit call
ok - inject_msg: dispatches busy-guard/composer-guard/submit through the herdr backend and succeeds on a confirmed empty composer
Evidence: Herdr backend unit test transcript
ok - fm_backend_herdr_version_check: accepts the current protocol (14)
ok - fm_backend_herdr_version_check: refuses an old protocol loudly
ok - fm_backend_herdr_version_check: refuses loudly when herdr is not installed
ok - fm_backend_herdr_workspace_label: a primary home (no marker) resolves to 'firstmate'
ok - fm_backend_herdr_workspace_label: a secondmate home (.fm-secondmate-home) resolves to '2ndmate-<id>'
ok - fm_backend_herdr_workspace_label: trims whitespace around the marker's secondmate id
ok - fm_backend_herdr_workspace_label: an empty marker file falls back to the primary label 'firstmate'
ok - fm_backend_herdr_workspace_label: two different secondmate homes get two different, non-colliding labels
ok - fm_backend_herdr_cli: sets HERDR_SESSION AND appends a trailing --session flag on every call
ok - fm_backend_herdr_container_ensure: version-gates, starts the server, ensures the firstmate workspace, echoes session:workspace_id + the seeded default tab id
ok - fm_backend_herdr_container_ensure: reuses an existing firstmate workspace without recreating it, and reports no seeded default tab (adopted, not created)
ok - fm_backend_herdr_container_ensure: workspace create passes --no-focus
ok - fm_backend_herdr_container_ensure: creates the workspace under the SECONDMATE home's own label, not 'firstmate'
ok - fm_backend_herdr_create_task: prunes exactly the seeded default tab container_ensure identified, once the first real task tab exists
ok - herdr repeated spawn/teardown: one persistent firstmate workspace reused, zero orphans, default tab pruned, create ran once
ok - fm_backend_herdr_create_task: an ADOPTED workspace's pre-existing tab is never pruned (the created-vs-adopted gate)
ok - fm_backend_herdr_create_task: the label-collision startup-workspace scenario (2026-07-02 incident) leaves the captain's live tab untouched
ok - fm_backend_herdr_workspace_prune_seeded_default_tab: refuses to close the seeded default tab when its pane reports a working agent (defense in depth)
ok - no bin/ jq filter names a --arg/--argjson variable after a jq reserved keyword
ok - fm_backend_herdr_create_task: refuses a duplicate tab label (herdr's own tab create has no uniqueness check)
ok - fm_backend_herdr_create_task: a same-labeled tab with a live (even idle) registered agent still refuses exactly as before
ok - fm_backend_herdr_create_task: scans every same-labeled tab and refuses if any duplicate is live
ok - fm_backend_herdr_create_task: closes and replaces a same-labeled tab whose pane is dead (pane_not_found)
ok - fm_backend_herdr_create_task: closes and replaces a same-labeled tab whose pane is alive but hosts no registered agent (a restored plain shell)
ok - fm_backend_herdr_create_task: closes every confirmed same-labeled husk only after creating the replacement
ok - fm_backend_herdr_create_task: refuses success when a preexisting husk tab remains after replacement
ok - fm_backend_herdr_create_task: refuses (fail-safe) rather than guessing when the duplicate's agent state cannot be classified confidently
ok - fm_backend_herdr_create_task: creates the replacement tab BEFORE closing the husk tab, never the reverse
ok - fm_backend_herdr_create_task: creates a tab and parses tab_id/pane_id from the JSON response, prunes nothing when no seeded tab id is given
ok - fm_backend_herdr_create_task: tab create passes --no-focus
ok - fm_backend_herdr_workspace_find: matches only THIS home's own label among several coexisting workspaces
ok - fm_backend_herdr_list_live: scoped to this home's own workspace, never a sibling home's
ok - fm_backend_herdr_parse_target: splits '<session>:<pane_id>' on the FIRST colon (pane_id itself contains one)
ok - fm_backend_herdr_normalize_key: Enter/Escape/C-c map to herdr's verified enter/escape/ctrl+c
ok - fm_backend_herdr_capture: calls 'pane read <pane> --source recent --lines N' with the session set
ok - fm_backend_herdr_capture: works around the verified small-N '--lines' bug by over-fetching and trimming locally
ok - fm_backend_herdr_capture: ensures the session and preserves pane read failure
ok - fm_backend_herdr_send_key: normalizes the key and targets the right pane
ok - fm_backend_herdr_kill: calls pane close and stays best-effort on failure
ok - fm_backend_herdr_current_path: reads pane foreground_cwd (the live running process), not the frozen creation-time cwd
ok - fm_backend_herdr_busy_state: working -> busy
ok - fm_backend_herdr_busy_state: done -> idle, blocked -> idle (surfaced like a stale pane, not suppressed as busy)
ok - fm_backend_herdr_busy_state: unparseable/absent agent state reports unknown, the regex-fallback cue
ok - fm_backend_herdr_composer_state: a bare '❯' composer row reads empty
ok - fm_backend_herdr_composer_state: the ghost placeholder text reads empty, not pending
ok - fm_backend_herdr_composer_state: real composer text reads pending
ok - fm_backend_herdr_composer_state: a slash-command popup's argument-hint placeholder still reads pending (the incident fix)
ok - fm_backend_herdr_composer_state: reports unknown when the pane cannot be captured
ok - fm_backend_herdr_composer_state: reports unknown when no border-delimited composer row is found
ok - fm_backend_herdr_send_text_submit: reports 'empty' once the composer row reads empty after one Enter
ok - fm_backend_herdr_send_text_submit: reports 'pending' when the composer never clears after retried Enters (swallowed)
ok - fm_backend_herdr_send_text_submit: a slash-command popup's placeholder fill on Enter #1 does not short-circuit as submitted; Enter #2 is retried and lands it
ok - fm_backend_herdr_send_text_submit: reports 'send-failed' when the literal send-text call itself errors
ok - fm_backend_herdr_send_text_submit: reports 'unknown' when the post-Enter capture fails (never retries past an unreadable pane)
ok - fm_backend_validate: herdr is a known backend (P2)
ok - fm_backend_busy_state: tmux (no native primitive) always reports unknown, preserving the P1 regex-only path
error: unknown backend 'bogus' (known: tmux herdr zellij orca cmux)
ok - fm_backend_composer_state dispatches tmux/herdr/orca to their named classifiers, unknown for zellij/unrecognized backends
ok - fm-peek/fm-send: explicit stale targets matching metadata use the recorded backend
Evidence: Unsupported backend refusal transcript
error: away-mode daemon does not support supervisor backend 'zellij' yet (supported: tmux herdr); set FM_SUPERVISOR_BACKEND=tmux|herdr and FM_SUPERVISOR_TARGET to run firstmate's own pane under a supported backend
exit_code=1

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed (3) ✅
  • 🚨 bin/fm-supervise-daemon.sh:495 - The Herdr busy guard trusts a native idle verdict as not busy and skips the regex capture fallback. That conflicts with the existing crew_pane_is_busy invariant: Herdr can report idle while the pane is still blocked in a foreground tool call and rendering the busy footer, so the AFK daemon can inject into firstmate's active turn. Treat only busy as conclusive and let both idle and unknown fall through to the capture/FM_BUSY_REGEX check.

🔧 Fix: Corroborate Herdr idle busy state
1 warning still open:

  • ⚠️ tests/fm-afk-inject-herdr-e2e.test.sh:214 - The test waits only for .supervise-daemon.pid, but the daemon writes that pidfile before backend discovery, target validation, and the startup log line. The immediate grep can race on slower Herdr/CI startup and fail even though the daemon would log backend=herdr moments later; fold this assertion into the wait loop and also fail early if the daemon exits.

🔧 Fix: Stabilize Herdr daemon startup wait
2 issues (1 warning, 1 info) still open:

  • ⚠️ bin/fm-backend.sh:440 - The new generic composer dispatcher never routes cmux, even though bin/backends/cmux.sh already has fm_backend_cmux_composer_state; any caller using this shared API for cmux will get unknown and skip pending-input protection.
  • ℹ️ .agents/skills/afk/SKILL.md:87 - This still documents the Herdr busy fallback as unknown-only, but the fixed daemon now corroborates every non-busy verdict, including idle; update this and the matching docs/herdr-backend.md wording so future AFK work does not reintroduce the unsafe idle shortcut.

🔧 Fix: Captain, route cmux composer and update AFK docs
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"
  • Pre-existing baseline reported successful before this validation: command -v tmux &gt;/dev/null || { echo &#34;tmux is required for e2e tests&#34; &gt;&amp;2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo &#34;== $t ==&#34;; bash &#34;$t&#34; || rc=1; done; exit &#34;$rc&#34;
  • command -v herdr
  • command -v jq
  • command -v tmux
  • bash tests/fm-daemon.test.sh > /var/folders/5x/4nqprlbx0518k3ybcb1sz6gr0000gn/T/no-mistakes-evidence/01KWQ882JBBTJ3F3S9J5SJ75VV/fm-daemon-test.log 2>&1
  • bash tests/fm-backend-herdr.test.sh > /var/folders/5x/4nqprlbx0518k3ybcb1sz6gr0000gn/T/no-mistakes-evidence/01KWQ882JBBTJ3F3S9J5SJ75VV/fm-backend-herdr-test.log 2>&1
  • bash tests/fm-afk-inject-e2e.test.sh > /var/folders/5x/4nqprlbx0518k3ybcb1sz6gr0000gn/T/no-mistakes-evidence/01KWQ882JBBTJ3F3S9J5SJ75VV/fm-afk-inject-tmux-e2e.log 2>&1
  • bash tests/fm-afk-inject-herdr-e2e.test.sh > /var/folders/5x/4nqprlbx0518k3ybcb1sz6gr0000gn/T/no-mistakes-evidence/01KWQ882JBBTJ3F3S9J5SJ75VV/fm-afk-inject-herdr-e2e.log 2>&1
  • /var/folders/5x/4nqprlbx0518k3ybcb1sz6gr0000gn/T/no-mistakes-evidence/01KWQ882JBBTJ3F3S9J5SJ75VV/manual-herdr-afk-evidence.sh > /var/folders/5x/4nqprlbx0518k3ybcb1sz6gr0000gn/T/no-mistakes-evidence/01KWQ882JBBTJ3F3S9J5SJ75VV/manual-herdr-afk-evidence.log 2>&1
  • FM_STATE_OVERRIDE=/var/folders/5x/4nqprlbx0518k3ybcb1sz6gr0000gn/T/no-mistakes-evidence/01KWQ882JBBTJ3F3S9J5SJ75VV/unsupported-backend-state FM_SUPERVISOR_BACKEND=zellij bash bin/fm-supervise-daemon.sh captured to unsupported-backend-refusal.log
  • git status --short
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

bin/fm-supervise-daemon.sh discovered its supervisor pane and injected
via raw tmux calls only, so /afk failed outright on a herdr-based
fleet (TMUX_PANE unset, firstmate:0 fallback unresolvable).

Discovery now resolves backend (tmux|herdr) and target independently,
mirroring fm-backend.sh's own runtime auto-detection, with an explicit
FM_SUPERVISOR_BACKEND override alongside the existing FM_SUPERVISOR_TARGET.
zellij/orca refuse loudly at startup instead of misapplying tmux
primitives. Injection (pane-exists probe, busy-guard, composer-guard,
verified submit) now dispatches through bin/fm-backend.sh's generic
primitives, adding a new fm_backend_composer_state dispatcher; the
tmux path is byte-identical to before. Also fixes a pre-existing bug
in fm_backend_target_exists's herdr arm (missing --session, so it
silently misrouted once more than one herdr server was running) found
while verifying this end to end against a real isolated herdr session.

Classification, batching, max-defer, the marker contract, locks, and
wake-queue handling are unchanged - this is a transport-layer fix.
@kunchenguid
kunchenguid merged commit 74a6973 into main Jul 4, 2026
4 checks passed
@kunchenguid
kunchenguid deleted the fm/afk-herdr-inject-h6 branch July 4, 2026 20:08
mielyemitchell added a commit to mielyemitchell/firstmate that referenced this pull request Jul 6, 2026
* feat(dispatch): add dynamic crew profiles (#154)

* feat(dispatch): add dynamic crew profiles

* no-mistakes(review): Captain, document dispatch profile inheritance

* no-mistakes(review): Captain, guard stale dispatch inheritance

* no-mistakes(document): Sync dispatch profile docs

* no-mistakes: apply CI fixes

* fix: harden crew dispatch profile enforcement (#159)

* Harden crew dispatch profile enforcement

* no-mistakes(document): Captain, synced crew dispatch docs

* feat: add live secondmate config push (#161)

* feat(config): add live secondmate config push

* no-mistakes(document): Document config push behavior

* no-mistakes(lint): Clean changed shell lint

* no-mistakes: apply CI fixes

* feat: support image attachments in X replies (#162)

* feat(x): add image attachments to reply helpers

* no-mistakes(review): Stream X image replies safely

* no-mistakes(review): Captain, clean X reply temp tracking

* no-mistakes(document): Document X reply image support

* fix(teardown): make landed PR detection robust (#167)

* fix(teardown): make landed-check robust when no pr= was ever recorded

fm-teardown.sh's squash-merge landed-check already falls back to
discovering a merged PR by branch name when state/<id>.meta has no
recorded pr=, but nothing guaranteed pr=/pr_head= actually got
recorded on a yolo-authorized merge - the "checks green" trigger that
normally runs fm-pr-check.sh never fires on repos with no PR CI, so a
merge done via a bare `gh-axi pr merge` silently skips it.

Add bin/fm-pr-merge.sh as the one path for merging a task's PR: it
always runs fm-pr-check.sh first, so pr=/pr_head= land in meta as part
of the merge itself regardless of any CI signal. Document both the
existing branch-name discovery fallback and the new merge path in
AGENTS.md, and add regression coverage for the no-pr=-recorded landed
scenario and for fm-pr-merge.sh's record-then-merge behavior.

* no-mistakes(review): Guard PR merges on task metadata

* no-mistakes(document): Document PR merge wrapper

* no-mistakes: apply CI fixes

* fix: parse PR merge URLs for gh-axi (#168)

* Fix fm-pr-merge.sh to parse PR URLs for gh-axi

gh-axi pr merge expects a PR number and --repo, not a full GitHub URL.
Parse the URL, default to --squash when no merge method is passed, and
fail fast on malformed URLs. Tests cover parsing, defaults, and refusal.

* no-mistakes(review): Harden PR merge validation

* no-mistakes(review): Harden PR merge URL guards

* no-mistakes(document): Document PR merge URL handling

* no-mistakes(lint): Clean shell lint

* feat(bin): pin secondmate model and effort (#180)

* feat: pin secondmate model/effort in config/secondmate-harness

Extend config/secondmate-harness's format to an optional
"<harness> [<model>] [<effort>]" line so a secondmate can be durably
locked to a concrete model/effort in the same file, without adding a
new config file. A bare harness-only file behaves exactly as before.

fm-harness.sh gains secondmate-model/secondmate-effort accessors;
fm-spawn.sh populates MODEL/EFFORT from them on every secondmate spawn
(including respawns) unless the caller passed an explicit --model/--effort.

* no-mistakes(review): Fix secondmate override pin precedence

* no-mistakes(document): Document secondmate harness pins

* feat(bin): add runtime backend interface (#183)

* feat(bin): extract tmux runtime behind a backend interface (P1)

Add bin/fm-backend.sh (selection, meta helpers, selector resolution,
dispatch) and bin/backends/tmux.sh (the tmux adapter), then route
fm-send.sh, fm-peek.sh, fm-watch.sh, fm-spawn.sh, and fm-teardown.sh
through them. Every default tmux command sequence, meta shape, and
printed output stays byte-identical: missing backend= still means
tmux, and a default spawn never writes backend=tmux.

Adds a --backend flag (tmux-only for now) and FM_BACKEND/config/backend
selection, refusing any unimplemented backend loudly. Names the
watcher's poll loop as the default event-source implementation over the
backend's pull primitives, per the herdr-addendum's events-as-the-core-
abstraction direction, without changing its behavior.

Verification: fake-tmux/treehouse old-vs-new command-log conformance
tests for send/peek/spawn/teardown, a real-tmux smoke test for the
adapter, and the full existing suite passing unmodified (bar two
fixture-only additions in fm-gotmp.test.sh for the new sibling
scripts).

* no-mistakes(review): Captain, harden backend baseline resolution

* no-mistakes(review): Captain, ignore and document backend config

* no-mistakes(review): Captain, make backend tests executable

* no-mistakes(document): Sync runtime backend documentation

* feat(bin): add experimental Herdr runtime backend (#186)

* feat(bin): add experimental herdr runtime backend (P2)

Implements bin/backends/herdr.sh (session-provider adapter, D3: treehouse
stays the worktree provider) wired through fm-backend.sh's dispatch, with
--backend herdr / FM_BACKEND=herdr / config/backend selection, a
version/protocol gate at spawn, semantic busy-state detection via herdr's
agent.get (fm-watch.sh and fm-crew-state.sh consult it before falling back to
the existing tmux pane-regex path), and label-based recovery discovery.

Container shape (D4) decided empirically: tab-per-task in one "firstmate"
workspace, mirroring tmux's one-session-many-windows model.

Found and fixed two real herdr v0.7.1 bugs during verification: `pane read
--lines N` returns empty for small N (worked around by over-fetching and
trimming locally), and `pane get`'s cwd field is frozen at pane-creation time
(fixed to read foreground_cwd instead, needed for fm-spawn's worktree-
discovery poll after `treehouse get`). Also fixed a pre-existing bug in
tests/fm-backend.test.sh's old-vs-new fixture that was silently missing
fm-backend.sh/bin/backends/ from the old bin/ shim.

Full empirical verification, the D4 decision evidence, and a real end-to-end
run (spawn/steer/peek/done/merge-local/teardown, including confirming
teardown refuses before the merge) are recorded in docs/herdr-backend.md.
The entire existing tmux conformance suite stays green.

* no-mistakes(review): Fix Herdr supervision recovery gaps

* no-mistakes(review): Captain, fix Herdr stale recovery gaps

* no-mistakes(review): Document Herdr composer primitive candidate

* no-mistakes(review): Captain, harden Herdr stale recovery and tests

* no-mistakes(document): Sync herdr backend docs

* no-mistakes: apply CI fixes

* feat(bin): auto-detect runtime backend (#188)

* feat(bin): auto-detect runtime backend from HERDR_ENV/TMUX markers

fm_backend_name now falls through to runtime auto-detection between
config/backend and the hard tmux default: a firstmate running natively
inside herdr (HERDR_ENV=1) now spawns crewmates into herdr by default,
mirroring how harness detection already works in fm-harness.sh. Nesting
resolves innermost-first (tmux wins over a nested herdr pane). Explicit
--backend/FM_BACKEND/config/backend settings always win over detection.
Selecting herdr via auto-detect prints a loud stderr notice; auto-detecting
tmux stays silent so the unconfigured default path is unchanged.

* no-mistakes(review): Captain, pin tmux tests and backend docs

* no-mistakes(document): Sync backend autodetect docs

* no-mistakes: apply CI fixes

* feat(stow): add operational memory capture (#197)

* feat(stow): add operational-memory learnings convention and /stow skill

Add data/learnings.md as the fleet-local operational-learnings home,
a knowledge-routing table in AGENTS.md, and a user-invocable /stow
skill that sweeps a session for uncaptured durable knowledge and
files it to the right disk home before a reset.

* no-mistakes(review): Fix stow backlog note command

* no-mistakes(document): Document stow memory routing

* fix(tests): protect herdr smoke cleanup from default sessions (#199)

* fix(tests): stop real-herdr smoke tests from ever killing the default session

Both fm-backend-herdr-smoke.test.sh and fm-backend-autodetect-smoke.test.sh
tore down their isolated throwaway HERDR_SESSION via a bare/inline-prefixed
`herdr server stop`, which is unscoped and resolves ambiently. On this herdr
client, that ambient resolution silently falls back to whatever server is
already running instead of the requested session - it killed the captain's
live default herdr server twice in production (2026-07-02), once from each
smoke test's cleanup trap.

Add tests/herdr-test-safety.sh with herdr_safe_stop_and_delete: it uses the
explicit-by-name `herdr session stop/delete <name>` form (never the ambient
`server stop`) and, before that, a read-only hard guard
(herdr_refuse_if_default) that re-queries `herdr session list --json` and
refuses outright if the target is literally "default", not found, or flagged
default:true. Fails closed on any ambiguity. Verified empirically against a
real isolated session: refuses on default/nonexistent/empty names without
ever calling stop, and correctly tears down a genuine isolated session while
leaving the default session's workspace state byte-identical before and
after.

* no-mistakes(review): guard herdr delete with fresh check

* no-mistakes(document): Document herdr smoke cleanup safety

* feat(bin): route herdr secondmates into per-home workspaces (#200)

* feat(bin): give each secondmate its own labeled herdr workspace

Give each secondmate its own labeled herdr workspace, and land crewmates
spawned from a secondmate home in that secondmate's own space, instead of
every firstmate home (primary and all secondmates) sharing one "firstmate"
workspace.

bin/backends/herdr.sh: replace the constant FM_BACKEND_HERDR_WORKSPACE_LABEL
with fm_backend_herdr_workspace_label(), resolved fresh from FM_HOME on every
call. The primary (no .fm-secondmate-home marker) still resolves to
"firstmate" - byte-identical to every pre-existing task's recorded label, no
forced migration. A secondmate home resolves to "firstmate-<secondmate-id>".
Every workspace-scoped path (find/ensure, tab create + duplicate check,
list-live recovery, pane-for-tab) uses this same resolution, so recovery and
duplicate checks stay scoped to each home's own space. Workspace and tab
create now pass --no-focus unconditionally (verified: neither focuses by
default once a workspace exists; --no-focus is defense in depth against the
one bootstrap edge case where the very first workspace in a session auto-
focuses).

Also fixes a session-targeting bug found while verifying this empirically:
HERDR_SESSION (env var, exported or inline-prefixed) is not reliably honored
by herdr 0.7.1 CLI subcommands once another herdr server is already running -
it silently falls back to whatever server IS running. fm_backend_herdr_cli
wraps every herdr invocation with both HERDR_SESSION and a trailing
--session <name> flag (verified to route correctly in every case tried),
fixing this for the whole adapter, not just the new label-scoped calls.

bin/fm-spawn.sh: a --secondmate spawn is launched BY the primary's own
process, whose FM_HOME still names the primary at that point. The herdr case
arm now shadows FM_HOME to the secondmate's own home (PROJ_ABS) for just the
two calls that resolve/create the workspace and tab, restored automatically
afterward (bash's temporary-assignment-before-a-command form works for shell
functions too). A crewmate/scout spawned FROM a secondmate's own fm-spawn.sh
process needs no such glue - its own FM_HOME already names it.

Tests: extended tests/fm-backend-herdr.test.sh (per-home label resolution,
--no-focus, --session flag, workspace-find/list-live scoping) and
tests/fm-backend-herdr-smoke.test.sh (a secondmate-shaped home's workspace
label, list-live scoping, restart stability in the multi-workspace shape).
Added tests/fm-backend-herdr-workspace-per-home-e2e.test.sh: the mandatory
isolated E2E, driving real bin/fm-spawn.sh/fm-teardown.sh - a primary-shaped
home into "firstmate", a --secondmate spawn into its own labeled space, a
crewmate spawned FROM that secondmate-shaped home landing in the same space
(this exact path had never run before), teardown closing only the right tab,
and list-live recovery seeing only each home's own tabs. All ten assertions
passed on the real binary; the default herdr session's own workspace state
was confirmed byte-identical before and after every real-herdr test run in
this change.

docs/herdr-backend.md: rewrote "Task container shape" for the workspace-per-
home design (label derivation, the --secondmate FM_HOME-shadow wrinkle, focus
behavior, label-collision/adopt-don't-duplicate semantics, no-forced-
migration), added "Session targeting: the --session flag, not HERDR_SESSION
alone", extended "ID stability" to the multi-workspace shape, and documented
the new E2E test.

* no-mistakes(review): Clarify herdr focus docs

* no-mistakes(review): Captain, clarify herdr server session docs

* no-mistakes(document): Document herdr per-home spaces

* fix(backends): rename Herdr secondmate workspace labels (#203)

* Rename herdr secondmate workspace prefix to 2ndmate-

The primary home keeps the firstmate label; secondmate homes now
resolve to 2ndmate-<id> so the herdr spaces sidebar is unambiguous.
Tests and docs updated; pre-rename workspaces can be aligned with
herdr workspace rename.

* no-mistakes(review): Clarify herdr workspace migration behavior

* no-mistakes(document): Herdr docs label alignment

* feat(bin): add unified session start digest (#201)

* feat(bin): collapse session start into one command

Add bin/fm-session-start.sh, composing fm-lock.sh, fm-bootstrap.sh, and
fm-wake-drain.sh into one ordered digest (lock, bootstrap diagnostics,
wake queue, context files, fleet state) instead of six-plus separate
turns. Lock now runs before bootstrap's mutating sweeps, closing a race
where a second concurrent session could mutate shared state before
discovering the lock was held. A lock refusal prints a loud read-only
banner, skips every mutating step via a new opt-in
FM_BOOTSTRAP_DETECT_ONLY flag on fm-bootstrap.sh, and still completes
the read-only-safe digest.

Add fm_backend_target_exists to fm-backend.sh as a shared, read-only,
never-side-effecting per-task endpoint-liveness primitive for both the
tmux and herdr backends.

Rewrite AGENTS.md sections 3 and 5 around the single command and add
tests/fm-session-start.test.sh.

* no-mistakes(review): Harden session-start read-only guidance

* no-mistakes(review): Suppress read-only tangle repair guidance

* no-mistakes(review): Include orphan status logs

* no-mistakes(review): Captain: make session-start test executable

* no-mistakes(review): Captain: clarify status tail guidance

* no-mistakes(document): Sync session-start docs

* no-mistakes(lint): Shell lint clean

* fix(tests): avoid shellcheck boolean chain

* fix(bin): corroborate herdr idle crew state (#207)

* fix(bin): corroborate herdr idle agent_status with the pane's own text

crew_pane_is_busy trusted a bare `idle` verdict from herdr's agent.get
outright, skipping the tail-regex corroboration unknown already gets.
agent.get reports generation state only (working while the model streams
a turn), so it reads idle for a crew blocked on its own long foreground
no-mistakes run - even though the pane still shows the busy banner the
whole time. Combined with the no-mistakes CLI's 10-run attribution cap,
this made a genuinely working herdr crew read as not provably working,
triggering an immediate stale wake instead of absorb-then-escalate.

* no-mistakes(document): Align herdr busy-state docs

* fix(backends): reuse the herdr firstmate workspace instead of leaking one per spawn (#202)

* fix(backends): reuse the herdr firstmate workspace instead of leaking one per spawn

Every herdr-backed crewmate left an orphaned `firstmate`-labelled workspace
behind, one per task, because `fm_backend_herdr_workspace_find` never matched
the existing workspace: its jq filter used `--arg label ... $label`, and
`label` is a reserved keyword in jq (label/break), so the filter was a compile
error. The error was swallowed by `2>/dev/null`, the find returned empty on
every call, and `workspace_ensure` took the create path each spawn, minting a
fresh workspace. The same collision silently disabled the create-task
duplicate-label check and the bare-selector tab lookup.

Rename the jq variable to `$want` in all three affected filters so reuse,
duplicate detection, and bare-selector lookup work. With reuse restored the
single `firstmate` workspace is persistent (like tmux's session) and teardown
correctly leaves it in place, closing only the task's pane/tab.

Also prune the default tab (label "1") herdr auto-creates inside a freshly
created workspace, best-effort, so the workspace holds only real task tabs.

Add stateful-fake-CLI tests that replay repeated spawn/teardown cycles and
assert one reused workspace, zero orphans, the default tab pruned, and
`workspace create` invoked exactly once. Verified against the real herdr binary
too: the pre-fix code failed the smoke idempotency check (minted wE then wF in
an isolated session); the fix passes it.

Document the workspace lifecycle, the jq-keyword pitfall, the default-tab
prune, and the project-labelled-workspace anomaly (not adapter-created) in
docs/herdr-backend.md.

* docs(herdr): trim workspace-lifecycle addition to current-state facts

The docs/herdr-backend.md convention documents current behavior, not
history - narrative belongs in the PR/commit message. Tightened the
workspace-leak and default-tab-prune write-up down to the operative
facts (the jq reserved-keyword guard, when pruning is safe, and the
persistence caveat), and corrected the CLI-facts table rows to match
the corrected prune timing.

* fix(backends): defer herdr default-tab prune until a real task tab exists

Closing a workspace's LAST tab deletes the whole workspace on real
herdr (verified). Pruning the auto-created default tab right after
workspace create closed the workspace's only tab at that point,
destroying the just-created workspace on every single spawn instead of
reusing it - the fake-CLI unit tests didn't model this real-herdr
behavior, so they passed while the real-herdr smoke test failed with
"container_ensure is not idempotent".

Move the prune into fm_backend_herdr_create_task, right after the
first real task tab is added to a freshly created workspace, when
closing the default tab alongside it is safe. Update the fake-CLI unit
test to match the corrected timing.

Also fix a smoke-test-only bug this surfaced: the test's second
create_task call reused a $CONTAINER captured before the first task
was killed, rather than re-running container_ensure like real
fm-spawn.sh always does immediately before every create_task call - so
once the workspace (correctly) disappeared after its last tab closed,
the stale reference no longer named a live workspace.

* test(backends): guard against jq --arg names colliding with jq keywords

Regression guard for the workspace-leak bug this PR fixes: a jq
--arg/--argjson variable named after a jq reserved keyword (e.g.
label) is a compile error on jq <= 1.6, and this adapter's
2>/dev/null silently turns that into an empty result instead of a
visible failure. Greps bin/ for the pattern so a future violation
fails loudly here instead of silently misbehaving on an older jq.

* docs(herdr): fix per-home staleness and drop contributor-specific example

The workspace-lifecycle write-up hardcoded "the firstmate workspace"
as if the label were always the fixed constant, stale against the
per-home labeling documented earlier in this file (primary: firstmate,
secondmate: 2ndmate-<id>). Rephrased per-home throughout, and pointed
the "workspace this adapter did not derive" case at the existing
Label-derivation section instead of a separate anomaly writeup.

Dropped the "Anomaly: a workspace labelled with a project name"
section - the python-teslemetry-stream example was a contributor's own
environment, not current adapter fact, and it repeated a now-incorrect
FM_BACKEND_HERDR_WORKSPACE_LABEL constant claim. Replaced with one
generic sentence already covered by the corrected wording above.

Also fixed the jq-reserved-keyword guard test's file reference, which
named tests/fm-backend.test.sh when the test actually lives in
tests/fm-backend-herdr.test.sh.

---------

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

* fix(backends): make Herdr default-tab pruning safe (#215)

* fix(backends): make the herdr default-tab prune provably safe

The default-tab prune could close a pane holding a LIVE agent: an
ADOPTED workspace (found pre-existing by label match) was pruned using
the same tab-count/label heuristic as a freshly created one, and herdr
derives a workspace's displayed label from its cwd basename when no
explicit --label is given. A captain launching herdr directly inside a
directory named "firstmate" produces a workspace that looks identical,
by label alone, to firstmate's own container - so the very next spawn
adopted the captain's own live workspace and closed their live pane
27ms after creating its task tab (2026-07-02 incident).

The fix is structural: fm_backend_herdr_workspace_ensure now captures
the seeded default tab's id straight from its own `workspace create`
response, only when it just created the workspace. That id threads
through fm_backend_herdr_container_ensure to fm_backend_herdr_create_task,
which is the only function allowed to prune it - an adopted workspace's
caller always passes an empty seeded-tab-id, so create_task never
re-derives "prunable" from a tab's label or count. Defense in depth:
the prune also refuses a tab whose pane reports a working agent.

Covered by new unit tests (adopted-never-prunes, created-prunes-exactly,
the exact label-collision incident shape) and a new isolated real-herdr
E2E test that reproduces the incident against the pre-fix code and shows
it fixed, plus the normal happy path.

* no-mistakes(document): Sync herdr prune docs

* no-mistakes: apply CI fixes

* fix(brief): remove apostrophe breaking bash -n and guard bash 3.2 set -u in spawn (#173)

* fix(brief): remove apostrophe that broke bash -n on fm-brief.sh

The no-mistakes DOD heredoc, built via VAR=$(cat <<EOF ... EOF), had an
unescaped apostrophe in "no-mistakes' own guidance". Nesting a heredoc
inside $(...) makes bash track quote state through the body, so the lone
apostrophe broke parsing of the rest of the script (bin/fm-brief.sh:211),
making the default no-mistakes ship path fail outright. Audited the other
two $(cat <<EOF...EOF) blocks (direct-PR, local-only) for the same class
of bug; none found. Added tests/fm-brief.test.sh as a regression guard.

* no-mistakes(test): fix(spawn): guard empty shared_args under bash 3.2 set -u

* no-mistakes(document): docs(contributing): list tests/fm-brief.test.sh in the test suite inventory

* fix(test): silence shellcheck SC2034/SC2100 in fm-brief.test.sh

Drop the unused out= capture (redirect to /dev/null instead) and quote
the id= assignment so shellcheck stops reading the hyphenated id value
as an arithmetic expression.

* feat: add experimental zellij runtime backend (#217)

* feat(backends): add experimental zellij runtime backend (P3)

Implements bin/backends/zellij.sh on the P1 dispatcher + P2 herdr precedent:
one zellij session, one tab per task, treehouse stays the worktree provider.
Wired through fm-backend.sh/fm-spawn.sh so fm-send/fm-peek/fm-watch/
fm-crew-state/fm-teardown work generically with zero changes to those scripts.

Empirically verified against real zellij 0.44.0: every "gaps to verify" item
from the design report, plus real findings the report missed - new-tab always
steals focus (mitigated with a restore call), zellij action always exits 0
even against a dead target, every pane op needs an explicit --pane-id, and
pane_cwd never tracks a subshell's own cd (treehouse get's exact shape) so
worktree-path discovery uses an active pwd-probe instead of passive JSON
polling. Findings and the full real-CLI evidence log are in
docs/zellij-backend.md.

Full real E2E cycle passed: spawn a real claude crewmate, accept its trust
dialog, steer it, receive done, confirm teardown refuses before merge, merge
local-only, confirm teardown then succeeds and the zellij tab is gone - all
in a scratch FM_HOME against a uniquely-named isolated zellij session, never
touching the real "firstmate" session or the live fleet.

Existing tmux and herdr conformance suites stay green; the two P1-era tests
asserting zellij was unimplemented now assert that of orca instead.

* no-mistakes(review): captain, guard zellij paste payloads

* no-mistakes(review): Captain, guard zellij pane readiness

* no-mistakes(review): Captain, harden zellij dead-target handling

* no-mistakes(review): Captain, harden zellij teardown and tests

* no-mistakes(review): Captain, harden zellij target validation

* no-mistakes(document): Document zellij backend

* docs: document Orca backend adapter contract (#209)

* docs: specify Orca backend adapter contract

* docs: require Orca window target alias

* no-mistakes(test): Captain: handle empty arrays under nounset

* no-mistakes(document): Document Orca backend proposal

---------

Co-authored-by: Stephen Brouhard <vesta@stephens-macbook-air.tail2122af.ts.net>

* feat(backends): add Orca primitive backend support (#210)

* feat(backends): add Orca adapter primitives

* no-mistakes(review): Gate Orca from task spawning

* no-mistakes(document): Document Orca backend limits

* fix(backends): stop mapping Orca Escape to interrupt

* no-mistakes(document): Document Orca primitive key support

* no-mistakes: apply CI fixes

* fix: drop CI-gate rewrite and normalize shared array-guard hunks

---------

Co-authored-by: Stephen Brouhard <vesta@stephens-macbook-air.tail2122af.ts.net>

* fix(backends): submit herdr slash commands reliably (#223)

* fix(backends): herdr slash-command submit verification false-positives on popup autocomplete

Two grok/herdr crewmates left /no-mistakes fully typed but unsubmitted for
minutes while fm-send exited 0. Live-reproduced against real grok 0.2.82:
the herdr adapter's submit verification declared success on ANY pane content
change after Enter, but an argument-taking slash command's first Enter only
closes the popup and expands the composer into an argument-hint placeholder
(/compact -> /compact compaction instructions) rather than submitting - a
real, visible change that isn't a submission. A second Enter is required.

fm_backend_herdr_composer_state replaces the delta check with a structural
read of the composer's own row (located by border-glyph shape, since herdr
exposes no cursor-row primitive), mirroring what cursor_y gives the tmux
adapter. A popup-close-with-placeholder-fill still reads pending, so the
retry loop now correctly sends the needed second Enter instead of stopping
early. The tmux backend was unaffected (its cursor-row read already handled
this correctly, verified side by side against the same live repro).

* no-mistakes(document): Sync herdr submit docs

* feat(skills): publish public stow skill with internal-skill hiding (#221)

* feat(skills): hide agent-only skills from installer discovery, add public stow

Mark every .agents/skills/* skill metadata.internal: true so the
skills.sh installer (npx skills add) hides them from discovery - all
assume a live firstmate home and are meaningless elsewhere. This is
inert to firstmate's own harness skill loader.

Add a new, fully standalone skills/stow for non-firstmate users: sweep
a conversation for durable knowledge and file it into whatever notes
convention the host project/user already has, asking once and
remembering the answer when ambiguous. No shared code with the
internal stow by design.

Document the two-tier layout in README and CONTRIBUTING.

* no-mistakes(review): Tighten public stow tracker routing

* no-mistakes(document): Sync skill docs

* fix(skills): make public stow's undone-next-steps routing local-first

Supersede the earlier ask-before-tracker-write tightening: the
standalone stow no longer treats an issue tracker as a routing option
at all based on inference (git remote, .github/ presence, etc).
Undone next steps always land in a local file by default - an
existing TODO/BACKLOG/NOTES file, or a freshly created local scratch
file otherwise. A tracker (or any other external system) is only ever
used when the user has explicitly said so, this session or as a
previously recorded standing preference.

* no-mistakes(document): Sync skill documentation

* feat(skills): clarify public stow resume and fallback routing (#225)

* feat(skills): resume pointer, routing tiers, default notes file for public stow

Adds a copy-pasteable resume pointer to the safe-to-end verdict so a
new session can pick the work back up cold, states the explicit three-tier
routing priority (explicit instruction > existing local convention > default
NOTES.md fallback) instead of leaving it implicit, and names NOTES.md as the
top-level discoverable default instead of letting each agent improvise a
location. Also removes a leftover internal-tooling word from the maintainer
comment so the public file carries zero internal vocabulary.

* fix(skills): make the public stow default fallback private and gitignored

Supersedes the earlier NOTES.md/ask-once split: the tier-3 default fallback
(no existing local convention fits) is now .stow-notes.md, gitignored so it
never lands unprompted in a shared/committed file. Because it's private, it's
safe for every finding-kind including user preferences, so the separate
ask-once carve-out for personal material is no longer needed there. Tier 2
(an already-established tracked convention) is unchanged and is the only
tier that still writes into a shared file. Step 7's resume pointer now flags
when notes landed in the private fallback and that they can be promoted into
a shared file later.

* fix(skills): split the public stow private fallback by scope, use git exclude

Supersedes the single .stow-notes.md fallback: user preferences (cross-project
by nature) now default to a host-local ~/.stow/notes.md instead of being
siloed into one project's repo. Project-scoped findings still default to
.stow-notes.md at the project root, but it's now kept out of git via the
local-only .git/info/exclude instead of the tracked .gitignore, so the
fallback is truly zero-shared-footprint: nothing lands in a tracked file and
nothing is left for the user to review or commit.

* fix(skills): keep the public stow private fallback sandbox-safe (current dir only)

Supersedes the home-file split: a home-directory path fails for agents
sandboxed to their current working directory, so the tier-3 default now
stays a single .stow-notes.md at the project root for every finding-kind,
including user preferences. Tier 2's user-level memory file is now framed
as a bonus when accessible, never assumed or required. Step 7 gains a
caveat when a preference lands in the project-local fallback: it applies to
this project only, and the user can copy it into their own global memory
file if they want it to follow them everywhere.

* fix(skills): use a current-directory .gitignore for the public stow fallback

Supersedes .git/info/exclude: that mechanism resolves outside the working
directory in a linked worktree, breaking the sandbox-safety guarantee for
exactly the setup this fleet uses everywhere. Switch to an ordinary
.gitignore file in the current directory instead - always in-directory
regardless of worktree layout - creating or appending a .stow-notes.md
line, left uncommitted for the user. If that write itself fails, the skill
still creates .stow-notes.md and tells the user to ignore it manually
rather than blocking. Also scopes the "never writes outside the current
directory" guarantee precisely to tier 3: tier 2 is exempted since it only
targets a destination the user's own existing convention already
established, which can legitimately be a user-level file outside the
project.

* no-mistakes(review): Clarify stow routing precedence

* no-mistakes(review): Clarify stow fallback metadata boundary

* no-mistakes(review): Guard tracked stow fallback

* no-mistakes(review): Clarify stow fallback verdict

* no-mistakes(document): Sync stow skill docs

* no-mistakes(lint): Public skill lint cleanup

* docs: clarify note hygiene guidance (#226)

* docs: add note-hygiene rule to backlog format section

Backlog and task notes accumulate volatile specifics that drift and
mislead; capture the general principle so every firstmate user avoids
trusting a stale note over the authoritative source.

* no-mistakes(review): Clarify note hygiene schema exemptions

* no-mistakes(document): Clarify note-hygiene docs

* revert: drop out-of-scope stow/architecture doc edits

Keep this PR's diff scoped to the AGENTS.md note-hygiene addition only;
skills/stow/SKILL.md, .agents/skills/stow/SKILL.md, and docs/architecture.md
are under separate close review and must not change out of band here.

This reverts commit ed2d73205b01e24ba8ffd40c523a24993f6eb1a5.

* feat(backends): add Orca task lifecycle support (#228)

* feat(backends): add Orca task lifecycle support

* fix(backends): harden Orca spawn lifecycle

* no-mistakes(review): Fix Orca lifecycle cleanup gaps

* no-mistakes(review): Release Orca worktrees without paths

* no-mistakes(review): Guard Orca spawn abort cleanup

* no-mistakes(review): Allow partial Orca child cleanup

* no-mistakes(review): Fix Orca selector and cleanup leaks

* no-mistakes(review): Preserve pathless Orca cleanup metadata

* no-mistakes(review): Harden Orca spawn and teardown lifecycle

* no-mistakes(review): Enforce Orca scout report gate

* no-mistakes(review): Harden Orca teardown path validation

* no-mistakes(review): Harden Orca capture errors

* no-mistakes(review): Harden Orca JSON cleanup validation

* no-mistakes(test): Fix zellij scout teardown fixture

* no-mistakes(document): Document Orca lifecycle support

* Harden Orca runtime and submit verification

* no-mistakes(review): Captain, preserve Orca current-tail verification

* no-mistakes(document): Sync Orca lifecycle docs

* no-mistakes(lint): Captain, silence deliberate ShellCheck

---------

Co-authored-by: Stephen Brouhard <vesta@stephens-macbook-air.tail2122af.ts.net>

* fix: use PR head for review diffs (#229)

* Fix fm-review-diff to compare PR head when pr= is recorded

After no-mistakes fix rounds push to the open PR, the crewmate worktree
branch can lag the authoritative PR head. When meta records pr=, resolve
the compare ref from reachable pr_head= or refs/pull/<n>/head before
diffing against the fetched authoritative base; fall back to the local
branch with a loud warning when the PR head cannot be resolved.

Add behavior tests for pr_head resolution, fetch, unchanged no-pr path,
and unreachable-PR fallback.

* no-mistakes(review): Make review-diff test executable

* no-mistakes(document): Document PR-head review diffs

* no-mistakes: apply CI fixes

* fix: make backend matching shell-portable (#230)

* Make fm-backend.sh backend-name matching shell-portable for zsh

Replace word-split-dependent for-loops in fm_backend_is_known() and
fm_backend_validate_spawn() with case-based membership tests so sourcing
the library from zsh no longer falsely rejects known backends.

Add zsh/bash regression coverage in tests/fm-backend.test.sh.

* no-mistakes(review): Fix zsh backend loading and validation

* no-mistakes(document): Document backend membership portability

* fix(backends): make herdr respawn idempotent (#231)

* fix(backends): make herdr respawn idempotent against restored-layout husks

herdr persists and restores its session layout (workspaces/tabs/panes)
across a server restart, so a restored fm-<id> task tab comes back a
husk - a dead pane, or a plain agent-less shell - which fm-spawn.sh's
duplicate-tab guard refused unconditionally, forcing manual pane closes
after every restart.

fm_backend_herdr_create_task now classifies an existing same-labeled
tab's pane conservatively (dead/no-agent/live/unknown) and
closes-and-replaces only a confirmed husk, always creating the
replacement tab before closing the old one so a husk that is a
workspace's only tab is never at risk of taking the whole workspace
down with it. A genuinely live agent, or anything not confidently
classifiable, still refuses exactly as before.

* no-mistakes(review): Harden herdr duplicate respawn guard

* no-mistakes(review): Enforce herdr husk cleanup postcondition

* no-mistakes(document): sync herdr respawn docs

* no-mistakes: apply CI fixes

* fix: absorb stale wakes during active validation (#233)

* fix(watcher): stop stale_is_terminal from ignoring an active run-step

A crewmate's status log gets no new entry once firstmate hands it to a
no-mistakes validation (the sparse status-reporting contract), so the
log's last line can stay a pre-validation "done:" (or needs-decision/
blocked) leftover for the run's entire duration. fm-watch.sh's
stale_is_terminal only reads that raw last line - it has no run-step
awareness - so it kept surfacing a stale pane as immediately terminal
every time it went quiet for two polls, no matter how actively the
pipeline was validating (confirmed live against fm-herdr-respawn-idem,
whose status log's last line is literally "done: ..." while its
no-mistakes run-step reads "validating (running)"). crew_is_provably_working
now gets a chance to override a stale captain-relevant log line on a
new stale hash, exactly as it already did for a non-captain-relevant one.

Also fixed a separate, independently-confirmed dead code path in
fm-crew-state.sh: its cross-branch run-attribution fallback shelled out
to `no-mistakes axi` (bare) expecting a runs[N]{...} TOON table that the
real CLI (v1.32.2) never emits - verified the axi surface exposes only
abort/logs/respond/run/status. Replaced it with the real top-level
`no-mistakes runs` listing.

* no-mistakes(document): Sync watcher stale docs

* no-mistakes(lint): Fix stale fake run-list variables

* docs: clarify no-mistakes evidence commit handling (#232)

* docs: no-mistakes evidence commits in crew branches are intentional

* no-mistakes(review): Scope evidence guidance to project repos

* no-mistakes(document): Document evidence commit policy

* fix: tighten Orca backend parsing (#237)

* fix: tighten orca parser coverage

* no-mistakes(document): Sync Orca backend docs

* no-mistakes(lint): Captain, lint clean

---------

Co-authored-by: Stephen Brouhard <vesta@stephens-macbook-air.tail2122af.ts.net>

* docs: add backend setup guides and slim README (#238)

* docs: make README pointer-first, add per-backend setup guides

Trim the Quick Start and How It Works walls of prose down to overview plus
pointers, relocating every removed sentence's content into docs/architecture.md,
docs/configuration.md, or the relevant backend doc. Add docs/tmux-backend.md as
the reference-backend setup guide, and add a Setup section to each experimental
backend doc (herdr, zellij, Orca) covering prerequisites, selection, first run,
watching/attaching, verification, and limitations. Record the README convention
in CONTRIBUTING.md.

* no-mistakes(review): Clarify backend setup docs

* no-mistakes(review): Clarify tmux secondmate support

* no-mistakes(document): Align backend documentation

* docs: stop telling users to run fm-spawn.sh for backend selection

fm-spawn.sh is firstmate-internal; a user never runs it directly. Rephrase
every user-facing backend-selection sentence across the tmux/herdr/zellij/orca
guides and docs/configuration.md to present the actual user mechanisms: a
local config/backend file, FM_BACKEND at launch, or telling the first mate in
chat. Internal-reference mentions of the --backend flag (fm-spawn.sh usage
notes, test coverage lists) are left as mechanics, not user instructions.

* Document herdr dual license in docs and install hint (#239)

Add AGPL-3.0-or-later/commercial licensing note to herdr-backend Setup
and the missing-binary error in bin/backends/herdr.sh.

* feat: support multiple X-mode follow-ups (#241)

* feat(x-mode): raise X follow-up cap to 3 within a 7-day window

Matches the relay's parallel contract change: fm-x-link.sh now records a
follow-up counter (with --carry-count to preserve it across a re-link onto
a successor task), fm-x-followup.sh posts up to three follow-ups per
mention within a 7-day window instead of one within 24h, clearing the
link on --final, cap exhaustion, window lapse, or a distinguishable relay
rejection (fm-x-reply.sh HTTP 409 -> exit 9) rather than treating that
rejection as a retryable failure. AGENTS.md and the fmx-respond skill are
updated to keep usage disciplined: spend follow-ups only on genuine
milestones, always finish with --final.

* no-mistakes(review): Update follow-up dry-run docs

* no-mistakes(review): Captain: refresh X follow-up docs

* no-mistakes(review): Captain: harden X follow-up relinks

* no-mistakes(review): Captain: harden follow-up state persistence

* no-mistakes(document): Document X follow-up carryover

* feat(backends): add experimental cmux runtime backend (#246)

* feat(backends): add cmux runtime backend (experimental)

Session-provider-only adapter for cmux (bin/backends/cmux.sh), mirroring
zellij/herdr structurally, wired into fm-backend.sh and fm-spawn.sh with
--secondmate refused for now. Verified against the real cmux 0.64.17 app:
send does not auto-submit, cwd is creation-time-frozen (zellij-shape,
pwd-marker-probe workaround), close-surface refuses on a workspace's last
surface (falls back to close-workspace), workspace ids do not survive a
relaunch, and the control socket defaults to cmuxOnly access (requires a
one-time password-mode setup, documented in docs/cmux-backend.md). Also
found and fixed a live bug during development: read-screen fails on a
surface that has never been written to, so liveness now uses list-panes
instead. Fake-CLI unit suite (40 tests), a real-binary smoke test, and a
full spawn/steer/peek/done/merge/teardown E2E pass against a real claude
crewmate all pass, including the popup/second-Enter regression class.

* no-mistakes(review): Harden cmux recovery and password parsing

* no-mistakes(review): Harden cmux capture failure handling

* no-mistakes(review): Mark cmux test scripts executable

* no-mistakes(review): Scope cmux workspaces and teardown

* no-mistakes(review): Captain, honor cmux password config override

* no-mistakes(review): Captain, hash cmux home labels

* no-mistakes(document): Sync cmux backend docs

* feat(agents): add firstmate coding guidelines skill (#248)

* Add firstmate-coding-guidelines skill (AGENTS.md diet PR 0)

Encodes the knowledge-placement decision tree, one-owner rule, and
inline-stub pattern from the diet analysis so future contributions stop
adding conditional detail inline. AGENTS.md gets one section-13 trigger
line; fm-brief.sh's REPO argument has no reliable signal for "this is
firstmate's own repo", so the load instruction goes in CONTRIBUTING.md's
Development section instead of the scaffold.

* no-mistakes(review): Captain, align tracked-material trigger scope

* no-mistakes(document): Sync coding guidelines docs

* no-mistakes(lint): Fix Markdown style issues

* fix: add turn-end supervision guard (#249)

* feat: structural Stop-hook backstop for primary turn-end supervision

fm-guard.sh is pull-based: it only warns when some other supervision
script happens to run, so a primary session that ends a turn without
re-arming the watcher and then runs no further fleet-touching command
can sit blind for hours (the 2026-07-04 incident this fixes).

Add bin/fm-turnend-guard.sh, a Claude Code Stop hook registered in the
tracked .claude/settings.json, that fires on every primary turn end and
blocks (exit 2, verified empirically to force continuation) when work
is in flight with no fresh watcher beacon. It never blocks more than
once per turn, using Claude Code's own stop_hook_active loop-guard
field, and scopes itself to the actual primary checkout only (inert in
crewmate/scout worktrees and secondmate homes).

Factor the shared "in-flight but no live watcher" predicate out of
fm-guard.sh into bin/fm-supervision-lib.sh so the pull-based banner and
the push-based hook can never drift on what "unhealthy" means.

Document the verified Stop-hook mechanism and scoping in
docs/turnend-guard.md, add a harness-adapters note, and cover the
predicate and hook with tests/fm-turnend-guard.test.sh.

* no-mistakes(review): Respect active home in turnend guard

* no-mistakes(review): Require live watcher for turn-end guard

* no-mistakes(review): Captain: portable turn-end timing

* no-mistakes(document): Sync turn-end guard documentation

* feat(backends): auto-detect cmux runtime (#250)

* feat(backends): auto-detect cmux runtime from CMUX_WORKSPACE_ID

Wires cmux into fm_backend_detect the same way herdr already is: a
firstmate process running inside a cmux-spawned terminal now spawns
new tasks into cmux by default, no config needed. Verified from cmux's
own shipped source that CMUX_WORKSPACE_ID/CMUX_SURFACE_ID/CMUX_SOCKET_PATH
are unconditionally, non-overridably injected into every terminal
surface it spawns, and that cmux's own CLI treats CMUX_WORKSPACE_ID as
its own ambient-target fallback - the same role $TMUX/HERDR_ENV play for
their backends. CMUX_WORKSPACE_ID is checked last (after $TMUX and
HERDR_ENV=1) since cmux is a terminal application, not a nestable
multiplexer. Socket auth (config/cmux-socket-password) stays required
regardless of how the backend was selected; the existing spawn refusal
now also names the config/backend=tmux / --backend tmux opt-out for a
caller who never explicitly chose cmux.

A live env dump inside a real cmux terminal was not obtained safely on
the shared dev machine (documented in docs/cmux-backend.md); this rests
on the source read instead, mirroring this doc's existing
verified-from-source precedent.

* no-mistakes(review): Fix cmux autodetect docs and tests

* no-mistakes(document): Document cmux auto-detection

* fix(afk): support herdr away-mode injection (#251)

* fix(afk): make the away-mode daemon backend-aware for herdr

bin/fm-supervise-daemon.sh discovered its supervisor pane and injected
via raw tmux calls only, so /afk failed outright on a herdr-based
fleet (TMUX_PANE unset, firstmate:0 fallback unresolvable).

Discovery now resolves backend (tmux|herdr) and target independently,
mirroring fm-backend.sh's own runtime auto-detection, with an explicit
FM_SUPERVISOR_BACKEND override alongside the existing FM_SUPERVISOR_TARGET.
zellij/orca refuse loudly at startup instead of misapplying tmux
primitives. Injection (pane-exists probe, busy-guard, composer-guard,
verified submit) now dispatches through bin/fm-backend.sh's generic
primitives, adding a new fm_backend_composer_state dispatcher; the
tmux path is byte-identical to before. Also fixes a pre-existing bug
in fm_backend_target_exists's herdr arm (missing --session, so it
silently misrouted once more than one herdr server was running) found
while verifying this end to end against a real isolated herdr session.

Classification, batching, max-defer, the marker contract, locks, and
wake-queue handling are unchanged - this is a transport-layer fix.

* no-mistakes(review): Corroborate Herdr idle busy state

* no-mistakes(review): Stabilize Herdr daemon startup wait

* no-mistakes(review): Captain, route cmux composer and update AFK docs

* no-mistakes(document): Document AFK supervisor backend support

* docs(agents): move X-mode procedures out of AGENTS (#253)

* docs(agents): collapse X-mode section 14 into fmx-respond/docs pointers

AGENTS.md diet PR 1 of 3 (agentsmd-diet-s2 report, move-plan items 1-2).
Replaces section 14's "Answering"/"Completion follow-up"/"Conversations"/
"Length and threads"/"Preview / dry-run" blocks (54 lines) and the
"Mechanism" narrative (6 lines) with two short pointers: fmx-respond
(section 13) for the procedure, docs/configuration.md "X mode (.env)"
for the wire protocol. Net -55 lines in AGENTS.md.

Destination edits landed first, deletions second (q4 discipline):
- docs/configuration.md: added the "purely additive, watcher untouched"
  guarantee that AGENTS.md's Mechanism block stated but configuration.md
  did not.
- fmx-respond/SKILL.md: added the x-mode-error wake boundary (report as
  a blocker, do not load this skill), the --image flag for replies and
  follow-ups, the "images are for real artifacts, not prose" rule, and
  the dry-run compact-image-marker behavior - none of these were
  previously in the skill even though AGENTS.md described them, so they
  were genuine gaps, not pre-existing duplication. Also made the skill's
  own "Completion follow-up" section the sole, full owner of that
  procedure instead of deferring to AGENTS.md section 14 for substance
  that no longer lives there (two internal cross-references updated to
  point at section 8's terminal-wake trigger and the skill's own section
  instead).

Mechanical line-by-line audit of every removed AGENTS.md line:

Mechanism block (6 lines removed):
- bootstrap artifact-writing description -> already owned by
  docs/configuration.md "X mode (.env)" (locked-bootstrap paragraph)
- check-shim/poll mechanism description -> already owned by
  docs/configuration.md same section
- missing-deps/x-mode-error diagnostic description -> already owned by
  docs/configuration.md ("Relay auth or config problems...") plus
  bin/fm-x-poll.sh's own header comment for the missing-curl/jq mechanics
- opt-out artifact removal description -> already owned by
  docs/configuration.md same section
- "purely additive, no edit to fm-watch.sh/fm-watch-arm.sh/fm-wake-lib.sh/
  afk daemon" guarantee -> MOVED to docs/configuration.md (added in this
  PR; this fact had no other home before)

Answering/Completion follow-up/Conversations/Length and threads/
Preview-dry-run blocks (54 lines removed):
- x-mention wake -> load fmx-respond: already owned by section 13's
  existing trigger line (unchanged) and restated in the new pointer
- x-mode-error wake -> report as blocker, don't load fmx-respond: MOVED
  to fmx-respond/SKILL.md (added in this PR)
- inbox-draining, classification, acting, reply composition, submission,
  cleanup-on-success/failure: already owned by fmx-respond/SKILL.md
  "Procedure" section (unchanged, pre-existing)
- owner-only routing / captain-as-asker framing: already owned by
  fmx-respond/SKILL.md "The asker is your own captain" section
- standing X-mode authorization / autonomous posting / dry-run as only
  non-posting path: already owned by fmx-respond/SKILL.md same section
- acknowledge-first -> act -> follow-up shape, three-case classification:
  already owned by fmx-respond/SKILL.md "A request to act on" section
- destructive/irreversible/security-sensitive escalation guardrail:
  already owned by fmx-respond/SKILL.md "Public channel..." section and
  Procedure step 2c
- dismiss-instead-of-reply for pure acknowledgments, relay re-offer
  prevention, dry-run honoring: already owned by fmx-respond/SKILL.md
  Procedure steps 2b/2c/2e-skip and docs/configuration.md
- public-safety bar (no task ids/internals/captain-private/secrets):
  already owned by fmx-respond/SKILL.md "The reply is public" section
- never-inline-into-shell-command / --text-file or stdin: already owned
  by fmx-respond/SKILL.md Procedure step 2e and Notes
- --image flag for replies (formats, base64, no-inline guarantee): MOVED
  to fmx-respond/SKILL.md Procedure step 2e (added in this PR - this was
  not previously in the skill)
- fm-x-link field names (x_request=, x_request_ts=, x_followups=):
  already owned by AGENTS.md section 2's state/<id>.meta field list
  (untouched, out of scope for this PR) and fmx-respond/SKILL.md
- carry-count/carry-ts relink behavior, three-follow-up budget, milestone
  sparingness, --check/--text-file posting, connector/followup wire
  detail, --final clearing, cap/window graceful degradation: already
  owned by fmx-respond/SKILL.md "Completion follow-up" section (now sole
  owner) and docs/configuration.md wire-protocol paragraphs
- --image flag for follow-ups: MOVED to fmx-respond/SKILL.md "Completion
  follow-up" section (added in this PR - genuine gap)
- "failed task still gets an honest final follow-up": already owned by
  fmx-respond/SKILL.md "Completion follow-up" section
- FMX_DRY_RUN whole-loop previewability: already owned by
  fmx-respond/SKILL.md "Dry-run / preview mode" section
- in_reply_to conversation continuity, untrusted-thread handling,
  follow-up worthiness judgment, relay-owned self-reply guard/cap:
  already owned by fmx-respond/SKILL.md "The direct ask is the captain's"
  section and Notes (one bullet is a verbatim match)
- concise-by-default / no hand-numbered threads: already owned by
  fmx-respond/SKILL.md "Voice" section
- auto-split behavior, char/tweet caps, premium-independence, wire shape
  ({text}/{text,texts}): behavior already owned by fmx-respond/SKILL.md
  Voice section; exact defaults and wire shape already owned by
  docs/configuration.md; "premium-independent" mechanics already owned
  by bin/fm-x-reply.sh's own header comment
- "images are for real artifacts, not prose": MOVED to fmx-respond/
  SKILL.md "Voice" section (added in this PR - genuine gap)
- image-on-thread wire behavior: already owned by docs/configuration.md;
  reinforced in fmx-respond/SKILL.md's new --image note
- dry-run POST-body shape, endpoint marker, truthy-value definition,
  jq-only dependency, end-to-end testability, x-outbox inspection:
  already owned by fmx-respond/SKILL.md "Dry-run / preview mode" section
  (several near-verbatim matches) and docs/configuration.md wire detail
- dry-run compact image marker: MOVED to fmx-respond/SKILL.md "Dry-run /
  preview mode" section (added in this PR - genuine gap)

Section 8's terminal-wake completion-follow-up trigger (the one fact
required to survive inline) is untouched and already present; the new
section 14 pointer references it instead of restating it.

Nothing outside section 14 (plus the two destination files) is touched.
Full test suite green, including all 74 fm-x-mode.test.sh checks.

* no-mistakes(review): Preserve X-linked follow-up triggers

* no-mistakes(review): Fix x-mode error trigger

* no-mistakes(document): Docs cross-reference synchronized

* no-mistakes(lint): Clean Markdown lint pass

* docs: trim duplicated harness guidance (#255)

* docs(agents): trim section 4 harness/secondmate duplication

AGENTS.md diet PR 2 of 3 (data/agentsmd-diet-s2/report.md, move-plan
items 3-4; redundancy item 2 folded into item 3).

Removed the five claude/codex/grok/pi/opencode model/effort-flag
bullets from section 4 - byte-for-byte duplicated by
harness-adapters' "Launch profile axes" table (which is already a
superset: it carries verified CLI versions per adapter that the
AGENTS.md bullets lacked). Replaced with a one-line pointer; the
skill is already loaded before every spawn per section 4's own
closing trigger, so no new trigger was needed.

Moved the config/secondmate-harness model/effort pin-format detail
(the `<harness> [<model>] [<effort>]` line format, the
secondmate-model/secondmate-effort accessors, back-compat, and the
durability-across-respawn behavior) into secondmate-provisioning,
which is already a mandatory load at every secondmate lifecycle
touchpoint. Added the destination content to the skill first, then
replaced the AGENTS.md paragraph with a 3-line pointer.

Mechanical audit - every removed line's new home:
- 5 harness bullets (claude/codex/grok/pi/opencode model+effort
  flags, per-harness max-omission rationale) -> already present in
  harness-adapters SKILL.md's "Launch profile axes" table (lines
  53-59), confirmed fact-by-fact before deleting.
- "config/secondmate-harness may also pin..." paragraph (pin format,
  bare-harness back-compat, secondmate-model/secondmate-effort
  accessors, per-spawn override precedence, respawn durability,
  secondmate-only scope) -> secondmate-provisioning SKILL.md's
  "Charter and seed" section, added verbatim before this trim.
- The following paragraph (inheritable config: crew-dispatch.json,
  crew-harness, backlog-backend) is untouched - out of scope for
  this PR, still inline.
- The bootstrap CREW_DISPATCH effort-mismatch diagnostic sentence is
  untouched - not part of the five-bullet duplication, stays inline.

No script changes. Section 4 shrinks from 104 to 87 lines
(958 -> 901 total AGENTS.md lines) with zero facts lost: every fact
is reachable through harness-adapters or secondmate-provisioning,
both already mandatory loads at the relevant lifecycle points.

* no-mistakes(document): Align secondmate skill triggers

* no-mistakes(lint): Markdown style clean

* fix: anchor turn-end Stop hook to project root (#256)

* Fix turn-end Stop hook to use CLAUDE_PROJECT_DIR path

Claude Code runs hook commands via /bin/sh from the session cwd, so the
bare relative bin/fm-turnend-guard.sh path fails when cwd is not the repo
root. Anchor the command with "$CLAUDE_PROJECT_DIR"/bin/fm-turnend-guard.sh
instead; verified CLAUDE_PROJECT_DIR is set on Stop hooks in Claude Code
2.1.201. Document the cwd caveat and add a settings.json regression test.

* no-mistakes(document): Document Stop hook path anchoring

* docs: trim firstmate agent guidance duplication (#258)

* docs: trim AGENTS.md redundancy (diet PR 3/3)

Consolidates five duplicated passages to a single owner each, per
data/agentsmd-diet-s2/report.md redundancy items c3-c7:

- Inheritable-config propagation mechanism: owned by section 3 (where
  the sweep runs); sections 4 and 7 keep compact references. Section 4
  retains its one genuinely unique fact (crew-harness inherit-vs-fallback
  semantics), just no longer restates the propagation mechanism itself.
- Landed-work definition: owned by section 7's ship-teardown detail
  (PR-containment mechanics, pr= discovery fallback); section 1's hard
  rule #3 keeps the rule plus a three-case summary and a pointer.
- Backend meta-field enumeration: owned by docs/configuration.md
  ("Runtime backend", already comprehensive including cmux) and each
  backend's own doc; AGENTS.md keeps only the fields common to every
  task plus a pointer.
- Dropped one redundant restatement of "silence is correct while
  waiting" in section 8.
- Worktree-tangle guard explanation: owned by section 8 (already the
  fuller, cross-referenced version); section 3's TANGLE bullet keeps
  the remediation action and points at section 8 for the why.

Also adds two captain-requested single-sentence rules: invoke bin/
scripts by absolute $FM_ROOT path after any cd away from the home, and
a backend spawn refusal must be surfaced to the captain rather than
silently worked around by switching backends.

AGENTS.md: 901 -> 889 lines, 112355 -> 108560 bytes.

* no-mistakes(review): Clarify post-cd bin invocation guidance

* no-mistakes(document): Sync AGENTS trim docs

* no-mistakes(lint): Fix Markdown line style

* feat(backends): improve cmux detection and socket-mode guidance (#259)

* feat(backends): cmux detection fallbacks and socket-mode matrix

Workstream A: cmux's bundled claude wrapper strips every CMUX_* env var on
its passthrough path (reproduced live 2026-07-04, cmux 0.64.17), so a
claude-harness firstmate inside a cmux tab has no CMUX_WORKSPACE_ID.
fm_backend_detect now falls back - macOS-only, only when the primary marker
is absent - to __CFBundleIdentifier=com.cmuxterm.app and then a process
ancestry walk resolved by bundle id (lsappinfo) plus a bundle-shaped ps comm
match. Innermost-first ordering is unchanged and absorbs the
tmux-inside-cmux bundle-id false positive; the auto-detect NOTICE names the
winning fallback signal.

Workstream B: the five socketControlMode values were traced through cmux
source (commit 9c91710e3f58): off/cmuxOnly can never admit an external CLI,
automation admits same-user clients with no secret (0600 socket only),
password needs the auth handshake, allowAll opens the socket to every local
user (0666). Automation mode is now the documented recommendation; the
adapter's refusals name every viable mode, classify Invalid password as
unauth, and the launch-timeout message names the off-mode possibility.

Docs carry the wrapper-strip empirical record, the fallback contract and
authority split, and the full mode matrix with rationale; tests cover the
new detection paths, the nested false positive, and the refusal wording.

* no-mistakes(review): Document cmux fallback detection

* no-mistakes(review): Update cmux architecture docs

* no-mistakes(document): Align cmux backend docs

* fix(backends): scope zellij tabs by firstmate home (#252)

* fix(backends): home-scope zellij tab titles to close cross-home collision gap

Zellij's one shared "firstmate" session has no per-home split and enforces
no tab-name uniqueness, so two firstmate homes with colliding task ids could
send/peek/close each other's tabs - the same gap a no-mistakes review gate
caught for cmux (docs/cmux-backend.md). Ports that fix: every new tab is
created with a home-scoped title (fm-<home-label>-<id>), and every
list/find/recover/kill path scopes matches to this home's own tag. A tab
spawned before this change still matches via its old untagged bare title,
but only when unambiguous - two live tabs sharing a bare title refuse rather
than guessing which one is ours.

Factors the home-label/hash derivation shared with cmux into
bin/fm-backend-hometag-lib.sh so the two adapters can't drift.

* no-mistakes(review): Fix zellij child teardown home tag

* no-mistakes(review): Fix zellij teardown and selector scoping

* no-mistakes(document): Sync zellij home-scope docs

* fix: sync project clones after merged PR wakes (#293)

* fix(fleet-sync): auto-sync on merged-PR wake, accept project name

fm-fleet-sync.sh's single-project form failed on a bare project name
("not a directory"), forcing hand-typed full paths (4 manual runs in
one incident). It now resolves a bare name or projects/<name> against
the home's projects dir.

AGENTS.md now encodes the trigger: a wake whose status reports a
merged PR for a project cloned in this home runs fleet-sync for that
project as part of handling the wake, so a secondmate-reported merge
does not leave the primary's clone stale until the next session start
or teardown.

* no-mistakes(review): Fix fleet-sync project name shadowing

* no-mistakes(document): sync fleet-sync docs

* fix: canonicalize spawn worktree path checks (#294)

* fix(spawn): canonicalize worktree-isolation guard against symlinked project prefixes

fm-spawn.sh compared a logical PROJ_ABS against the physically-resolved
pane cwd every backend reports, so a project reached through a symlinked
prefix (e.g. macOS's /tmp -> /private/tmp) could trip the isolation
guard's false refusal before treehouse ever moved the pane. Canonicalize
once into PROJ_ABS_REAL and compare against that everywhere instead.

* no-mistakes(review): Canonicalize spawn cwd comparisons

* no-mistakes(document): Refresh symlinked spawn docs

* docs: add Orca operator skill (#276)

* docs: add Orca operator skill

* no-mistakes(document): Document Orca checklist

---------

Co-authored-by: Stephen Brouhard <vesta@stephens-macbook-air.tail2122af.ts.net>

* fix: surface green PRs during CI monitoring (#297)

* fix(crew-state): detect green-PR CI monitoring, escalate repeat wedges

fm-crew-state.sh's ci step never distinguishes "still waiting on checks"
from "checks green, waiting on merge" via axi status alone, since a repo
that defers merge to the captain keeps the ci step at status=running for
the whole monitor phase. Read the ci step's own log tail (axi logs) for
the checks-passed marker and surface done instead of a false "validating
(running)" - verified against the real PR #252 run's ci.log.

The watcher's wedge timer can re-escalate the same stale pane forever
without ever signaling that it is a repeat; track a per-pane consecutive
escalation count and add a demand-deep-inspection marker to the wake
payload once it crosses a threshold, so the supervisor can no longer
dismiss each one as an isolated, still-validating pane.

Also clarify the ship-brief's checks-green line: it is owed at the
CI-ready return point, not after the background monitor-until-merge
loop finishes.

* no-mistakes(review): Captain, distinguish pending no-checks CI marker

* no-mistakes(review): Harden CI relapse handling

* no-mistakes(review): Block stale done during fixing

* no-mistakes(review): Captain, tighten CI status gating

* no-mistakes(review): Captain, harden stale CI green handling

* no-mistakes(review): Captain, recognize ranged CI rearm markers

* no-mistakes(document): Sync crew-state supervision docs

* fix(teardown): recover provably stale git index locks (#296)

* fix(teardown): recover from a stale worktree git index.lock

A crew process killed mid-git-operation can leave a stale
.git/worktrees/<wt>/index.lock behind, making fm-teardown.sh's
`treehouse return --force` fail closed. On that failure, retry once
after a short wait (the owning process may be exiting), then remove
the lock and retry once more only when it is provably stale: old
enough by mtime and lsof shows no live holder on the lock or the
worktree itself. A lock that isn't provably stale is left in place and
the original…
kostas-nt added a commit to kostas-nt/firstmate that referenced this pull request Jul 8, 2026
* feat: add grok crewmate harness support (#143)

* feat(harness): add grok (Grok Build) as a verified crewmate adapter

Empirically verified against grok 0.2.73 and encoded across the machinery:

- fm-harness.sh: detect grok via GROK_AGENT=1 env marker (grok does not set
  CLAUDECODE) and `grok` command-name ancestry.
- fm-spawn.sh: grok launch template (`grok --always-approve "$(cat BRIEF)"`,
  fully autonomous, no permission gate) and a turn-end Stop hook. grok only
  loads project hooks after a manual folder-trust grant, so the hook is a
  single firstmate-owned global hook (~/.grok/hooks/fm-turn-end.json, always
  trusted) that is a guarded no-op unless the workspace holds a per-task
  .fm-grok-turnend pointer; fm-spawn drops that gitignored pointer naming
  state/<id>.turn-ended. Hook stays outside the worktree, needs no trust grant.
- fm-watch.sh + fm-tmux-lib.sh: grok busy signature `Ctrl+c:cancel` (the
  mid-turn cancel hint; ASCII, present iff a turn runs).
- harness-adapters skill: grok facts section (busy, exit=Ctrl+Q x2,
  interrupt=Ctrl+C, skill invocation /<skill>, resume) and /no-mistakes form.

Gating question confirmed: grok invokes /no-mistakes and drives a real
no-mistakes axi run, so grok is usable for no-mistakes-mode tasks. End-to-end
verified through fm-spawn: autonomous launch past the dir picker into the
worktree, brief processed, busy->idle and turn-end signal detected, fm-send
steer lands, clean Ctrl+Q exit and teardown. config/crew-harness is left
unchanged; this only makes grok available as a verified option.

* no-mistakes(review): Captain, harden Grok hook lifecycle

* no-mistakes(review): Captain, make Grok harness test executable

* no-mistakes(review): Captain, bound Grok pointer reads

* no-mistakes(test): Captain, harden crew-state and watcher-lock timing

* no-mistakes(document): Document Grok harness support

* feat(harness): split secondmate harness configuration (#144)

* feat(harness): split secondmate harness and inherit primary config into secondmate homes

Add config/secondmate-harness so secondmates can run on a different adapter
than crewmates. fm-harness.sh gains a `secondmate` mode resolving the chain
config/secondmate-harness -> config/crew-harness -> own; `crew` mode is
unchanged. fm-spawn resolves a --secondmate launch through that mode (durable:
every respawn re-resolves), while an explicit per-spawn harness arg still wins
and the unverified-adapter guard still holds.

Add a generic, extensible inheritable-config mechanism (fm-config-inherit-lib.sh)
that pushes the primary's declared LOCAL config into each secondmate home's
config/ at secondmate spawn and on the bootstrap secondmate sweep. Exactly one
item is wired today: config/crew-harness, so a secondmate's own crewmates use
the primary's setting. Primary-authoritative (re-pushed every convergence,
mirrors absence); config/secondmate-harness is deliberately not inherited since
secondmates never spawn secondmates. config/ is gitignored, so this is a copy
separate from the tracked-files fast-forward.

Update AGENTS.md (layout, bootstrap, harness, spawn), the harness-adapters
skill, docs/scripts.md, and .gitignore. New tests cover secondmate resolution
and fallback, spawn/respawn honoring config/secondmate-harness, config
propagation on spawn and sweep, the unverified-adapter guard, and backward
compatibility.

* no-mistakes(review): Surface inherited config propagation failures

* no-mistakes(review): Harden inherited config propagation

* no-mistakes(review): Document literal harness inheritance requirement

* no-mistakes(document): Document secondmate harness config

* feat(backlog): default backlog operations to tasks-axi (#145)

* feat(backlog): default to tasks-axi backend

* no-mistakes(document): Sync backlog backend docs

* fix(spawn): set per-task GOTMPDIR so interrupted Go builds don't leak /tmp (#36)

* fix(spawn): set per-task GOTMPDIR so interrupted Go builds don't leak /tmp

Go's GOTMPDIR is unset, so every go build/test creates numbered /tmp/go-build*
dirs. Go cleans them on a clean exit but LEAVES THEM when interrupted (signal,
timeout, OOM, full disk), accumulating and filling the disk over time.

Give each task its own temp root at /tmp/fm-<id>/ with Go's build temp nested at
gotmp/. fm-spawn creates the dir (Go won't mkdir GOTMPDIR), exports GOTMPDIR into
the crewmate pane so the agent and child processes inherit it, and records
tasktmp= in meta. fm-teardown reads tasktmp= and removes the whole root on
cleanup, deterministically.

GOTMPDIR (not TMPDIR) is the targeted knob: TMPDIR is too broad (affects every
program's temp). The nested root is extensible: other per-task temp can live
under /tmp/fm-<id>/ later.

Backward compat: tasks spawned before this change have no tasktmp= in meta;
teardown tolerates the empty value as a no-op. The daily fm-disk-cleanup.sh cron
remains a safety net for any pre-fix stray dirs.

* fix(tests): silence SC2016 for literal grep -F patterns in fm-gotmp test

The structural grep -F assertions deliberately match literal $TASK_TMP in the
fm-spawn source; add per-line shellcheck disable=SC2016 (the codebase's existing
pattern, e.g. bin/fm-spawn.sh) so CI lint passes.

* no-mistakes(document): docs: document tasktmp= meta field for per-task GOTMPDIR

---------

Co-authored-by: e-jung <8334081+e-jung@users.noreply.github.com>

* fix: accept landed squash-merged PR heads (#149)

* fix(teardown): accept landed squash-merge PR heads

* no-mistakes(document): Document teardown landing behavior

* no-mistakes: apply CI fixes

* fix(test): pass explicit teardown git identity

* feat(dispatch): add dynamic crew profiles (#154)

* feat(dispatch): add dynamic crew profiles

* no-mistakes(review): Captain, document dispatch profile inheritance

* no-mistakes(review): Captain, guard stale dispatch inheritance

* no-mistakes(document): Sync dispatch profile docs

* no-mistakes: apply CI fixes

* fix: harden crew dispatch profile enforcement (#159)

* Harden crew dispatch profile enforcement

* no-mistakes(document): Captain, synced crew dispatch docs

* feat: add live secondmate config push (#161)

* feat(config): add live secondmate config push

* no-mistakes(document): Document config push behavior

* no-mistakes(lint): Clean changed shell lint

* no-mistakes: apply CI fixes

* feat: support image attachments in X replies (#162)

* feat(x): add image attachments to reply helpers

* no-mistakes(review): Stream X image replies safely

* no-mistakes(review): Captain, clean X reply temp tracking

* no-mistakes(document): Document X reply image support

* fix(teardown): make landed PR detection robust (#167)

* fix(teardown): make landed-check robust when no pr= was ever recorded

fm-teardown.sh's squash-merge landed-check already falls back to
discovering a merged PR by branch name when state/<id>.meta has no
recorded pr=, but nothing guaranteed pr=/pr_head= actually got
recorded on a yolo-authorized merge - the "checks green" trigger that
normally runs fm-pr-check.sh never fires on repos with no PR CI, so a
merge done via a bare `gh-axi pr merge` silently skips it.

Add bin/fm-pr-merge.sh as the one path for merging a task's PR: it
always runs fm-pr-check.sh first, so pr=/pr_head= land in meta as part
of the merge itself regardless of any CI signal. Document both the
existing branch-name discovery fallback and the new merge path in
AGENTS.md, and add regression coverage for the no-pr=-recorded landed
scenario and for fm-pr-merge.sh's record-then-merge behavior.

* no-mistakes(review): Guard PR merges on task metadata

* no-mistakes(document): Document PR merge wrapper

* no-mistakes: apply CI fixes

* fix: parse PR merge URLs for gh-axi (#168)

* Fix fm-pr-merge.sh to parse PR URLs for gh-axi

gh-axi pr merge expects a PR number and --repo, not a full GitHub URL.
Parse the URL, default to --squash when no merge method is passed, and
fail fast on malformed URLs. Tests cover parsing, defaults, and refusal.

* no-mistakes(review): Harden PR merge validation

* no-mistakes(review): Harden PR merge URL guards

* no-mistakes(document): Document PR merge URL handling

* no-mistakes(lint): Clean shell lint

* feat(bin): pin secondmate model and effort (#180)

* feat: pin secondmate model/effort in config/secondmate-harness

Extend config/secondmate-harness's format to an optional
"<harness> [<model>] [<effort>]" line so a secondmate can be durably
locked to a concrete model/effort in the same file, without adding a
new config file. A bare harness-only file behaves exactly as before.

fm-harness.sh gains secondmate-model/secondmate-effort accessors;
fm-spawn.sh populates MODEL/EFFORT from them on every secondmate spawn
(including respawns) unless the caller passed an explicit --model/--effort.

* no-mistakes(review): Fix secondmate override pin precedence

* no-mistakes(document): Document secondmate harness pins

* feat(bin): add runtime backend interface (#183)

* feat(bin): extract tmux runtime behind a backend interface (P1)

Add bin/fm-backend.sh (selection, meta helpers, selector resolution,
dispatch) and bin/backends/tmux.sh (the tmux adapter), then route
fm-send.sh, fm-peek.sh, fm-watch.sh, fm-spawn.sh, and fm-teardown.sh
through them. Every default tmux command sequence, meta shape, and
printed output stays byte-identical: missing backend= still means
tmux, and a default spawn never writes backend=tmux.

Adds a --backend flag (tmux-only for now) and FM_BACKEND/config/backend
selection, refusing any unimplemented backend loudly. Names the
watcher's poll loop as the default event-source implementation over the
backend's pull primitives, per the herdr-addendum's events-as-the-core-
abstraction direction, without changing its behavior.

Verification: fake-tmux/treehouse old-vs-new command-log conformance
tests for send/peek/spawn/teardown, a real-tmux smoke test for the
adapter, and the full existing suite passing unmodified (bar two
fixture-only additions in fm-gotmp.test.sh for the new sibling
scripts).

* no-mistakes(review): Captain, harden backend baseline resolution

* no-mistakes(review): Captain, ignore and document backend config

* no-mistakes(review): Captain, make backend tests executable

* no-mistakes(document): Sync runtime backend documentation

* feat(bin): add experimental Herdr runtime backend (#186)

* feat(bin): add experimental herdr runtime backend (P2)

Implements bin/backends/herdr.sh (session-provider adapter, D3: treehouse
stays the worktree provider) wired through fm-backend.sh's dispatch, with
--backend herdr / FM_BACKEND=herdr / config/backend selection, a
version/protocol gate at spawn, semantic busy-state detection via herdr's
agent.get (fm-watch.sh and fm-crew-state.sh consult it before falling back to
the existing tmux pane-regex path), and label-based recovery discovery.

Container shape (D4) decided empirically: tab-per-task in one "firstmate"
workspace, mirroring tmux's one-session-many-windows model.

Found and fixed two real herdr v0.7.1 bugs during verification: `pane read
--lines N` returns empty for small N (worked around by over-fetching and
trimming locally), and `pane get`'s cwd field is frozen at pane-creation time
(fixed to read foreground_cwd instead, needed for fm-spawn's worktree-
discovery poll after `treehouse get`). Also fixed a pre-existing bug in
tests/fm-backend.test.sh's old-vs-new fixture that was silently missing
fm-backend.sh/bin/backends/ from the old bin/ shim.

Full empirical verification, the D4 decision evidence, and a real end-to-end
run (spawn/steer/peek/done/merge-local/teardown, including confirming
teardown refuses before the merge) are recorded in docs/herdr-backend.md.
The entire existing tmux conformance suite stays green.

* no-mistakes(review): Fix Herdr supervision recovery gaps

* no-mistakes(review): Captain, fix Herdr stale recovery gaps

* no-mistakes(review): Document Herdr composer primitive candidate

* no-mistakes(review): Captain, harden Herdr stale recovery and tests

* no-mistakes(document): Sync herdr backend docs

* no-mistakes: apply CI fixes

* feat(bin): auto-detect runtime backend (#188)

* feat(bin): auto-detect runtime backend from HERDR_ENV/TMUX markers

fm_backend_name now falls through to runtime auto-detection between
config/backend and the hard tmux default: a firstmate running natively
inside herdr (HERDR_ENV=1) now spawns crewmates into herdr by default,
mirroring how harness detection already works in fm-harness.sh. Nesting
resolves innermost-first (tmux wins over a nested herdr pane). Explicit
--backend/FM_BACKEND/config/backend settings always win over detection.
Selecting herdr via auto-detect prints a loud stderr notice; auto-detecting
tmux stays silent so the unconfigured default path is unchanged.

* no-mistakes(review): Captain, pin tmux tests and backend docs

* no-mistakes(document): Sync backend autodetect docs

* no-mistakes: apply CI fixes

* feat(stow): add operational memory capture (#197)

* feat(stow): add operational-memory learnings convention and /stow skill

Add data/learnings.md as the fleet-local operational-learnings home,
a knowledge-routing table in AGENTS.md, and a user-invocable /stow
skill that sweeps a session for uncaptured durable knowledge and
files it to the right disk home before a reset.

* no-mistakes(review): Fix stow backlog note command

* no-mistakes(document): Document stow memory routing

* fix(tests): protect herdr smoke cleanup from default sessions (#199)

* fix(tests): stop real-herdr smoke tests from ever killing the default session

Both fm-backend-herdr-smoke.test.sh and fm-backend-autodetect-smoke.test.sh
tore down their isolated throwaway HERDR_SESSION via a bare/inline-prefixed
`herdr server stop`, which is unscoped and resolves ambiently. On this herdr
client, that ambient resolution silently falls back to whatever server is
already running instead of the requested session - it killed the captain's
live default herdr server twice in production (2026-07-02), once from each
smoke test's cleanup trap.

Add tests/herdr-test-safety.sh with herdr_safe_stop_and_delete: it uses the
explicit-by-name `herdr session stop/delete <name>` form (never the ambient
`server stop`) and, before that, a read-only hard guard
(herdr_refuse_if_default) that re-queries `herdr session list --json` and
refuses outright if the target is literally "default", not found, or flagged
default:true. Fails closed on any ambiguity. Verified empirically against a
real isolated session: refuses on default/nonexistent/empty names without
ever calling stop, and correctly tears down a genuine isolated session while
leaving the default session's workspace state byte-identical before and
after.

* no-mistakes(review): guard herdr delete with fresh check

* no-mistakes(document): Document herdr smoke cleanup safety

* feat(bin): route herdr secondmates into per-home workspaces (#200)

* feat(bin): give each secondmate its own labeled herdr workspace

Give each secondmate its own labeled herdr workspace, and land crewmates
spawned from a secondmate home in that secondmate's own space, instead of
every firstmate home (primary and all secondmates) sharing one "firstmate"
workspace.

bin/backends/herdr.sh: replace the constant FM_BACKEND_HERDR_WORKSPACE_LABEL
with fm_backend_herdr_workspace_label(), resolved fresh from FM_HOME on every
call. The primary (no .fm-secondmate-home marker) still resolves to
"firstmate" - byte-identical to every pre-existing task's recorded label, no
forced migration. A secondmate home resolves to "firstmate-<secondmate-id>".
Every workspace-scoped path (find/ensure, tab create + duplicate check,
list-live recovery, pane-for-tab) uses this same resolution, so recovery and
duplicate checks stay scoped to each home's own space. Workspace and tab
create now pass --no-focus unconditionally (verified: neither focuses by
default once a workspace exists; --no-focus is defense in depth against the
one bootstrap edge case where the very first workspace in a session auto-
focuses).

Also fixes a session-targeting bug found while verifying this empirically:
HERDR_SESSION (env var, exported or inline-prefixed) is not reliably honored
by herdr 0.7.1 CLI subcommands once another herdr server is already running -
it silently falls back to whatever server IS running. fm_backend_herdr_cli
wraps every herdr invocation with both HERDR_SESSION and a trailing
--session <name> flag (verified to route correctly in every case tried),
fixing this for the whole adapter, not just the new label-scoped calls.

bin/fm-spawn.sh: a --secondmate spawn is launched BY the primary's own
process, whose FM_HOME still names the primary at that point. The herdr case
arm now shadows FM_HOME to the secondmate's own home (PROJ_ABS) for just the
two calls that resolve/create the workspace and tab, restored automatically
afterward (bash's temporary-assignment-before-a-command form works for shell
functions too). A crewmate/scout spawned FROM a secondmate's own fm-spawn.sh
process needs no such glue - its own FM_HOME already names it.

Tests: extended tests/fm-backend-herdr.test.sh (per-home label resolution,
--no-focus, --session flag, workspace-find/list-live scoping) and
tests/fm-backend-herdr-smoke.test.sh (a secondmate-shaped home's workspace
label, list-live scoping, restart stability in the multi-workspace shape).
Added tests/fm-backend-herdr-workspace-per-home-e2e.test.sh: the mandatory
isolated E2E, driving real bin/fm-spawn.sh/fm-teardown.sh - a primary-shaped
home into "firstmate", a --secondmate spawn into its own labeled space, a
crewmate spawned FROM that secondmate-shaped home landing in the same space
(this exact path had never run before), teardown closing only the right tab,
and list-live recovery seeing only each home's own tabs. All ten assertions
passed on the real binary; the default herdr session's own workspace state
was confirmed byte-identical before and after every real-herdr test run in
this change.

docs/herdr-backend.md: rewrote "Task container shape" for the workspace-per-
home design (label derivation, the --secondmate FM_HOME-shadow wrinkle, focus
behavior, label-collision/adopt-don't-duplicate semantics, no-forced-
migration), added "Session targeting: the --session flag, not HERDR_SESSION
alone", extended "ID stability" to the multi-workspace shape, and documented
the new E2E test.

* no-mistakes(review): Clarify herdr focus docs

* no-mistakes(review): Captain, clarify herdr server session docs

* no-mistakes(document): Document herdr per-home spaces

* fix(backends): rename Herdr secondmate workspace labels (#203)

* Rename herdr secondmate workspace prefix to 2ndmate-

The primary home keeps the firstmate label; secondmate homes now
resolve to 2ndmate-<id> so the herdr spaces sidebar is unambiguous.
Tests and docs updated; pre-rename workspaces can be aligned with
herdr workspace rename.

* no-mistakes(review): Clarify herdr workspace migration behavior

* no-mistakes(document): Herdr docs label alignment

* feat(bin): add unified session start digest (#201)

* feat(bin): collapse session start into one command

Add bin/fm-session-start.sh, composing fm-lock.sh, fm-bootstrap.sh, and
fm-wake-drain.sh into one ordered digest (lock, bootstrap diagnostics,
wake queue, context files, fleet state) instead of six-plus separate
turns. Lock now runs before bootstrap's mutating sweeps, closing a race
where a second concurrent session could mutate shared state before
discovering the lock was held. A lock refusal prints a loud read-only
banner, skips every mutating step via a new opt-in
FM_BOOTSTRAP_DETECT_ONLY flag on fm-bootstrap.sh, and still completes
the read-only-safe digest.

Add fm_backend_target_exists to fm-backend.sh as a shared, read-only,
never-side-effecting per-task endpoint-liveness primitive for both the
tmux and herdr backends.

Rewrite AGENTS.md sections 3 and 5 around the single command and add
tests/fm-session-start.test.sh.

* no-mistakes(review): Harden session-start read-only guidance

* no-mistakes(review): Suppress read-only tangle repair guidance

* no-mistakes(review): Include orphan status logs

* no-mistakes(review): Captain: make session-start test executable

* no-mistakes(review): Captain: clarify status tail guidance

* no-mistakes(document): Sync session-start docs

* no-mistakes(lint): Shell lint clean

* fix(tests): avoid shellcheck boolean chain

* fix(bin): corroborate herdr idle crew state (#207)

* fix(bin): corroborate herdr idle agent_status with the pane's own text

crew_pane_is_busy trusted a bare `idle` verdict from herdr's agent.get
outright, skipping the tail-regex corroboration unknown already gets.
agent.get reports generation state only (working while the model streams
a turn), so it reads idle for a crew blocked on its own long foreground
no-mistakes run - even though the pane still shows the busy banner the
whole time. Combined with the no-mistakes CLI's 10-run attribution cap,
this made a genuinely working herdr crew read as not provably working,
triggering an immediate stale wake instead of absorb-then-escalate.

* no-mistakes(document): Align herdr busy-state docs

* fix(backends): reuse the herdr firstmate workspace instead of leaking one per spawn (#202)

* fix(backends): reuse the herdr firstmate workspace instead of leaking one per spawn

Every herdr-backed crewmate left an orphaned `firstmate`-labelled workspace
behind, one per task, because `fm_backend_herdr_workspace_find` never matched
the existing workspace: its jq filter used `--arg label ... $label`, and
`label` is a reserved keyword in jq (label/break), so the filter was a compile
error. The error was swallowed by `2>/dev/null`, the find returned empty on
every call, and `workspace_ensure` took the create path each spawn, minting a
fresh workspace. The same collision silently disabled the create-task
duplicate-label check and the bare-selector tab lookup.

Rename the jq variable to `$want` in all three affected filters so reuse,
duplicate detection, and bare-selector lookup work. With reuse restored the
single `firstmate` workspace is persistent (like tmux's session) and teardown
correctly leaves it in place, closing only the task's pane/tab.

Also prune the default tab (label "1") herdr auto-creates inside a freshly
created workspace, best-effort, so the workspace holds only real task tabs.

Add stateful-fake-CLI tests that replay repeated spawn/teardown cycles and
assert one reused workspace, zero orphans, the default tab pruned, and
`workspace create` invoked exactly once. Verified against the real herdr binary
too: the pre-fix code failed the smoke idempotency check (minted wE then wF in
an isolated session); the fix passes it.

Document the workspace lifecycle, the jq-keyword pitfall, the default-tab
prune, and the project-labelled-workspace anomaly (not adapter-created) in
docs/herdr-backend.md.

* docs(herdr): trim workspace-lifecycle addition to current-state facts

The docs/herdr-backend.md convention documents current behavior, not
history - narrative belongs in the PR/commit message. Tightened the
workspace-leak and default-tab-prune write-up down to the operative
facts (the jq reserved-keyword guard, when pruning is safe, and the
persistence caveat), and corrected the CLI-facts table rows to match
the corrected prune timing.

* fix(backends): defer herdr default-tab prune until a real task tab exists

Closing a workspace's LAST tab deletes the whole workspace on real
herdr (verified). Pruning the auto-created default tab right after
workspace create closed the workspace's only tab at that point,
destroying the just-created workspace on every single spawn instead of
reusing it - the fake-CLI unit tests didn't model this real-herdr
behavior, so they passed while the real-herdr smoke test failed with
"container_ensure is not idempotent".

Move the prune into fm_backend_herdr_create_task, right after the
first real task tab is added to a freshly created workspace, when
closing the default tab alongside it is safe. Update the fake-CLI unit
test to match the corrected timing.

Also fix a smoke-test-only bug this surfaced: the test's second
create_task call reused a $CONTAINER captured before the first task
was killed, rather than re-running container_ensure like real
fm-spawn.sh always does immediately before every create_task call - so
once the workspace (correctly) disappeared after its last tab closed,
the stale reference no longer named a live workspace.

* test(backends): guard against jq --arg names colliding with jq keywords

Regression guard for the workspace-leak bug this PR fixes: a jq
--arg/--argjson variable named after a jq reserved keyword (e.g.
label) is a compile error on jq <= 1.6, and this adapter's
2>/dev/null silently turns that into an empty result instead of a
visible failure. Greps bin/ for the pattern so a future violation
fails loudly here instead of silently misbehaving on an older jq.

* docs(herdr): fix per-home staleness and drop contributor-specific example

The workspace-lifecycle write-up hardcoded "the firstmate workspace"
as if the label were always the fixed constant, stale against the
per-home labeling documented earlier in this file (primary: firstmate,
secondmate: 2ndmate-<id>). Rephrased per-home throughout, and pointed
the "workspace this adapter did not derive" case at the existing
Label-derivation section instead of a separate anomaly writeup.

Dropped the "Anomaly: a workspace labelled with a project name"
section - the python-teslemetry-stream example was a contributor's own
environment, not current adapter fact, and it repeated a now-incorrect
FM_BACKEND_HERDR_WORKSPACE_LABEL constant claim. Replaced with one
generic sentence already covered by the corrected wording above.

Also fixed the jq-reserved-keyword guard test's file reference, which
named tests/fm-backend.test.sh when the test actually lives in
tests/fm-backend-herdr.test.sh.

---------

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

* fix(backends): make Herdr default-tab pruning safe (#215)

* fix(backends): make the herdr default-tab prune provably safe

The default-tab prune could close a pane holding a LIVE agent: an
ADOPTED workspace (found pre-existing by label match) was pruned using
the same tab-count/label heuristic as a freshly created one, and herdr
derives a workspace's displayed label from its cwd basename when no
explicit --label is given. A captain launching herdr directly inside a
directory named "firstmate" produces a workspace that looks identical,
by label alone, to firstmate's own container - so the very next spawn
adopted the captain's own live workspace and closed their live pane
27ms after creating its task tab (2026-07-02 incident).

The fix is structural: fm_backend_herdr_workspace_ensure now captures
the seeded default tab's id straight from its own `workspace create`
response, only when it just created the workspace. That id threads
through fm_backend_herdr_container_ensure to fm_backend_herdr_create_task,
which is the only function allowed to prune it - an adopted workspace's
caller always passes an empty seeded-tab-id, so create_task never
re-derives "prunable" from a tab's label or count. Defense in depth:
the prune also refuses a tab whose pane reports a working agent.

Covered by new unit tests (adopted-never-prunes, created-prunes-exactly,
the exact label-collision incident shape) and a new isolated real-herdr
E2E test that reproduces the incident against the pre-fix code and shows
it fixed, plus the normal happy path.

* no-mistakes(document): Sync herdr prune docs

* no-mistakes: apply CI fixes

* fix(brief): remove apostrophe breaking bash -n and guard bash 3.2 set -u in spawn (#173)

* fix(brief): remove apostrophe that broke bash -n on fm-brief.sh

The no-mistakes DOD heredoc, built via VAR=$(cat <<EOF ... EOF), had an
unescaped apostrophe in "no-mistakes' own guidance". Nesting a heredoc
inside $(...) makes bash track quote state through the body, so the lone
apostrophe broke parsing of the rest of the script (bin/fm-brief.sh:211),
making the default no-mistakes ship path fail outright. Audited the other
two $(cat <<EOF...EOF) blocks (direct-PR, local-only) for the same class
of bug; none found. Added tests/fm-brief.test.sh as a regression guard.

* no-mistakes(test): fix(spawn): guard empty shared_args under bash 3.2 set -u

* no-mistakes(document): docs(contributing): list tests/fm-brief.test.sh in the test suite inventory

* fix(test): silence shellcheck SC2034/SC2100 in fm-brief.test.sh

Drop the unused out= capture (redirect to /dev/null instead) and quote
the id= assignment so shellcheck stops reading the hyphenated id value
as an arithmetic expression.

* feat: add experimental zellij runtime backend (#217)

* feat(backends): add experimental zellij runtime backend (P3)

Implements bin/backends/zellij.sh on the P1 dispatcher + P2 herdr precedent:
one zellij session, one tab per task, treehouse stays the worktree provider.
Wired through fm-backend.sh/fm-spawn.sh so fm-send/fm-peek/fm-watch/
fm-crew-state/fm-teardown work generically with zero changes to those scripts.

Empirically verified against real zellij 0.44.0: every "gaps to verify" item
from the design report, plus real findings the report missed - new-tab always
steals focus (mitigated with a restore call), zellij action always exits 0
even against a dead target, every pane op needs an explicit --pane-id, and
pane_cwd never tracks a subshell's own cd (treehouse get's exact shape) so
worktree-path discovery uses an active pwd-probe instead of passive JSON
polling. Findings and the full real-CLI evidence log are in
docs/zellij-backend.md.

Full real E2E cycle passed: spawn a real claude crewmate, accept its trust
dialog, steer it, receive done, confirm teardown refuses before merge, merge
local-only, confirm teardown then succeeds and the zellij tab is gone - all
in a scratch FM_HOME against a uniquely-named isolated zellij session, never
touching the real "firstmate" session or the live fleet.

Existing tmux and herdr conformance suites stay green; the two P1-era tests
asserting zellij was unimplemented now assert that of orca instead.

* no-mistakes(review): captain, guard zellij paste payloads

* no-mistakes(review): Captain, guard zellij pane readiness

* no-mistakes(review): Captain, harden zellij dead-target handling

* no-mistakes(review): Captain, harden zellij teardown and tests

* no-mistakes(review): Captain, harden zellij target validation

* no-mistakes(document): Document zellij backend

* docs: document Orca backend adapter contract (#209)

* docs: specify Orca backend adapter contract

* docs: require Orca window target alias

* no-mistakes(test): Captain: handle empty arrays under nounset

* no-mistakes(document): Document Orca backend proposal

---------

Co-authored-by: Stephen Brouhard <vesta@stephens-macbook-air.tail2122af.ts.net>

* feat(backends): add Orca primitive backend support (#210)

* feat(backends): add Orca adapter primitives

* no-mistakes(review): Gate Orca from task spawning

* no-mistakes(document): Document Orca backend limits

* fix(backends): stop mapping Orca Escape to interrupt

* no-mistakes(document): Document Orca primitive key support

* no-mistakes: apply CI fixes

* fix: drop CI-gate rewrite and normalize shared array-guard hunks

---------

Co-authored-by: Stephen Brouhard <vesta@stephens-macbook-air.tail2122af.ts.net>

* fix(backends): submit herdr slash commands reliably (#223)

* fix(backends): herdr slash-command submit verification false-positives on popup autocomplete

Two grok/herdr crewmates left /no-mistakes fully typed but unsubmitted for
minutes while fm-send exited 0. Live-reproduced against real grok 0.2.82:
the herdr adapter's submit verification declared success on ANY pane content
change after Enter, but an argument-taking slash command's first Enter only
closes the popup and expands the composer into an argument-hint placeholder
(/compact -> /compact compaction instructions) rather than submitting - a
real, visible change that isn't a submission. A second Enter is required.

fm_backend_herdr_composer_state replaces the delta check with a structural
read of the composer's own row (located by border-glyph shape, since herdr
exposes no cursor-row primitive), mirroring what cursor_y gives the tmux
adapter. A popup-close-with-placeholder-fill still reads pending, so the
retry loop now correctly sends the needed second Enter instead of stopping
early. The tmux backend was unaffected (its cursor-row read already handled
this correctly, verified side by side against the same live repro).

* no-mistakes(document): Sync herdr submit docs

* feat(skills): publish public stow skill with internal-skill hiding (#221)

* feat(skills): hide agent-only skills from installer discovery, add public stow

Mark every .agents/skills/* skill metadata.internal: true so the
skills.sh installer (npx skills add) hides them from discovery - all
assume a live firstmate home and are meaningless elsewhere. This is
inert to firstmate's own harness skill loader.

Add a new, fully standalone skills/stow for non-firstmate users: sweep
a conversation for durable knowledge and file it into whatever notes
convention the host project/user already has, asking once and
remembering the answer when ambiguous. No shared code with the
internal stow by design.

Document the two-tier layout in README and CONTRIBUTING.

* no-mistakes(review): Tighten public stow tracker routing

* no-mistakes(document): Sync skill docs

* fix(skills): make public stow's undone-next-steps routing local-first

Supersede the earlier ask-before-tracker-write tightening: the
standalone stow no longer treats an issue tracker as a routing option
at all based on inference (git remote, .github/ presence, etc).
Undone next steps always land in a local file by default - an
existing TODO/BACKLOG/NOTES file, or a freshly created local scratch
file otherwise. A tracker (or any other external system) is only ever
used when the user has explicitly said so, this session or as a
previously recorded standing preference.

* no-mistakes(document): Sync skill documentation

* feat(skills): clarify public stow resume and fallback routing (#225)

* feat(skills): resume pointer, routing tiers, default notes file for public stow

Adds a copy-pasteable resume pointer to the safe-to-end verdict so a
new session can pick the work back up cold, states the explicit three-tier
routing priority (explicit instruction > existing local convention > default
NOTES.md fallback) instead of leaving it implicit, and names NOTES.md as the
top-level discoverable default instead of letting each agent improvise a
location. Also removes a leftover internal-tooling word from the maintainer
comment so the public file carries zero internal vocabulary.

* fix(skills): make the public stow default fallback private and gitignored

Supersedes the earlier NOTES.md/ask-once split: the tier-3 default fallback
(no existing local convention fits) is now .stow-notes.md, gitignored so it
never lands unprompted in a shared/committed file. Because it's private, it's
safe for every finding-kind including user preferences, so the separate
ask-once carve-out for personal material is no longer needed there. Tier 2
(an already-established tracked convention) is unchanged and is the only
tier that still writes into a shared file. Step 7's resume pointer now flags
when notes landed in the private fallback and that they can be promoted into
a shared file later.

* fix(skills): split the public stow private fallback by scope, use git exclude

Supersedes the single .stow-notes.md fallback: user preferences (cross-project
by nature) now default to a host-local ~/.stow/notes.md instead of being
siloed into one project's repo. Project-scoped findings still default to
.stow-notes.md at the project root, but it's now kept out of git via the
local-only .git/info/exclude instead of the tracked .gitignore, so the
fallback is truly zero-shared-footprint: nothing lands in a tracked file and
nothing is left for the user to review or commit.

* fix(skills): keep the public stow private fallback sandbox-safe (current dir only)

Supersedes the home-file split: a home-directory path fails for agents
sandboxed to their current working directory, so the tier-3 default now
stays a single .stow-notes.md at the project root for every finding-kind,
including user preferences. Tier 2's user-level memory file is now framed
as a bonus when accessible, never assumed or required. Step 7 gains a
caveat when a preference lands in the project-local fallback: it applies to
this project only, and the user can copy it into their own global memory
file if they want it to follow them everywhere.

* fix(skills): use a current-directory .gitignore for the public stow fallback

Supersedes .git/info/exclude: that mechanism resolves outside the working
directory in a linked worktree, breaking the sandbox-safety guarantee for
exactly the setup this fleet uses everywhere. Switch to an ordinary
.gitignore file in the current directory instead - always in-directory
regardless of worktree layout - creating or appending a .stow-notes.md
line, left uncommitted for the user. If that write itself fails, the skill
still creates .stow-notes.md and tells the user to ignore it manually
rather than blocking. Also scopes the "never writes outside the current
directory" guarantee precisely to tier 3: tier 2 is exempted since it only
targets a destination the user's own existing convention already
established, which can legitimately be a user-level file outside the
project.

* no-mistakes(review): Clarify stow routing precedence

* no-mistakes(review): Clarify stow fallback metadata boundary

* no-mistakes(review): Guard tracked stow fallback

* no-mistakes(review): Clarify stow fallback verdict

* no-mistakes(document): Sync stow skill docs

* no-mistakes(lint): Public skill lint cleanup

* docs: clarify note hygiene guidance (#226)

* docs: add note-hygiene rule to backlog format section

Backlog and task notes accumulate volatile specifics that drift and
mislead; capture the general principle so every firstmate user avoids
trusting a stale note over the authoritative source.

* no-mistakes(review): Clarify note hygiene schema exemptions

* no-mistakes(document): Clarify note-hygiene docs

* revert: drop out-of-scope stow/architecture doc edits

Keep this PR's diff scoped to the AGENTS.md note-hygiene addition only;
skills/stow/SKILL.md, .agents/skills/stow/SKILL.md, and docs/architecture.md
are under separate close review and must not change out of band here.

This reverts commit ed2d73205b01e24ba8ffd40c523a24993f6eb1a5.

* feat(backends): add Orca task lifecycle support (#228)

* feat(backends): add Orca task lifecycle support

* fix(backends): harden Orca spawn lifecycle

* no-mistakes(review): Fix Orca lifecycle cleanup gaps

* no-mistakes(review): Release Orca worktrees without paths

* no-mistakes(review): Guard Orca spawn abort cleanup

* no-mistakes(review): Allow partial Orca child cleanup

* no-mistakes(review): Fix Orca selector and cleanup leaks

* no-mistakes(review): Preserve pathless Orca cleanup metadata

* no-mistakes(review): Harden Orca spawn and teardown lifecycle

* no-mistakes(review): Enforce Orca scout report gate

* no-mistakes(review): Harden Orca teardown path validation

* no-mistakes(review): Harden Orca capture errors

* no-mistakes(review): Harden Orca JSON cleanup validation

* no-mistakes(test): Fix zellij scout teardown fixture

* no-mistakes(document): Document Orca lifecycle support

* Harden Orca runtime and submit verification

* no-mistakes(review): Captain, preserve Orca current-tail verification

* no-mistakes(document): Sync Orca lifecycle docs

* no-mistakes(lint): Captain, silence deliberate ShellCheck

---------

Co-authored-by: Stephen Brouhard <vesta@stephens-macbook-air.tail2122af.ts.net>

* fix: use PR head for review diffs (#229)

* Fix fm-review-diff to compare PR head when pr= is recorded

After no-mistakes fix rounds push to the open PR, the crewmate worktree
branch can lag the authoritative PR head. When meta records pr=, resolve
the compare ref from reachable pr_head= or refs/pull/<n>/head before
diffing against the fetched authoritative base; fall back to the local
branch with a loud warning when the PR head cannot be resolved.

Add behavior tests for pr_head resolution, fetch, unchanged no-pr path,
and unreachable-PR fallback.

* no-mistakes(review): Make review-diff test executable

* no-mistakes(document): Document PR-head review diffs

* no-mistakes: apply CI fixes

* fix: make backend matching shell-portable (#230)

* Make fm-backend.sh backend-name matching shell-portable for zsh

Replace word-split-dependent for-loops in fm_backend_is_known() and
fm_backend_validate_spawn() with case-based membership tests so sourcing
the library from zsh no longer falsely rejects known backends.

Add zsh/bash regression coverage in tests/fm-backend.test.sh.

* no-mistakes(review): Fix zsh backend loading and validation

* no-mistakes(document): Document backend membership portability

* fix(backends): make herdr respawn idempotent (#231)

* fix(backends): make herdr respawn idempotent against restored-layout husks

herdr persists and restores its session layout (workspaces/tabs/panes)
across a server restart, so a restored fm-<id> task tab comes back a
husk - a dead pane, or a plain agent-less shell - which fm-spawn.sh's
duplicate-tab guard refused unconditionally, forcing manual pane closes
after every restart.

fm_backend_herdr_create_task now classifies an existing same-labeled
tab's pane conservatively (dead/no-agent/live/unknown) and
closes-and-replaces only a confirmed husk, always creating the
replacement tab before closing the old one so a husk that is a
workspace's only tab is never at risk of taking the whole workspace
down with it. A genuinely live agent, or anything not confidently
classifiable, still refuses exactly as before.

* no-mistakes(review): Harden herdr duplicate respawn guard

* no-mistakes(review): Enforce herdr husk cleanup postcondition

* no-mistakes(document): sync herdr respawn docs

* no-mistakes: apply CI fixes

* fix: absorb stale wakes during active validation (#233)

* fix(watcher): stop stale_is_terminal from ignoring an active run-step

A crewmate's status log gets no new entry once firstmate hands it to a
no-mistakes validation (the sparse status-reporting contract), so the
log's last line can stay a pre-validation "done:" (or needs-decision/
blocked) leftover for the run's entire duration. fm-watch.sh's
stale_is_terminal only reads that raw last line - it has no run-step
awareness - so it kept surfacing a stale pane as immediately terminal
every time it went quiet for two polls, no matter how actively the
pipeline was validating (confirmed live against fm-herdr-respawn-idem,
whose status log's last line is literally "done: ..." while its
no-mistakes run-step reads "validating (running)"). crew_is_provably_working
now gets a chance to override a stale captain-relevant log line on a
new stale hash, exactly as it already did for a non-captain-relevant one.

Also fixed a separate, independently-confirmed dead code path in
fm-crew-state.sh: its cross-branch run-attribution fallback shelled out
to `no-mistakes axi` (bare) expecting a runs[N]{...} TOON table that the
real CLI (v1.32.2) never emits - verified the axi surface exposes only
abort/logs/respond/run/status. Replaced it with the real top-level
`no-mistakes runs` listing.

* no-mistakes(document): Sync watcher stale docs

* no-mistakes(lint): Fix stale fake run-list variables

* docs: clarify no-mistakes evidence commit handling (#232)

* docs: no-mistakes evidence commits in crew branches are intentional

* no-mistakes(review): Scope evidence guidance to project repos

* no-mistakes(document): Document evidence commit policy

* fix: tighten Orca backend parsing (#237)

* fix: tighten orca parser coverage

* no-mistakes(document): Sync Orca backend docs

* no-mistakes(lint): Captain, lint clean

---------

Co-authored-by: Stephen Brouhard <vesta@stephens-macbook-air.tail2122af.ts.net>

* docs: add backend setup guides and slim README (#238)

* docs: make README pointer-first, add per-backend setup guides

Trim the Quick Start and How It Works walls of prose down to overview plus
pointers, relocating every removed sentence's content into docs/architecture.md,
docs/configuration.md, or the relevant backend doc. Add docs/tmux-backend.md as
the reference-backend setup guide, and add a Setup section to each experimental
backend doc (herdr, zellij, Orca) covering prerequisites, selection, first run,
watching/attaching, verification, and limitations. Record the README convention
in CONTRIBUTING.md.

* no-mistakes(review): Clarify backend setup docs

* no-mistakes(review): Clarify tmux secondmate support

* no-mistakes(document): Align backend documentation

* docs: stop telling users to run fm-spawn.sh for backend selection

fm-spawn.sh is firstmate-internal; a user never runs it directly. Rephrase
every user-facing backend-selection sentence across the tmux/herdr/zellij/orca
guides and docs/configuration.md to present the actual user mechanisms: a
local config/backend file, FM_BACKEND at launch, or telling the first mate in
chat. Internal-reference mentions of the --backend flag (fm-spawn.sh usage
notes, test coverage lists) are left as mechanics, not user instructions.

* Document herdr dual license in docs and install hint (#239)

Add AGPL-3.0-or-later/commercial licensing note to herdr-backend Setup
and the missing-binary error in bin/backends/herdr.sh.

* feat: support multiple X-mode follow-ups (#241)

* feat(x-mode): raise X follow-up cap to 3 within a 7-day window

Matches the relay's parallel contract change: fm-x-link.sh now records a
follow-up counter (with --carry-count to preserve it across a re-link onto
a successor task), fm-x-followup.sh posts up to three follow-ups per
mention within a 7-day window instead of one within 24h, clearing the
link on --final, cap exhaustion, window lapse, or a distinguishable relay
rejection (fm-x-reply.sh HTTP 409 -> exit 9) rather than treating that
rejection as a retryable failure. AGENTS.md and the fmx-respond skill are
updated to keep usage disciplined: spend follow-ups only on genuine
milestones, always finish with --final.

* no-mistakes(review): Update follow-up dry-run docs

* no-mistakes(review): Captain: refresh X follow-up docs

* no-mistakes(review): Captain: harden X follow-up relinks

* no-mistakes(review): Captain: harden follow-up state persistence

* no-mistakes(document): Document X follow-up carryover

* feat(backends): add experimental cmux runtime backend (#246)

* feat(backends): add cmux runtime backend (experimental)

Session-provider-only adapter for cmux (bin/backends/cmux.sh), mirroring
zellij/herdr structurally, wired into fm-backend.sh and fm-spawn.sh with
--secondmate refused for now. Verified against the real cmux 0.64.17 app:
send does not auto-submit, cwd is creation-time-frozen (zellij-shape,
pwd-marker-probe workaround), close-surface refuses on a workspace's last
surface (falls back to close-workspace), workspace ids do not survive a
relaunch, and the control socket defaults to cmuxOnly access (requires a
one-time password-mode setup, documented in docs/cmux-backend.md). Also
found and fixed a live bug during development: read-screen fails on a
surface that has never been written to, so liveness now uses list-panes
instead. Fake-CLI unit suite (40 tests), a real-binary smoke test, and a
full spawn/steer/peek/done/merge/teardown E2E pass against a real claude
crewmate all pass, including the popup/second-Enter regression class.

* no-mistakes(review): Harden cmux recovery and password parsing

* no-mistakes(review): Harden cmux capture failure handling

* no-mistakes(review): Mark cmux test scripts executable

* no-mistakes(review): Scope cmux workspaces and teardown

* no-mistakes(review): Captain, honor cmux password config override

* no-mistakes(review): Captain, hash cmux home labels

* no-mistakes(document): Sync cmux backend docs

* feat(agents): add firstmate coding guidelines skill (#248)

* Add firstmate-coding-guidelines skill (AGENTS.md diet PR 0)

Encodes the knowledge-placement decision tree, one-owner rule, and
inline-stub pattern from the diet analysis so future contributions stop
adding conditional detail inline. AGENTS.md gets one section-13 trigger
line; fm-brief.sh's REPO argument has no reliable signal for "this is
firstmate's own repo", so the load instruction goes in CONTRIBUTING.md's
Development section instead of the scaffold.

* no-mistakes(review): Captain, align tracked-material trigger scope

* no-mistakes(document): Sync coding guidelines docs

* no-mistakes(lint): Fix Markdown style issues

* fix: add turn-end supervision guard (#249)

* feat: structural Stop-hook backstop for primary turn-end supervision

fm-guard.sh is pull-based: it only warns when some other supervision
script happens to run, so a primary session that ends a turn without
re-arming the watcher and then runs no further fleet-touching command
can sit blind for hours (the 2026-07-04 incident this fixes).

Add bin/fm-turnend-guard.sh, a Claude Code Stop hook registered in the
tracked .claude/settings.json, that fires on every primary turn end and
blocks (exit 2, verified empirically to force continuation) when work
is in flight with no fresh watcher beacon. It never blocks more than
once per turn, using Claude Code's own stop_hook_active loop-guard
field, and scopes itself to the actual primary checkout only (inert in
crewmate/scout worktrees and secondmate homes).

Factor the shared "in-flight but no live watcher" predicate out of
fm-guard.sh into bin/fm-supervision-lib.sh so the pull-based banner and
the push-based hook can never drift on what "unhealthy" means.

Document the verified Stop-hook mechanism and scoping in
docs/turnend-guard.md, add a harness-adapters note, and cover the
predicate and hook with tests/fm-turnend-guard.test.sh.

* no-mistakes(review): Respect active home in turnend guard

* no-mistakes(review): Require live watcher for turn-end guard

* no-mistakes(review): Captain: portable turn-end timing

* no-mistakes(document): Sync turn-end guard documentation

* feat(backends): auto-detect cmux runtime (#250)

* feat(backends): auto-detect cmux runtime from CMUX_WORKSPACE_ID

Wires cmux into fm_backend_detect the same way herdr already is: a
firstmate process running inside a cmux-spawned terminal now spawns
new tasks into cmux by default, no config needed. Verified from cmux's
own shipped source that CMUX_WORKSPACE_ID/CMUX_SURFACE_ID/CMUX_SOCKET_PATH
are unconditionally, non-overridably injected into every terminal
surface it spawns, and that cmux's own CLI treats CMUX_WORKSPACE_ID as
its own ambient-target fallback - the same role $TMUX/HERDR_ENV play for
their backends. CMUX_WORKSPACE_ID is checked last (after $TMUX and
HERDR_ENV=1) since cmux is a terminal application, not a nestable
multiplexer. Socket auth (config/cmux-socket-password) stays required
regardless of how the backend was selected; the existing spawn refusal
now also names the config/backend=tmux / --backend tmux opt-out for a
caller who never explicitly chose cmux.

A live env dump inside a real cmux terminal was not obtained safely on
the shared dev machine (documented in docs/cmux-backend.md); this rests
on the source read instead, mirroring this doc's existing
verified-from-source precedent.

* no-mistakes(review): Fix cmux autodetect docs and tests

* no-mistakes(document): Document cmux auto-detection

* fix(afk): support herdr away-mode injection (#251)

* fix(afk): make the away-mode daemon backend-aware for herdr

bin/fm-supervise-daemon.sh discovered its supervisor pane and injected
via raw tmux calls only, so /afk failed outright on a herdr-based
fleet (TMUX_PANE unset, firstmate:0 fallback unresolvable).

Discovery now resolves backend (tmux|herdr) and target independently,
mirroring fm-backend.sh's own runtime auto-detection, with an explicit
FM_SUPERVISOR_BACKEND override alongside the existing FM_SUPERVISOR_TARGET.
zellij/orca refuse loudly at startup instead of misapplying tmux
primitives. Injection (pane-exists probe, busy-guard, composer-guard,
verified submit) now dispatches through bin/fm-backend.sh's generic
primitives, adding a new fm_backend_composer_state dispatcher; the
tmux path is byte-identical to before. Also fixes a pre-existing bug
in fm_backend_target_exists's herdr arm (missing --session, so it
silently misrouted once more than one herdr server was running) found
while verifying this end to end against a real isolated herdr session.

Classification, batching, max-defer, the marker contract, locks, and
wake-queue handling are unchanged - this is a transport-layer fix.

* no-mistakes(review): Corroborate Herdr idle busy state

* no-mistakes(review): Stabilize Herdr daemon startup wait

* no-mistakes(review): Captain, route cmux composer and update AFK docs

* no-mistakes(document): Document AFK supervisor backend support

* docs(agents): move X-mode procedures out of AGENTS (#253)

* docs(agents): collapse X-mode section 14 into fmx-respond/docs pointers

AGENTS.md diet PR 1 of 3 (agentsmd-diet-s2 report, move-plan items 1-2).
Replaces section 14's "Answering"/"Completion follow-up"/"Conversations"/
"Length and threads"/"Preview / dry-run" blocks (54 lines) and the
"Mechanism" narrative (6 lines) with two short pointers: fmx-respond
(section 13) for the procedure, docs/configuration.md "X mode (.env)"
for the wire protocol. Net -55 lines in AGENTS.md.

Destination edits landed first, deletions second (q4 discipline):
- docs/configuration.md: added the "purely additive, watcher untouched"
  guarantee that AGENTS.md's Mechanism block stated but configuration.md
  did not.
- fmx-respond/SKILL.md: added the x-mode-error wake boundary (report as
  a blocker, do not load this skill), the --image flag for replies and
  follow-ups, the "images are for real artifacts, not prose" rule, and
  the dry-run compact-image-marker behavior - none of these were
  previously in the skill even though AGENTS.md described them, so they
  were genuine gaps, not pre-existing duplication. Also made the skill's
  own "Completion follow-up" section the sole, full owner of that
  procedure instead of deferring to AGENTS.md section 14 for substance
  that no longer lives there (two internal cross-references updated to
  point at section 8's terminal-wake trigger and the skill's own section
  instead).

Mechanical line-by-line audit of every removed AGENTS.md line:

Mechanism block (6 lines removed):
- bootstrap artifact-writing description -> already owned by
  docs/configuration.md "X mode (.env)" (locked-bootstrap paragraph)
- check-shim/poll mechanism description -> already owned by
  docs/configuration.md same section
- missing-deps/x-mode-error diagnostic description -> already owned by
  docs/configuration.md ("Relay auth or config problems...") plus
  bin/fm-x-poll.sh's own header comment for the missing-curl/jq mechanics
- opt-out artifact removal description -> already owned by
  docs/configuration.md same section
- "purely additive, no edit to fm-watch.sh/fm-watch-arm.sh/fm-wake-lib.sh/
  afk daemon" guarantee -> MOVED to docs/configuration.md (added in this
  PR; this fact had no other home before)

Answering/Completion follow-up/Conversations/Length and threads/
Preview-dry-run blocks (54 lines removed):
- x-mention wake -> load fmx-respond: already owned by section 13's
  existing trigger line (unchanged) and restated in the new pointer
- x-mode-error wake -> report as blocker, don't load fmx-respond: MOVED
  to fmx-respond/SKILL.md (added in this PR)
- inbox-draining, classification, acting, reply composition, submission,
  cleanup-on-success/failure: already owned by fmx-respond/SKILL.md
  "Procedure" section (unchanged, pre-existing)
- owner-only routing / captain-as-asker framing: already owned by
  fmx-respond/SKILL.md "The asker is your own captain" section
- standing X-mode authorization / autonomous posting / dry-run as only
  non-posting path: already owned by fmx-respond/SKILL.md same section
- acknowledge-first -> act -> follow-up shape, three-case classification:
  already owned by fmx-respond/SKILL.md "A request to act on" section
- destructive/irreversible/security-sensitive escalation guardrail:
  already owned by fmx-respond/SKILL.md "Public channel..." section and
  Procedure step 2c
- dismiss-instead-of-reply for pure acknowledgments, relay re-offer
  prevention, dry-run honoring: already owned by fmx-respond/SKILL.md
  Procedure steps 2b/2c/2e-skip and docs/configuration.md
- public-safety bar (no task ids/internals/captain-private/secrets):
  already owned by fmx-respond/SKILL.md "The reply is public" section
- never-inline-into-shell-command / --text-file or stdin: already owned
  by fmx-respond/SKILL.md Procedure step 2e and Notes
- --image flag for replies (formats, base64, no-inline guarantee): MOVED
  to fmx-respond/SKILL.md Procedure step 2e (added in this PR - this was
  not previously in the skill)
- fm-x-link field names (x_request=, x_request_ts=, x_followups=):
  already owned by AGENTS.md section 2's state/<id>.meta field list
  (untouched, out of scope for this PR) and fmx-respond/SKILL.md
- carry-count/carry-ts relink behavior, three-follow-up budget, milestone
  sparingness, --check/--text-file posting, connector/followup wire
  detail, --final clearing, cap/window graceful degradation: already
  owned by fmx-respond/SKILL.md "Completion follow-up" section (now sole
  owner) and docs/configuration.md wire-protocol paragraphs
- --image flag for follow-ups: MOVED to fmx-respond/SKILL.md "Completion
  follow-up" section (added in this PR - genuine gap)
- "failed task still gets an honest final follow-up": already owned by
  fmx-respond/SKILL.md "Completion follow-up" section
- FMX_DRY_RUN whole-loop previewability: already owned by
  fmx-respond/SKILL.md "Dry-run / preview mode" section
- in_reply_to conversation continuity, untrusted-thread handling,
  follow-up worthiness judgment, relay-owned self-reply guard/cap:
  already owned by fmx-respond/SKILL.md "The direct ask is the captain's"
  section and Notes (one bullet is a verbatim match)
- concise-by-default / no hand-numbered threads: already owned by
  fmx-respond/SKILL.md "Voice" section
- auto-split behavior, char/tweet caps, premium-independence, wire shape
  ({text}/{text,texts}): behavior already owned by fmx-respond/SKILL.md
  Voice section; exact defaults and wire shape already owned by
  docs/configuration.md; "premium-independent" mechanics already owned
  by bin/fm-x-reply.sh's own header comment
- "images are for real artifacts, not prose": MOVED to fmx-respond/
  SKILL.md "Voice" section (added in this PR - genuine gap)
- image-on-thread wire behavior: already owned by docs/configuration.md;
  reinforced in fmx-respond/SKILL.md's new --image note
- dry-run POST-body shape, endpoint marker, truthy-value definition,
  jq-only dependency, end-to-end testability, x-outbox inspection:
  already owned by fmx-respond/SKILL.md "Dry-run / preview mode" section
  (several near-verbatim matches) and docs/configuration.md wire detail
- dry-run compact image marker: MOVED to fmx-respond/SKILL.md "Dry-run /
  preview mode" section (added in this PR - genuine gap)

Section 8's terminal-wake completion-follow-up trigger (the one fact
required to survive inline) is untouched and already present; the new
section 14 pointer references it instead of restating it.

Nothing outside section 14 (plus the two destination files) is touched.
Full test suite green, including all 74 fm-x-mode.test.sh checks.

* no-mistakes(review): Preserve X-linked follow-up triggers

* no-mistakes(review): Fix x-mode error trigger

* no-mistakes(document): Docs cross-reference synchronized

* no-mistakes(lint): Clean Markdown lint pass

* docs: trim duplicated harness guidance (#255)

* docs(agents): trim section 4 harness/secondmate duplication

AGENTS.md diet PR 2 of 3 (data/agentsmd-diet-s2/report.md, move-plan
items 3-4; redundancy item 2 folded into item 3).

Removed the five claude/codex/grok/pi/opencode model/effort-flag
bullets from section 4 - byte-for-byte duplicated by
harness-adapters' "Launch profile axes" table (which is already a
superset: it carries verified CLI versions per adapter that the
AGENTS.md bullets lacked). Replaced with a one-line pointer; the
skill is already loaded before every spawn per section 4's own
closing trigger, so no new trigger was needed.

Moved the config/secondmate-harness model/effort pin-format detail
(the `<harness> [<model>] [<effort>]` line format, the
secondmate-model/secondmate-effort accessors, back-compat, and the
durability-across-respawn behavior) into secondmate-provisioning,
which is already a mandatory load at every secondmate lifecycle
touchpoint. Added the destination content to the skill first, then
replaced the AGENTS.md paragraph with a 3-line pointer.

Mechanical audit - every removed line's new home:
- 5 harness bullets (claude/codex/grok/pi/opencode model+effort
  flags, per-harness max-omission rationale) -> already present in
  harness-adapters SKILL.md's "Launch profile axes" table (lines
  53-59), confirmed fact-by-fact before deleting.
- "config/secondmate-harness may also pin..." paragraph (pin format,
  bare-harness back-compat, secondmate-model/secondmate-effort
  accessors, per-spawn override precedence, respawn durability,
  secondmate-only scope) -> secondmate-provisioning SKILL.md's
  "Charter and seed" section, added verbatim before this trim.
- The following paragraph (inheritable config: crew-dispatch.json,
  crew-harness, backlog-backend) is untouched - out of scope for
  this PR, still inline.
- The bootstrap CREW_DISPATCH effort-mismatch diagnostic sentence is
  untouched - not part of the five-bullet duplication, stays inline.

No script changes. Section 4 shrinks from 104 to 87 lines
(958 -> 901 total AGENTS.md lines) with zero facts lost: every fact
is reachable through harness-adapters or secondmate-provisioning,
both already mandatory loads at the relevant lifecycle points.

* no-mistakes(document): Align secondmate skill triggers

* no-mistakes(lint): Markdown style clean

* fix: anchor turn-end Stop hook to project root (#256)

* Fix turn-end Stop hook to use CLAUDE_PROJECT_DIR path

Claude Code runs hook commands via /bin/sh from the session cwd, so the
bare relative bin/fm-turnend-guard.sh path fails when cwd is not the repo
root. Anchor the command with "$CLAUDE_PROJECT_DIR"/bin/fm-turnend-guard.sh
instead; verified CLAUDE_PROJECT_DIR is set on Stop hooks in Claude Code
2.1.201. Document the cwd caveat and add a settings.json regression test.

* no-mistakes(document): Document Stop hook path anchoring

* docs: trim firstmate agent guidance duplication (#258)

* docs: trim AGENTS.md redundancy (diet PR 3/3)

Consolidates five duplicated passages to a single owner each, per
data/agentsmd-diet-s2/report.md redundancy items c3-c7:

- Inheritable-config propagation mechanism: owned by section 3 (where
  the sweep runs); sections 4 and 7 keep compact references. Section 4
  retains its one genuinely unique fact (crew-harness inherit-vs-fallback
  semantics), just no longer restates the propagation mechanism itself.
- Landed-work definition: owned by section 7's ship-teardown detail
  (PR-containment mechanics, pr= discovery fallback); section 1's hard
  rule #3 keeps the rule plus a three-case summary and a pointer.
- Backend meta-field enumeration: owned by docs/configuration.md
  ("Runtime backend", already comprehensive including cmux) and each
  backend's own doc; AGENTS.md keeps only the fields common to every
  task plus a pointer.
- Dropped one redundant restatement of "silence is correct while
  waiting" in section 8.
- Worktree-tangle guard explanation: owned by section 8 (already the
  fuller, cross-referenced version); section 3's TANGLE bullet keeps
  the remediation action and points at section 8 for the why.

Also adds two captain-requested single-sentence rules: invoke bin/
scripts by absolute $FM_ROOT path after any cd away from the home, and
a backend spawn refusal must be surfaced to the captain rather than
silently worked around by switching backends.

AGENTS.md: 901 -> 889 lines, 112355 -> 108560 bytes.

* no-mistakes(review): Clarify post-cd bin invocation guidance

* no-mistakes(document): Sync AGENTS trim docs

* no-mistakes(lint): Fix Markdown line style

* feat(backends): improve cmux detection and socket-mode guidance (#259)

* feat(backends): cmux detection fallbacks and socket-mode matrix

Workstream A: cmux's bundled claude wrapper strips every CMUX_* env var on
its passthrough path (reproduced live 2026-07-04, cmux 0.64.17), so a
claude-harness firstmate inside a cmux tab has no CMUX_WORKSPACE_ID.
fm_backend_detect now falls back - macOS-only, only when the primary marker
is absent - to __CFBundleIdentifier=com.cmuxterm.app and then a process
ancestry walk resolved by bundle id (lsappinfo) plus a bundle-shaped ps comm
match. Innermost-first ordering is unchanged and absorbs the
tmux-inside-cmux bundle-id false positive; the auto-detect NOTICE names the
winning fallback signal.

Workstream B…
DereKk8 added a commit to DereKk8/firstmate that referenced this pull request Jul 12, 2026
…te automation (#7)

* feat(backends): add experimental cmux runtime backend (#246)

* feat(backends): add cmux runtime backend (experimental)

Session-provider-only adapter for cmux (bin/backends/cmux.sh), mirroring
zellij/herdr structurally, wired into fm-backend.sh and fm-spawn.sh with
--secondmate refused for now. Verified against the real cmux 0.64.17 app:
send does not auto-submit, cwd is creation-time-frozen (zellij-shape,
pwd-marker-probe workaround), close-surface refuses on a workspace's last
surface (falls back to close-workspace), workspace ids do not survive a
relaunch, and the control socket defaults to cmuxOnly access (requires a
one-time password-mode setup, documented in docs/cmux-backend.md). Also
found and fixed a live bug during development: read-screen fails on a
surface that has never been written to, so liveness now uses list-panes
instead. Fake-CLI unit suite (40 tests), a real-binary smoke test, and a
full spawn/steer/peek/done/merge/teardown E2E pass against a real claude
crewmate all pass, including the popup/second-Enter regression class.

* no-mistakes(review): Harden cmux recovery and password parsing

* no-mistakes(review): Harden cmux capture failure handling

* no-mistakes(review): Mark cmux test scripts executable

* no-mistakes(review): Scope cmux workspaces and teardown

* no-mistakes(review): Captain, honor cmux password config override

* no-mistakes(review): Captain, hash cmux home labels

* no-mistakes(document): Sync cmux backend docs

* feat(agents): add firstmate coding guidelines skill (#248)

* Add firstmate-coding-guidelines skill (AGENTS.md diet PR 0)

Encodes the knowledge-placement decision tree, one-owner rule, and
inline-stub pattern from the diet analysis so future contributions stop
adding conditional detail inline. AGENTS.md gets one section-13 trigger
line; fm-brief.sh's REPO argument has no reliable signal for "this is
firstmate's own repo", so the load instruction goes in CONTRIBUTING.md's
Development section instead of the scaffold.

* no-mistakes(review): Captain, align tracked-material trigger scope

* no-mistakes(document): Sync coding guidelines docs

* no-mistakes(lint): Fix Markdown style issues

* fix: add turn-end supervision guard (#249)

* feat: structural Stop-hook backstop for primary turn-end supervision

fm-guard.sh is pull-based: it only warns when some other supervision
script happens to run, so a primary session that ends a turn without
re-arming the watcher and then runs no further fleet-touching command
can sit blind for hours (the 2026-07-04 incident this fixes).

Add bin/fm-turnend-guard.sh, a Claude Code Stop hook registered in the
tracked .claude/settings.json, that fires on every primary turn end and
blocks (exit 2, verified empirically to force continuation) when work
is in flight with no fresh watcher beacon. It never blocks more than
once per turn, using Claude Code's own stop_hook_active loop-guard
field, and scopes itself to the actual primary checkout only (inert in
crewmate/scout worktrees and secondmate homes).

Factor the shared "in-flight but no live watcher" predicate out of
fm-guard.sh into bin/fm-supervision-lib.sh so the pull-based banner and
the push-based hook can never drift on what "unhealthy" means.

Document the verified Stop-hook mechanism and scoping in
docs/turnend-guard.md, add a harness-adapters note, and cover the
predicate and hook with tests/fm-turnend-guard.test.sh.

* no-mistakes(review): Respect active home in turnend guard

* no-mistakes(review): Require live watcher for turn-end guard

* no-mistakes(review): Captain: portable turn-end timing

* no-mistakes(document): Sync turn-end guard documentation

* feat(backends): auto-detect cmux runtime (#250)

* feat(backends): auto-detect cmux runtime from CMUX_WORKSPACE_ID

Wires cmux into fm_backend_detect the same way herdr already is: a
firstmate process running inside a cmux-spawned terminal now spawns
new tasks into cmux by default, no config needed. Verified from cmux's
own shipped source that CMUX_WORKSPACE_ID/CMUX_SURFACE_ID/CMUX_SOCKET_PATH
are unconditionally, non-overridably injected into every terminal
surface it spawns, and that cmux's own CLI treats CMUX_WORKSPACE_ID as
its own ambient-target fallback - the same role $TMUX/HERDR_ENV play for
their backends. CMUX_WORKSPACE_ID is checked last (after $TMUX and
HERDR_ENV=1) since cmux is a terminal application, not a nestable
multiplexer. Socket auth (config/cmux-socket-password) stays required
regardless of how the backend was selected; the existing spawn refusal
now also names the config/backend=tmux / --backend tmux opt-out for a
caller who never explicitly chose cmux.

A live env dump inside a real cmux terminal was not obtained safely on
the shared dev machine (documented in docs/cmux-backend.md); this rests
on the source read instead, mirroring this doc's existing
verified-from-source precedent.

* no-mistakes(review): Fix cmux autodetect docs and tests

* no-mistakes(document): Document cmux auto-detection

* fix(afk): support herdr away-mode injection (#251)

* fix(afk): make the away-mode daemon backend-aware for herdr

bin/fm-supervise-daemon.sh discovered its supervisor pane and injected
via raw tmux calls only, so /afk failed outright on a herdr-based
fleet (TMUX_PANE unset, firstmate:0 fallback unresolvable).

Discovery now resolves backend (tmux|herdr) and target independently,
mirroring fm-backend.sh's own runtime auto-detection, with an explicit
FM_SUPERVISOR_BACKEND override alongside the existing FM_SUPERVISOR_TARGET.
zellij/orca refuse loudly at startup instead of misapplying tmux
primitives. Injection (pane-exists probe, busy-guard, composer-guard,
verified submit) now dispatches through bin/fm-backend.sh's generic
primitives, adding a new fm_backend_composer_state dispatcher; the
tmux path is byte-identical to before. Also fixes a pre-existing bug
in fm_backend_target_exists's herdr arm (missing --session, so it
silently misrouted once more than one herdr server was running) found
while verifying this end to end against a real isolated herdr session.

Classification, batching, max-defer, the marker contract, locks, and
wake-queue handling are unchanged - this is a transport-layer fix.

* no-mistakes(review): Corroborate Herdr idle busy state

* no-mistakes(review): Stabilize Herdr daemon startup wait

* no-mistakes(review): Captain, route cmux composer and update AFK docs

* no-mistakes(document): Document AFK supervisor backend support

* docs(agents): move X-mode procedures out of AGENTS (#253)

* docs(agents): collapse X-mode section 14 into fmx-respond/docs pointers

AGENTS.md diet PR 1 of 3 (agentsmd-diet-s2 report, move-plan items 1-2).
Replaces section 14's "Answering"/"Completion follow-up"/"Conversations"/
"Length and threads"/"Preview / dry-run" blocks (54 lines) and the
"Mechanism" narrative (6 lines) with two short pointers: fmx-respond
(section 13) for the procedure, docs/configuration.md "X mode (.env)"
for the wire protocol. Net -55 lines in AGENTS.md.

Destination edits landed first, deletions second (q4 discipline):
- docs/configuration.md: added the "purely additive, watcher untouched"
  guarantee that AGENTS.md's Mechanism block stated but configuration.md
  did not.
- fmx-respond/SKILL.md: added the x-mode-error wake boundary (report as
  a blocker, do not load this skill), the --image flag for replies and
  follow-ups, the "images are for real artifacts, not prose" rule, and
  the dry-run compact-image-marker behavior - none of these were
  previously in the skill even though AGENTS.md described them, so they
  were genuine gaps, not pre-existing duplication. Also made the skill's
  own "Completion follow-up" section the sole, full owner of that
  procedure instead of deferring to AGENTS.md section 14 for substance
  that no longer lives there (two internal cross-references updated to
  point at section 8's terminal-wake trigger and the skill's own section
  instead).

Mechanical line-by-line audit of every removed AGENTS.md line:

Mechanism block (6 lines removed):
- bootstrap artifact-writing description -> already owned by
  docs/configuration.md "X mode (.env)" (locked-bootstrap paragraph)
- check-shim/poll mechanism description -> already owned by
  docs/configuration.md same section
- missing-deps/x-mode-error diagnostic description -> already owned by
  docs/configuration.md ("Relay auth or config problems...") plus
  bin/fm-x-poll.sh's own header comment for the missing-curl/jq mechanics
- opt-out artifact removal description -> already owned by
  docs/configuration.md same section
- "purely additive, no edit to fm-watch.sh/fm-watch-arm.sh/fm-wake-lib.sh/
  afk daemon" guarantee -> MOVED to docs/configuration.md (added in this
  PR; this fact had no other home before)

Answering/Completion follow-up/Conversations/Length and threads/
Preview-dry-run blocks (54 lines removed):
- x-mention wake -> load fmx-respond: already owned by section 13's
  existing trigger line (unchanged) and restated in the new pointer
- x-mode-error wake -> report as blocker, don't load fmx-respond: MOVED
  to fmx-respond/SKILL.md (added in this PR)
- inbox-draining, classification, acting, reply composition, submission,
  cleanup-on-success/failure: already owned by fmx-respond/SKILL.md
  "Procedure" section (unchanged, pre-existing)
- owner-only routing / captain-as-asker framing: already owned by
  fmx-respond/SKILL.md "The asker is your own captain" section
- standing X-mode authorization / autonomous posting / dry-run as only
  non-posting path: already owned by fmx-respond/SKILL.md same section
- acknowledge-first -> act -> follow-up shape, three-case classification:
  already owned by fmx-respond/SKILL.md "A request to act on" section
- destructive/irreversible/security-sensitive escalation guardrail:
  already owned by fmx-respond/SKILL.md "Public channel..." section and
  Procedure step 2c
- dismiss-instead-of-reply for pure acknowledgments, relay re-offer
  prevention, dry-run honoring: already owned by fmx-respond/SKILL.md
  Procedure steps 2b/2c/2e-skip and docs/configuration.md
- public-safety bar (no task ids/internals/captain-private/secrets):
  already owned by fmx-respond/SKILL.md "The reply is public" section
- never-inline-into-shell-command / --text-file or stdin: already owned
  by fmx-respond/SKILL.md Procedure step 2e and Notes
- --image flag for replies (formats, base64, no-inline guarantee): MOVED
  to fmx-respond/SKILL.md Procedure step 2e (added in this PR - this was
  not previously in the skill)
- fm-x-link field names (x_request=, x_request_ts=, x_followups=):
  already owned by AGENTS.md section 2's state/<id>.meta field list
  (untouched, out of scope for this PR) and fmx-respond/SKILL.md
- carry-count/carry-ts relink behavior, three-follow-up budget, milestone
  sparingness, --check/--text-file posting, connector/followup wire
  detail, --final clearing, cap/window graceful degradation: already
  owned by fmx-respond/SKILL.md "Completion follow-up" section (now sole
  owner) and docs/configuration.md wire-protocol paragraphs
- --image flag for follow-ups: MOVED to fmx-respond/SKILL.md "Completion
  follow-up" section (added in this PR - genuine gap)
- "failed task still gets an honest final follow-up": already owned by
  fmx-respond/SKILL.md "Completion follow-up" section
- FMX_DRY_RUN whole-loop previewability: already owned by
  fmx-respond/SKILL.md "Dry-run / preview mode" section
- in_reply_to conversation continuity, untrusted-thread handling,
  follow-up worthiness judgment, relay-owned self-reply guard/cap:
  already owned by fmx-respond/SKILL.md "The direct ask is the captain's"
  section and Notes (one bullet is a verbatim match)
- concise-by-default / no hand-numbered threads: already owned by
  fmx-respond/SKILL.md "Voice" section
- auto-split behavior, char/tweet caps, premium-independence, wire shape
  ({text}/{text,texts}): behavior already owned by fmx-respond/SKILL.md
  Voice section; exact defaults and wire shape already owned by
  docs/configuration.md; "premium-independent" mechanics already owned
  by bin/fm-x-reply.sh's own header comment
- "images are for real artifacts, not prose": MOVED to fmx-respond/
  SKILL.md "Voice" section (added in this PR - genuine gap)
- image-on-thread wire behavior: already owned by docs/configuration.md;
  reinforced in fmx-respond/SKILL.md's new --image note
- dry-run POST-body shape, endpoint marker, truthy-value definition,
  jq-only dependency, end-to-end testability, x-outbox inspection:
  already owned by fmx-respond/SKILL.md "Dry-run / preview mode" section
  (several near-verbatim matches) and docs/configuration.md wire detail
- dry-run compact image marker: MOVED to fmx-respond/SKILL.md "Dry-run /
  preview mode" section (added in this PR - genuine gap)

Section 8's terminal-wake completion-follow-up trigger (the one fact
required to survive inline) is untouched and already present; the new
section 14 pointer references it instead of restating it.

Nothing outside section 14 (plus the two destination files) is touched.
Full test suite green, including all 74 fm-x-mode.test.sh checks.

* no-mistakes(review): Preserve X-linked follow-up triggers

* no-mistakes(review): Fix x-mode error trigger

* no-mistakes(document): Docs cross-reference synchronized

* no-mistakes(lint): Clean Markdown lint pass

* docs: trim duplicated harness guidance (#255)

* docs(agents): trim section 4 harness/secondmate duplication

AGENTS.md diet PR 2 of 3 (data/agentsmd-diet-s2/report.md, move-plan
items 3-4; redundancy item 2 folded into item 3).

Removed the five claude/codex/grok/pi/opencode model/effort-flag
bullets from section 4 - byte-for-byte duplicated by
harness-adapters' "Launch profile axes" table (which is already a
superset: it carries verified CLI versions per adapter that the
AGENTS.md bullets lacked). Replaced with a one-line pointer; the
skill is already loaded before every spawn per section 4's own
closing trigger, so no new trigger was needed.

Moved the config/secondmate-harness model/effort pin-format detail
(the `<harness> [<model>] [<effort>]` line format, the
secondmate-model/secondmate-effort accessors, back-compat, and the
durability-across-respawn behavior) into secondmate-provisioning,
which is already a mandatory load at every secondmate lifecycle
touchpoint. Added the destination content to the skill first, then
replaced the AGENTS.md paragraph with a 3-line pointer.

Mechanical audit - every removed line's new home:
- 5 harness bullets (claude/codex/grok/pi/opencode model+effort
  flags, per-harness max-omission rationale) -> already present in
  harness-adapters SKILL.md's "Launch profile axes" table (lines
  53-59), confirmed fact-by-fact before deleting.
- "config/secondmate-harness may also pin..." paragraph (pin format,
  bare-harness back-compat, secondmate-model/secondmate-effort
  accessors, per-spawn override precedence, respawn durability,
  secondmate-only scope) -> secondmate-provisioning SKILL.md's
  "Charter and seed" section, added verbatim before this trim.
- The following paragraph (inheritable config: crew-dispatch.json,
  crew-harness, backlog-backend) is untouched - out of scope for
  this PR, still inline.
- The bootstrap CREW_DISPATCH effort-mismatch diagnostic sentence is
  untouched - not part of the five-bullet duplication, stays inline.

No script changes. Section 4 shrinks from 104 to 87 lines
(958 -> 901 total AGENTS.md lines) with zero facts lost: every fact
is reachable through harness-adapters or secondmate-provisioning,
both already mandatory loads at the relevant lifecycle points.

* no-mistakes(document): Align secondmate skill triggers

* no-mistakes(lint): Markdown style clean

* fix: anchor turn-end Stop hook to project root (#256)

* Fix turn-end Stop hook to use CLAUDE_PROJECT_DIR path

Claude Code runs hook commands via /bin/sh from the session cwd, so the
bare relative bin/fm-turnend-guard.sh path fails when cwd is not the repo
root. Anchor the command with "$CLAUDE_PROJECT_DIR"/bin/fm-turnend-guard.sh
instead; verified CLAUDE_PROJECT_DIR is set on Stop hooks in Claude Code
2.1.201. Document the cwd caveat and add a settings.json regression test.

* no-mistakes(document): Document Stop hook path anchoring

* docs: trim firstmate agent guidance duplication (#258)

* docs: trim AGENTS.md redundancy (diet PR 3/3)

Consolidates five duplicated passages to a single owner each, per
data/agentsmd-diet-s2/report.md redundancy items c3-c7:

- Inheritable-config propagation mechanism: owned by section 3 (where
  the sweep runs); sections 4 and 7 keep compact references. Section 4
  retains its one genuinely unique fact (crew-harness inherit-vs-fallback
  semantics), just no longer restates the propagation mechanism itself.
- Landed-work definition: owned by section 7's ship-teardown detail
  (PR-containment mechanics, pr= discovery fallback); section 1's hard
  rule #3 keeps the rule plus a three-case summary and a pointer.
- Backend meta-field enumeration: owned by docs/configuration.md
  ("Runtime backend", already comprehensive including cmux) and each
  backend's own doc; AGENTS.md keeps only the fields common to every
  task plus a pointer.
- Dropped one redundant restatement of "silence is correct while
  waiting" in section 8.
- Worktree-tangle guard explanation: owned by section 8 (already the
  fuller, cross-referenced version); section 3's TANGLE bullet keeps
  the remediation action and points at section 8 for the why.

Also adds two captain-requested single-sentence rules: invoke bin/
scripts by absolute $FM_ROOT path after any cd away from the home, and
a backend spawn refusal must be surfaced to the captain rather than
silently worked around by switching backends.

AGENTS.md: 901 -> 889 lines, 112355 -> 108560 bytes.

* no-mistakes(review): Clarify post-cd bin invocation guidance

* no-mistakes(document): Sync AGENTS trim docs

* no-mistakes(lint): Fix Markdown line style

* feat(backends): improve cmux detection and socket-mode guidance (#259)

* feat(backends): cmux detection fallbacks and socket-mode matrix

Workstream A: cmux's bundled claude wrapper strips every CMUX_* env var on
its passthrough path (reproduced live 2026-07-04, cmux 0.64.17), so a
claude-harness firstmate inside a cmux tab has no CMUX_WORKSPACE_ID.
fm_backend_detect now falls back - macOS-only, only when the primary marker
is absent - to __CFBundleIdentifier=com.cmuxterm.app and then a process
ancestry walk resolved by bundle id (lsappinfo) plus a bundle-shaped ps comm
match. Innermost-first ordering is unchanged and absorbs the
tmux-inside-cmux bundle-id false positive; the auto-detect NOTICE names the
winning fallback signal.

Workstream B: the five socketControlMode values were traced through cmux
source (commit 9c91710e3f58): off/cmuxOnly can never admit an external CLI,
automation admits same-user clients with no secret (0600 socket only),
password needs the auth handshake, allowAll opens the socket to every local
user (0666). Automation mode is now the documented recommendation; the
adapter's refusals name every viable mode, classify Invalid password as
unauth, and the launch-timeout message names the off-mode possibility.

Docs carry the wrapper-strip empirical record, the fallback contract and
authority split, and the full mode matrix with rationale; tests cover the
new detection paths, the nested false positive, and the refusal wording.

* no-mistakes(review): Document cmux fallback detection

* no-mistakes(review): Update cmux architecture docs

* no-mistakes(document): Align cmux backend docs

* fix(backends): scope zellij tabs by firstmate home (#252)

* fix(backends): home-scope zellij tab titles to close cross-home collision gap

Zellij's one shared "firstmate" session has no per-home split and enforces
no tab-name uniqueness, so two firstmate homes with colliding task ids could
send/peek/close each other's tabs - the same gap a no-mistakes review gate
caught for cmux (docs/cmux-backend.md). Ports that fix: every new tab is
created with a home-scoped title (fm-<home-label>-<id>), and every
list/find/recover/kill path scopes matches to this home's own tag. A tab
spawned before this change still matches via its old untagged bare title,
but only when unambiguous - two live tabs sharing a bare title refuse rather
than guessing which one is ours.

Factors the home-label/hash derivation shared with cmux into
bin/fm-backend-hometag-lib.sh so the two adapters can't drift.

* no-mistakes(review): Fix zellij child teardown home tag

* no-mistakes(review): Fix zellij teardown and selector scoping

* no-mistakes(document): Sync zellij home-scope docs

* fix: sync project clones after merged PR wakes (#293)

* fix(fleet-sync): auto-sync on merged-PR wake, accept project name

fm-fleet-sync.sh's single-project form failed on a bare project name
("not a directory"), forcing hand-typed full paths (4 manual runs in
one incident). It now resolves a bare name or projects/<name> against
the home's projects dir.

AGENTS.md now encodes the trigger: a wake whose status reports a
merged PR for a project cloned in this home runs fleet-sync for that
project as part of handling the wake, so a secondmate-reported merge
does not leave the primary's clone stale until the next session start
or teardown.

* no-mistakes(review): Fix fleet-sync project name shadowing

* no-mistakes(document): sync fleet-sync docs

* fix: canonicalize spawn worktree path checks (#294)

* fix(spawn): canonicalize worktree-isolation guard against symlinked project prefixes

fm-spawn.sh compared a logical PROJ_ABS against the physically-resolved
pane cwd every backend reports, so a project reached through a symlinked
prefix (e.g. macOS's /tmp -> /private/tmp) could trip the isolation
guard's false refusal before treehouse ever moved the pane. Canonicalize
once into PROJ_ABS_REAL and compare against that everywhere instead.

* no-mistakes(review): Canonicalize spawn cwd comparisons

* no-mistakes(document): Refresh symlinked spawn docs

* docs: add Orca operator skill (#276)

* docs: add Orca operator skill

* no-mistakes(document): Document Orca checklist

---------

Co-authored-by: Stephen Brouhard <vesta@stephens-macbook-air.tail2122af.ts.net>

* fix: surface green PRs during CI monitoring (#297)

* fix(crew-state): detect green-PR CI monitoring, escalate repeat wedges

fm-crew-state.sh's ci step never distinguishes "still waiting on checks"
from "checks green, waiting on merge" via axi status alone, since a repo
that defers merge to the captain keeps the ci step at status=running for
the whole monitor phase. Read the ci step's own log tail (axi logs) for
the checks-passed marker and surface done instead of a false "validating
(running)" - verified against the real PR #252 run's ci.log.

The watcher's wedge timer can re-escalate the same stale pane forever
without ever signaling that it is a repeat; track a per-pane consecutive
escalation count and add a demand-deep-inspection marker to the wake
payload once it crosses a threshold, so the supervisor can no longer
dismiss each one as an isolated, still-validating pane.

Also clarify the ship-brief's checks-green line: it is owed at the
CI-ready return point, not after the background monitor-until-merge
loop finishes.

* no-mistakes(review): Captain, distinguish pending no-checks CI marker

* no-mistakes(review): Harden CI relapse handling

* no-mistakes(review): Block stale done during fixing

* no-mistakes(review): Captain, tighten CI status gating

* no-mistakes(review): Captain, harden stale CI green handling

* no-mistakes(review): Captain, recognize ranged CI rearm markers

* no-mistakes(document): Sync crew-state supervision docs

* fix(teardown): recover provably stale git index locks (#296)

* fix(teardown): recover from a stale worktree git index.lock

A crew process killed mid-git-operation can leave a stale
.git/worktrees/<wt>/index.lock behind, making fm-teardown.sh's
`treehouse return --force` fail closed. On that failure, retry once
after a short wait (the owning process may be exiting), then remove
the lock and retry once more only when it is provably stale: old
enough by mtime and lsof shows no live holder on the lock or the
worktree itself. A lock that isn't provably stale is left in place and
the original failure still surfaces.

* no-mistakes(review): Harden teardown lock refusal paths

* no-mistakes(review): Harden stale-lock teardown safety rechecks

* no-mistakes(review): Harden stale teardown lock checks

* no-mistakes(document): Document teardown lock recovery

* feat(bin): encode project AGENTS.md authoring bar with canonical self-governance section (#307)

* Encode project AGENTS authoring bar

* no-mistakes(review): Captain, centralize CLAUDE promotion governance

* no-mistakes(review): make ensure_maintenance_section idempotent-success, drop || true guards

* no-mistakes(review): separate appended maintenance section on newline-less CLAUDE.md promotion

* no-mistakes(review): assert maintenance heading present before separator check in test

* no-mistakes(document): sync docs with AGENTS.md authoring bar and self-governance

---------

Co-authored-by: fmtest <fmtest@example.invalid>

* feat(skills): add captain-invocable bearings status-report skill (#300)

* Add captain-invocable bearings skill

Generates a pick-up-where-I-left-off status report from live fleet
state to data/status-report-<YYYY-MM-DD>.md plus a concise chat
summary. Read-mostly procedure: reads backlog, per-task crew state
via bin/fm-crew-state.sh, open PRs via gh-axi, scout reports,
pending decisions, and date-gated queued work; composes the
exemplar's sections (TL;DR, Check first, Landed, In flight, Plans,
Decisions pending, Date-gated/queued); never tears down, merges, or
mutates task state as a side effect.

* no-mistakes(document): docs: list new /bearings skill in README built-in skills table

* fix(watcher): make PID identity locale-invariant (#285)

* fix(watcher): pin LC_ALL=C in fm_pid_identity for locale-invariant identity

ps's lstart date format follows the caller's LC_TIME/LC_ALL. The watcher records
its process identity under one locale, but arm/guard/turn-end re-read it under the
machine's ambient locale. On a non-C locale (e.g. ko_KR) the two strings differ
only in the date portion, so fm_watcher_lock_matches_pid / fm_watcher_healthy
reject a genuinely live watcher - breaking fm-watch-arm.sh, fm-guard.sh, and
fm-turnend-guard.sh on every non-C-locale machine.

Pin LC_ALL=C on that one ps call so the write and read sides agree regardless of
machine locale, matching the LC_ALL=C determinism the file already uses elsewhere.
Add a colocated regression test asserting fm_pid_identity is locale-invariant
across exported LC_ALL/LC_TIME.

* no-mistakes(document): Document watcher PID identity coverage

* docs: document codex app backend contract (#222)

* docs: reconcile Codex App backend contract

* no-mistakes(document): Sync backend docs

* docs: clarify Codex Desktop bridge blocker

* no-mistakes(document): Align Codex App backend docs

* no-mistakes(test): Captain, stabilize watcher self-eviction test cadence

* no-mistakes(document): Document Codex App backend contract

* no-mistakes(document): Captain, document blocked codex-app coverage

* docs: make Codex App contract doc authoritative

* no-mistakes(document): Align Codex App backend docs

* docs: redact local Codex App smoke paths

---------

Co-authored-by: Stephen Brouhard <vesta@stephens-macbook-air.tail2122af.ts.net>

* docs: add Codex Desktop coordination skill (#275)

* docs: add Codex App coordination skill

* no-mistakes(review): Captain, mark Codex App skill agent-only

* no-mistakes(document): Document Codex App backend boundary

* no-mistakes(document): Captain, document Codex Desktop backend boundary

* no-mistakes(lint): Captain, lint clean

* no-mistakes(document): Document Codex Desktop boundaries

* docs: narrow Codex App skill playbook

* fix(afk): stop herdr escalation redelivery loop (#317)

* fix(afk): recognize unbordered herdr composer rows to stop escalation redelivery loop

fm_backend_herdr_composer_state only recognized bordered composer rows
(the grok shape). Real claude and codex render their live input row
with no border at all, so once a harness's own startup banner scrolled
out of the capture window the classifier read the composer as unknown
forever. fm_backend_herdr_send_text_submit never confirmed "empty", so
escalate_flush never cleared state/.subsuper-escalations, and the
away-mode daemon retyped and resubmitted the same buffered digest every
housekeeping cycle - reproduced live against a real herdr+claude pane
(5+ identical deliveries in 40s).

The classifier now recognizes an unbordered (bare) composer row led by
a known prompt glyph alongside the existing bordered shape, keeping
whichever match is bottom-most so a stale decorative box never
outranks the live composer.

* no-mistakes(review): Narrow herdr bare prompt matcher

* no-mistakes(document): Sync herdr composer docs

* fix(backends): confirm Herdr submits with native agent state (#323)

* fix(herdr): confirm message submit via native agent-state, not composer text

fm_backend_herdr_send_text_submit now confirms a landed submit by polling
herdr's own agent-state (agent get) for the idle->working transition instead
of reading composer content. Composer scraping remains, unchanged, for the
away-mode daemon's pre-injection empty-box guard only.

This fixes the practical effect of the codex idle-tip gap from the
2026-07-07 incident: codex's dynamic idle-composer hint text can no longer
misread as pending and block/mis-confirm a send, since confirmation no
longer looks at composer text at all. Verified empirically against real
claude and codex agents (timing, swallowed-Enter, unreadable-target, and
already-busy-target scenarios), and against the real away-mode daemon
end-to-end after updating its synthetic supervisor-pane test fixture to
register itself as a real herdr agent (herdr's own report-agent primitive)
so it can still exercise the new confirmation path.

* no-mistakes(review): Captain, harden herdr submit confirmation

* no-mistakes(review): Captain, harden herdr submit confirmation

* no-mistakes(document): Sync Herdr submit docs

* no-mistakes: apply CI fixes

* feat: add quota-balanced crew dispatch selection (#327)

* Add quota-balanced dispatch selection

* no-mistakes(document): Document dispatch selector guidance

* fix(session-start): respawn dead secondmate agents conservatively

* fix(session-start): deterministically respawn dead-shell secondmates

A secondmate agent that exits leaves its backend pane alive as a bare
shell. The session-start endpoint check only verified pane presence, so
recovery and the watcher (which exempts secondmates from stale-pane
detection) never noticed - evidence 2026-07-07: every secondmate in one
fleet was found sitting at a dead zsh shell.

Add fm_backend_agent_alive (bin/fm-backend.sh), a deeper per-backend
liveness probe distinct from pane presence: fm_backend_tmux_agent_alive
classifies the pane's live foreground process via tmux's own
pane_current_command, and fm_backend_herdr_agent_alive reuses the
already-verified pane_agent_state husk classifier. Both are conservative:
anything ambiguous reports unknown, never a false dead.

Wire this into a new session-start-only, locked-and-primary-only sweep in
bin/fm-bootstrap.sh that kills and respawns only a confidently dead
secondmate endpoint, leaving alive/unknown readings untouched - idempotent
by construction, so repeated runs converge without duplicating agents.

* no-mistakes(review): Guard raw secondmate liveness respawns

* no-mistakes(review): Fix detect-only bootstrap test

* no-mistakes(test): Pin liveness fixture harness

* no-mistakes(document): Sync secondmate liveness docs

* no-mistakes: apply CI fixes

* fix: emit stable secondmate nudge selectors (#331)

* Fix NUDGE_SECONDMATES to print stable fm-<id> selectors.

Session-start secondmate sync used to accumulate raw backend window targets
into NUDGE_SECONDMATES, but the liveness sweep in the same bootstrap run can
respawn secondmates onto new endpoints. fm-send with those stale explicit
targets bypasses meta resolution and fails, while fm-<id> resolves correctly.

Accumulate fm-<id> in process_secondmate, update the bootstrap/update contracts
and /updatefirstmate skill, and add a herdr respawn regression test.

* no-mistakes(review): Captain, guard herdr regression jq dependency

* no-mistakes(document): Document stable secondmate nudge selectors

* no-mistakes(lint): Fix shell lint hints

* feat: require bootstrap detection for AXI tools (#332)

* Make tasks-axi and quota-axi required bootstrap tools

Add both to the normal toolchain checks alongside lavish-axi, keep the
tasks-axi 0.1.1+ compatibility gate, and report quota-axi through the
standard MISSING install-consent flow. TASKS_AXI: available remains a
backlog-backend capability signal only; manual opt-out no longer suppresses
the missing-tool report.

Update bootstrap tests and point docs/configuration.md at the canonical
toolchain contract.

* no-mistakes(review): Clarify manual backlog bootstrap reporting

* no-mistakes(document): Document bootstrap AXI tools

* bearings: delete today's report before recreating (#333)

Replace overwrite-in-place wording with explicit delete-then-create
instructions so agents do not modify an existing daily report file.

* feat: guard primary turn ends across harnesses (#339)

* Add primary turn-end guards for all harnesses

* no-mistakes(review): Normalize Codex hook cwd resolution

* no-mistakes(review): Fix OpenCode guard worktree anchoring

* no-mistakes(review): Anchor Codex guard outside nested roots

* no-mistakes(review): Anchor Codex guard to hook root

* no-mistakes(review): Avoid Grok permission escalation

* no-mistakes(document): Sync turn-end guard docs

* fix: resolve backend selectors by exact task id first (#342)

* fix backend selector task id resolution

* no-mistakes(document): Document selector resolution behavior

* fix: scale bootstrap fleet-sync timeout (#341)

* fix bootstrap fleet sync timeout

* no-mistakes(review): Fix bootstrap fleet-sync timeout regressions

* no-mistakes(document): Sync bootstrap timeout docs

* no-mistakes(lint): Clean ShellCheck directives

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* feat: add fleet snapshot and view commands (#343)

* Add fleet snapshot and view

* no-mistakes(review): Fix fleet snapshot parsing and overrides

* no-mistakes(review): Fix secondmate fleet rendering

* no-mistakes(review): Fix backlog title and completion parsing

* no-mistakes(review): Include durable scout reports

* no-mistakes(review): Fix fleet snapshot edge cases

* no-mistakes(review): Captain: gate fleet hints on current state

* no-mistakes(review): Captain: parse bracketed Done PR artifacts

* no-mistakes(document): Sync fleet snapshot docs

* fix(fm-send): fail loudly on unresolvable send targets (#254)

* Make fm-send fail loudly on unresolved targets

* no-mistakes(review): Document fm-send FM_HOME contract

* Fix fm-send readiness docs and backend send path

* Fix fm-send docs for cmux and X skill metadata

* Make gotmp teardown test home-explicit

* Scope watcher warning wording to fm-send

* Fix fm-send review findings

* Verify explicit tmux targets before sending

* Isolate turnend guard test home

* no-mistakes(document): Documented fm-send FM_HOME/backend guard additions missing from doc inventories

---------

Co-authored-by: mielyemitchell <249051873+mielyemitchell@users.noreply.github.com>

* fix: deliver AFK escalations through herdr supervisors (#353)

* fix afk codex ghost composer delivery

* no-mistakes(review): Harden AFK startup flag writes

* no-mistakes(review): Harden AFK daemon liveness checks

* no-mistakes(document): Sync AFK herdr docs

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* feat: add harness-aware supervision (#367)

* Add harness-aware supervision

* no-mistakes(review): Captain, harden watcher supervision regressions

* no-mistakes(review): Captain, harden watcher supervision cadence

* no-mistakes(review): Harden watcher supervision ownership

* no-mistakes(review): Captain, harden Pi extension marker

* no-mistakes(review): Captain, harden Pi supervision restart checks

* no-mistakes(review): Harden watcher ownership checks

* no-mistakes(review): Captain, harden Pi supervision loading

* no-mistakes(review): Captain, require Pi guard extension loading

* no-mistakes(review): Captain, harden watcher supervision recovery

* no-mistakes(test): Fix fm-send baseline log filtering

* no-mistakes(document): Sync harness supervision docs

* no-mistakes: apply CI fixes

* fix: split X-mode replies by platform (#369)

* fix: make x replies split by platform

* no-mistakes(review): Captain: preserve Discord recovery relink context

* no-mistakes(test): Captain: keep split markers outside fences

* no-mistakes(document): Sync X-mode reply docs

* fix: make stow memory writes inspect before update (#372)

* docs: make stow inspect-then-update

* no-mistakes(review): Remove unsupported archive-body guidance

* no-mistakes(review): Clarify stow read-before-write exception

* no-mistakes(test): Require archive-body for stow task notes

* no-mistakes(document): Sync stow memory docs

* no-mistakes(lint): Silence ShellCheck source warning

* fix(watcher): wait when arm attaches to a healthy watcher (#375)

* fix: attach-and-wait when arm finds a healthy watcher

Grok and Claude re-arm after every turn with work in flight. When a
watcher was already healthy, fm-watch-arm exited immediately with
watcher: healthy, which completed the harness background task and
injected an empty false wake.

Attach to the live identity-matched holder instead, stay until that
cycle ends, then exit 0 so notify fires for a real end-of-cycle. The
peer-startup-race path uses the same contract. --restart and the
started path are unchanged.

* no-mistakes(review): Gate restart watcher peer attach

* no-mistakes(document): Sync watcher arm docs

* feat(pi): simplify primary session launch (#386)

* docs(readme): reformat Quick Start and recommend Grok equally with Claude Code

* no-mistakes(review): Captain: align harness launch guidance

* no-mistakes(review): Captain, clarify Pi supervised launch

* no-mistakes(review): Captain, document Pi first-launch bridge

* feat(pi): track primary watcher extension for plain-pi launch

Move Pi's primary watcher bridge from a generated state/ file to a
tracked .pi/extensions/fm-primary-pi-watch.ts, matching how the turn-end
guard extension already works: self-hashing version, project-local
auto-discovery after one-time Pi trust. This drops the state/-generation
step and dual -e requirement from the happy path, so Pi's Quick Start
launch becomes plain 'pi', the same friction class as 'claude' and
'grok --trust'.

- bin/fm-pi-watch-extension.sh is removed; nothing generates the
  extension anymore since it is committed.
- fm-session-start.sh and fm-supervision-instructions.sh resolve the
  watcher extension path from FM_ROOT instead of state/, and the
  session-start diagnostic now points at restarting plain pi after
  trust, with -e as a documented fallback.
- fm-spawn.sh points Pi secondmate launches at the tracked extension
  path in the secondmate home instead of generating a state/ copy.
- README Quick Start Pi block is now just 'pi' plus a trust note.
- Tests, docs, and the harness-adapters skill updated to match.

* fix(pi): drop backticks from session-start diagnostic to satisfy shellcheck SC2016

* feat(supervision): prevent unsafe watcher-arm commands (#387)

* feat(supervision): add PreToolUse seatbelt against watcher-arm anti-patterns

Adds bin/fm-arm-pretool-check.sh, a shared PreToolUse-style checker that
denies a primary shell command backgrounding, piping, or bundling the
watcher arm/checkpoint, or force-killing the watcher process broadly -
the exact shapes that silently took Grok's supervision down. Wires it
into all five verified harnesses (grok, claude, codex, opencode, pi),
each validated empirically against the real harness.

Also fixes a grok 0.2.93 regression discovered during that validation: the
existing turnend-guard Stop hook's bare root variable broke grok's own
variable pre-substitution and silently no-op'd the hook.

* no-mistakes(review): Harden watcher arm validation

* no-mistakes(review): Harden arm guard metacharacter checks

* no-mistakes(review): Harden nested shell arm guard

* fix(lint): rewrite SC2015 guards in fm-arm-pretool-check.sh as if/then

A && B || C is not if-then-else; C can run when A is true. Replace both
occurrences of the quote-state early-continue with an explicit if/then.

* fix(pi): restore primary watcher supervision lifecycle (#397)

* fix Pi primary supervision lifecycle

* no-mistakes(document): Synchronize Pi primary extension documentation

* fix: keep persistent secondmates out of the main backlog (#398)

* fix secondmate backlog guidance

* no-mistakes(review): Require reasons for captain backlog holds

* no-mistakes(test): Document secondmate handoff skill requirement

* fix secondmate teardown reminder

* no-mistakes(document): sync teardown reminder docs to work-items-only backlog contract

* fix(backlog-handoff): move full item blocks including indented bodies (#401)

* fix(backlog-handoff): move full item blocks including indented bodies

fm-backlog-handoff only moved the checklist header line, so multi-line
item bodies were left orphaned in the source backlog and never reached
the secondmate. Move the full block (header plus indented body lines)
atomically, treating body membership by indentation so lines like
## Intent stay with the item, and add regression coverage.

* no-mistakes(review): Captain: preserve EOF handoff terminators

* no-mistakes(review): treat blank lines inside item bodies as movable body

* no-mistakes(document): sync backlog-handoff docs with full-block move behavior

* feat(herdr): make Herdr lab lifecycle safety deterministic for briefs (#402)

* guard Herdr lab lifecycle in briefs

* no-mistakes(review): Fix Herdr lab helper and provisioning safety

* no-mistakes(review): Captain, harden Herdr lab lifecycle safety

* no-mistakes(review): fix Herdr lab test cleanup ordering and brief help range

* no-mistakes(review): reject leading options in Herdr lab run guard

* no-mistakes(review): strip leading non-alnum in Herdr lab name generator

* no-mistakes(document): document Herdr lab helper and --herdr-lab brief flag

* no-mistakes(lint): add shellcheck disable for deliberate SC2016 literals in fm-brief herdr-lab

* no-mistakes: apply CI fixes

* fix(watcher): classify arm-command seatbelt by execution position (#403)

* fix watcher arm command policy

* no-mistakes(review): Harden watcher command policy parsing

* no-mistakes(review): Captain: harden watcher policy parsing

* no-mistakes(review): harden watcher policy for expanded paths, direct-watch, and sound prefilter

* no-mistakes(review): close prefilter and classifier locale/ANSI-C watcher-path decode gaps

* no-mistakes(review): fail closed on loop-wrapped broad watcher kills

* no-mistakes(document): sync docs for watcher-arm command-position policy

* fix: reconcile existing AGENTS.md safely (#405)

* fix(agents-md): inject self-governance section into existing AGENTS.md

fm-ensure-agents-md.sh only appended the canonical "## Maintaining this
file" section on skeleton create or CLAUDE.md promotion, so an existing
AGENTS.md that lacked it exited unchanged and forced hand-copying the
wording during a rollout across existing projects. Call the already-
idempotent ensure_maintenance_section on the existing-AGENTS.md paths and
report whether the file changed; a re-run and an already-complete file stay
byte-identical.

Also fixes #389: refuse a case-variant real memory file (e.g. a lowercase
agents.md) instead of silently emitting a CLAUDE.md symlink whose uppercase
literal target dangles once the tree lands on a case-sensitive filesystem.

Tests extend tests/fm-ensure-agents-md.test.sh; skeleton-create and
CLAUDE.md-promotion regressions still pass. Docs updated to match.

* no-mistakes(review): Captain: preserve CRLF maintenance-section idempotency

* no-mistakes(review): Preserve CRLF during maintenance-section injection

* no-mistakes(review): Captain: harden dangling-symlink regression coverage

* no-mistakes(document): Document agent-memory injection outcomes

* feat: support project-less secondmate homes (#409)

* feat(secondmate): support project-less homes via --no-projects

fm-brief.sh --secondmate and fm-home-seed.sh now accept an explicit
--no-projects signal to scaffold, seed, and register a secondmate home
whose subject is the firstmate repo itself (no clones). The signal is
mutually exclusive with a project list; omitting both still fails loudly
so an accidental omission is never a silent project-less seed. The
registry line renders an empty projects: field, which spawn and the
snapshot already tolerate. Docs updated in the secondmate-provisioning
skill and both script headers.

* no-mistakes(review): Captain: document project-less secondmate flow

* no-mistakes(review): Captain: refuse project-less reseeding of populated homes

* fix(seed): fail closed on unreadable project data

* no-mistakes(review): Captain: reject stale projectful charters

* no-mistakes(review): Captain: fail closed on unsafe project paths

* no-mistakes(review): Captain: validate project-less charter clone sections

* no-mistakes(document): Document project-less secondmate seeding

* fix: delegate backlog handoffs to tasks-axi (#411)

* wip(handoff): record verified delegation design + tasks-axi mv blocker

No production code changed yet. tasks-axi mv (v0.2.1) cannot atomically
move a blocked-by-linked item set across backlogs (deadlocks both orders,
no batch/--force), which fm-secondmate-lifecycle-e2e requires. Parked
pending a tasks-axi connected-set mv enhancement; note captures the
verified design, semantics, test/CI/doc changes, and resume checklist.

* refactor(handoff): delegate the item move to tasks-axi mv

fm-backlog-handoff.sh's two-pass awk was a second parser of the backlog
format and the source of the PR #401 body-orphaning drift. Delete it and
delegate the move to `tasks-axi mv <id>... --to <dest>` (v0.2.2 atomic
multi-id), the single owner of the format: a connected set (blocker plus
dependents) moves together with blocked-by preserved, item blocks stay
byte-exact, and destination section placement holds. The helper keeps only
the fleet-level validation tasks-axi cannot know - secondmate-home
resolution, the seeded-home safety checks, the In-flight refusal, and
idempotent per-key reporting - and is atomic: on any move failure nothing
moves.

Tests: fm-backlog-handoff.test.sh keeps PR #401's regression matrix but now
exercises the delegated path and skips cleanly when tasks-axi is absent; the
two whole-file fixtures move to tasks-axi's canonical whitespace. The
lifecycle-e2e and safety move-cases gain the same skip guard. CI installs
tasks-axi so the delegated path is exercised. Docs state that
config/backlog-backend=manual governs firstmate's own hand-editing, not this
validated helper, which delegates fleet-wide because bootstrap requires
tasks-axi on PATH.

Remove the now-redundant WIP design note.

* no-mistakes(review): Captain: harden atomic backlog handoffs

* no-mistakes(review): Captain: enforce queued-only backlog handoffs

* no-mistakes(review): Captain: harden handoff section parsing

* no-mistakes(document): Document delegated backlog handoffs

* no-mistakes(lint): Silence ShellCheck source diagnostics

* fix: ignore secondmate home marker during sync (#417)

* fix: gitignore the secondmate home marker

bin/fm-home-seed.sh writes an untracked .fm-secondmate-home marker into
every seeded secondmate home. A secondmate home is a worktree of the
firstmate repo, so any plain `git status --porcelain` dirtiness check
counted the untracked marker and the home read as dirty forever:
fleet-sync reported it STUCK and the local fast-forward convergence
sweeps risked leaving it stale on firstmate updates.

Add .fm-secondmate-home to the tracked .gitignore so the marker is
invisible to every dirtiness check uniformly, without weakening
fleet-sync's deliberate untracked-counting for project clones.

Convergence chicken-and-egg: existing homes predate the fix and it only
arrives by fast-forward. The already-present marker-tolerant ff-skip
(ignore_seed_marker=yes, used by the bootstrap sweep, /updatefirstmate,
and spawn pre-launch) advances such a home past the fix commit, after
which .gitignore takes over - no hand intervention.

Tests in tests/fm-secondmate-sync.test.sh cover a freshly seeded home
reading clean, an existing marker-only home converging then reading
clean, and a genuinely dirty home still skipping.

* no-mistakes(review): Captain: document standalone-clone update path

* no-mistakes(document): Document secondmate marker migration

* fix(composer): prevent dead-shell message injection (#416)

* fix(composer): stop reading dead-shell prompts as empty agent composers

Consolidate composer empty/pending/unknown classification into one shared
owner, bin/fm-composer-lib.sh's fm_composer_classify_content, delegated to by
all four backend adapters (tmux via fm-tmux-lib.sh, herdr, orca, cmux). This
replaces four drifting copies of the glyph decision.

Safety fix: a bare shell prompt glyph (> $ % #) on an unstructured row is now
classified unknown (a dead shell, unsafe for injection), not empty. It is only
empty inside a bordered composer box (the harness's own prompt). Agent glyphs
❯ (claude) and › (codex) read empty either way. The away-mode injector
(inject_msg) now requires an affirmatively-empty composer, deferring on pending
or unknown, so an escalation can never be typed into (or executed by) a pane
whose agent exited to its login shell.

Regression coverage: new tests/fm-composer-lib.test.sh pins the shared owner;
per-backend dead-shell tests in fm-daemon (tmux + injector), orca, and the
existing herdr/cmux suites. shellcheck clean; herdr incident regressions stay
green.

* no-mistakes(review): Captain: harden composer safety checks

* no-mistakes(test): Stabilize Herdr prune safety setup

* no-mistakes(document): Document composer injection safety

* no-mistakes(lint): Clean composer safety lint

* no-mistakes: apply CI fixes

* feat(watcher): add paused external-wait supervision (#421)

* feat(watcher): add paused/awaiting-external crew state

A crew (or firstmate steering it) can declare a deliberate wait on a known
external dependency with a paused: <reason> status. Both the always-on watcher
and the away-mode daemon absorb such an idle pane through shared fm-classify-lib.sh
vocabulary instead of tripping the possible-wedge stale escalation, and re-surface
it for a recheck only on a long bounded cadence (FM_PAUSE_RESURFACE_SECS) so a
forgotten pause cannot rot invisibly. fm-crew-state.sh reports state: paused
distinctly. A crew that goes idle without declaring a pause classifies exactly as
before. Docs and brief scaffold state lists updated; tests colocated.

* no-mistakes(review): Captain: fix paused-state transitions

* init

* no-mistakes(review): Captain: fix paused-state supervision transitions

* no-mistakes(review): Captain: fix paused supervision handoffs

* no-mistakes(review): Reconcile paused supervision markers

* no-mistakes(review): Captain: prioritize paused states over captain relevance

* no-mistakes(review): Captain: preserve paused-working wedge timer

* no-mistakes(review): Captain: honor configured pause verb in briefs

* no-mistakes(test): Captain: fix AFK paused watcher handoff

* no-mistakes(document): Document declared external waits

* no-mistakes(lint): Clean paused-state lint

---------

Co-authored-by: fmtest <fmtest@example.invalid>

* fix: preserve X-mode follow-up platform limits (#425)

* fix(x-mode): make follow-up platform splitting immune to link ordering

A ~470-char Discord follow-up posted as a (1/2)(2/2) thread split at ~280
chars because fm-x-link only learned the platform from the inbox payload,
and the fmx-respond ack path can drain that inbox file before the task is
linked. A link recorded after cleanup silently lost the platform and the
splitter defaulted to the X 280-char budget.

Make platform resolution ordering-proof:

- fm-x-link now resolves the platform AUTHORITATIVELY by request_id via a
  new fmx_request_relay_context helper (POST /connector/request-context)
  when neither the inbox payload nor carry flags carry it. The request_id
  survives the inbox drain, so a post-cleanup link still learns the right
  split budget. Best-effort: no token/curl or a non-2xx relay degrades to
  the loud warning below rather than a silent X default.
- fm-x-link warns loudly when no platform source resolves, so the loss is
  never silent.
- The fmx-respond procedure now orders link-before-inbox-cleanup so the
  fast local path stays correct without a relay round-trip.

Colocated regression tests: a Discord follow-up >280 <2000 posts as ONE
message even when linked after inbox cleanup, and an unresolvable platform
warns loudly instead of splitting silently. docs/configuration.md documents
the request-context lookup.

The relay endpoint is the companion durable change (see done status); until
it ships, the link-before-cleanup reorder keeps the normal path correct.

* no-mistakes(document): Document X-mode platform recovery

* fix(composer): handle ANSI ghost text safely (#429)

* fix(composer): one ANSI-aware ghost owner covers claude dim + grok truecolor

Away-mode injection wedged all night on the primary claude-on-herdr pane:
the herdr composer classifier never stripped generic dim ghost text (only a
narrow codex bold-wrapped byte-pattern check), so claude's rotating
prompt-suggestion ghost - a bare "❯" then SGR-2 dim text, which herdr's ANSI
pane read preserves - read as real pending input and every escalation deferred
(6524 lifetime "pending input (non-empty composer)" defers; wedge 30623s).

Consolidate ghost extraction into one fleet-wide ANSI-aware owner,
fm_composer_strip_ghost (bin/fm-composer-lib.sh), that drops every
de-emphasised run - dim/faint (SGR 2: claude, codex) AND a dark/muted truecolor
foreground (grok's placeholder, luminance below FM_COMPOSER_GHOST_LUMA_MAX,
default 128, dark-theme assumption). Both ANSI-capable backends route through
it: fm_tmux_composer_state (fm_tmux_strip_ghost is now a thin adapter) and
fm_backend_herdr_composer_state. The herdr-only faint byte-pattern check is
removed and fm_backend_herdr_strip_ansi reduced to a thin adapter over the
shared fm_composer_strip_ansi. Bordered detection now reads the plain row so a
dark box border dropped with the ghost does not lose the composer shape.

This also closes the documented grok TRUECOLOR placeholder gap by the same
mechanism (harness-adapters skill note updated).

Empirical evidence (read-only live capture + isolated tmux, no herdr lifecycle)
and the incident write-up are in docs/herdr-backend.md; deterministic
regressions feed the exact captured bytes through the real classifiers
(tests/fm-backend-herdr.test.sh, tests/fm-composer-ghost.test.sh). Two prior
ghost-test fixtures that used a near-black 38;2;1;2;3 as "real" colored text
(never a realistic real-input color) are corrected to a bright 38;2;224;222;244,
preserving the truecolor payload-skip parser intent.

* no-mistakes(review): Preserve dark shell prompt safety

* no-mistakes(review): Harden erased shell prompt classification

* no-mistakes(document): Document shared composer ghost extraction

* no-mistakes(lint): Normalize tmux comment punctuation

* fix(spawn): make tmux window handling robust under non-default config (#134)

* test: isolate session-start suite from ambient harness markers (#432)

* fix(session-start): isolate harness env markers in suite runner

Neutralize CLAUDECODE, PI_CODING_AGENT, and GROK_AGENT in
run_session_start so ambient interactive shells cannot override the
suite's fake ps harness (local-vs-CI split on the pi supervision case).

* no-mistakes(document): Correct Pi marker documentation

* fix(teardown): retry transient index locks during worktree return (#435)

* fix(teardown): retry treehouse return on transient index.lock

Killed crew git ops can leave a short-lived worktree index.lock that
makes treehouse return fail. Retry on that error signature with a
bounded wait (env-overridable), never force-delete a live lock, and
only then fall back to the existing provably-stale cleanup path.

* no-mistakes(review): Harden teardown retry configuration

* no-mistakes(document): Document teardown index-lock retry behavior

* no-mistakes(lint): Fix empty shell variable assignments

* fix: complete brief help and consolidate documentation (#438)

* docs: de-feature the scripts.md and CONTRIBUTING test inventories

Slice 1 of the documentation redundancy cleanup wave (firstmate scope).

docs/scripts.md: every row is now one purpose clause; script headers
are the declared owner of behavior, flags, and contracts. Coverage
stays 61/61 scripts; bytes drop 19,922 -> 7,958.

CONTRIBUTING.md: the 54-row per-test inventory is gone; contributors
discover tests by listing tests/*.test.sh and reading each script's
own header, and gated tests print their own skip gates. The run
commands, symlink assertions, and watcher smoke line are unchanged.
Lines drop 135 -> 84 (18,797 -> 7,831 bytes).

Two facts that existed only as inventory rows moved into their
owners' headers first: fm-brief.sh's paused-vs-blocked scaffold
distinction and fm-session-start.sh's Pi extension-loaded check.

No instruction-surface or behavior change; AGENTS.md untouched.

* no-mistakes(review): Captain, fix brief help and Grok test discovery

* no-mistakes(review): Captain: document Grok lock-holder test coverage

* fix: detect Git and centralize backend configuration (#445)

* docs: consolidate universal backend contracts into configuration.md

Slice 2 of the documentation redundancy cleanup wave (firstmate scope).

docs/configuration.md is now the declared single owner of three
universal contracts, each with an explicit ownership sentence:
- the universal toolchain list (Toolchain), now also carrying the
  per-tool purpose clauses that previously lived only in the tmux guide;
- the task-selector vocabulary (Runtime backend);
- the tasks-axi compatibility definition (Backlog backend).

The five backend guides' prerequisites replace their verbatim
universal-requirements parentheticals (5 full copies) with a pointer
plus only backend-specific items; zellij/cmux selector restatements
and architecture.md's partial copy become pointers or are dropped;
CONTRIBUTING's compatibility sentence becomes a pointer; two
near-verbatim orca-bootstrap restatements (configuration.md Runtime
backend, orca guide) collapse into the Toolchain owner copy.

Backend-specific setup, behavior, target-string shapes, and every
empirical verification record are untouched. AGENTS.md untouched
(slice 3).

* docs: include git and GitHub auth in the toolchain owner list

The review flagged that the new universal-toolchain owner omitted git
and GitHub authentication while every backend guide now defers its
prerequisites here; bootstrap's NEEDS_GH_AUTH check makes them real
universal requirements.

* no-mistakes(review): Detect Git in bootstrap toolchain

* no-mistakes(document): Clarify GitHub CLI and centralize selector documentation

* feat(daemon): add backend-independent wedge alerts (#444)

* feat(daemon): backend-independent active alert for the wedge alarm

When away-mode injection wedges past max-defer, inject_wedge_alarm only
actively signalled via the tmux status-line, which is skipped on non-tmux
backends. A wedged claude-on-herdr primary left only the passive
state/.subsuper-inject-wedged marker (2026-07-10 overnight incident).

Add a config-gated active alert (config/wedge-alarm, local/gitignored;
FM_WEDGE_ALARM_CHANNEL) that reaches the captain even when every pane and
its status-line is unreadable: an OS-level macOS notification (osascript),
a herdr notification, or a captain-supplied command. Default-on (auto) so
the alarm is never silent; each channel best-effort, degrading to the next
and never crashing the daemon loop. The tmux flash and durable marker stay.

The OS notifiers route through a single FM_WEDGE_ALARM_EXEC seam. When the
daemon is sourced (only tests do this; production execs it) the seam
defaults to "discard", and tests/wake-helpers.sh points it at a recorder,
so it is structurally impossible for any test to post a real notification.

Channels verified once manually on macOS 26.5.2 / herdr 0.7.3; see
docs/wedge-alarm.md.

* no-mistakes(review): Bound wedge alarm notifier execution

* no-mistakes(review): Captain: harden wedge alarm notifier safety

* no-mistakes(review): Captain: harden wedge alarm test notifier isolation

* no-mistakes(review): Captain: harden wedge alarm throttling

* no-mistakes(review): Redact wedge alarm directive logs

* no-mistakes(review): Harden wedge alarm notifier safety

* no-mistakes(review): Track notifier process groups through cleanup

* no-mistakes(document): Document wedge-alarm active alert behavior

* docs: centralize firstmate operating contracts (#447)

* docs(agents): extract conditional AGENTS.md material to owned homes

Slice 3 of the documentation redundancy cleanup wave (firstmate scope):
the always-loaded instruction surface drops from 941 lines / 116,733
bytes (~29k tokens per session per fleet member) to 785 / 91,353
(~22.8k tokens), moving only audit-identified conditional and
situational material while preserving every load-bearing invariant at
its trigger point via the inline-stub pattern.

Moves, each to one declared owner plus an inline stub:
- section 3's bootstrap output-line handbook (~44 lines) -> new
  agent-only bootstrap-diagnostics skill, added to the section 13
  trigger index; the detect-consent-install rule and the
  do-not-dispatch gate stay inline as safety-critical.
- section 4's crew-dispatch JSON schema and field semantics ->
  docs/configuration.md 'Crew dispatch profiles' (pointer direction
  flipped); the intake procedure, precedence, backstop, and
  never-select-unverified rules stay inline.
- section 4's quota-balanced algorithm -> bin/fm-dispatch-select.sh
  header (now the declared owner; usage() converted to the dynamic
  header extraction pattern PR #438 established for fm-brief.sh).
- section 7's spawn resolution narrative and example sprawl ->
  bin/fm-spawn.sh header; the isolated-worktree assertion, refusal-is-
  a-blocker rule, and post-spawn duties stay inline.
- section 7's teardown landed-work mechanics -> bin/fm-teardown.sh
  header (section 1's containment pointer retargeted); the fork benign
  case and never-force rule stay inline.
- section 8's watcher classification narrative -> docs/architecture.md
  'Event-driven supervision' (already the owner); every operative rule
  (one live cycle, no turn ends blind, drain first, wake ladder,
  never-pkill, guard responses) stays inline.
- sections 3/4/6/7 secondmate sync, propagation, schema, and handoff
  restatements -> secondmate-provisioning skill, now the declared
  owner including the literal-file inheritance nuance.
- section 14's X-mode cadence mechanism -> docs/configuration.md
  'X mode (.env)', closing issue #363; activation semantics, the
  fmx-respond trigger, and the terminal-wake final-follow-up duty
  stay inline.

CLAUDE.md stays a symlink; no behavior or test change.

* no-mistakes(document): Centralize contract-owner documentation

* fix(cmux): close last workspace during teardown (#449)

* fix(cmux): close the last/selected workspace in a window at teardown

cmux keeps every window at >=1 workspace, so close-workspace on the only
workspace in a window silently no-ops (returns OK, workspace stays), and a
window holding a live session cannot be closed over the control socket.
That left a selected task workspace open at teardown (the last workspace
in a window is always the selected one).

Add fm_backend_cmux_window_of_workspace and have fm_backend_cmux_kill
create a throwaway default sibling in the target's window before closing
when the target is the last workspace there, so the close lands; the
window keeps a fresh default workspace (cmux's own "closed the last tab"
outcome). Non-last teardown closes directly, as before.

Cover both kill branches plus the helper with fake-CLI unit tests, add a
real-cmux window/count detection smoke assertion, and record the
empirical evidence in docs/cmux-backend.md.

* no-mistakes(review): Derive cmux count from membership snapshot

* no-mistakes(document): Document cmux last-workspace teardown behavior

* fix: recover orphaned packed-refs locks during fleet sync (#453)

* fix(fleet-sync): recover from an orphaned packed-refs.lock

A git ref rewrite (fetch --prune, pack-refs, branch -D) killed after
creating .git/packed-refs.lock but before renaming it - e.g. bootstrap's
timed-out fleet-sync kill or teardown's process kills - leaves a lock that
makes the next sync's fetch fail with "Unable to…
DereKk8 added a commit to DereKk8/firstmate that referenced this pull request Jul 17, 2026
* feat(backends): add experimental cmux runtime backend (#246)

* feat(backends): add cmux runtime backend (experimental)

Session-provider-only adapter for cmux (bin/backends/cmux.sh), mirroring
zellij/herdr structurally, wired into fm-backend.sh and fm-spawn.sh with
--secondmate refused for now. Verified against the real cmux 0.64.17 app:
send does not auto-submit, cwd is creation-time-frozen (zellij-shape,
pwd-marker-probe workaround), close-surface refuses on a workspace's last
surface (falls back to close-workspace), workspace ids do not survive a
relaunch, and the control socket defaults to cmuxOnly access (requires a
one-time password-mode setup, documented in docs/cmux-backend.md). Also
found and fixed a live bug during development: read-screen fails on a
surface that has never been written to, so liveness now uses list-panes
instead. Fake-CLI unit suite (40 tests), a real-binary smoke test, and a
full spawn/steer/peek/done/merge/teardown E2E pass against a real claude
crewmate all pass, including the popup/second-Enter regression class.

* no-mistakes(review): Harden cmux recovery and password parsing

* no-mistakes(review): Harden cmux capture failure handling

* no-mistakes(review): Mark cmux test scripts executable

* no-mistakes(review): Scope cmux workspaces and teardown

* no-mistakes(review): Captain, honor cmux password config override

* no-mistakes(review): Captain, hash cmux home labels

* no-mistakes(document): Sync cmux backend docs

* feat(agents): add firstmate coding guidelines skill (#248)

* Add firstmate-coding-guidelines skill (AGENTS.md diet PR 0)

Encodes the knowledge-placement decision tree, one-owner rule, and
inline-stub pattern from the diet analysis so future contributions stop
adding conditional detail inline. AGENTS.md gets one section-13 trigger
line; fm-brief.sh's REPO argument has no reliable signal for "this is
firstmate's own repo", so the load instruction goes in CONTRIBUTING.md's
Development section instead of the scaffold.

* no-mistakes(review): Captain, align tracked-material trigger scope

* no-mistakes(document): Sync coding guidelines docs

* no-mistakes(lint): Fix Markdown style issues

* fix: add turn-end supervision guard (#249)

* feat: structural Stop-hook backstop for primary turn-end supervision

fm-guard.sh is pull-based: it only warns when some other supervision
script happens to run, so a primary session that ends a turn without
re-arming the watcher and then runs no further fleet-touching command
can sit blind for hours (the 2026-07-04 incident this fixes).

Add bin/fm-turnend-guard.sh, a Claude Code Stop hook registered in the
tracked .claude/settings.json, that fires on every primary turn end and
blocks (exit 2, verified empirically to force continuation) when work
is in flight with no fresh watcher beacon. It never blocks more than
once per turn, using Claude Code's own stop_hook_active loop-guard
field, and scopes itself to the actual primary checkout only (inert in
crewmate/scout worktrees and secondmate homes).

Factor the shared "in-flight but no live watcher" predicate out of
fm-guard.sh into bin/fm-supervision-lib.sh so the pull-based banner and
the push-based hook can never drift on what "unhealthy" means.

Document the verified Stop-hook mechanism and scoping in
docs/turnend-guard.md, add a harness-adapters note, and cover the
predicate and hook with tests/fm-turnend-guard.test.sh.

* no-mistakes(review): Respect active home in turnend guard

* no-mistakes(review): Require live watcher for turn-end guard

* no-mistakes(review): Captain: portable turn-end timing

* no-mistakes(document): Sync turn-end guard documentation

* feat(backends): auto-detect cmux runtime (#250)

* feat(backends): auto-detect cmux runtime from CMUX_WORKSPACE_ID

Wires cmux into fm_backend_detect the same way herdr already is: a
firstmate process running inside a cmux-spawned terminal now spawns
new tasks into cmux by default, no config needed. Verified from cmux's
own shipped source that CMUX_WORKSPACE_ID/CMUX_SURFACE_ID/CMUX_SOCKET_PATH
are unconditionally, non-overridably injected into every terminal
surface it spawns, and that cmux's own CLI treats CMUX_WORKSPACE_ID as
its own ambient-target fallback - the same role $TMUX/HERDR_ENV play for
their backends. CMUX_WORKSPACE_ID is checked last (after $TMUX and
HERDR_ENV=1) since cmux is a terminal application, not a nestable
multiplexer. Socket auth (config/cmux-socket-password) stays required
regardless of how the backend was selected; the existing spawn refusal
now also names the config/backend=tmux / --backend tmux opt-out for a
caller who never explicitly chose cmux.

A live env dump inside a real cmux terminal was not obtained safely on
the shared dev machine (documented in docs/cmux-backend.md); this rests
on the source read instead, mirroring this doc's existing
verified-from-source precedent.

* no-mistakes(review): Fix cmux autodetect docs and tests

* no-mistakes(document): Document cmux auto-detection

* fix(afk): support herdr away-mode injection (#251)

* fix(afk): make the away-mode daemon backend-aware for herdr

bin/fm-supervise-daemon.sh discovered its supervisor pane and injected
via raw tmux calls only, so /afk failed outright on a herdr-based
fleet (TMUX_PANE unset, firstmate:0 fallback unresolvable).

Discovery now resolves backend (tmux|herdr) and target independently,
mirroring fm-backend.sh's own runtime auto-detection, with an explicit
FM_SUPERVISOR_BACKEND override alongside the existing FM_SUPERVISOR_TARGET.
zellij/orca refuse loudly at startup instead of misapplying tmux
primitives. Injection (pane-exists probe, busy-guard, composer-guard,
verified submit) now dispatches through bin/fm-backend.sh's generic
primitives, adding a new fm_backend_composer_state dispatcher; the
tmux path is byte-identical to before. Also fixes a pre-existing bug
in fm_backend_target_exists's herdr arm (missing --session, so it
silently misrouted once more than one herdr server was running) found
while verifying this end to end against a real isolated herdr session.

Classification, batching, max-defer, the marker contract, locks, and
wake-queue handling are unchanged - this is a transport-layer fix.

* no-mistakes(review): Corroborate Herdr idle busy state

* no-mistakes(review): Stabilize Herdr daemon startup wait

* no-mistakes(review): Captain, route cmux composer and update AFK docs

* no-mistakes(document): Document AFK supervisor backend support

* docs(agents): move X-mode procedures out of AGENTS (#253)

* docs(agents): collapse X-mode section 14 into fmx-respond/docs pointers

AGENTS.md diet PR 1 of 3 (agentsmd-diet-s2 report, move-plan items 1-2).
Replaces section 14's "Answering"/"Completion follow-up"/"Conversations"/
"Length and threads"/"Preview / dry-run" blocks (54 lines) and the
"Mechanism" narrative (6 lines) with two short pointers: fmx-respond
(section 13) for the procedure, docs/configuration.md "X mode (.env)"
for the wire protocol. Net -55 lines in AGENTS.md.

Destination edits landed first, deletions second (q4 discipline):
- docs/configuration.md: added the "purely additive, watcher untouched"
  guarantee that AGENTS.md's Mechanism block stated but configuration.md
  did not.
- fmx-respond/SKILL.md: added the x-mode-error wake boundary (report as
  a blocker, do not load this skill), the --image flag for replies and
  follow-ups, the "images are for real artifacts, not prose" rule, and
  the dry-run compact-image-marker behavior - none of these were
  previously in the skill even though AGENTS.md described them, so they
  were genuine gaps, not pre-existing duplication. Also made the skill's
  own "Completion follow-up" section the sole, full owner of that
  procedure instead of deferring to AGENTS.md section 14 for substance
  that no longer lives there (two internal cross-references updated to
  point at section 8's terminal-wake trigger and the skill's own section
  instead).

Mechanical line-by-line audit of every removed AGENTS.md line:

Mechanism block (6 lines removed):
- bootstrap artifact-writing description -> already owned by
  docs/configuration.md "X mode (.env)" (locked-bootstrap paragraph)
- check-shim/poll mechanism description -> already owned by
  docs/configuration.md same section
- missing-deps/x-mode-error diagnostic description -> already owned by
  docs/configuration.md ("Relay auth or config problems...") plus
  bin/fm-x-poll.sh's own header comment for the missing-curl/jq mechanics
- opt-out artifact removal description -> already owned by
  docs/configuration.md same section
- "purely additive, no edit to fm-watch.sh/fm-watch-arm.sh/fm-wake-lib.sh/
  afk daemon" guarantee -> MOVED to docs/configuration.md (added in this
  PR; this fact had no other home before)

Answering/Completion follow-up/Conversations/Length and threads/
Preview-dry-run blocks (54 lines removed):
- x-mention wake -> load fmx-respond: already owned by section 13's
  existing trigger line (unchanged) and restated in the new pointer
- x-mode-error wake -> report as blocker, don't load fmx-respond: MOVED
  to fmx-respond/SKILL.md (added in this PR)
- inbox-draining, classification, acting, reply composition, submission,
  cleanup-on-success/failure: already owned by fmx-respond/SKILL.md
  "Procedure" section (unchanged, pre-existing)
- owner-only routing / captain-as-asker framing: already owned by
  fmx-respond/SKILL.md "The asker is your own captain" section
- standing X-mode authorization / autonomous posting / dry-run as only
  non-posting path: already owned by fmx-respond/SKILL.md same section
- acknowledge-first -> act -> follow-up shape, three-case classification:
  already owned by fmx-respond/SKILL.md "A request to act on" section
- destructive/irreversible/security-sensitive escalation guardrail:
  already owned by fmx-respond/SKILL.md "Public channel..." section and
  Procedure step 2c
- dismiss-instead-of-reply for pure acknowledgments, relay re-offer
  prevention, dry-run honoring: already owned by fmx-respond/SKILL.md
  Procedure steps 2b/2c/2e-skip and docs/configuration.md
- public-safety bar (no task ids/internals/captain-private/secrets):
  already owned by fmx-respond/SKILL.md "The reply is public" section
- never-inline-into-shell-command / --text-file or stdin: already owned
  by fmx-respond/SKILL.md Procedure step 2e and Notes
- --image flag for replies (formats, base64, no-inline guarantee): MOVED
  to fmx-respond/SKILL.md Procedure step 2e (added in this PR - this was
  not previously in the skill)
- fm-x-link field names (x_request=, x_request_ts=, x_followups=):
  already owned by AGENTS.md section 2's state/<id>.meta field list
  (untouched, out of scope for this PR) and fmx-respond/SKILL.md
- carry-count/carry-ts relink behavior, three-follow-up budget, milestone
  sparingness, --check/--text-file posting, connector/followup wire
  detail, --final clearing, cap/window graceful degradation: already
  owned by fmx-respond/SKILL.md "Completion follow-up" section (now sole
  owner) and docs/configuration.md wire-protocol paragraphs
- --image flag for follow-ups: MOVED to fmx-respond/SKILL.md "Completion
  follow-up" section (added in this PR - genuine gap)
- "failed task still gets an honest final follow-up": already owned by
  fmx-respond/SKILL.md "Completion follow-up" section
- FMX_DRY_RUN whole-loop previewability: already owned by
  fmx-respond/SKILL.md "Dry-run / preview mode" section
- in_reply_to conversation continuity, untrusted-thread handling,
  follow-up worthiness judgment, relay-owned self-reply guard/cap:
  already owned by fmx-respond/SKILL.md "The direct ask is the captain's"
  section and Notes (one bullet is a verbatim match)
- concise-by-default / no hand-numbered threads: already owned by
  fmx-respond/SKILL.md "Voice" section
- auto-split behavior, char/tweet caps, premium-independence, wire shape
  ({text}/{text,texts}): behavior already owned by fmx-respond/SKILL.md
  Voice section; exact defaults and wire shape already owned by
  docs/configuration.md; "premium-independent" mechanics already owned
  by bin/fm-x-reply.sh's own header comment
- "images are for real artifacts, not prose": MOVED to fmx-respond/
  SKILL.md "Voice" section (added in this PR - genuine gap)
- image-on-thread wire behavior: already owned by docs/configuration.md;
  reinforced in fmx-respond/SKILL.md's new --image note
- dry-run POST-body shape, endpoint marker, truthy-value definition,
  jq-only dependency, end-to-end testability, x-outbox inspection:
  already owned by fmx-respond/SKILL.md "Dry-run / preview mode" section
  (several near-verbatim matches) and docs/configuration.md wire detail
- dry-run compact image marker: MOVED to fmx-respond/SKILL.md "Dry-run /
  preview mode" section (added in this PR - genuine gap)

Section 8's terminal-wake completion-follow-up trigger (the one fact
required to survive inline) is untouched and already present; the new
section 14 pointer references it instead of restating it.

Nothing outside section 14 (plus the two destination files) is touched.
Full test suite green, including all 74 fm-x-mode.test.sh checks.

* no-mistakes(review): Preserve X-linked follow-up triggers

* no-mistakes(review): Fix x-mode error trigger

* no-mistakes(document): Docs cross-reference synchronized

* no-mistakes(lint): Clean Markdown lint pass

* docs: trim duplicated harness guidance (#255)

* docs(agents): trim section 4 harness/secondmate duplication

AGENTS.md diet PR 2 of 3 (data/agentsmd-diet-s2/report.md, move-plan
items 3-4; redundancy item 2 folded into item 3).

Removed the five claude/codex/grok/pi/opencode model/effort-flag
bullets from section 4 - byte-for-byte duplicated by
harness-adapters' "Launch profile axes" table (which is already a
superset: it carries verified CLI versions per adapter that the
AGENTS.md bullets lacked). Replaced with a one-line pointer; the
skill is already loaded before every spawn per section 4's own
closing trigger, so no new trigger was needed.

Moved the config/secondmate-harness model/effort pin-format detail
(the `<harness> [<model>] [<effort>]` line format, the
secondmate-model/secondmate-effort accessors, back-compat, and the
durability-across-respawn behavior) into secondmate-provisioning,
which is already a mandatory load at every secondmate lifecycle
touchpoint. Added the destination content to the skill first, then
replaced the AGENTS.md paragraph with a 3-line pointer.

Mechanical audit - every removed line's new home:
- 5 harness bullets (claude/codex/grok/pi/opencode model+effort
  flags, per-harness max-omission rationale) -> already present in
  harness-adapters SKILL.md's "Launch profile axes" table (lines
  53-59), confirmed fact-by-fact before deleting.
- "config/secondmate-harness may also pin..." paragraph (pin format,
  bare-harness back-compat, secondmate-model/secondmate-effort
  accessors, per-spawn override precedence, respawn durability,
  secondmate-only scope) -> secondmate-provisioning SKILL.md's
  "Charter and seed" section, added verbatim before this trim.
- The following paragraph (inheritable config: crew-dispatch.json,
  crew-harness, backlog-backend) is untouched - out of scope for
  this PR, still inline.
- The bootstrap CREW_DISPATCH effort-mismatch diagnostic sentence is
  untouched - not part of the five-bullet duplication, stays inline.

No script changes. Section 4 shrinks from 104 to 87 lines
(958 -> 901 total AGENTS.md lines) with zero facts lost: every fact
is reachable through harness-adapters or secondmate-provisioning,
both already mandatory loads at the relevant lifecycle points.

* no-mistakes(document): Align secondmate skill triggers

* no-mistakes(lint): Markdown style clean

* fix: anchor turn-end Stop hook to project root (#256)

* Fix turn-end Stop hook to use CLAUDE_PROJECT_DIR path

Claude Code runs hook commands via /bin/sh from the session cwd, so the
bare relative bin/fm-turnend-guard.sh path fails when cwd is not the repo
root. Anchor the command with "$CLAUDE_PROJECT_DIR"/bin/fm-turnend-guard.sh
instead; verified CLAUDE_PROJECT_DIR is set on Stop hooks in Claude Code
2.1.201. Document the cwd caveat and add a settings.json regression test.

* no-mistakes(document): Document Stop hook path anchoring

* docs: trim firstmate agent guidance duplication (#258)

* docs: trim AGENTS.md redundancy (diet PR 3/3)

Consolidates five duplicated passages to a single owner each, per
data/agentsmd-diet-s2/report.md redundancy items c3-c7:

- Inheritable-config propagation mechanism: owned by section 3 (where
  the sweep runs); sections 4 and 7 keep compact references. Section 4
  retains its one genuinely unique fact (crew-harness inherit-vs-fallback
  semantics), just no longer restates the propagation mechanism itself.
- Landed-work definition: owned by section 7's ship-teardown detail
  (PR-containment mechanics, pr= discovery fallback); section 1's hard
  rule #3 keeps the rule plus a three-case summary and a pointer.
- Backend meta-field enumeration: owned by docs/configuration.md
  ("Runtime backend", already comprehensive including cmux) and each
  backend's own doc; AGENTS.md keeps only the fields common to every
  task plus a pointer.
- Dropped one redundant restatement of "silence is correct while
  waiting" in section 8.
- Worktree-tangle guard explanation: owned by section 8 (already the
  fuller, cross-referenced version); section 3's TANGLE bullet keeps
  the remediation action and points at section 8 for the why.

Also adds two captain-requested single-sentence rules: invoke bin/
scripts by absolute $FM_ROOT path after any cd away from the home, and
a backend spawn refusal must be surfaced to the captain rather than
silently worked around by switching backends.

AGENTS.md: 901 -> 889 lines, 112355 -> 108560 bytes.

* no-mistakes(review): Clarify post-cd bin invocation guidance

* no-mistakes(document): Sync AGENTS trim docs

* no-mistakes(lint): Fix Markdown line style

* feat(backends): improve cmux detection and socket-mode guidance (#259)

* feat(backends): cmux detection fallbacks and socket-mode matrix

Workstream A: cmux's bundled claude wrapper strips every CMUX_* env var on
its passthrough path (reproduced live 2026-07-04, cmux 0.64.17), so a
claude-harness firstmate inside a cmux tab has no CMUX_WORKSPACE_ID.
fm_backend_detect now falls back - macOS-only, only when the primary marker
is absent - to __CFBundleIdentifier=com.cmuxterm.app and then a process
ancestry walk resolved by bundle id (lsappinfo) plus a bundle-shaped ps comm
match. Innermost-first ordering is unchanged and absorbs the
tmux-inside-cmux bundle-id false positive; the auto-detect NOTICE names the
winning fallback signal.

Workstream B: the five socketControlMode values were traced through cmux
source (commit 9c91710e3f58): off/cmuxOnly can never admit an external CLI,
automation admits same-user clients with no secret (0600 socket only),
password needs the auth handshake, allowAll opens the socket to every local
user (0666). Automation mode is now the documented recommendation; the
adapter's refusals name every viable mode, classify Invalid password as
unauth, and the launch-timeout message names the off-mode possibility.

Docs carry the wrapper-strip empirical record, the fallback contract and
authority split, and the full mode matrix with rationale; tests cover the
new detection paths, the nested false positive, and the refusal wording.

* no-mistakes(review): Document cmux fallback detection

* no-mistakes(review): Update cmux architecture docs

* no-mistakes(document): Align cmux backend docs

* fix(backends): scope zellij tabs by firstmate home (#252)

* fix(backends): home-scope zellij tab titles to close cross-home collision gap

Zellij's one shared "firstmate" session has no per-home split and enforces
no tab-name uniqueness, so two firstmate homes with colliding task ids could
send/peek/close each other's tabs - the same gap a no-mistakes review gate
caught for cmux (docs/cmux-backend.md). Ports that fix: every new tab is
created with a home-scoped title (fm-<home-label>-<id>), and every
list/find/recover/kill path scopes matches to this home's own tag. A tab
spawned before this change still matches via its old untagged bare title,
but only when unambiguous - two live tabs sharing a bare title refuse rather
than guessing which one is ours.

Factors the home-label/hash derivation shared with cmux into
bin/fm-backend-hometag-lib.sh so the two adapters can't drift.

* no-mistakes(review): Fix zellij child teardown home tag

* no-mistakes(review): Fix zellij teardown and selector scoping

* no-mistakes(document): Sync zellij home-scope docs

* fix: sync project clones after merged PR wakes (#293)

* fix(fleet-sync): auto-sync on merged-PR wake, accept project name

fm-fleet-sync.sh's single-project form failed on a bare project name
("not a directory"), forcing hand-typed full paths (4 manual runs in
one incident). It now resolves a bare name or projects/<name> against
the home's projects dir.

AGENTS.md now encodes the trigger: a wake whose status reports a
merged PR for a project cloned in this home runs fleet-sync for that
project as part of handling the wake, so a secondmate-reported merge
does not leave the primary's clone stale until the next session start
or teardown.

* no-mistakes(review): Fix fleet-sync project name shadowing

* no-mistakes(document): sync fleet-sync docs

* fix: canonicalize spawn worktree path checks (#294)

* fix(spawn): canonicalize worktree-isolation guard against symlinked project prefixes

fm-spawn.sh compared a logical PROJ_ABS against the physically-resolved
pane cwd every backend reports, so a project reached through a symlinked
prefix (e.g. macOS's /tmp -> /private/tmp) could trip the isolation
guard's false refusal before treehouse ever moved the pane. Canonicalize
once into PROJ_ABS_REAL and compare against that everywhere instead.

* no-mistakes(review): Canonicalize spawn cwd comparisons

* no-mistakes(document): Refresh symlinked spawn docs

* docs: add Orca operator skill (#276)

* docs: add Orca operator skill

* no-mistakes(document): Document Orca checklist

---------

Co-authored-by: Stephen Brouhard <vesta@stephens-macbook-air.tail2122af.ts.net>

* fix: surface green PRs during CI monitoring (#297)

* fix(crew-state): detect green-PR CI monitoring, escalate repeat wedges

fm-crew-state.sh's ci step never distinguishes "still waiting on checks"
from "checks green, waiting on merge" via axi status alone, since a repo
that defers merge to the captain keeps the ci step at status=running for
the whole monitor phase. Read the ci step's own log tail (axi logs) for
the checks-passed marker and surface done instead of a false "validating
(running)" - verified against the real PR #252 run's ci.log.

The watcher's wedge timer can re-escalate the same stale pane forever
without ever signaling that it is a repeat; track a per-pane consecutive
escalation count and add a demand-deep-inspection marker to the wake
payload once it crosses a threshold, so the supervisor can no longer
dismiss each one as an isolated, still-validating pane.

Also clarify the ship-brief's checks-green line: it is owed at the
CI-ready return point, not after the background monitor-until-merge
loop finishes.

* no-mistakes(review): Captain, distinguish pending no-checks CI marker

* no-mistakes(review): Harden CI relapse handling

* no-mistakes(review): Block stale done during fixing

* no-mistakes(review): Captain, tighten CI status gating

* no-mistakes(review): Captain, harden stale CI green handling

* no-mistakes(review): Captain, recognize ranged CI rearm markers

* no-mistakes(document): Sync crew-state supervision docs

* fix(teardown): recover provably stale git index locks (#296)

* fix(teardown): recover from a stale worktree git index.lock

A crew process killed mid-git-operation can leave a stale
.git/worktrees/<wt>/index.lock behind, making fm-teardown.sh's
`treehouse return --force` fail closed. On that failure, retry once
after a short wait (the owning process may be exiting), then remove
the lock and retry once more only when it is provably stale: old
enough by mtime and lsof shows no live holder on the lock or the
worktree itself. A lock that isn't provably stale is left in place and
the original failure still surfaces.

* no-mistakes(review): Harden teardown lock refusal paths

* no-mistakes(review): Harden stale-lock teardown safety rechecks

* no-mistakes(review): Harden stale teardown lock checks

* no-mistakes(document): Document teardown lock recovery

* feat(bin): encode project AGENTS.md authoring bar with canonical self-governance section (#307)

* Encode project AGENTS authoring bar

* no-mistakes(review): Captain, centralize CLAUDE promotion governance

* no-mistakes(review): make ensure_maintenance_section idempotent-success, drop || true guards

* no-mistakes(review): separate appended maintenance section on newline-less CLAUDE.md promotion

* no-mistakes(review): assert maintenance heading present before separator check in test

* no-mistakes(document): sync docs with AGENTS.md authoring bar and self-governance

---------

Co-authored-by: fmtest <fmtest@example.invalid>

* feat(skills): add captain-invocable bearings status-report skill (#300)

* Add captain-invocable bearings skill

Generates a pick-up-where-I-left-off status report from live fleet
state to data/status-report-<YYYY-MM-DD>.md plus a concise chat
summary. Read-mostly procedure: reads backlog, per-task crew state
via bin/fm-crew-state.sh, open PRs via gh-axi, scout reports,
pending decisions, and date-gated queued work; composes the
exemplar's sections (TL;DR, Check first, Landed, In flight, Plans,
Decisions pending, Date-gated/queued); never tears down, merges, or
mutates task state as a side effect.

* no-mistakes(document): docs: list new /bearings skill in README built-in skills table

* fix(watcher): make PID identity locale-invariant (#285)

* fix(watcher): pin LC_ALL=C in fm_pid_identity for locale-invariant identity

ps's lstart date format follows the caller's LC_TIME/LC_ALL. The watcher records
its process identity under one locale, but arm/guard/turn-end re-read it under the
machine's ambient locale. On a non-C locale (e.g. ko_KR) the two strings differ
only in the date portion, so fm_watcher_lock_matches_pid / fm_watcher_healthy
reject a genuinely live watcher - breaking fm-watch-arm.sh, fm-guard.sh, and
fm-turnend-guard.sh on every non-C-locale machine.

Pin LC_ALL=C on that one ps call so the write and read sides agree regardless of
machine locale, matching the LC_ALL=C determinism the file already uses elsewhere.
Add a colocated regression test asserting fm_pid_identity is locale-invariant
across exported LC_ALL/LC_TIME.

* no-mistakes(document): Document watcher PID identity coverage

* docs: document codex app backend contract (#222)

* docs: reconcile Codex App backend contract

* no-mistakes(document): Sync backend docs

* docs: clarify Codex Desktop bridge blocker

* no-mistakes(document): Align Codex App backend docs

* no-mistakes(test): Captain, stabilize watcher self-eviction test cadence

* no-mistakes(document): Document Codex App backend contract

* no-mistakes(document): Captain, document blocked codex-app coverage

* docs: make Codex App contract doc authoritative

* no-mistakes(document): Align Codex App backend docs

* docs: redact local Codex App smoke paths

---------

Co-authored-by: Stephen Brouhard <vesta@stephens-macbook-air.tail2122af.ts.net>

* docs: add Codex Desktop coordination skill (#275)

* docs: add Codex App coordination skill

* no-mistakes(review): Captain, mark Codex App skill agent-only

* no-mistakes(document): Document Codex App backend boundary

* no-mistakes(document): Captain, document Codex Desktop backend boundary

* no-mistakes(lint): Captain, lint clean

* no-mistakes(document): Document Codex Desktop boundaries

* docs: narrow Codex App skill playbook

* fix(afk): stop herdr escalation redelivery loop (#317)

* fix(afk): recognize unbordered herdr composer rows to stop escalation redelivery loop

fm_backend_herdr_composer_state only recognized bordered composer rows
(the grok shape). Real claude and codex render their live input row
with no border at all, so once a harness's own startup banner scrolled
out of the capture window the classifier read the composer as unknown
forever. fm_backend_herdr_send_text_submit never confirmed "empty", so
escalate_flush never cleared state/.subsuper-escalations, and the
away-mode daemon retyped and resubmitted the same buffered digest every
housekeeping cycle - reproduced live against a real herdr+claude pane
(5+ identical deliveries in 40s).

The classifier now recognizes an unbordered (bare) composer row led by
a known prompt glyph alongside the existing bordered shape, keeping
whichever match is bottom-most so a stale decorative box never
outranks the live composer.

* no-mistakes(review): Narrow herdr bare prompt matcher

* no-mistakes(document): Sync herdr composer docs

* fix(backends): confirm Herdr submits with native agent state (#323)

* fix(herdr): confirm message submit via native agent-state, not composer text

fm_backend_herdr_send_text_submit now confirms a landed submit by polling
herdr's own agent-state (agent get) for the idle->working transition instead
of reading composer content. Composer scraping remains, unchanged, for the
away-mode daemon's pre-injection empty-box guard only.

This fixes the practical effect of the codex idle-tip gap from the
2026-07-07 incident: codex's dynamic idle-composer hint text can no longer
misread as pending and block/mis-confirm a send, since confirmation no
longer looks at composer text at all. Verified empirically against real
claude and codex agents (timing, swallowed-Enter, unreadable-target, and
already-busy-target scenarios), and against the real away-mode daemon
end-to-end after updating its synthetic supervisor-pane test fixture to
register itself as a real herdr agent (herdr's own report-agent primitive)
so it can still exercise the new confirmation path.

* no-mistakes(review): Captain, harden herdr submit confirmation

* no-mistakes(review): Captain, harden herdr submit confirmation

* no-mistakes(document): Sync Herdr submit docs

* no-mistakes: apply CI fixes

* feat: add quota-balanced crew dispatch selection (#327)

* Add quota-balanced dispatch selection

* no-mistakes(document): Document dispatch selector guidance

* fix(session-start): respawn dead secondmate agents conservatively

* fix(session-start): deterministically respawn dead-shell secondmates

A secondmate agent that exits leaves its backend pane alive as a bare
shell. The session-start endpoint check only verified pane presence, so
recovery and the watcher (which exempts secondmates from stale-pane
detection) never noticed - evidence 2026-07-07: every secondmate in one
fleet was found sitting at a dead zsh shell.

Add fm_backend_agent_alive (bin/fm-backend.sh), a deeper per-backend
liveness probe distinct from pane presence: fm_backend_tmux_agent_alive
classifies the pane's live foreground process via tmux's own
pane_current_command, and fm_backend_herdr_agent_alive reuses the
already-verified pane_agent_state husk classifier. Both are conservative:
anything ambiguous reports unknown, never a false dead.

Wire this into a new session-start-only, locked-and-primary-only sweep in
bin/fm-bootstrap.sh that kills and respawns only a confidently dead
secondmate endpoint, leaving alive/unknown readings untouched - idempotent
by construction, so repeated runs converge without duplicating agents.

* no-mistakes(review): Guard raw secondmate liveness respawns

* no-mistakes(review): Fix detect-only bootstrap test

* no-mistakes(test): Pin liveness fixture harness

* no-mistakes(document): Sync secondmate liveness docs

* no-mistakes: apply CI fixes

* fix: emit stable secondmate nudge selectors (#331)

* Fix NUDGE_SECONDMATES to print stable fm-<id> selectors.

Session-start secondmate sync used to accumulate raw backend window targets
into NUDGE_SECONDMATES, but the liveness sweep in the same bootstrap run can
respawn secondmates onto new endpoints. fm-send with those stale explicit
targets bypasses meta resolution and fails, while fm-<id> resolves correctly.

Accumulate fm-<id> in process_secondmate, update the bootstrap/update contracts
and /updatefirstmate skill, and add a herdr respawn regression test.

* no-mistakes(review): Captain, guard herdr regression jq dependency

* no-mistakes(document): Document stable secondmate nudge selectors

* no-mistakes(lint): Fix shell lint hints

* feat: require bootstrap detection for AXI tools (#332)

* Make tasks-axi and quota-axi required bootstrap tools

Add both to the normal toolchain checks alongside lavish-axi, keep the
tasks-axi 0.1.1+ compatibility gate, and report quota-axi through the
standard MISSING install-consent flow. TASKS_AXI: available remains a
backlog-backend capability signal only; manual opt-out no longer suppresses
the missing-tool report.

Update bootstrap tests and point docs/configuration.md at the canonical
toolchain contract.

* no-mistakes(review): Clarify manual backlog bootstrap reporting

* no-mistakes(document): Document bootstrap AXI tools

* bearings: delete today's report before recreating (#333)

Replace overwrite-in-place wording with explicit delete-then-create
instructions so agents do not modify an existing daily report file.

* feat: guard primary turn ends across harnesses (#339)

* Add primary turn-end guards for all harnesses

* no-mistakes(review): Normalize Codex hook cwd resolution

* no-mistakes(review): Fix OpenCode guard worktree anchoring

* no-mistakes(review): Anchor Codex guard outside nested roots

* no-mistakes(review): Anchor Codex guard to hook root

* no-mistakes(review): Avoid Grok permission escalation

* no-mistakes(document): Sync turn-end guard docs

* fix: resolve backend selectors by exact task id first (#342)

* fix backend selector task id resolution

* no-mistakes(document): Document selector resolution behavior

* fix: scale bootstrap fleet-sync timeout (#341)

* fix bootstrap fleet sync timeout

* no-mistakes(review): Fix bootstrap fleet-sync timeout regressions

* no-mistakes(document): Sync bootstrap timeout docs

* no-mistakes(lint): Clean ShellCheck directives

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* feat: add fleet snapshot and view commands (#343)

* Add fleet snapshot and view

* no-mistakes(review): Fix fleet snapshot parsing and overrides

* no-mistakes(review): Fix secondmate fleet rendering

* no-mistakes(review): Fix backlog title and completion parsing

* no-mistakes(review): Include durable scout reports

* no-mistakes(review): Fix fleet snapshot edge cases

* no-mistakes(review): Captain: gate fleet hints on current state

* no-mistakes(review): Captain: parse bracketed Done PR artifacts

* no-mistakes(document): Sync fleet snapshot docs

* fix(fm-send): fail loudly on unresolvable send targets (#254)

* Make fm-send fail loudly on unresolved targets

* no-mistakes(review): Document fm-send FM_HOME contract

* Fix fm-send readiness docs and backend send path

* Fix fm-send docs for cmux and X skill metadata

* Make gotmp teardown test home-explicit

* Scope watcher warning wording to fm-send

* Fix fm-send review findings

* Verify explicit tmux targets before sending

* Isolate turnend guard test home

* no-mistakes(document): Documented fm-send FM_HOME/backend guard additions missing from doc inventories

---------

Co-authored-by: mielyemitchell <249051873+mielyemitchell@users.noreply.github.com>

* fix: deliver AFK escalations through herdr supervisors (#353)

* fix afk codex ghost composer delivery

* no-mistakes(review): Harden AFK startup flag writes

* no-mistakes(review): Harden AFK daemon liveness checks

* no-mistakes(document): Sync AFK herdr docs

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* feat: add harness-aware supervision (#367)

* Add harness-aware supervision

* no-mistakes(review): Captain, harden watcher supervision regressions

* no-mistakes(review): Captain, harden watcher supervision cadence

* no-mistakes(review): Harden watcher supervision ownership

* no-mistakes(review): Captain, harden Pi extension marker

* no-mistakes(review): Captain, harden Pi supervision restart checks

* no-mistakes(review): Harden watcher ownership checks

* no-mistakes(review): Captain, harden Pi supervision loading

* no-mistakes(review): Captain, require Pi guard extension loading

* no-mistakes(review): Captain, harden watcher supervision recovery

* no-mistakes(test): Fix fm-send baseline log filtering

* no-mistakes(document): Sync harness supervision docs

* no-mistakes: apply CI fixes

* fix: split X-mode replies by platform (#369)

* fix: make x replies split by platform

* no-mistakes(review): Captain: preserve Discord recovery relink context

* no-mistakes(test): Captain: keep split markers outside fences

* no-mistakes(document): Sync X-mode reply docs

* fix: make stow memory writes inspect before update (#372)

* docs: make stow inspect-then-update

* no-mistakes(review): Remove unsupported archive-body guidance

* no-mistakes(review): Clarify stow read-before-write exception

* no-mistakes(test): Require archive-body for stow task notes

* no-mistakes(document): Sync stow memory docs

* no-mistakes(lint): Silence ShellCheck source warning

* fix(watcher): wait when arm attaches to a healthy watcher (#375)

* fix: attach-and-wait when arm finds a healthy watcher

Grok and Claude re-arm after every turn with work in flight. When a
watcher was already healthy, fm-watch-arm exited immediately with
watcher: healthy, which completed the harness background task and
injected an empty false wake.

Attach to the live identity-matched holder instead, stay until that
cycle ends, then exit 0 so notify fires for a real end-of-cycle. The
peer-startup-race path uses the same contract. --restart and the
started path are unchanged.

* no-mistakes(review): Gate restart watcher peer attach

* no-mistakes(document): Sync watcher arm docs

* feat(pi): simplify primary session launch (#386)

* docs(readme): reformat Quick Start and recommend Grok equally with Claude Code

* no-mistakes(review): Captain: align harness launch guidance

* no-mistakes(review): Captain, clarify Pi supervised launch

* no-mistakes(review): Captain, document Pi first-launch bridge

* feat(pi): track primary watcher extension for plain-pi launch

Move Pi's primary watcher bridge from a generated state/ file to a
tracked .pi/extensions/fm-primary-pi-watch.ts, matching how the turn-end
guard extension already works: self-hashing version, project-local
auto-discovery after one-time Pi trust. This drops the state/-generation
step and dual -e requirement from the happy path, so Pi's Quick Start
launch becomes plain 'pi', the same friction class as 'claude' and
'grok --trust'.

- bin/fm-pi-watch-extension.sh is removed; nothing generates the
  extension anymore since it is committed.
- fm-session-start.sh and fm-supervision-instructions.sh resolve the
  watcher extension path from FM_ROOT instead of state/, and the
  session-start diagnostic now points at restarting plain pi after
  trust, with -e as a documented fallback.
- fm-spawn.sh points Pi secondmate launches at the tracked extension
  path in the secondmate home instead of generating a state/ copy.
- README Quick Start Pi block is now just 'pi' plus a trust note.
- Tests, docs, and the harness-adapters skill updated to match.

* fix(pi): drop backticks from session-start diagnostic to satisfy shellcheck SC2016

* feat(supervision): prevent unsafe watcher-arm commands (#387)

* feat(supervision): add PreToolUse seatbelt against watcher-arm anti-patterns

Adds bin/fm-arm-pretool-check.sh, a shared PreToolUse-style checker that
denies a primary shell command backgrounding, piping, or bundling the
watcher arm/checkpoint, or force-killing the watcher process broadly -
the exact shapes that silently took Grok's supervision down. Wires it
into all five verified harnesses (grok, claude, codex, opencode, pi),
each validated empirically against the real harness.

Also fixes a grok 0.2.93 regression discovered during that validation: the
existing turnend-guard Stop hook's bare root variable broke grok's own
variable pre-substitution and silently no-op'd the hook.

* no-mistakes(review): Harden watcher arm validation

* no-mistakes(review): Harden arm guard metacharacter checks

* no-mistakes(review): Harden nested shell arm guard

* fix(lint): rewrite SC2015 guards in fm-arm-pretool-check.sh as if/then

A && B || C is not if-then-else; C can run when A is true. Replace both
occurrences of the quote-state early-continue with an explicit if/then.

* fix(pi): restore primary watcher supervision lifecycle (#397)

* fix Pi primary supervision lifecycle

* no-mistakes(document): Synchronize Pi primary extension documentation

* fix: keep persistent secondmates out of the main backlog (#398)

* fix secondmate backlog guidance

* no-mistakes(review): Require reasons for captain backlog holds

* no-mistakes(test): Document secondmate handoff skill requirement

* fix secondmate teardown reminder

* no-mistakes(document): sync teardown reminder docs to work-items-only backlog contract

* fix(backlog-handoff): move full item blocks including indented bodies (#401)

* fix(backlog-handoff): move full item blocks including indented bodies

fm-backlog-handoff only moved the checklist header line, so multi-line
item bodies were left orphaned in the source backlog and never reached
the secondmate. Move the full block (header plus indented body lines)
atomically, treating body membership by indentation so lines like
## Intent stay with the item, and add regression coverage.

* no-mistakes(review): Captain: preserve EOF handoff terminators

* no-mistakes(review): treat blank lines inside item bodies as movable body

* no-mistakes(document): sync backlog-handoff docs with full-block move behavior

* feat(herdr): make Herdr lab lifecycle safety deterministic for briefs (#402)

* guard Herdr lab lifecycle in briefs

* no-mistakes(review): Fix Herdr lab helper and provisioning safety

* no-mistakes(review): Captain, harden Herdr lab lifecycle safety

* no-mistakes(review): fix Herdr lab test cleanup ordering and brief help range

* no-mistakes(review): reject leading options in Herdr lab run guard

* no-mistakes(review): strip leading non-alnum in Herdr lab name generator

* no-mistakes(document): document Herdr lab helper and --herdr-lab brief flag

* no-mistakes(lint): add shellcheck disable for deliberate SC2016 literals in fm-brief herdr-lab

* no-mistakes: apply CI fixes

* fix(watcher): classify arm-command seatbelt by execution position (#403)

* fix watcher arm command policy

* no-mistakes(review): Harden watcher command policy parsing

* no-mistakes(review): Captain: harden watcher policy parsing

* no-mistakes(review): harden watcher policy for expanded paths, direct-watch, and sound prefilter

* no-mistakes(review): close prefilter and classifier locale/ANSI-C watcher-path decode gaps

* no-mistakes(review): fail closed on loop-wrapped broad watcher kills

* no-mistakes(document): sync docs for watcher-arm command-position policy

* fix: reconcile existing AGENTS.md safely (#405)

* fix(agents-md): inject self-governance section into existing AGENTS.md

fm-ensure-agents-md.sh only appended the canonical "## Maintaining this
file" section on skeleton create or CLAUDE.md promotion, so an existing
AGENTS.md that lacked it exited unchanged and forced hand-copying the
wording during a rollout across existing projects. Call the already-
idempotent ensure_maintenance_section on the existing-AGENTS.md paths and
report whether the file changed; a re-run and an already-complete file stay
byte-identical.

Also fixes #389: refuse a case-variant real memory file (e.g. a lowercase
agents.md) instead of silently emitting a CLAUDE.md symlink whose uppercase
literal target dangles once the tree lands on a case-sensitive filesystem.

Tests extend tests/fm-ensure-agents-md.test.sh; skeleton-create and
CLAUDE.md-promotion regressions still pass. Docs updated to match.

* no-mistakes(review): Captain: preserve CRLF maintenance-section idempotency

* no-mistakes(review): Preserve CRLF during maintenance-section injection

* no-mistakes(review): Captain: harden dangling-symlink regression coverage

* no-mistakes(document): Document agent-memory injection outcomes

* feat: support project-less secondmate homes (#409)

* feat(secondmate): support project-less homes via --no-projects

fm-brief.sh --secondmate and fm-home-seed.sh now accept an explicit
--no-projects signal to scaffold, seed, and register a secondmate home
whose subject is the firstmate repo itself (no clones). The signal is
mutually exclusive with a project list; omitting both still fails loudly
so an accidental omission is never a silent project-less seed. The
registry line renders an empty projects: field, which spawn and the
snapshot already tolerate. Docs updated in the secondmate-provisioning
skill and both script headers.

* no-mistakes(review): Captain: document project-less secondmate flow

* no-mistakes(review): Captain: refuse project-less reseeding of populated homes

* fix(seed): fail closed on unreadable project data

* no-mistakes(review): Captain: reject stale projectful charters

* no-mistakes(review): Captain: fail closed on unsafe project paths

* no-mistakes(review): Captain: validate project-less charter clone sections

* no-mistakes(document): Document project-less secondmate seeding

* fix: delegate backlog handoffs to tasks-axi (#411)

* wip(handoff): record verified delegation design + tasks-axi mv blocker

No production code changed yet. tasks-axi mv (v0.2.1) cannot atomically
move a blocked-by-linked item set across backlogs (deadlocks both orders,
no batch/--force), which fm-secondmate-lifecycle-e2e requires. Parked
pending a tasks-axi connected-set mv enhancement; note captures the
verified design, semantics, test/CI/doc changes, and resume checklist.

* refactor(handoff): delegate the item move to tasks-axi mv

fm-backlog-handoff.sh's two-pass awk was a second parser of the backlog
format and the source of the PR #401 body-orphaning drift. Delete it and
delegate the move to `tasks-axi mv <id>... --to <dest>` (v0.2.2 atomic
multi-id), the single owner of the format: a connected set (blocker plus
dependents) moves together with blocked-by preserved, item blocks stay
byte-exact, and destination section placement holds. The helper keeps only
the fleet-level validation tasks-axi cannot know - secondmate-home
resolution, the seeded-home safety checks, the In-flight refusal, and
idempotent per-key reporting - and is atomic: on any move failure nothing
moves.

Tests: fm-backlog-handoff.test.sh keeps PR #401's regression matrix but now
exercises the delegated path and skips cleanly when tasks-axi is absent; the
two whole-file fixtures move to tasks-axi's canonical whitespace. The
lifecycle-e2e and safety move-cases gain the same skip guard. CI installs
tasks-axi so the delegated path is exercised. Docs state that
config/backlog-backend=manual governs firstmate's own hand-editing, not this
validated helper, which delegates fleet-wide because bootstrap requires
tasks-axi on PATH.

Remove the now-redundant WIP design note.

* no-mistakes(review): Captain: harden atomic backlog handoffs

* no-mistakes(review): Captain: enforce queued-only backlog handoffs

* no-mistakes(review): Captain: harden handoff section parsing

* no-mistakes(document): Document delegated backlog handoffs

* no-mistakes(lint): Silence ShellCheck source diagnostics

* fix: ignore secondmate home marker during sync (#417)

* fix: gitignore the secondmate home marker

bin/fm-home-seed.sh writes an untracked .fm-secondmate-home marker into
every seeded secondmate home. A secondmate home is a worktree of the
firstmate repo, so any plain `git status --porcelain` dirtiness check
counted the untracked marker and the home read as dirty forever:
fleet-sync reported it STUCK and the local fast-forward convergence
sweeps risked leaving it stale on firstmate updates.

Add .fm-secondmate-home to the tracked .gitignore so the marker is
invisible to every dirtiness check uniformly, without weakening
fleet-sync's deliberate untracked-counting for project clones.

Convergence chicken-and-egg: existing homes predate the fix and it only
arrives by fast-forward. The already-present marker-tolerant ff-skip
(ignore_seed_marker=yes, used by the bootstrap sweep, /updatefirstmate,
and spawn pre-launch) advances such a home past the fix commit, after
which .gitignore takes over - no hand intervention.

Tests in tests/fm-secondmate-sync.test.sh cover a freshly seeded home
reading clean, an existing marker-only home converging then reading
clean, and a genuinely dirty home still skipping.

* no-mistakes(review): Captain: document standalone-clone update path

* no-mistakes(document): Document secondmate marker migration

* fix(composer): prevent dead-shell message injection (#416)

* fix(composer): stop reading dead-shell prompts as empty agent composers

Consolidate composer empty/pending/unknown classification into one shared
owner, bin/fm-composer-lib.sh's fm_composer_classify_content, delegated to by
all four backend adapters (tmux via fm-tmux-lib.sh, herdr, orca, cmux). This
replaces four drifting copies of the glyph decision.

Safety fix: a bare shell prompt glyph (> $ % #) on an unstructured row is now
classified unknown (a dead shell, unsafe for injection), not empty. It is only
empty inside a bordered composer box (the harness's own prompt). Agent glyphs
❯ (claude) and › (codex) read empty either way. The away-mode injector
(inject_msg) now requires an affirmatively-empty composer, deferring on pending
or unknown, so an escalation can never be typed into (or executed by) a pane
whose agent exited to its login shell.

Regression coverage: new tests/fm-composer-lib.test.sh pins the shared owner;
per-backend dead-shell tests in fm-daemon (tmux + injector), orca, and the
existing herdr/cmux suites. shellcheck clean; herdr incident regressions stay
green.

* no-mistakes(review): Captain: harden composer safety checks

* no-mistakes(test): Stabilize Herdr prune safety setup

* no-mistakes(document): Document composer injection safety

* no-mistakes(lint): Clean composer safety lint

* no-mistakes: apply CI fixes

* feat(watcher): add paused external-wait supervision (#421)

* feat(watcher): add paused/awaiting-external crew state

A crew (or firstmate steering it) can declare a deliberate wait on a known
external dependency with a paused: <reason> status. Both the always-on watcher
and the away-mode daemon absorb such an idle pane through shared fm-classify-lib.sh
vocabulary instead of tripping the possible-wedge stale escalation, and re-surface
it for a recheck only on a long bounded cadence (FM_PAUSE_RESURFACE_SECS) so a
forgotten pause cannot rot invisibly. fm-crew-state.sh reports state: paused
distinctly. A crew that goes idle without declaring a pause classifies exactly as
before. Docs and brief scaffold state lists updated; tests colocated.

* no-mistakes(review): Captain: fix paused-state transitions

* init

* no-mistakes(review): Captain: fix paused-state supervision transitions

* no-mistakes(review): Captain: fix paused supervision handoffs

* no-mistakes(review): Reconcile paused supervision markers

* no-mistakes(review): Captain: prioritize paused states over captain relevance

* no-mistakes(review): Captain: preserve paused-working wedge timer

* no-mistakes(review): Captain: honor configured pause verb in briefs

* no-mistakes(test): Captain: fix AFK paused watcher handoff

* no-mistakes(document): Document declared external waits

* no-mistakes(lint): Clean paused-state lint

---------

Co-authored-by: fmtest <fmtest@example.invalid>

* fix: preserve X-mode follow-up platform limits (#425)

* fix(x-mode): make follow-up platform splitting immune to link ordering

A ~470-char Discord follow-up posted as a (1/2)(2/2) thread split at ~280
chars because fm-x-link only learned the platform from the inbox payload,
and the fmx-respond ack path can drain that inbox file before the task is
linked. A link recorded after cleanup silently lost the platform and the
splitter defaulted to the X 280-char budget.

Make platform resolution ordering-proof:

- fm-x-link now resolves the platform AUTHORITATIVELY by request_id via a
  new fmx_request_relay_context helper (POST /connector/request-context)
  when neither the inbox payload nor carry flags carry it. The request_id
  survives the inbox drain, so a post-cleanup link still learns the right
  split budget. Best-effort: no token/curl or a non-2xx relay degrades to
  the loud warning below rather than a silent X default.
- fm-x-link warns loudly when no platform source resolves, so the loss is
  never silent.
- The fmx-respond procedure now orders link-before-inbox-cleanup so the
  fast local path stays correct without a relay round-trip.

Colocated regression tests: a Discord follow-up >280 <2000 posts as ONE
message even when linked after inbox cleanup, and an unresolvable platform
warns loudly instead of splitting silently. docs/configuration.md documents
the request-context lookup.

The relay endpoint is the companion durable change (see done status); until
it ships, the link-before-cleanup reorder keeps the normal path correct.

* no-mistakes(document): Document X-mode platform recovery

* fix(composer): handle ANSI ghost text safely (#429)

* fix(composer): one ANSI-aware ghost owner covers claude dim + grok truecolor

Away-mode injection wedged all night on the primary claude-on-herdr pane:
the herdr composer classifier never stripped generic dim ghost text (only a
narrow codex bold-wrapped byte-pattern check), so claude's rotating
prompt-suggestion ghost - a bare "❯" then SGR-2 dim text, which herdr's ANSI
pane read preserves - read as real pending input and every escalation deferred
(6524 lifetime "pending input (non-empty composer)" defers; wedge 30623s).

Consolidate ghost extraction into one fleet-wide ANSI-aware owner,
fm_composer_strip_ghost (bin/fm-composer-lib.sh), that drops every
de-emphasised run - dim/faint (SGR 2: claude, codex) AND a dark/muted truecolor
foreground (grok's placeholder, luminance below FM_COMPOSER_GHOST_LUMA_MAX,
default 128, dark-theme assumption). Both ANSI-capable backends route through
it: fm_tmux_composer_state (fm_tmux_strip_ghost is now a thin adapter) and
fm_backend_herdr_composer_state. The herdr-only faint byte-pattern check is
removed and fm_backend_herdr_strip_ansi reduced to a thin adapter over the
shared fm_composer_strip_ansi. Bordered detection now reads the plain row so a
dark box border dropped with the ghost does not lose the composer shape.

This also closes the documented grok TRUECOLOR placeholder gap by the same
mechanism (harness-adapters skill note updated).

Empirical evidence (read-only live capture + isolated tmux, no herdr lifecycle)
and the incident write-up are in docs/herdr-backend.md; deterministic
regressions feed the exact captured bytes through the real classifiers
(tests/fm-backend-herdr.test.sh, tests/fm-composer-ghost.test.sh). Two prior
ghost-test fixtures that used a near-black 38;2;1;2;3 as "real" colored text
(never a realistic real-input color) are corrected to a bright 38;2;224;222;244,
preserving the truecolor payload-skip parser intent.

* no-mistakes(review): Preserve dark shell prompt safety

* no-mistakes(review): Harden erased shell prompt classification

* no-mistakes(document): Document shared composer ghost extraction

* no-mistakes(lint): Normalize tmux comment punctuation

* fix(spawn): make tmux window handling robust under non-default config (#134)

* test: isolate session-start suite from ambient harness markers (#432)

* fix(session-start): isolate harness env markers in suite runner

Neutralize CLAUDECODE, PI_CODING_AGENT, and GROK_AGENT in
run_session_start so ambient interactive shells cannot override the
suite's fake ps harness (local-vs-CI split on the pi supervision case).

* no-mistakes(document): Correct Pi marker documentation

* fix(teardown): retry transient index locks during worktree return (#435)

* fix(teardown): retry treehouse return on transient index.lock

Killed crew git ops can leave a short-lived worktree index.lock that
makes treehouse return fail. Retry on that error signature with a
bounded wait (env-overridable), never force-delete a live lock, and
only then fall back to the existing provably-stale cleanup path.

* no-mistakes(review): Harden teardown retry configuration

* no-mistakes(document): Document teardown index-lock retry behavior

* no-mistakes(lint): Fix empty shell variable assignments

* fix: complete brief help and consolidate documentation (#438)

* docs: de-feature the scripts.md and CONTRIBUTING test inventories

Slice 1 of the documentation redundancy cleanup wave (firstmate scope).

docs/scripts.md: every row is now one purpose clause; script headers
are the declared owner of behavior, flags, and contracts. Coverage
stays 61/61 scripts; bytes drop 19,922 -> 7,958.

CONTRIBUTING.md: the 54-row per-test inventory is gone; contributors
discover tests by listing tests/*.test.sh and reading each script's
own header, and gated tests print their own skip gates. The run
commands, symlink assertions, and watcher smoke line are unchanged.
Lines drop 135 -> 84 (18,797 -> 7,831 bytes).

Two facts that existed only as inventory rows moved into their
owners' headers first: fm-brief.sh's paused-vs-blocked scaffold
distinction and fm-session-start.sh's Pi extension-loaded check.

No instruction-surface or behavior change; AGENTS.md untouched.

* no-mistakes(review): Captain, fix brief help and Grok test discovery

* no-mistakes(review): Captain: document Grok lock-holder test coverage

* fix: detect Git and centralize backend configuration (#445)

* docs: consolidate universal backend contracts into configuration.md

Slice 2 of the documentation redundancy cleanup wave (firstmate scope).

docs/configuration.md is now the declared single owner of three
universal contracts, each with an explicit ownership sentence:
- the universal toolchain list (Toolchain), now also carrying the
  per-tool purpose clauses that previously lived only in the tmux guide;
- the task-selector vocabulary (Runtime backend);
- the tasks-axi compatibility definition (Backlog backend).

The five backend guides' prerequisites replace their verbatim
universal-requirements parentheticals (5 full copies) with a pointer
plus only backend-specific items; zellij/cmux selector restatements
and architecture.md's partial copy become pointers or are dropped;
CONTRIBUTING's compatibility sentence becomes a pointer; two
near-verbatim orca-bootstrap restatements (configuration.md Runtime
backend, orca guide) collapse into the Toolchain owner copy.

Backend-specific setup, behavior, target-string shapes, and every
empirical verification record are untouched. AGENTS.md untouched
(slice 3).

* docs: include git and GitHub auth in the toolchain owner list

The review flagged that the new universal-toolchain owner omitted git
and GitHub authentication while every backend guide now defers its
prerequisites here; bootstrap's NEEDS_GH_AUTH check makes them real
universal requirements.

* no-mistakes(review): Detect Git in bootstrap toolchain

* no-mistakes(document): Clarify GitHub CLI and centralize selector documentation

* feat(daemon): add backend-independent wedge alerts (#444)

* feat(daemon): backend-independent active alert for the wedge alarm

When away-mode injection wedges past max-defer, inject_wedge_alarm only
actively signalled via the tmux status-line, which is skipped on non-tmux
backends. A wedged claude-on-herdr primary left only the passive
state/.subsuper-inject-wedged marker (2026-07-10 overnight incident).

Add a config-gated active alert (config/wedge-alarm, local/gitignored;
FM_WEDGE_ALARM_CHANNEL) that reaches the captain even when every pane and
its status-line is unreadable: an OS-level macOS notification (osascript),
a herdr notification, or a captain-supplied command. Default-on (auto) so
the alarm is never silent; each channel best-effort, degrading to the next
and never crashing the daemon loop. The tmux flash and durable marker stay.

The OS notifiers route through a single FM_WEDGE_ALARM_EXEC seam. When the
daemon is sourced (only tests do this; production execs it) the seam
defaults to "discard", and tests/wake-helpers.sh points it at a recorder,
so it is structurally impossible for any test to post a real notification.

Channels verified once manually on macOS 26.5.2 / herdr 0.7.3; see
docs/wedge-alarm.md.

* no-mistakes(review): Bound wedge alarm notifier execution

* no-mistakes(review): Captain: harden wedge alarm notifier safety

* no-mistakes(review): Captain: harden wedge alarm test notifier isolation

* no-mistakes(review): Captain: harden wedge alarm throttling

* no-mistakes(review): Redact wedge alarm directive logs

* no-mistakes(review): Harden wedge alarm notifier safety

* no-mistakes(review): Track notifier process groups through cleanup

* no-mistakes(document): Document wedge-alarm active alert behavior

* docs: centralize firstmate operating contracts (#447)

* docs(agents): extract conditional AGENTS.md material to owned homes

Slice 3 of the documentation redundancy cleanup wave (firstmate scope):
the always-loaded instruction surface drops from 941 lines / 116,733
bytes (~29k tokens per session per fleet member) to 785 / 91,353
(~22.8k tokens), moving only audit-identified conditional and
situational material while preserving every load-bearing invariant at
its trigger point via the inline-stub pattern.

Moves, each to one declared owner plus an inline stub:
- section 3's bootstrap output-line handbook (~44 lines) -> new
  agent-only bootstrap-diagnostics skill, added to the section 13
  trigger index; the detect-consent-install rule and the
  do-not-dispatch gate stay inline as safety-critical.
- section 4's crew-dispatch JSON schema and field semantics ->
  docs/configuration.md 'Crew dispatch profiles' (pointer direction
  flipped); the intake procedure, precedence, backstop, and
  never-select-unverified rules stay inline.
- section 4's quota-balanced algorithm -> bin/fm-dispatch-select.sh
  header (now the declared owner; usage() converted to the dynamic
  header extraction pattern PR #438 established for fm-brief.sh).
- section 7's spawn resolution narrative and example sprawl ->
  bin/fm-spawn.sh header; the isolated-worktree assertion, refusal-is-
  a-blocker rule, and post-spawn duties stay inline.
- section 7's teardown landed-work mechanics -> bin/fm-teardown.sh
  header (section 1's containment pointer retargeted); the fork benign
  case and never-force rule stay inline.
- section 8's watcher classification narrative -> docs/architecture.md
  'Event-driven supervision' (already the owner); every operative rule
  (one live cycle, no turn ends blind, drain first, wake ladder,
  never-pkill, guard responses) stays inline.
- sections 3/4/6/7 secondmate sync, propagation, schema, and handoff
  restatements -> secondmate-provisioning skill, now the declared
  owner including the literal-file inheritance nuance.
- section 14's X-mode cadence mechanism -> docs/configuration.md
  'X mode (.env)', closing issue #363; activation semantics, the
  fmx-respond trigger, and the terminal-wake final-follow-up duty
  stay inline.

CLAUDE.md stays a symlink; no behavior or test change.

* no-mistakes(document): Centralize contract-owner documentation

* fix(cmux): close last workspace during teardown (#449)

* fix(cmux): close the last/selected workspace in a window at teardown

cmux keeps every window at >=1 workspace, so close-workspace on the only
workspace in a window silently no-ops (returns OK, workspace stays), and a
window holding a live session cannot be closed over the control socket.
That left a selected task workspace open at teardown (the last workspace
in a window is always the selected one).

Add fm_backend_cmux_window_of_workspace and have fm_backend_cmux_kill
create a throwaway default sibling in the target's window before closing
when the target is the last workspace there, so the close lands; the
window keeps a fresh default workspace (cmux's own "closed the last tab"
outcome). Non-last teardown closes directly, as before.

Cover both kill branches plus the helper with fake-CLI unit tests, add a
real-cmux window/count detection smoke assertion, and record the
empirical evidence in docs/cmux-backend.md.

* no-mistakes(review): Derive cmux count from membership snapshot

* no-mistakes(document): Document cmux last-workspace teardown behavior

* fix: recover orphaned packed-refs locks during fleet sync (#453)

* fix(fleet-sync): recover from an orphaned packed-refs.lock

A git ref rewrite (fetch --prune, pack-refs, branch -D) killed after
creating .git/packed-refs.lock but before renaming it - e.g. bootstrap's
timed-out fleet-sync kill or teardown's process kills - leaves a lock that
makes the next sync's fetch fail with "Unable to create
'...packed…
DereKk8 added a commit to DereKk8/firstmate that referenced this pull request Jul 26, 2026
* feat(backends): add experimental cmux runtime backend (#246)

* feat(backends): add cmux runtime backend (experimental)

Session-provider-only adapter for cmux (bin/backends/cmux.sh), mirroring
zellij/herdr structurally, wired into fm-backend.sh and fm-spawn.sh with
--secondmate refused for now. Verified against the real cmux 0.64.17 app:
send does not auto-submit, cwd is creation-time-frozen (zellij-shape,
pwd-marker-probe workaround), close-surface refuses on a workspace's last
surface (falls back to close-workspace), workspace ids do not survive a
relaunch, and the control socket defaults to cmuxOnly access (requires a
one-time password-mode setup, documented in docs/cmux-backend.md). Also
found and fixed a live bug during development: read-screen fails on a
surface that has never been written to, so liveness now uses list-panes
instead. Fake-CLI unit suite (40 tests), a real-binary smoke test, and a
full spawn/steer/peek/done/merge/teardown E2E pass against a real claude
crewmate all pass, including the popup/second-Enter regression class.

* no-mistakes(review): Harden cmux recovery and password parsing

* no-mistakes(review): Harden cmux capture failure handling

* no-mistakes(review): Mark cmux test scripts executable

* no-mistakes(review): Scope cmux workspaces and teardown

* no-mistakes(review): Captain, honor cmux password config override

* no-mistakes(review): Captain, hash cmux home labels

* no-mistakes(document): Sync cmux backend docs

* feat(agents): add firstmate coding guidelines skill (#248)

* Add firstmate-coding-guidelines skill (AGENTS.md diet PR 0)

Encodes the knowledge-placement decision tree, one-owner rule, and
inline-stub pattern from the diet analysis so future contributions stop
adding conditional detail inline. AGENTS.md gets one section-13 trigger
line; fm-brief.sh's REPO argument has no reliable signal for "this is
firstmate's own repo", so the load instruction goes in CONTRIBUTING.md's
Development section instead of the scaffold.

* no-mistakes(review): Captain, align tracked-material trigger scope

* no-mistakes(document): Sync coding guidelines docs

* no-mistakes(lint): Fix Markdown style issues

* fix: add turn-end supervision guard (#249)

* feat: structural Stop-hook backstop for primary turn-end supervision

fm-guard.sh is pull-based: it only warns when some other supervision
script happens to run, so a primary session that ends a turn without
re-arming the watcher and then runs no further fleet-touching command
can sit blind for hours (the 2026-07-04 incident this fixes).

Add bin/fm-turnend-guard.sh, a Claude Code Stop hook registered in the
tracked .claude/settings.json, that fires on every primary turn end and
blocks (exit 2, verified empirically to force continuation) when work
is in flight with no fresh watcher beacon. It never blocks more than
once per turn, using Claude Code's own stop_hook_active loop-guard
field, and scopes itself to the actual primary checkout only (inert in
crewmate/scout worktrees and secondmate homes).

Factor the shared "in-flight but no live watcher" predicate out of
fm-guard.sh into bin/fm-supervision-lib.sh so the pull-based banner and
the push-based hook can never drift on what "unhealthy" means.

Document the verified Stop-hook mechanism and scoping in
docs/turnend-guard.md, add a harness-adapters note, and cover the
predicate and hook with tests/fm-turnend-guard.test.sh.

* no-mistakes(review): Respect active home in turnend guard

* no-mistakes(review): Require live watcher for turn-end guard

* no-mistakes(review): Captain: portable turn-end timing

* no-mistakes(document): Sync turn-end guard documentation

* feat(backends): auto-detect cmux runtime (#250)

* feat(backends): auto-detect cmux runtime from CMUX_WORKSPACE_ID

Wires cmux into fm_backend_detect the same way herdr already is: a
firstmate process running inside a cmux-spawned terminal now spawns
new tasks into cmux by default, no config needed. Verified from cmux's
own shipped source that CMUX_WORKSPACE_ID/CMUX_SURFACE_ID/CMUX_SOCKET_PATH
are unconditionally, non-overridably injected into every terminal
surface it spawns, and that cmux's own CLI treats CMUX_WORKSPACE_ID as
its own ambient-target fallback - the same role $TMUX/HERDR_ENV play for
their backends. CMUX_WORKSPACE_ID is checked last (after $TMUX and
HERDR_ENV=1) since cmux is a terminal application, not a nestable
multiplexer. Socket auth (config/cmux-socket-password) stays required
regardless of how the backend was selected; the existing spawn refusal
now also names the config/backend=tmux / --backend tmux opt-out for a
caller who never explicitly chose cmux.

A live env dump inside a real cmux terminal was not obtained safely on
the shared dev machine (documented in docs/cmux-backend.md); this rests
on the source read instead, mirroring this doc's existing
verified-from-source precedent.

* no-mistakes(review): Fix cmux autodetect docs and tests

* no-mistakes(document): Document cmux auto-detection

* fix(afk): support herdr away-mode injection (#251)

* fix(afk): make the away-mode daemon backend-aware for herdr

bin/fm-supervise-daemon.sh discovered its supervisor pane and injected
via raw tmux calls only, so /afk failed outright on a herdr-based
fleet (TMUX_PANE unset, firstmate:0 fallback unresolvable).

Discovery now resolves backend (tmux|herdr) and target independently,
mirroring fm-backend.sh's own runtime auto-detection, with an explicit
FM_SUPERVISOR_BACKEND override alongside the existing FM_SUPERVISOR_TARGET.
zellij/orca refuse loudly at startup instead of misapplying tmux
primitives. Injection (pane-exists probe, busy-guard, composer-guard,
verified submit) now dispatches through bin/fm-backend.sh's generic
primitives, adding a new fm_backend_composer_state dispatcher; the
tmux path is byte-identical to before. Also fixes a pre-existing bug
in fm_backend_target_exists's herdr arm (missing --session, so it
silently misrouted once more than one herdr server was running) found
while verifying this end to end against a real isolated herdr session.

Classification, batching, max-defer, the marker contract, locks, and
wake-queue handling are unchanged - this is a transport-layer fix.

* no-mistakes(review): Corroborate Herdr idle busy state

* no-mistakes(review): Stabilize Herdr daemon startup wait

* no-mistakes(review): Captain, route cmux composer and update AFK docs

* no-mistakes(document): Document AFK supervisor backend support

* docs(agents): move X-mode procedures out of AGENTS (#253)

* docs(agents): collapse X-mode section 14 into fmx-respond/docs pointers

AGENTS.md diet PR 1 of 3 (agentsmd-diet-s2 report, move-plan items 1-2).
Replaces section 14's "Answering"/"Completion follow-up"/"Conversations"/
"Length and threads"/"Preview / dry-run" blocks (54 lines) and the
"Mechanism" narrative (6 lines) with two short pointers: fmx-respond
(section 13) for the procedure, docs/configuration.md "X mode (.env)"
for the wire protocol. Net -55 lines in AGENTS.md.

Destination edits landed first, deletions second (q4 discipline):
- docs/configuration.md: added the "purely additive, watcher untouched"
  guarantee that AGENTS.md's Mechanism block stated but configuration.md
  did not.
- fmx-respond/SKILL.md: added the x-mode-error wake boundary (report as
  a blocker, do not load this skill), the --image flag for replies and
  follow-ups, the "images are for real artifacts, not prose" rule, and
  the dry-run compact-image-marker behavior - none of these were
  previously in the skill even though AGENTS.md described them, so they
  were genuine gaps, not pre-existing duplication. Also made the skill's
  own "Completion follow-up" section the sole, full owner of that
  procedure instead of deferring to AGENTS.md section 14 for substance
  that no longer lives there (two internal cross-references updated to
  point at section 8's terminal-wake trigger and the skill's own section
  instead).

Mechanical line-by-line audit of every removed AGENTS.md line:

Mechanism block (6 lines removed):
- bootstrap artifact-writing description -> already owned by
  docs/configuration.md "X mode (.env)" (locked-bootstrap paragraph)
- check-shim/poll mechanism description -> already owned by
  docs/configuration.md same section
- missing-deps/x-mode-error diagnostic description -> already owned by
  docs/configuration.md ("Relay auth or config problems...") plus
  bin/fm-x-poll.sh's own header comment for the missing-curl/jq mechanics
- opt-out artifact removal description -> already owned by
  docs/configuration.md same section
- "purely additive, no edit to fm-watch.sh/fm-watch-arm.sh/fm-wake-lib.sh/
  afk daemon" guarantee -> MOVED to docs/configuration.md (added in this
  PR; this fact had no other home before)

Answering/Completion follow-up/Conversations/Length and threads/
Preview-dry-run blocks (54 lines removed):
- x-mention wake -> load fmx-respond: already owned by section 13's
  existing trigger line (unchanged) and restated in the new pointer
- x-mode-error wake -> report as blocker, don't load fmx-respond: MOVED
  to fmx-respond/SKILL.md (added in this PR)
- inbox-draining, classification, acting, reply composition, submission,
  cleanup-on-success/failure: already owned by fmx-respond/SKILL.md
  "Procedure" section (unchanged, pre-existing)
- owner-only routing / captain-as-asker framing: already owned by
  fmx-respond/SKILL.md "The asker is your own captain" section
- standing X-mode authorization / autonomous posting / dry-run as only
  non-posting path: already owned by fmx-respond/SKILL.md same section
- acknowledge-first -> act -> follow-up shape, three-case classification:
  already owned by fmx-respond/SKILL.md "A request to act on" section
- destructive/irreversible/security-sensitive escalation guardrail:
  already owned by fmx-respond/SKILL.md "Public channel..." section and
  Procedure step 2c
- dismiss-instead-of-reply for pure acknowledgments, relay re-offer
  prevention, dry-run honoring: already owned by fmx-respond/SKILL.md
  Procedure steps 2b/2c/2e-skip and docs/configuration.md
- public-safety bar (no task ids/internals/captain-private/secrets):
  already owned by fmx-respond/SKILL.md "The reply is public" section
- never-inline-into-shell-command / --text-file or stdin: already owned
  by fmx-respond/SKILL.md Procedure step 2e and Notes
- --image flag for replies (formats, base64, no-inline guarantee): MOVED
  to fmx-respond/SKILL.md Procedure step 2e (added in this PR - this was
  not previously in the skill)
- fm-x-link field names (x_request=, x_request_ts=, x_followups=):
  already owned by AGENTS.md section 2's state/<id>.meta field list
  (untouched, out of scope for this PR) and fmx-respond/SKILL.md
- carry-count/carry-ts relink behavior, three-follow-up budget, milestone
  sparingness, --check/--text-file posting, connector/followup wire
  detail, --final clearing, cap/window graceful degradation: already
  owned by fmx-respond/SKILL.md "Completion follow-up" section (now sole
  owner) and docs/configuration.md wire-protocol paragraphs
- --image flag for follow-ups: MOVED to fmx-respond/SKILL.md "Completion
  follow-up" section (added in this PR - genuine gap)
- "failed task still gets an honest final follow-up": already owned by
  fmx-respond/SKILL.md "Completion follow-up" section
- FMX_DRY_RUN whole-loop previewability: already owned by
  fmx-respond/SKILL.md "Dry-run / preview mode" section
- in_reply_to conversation continuity, untrusted-thread handling,
  follow-up worthiness judgment, relay-owned self-reply guard/cap:
  already owned by fmx-respond/SKILL.md "The direct ask is the captain's"
  section and Notes (one bullet is a verbatim match)
- concise-by-default / no hand-numbered threads: already owned by
  fmx-respond/SKILL.md "Voice" section
- auto-split behavior, char/tweet caps, premium-independence, wire shape
  ({text}/{text,texts}): behavior already owned by fmx-respond/SKILL.md
  Voice section; exact defaults and wire shape already owned by
  docs/configuration.md; "premium-independent" mechanics already owned
  by bin/fm-x-reply.sh's own header comment
- "images are for real artifacts, not prose": MOVED to fmx-respond/
  SKILL.md "Voice" section (added in this PR - genuine gap)
- image-on-thread wire behavior: already owned by docs/configuration.md;
  reinforced in fmx-respond/SKILL.md's new --image note
- dry-run POST-body shape, endpoint marker, truthy-value definition,
  jq-only dependency, end-to-end testability, x-outbox inspection:
  already owned by fmx-respond/SKILL.md "Dry-run / preview mode" section
  (several near-verbatim matches) and docs/configuration.md wire detail
- dry-run compact image marker: MOVED to fmx-respond/SKILL.md "Dry-run /
  preview mode" section (added in this PR - genuine gap)

Section 8's terminal-wake completion-follow-up trigger (the one fact
required to survive inline) is untouched and already present; the new
section 14 pointer references it instead of restating it.

Nothing outside section 14 (plus the two destination files) is touched.
Full test suite green, including all 74 fm-x-mode.test.sh checks.

* no-mistakes(review): Preserve X-linked follow-up triggers

* no-mistakes(review): Fix x-mode error trigger

* no-mistakes(document): Docs cross-reference synchronized

* no-mistakes(lint): Clean Markdown lint pass

* docs: trim duplicated harness guidance (#255)

* docs(agents): trim section 4 harness/secondmate duplication

AGENTS.md diet PR 2 of 3 (data/agentsmd-diet-s2/report.md, move-plan
items 3-4; redundancy item 2 folded into item 3).

Removed the five claude/codex/grok/pi/opencode model/effort-flag
bullets from section 4 - byte-for-byte duplicated by
harness-adapters' "Launch profile axes" table (which is already a
superset: it carries verified CLI versions per adapter that the
AGENTS.md bullets lacked). Replaced with a one-line pointer; the
skill is already loaded before every spawn per section 4's own
closing trigger, so no new trigger was needed.

Moved the config/secondmate-harness model/effort pin-format detail
(the `<harness> [<model>] [<effort>]` line format, the
secondmate-model/secondmate-effort accessors, back-compat, and the
durability-across-respawn behavior) into secondmate-provisioning,
which is already a mandatory load at every secondmate lifecycle
touchpoint. Added the destination content to the skill first, then
replaced the AGENTS.md paragraph with a 3-line pointer.

Mechanical audit - every removed line's new home:
- 5 harness bullets (claude/codex/grok/pi/opencode model+effort
  flags, per-harness max-omission rationale) -> already present in
  harness-adapters SKILL.md's "Launch profile axes" table (lines
  53-59), confirmed fact-by-fact before deleting.
- "config/secondmate-harness may also pin..." paragraph (pin format,
  bare-harness back-compat, secondmate-model/secondmate-effort
  accessors, per-spawn override precedence, respawn durability,
  secondmate-only scope) -> secondmate-provisioning SKILL.md's
  "Charter and seed" section, added verbatim before this trim.
- The following paragraph (inheritable config: crew-dispatch.json,
  crew-harness, backlog-backend) is untouched - out of scope for
  this PR, still inline.
- The bootstrap CREW_DISPATCH effort-mismatch diagnostic sentence is
  untouched - not part of the five-bullet duplication, stays inline.

No script changes. Section 4 shrinks from 104 to 87 lines
(958 -> 901 total AGENTS.md lines) with zero facts lost: every fact
is reachable through harness-adapters or secondmate-provisioning,
both already mandatory loads at the relevant lifecycle points.

* no-mistakes(document): Align secondmate skill triggers

* no-mistakes(lint): Markdown style clean

* fix: anchor turn-end Stop hook to project root (#256)

* Fix turn-end Stop hook to use CLAUDE_PROJECT_DIR path

Claude Code runs hook commands via /bin/sh from the session cwd, so the
bare relative bin/fm-turnend-guard.sh path fails when cwd is not the repo
root. Anchor the command with "$CLAUDE_PROJECT_DIR"/bin/fm-turnend-guard.sh
instead; verified CLAUDE_PROJECT_DIR is set on Stop hooks in Claude Code
2.1.201. Document the cwd caveat and add a settings.json regression test.

* no-mistakes(document): Document Stop hook path anchoring

* docs: trim firstmate agent guidance duplication (#258)

* docs: trim AGENTS.md redundancy (diet PR 3/3)

Consolidates five duplicated passages to a single owner each, per
data/agentsmd-diet-s2/report.md redundancy items c3-c7:

- Inheritable-config propagation mechanism: owned by section 3 (where
  the sweep runs); sections 4 and 7 keep compact references. Section 4
  retains its one genuinely unique fact (crew-harness inherit-vs-fallback
  semantics), just no longer restates the propagation mechanism itself.
- Landed-work definition: owned by section 7's ship-teardown detail
  (PR-containment mechanics, pr= discovery fallback); section 1's hard
  rule #3 keeps the rule plus a three-case summary and a pointer.
- Backend meta-field enumeration: owned by docs/configuration.md
  ("Runtime backend", already comprehensive including cmux) and each
  backend's own doc; AGENTS.md keeps only the fields common to every
  task plus a pointer.
- Dropped one redundant restatement of "silence is correct while
  waiting" in section 8.
- Worktree-tangle guard explanation: owned by section 8 (already the
  fuller, cross-referenced version); section 3's TANGLE bullet keeps
  the remediation action and points at section 8 for the why.

Also adds two captain-requested single-sentence rules: invoke bin/
scripts by absolute $FM_ROOT path after any cd away from the home, and
a backend spawn refusal must be surfaced to the captain rather than
silently worked around by switching backends.

AGENTS.md: 901 -> 889 lines, 112355 -> 108560 bytes.

* no-mistakes(review): Clarify post-cd bin invocation guidance

* no-mistakes(document): Sync AGENTS trim docs

* no-mistakes(lint): Fix Markdown line style

* feat(backends): improve cmux detection and socket-mode guidance (#259)

* feat(backends): cmux detection fallbacks and socket-mode matrix

Workstream A: cmux's bundled claude wrapper strips every CMUX_* env var on
its passthrough path (reproduced live 2026-07-04, cmux 0.64.17), so a
claude-harness firstmate inside a cmux tab has no CMUX_WORKSPACE_ID.
fm_backend_detect now falls back - macOS-only, only when the primary marker
is absent - to __CFBundleIdentifier=com.cmuxterm.app and then a process
ancestry walk resolved by bundle id (lsappinfo) plus a bundle-shaped ps comm
match. Innermost-first ordering is unchanged and absorbs the
tmux-inside-cmux bundle-id false positive; the auto-detect NOTICE names the
winning fallback signal.

Workstream B: the five socketControlMode values were traced through cmux
source (commit 9c91710e3f58): off/cmuxOnly can never admit an external CLI,
automation admits same-user clients with no secret (0600 socket only),
password needs the auth handshake, allowAll opens the socket to every local
user (0666). Automation mode is now the documented recommendation; the
adapter's refusals name every viable mode, classify Invalid password as
unauth, and the launch-timeout message names the off-mode possibility.

Docs carry the wrapper-strip empirical record, the fallback contract and
authority split, and the full mode matrix with rationale; tests cover the
new detection paths, the nested false positive, and the refusal wording.

* no-mistakes(review): Document cmux fallback detection

* no-mistakes(review): Update cmux architecture docs

* no-mistakes(document): Align cmux backend docs

* fix(backends): scope zellij tabs by firstmate home (#252)

* fix(backends): home-scope zellij tab titles to close cross-home collision gap

Zellij's one shared "firstmate" session has no per-home split and enforces
no tab-name uniqueness, so two firstmate homes with colliding task ids could
send/peek/close each other's tabs - the same gap a no-mistakes review gate
caught for cmux (docs/cmux-backend.md). Ports that fix: every new tab is
created with a home-scoped title (fm-<home-label>-<id>), and every
list/find/recover/kill path scopes matches to this home's own tag. A tab
spawned before this change still matches via its old untagged bare title,
but only when unambiguous - two live tabs sharing a bare title refuse rather
than guessing which one is ours.

Factors the home-label/hash derivation shared with cmux into
bin/fm-backend-hometag-lib.sh so the two adapters can't drift.

* no-mistakes(review): Fix zellij child teardown home tag

* no-mistakes(review): Fix zellij teardown and selector scoping

* no-mistakes(document): Sync zellij home-scope docs

* fix: sync project clones after merged PR wakes (#293)

* fix(fleet-sync): auto-sync on merged-PR wake, accept project name

fm-fleet-sync.sh's single-project form failed on a bare project name
("not a directory"), forcing hand-typed full paths (4 manual runs in
one incident). It now resolves a bare name or projects/<name> against
the home's projects dir.

AGENTS.md now encodes the trigger: a wake whose status reports a
merged PR for a project cloned in this home runs fleet-sync for that
project as part of handling the wake, so a secondmate-reported merge
does not leave the primary's clone stale until the next session start
or teardown.

* no-mistakes(review): Fix fleet-sync project name shadowing

* no-mistakes(document): sync fleet-sync docs

* fix: canonicalize spawn worktree path checks (#294)

* fix(spawn): canonicalize worktree-isolation guard against symlinked project prefixes

fm-spawn.sh compared a logical PROJ_ABS against the physically-resolved
pane cwd every backend reports, so a project reached through a symlinked
prefix (e.g. macOS's /tmp -> /private/tmp) could trip the isolation
guard's false refusal before treehouse ever moved the pane. Canonicalize
once into PROJ_ABS_REAL and compare against that everywhere instead.

* no-mistakes(review): Canonicalize spawn cwd comparisons

* no-mistakes(document): Refresh symlinked spawn docs

* docs: add Orca operator skill (#276)

* docs: add Orca operator skill

* no-mistakes(document): Document Orca checklist

---------

Co-authored-by: Stephen Brouhard <vesta@stephens-macbook-air.tail2122af.ts.net>

* fix: surface green PRs during CI monitoring (#297)

* fix(crew-state): detect green-PR CI monitoring, escalate repeat wedges

fm-crew-state.sh's ci step never distinguishes "still waiting on checks"
from "checks green, waiting on merge" via axi status alone, since a repo
that defers merge to the captain keeps the ci step at status=running for
the whole monitor phase. Read the ci step's own log tail (axi logs) for
the checks-passed marker and surface done instead of a false "validating
(running)" - verified against the real PR #252 run's ci.log.

The watcher's wedge timer can re-escalate the same stale pane forever
without ever signaling that it is a repeat; track a per-pane consecutive
escalation count and add a demand-deep-inspection marker to the wake
payload once it crosses a threshold, so the supervisor can no longer
dismiss each one as an isolated, still-validating pane.

Also clarify the ship-brief's checks-green line: it is owed at the
CI-ready return point, not after the background monitor-until-merge
loop finishes.

* no-mistakes(review): Captain, distinguish pending no-checks CI marker

* no-mistakes(review): Harden CI relapse handling

* no-mistakes(review): Block stale done during fixing

* no-mistakes(review): Captain, tighten CI status gating

* no-mistakes(review): Captain, harden stale CI green handling

* no-mistakes(review): Captain, recognize ranged CI rearm markers

* no-mistakes(document): Sync crew-state supervision docs

* fix(teardown): recover provably stale git index locks (#296)

* fix(teardown): recover from a stale worktree git index.lock

A crew process killed mid-git-operation can leave a stale
.git/worktrees/<wt>/index.lock behind, making fm-teardown.sh's
`treehouse return --force` fail closed. On that failure, retry once
after a short wait (the owning process may be exiting), then remove
the lock and retry once more only when it is provably stale: old
enough by mtime and lsof shows no live holder on the lock or the
worktree itself. A lock that isn't provably stale is left in place and
the original failure still surfaces.

* no-mistakes(review): Harden teardown lock refusal paths

* no-mistakes(review): Harden stale-lock teardown safety rechecks

* no-mistakes(review): Harden stale teardown lock checks

* no-mistakes(document): Document teardown lock recovery

* feat(bin): encode project AGENTS.md authoring bar with canonical self-governance section (#307)

* Encode project AGENTS authoring bar

* no-mistakes(review): Captain, centralize CLAUDE promotion governance

* no-mistakes(review): make ensure_maintenance_section idempotent-success, drop || true guards

* no-mistakes(review): separate appended maintenance section on newline-less CLAUDE.md promotion

* no-mistakes(review): assert maintenance heading present before separator check in test

* no-mistakes(document): sync docs with AGENTS.md authoring bar and self-governance

---------

Co-authored-by: fmtest <fmtest@example.invalid>

* feat(skills): add captain-invocable bearings status-report skill (#300)

* Add captain-invocable bearings skill

Generates a pick-up-where-I-left-off status report from live fleet
state to data/status-report-<YYYY-MM-DD>.md plus a concise chat
summary. Read-mostly procedure: reads backlog, per-task crew state
via bin/fm-crew-state.sh, open PRs via gh-axi, scout reports,
pending decisions, and date-gated queued work; composes the
exemplar's sections (TL;DR, Check first, Landed, In flight, Plans,
Decisions pending, Date-gated/queued); never tears down, merges, or
mutates task state as a side effect.

* no-mistakes(document): docs: list new /bearings skill in README built-in skills table

* fix(watcher): make PID identity locale-invariant (#285)

* fix(watcher): pin LC_ALL=C in fm_pid_identity for locale-invariant identity

ps's lstart date format follows the caller's LC_TIME/LC_ALL. The watcher records
its process identity under one locale, but arm/guard/turn-end re-read it under the
machine's ambient locale. On a non-C locale (e.g. ko_KR) the two strings differ
only in the date portion, so fm_watcher_lock_matches_pid / fm_watcher_healthy
reject a genuinely live watcher - breaking fm-watch-arm.sh, fm-guard.sh, and
fm-turnend-guard.sh on every non-C-locale machine.

Pin LC_ALL=C on that one ps call so the write and read sides agree regardless of
machine locale, matching the LC_ALL=C determinism the file already uses elsewhere.
Add a colocated regression test asserting fm_pid_identity is locale-invariant
across exported LC_ALL/LC_TIME.

* no-mistakes(document): Document watcher PID identity coverage

* docs: document codex app backend contract (#222)

* docs: reconcile Codex App backend contract

* no-mistakes(document): Sync backend docs

* docs: clarify Codex Desktop bridge blocker

* no-mistakes(document): Align Codex App backend docs

* no-mistakes(test): Captain, stabilize watcher self-eviction test cadence

* no-mistakes(document): Document Codex App backend contract

* no-mistakes(document): Captain, document blocked codex-app coverage

* docs: make Codex App contract doc authoritative

* no-mistakes(document): Align Codex App backend docs

* docs: redact local Codex App smoke paths

---------

Co-authored-by: Stephen Brouhard <vesta@stephens-macbook-air.tail2122af.ts.net>

* docs: add Codex Desktop coordination skill (#275)

* docs: add Codex App coordination skill

* no-mistakes(review): Captain, mark Codex App skill agent-only

* no-mistakes(document): Document Codex App backend boundary

* no-mistakes(document): Captain, document Codex Desktop backend boundary

* no-mistakes(lint): Captain, lint clean

* no-mistakes(document): Document Codex Desktop boundaries

* docs: narrow Codex App skill playbook

* fix(afk): stop herdr escalation redelivery loop (#317)

* fix(afk): recognize unbordered herdr composer rows to stop escalation redelivery loop

fm_backend_herdr_composer_state only recognized bordered composer rows
(the grok shape). Real claude and codex render their live input row
with no border at all, so once a harness's own startup banner scrolled
out of the capture window the classifier read the composer as unknown
forever. fm_backend_herdr_send_text_submit never confirmed "empty", so
escalate_flush never cleared state/.subsuper-escalations, and the
away-mode daemon retyped and resubmitted the same buffered digest every
housekeeping cycle - reproduced live against a real herdr+claude pane
(5+ identical deliveries in 40s).

The classifier now recognizes an unbordered (bare) composer row led by
a known prompt glyph alongside the existing bordered shape, keeping
whichever match is bottom-most so a stale decorative box never
outranks the live composer.

* no-mistakes(review): Narrow herdr bare prompt matcher

* no-mistakes(document): Sync herdr composer docs

* fix(backends): confirm Herdr submits with native agent state (#323)

* fix(herdr): confirm message submit via native agent-state, not composer text

fm_backend_herdr_send_text_submit now confirms a landed submit by polling
herdr's own agent-state (agent get) for the idle->working transition instead
of reading composer content. Composer scraping remains, unchanged, for the
away-mode daemon's pre-injection empty-box guard only.

This fixes the practical effect of the codex idle-tip gap from the
2026-07-07 incident: codex's dynamic idle-composer hint text can no longer
misread as pending and block/mis-confirm a send, since confirmation no
longer looks at composer text at all. Verified empirically against real
claude and codex agents (timing, swallowed-Enter, unreadable-target, and
already-busy-target scenarios), and against the real away-mode daemon
end-to-end after updating its synthetic supervisor-pane test fixture to
register itself as a real herdr agent (herdr's own report-agent primitive)
so it can still exercise the new confirmation path.

* no-mistakes(review): Captain, harden herdr submit confirmation

* no-mistakes(review): Captain, harden herdr submit confirmation

* no-mistakes(document): Sync Herdr submit docs

* no-mistakes: apply CI fixes

* feat: add quota-balanced crew dispatch selection (#327)

* Add quota-balanced dispatch selection

* no-mistakes(document): Document dispatch selector guidance

* fix(session-start): respawn dead secondmate agents conservatively

* fix(session-start): deterministically respawn dead-shell secondmates

A secondmate agent that exits leaves its backend pane alive as a bare
shell. The session-start endpoint check only verified pane presence, so
recovery and the watcher (which exempts secondmates from stale-pane
detection) never noticed - evidence 2026-07-07: every secondmate in one
fleet was found sitting at a dead zsh shell.

Add fm_backend_agent_alive (bin/fm-backend.sh), a deeper per-backend
liveness probe distinct from pane presence: fm_backend_tmux_agent_alive
classifies the pane's live foreground process via tmux's own
pane_current_command, and fm_backend_herdr_agent_alive reuses the
already-verified pane_agent_state husk classifier. Both are conservative:
anything ambiguous reports unknown, never a false dead.

Wire this into a new session-start-only, locked-and-primary-only sweep in
bin/fm-bootstrap.sh that kills and respawns only a confidently dead
secondmate endpoint, leaving alive/unknown readings untouched - idempotent
by construction, so repeated runs converge without duplicating agents.

* no-mistakes(review): Guard raw secondmate liveness respawns

* no-mistakes(review): Fix detect-only bootstrap test

* no-mistakes(test): Pin liveness fixture harness

* no-mistakes(document): Sync secondmate liveness docs

* no-mistakes: apply CI fixes

* fix: emit stable secondmate nudge selectors (#331)

* Fix NUDGE_SECONDMATES to print stable fm-<id> selectors.

Session-start secondmate sync used to accumulate raw backend window targets
into NUDGE_SECONDMATES, but the liveness sweep in the same bootstrap run can
respawn secondmates onto new endpoints. fm-send with those stale explicit
targets bypasses meta resolution and fails, while fm-<id> resolves correctly.

Accumulate fm-<id> in process_secondmate, update the bootstrap/update contracts
and /updatefirstmate skill, and add a herdr respawn regression test.

* no-mistakes(review): Captain, guard herdr regression jq dependency

* no-mistakes(document): Document stable secondmate nudge selectors

* no-mistakes(lint): Fix shell lint hints

* feat: require bootstrap detection for AXI tools (#332)

* Make tasks-axi and quota-axi required bootstrap tools

Add both to the normal toolchain checks alongside lavish-axi, keep the
tasks-axi 0.1.1+ compatibility gate, and report quota-axi through the
standard MISSING install-consent flow. TASKS_AXI: available remains a
backlog-backend capability signal only; manual opt-out no longer suppresses
the missing-tool report.

Update bootstrap tests and point docs/configuration.md at the canonical
toolchain contract.

* no-mistakes(review): Clarify manual backlog bootstrap reporting

* no-mistakes(document): Document bootstrap AXI tools

* bearings: delete today's report before recreating (#333)

Replace overwrite-in-place wording with explicit delete-then-create
instructions so agents do not modify an existing daily report file.

* feat: guard primary turn ends across harnesses (#339)

* Add primary turn-end guards for all harnesses

* no-mistakes(review): Normalize Codex hook cwd resolution

* no-mistakes(review): Fix OpenCode guard worktree anchoring

* no-mistakes(review): Anchor Codex guard outside nested roots

* no-mistakes(review): Anchor Codex guard to hook root

* no-mistakes(review): Avoid Grok permission escalation

* no-mistakes(document): Sync turn-end guard docs

* fix: resolve backend selectors by exact task id first (#342)

* fix backend selector task id resolution

* no-mistakes(document): Document selector resolution behavior

* fix: scale bootstrap fleet-sync timeout (#341)

* fix bootstrap fleet sync timeout

* no-mistakes(review): Fix bootstrap fleet-sync timeout regressions

* no-mistakes(document): Sync bootstrap timeout docs

* no-mistakes(lint): Clean ShellCheck directives

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* feat: add fleet snapshot and view commands (#343)

* Add fleet snapshot and view

* no-mistakes(review): Fix fleet snapshot parsing and overrides

* no-mistakes(review): Fix secondmate fleet rendering

* no-mistakes(review): Fix backlog title and completion parsing

* no-mistakes(review): Include durable scout reports

* no-mistakes(review): Fix fleet snapshot edge cases

* no-mistakes(review): Captain: gate fleet hints on current state

* no-mistakes(review): Captain: parse bracketed Done PR artifacts

* no-mistakes(document): Sync fleet snapshot docs

* fix(fm-send): fail loudly on unresolvable send targets (#254)

* Make fm-send fail loudly on unresolved targets

* no-mistakes(review): Document fm-send FM_HOME contract

* Fix fm-send readiness docs and backend send path

* Fix fm-send docs for cmux and X skill metadata

* Make gotmp teardown test home-explicit

* Scope watcher warning wording to fm-send

* Fix fm-send review findings

* Verify explicit tmux targets before sending

* Isolate turnend guard test home

* no-mistakes(document): Documented fm-send FM_HOME/backend guard additions missing from doc inventories

---------

Co-authored-by: mielyemitchell <249051873+mielyemitchell@users.noreply.github.com>

* fix: deliver AFK escalations through herdr supervisors (#353)

* fix afk codex ghost composer delivery

* no-mistakes(review): Harden AFK startup flag writes

* no-mistakes(review): Harden AFK daemon liveness checks

* no-mistakes(document): Sync AFK herdr docs

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* feat: add harness-aware supervision (#367)

* Add harness-aware supervision

* no-mistakes(review): Captain, harden watcher supervision regressions

* no-mistakes(review): Captain, harden watcher supervision cadence

* no-mistakes(review): Harden watcher supervision ownership

* no-mistakes(review): Captain, harden Pi extension marker

* no-mistakes(review): Captain, harden Pi supervision restart checks

* no-mistakes(review): Harden watcher ownership checks

* no-mistakes(review): Captain, harden Pi supervision loading

* no-mistakes(review): Captain, require Pi guard extension loading

* no-mistakes(review): Captain, harden watcher supervision recovery

* no-mistakes(test): Fix fm-send baseline log filtering

* no-mistakes(document): Sync harness supervision docs

* no-mistakes: apply CI fixes

* fix: split X-mode replies by platform (#369)

* fix: make x replies split by platform

* no-mistakes(review): Captain: preserve Discord recovery relink context

* no-mistakes(test): Captain: keep split markers outside fences

* no-mistakes(document): Sync X-mode reply docs

* fix: make stow memory writes inspect before update (#372)

* docs: make stow inspect-then-update

* no-mistakes(review): Remove unsupported archive-body guidance

* no-mistakes(review): Clarify stow read-before-write exception

* no-mistakes(test): Require archive-body for stow task notes

* no-mistakes(document): Sync stow memory docs

* no-mistakes(lint): Silence ShellCheck source warning

* fix(watcher): wait when arm attaches to a healthy watcher (#375)

* fix: attach-and-wait when arm finds a healthy watcher

Grok and Claude re-arm after every turn with work in flight. When a
watcher was already healthy, fm-watch-arm exited immediately with
watcher: healthy, which completed the harness background task and
injected an empty false wake.

Attach to the live identity-matched holder instead, stay until that
cycle ends, then exit 0 so notify fires for a real end-of-cycle. The
peer-startup-race path uses the same contract. --restart and the
started path are unchanged.

* no-mistakes(review): Gate restart watcher peer attach

* no-mistakes(document): Sync watcher arm docs

* feat(pi): simplify primary session launch (#386)

* docs(readme): reformat Quick Start and recommend Grok equally with Claude Code

* no-mistakes(review): Captain: align harness launch guidance

* no-mistakes(review): Captain, clarify Pi supervised launch

* no-mistakes(review): Captain, document Pi first-launch bridge

* feat(pi): track primary watcher extension for plain-pi launch

Move Pi's primary watcher bridge from a generated state/ file to a
tracked .pi/extensions/fm-primary-pi-watch.ts, matching how the turn-end
guard extension already works: self-hashing version, project-local
auto-discovery after one-time Pi trust. This drops the state/-generation
step and dual -e requirement from the happy path, so Pi's Quick Start
launch becomes plain 'pi', the same friction class as 'claude' and
'grok --trust'.

- bin/fm-pi-watch-extension.sh is removed; nothing generates the
  extension anymore since it is committed.
- fm-session-start.sh and fm-supervision-instructions.sh resolve the
  watcher extension path from FM_ROOT instead of state/, and the
  session-start diagnostic now points at restarting plain pi after
  trust, with -e as a documented fallback.
- fm-spawn.sh points Pi secondmate launches at the tracked extension
  path in the secondmate home instead of generating a state/ copy.
- README Quick Start Pi block is now just 'pi' plus a trust note.
- Tests, docs, and the harness-adapters skill updated to match.

* fix(pi): drop backticks from session-start diagnostic to satisfy shellcheck SC2016

* feat(supervision): prevent unsafe watcher-arm commands (#387)

* feat(supervision): add PreToolUse seatbelt against watcher-arm anti-patterns

Adds bin/fm-arm-pretool-check.sh, a shared PreToolUse-style checker that
denies a primary shell command backgrounding, piping, or bundling the
watcher arm/checkpoint, or force-killing the watcher process broadly -
the exact shapes that silently took Grok's supervision down. Wires it
into all five verified harnesses (grok, claude, codex, opencode, pi),
each validated empirically against the real harness.

Also fixes a grok 0.2.93 regression discovered during that validation: the
existing turnend-guard Stop hook's bare root variable broke grok's own
variable pre-substitution and silently no-op'd the hook.

* no-mistakes(review): Harden watcher arm validation

* no-mistakes(review): Harden arm guard metacharacter checks

* no-mistakes(review): Harden nested shell arm guard

* fix(lint): rewrite SC2015 guards in fm-arm-pretool-check.sh as if/then

A && B || C is not if-then-else; C can run when A is true. Replace both
occurrences of the quote-state early-continue with an explicit if/then.

* fix(pi): restore primary watcher supervision lifecycle (#397)

* fix Pi primary supervision lifecycle

* no-mistakes(document): Synchronize Pi primary extension documentation

* fix: keep persistent secondmates out of the main backlog (#398)

* fix secondmate backlog guidance

* no-mistakes(review): Require reasons for captain backlog holds

* no-mistakes(test): Document secondmate handoff skill requirement

* fix secondmate teardown reminder

* no-mistakes(document): sync teardown reminder docs to work-items-only backlog contract

* fix(backlog-handoff): move full item blocks including indented bodies (#401)

* fix(backlog-handoff): move full item blocks including indented bodies

fm-backlog-handoff only moved the checklist header line, so multi-line
item bodies were left orphaned in the source backlog and never reached
the secondmate. Move the full block (header plus indented body lines)
atomically, treating body membership by indentation so lines like
## Intent stay with the item, and add regression coverage.

* no-mistakes(review): Captain: preserve EOF handoff terminators

* no-mistakes(review): treat blank lines inside item bodies as movable body

* no-mistakes(document): sync backlog-handoff docs with full-block move behavior

* feat(herdr): make Herdr lab lifecycle safety deterministic for briefs (#402)

* guard Herdr lab lifecycle in briefs

* no-mistakes(review): Fix Herdr lab helper and provisioning safety

* no-mistakes(review): Captain, harden Herdr lab lifecycle safety

* no-mistakes(review): fix Herdr lab test cleanup ordering and brief help range

* no-mistakes(review): reject leading options in Herdr lab run guard

* no-mistakes(review): strip leading non-alnum in Herdr lab name generator

* no-mistakes(document): document Herdr lab helper and --herdr-lab brief flag

* no-mistakes(lint): add shellcheck disable for deliberate SC2016 literals in fm-brief herdr-lab

* no-mistakes: apply CI fixes

* fix(watcher): classify arm-command seatbelt by execution position (#403)

* fix watcher arm command policy

* no-mistakes(review): Harden watcher command policy parsing

* no-mistakes(review): Captain: harden watcher policy parsing

* no-mistakes(review): harden watcher policy for expanded paths, direct-watch, and sound prefilter

* no-mistakes(review): close prefilter and classifier locale/ANSI-C watcher-path decode gaps

* no-mistakes(review): fail closed on loop-wrapped broad watcher kills

* no-mistakes(document): sync docs for watcher-arm command-position policy

* fix: reconcile existing AGENTS.md safely (#405)

* fix(agents-md): inject self-governance section into existing AGENTS.md

fm-ensure-agents-md.sh only appended the canonical "## Maintaining this
file" section on skeleton create or CLAUDE.md promotion, so an existing
AGENTS.md that lacked it exited unchanged and forced hand-copying the
wording during a rollout across existing projects. Call the already-
idempotent ensure_maintenance_section on the existing-AGENTS.md paths and
report whether the file changed; a re-run and an already-complete file stay
byte-identical.

Also fixes #389: refuse a case-variant real memory file (e.g. a lowercase
agents.md) instead of silently emitting a CLAUDE.md symlink whose uppercase
literal target dangles once the tree lands on a case-sensitive filesystem.

Tests extend tests/fm-ensure-agents-md.test.sh; skeleton-create and
CLAUDE.md-promotion regressions still pass. Docs updated to match.

* no-mistakes(review): Captain: preserve CRLF maintenance-section idempotency

* no-mistakes(review): Preserve CRLF during maintenance-section injection

* no-mistakes(review): Captain: harden dangling-symlink regression coverage

* no-mistakes(document): Document agent-memory injection outcomes

* feat: support project-less secondmate homes (#409)

* feat(secondmate): support project-less homes via --no-projects

fm-brief.sh --secondmate and fm-home-seed.sh now accept an explicit
--no-projects signal to scaffold, seed, and register a secondmate home
whose subject is the firstmate repo itself (no clones). The signal is
mutually exclusive with a project list; omitting both still fails loudly
so an accidental omission is never a silent project-less seed. The
registry line renders an empty projects: field, which spawn and the
snapshot already tolerate. Docs updated in the secondmate-provisioning
skill and both script headers.

* no-mistakes(review): Captain: document project-less secondmate flow

* no-mistakes(review): Captain: refuse project-less reseeding of populated homes

* fix(seed): fail closed on unreadable project data

* no-mistakes(review): Captain: reject stale projectful charters

* no-mistakes(review): Captain: fail closed on unsafe project paths

* no-mistakes(review): Captain: validate project-less charter clone sections

* no-mistakes(document): Document project-less secondmate seeding

* fix: delegate backlog handoffs to tasks-axi (#411)

* wip(handoff): record verified delegation design + tasks-axi mv blocker

No production code changed yet. tasks-axi mv (v0.2.1) cannot atomically
move a blocked-by-linked item set across backlogs (deadlocks both orders,
no batch/--force), which fm-secondmate-lifecycle-e2e requires. Parked
pending a tasks-axi connected-set mv enhancement; note captures the
verified design, semantics, test/CI/doc changes, and resume checklist.

* refactor(handoff): delegate the item move to tasks-axi mv

fm-backlog-handoff.sh's two-pass awk was a second parser of the backlog
format and the source of the PR #401 body-orphaning drift. Delete it and
delegate the move to `tasks-axi mv <id>... --to <dest>` (v0.2.2 atomic
multi-id), the single owner of the format: a connected set (blocker plus
dependents) moves together with blocked-by preserved, item blocks stay
byte-exact, and destination section placement holds. The helper keeps only
the fleet-level validation tasks-axi cannot know - secondmate-home
resolution, the seeded-home safety checks, the In-flight refusal, and
idempotent per-key reporting - and is atomic: on any move failure nothing
moves.

Tests: fm-backlog-handoff.test.sh keeps PR #401's regression matrix but now
exercises the delegated path and skips cleanly when tasks-axi is absent; the
two whole-file fixtures move to tasks-axi's canonical whitespace. The
lifecycle-e2e and safety move-cases gain the same skip guard. CI installs
tasks-axi so the delegated path is exercised. Docs state that
config/backlog-backend=manual governs firstmate's own hand-editing, not this
validated helper, which delegates fleet-wide because bootstrap requires
tasks-axi on PATH.

Remove the now-redundant WIP design note.

* no-mistakes(review): Captain: harden atomic backlog handoffs

* no-mistakes(review): Captain: enforce queued-only backlog handoffs

* no-mistakes(review): Captain: harden handoff section parsing

* no-mistakes(document): Document delegated backlog handoffs

* no-mistakes(lint): Silence ShellCheck source diagnostics

* fix: ignore secondmate home marker during sync (#417)

* fix: gitignore the secondmate home marker

bin/fm-home-seed.sh writes an untracked .fm-secondmate-home marker into
every seeded secondmate home. A secondmate home is a worktree of the
firstmate repo, so any plain `git status --porcelain` dirtiness check
counted the untracked marker and the home read as dirty forever:
fleet-sync reported it STUCK and the local fast-forward convergence
sweeps risked leaving it stale on firstmate updates.

Add .fm-secondmate-home to the tracked .gitignore so the marker is
invisible to every dirtiness check uniformly, without weakening
fleet-sync's deliberate untracked-counting for project clones.

Convergence chicken-and-egg: existing homes predate the fix and it only
arrives by fast-forward. The already-present marker-tolerant ff-skip
(ignore_seed_marker=yes, used by the bootstrap sweep, /updatefirstmate,
and spawn pre-launch) advances such a home past the fix commit, after
which .gitignore takes over - no hand intervention.

Tests in tests/fm-secondmate-sync.test.sh cover a freshly seeded home
reading clean, an existing marker-only home converging then reading
clean, and a genuinely dirty home still skipping.

* no-mistakes(review): Captain: document standalone-clone update path

* no-mistakes(document): Document secondmate marker migration

* fix(composer): prevent dead-shell message injection (#416)

* fix(composer): stop reading dead-shell prompts as empty agent composers

Consolidate composer empty/pending/unknown classification into one shared
owner, bin/fm-composer-lib.sh's fm_composer_classify_content, delegated to by
all four backend adapters (tmux via fm-tmux-lib.sh, herdr, orca, cmux). This
replaces four drifting copies of the glyph decision.

Safety fix: a bare shell prompt glyph (> $ % #) on an unstructured row is now
classified unknown (a dead shell, unsafe for injection), not empty. It is only
empty inside a bordered composer box (the harness's own prompt). Agent glyphs
❯ (claude) and › (codex) read empty either way. The away-mode injector
(inject_msg) now requires an affirmatively-empty composer, deferring on pending
or unknown, so an escalation can never be typed into (or executed by) a pane
whose agent exited to its login shell.

Regression coverage: new tests/fm-composer-lib.test.sh pins the shared owner;
per-backend dead-shell tests in fm-daemon (tmux + injector), orca, and the
existing herdr/cmux suites. shellcheck clean; herdr incident regressions stay
green.

* no-mistakes(review): Captain: harden composer safety checks

* no-mistakes(test): Stabilize Herdr prune safety setup

* no-mistakes(document): Document composer injection safety

* no-mistakes(lint): Clean composer safety lint

* no-mistakes: apply CI fixes

* feat(watcher): add paused external-wait supervision (#421)

* feat(watcher): add paused/awaiting-external crew state

A crew (or firstmate steering it) can declare a deliberate wait on a known
external dependency with a paused: <reason> status. Both the always-on watcher
and the away-mode daemon absorb such an idle pane through shared fm-classify-lib.sh
vocabulary instead of tripping the possible-wedge stale escalation, and re-surface
it for a recheck only on a long bounded cadence (FM_PAUSE_RESURFACE_SECS) so a
forgotten pause cannot rot invisibly. fm-crew-state.sh reports state: paused
distinctly. A crew that goes idle without declaring a pause classifies exactly as
before. Docs and brief scaffold state lists updated; tests colocated.

* no-mistakes(review): Captain: fix paused-state transitions

* init

* no-mistakes(review): Captain: fix paused-state supervision transitions

* no-mistakes(review): Captain: fix paused supervision handoffs

* no-mistakes(review): Reconcile paused supervision markers

* no-mistakes(review): Captain: prioritize paused states over captain relevance

* no-mistakes(review): Captain: preserve paused-working wedge timer

* no-mistakes(review): Captain: honor configured pause verb in briefs

* no-mistakes(test): Captain: fix AFK paused watcher handoff

* no-mistakes(document): Document declared external waits

* no-mistakes(lint): Clean paused-state lint

---------

Co-authored-by: fmtest <fmtest@example.invalid>

* fix: preserve X-mode follow-up platform limits (#425)

* fix(x-mode): make follow-up platform splitting immune to link ordering

A ~470-char Discord follow-up posted as a (1/2)(2/2) thread split at ~280
chars because fm-x-link only learned the platform from the inbox payload,
and the fmx-respond ack path can drain that inbox file before the task is
linked. A link recorded after cleanup silently lost the platform and the
splitter defaulted to the X 280-char budget.

Make platform resolution ordering-proof:

- fm-x-link now resolves the platform AUTHORITATIVELY by request_id via a
  new fmx_request_relay_context helper (POST /connector/request-context)
  when neither the inbox payload nor carry flags carry it. The request_id
  survives the inbox drain, so a post-cleanup link still learns the right
  split budget. Best-effort: no token/curl or a non-2xx relay degrades to
  the loud warning below rather than a silent X default.
- fm-x-link warns loudly when no platform source resolves, so the loss is
  never silent.
- The fmx-respond procedure now orders link-before-inbox-cleanup so the
  fast local path stays correct without a relay round-trip.

Colocated regression tests: a Discord follow-up >280 <2000 posts as ONE
message even when linked after inbox cleanup, and an unresolvable platform
warns loudly instead of splitting silently. docs/configuration.md documents
the request-context lookup.

The relay endpoint is the companion durable change (see done status); until
it ships, the link-before-cleanup reorder keeps the normal path correct.

* no-mistakes(document): Document X-mode platform recovery

* fix(composer): handle ANSI ghost text safely (#429)

* fix(composer): one ANSI-aware ghost owner covers claude dim + grok truecolor

Away-mode injection wedged all night on the primary claude-on-herdr pane:
the herdr composer classifier never stripped generic dim ghost text (only a
narrow codex bold-wrapped byte-pattern check), so claude's rotating
prompt-suggestion ghost - a bare "❯" then SGR-2 dim text, which herdr's ANSI
pane read preserves - read as real pending input and every escalation deferred
(6524 lifetime "pending input (non-empty composer)" defers; wedge 30623s).

Consolidate ghost extraction into one fleet-wide ANSI-aware owner,
fm_composer_strip_ghost (bin/fm-composer-lib.sh), that drops every
de-emphasised run - dim/faint (SGR 2: claude, codex) AND a dark/muted truecolor
foreground (grok's placeholder, luminance below FM_COMPOSER_GHOST_LUMA_MAX,
default 128, dark-theme assumption). Both ANSI-capable backends route through
it: fm_tmux_composer_state (fm_tmux_strip_ghost is now a thin adapter) and
fm_backend_herdr_composer_state. The herdr-only faint byte-pattern check is
removed and fm_backend_herdr_strip_ansi reduced to a thin adapter over the
shared fm_composer_strip_ansi. Bordered detection now reads the plain row so a
dark box border dropped with the ghost does not lose the composer shape.

This also closes the documented grok TRUECOLOR placeholder gap by the same
mechanism (harness-adapters skill note updated).

Empirical evidence (read-only live capture + isolated tmux, no herdr lifecycle)
and the incident write-up are in docs/herdr-backend.md; deterministic
regressions feed the exact captured bytes through the real classifiers
(tests/fm-backend-herdr.test.sh, tests/fm-composer-ghost.test.sh). Two prior
ghost-test fixtures that used a near-black 38;2;1;2;3 as "real" colored text
(never a realistic real-input color) are corrected to a bright 38;2;224;222;244,
preserving the truecolor payload-skip parser intent.

* no-mistakes(review): Preserve dark shell prompt safety

* no-mistakes(review): Harden erased shell prompt classification

* no-mistakes(document): Document shared composer ghost extraction

* no-mistakes(lint): Normalize tmux comment punctuation

* fix(spawn): make tmux window handling robust under non-default config (#134)

* test: isolate session-start suite from ambient harness markers (#432)

* fix(session-start): isolate harness env markers in suite runner

Neutralize CLAUDECODE, PI_CODING_AGENT, and GROK_AGENT in
run_session_start so ambient interactive shells cannot override the
suite's fake ps harness (local-vs-CI split on the pi supervision case).

* no-mistakes(document): Correct Pi marker documentation

* fix(teardown): retry transient index locks during worktree return (#435)

* fix(teardown): retry treehouse return on transient index.lock

Killed crew git ops can leave a short-lived worktree index.lock that
makes treehouse return fail. Retry on that error signature with a
bounded wait (env-overridable), never force-delete a live lock, and
only then fall back to the existing provably-stale cleanup path.

* no-mistakes(review): Harden teardown retry configuration

* no-mistakes(document): Document teardown index-lock retry behavior

* no-mistakes(lint): Fix empty shell variable assignments

* fix: complete brief help and consolidate documentation (#438)

* docs: de-feature the scripts.md and CONTRIBUTING test inventories

Slice 1 of the documentation redundancy cleanup wave (firstmate scope).

docs/scripts.md: every row is now one purpose clause; script headers
are the declared owner of behavior, flags, and contracts. Coverage
stays 61/61 scripts; bytes drop 19,922 -> 7,958.

CONTRIBUTING.md: the 54-row per-test inventory is gone; contributors
discover tests by listing tests/*.test.sh and reading each script's
own header, and gated tests print their own skip gates. The run
commands, symlink assertions, and watcher smoke line are unchanged.
Lines drop 135 -> 84 (18,797 -> 7,831 bytes).

Two facts that existed only as inventory rows moved into their
owners' headers first: fm-brief.sh's paused-vs-blocked scaffold
distinction and fm-session-start.sh's Pi extension-loaded check.

No instruction-surface or behavior change; AGENTS.md untouched.

* no-mistakes(review): Captain, fix brief help and Grok test discovery

* no-mistakes(review): Captain: document Grok lock-holder test coverage

* fix: detect Git and centralize backend configuration (#445)

* docs: consolidate universal backend contracts into configuration.md

Slice 2 of the documentation redundancy cleanup wave (firstmate scope).

docs/configuration.md is now the declared single owner of three
universal contracts, each with an explicit ownership sentence:
- the universal toolchain list (Toolchain), now also carrying the
  per-tool purpose clauses that previously lived only in the tmux guide;
- the task-selector vocabulary (Runtime backend);
- the tasks-axi compatibility definition (Backlog backend).

The five backend guides' prerequisites replace their verbatim
universal-requirements parentheticals (5 full copies) with a pointer
plus only backend-specific items; zellij/cmux selector restatements
and architecture.md's partial copy become pointers or are dropped;
CONTRIBUTING's compatibility sentence becomes a pointer; two
near-verbatim orca-bootstrap restatements (configuration.md Runtime
backend, orca guide) collapse into the Toolchain owner copy.

Backend-specific setup, behavior, target-string shapes, and every
empirical verification record are untouched. AGENTS.md untouched
(slice 3).

* docs: include git and GitHub auth in the toolchain owner list

The review flagged that the new universal-toolchain owner omitted git
and GitHub authentication while every backend guide now defers its
prerequisites here; bootstrap's NEEDS_GH_AUTH check makes them real
universal requirements.

* no-mistakes(review): Detect Git in bootstrap toolchain

* no-mistakes(document): Clarify GitHub CLI and centralize selector documentation

* feat(daemon): add backend-independent wedge alerts (#444)

* feat(daemon): backend-independent active alert for the wedge alarm

When away-mode injection wedges past max-defer, inject_wedge_alarm only
actively signalled via the tmux status-line, which is skipped on non-tmux
backends. A wedged claude-on-herdr primary left only the passive
state/.subsuper-inject-wedged marker (2026-07-10 overnight incident).

Add a config-gated active alert (config/wedge-alarm, local/gitignored;
FM_WEDGE_ALARM_CHANNEL) that reaches the captain even when every pane and
its status-line is unreadable: an OS-level macOS notification (osascript),
a herdr notification, or a captain-supplied command. Default-on (auto) so
the alarm is never silent; each channel best-effort, degrading to the next
and never crashing the daemon loop. The tmux flash and durable marker stay.

The OS notifiers route through a single FM_WEDGE_ALARM_EXEC seam. When the
daemon is sourced (only tests do this; production execs it) the seam
defaults to "discard", and tests/wake-helpers.sh points it at a recorder,
so it is structurally impossible for any test to post a real notification.

Channels verified once manually on macOS 26.5.2 / herdr 0.7.3; see
docs/wedge-alarm.md.

* no-mistakes(review): Bound wedge alarm notifier execution

* no-mistakes(review): Captain: harden wedge alarm notifier safety

* no-mistakes(review): Captain: harden wedge alarm test notifier isolation

* no-mistakes(review): Captain: harden wedge alarm throttling

* no-mistakes(review): Redact wedge alarm directive logs

* no-mistakes(review): Harden wedge alarm notifier safety

* no-mistakes(review): Track notifier process groups through cleanup

* no-mistakes(document): Document wedge-alarm active alert behavior

* docs: centralize firstmate operating contracts (#447)

* docs(agents): extract conditional AGENTS.md material to owned homes

Slice 3 of the documentation redundancy cleanup wave (firstmate scope):
the always-loaded instruction surface drops from 941 lines / 116,733
bytes (~29k tokens per session per fleet member) to 785 / 91,353
(~22.8k tokens), moving only audit-identified conditional and
situational material while preserving every load-bearing invariant at
its trigger point via the inline-stub pattern.

Moves, each to one declared owner plus an inline stub:
- section 3's bootstrap output-line handbook (~44 lines) -> new
  agent-only bootstrap-diagnostics skill, added to the section 13
  trigger index; the detect-consent-install rule and the
  do-not-dispatch gate stay inline as safety-critical.
- section 4's crew-dispatch JSON schema and field semantics ->
  docs/configuration.md 'Crew dispatch profiles' (pointer direction
  flipped); the intake procedure, precedence, backstop, and
  never-select-unverified rules stay inline.
- section 4's quota-balanced algorithm -> bin/fm-dispatch-select.sh
  header (now the declared owner; usage() converted to the dynamic
  header extraction pattern PR #438 established for fm-brief.sh).
- section 7's spawn resolution narrative and example sprawl ->
  bin/fm-spawn.sh header; the isolated-worktree assertion, refusal-is-
  a-blocker rule, and post-spawn duties stay inline.
- section 7's teardown landed-work mechanics -> bin/fm-teardown.sh
  header (section 1's containment pointer retargeted); the fork benign
  case and never-force rule stay inline.
- section 8's watcher classification narrative -> docs/architecture.md
  'Event-driven supervision' (already the owner); every operative rule
  (one live cycle, no turn ends blind, drain first, wake ladder,
  never-pkill, guard responses) stays inline.
- sections 3/4/6/7 secondmate sync, propagation, schema, and handoff
  restatements -> secondmate-provisioning skill, now the declared
  owner including the literal-file inheritance nuance.
- section 14's X-mode cadence mechanism -> docs/configuration.md
  'X mode (.env)', closing issue #363; activation semantics, the
  fmx-respond trigger, and the terminal-wake final-follow-up duty
  stay inline.

CLAUDE.md stays a symlink; no behavior or test change.

* no-mistakes(document): Centralize contract-owner documentation

* fix(cmux): close last workspace during teardown (#449)

* fix(cmux): close the last/selected workspace in a window at teardown

cmux keeps every window at >=1 workspace, so close-workspace on the only
workspace in a window silently no-ops (returns OK, workspace stays), and a
window holding a live session cannot be closed over the control socket.
That left a selected task workspace open at teardown (the last workspace
in a window is always the selected one).

Add fm_backend_cmux_window_of_workspace and have fm_backend_cmux_kill
create a throwaway default sibling in the target's window before closing
when the target is the last workspace there, so the close lands; the
window keeps a fresh default workspace (cmux's own "closed the last tab"
outcome). Non-last teardown closes directly, as before.

Cover both kill branches plus the helper with fake-CLI unit tests, add a
real-cmux window/count detection smoke assertion, and record the
empirical evidence in docs/cmux-backend.md.

* no-mistakes(review): Derive cmux count from membership snapshot

* no-mistakes(document): Document cmux last-workspace teardown behavior

* fix: recover orphaned packed-refs locks during fleet sync (#453)

* fix(fleet-sync): recover from an orphaned packed-refs.lock

A git ref rewrite (fetch --prune, pack-refs, branch -D) killed after
creating .git/packed-refs.lock but before renaming it - e.g. bootstrap's
timed-out fleet-sync kill or teardown's process kills - leaves a lock that
makes the next sync's fetch fail with "Unable to create
'...packed-refs.lock': File exists", l…
DereKk8 added a commit to DereKk8/firstmate that referenced this pull request Aug 2, 2026
* feat(backends): add experimental cmux runtime backend (#246)

* feat(backends): add cmux runtime backend (experimental)

Session-provider-only adapter for cmux (bin/backends/cmux.sh), mirroring
zellij/herdr structurally, wired into fm-backend.sh and fm-spawn.sh with
--secondmate refused for now. Verified against the real cmux 0.64.17 app:
send does not auto-submit, cwd is creation-time-frozen (zellij-shape,
pwd-marker-probe workaround), close-surface refuses on a workspace's last
surface (falls back to close-workspace), workspace ids do not survive a
relaunch, and the control socket defaults to cmuxOnly access (requires a
one-time password-mode setup, documented in docs/cmux-backend.md). Also
found and fixed a live bug during development: read-screen fails on a
surface that has never been written to, so liveness now uses list-panes
instead. Fake-CLI unit suite (40 tests), a real-binary smoke test, and a
full spawn/steer/peek/done/merge/teardown E2E pass against a real claude
crewmate all pass, including the popup/second-Enter regression class.

* no-mistakes(review): Harden cmux recovery and password parsing

* no-mistakes(review): Harden cmux capture failure handling

* no-mistakes(review): Mark cmux test scripts executable

* no-mistakes(review): Scope cmux workspaces and teardown

* no-mistakes(review): Captain, honor cmux password config override

* no-mistakes(review): Captain, hash cmux home labels

* no-mistakes(document): Sync cmux backend docs

* feat(agents): add firstmate coding guidelines skill (#248)

* Add firstmate-coding-guidelines skill (AGENTS.md diet PR 0)

Encodes the knowledge-placement decision tree, one-owner rule, and
inline-stub pattern from the diet analysis so future contributions stop
adding conditional detail inline. AGENTS.md gets one section-13 trigger
line; fm-brief.sh's REPO argument has no reliable signal for "this is
firstmate's own repo", so the load instruction goes in CONTRIBUTING.md's
Development section instead of the scaffold.

* no-mistakes(review): Captain, align tracked-material trigger scope

* no-mistakes(document): Sync coding guidelines docs

* no-mistakes(lint): Fix Markdown style issues

* fix: add turn-end supervision guard (#249)

* feat: structural Stop-hook backstop for primary turn-end supervision

fm-guard.sh is pull-based: it only warns when some other supervision
script happens to run, so a primary session that ends a turn without
re-arming the watcher and then runs no further fleet-touching command
can sit blind for hours (the 2026-07-04 incident this fixes).

Add bin/fm-turnend-guard.sh, a Claude Code Stop hook registered in the
tracked .claude/settings.json, that fires on every primary turn end and
blocks (exit 2, verified empirically to force continuation) when work
is in flight with no fresh watcher beacon. It never blocks more than
once per turn, using Claude Code's own stop_hook_active loop-guard
field, and scopes itself to the actual primary checkout only (inert in
crewmate/scout worktrees and secondmate homes).

Factor the shared "in-flight but no live watcher" predicate out of
fm-guard.sh into bin/fm-supervision-lib.sh so the pull-based banner and
the push-based hook can never drift on what "unhealthy" means.

Document the verified Stop-hook mechanism and scoping in
docs/turnend-guard.md, add a harness-adapters note, and cover the
predicate and hook with tests/fm-turnend-guard.test.sh.

* no-mistakes(review): Respect active home in turnend guard

* no-mistakes(review): Require live watcher for turn-end guard

* no-mistakes(review): Captain: portable turn-end timing

* no-mistakes(document): Sync turn-end guard documentation

* feat(backends): auto-detect cmux runtime (#250)

* feat(backends): auto-detect cmux runtime from CMUX_WORKSPACE_ID

Wires cmux into fm_backend_detect the same way herdr already is: a
firstmate process running inside a cmux-spawned terminal now spawns
new tasks into cmux by default, no config needed. Verified from cmux's
own shipped source that CMUX_WORKSPACE_ID/CMUX_SURFACE_ID/CMUX_SOCKET_PATH
are unconditionally, non-overridably injected into every terminal
surface it spawns, and that cmux's own CLI treats CMUX_WORKSPACE_ID as
its own ambient-target fallback - the same role $TMUX/HERDR_ENV play for
their backends. CMUX_WORKSPACE_ID is checked last (after $TMUX and
HERDR_ENV=1) since cmux is a terminal application, not a nestable
multiplexer. Socket auth (config/cmux-socket-password) stays required
regardless of how the backend was selected; the existing spawn refusal
now also names the config/backend=tmux / --backend tmux opt-out for a
caller who never explicitly chose cmux.

A live env dump inside a real cmux terminal was not obtained safely on
the shared dev machine (documented in docs/cmux-backend.md); this rests
on the source read instead, mirroring this doc's existing
verified-from-source precedent.

* no-mistakes(review): Fix cmux autodetect docs and tests

* no-mistakes(document): Document cmux auto-detection

* fix(afk): support herdr away-mode injection (#251)

* fix(afk): make the away-mode daemon backend-aware for herdr

bin/fm-supervise-daemon.sh discovered its supervisor pane and injected
via raw tmux calls only, so /afk failed outright on a herdr-based
fleet (TMUX_PANE unset, firstmate:0 fallback unresolvable).

Discovery now resolves backend (tmux|herdr) and target independently,
mirroring fm-backend.sh's own runtime auto-detection, with an explicit
FM_SUPERVISOR_BACKEND override alongside the existing FM_SUPERVISOR_TARGET.
zellij/orca refuse loudly at startup instead of misapplying tmux
primitives. Injection (pane-exists probe, busy-guard, composer-guard,
verified submit) now dispatches through bin/fm-backend.sh's generic
primitives, adding a new fm_backend_composer_state dispatcher; the
tmux path is byte-identical to before. Also fixes a pre-existing bug
in fm_backend_target_exists's herdr arm (missing --session, so it
silently misrouted once more than one herdr server was running) found
while verifying this end to end against a real isolated herdr session.

Classification, batching, max-defer, the marker contract, locks, and
wake-queue handling are unchanged - this is a transport-layer fix.

* no-mistakes(review): Corroborate Herdr idle busy state

* no-mistakes(review): Stabilize Herdr daemon startup wait

* no-mistakes(review): Captain, route cmux composer and update AFK docs

* no-mistakes(document): Document AFK supervisor backend support

* docs(agents): move X-mode procedures out of AGENTS (#253)

* docs(agents): collapse X-mode section 14 into fmx-respond/docs pointers

AGENTS.md diet PR 1 of 3 (agentsmd-diet-s2 report, move-plan items 1-2).
Replaces section 14's "Answering"/"Completion follow-up"/"Conversations"/
"Length and threads"/"Preview / dry-run" blocks (54 lines) and the
"Mechanism" narrative (6 lines) with two short pointers: fmx-respond
(section 13) for the procedure, docs/configuration.md "X mode (.env)"
for the wire protocol. Net -55 lines in AGENTS.md.

Destination edits landed first, deletions second (q4 discipline):
- docs/configuration.md: added the "purely additive, watcher untouched"
  guarantee that AGENTS.md's Mechanism block stated but configuration.md
  did not.
- fmx-respond/SKILL.md: added the x-mode-error wake boundary (report as
  a blocker, do not load this skill), the --image flag for replies and
  follow-ups, the "images are for real artifacts, not prose" rule, and
  the dry-run compact-image-marker behavior - none of these were
  previously in the skill even though AGENTS.md described them, so they
  were genuine gaps, not pre-existing duplication. Also made the skill's
  own "Completion follow-up" section the sole, full owner of that
  procedure instead of deferring to AGENTS.md section 14 for substance
  that no longer lives there (two internal cross-references updated to
  point at section 8's terminal-wake trigger and the skill's own section
  instead).

Mechanical line-by-line audit of every removed AGENTS.md line:

Mechanism block (6 lines removed):
- bootstrap artifact-writing description -> already owned by
  docs/configuration.md "X mode (.env)" (locked-bootstrap paragraph)
- check-shim/poll mechanism description -> already owned by
  docs/configuration.md same section
- missing-deps/x-mode-error diagnostic description -> already owned by
  docs/configuration.md ("Relay auth or config problems...") plus
  bin/fm-x-poll.sh's own header comment for the missing-curl/jq mechanics
- opt-out artifact removal description -> already owned by
  docs/configuration.md same section
- "purely additive, no edit to fm-watch.sh/fm-watch-arm.sh/fm-wake-lib.sh/
  afk daemon" guarantee -> MOVED to docs/configuration.md (added in this
  PR; this fact had no other home before)

Answering/Completion follow-up/Conversations/Length and threads/
Preview-dry-run blocks (54 lines removed):
- x-mention wake -> load fmx-respond: already owned by section 13's
  existing trigger line (unchanged) and restated in the new pointer
- x-mode-error wake -> report as blocker, don't load fmx-respond: MOVED
  to fmx-respond/SKILL.md (added in this PR)
- inbox-draining, classification, acting, reply composition, submission,
  cleanup-on-success/failure: already owned by fmx-respond/SKILL.md
  "Procedure" section (unchanged, pre-existing)
- owner-only routing / captain-as-asker framing: already owned by
  fmx-respond/SKILL.md "The asker is your own captain" section
- standing X-mode authorization / autonomous posting / dry-run as only
  non-posting path: already owned by fmx-respond/SKILL.md same section
- acknowledge-first -> act -> follow-up shape, three-case classification:
  already owned by fmx-respond/SKILL.md "A request to act on" section
- destructive/irreversible/security-sensitive escalation guardrail:
  already owned by fmx-respond/SKILL.md "Public channel..." section and
  Procedure step 2c
- dismiss-instead-of-reply for pure acknowledgments, relay re-offer
  prevention, dry-run honoring: already owned by fmx-respond/SKILL.md
  Procedure steps 2b/2c/2e-skip and docs/configuration.md
- public-safety bar (no task ids/internals/captain-private/secrets):
  already owned by fmx-respond/SKILL.md "The reply is public" section
- never-inline-into-shell-command / --text-file or stdin: already owned
  by fmx-respond/SKILL.md Procedure step 2e and Notes
- --image flag for replies (formats, base64, no-inline guarantee): MOVED
  to fmx-respond/SKILL.md Procedure step 2e (added in this PR - this was
  not previously in the skill)
- fm-x-link field names (x_request=, x_request_ts=, x_followups=):
  already owned by AGENTS.md section 2's state/<id>.meta field list
  (untouched, out of scope for this PR) and fmx-respond/SKILL.md
- carry-count/carry-ts relink behavior, three-follow-up budget, milestone
  sparingness, --check/--text-file posting, connector/followup wire
  detail, --final clearing, cap/window graceful degradation: already
  owned by fmx-respond/SKILL.md "Completion follow-up" section (now sole
  owner) and docs/configuration.md wire-protocol paragraphs
- --image flag for follow-ups: MOVED to fmx-respond/SKILL.md "Completion
  follow-up" section (added in this PR - genuine gap)
- "failed task still gets an honest final follow-up": already owned by
  fmx-respond/SKILL.md "Completion follow-up" section
- FMX_DRY_RUN whole-loop previewability: already owned by
  fmx-respond/SKILL.md "Dry-run / preview mode" section
- in_reply_to conversation continuity, untrusted-thread handling,
  follow-up worthiness judgment, relay-owned self-reply guard/cap:
  already owned by fmx-respond/SKILL.md "The direct ask is the captain's"
  section and Notes (one bullet is a verbatim match)
- concise-by-default / no hand-numbered threads: already owned by
  fmx-respond/SKILL.md "Voice" section
- auto-split behavior, char/tweet caps, premium-independence, wire shape
  ({text}/{text,texts}): behavior already owned by fmx-respond/SKILL.md
  Voice section; exact defaults and wire shape already owned by
  docs/configuration.md; "premium-independent" mechanics already owned
  by bin/fm-x-reply.sh's own header comment
- "images are for real artifacts, not prose": MOVED to fmx-respond/
  SKILL.md "Voice" section (added in this PR - genuine gap)
- image-on-thread wire behavior: already owned by docs/configuration.md;
  reinforced in fmx-respond/SKILL.md's new --image note
- dry-run POST-body shape, endpoint marker, truthy-value definition,
  jq-only dependency, end-to-end testability, x-outbox inspection:
  already owned by fmx-respond/SKILL.md "Dry-run / preview mode" section
  (several near-verbatim matches) and docs/configuration.md wire detail
- dry-run compact image marker: MOVED to fmx-respond/SKILL.md "Dry-run /
  preview mode" section (added in this PR - genuine gap)

Section 8's terminal-wake completion-follow-up trigger (the one fact
required to survive inline) is untouched and already present; the new
section 14 pointer references it instead of restating it.

Nothing outside section 14 (plus the two destination files) is touched.
Full test suite green, including all 74 fm-x-mode.test.sh checks.

* no-mistakes(review): Preserve X-linked follow-up triggers

* no-mistakes(review): Fix x-mode error trigger

* no-mistakes(document): Docs cross-reference synchronized

* no-mistakes(lint): Clean Markdown lint pass

* docs: trim duplicated harness guidance (#255)

* docs(agents): trim section 4 harness/secondmate duplication

AGENTS.md diet PR 2 of 3 (data/agentsmd-diet-s2/report.md, move-plan
items 3-4; redundancy item 2 folded into item 3).

Removed the five claude/codex/grok/pi/opencode model/effort-flag
bullets from section 4 - byte-for-byte duplicated by
harness-adapters' "Launch profile axes" table (which is already a
superset: it carries verified CLI versions per adapter that the
AGENTS.md bullets lacked). Replaced with a one-line pointer; the
skill is already loaded before every spawn per section 4's own
closing trigger, so no new trigger was needed.

Moved the config/secondmate-harness model/effort pin-format detail
(the `<harness> [<model>] [<effort>]` line format, the
secondmate-model/secondmate-effort accessors, back-compat, and the
durability-across-respawn behavior) into secondmate-provisioning,
which is already a mandatory load at every secondmate lifecycle
touchpoint. Added the destination content to the skill first, then
replaced the AGENTS.md paragraph with a 3-line pointer.

Mechanical audit - every removed line's new home:
- 5 harness bullets (claude/codex/grok/pi/opencode model+effort
  flags, per-harness max-omission rationale) -> already present in
  harness-adapters SKILL.md's "Launch profile axes" table (lines
  53-59), confirmed fact-by-fact before deleting.
- "config/secondmate-harness may also pin..." paragraph (pin format,
  bare-harness back-compat, secondmate-model/secondmate-effort
  accessors, per-spawn override precedence, respawn durability,
  secondmate-only scope) -> secondmate-provisioning SKILL.md's
  "Charter and seed" section, added verbatim before this trim.
- The following paragraph (inheritable config: crew-dispatch.json,
  crew-harness, backlog-backend) is untouched - out of scope for
  this PR, still inline.
- The bootstrap CREW_DISPATCH effort-mismatch diagnostic sentence is
  untouched - not part of the five-bullet duplication, stays inline.

No script changes. Section 4 shrinks from 104 to 87 lines
(958 -> 901 total AGENTS.md lines) with zero facts lost: every fact
is reachable through harness-adapters or secondmate-provisioning,
both already mandatory loads at the relevant lifecycle points.

* no-mistakes(document): Align secondmate skill triggers

* no-mistakes(lint): Markdown style clean

* fix: anchor turn-end Stop hook to project root (#256)

* Fix turn-end Stop hook to use CLAUDE_PROJECT_DIR path

Claude Code runs hook commands via /bin/sh from the session cwd, so the
bare relative bin/fm-turnend-guard.sh path fails when cwd is not the repo
root. Anchor the command with "$CLAUDE_PROJECT_DIR"/bin/fm-turnend-guard.sh
instead; verified CLAUDE_PROJECT_DIR is set on Stop hooks in Claude Code
2.1.201. Document the cwd caveat and add a settings.json regression test.

* no-mistakes(document): Document Stop hook path anchoring

* docs: trim firstmate agent guidance duplication (#258)

* docs: trim AGENTS.md redundancy (diet PR 3/3)

Consolidates five duplicated passages to a single owner each, per
data/agentsmd-diet-s2/report.md redundancy items c3-c7:

- Inheritable-config propagation mechanism: owned by section 3 (where
  the sweep runs); sections 4 and 7 keep compact references. Section 4
  retains its one genuinely unique fact (crew-harness inherit-vs-fallback
  semantics), just no longer restates the propagation mechanism itself.
- Landed-work definition: owned by section 7's ship-teardown detail
  (PR-containment mechanics, pr= discovery fallback); section 1's hard
  rule #3 keeps the rule plus a three-case summary and a pointer.
- Backend meta-field enumeration: owned by docs/configuration.md
  ("Runtime backend", already comprehensive including cmux) and each
  backend's own doc; AGENTS.md keeps only the fields common to every
  task plus a pointer.
- Dropped one redundant restatement of "silence is correct while
  waiting" in section 8.
- Worktree-tangle guard explanation: owned by section 8 (already the
  fuller, cross-referenced version); section 3's TANGLE bullet keeps
  the remediation action and points at section 8 for the why.

Also adds two captain-requested single-sentence rules: invoke bin/
scripts by absolute $FM_ROOT path after any cd away from the home, and
a backend spawn refusal must be surfaced to the captain rather than
silently worked around by switching backends.

AGENTS.md: 901 -> 889 lines, 112355 -> 108560 bytes.

* no-mistakes(review): Clarify post-cd bin invocation guidance

* no-mistakes(document): Sync AGENTS trim docs

* no-mistakes(lint): Fix Markdown line style

* feat(backends): improve cmux detection and socket-mode guidance (#259)

* feat(backends): cmux detection fallbacks and socket-mode matrix

Workstream A: cmux's bundled claude wrapper strips every CMUX_* env var on
its passthrough path (reproduced live 2026-07-04, cmux 0.64.17), so a
claude-harness firstmate inside a cmux tab has no CMUX_WORKSPACE_ID.
fm_backend_detect now falls back - macOS-only, only when the primary marker
is absent - to __CFBundleIdentifier=com.cmuxterm.app and then a process
ancestry walk resolved by bundle id (lsappinfo) plus a bundle-shaped ps comm
match. Innermost-first ordering is unchanged and absorbs the
tmux-inside-cmux bundle-id false positive; the auto-detect NOTICE names the
winning fallback signal.

Workstream B: the five socketControlMode values were traced through cmux
source (commit 9c91710e3f58): off/cmuxOnly can never admit an external CLI,
automation admits same-user clients with no secret (0600 socket only),
password needs the auth handshake, allowAll opens the socket to every local
user (0666). Automation mode is now the documented recommendation; the
adapter's refusals name every viable mode, classify Invalid password as
unauth, and the launch-timeout message names the off-mode possibility.

Docs carry the wrapper-strip empirical record, the fallback contract and
authority split, and the full mode matrix with rationale; tests cover the
new detection paths, the nested false positive, and the refusal wording.

* no-mistakes(review): Document cmux fallback detection

* no-mistakes(review): Update cmux architecture docs

* no-mistakes(document): Align cmux backend docs

* fix(backends): scope zellij tabs by firstmate home (#252)

* fix(backends): home-scope zellij tab titles to close cross-home collision gap

Zellij's one shared "firstmate" session has no per-home split and enforces
no tab-name uniqueness, so two firstmate homes with colliding task ids could
send/peek/close each other's tabs - the same gap a no-mistakes review gate
caught for cmux (docs/cmux-backend.md). Ports that fix: every new tab is
created with a home-scoped title (fm-<home-label>-<id>), and every
list/find/recover/kill path scopes matches to this home's own tag. A tab
spawned before this change still matches via its old untagged bare title,
but only when unambiguous - two live tabs sharing a bare title refuse rather
than guessing which one is ours.

Factors the home-label/hash derivation shared with cmux into
bin/fm-backend-hometag-lib.sh so the two adapters can't drift.

* no-mistakes(review): Fix zellij child teardown home tag

* no-mistakes(review): Fix zellij teardown and selector scoping

* no-mistakes(document): Sync zellij home-scope docs

* fix: sync project clones after merged PR wakes (#293)

* fix(fleet-sync): auto-sync on merged-PR wake, accept project name

fm-fleet-sync.sh's single-project form failed on a bare project name
("not a directory"), forcing hand-typed full paths (4 manual runs in
one incident). It now resolves a bare name or projects/<name> against
the home's projects dir.

AGENTS.md now encodes the trigger: a wake whose status reports a
merged PR for a project cloned in this home runs fleet-sync for that
project as part of handling the wake, so a secondmate-reported merge
does not leave the primary's clone stale until the next session start
or teardown.

* no-mistakes(review): Fix fleet-sync project name shadowing

* no-mistakes(document): sync fleet-sync docs

* fix: canonicalize spawn worktree path checks (#294)

* fix(spawn): canonicalize worktree-isolation guard against symlinked project prefixes

fm-spawn.sh compared a logical PROJ_ABS against the physically-resolved
pane cwd every backend reports, so a project reached through a symlinked
prefix (e.g. macOS's /tmp -> /private/tmp) could trip the isolation
guard's false refusal before treehouse ever moved the pane. Canonicalize
once into PROJ_ABS_REAL and compare against that everywhere instead.

* no-mistakes(review): Canonicalize spawn cwd comparisons

* no-mistakes(document): Refresh symlinked spawn docs

* docs: add Orca operator skill (#276)

* docs: add Orca operator skill

* no-mistakes(document): Document Orca checklist

---------

Co-authored-by: Stephen Brouhard <vesta@stephens-macbook-air.tail2122af.ts.net>

* fix: surface green PRs during CI monitoring (#297)

* fix(crew-state): detect green-PR CI monitoring, escalate repeat wedges

fm-crew-state.sh's ci step never distinguishes "still waiting on checks"
from "checks green, waiting on merge" via axi status alone, since a repo
that defers merge to the captain keeps the ci step at status=running for
the whole monitor phase. Read the ci step's own log tail (axi logs) for
the checks-passed marker and surface done instead of a false "validating
(running)" - verified against the real PR #252 run's ci.log.

The watcher's wedge timer can re-escalate the same stale pane forever
without ever signaling that it is a repeat; track a per-pane consecutive
escalation count and add a demand-deep-inspection marker to the wake
payload once it crosses a threshold, so the supervisor can no longer
dismiss each one as an isolated, still-validating pane.

Also clarify the ship-brief's checks-green line: it is owed at the
CI-ready return point, not after the background monitor-until-merge
loop finishes.

* no-mistakes(review): Captain, distinguish pending no-checks CI marker

* no-mistakes(review): Harden CI relapse handling

* no-mistakes(review): Block stale done during fixing

* no-mistakes(review): Captain, tighten CI status gating

* no-mistakes(review): Captain, harden stale CI green handling

* no-mistakes(review): Captain, recognize ranged CI rearm markers

* no-mistakes(document): Sync crew-state supervision docs

* fix(teardown): recover provably stale git index locks (#296)

* fix(teardown): recover from a stale worktree git index.lock

A crew process killed mid-git-operation can leave a stale
.git/worktrees/<wt>/index.lock behind, making fm-teardown.sh's
`treehouse return --force` fail closed. On that failure, retry once
after a short wait (the owning process may be exiting), then remove
the lock and retry once more only when it is provably stale: old
enough by mtime and lsof shows no live holder on the lock or the
worktree itself. A lock that isn't provably stale is left in place and
the original failure still surfaces.

* no-mistakes(review): Harden teardown lock refusal paths

* no-mistakes(review): Harden stale-lock teardown safety rechecks

* no-mistakes(review): Harden stale teardown lock checks

* no-mistakes(document): Document teardown lock recovery

* feat(bin): encode project AGENTS.md authoring bar with canonical self-governance section (#307)

* Encode project AGENTS authoring bar

* no-mistakes(review): Captain, centralize CLAUDE promotion governance

* no-mistakes(review): make ensure_maintenance_section idempotent-success, drop || true guards

* no-mistakes(review): separate appended maintenance section on newline-less CLAUDE.md promotion

* no-mistakes(review): assert maintenance heading present before separator check in test

* no-mistakes(document): sync docs with AGENTS.md authoring bar and self-governance

---------

Co-authored-by: fmtest <fmtest@example.invalid>

* feat(skills): add captain-invocable bearings status-report skill (#300)

* Add captain-invocable bearings skill

Generates a pick-up-where-I-left-off status report from live fleet
state to data/status-report-<YYYY-MM-DD>.md plus a concise chat
summary. Read-mostly procedure: reads backlog, per-task crew state
via bin/fm-crew-state.sh, open PRs via gh-axi, scout reports,
pending decisions, and date-gated queued work; composes the
exemplar's sections (TL;DR, Check first, Landed, In flight, Plans,
Decisions pending, Date-gated/queued); never tears down, merges, or
mutates task state as a side effect.

* no-mistakes(document): docs: list new /bearings skill in README built-in skills table

* fix(watcher): make PID identity locale-invariant (#285)

* fix(watcher): pin LC_ALL=C in fm_pid_identity for locale-invariant identity

ps's lstart date format follows the caller's LC_TIME/LC_ALL. The watcher records
its process identity under one locale, but arm/guard/turn-end re-read it under the
machine's ambient locale. On a non-C locale (e.g. ko_KR) the two strings differ
only in the date portion, so fm_watcher_lock_matches_pid / fm_watcher_healthy
reject a genuinely live watcher - breaking fm-watch-arm.sh, fm-guard.sh, and
fm-turnend-guard.sh on every non-C-locale machine.

Pin LC_ALL=C on that one ps call so the write and read sides agree regardless of
machine locale, matching the LC_ALL=C determinism the file already uses elsewhere.
Add a colocated regression test asserting fm_pid_identity is locale-invariant
across exported LC_ALL/LC_TIME.

* no-mistakes(document): Document watcher PID identity coverage

* docs: document codex app backend contract (#222)

* docs: reconcile Codex App backend contract

* no-mistakes(document): Sync backend docs

* docs: clarify Codex Desktop bridge blocker

* no-mistakes(document): Align Codex App backend docs

* no-mistakes(test): Captain, stabilize watcher self-eviction test cadence

* no-mistakes(document): Document Codex App backend contract

* no-mistakes(document): Captain, document blocked codex-app coverage

* docs: make Codex App contract doc authoritative

* no-mistakes(document): Align Codex App backend docs

* docs: redact local Codex App smoke paths

---------

Co-authored-by: Stephen Brouhard <vesta@stephens-macbook-air.tail2122af.ts.net>

* docs: add Codex Desktop coordination skill (#275)

* docs: add Codex App coordination skill

* no-mistakes(review): Captain, mark Codex App skill agent-only

* no-mistakes(document): Document Codex App backend boundary

* no-mistakes(document): Captain, document Codex Desktop backend boundary

* no-mistakes(lint): Captain, lint clean

* no-mistakes(document): Document Codex Desktop boundaries

* docs: narrow Codex App skill playbook

* fix(afk): stop herdr escalation redelivery loop (#317)

* fix(afk): recognize unbordered herdr composer rows to stop escalation redelivery loop

fm_backend_herdr_composer_state only recognized bordered composer rows
(the grok shape). Real claude and codex render their live input row
with no border at all, so once a harness's own startup banner scrolled
out of the capture window the classifier read the composer as unknown
forever. fm_backend_herdr_send_text_submit never confirmed "empty", so
escalate_flush never cleared state/.subsuper-escalations, and the
away-mode daemon retyped and resubmitted the same buffered digest every
housekeeping cycle - reproduced live against a real herdr+claude pane
(5+ identical deliveries in 40s).

The classifier now recognizes an unbordered (bare) composer row led by
a known prompt glyph alongside the existing bordered shape, keeping
whichever match is bottom-most so a stale decorative box never
outranks the live composer.

* no-mistakes(review): Narrow herdr bare prompt matcher

* no-mistakes(document): Sync herdr composer docs

* fix(backends): confirm Herdr submits with native agent state (#323)

* fix(herdr): confirm message submit via native agent-state, not composer text

fm_backend_herdr_send_text_submit now confirms a landed submit by polling
herdr's own agent-state (agent get) for the idle->working transition instead
of reading composer content. Composer scraping remains, unchanged, for the
away-mode daemon's pre-injection empty-box guard only.

This fixes the practical effect of the codex idle-tip gap from the
2026-07-07 incident: codex's dynamic idle-composer hint text can no longer
misread as pending and block/mis-confirm a send, since confirmation no
longer looks at composer text at all. Verified empirically against real
claude and codex agents (timing, swallowed-Enter, unreadable-target, and
already-busy-target scenarios), and against the real away-mode daemon
end-to-end after updating its synthetic supervisor-pane test fixture to
register itself as a real herdr agent (herdr's own report-agent primitive)
so it can still exercise the new confirmation path.

* no-mistakes(review): Captain, harden herdr submit confirmation

* no-mistakes(review): Captain, harden herdr submit confirmation

* no-mistakes(document): Sync Herdr submit docs

* no-mistakes: apply CI fixes

* feat: add quota-balanced crew dispatch selection (#327)

* Add quota-balanced dispatch selection

* no-mistakes(document): Document dispatch selector guidance

* fix(session-start): respawn dead secondmate agents conservatively

* fix(session-start): deterministically respawn dead-shell secondmates

A secondmate agent that exits leaves its backend pane alive as a bare
shell. The session-start endpoint check only verified pane presence, so
recovery and the watcher (which exempts secondmates from stale-pane
detection) never noticed - evidence 2026-07-07: every secondmate in one
fleet was found sitting at a dead zsh shell.

Add fm_backend_agent_alive (bin/fm-backend.sh), a deeper per-backend
liveness probe distinct from pane presence: fm_backend_tmux_agent_alive
classifies the pane's live foreground process via tmux's own
pane_current_command, and fm_backend_herdr_agent_alive reuses the
already-verified pane_agent_state husk classifier. Both are conservative:
anything ambiguous reports unknown, never a false dead.

Wire this into a new session-start-only, locked-and-primary-only sweep in
bin/fm-bootstrap.sh that kills and respawns only a confidently dead
secondmate endpoint, leaving alive/unknown readings untouched - idempotent
by construction, so repeated runs converge without duplicating agents.

* no-mistakes(review): Guard raw secondmate liveness respawns

* no-mistakes(review): Fix detect-only bootstrap test

* no-mistakes(test): Pin liveness fixture harness

* no-mistakes(document): Sync secondmate liveness docs

* no-mistakes: apply CI fixes

* fix: emit stable secondmate nudge selectors (#331)

* Fix NUDGE_SECONDMATES to print stable fm-<id> selectors.

Session-start secondmate sync used to accumulate raw backend window targets
into NUDGE_SECONDMATES, but the liveness sweep in the same bootstrap run can
respawn secondmates onto new endpoints. fm-send with those stale explicit
targets bypasses meta resolution and fails, while fm-<id> resolves correctly.

Accumulate fm-<id> in process_secondmate, update the bootstrap/update contracts
and /updatefirstmate skill, and add a herdr respawn regression test.

* no-mistakes(review): Captain, guard herdr regression jq dependency

* no-mistakes(document): Document stable secondmate nudge selectors

* no-mistakes(lint): Fix shell lint hints

* feat: require bootstrap detection for AXI tools (#332)

* Make tasks-axi and quota-axi required bootstrap tools

Add both to the normal toolchain checks alongside lavish-axi, keep the
tasks-axi 0.1.1+ compatibility gate, and report quota-axi through the
standard MISSING install-consent flow. TASKS_AXI: available remains a
backlog-backend capability signal only; manual opt-out no longer suppresses
the missing-tool report.

Update bootstrap tests and point docs/configuration.md at the canonical
toolchain contract.

* no-mistakes(review): Clarify manual backlog bootstrap reporting

* no-mistakes(document): Document bootstrap AXI tools

* bearings: delete today's report before recreating (#333)

Replace overwrite-in-place wording with explicit delete-then-create
instructions so agents do not modify an existing daily report file.

* feat: guard primary turn ends across harnesses (#339)

* Add primary turn-end guards for all harnesses

* no-mistakes(review): Normalize Codex hook cwd resolution

* no-mistakes(review): Fix OpenCode guard worktree anchoring

* no-mistakes(review): Anchor Codex guard outside nested roots

* no-mistakes(review): Anchor Codex guard to hook root

* no-mistakes(review): Avoid Grok permission escalation

* no-mistakes(document): Sync turn-end guard docs

* fix: resolve backend selectors by exact task id first (#342)

* fix backend selector task id resolution

* no-mistakes(document): Document selector resolution behavior

* fix: scale bootstrap fleet-sync timeout (#341)

* fix bootstrap fleet sync timeout

* no-mistakes(review): Fix bootstrap fleet-sync timeout regressions

* no-mistakes(document): Sync bootstrap timeout docs

* no-mistakes(lint): Clean ShellCheck directives

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* feat: add fleet snapshot and view commands (#343)

* Add fleet snapshot and view

* no-mistakes(review): Fix fleet snapshot parsing and overrides

* no-mistakes(review): Fix secondmate fleet rendering

* no-mistakes(review): Fix backlog title and completion parsing

* no-mistakes(review): Include durable scout reports

* no-mistakes(review): Fix fleet snapshot edge cases

* no-mistakes(review): Captain: gate fleet hints on current state

* no-mistakes(review): Captain: parse bracketed Done PR artifacts

* no-mistakes(document): Sync fleet snapshot docs

* fix(fm-send): fail loudly on unresolvable send targets (#254)

* Make fm-send fail loudly on unresolved targets

* no-mistakes(review): Document fm-send FM_HOME contract

* Fix fm-send readiness docs and backend send path

* Fix fm-send docs for cmux and X skill metadata

* Make gotmp teardown test home-explicit

* Scope watcher warning wording to fm-send

* Fix fm-send review findings

* Verify explicit tmux targets before sending

* Isolate turnend guard test home

* no-mistakes(document): Documented fm-send FM_HOME/backend guard additions missing from doc inventories

---------

Co-authored-by: mielyemitchell <249051873+mielyemitchell@users.noreply.github.com>

* fix: deliver AFK escalations through herdr supervisors (#353)

* fix afk codex ghost composer delivery

* no-mistakes(review): Harden AFK startup flag writes

* no-mistakes(review): Harden AFK daemon liveness checks

* no-mistakes(document): Sync AFK herdr docs

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* feat: add harness-aware supervision (#367)

* Add harness-aware supervision

* no-mistakes(review): Captain, harden watcher supervision regressions

* no-mistakes(review): Captain, harden watcher supervision cadence

* no-mistakes(review): Harden watcher supervision ownership

* no-mistakes(review): Captain, harden Pi extension marker

* no-mistakes(review): Captain, harden Pi supervision restart checks

* no-mistakes(review): Harden watcher ownership checks

* no-mistakes(review): Captain, harden Pi supervision loading

* no-mistakes(review): Captain, require Pi guard extension loading

* no-mistakes(review): Captain, harden watcher supervision recovery

* no-mistakes(test): Fix fm-send baseline log filtering

* no-mistakes(document): Sync harness supervision docs

* no-mistakes: apply CI fixes

* fix: split X-mode replies by platform (#369)

* fix: make x replies split by platform

* no-mistakes(review): Captain: preserve Discord recovery relink context

* no-mistakes(test): Captain: keep split markers outside fences

* no-mistakes(document): Sync X-mode reply docs

* fix: make stow memory writes inspect before update (#372)

* docs: make stow inspect-then-update

* no-mistakes(review): Remove unsupported archive-body guidance

* no-mistakes(review): Clarify stow read-before-write exception

* no-mistakes(test): Require archive-body for stow task notes

* no-mistakes(document): Sync stow memory docs

* no-mistakes(lint): Silence ShellCheck source warning

* fix(watcher): wait when arm attaches to a healthy watcher (#375)

* fix: attach-and-wait when arm finds a healthy watcher

Grok and Claude re-arm after every turn with work in flight. When a
watcher was already healthy, fm-watch-arm exited immediately with
watcher: healthy, which completed the harness background task and
injected an empty false wake.

Attach to the live identity-matched holder instead, stay until that
cycle ends, then exit 0 so notify fires for a real end-of-cycle. The
peer-startup-race path uses the same contract. --restart and the
started path are unchanged.

* no-mistakes(review): Gate restart watcher peer attach

* no-mistakes(document): Sync watcher arm docs

* feat(pi): simplify primary session launch (#386)

* docs(readme): reformat Quick Start and recommend Grok equally with Claude Code

* no-mistakes(review): Captain: align harness launch guidance

* no-mistakes(review): Captain, clarify Pi supervised launch

* no-mistakes(review): Captain, document Pi first-launch bridge

* feat(pi): track primary watcher extension for plain-pi launch

Move Pi's primary watcher bridge from a generated state/ file to a
tracked .pi/extensions/fm-primary-pi-watch.ts, matching how the turn-end
guard extension already works: self-hashing version, project-local
auto-discovery after one-time Pi trust. This drops the state/-generation
step and dual -e requirement from the happy path, so Pi's Quick Start
launch becomes plain 'pi', the same friction class as 'claude' and
'grok --trust'.

- bin/fm-pi-watch-extension.sh is removed; nothing generates the
  extension anymore since it is committed.
- fm-session-start.sh and fm-supervision-instructions.sh resolve the
  watcher extension path from FM_ROOT instead of state/, and the
  session-start diagnostic now points at restarting plain pi after
  trust, with -e as a documented fallback.
- fm-spawn.sh points Pi secondmate launches at the tracked extension
  path in the secondmate home instead of generating a state/ copy.
- README Quick Start Pi block is now just 'pi' plus a trust note.
- Tests, docs, and the harness-adapters skill updated to match.

* fix(pi): drop backticks from session-start diagnostic to satisfy shellcheck SC2016

* feat(supervision): prevent unsafe watcher-arm commands (#387)

* feat(supervision): add PreToolUse seatbelt against watcher-arm anti-patterns

Adds bin/fm-arm-pretool-check.sh, a shared PreToolUse-style checker that
denies a primary shell command backgrounding, piping, or bundling the
watcher arm/checkpoint, or force-killing the watcher process broadly -
the exact shapes that silently took Grok's supervision down. Wires it
into all five verified harnesses (grok, claude, codex, opencode, pi),
each validated empirically against the real harness.

Also fixes a grok 0.2.93 regression discovered during that validation: the
existing turnend-guard Stop hook's bare root variable broke grok's own
variable pre-substitution and silently no-op'd the hook.

* no-mistakes(review): Harden watcher arm validation

* no-mistakes(review): Harden arm guard metacharacter checks

* no-mistakes(review): Harden nested shell arm guard

* fix(lint): rewrite SC2015 guards in fm-arm-pretool-check.sh as if/then

A && B || C is not if-then-else; C can run when A is true. Replace both
occurrences of the quote-state early-continue with an explicit if/then.

* fix(pi): restore primary watcher supervision lifecycle (#397)

* fix Pi primary supervision lifecycle

* no-mistakes(document): Synchronize Pi primary extension documentation

* fix: keep persistent secondmates out of the main backlog (#398)

* fix secondmate backlog guidance

* no-mistakes(review): Require reasons for captain backlog holds

* no-mistakes(test): Document secondmate handoff skill requirement

* fix secondmate teardown reminder

* no-mistakes(document): sync teardown reminder docs to work-items-only backlog contract

* fix(backlog-handoff): move full item blocks including indented bodies (#401)

* fix(backlog-handoff): move full item blocks including indented bodies

fm-backlog-handoff only moved the checklist header line, so multi-line
item bodies were left orphaned in the source backlog and never reached
the secondmate. Move the full block (header plus indented body lines)
atomically, treating body membership by indentation so lines like
## Intent stay with the item, and add regression coverage.

* no-mistakes(review): Captain: preserve EOF handoff terminators

* no-mistakes(review): treat blank lines inside item bodies as movable body

* no-mistakes(document): sync backlog-handoff docs with full-block move behavior

* feat(herdr): make Herdr lab lifecycle safety deterministic for briefs (#402)

* guard Herdr lab lifecycle in briefs

* no-mistakes(review): Fix Herdr lab helper and provisioning safety

* no-mistakes(review): Captain, harden Herdr lab lifecycle safety

* no-mistakes(review): fix Herdr lab test cleanup ordering and brief help range

* no-mistakes(review): reject leading options in Herdr lab run guard

* no-mistakes(review): strip leading non-alnum in Herdr lab name generator

* no-mistakes(document): document Herdr lab helper and --herdr-lab brief flag

* no-mistakes(lint): add shellcheck disable for deliberate SC2016 literals in fm-brief herdr-lab

* no-mistakes: apply CI fixes

* fix(watcher): classify arm-command seatbelt by execution position (#403)

* fix watcher arm command policy

* no-mistakes(review): Harden watcher command policy parsing

* no-mistakes(review): Captain: harden watcher policy parsing

* no-mistakes(review): harden watcher policy for expanded paths, direct-watch, and sound prefilter

* no-mistakes(review): close prefilter and classifier locale/ANSI-C watcher-path decode gaps

* no-mistakes(review): fail closed on loop-wrapped broad watcher kills

* no-mistakes(document): sync docs for watcher-arm command-position policy

* fix: reconcile existing AGENTS.md safely (#405)

* fix(agents-md): inject self-governance section into existing AGENTS.md

fm-ensure-agents-md.sh only appended the canonical "## Maintaining this
file" section on skeleton create or CLAUDE.md promotion, so an existing
AGENTS.md that lacked it exited unchanged and forced hand-copying the
wording during a rollout across existing projects. Call the already-
idempotent ensure_maintenance_section on the existing-AGENTS.md paths and
report whether the file changed; a re-run and an already-complete file stay
byte-identical.

Also fixes #389: refuse a case-variant real memory file (e.g. a lowercase
agents.md) instead of silently emitting a CLAUDE.md symlink whose uppercase
literal target dangles once the tree lands on a case-sensitive filesystem.

Tests extend tests/fm-ensure-agents-md.test.sh; skeleton-create and
CLAUDE.md-promotion regressions still pass. Docs updated to match.

* no-mistakes(review): Captain: preserve CRLF maintenance-section idempotency

* no-mistakes(review): Preserve CRLF during maintenance-section injection

* no-mistakes(review): Captain: harden dangling-symlink regression coverage

* no-mistakes(document): Document agent-memory injection outcomes

* feat: support project-less secondmate homes (#409)

* feat(secondmate): support project-less homes via --no-projects

fm-brief.sh --secondmate and fm-home-seed.sh now accept an explicit
--no-projects signal to scaffold, seed, and register a secondmate home
whose subject is the firstmate repo itself (no clones). The signal is
mutually exclusive with a project list; omitting both still fails loudly
so an accidental omission is never a silent project-less seed. The
registry line renders an empty projects: field, which spawn and the
snapshot already tolerate. Docs updated in the secondmate-provisioning
skill and both script headers.

* no-mistakes(review): Captain: document project-less secondmate flow

* no-mistakes(review): Captain: refuse project-less reseeding of populated homes

* fix(seed): fail closed on unreadable project data

* no-mistakes(review): Captain: reject stale projectful charters

* no-mistakes(review): Captain: fail closed on unsafe project paths

* no-mistakes(review): Captain: validate project-less charter clone sections

* no-mistakes(document): Document project-less secondmate seeding

* fix: delegate backlog handoffs to tasks-axi (#411)

* wip(handoff): record verified delegation design + tasks-axi mv blocker

No production code changed yet. tasks-axi mv (v0.2.1) cannot atomically
move a blocked-by-linked item set across backlogs (deadlocks both orders,
no batch/--force), which fm-secondmate-lifecycle-e2e requires. Parked
pending a tasks-axi connected-set mv enhancement; note captures the
verified design, semantics, test/CI/doc changes, and resume checklist.

* refactor(handoff): delegate the item move to tasks-axi mv

fm-backlog-handoff.sh's two-pass awk was a second parser of the backlog
format and the source of the PR #401 body-orphaning drift. Delete it and
delegate the move to `tasks-axi mv <id>... --to <dest>` (v0.2.2 atomic
multi-id), the single owner of the format: a connected set (blocker plus
dependents) moves together with blocked-by preserved, item blocks stay
byte-exact, and destination section placement holds. The helper keeps only
the fleet-level validation tasks-axi cannot know - secondmate-home
resolution, the seeded-home safety checks, the In-flight refusal, and
idempotent per-key reporting - and is atomic: on any move failure nothing
moves.

Tests: fm-backlog-handoff.test.sh keeps PR #401's regression matrix but now
exercises the delegated path and skips cleanly when tasks-axi is absent; the
two whole-file fixtures move to tasks-axi's canonical whitespace. The
lifecycle-e2e and safety move-cases gain the same skip guard. CI installs
tasks-axi so the delegated path is exercised. Docs state that
config/backlog-backend=manual governs firstmate's own hand-editing, not this
validated helper, which delegates fleet-wide because bootstrap requires
tasks-axi on PATH.

Remove the now-redundant WIP design note.

* no-mistakes(review): Captain: harden atomic backlog handoffs

* no-mistakes(review): Captain: enforce queued-only backlog handoffs

* no-mistakes(review): Captain: harden handoff section parsing

* no-mistakes(document): Document delegated backlog handoffs

* no-mistakes(lint): Silence ShellCheck source diagnostics

* fix: ignore secondmate home marker during sync (#417)

* fix: gitignore the secondmate home marker

bin/fm-home-seed.sh writes an untracked .fm-secondmate-home marker into
every seeded secondmate home. A secondmate home is a worktree of the
firstmate repo, so any plain `git status --porcelain` dirtiness check
counted the untracked marker and the home read as dirty forever:
fleet-sync reported it STUCK and the local fast-forward convergence
sweeps risked leaving it stale on firstmate updates.

Add .fm-secondmate-home to the tracked .gitignore so the marker is
invisible to every dirtiness check uniformly, without weakening
fleet-sync's deliberate untracked-counting for project clones.

Convergence chicken-and-egg: existing homes predate the fix and it only
arrives by fast-forward. The already-present marker-tolerant ff-skip
(ignore_seed_marker=yes, used by the bootstrap sweep, /updatefirstmate,
and spawn pre-launch) advances such a home past the fix commit, after
which .gitignore takes over - no hand intervention.

Tests in tests/fm-secondmate-sync.test.sh cover a freshly seeded home
reading clean, an existing marker-only home converging then reading
clean, and a genuinely dirty home still skipping.

* no-mistakes(review): Captain: document standalone-clone update path

* no-mistakes(document): Document secondmate marker migration

* fix(composer): prevent dead-shell message injection (#416)

* fix(composer): stop reading dead-shell prompts as empty agent composers

Consolidate composer empty/pending/unknown classification into one shared
owner, bin/fm-composer-lib.sh's fm_composer_classify_content, delegated to by
all four backend adapters (tmux via fm-tmux-lib.sh, herdr, orca, cmux). This
replaces four drifting copies of the glyph decision.

Safety fix: a bare shell prompt glyph (> $ % #) on an unstructured row is now
classified unknown (a dead shell, unsafe for injection), not empty. It is only
empty inside a bordered composer box (the harness's own prompt). Agent glyphs
❯ (claude) and › (codex) read empty either way. The away-mode injector
(inject_msg) now requires an affirmatively-empty composer, deferring on pending
or unknown, so an escalation can never be typed into (or executed by) a pane
whose agent exited to its login shell.

Regression coverage: new tests/fm-composer-lib.test.sh pins the shared owner;
per-backend dead-shell tests in fm-daemon (tmux + injector), orca, and the
existing herdr/cmux suites. shellcheck clean; herdr incident regressions stay
green.

* no-mistakes(review): Captain: harden composer safety checks

* no-mistakes(test): Stabilize Herdr prune safety setup

* no-mistakes(document): Document composer injection safety

* no-mistakes(lint): Clean composer safety lint

* no-mistakes: apply CI fixes

* feat(watcher): add paused external-wait supervision (#421)

* feat(watcher): add paused/awaiting-external crew state

A crew (or firstmate steering it) can declare a deliberate wait on a known
external dependency with a paused: <reason> status. Both the always-on watcher
and the away-mode daemon absorb such an idle pane through shared fm-classify-lib.sh
vocabulary instead of tripping the possible-wedge stale escalation, and re-surface
it for a recheck only on a long bounded cadence (FM_PAUSE_RESURFACE_SECS) so a
forgotten pause cannot rot invisibly. fm-crew-state.sh reports state: paused
distinctly. A crew that goes idle without declaring a pause classifies exactly as
before. Docs and brief scaffold state lists updated; tests colocated.

* no-mistakes(review): Captain: fix paused-state transitions

* init

* no-mistakes(review): Captain: fix paused-state supervision transitions

* no-mistakes(review): Captain: fix paused supervision handoffs

* no-mistakes(review): Reconcile paused supervision markers

* no-mistakes(review): Captain: prioritize paused states over captain relevance

* no-mistakes(review): Captain: preserve paused-working wedge timer

* no-mistakes(review): Captain: honor configured pause verb in briefs

* no-mistakes(test): Captain: fix AFK paused watcher handoff

* no-mistakes(document): Document declared external waits

* no-mistakes(lint): Clean paused-state lint

---------

Co-authored-by: fmtest <fmtest@example.invalid>

* fix: preserve X-mode follow-up platform limits (#425)

* fix(x-mode): make follow-up platform splitting immune to link ordering

A ~470-char Discord follow-up posted as a (1/2)(2/2) thread split at ~280
chars because fm-x-link only learned the platform from the inbox payload,
and the fmx-respond ack path can drain that inbox file before the task is
linked. A link recorded after cleanup silently lost the platform and the
splitter defaulted to the X 280-char budget.

Make platform resolution ordering-proof:

- fm-x-link now resolves the platform AUTHORITATIVELY by request_id via a
  new fmx_request_relay_context helper (POST /connector/request-context)
  when neither the inbox payload nor carry flags carry it. The request_id
  survives the inbox drain, so a post-cleanup link still learns the right
  split budget. Best-effort: no token/curl or a non-2xx relay degrades to
  the loud warning below rather than a silent X default.
- fm-x-link warns loudly when no platform source resolves, so the loss is
  never silent.
- The fmx-respond procedure now orders link-before-inbox-cleanup so the
  fast local path stays correct without a relay round-trip.

Colocated regression tests: a Discord follow-up >280 <2000 posts as ONE
message even when linked after inbox cleanup, and an unresolvable platform
warns loudly instead of splitting silently. docs/configuration.md documents
the request-context lookup.

The relay endpoint is the companion durable change (see done status); until
it ships, the link-before-cleanup reorder keeps the normal path correct.

* no-mistakes(document): Document X-mode platform recovery

* fix(composer): handle ANSI ghost text safely (#429)

* fix(composer): one ANSI-aware ghost owner covers claude dim + grok truecolor

Away-mode injection wedged all night on the primary claude-on-herdr pane:
the herdr composer classifier never stripped generic dim ghost text (only a
narrow codex bold-wrapped byte-pattern check), so claude's rotating
prompt-suggestion ghost - a bare "❯" then SGR-2 dim text, which herdr's ANSI
pane read preserves - read as real pending input and every escalation deferred
(6524 lifetime "pending input (non-empty composer)" defers; wedge 30623s).

Consolidate ghost extraction into one fleet-wide ANSI-aware owner,
fm_composer_strip_ghost (bin/fm-composer-lib.sh), that drops every
de-emphasised run - dim/faint (SGR 2: claude, codex) AND a dark/muted truecolor
foreground (grok's placeholder, luminance below FM_COMPOSER_GHOST_LUMA_MAX,
default 128, dark-theme assumption). Both ANSI-capable backends route through
it: fm_tmux_composer_state (fm_tmux_strip_ghost is now a thin adapter) and
fm_backend_herdr_composer_state. The herdr-only faint byte-pattern check is
removed and fm_backend_herdr_strip_ansi reduced to a thin adapter over the
shared fm_composer_strip_ansi. Bordered detection now reads the plain row so a
dark box border dropped with the ghost does not lose the composer shape.

This also closes the documented grok TRUECOLOR placeholder gap by the same
mechanism (harness-adapters skill note updated).

Empirical evidence (read-only live capture + isolated tmux, no herdr lifecycle)
and the incident write-up are in docs/herdr-backend.md; deterministic
regressions feed the exact captured bytes through the real classifiers
(tests/fm-backend-herdr.test.sh, tests/fm-composer-ghost.test.sh). Two prior
ghost-test fixtures that used a near-black 38;2;1;2;3 as "real" colored text
(never a realistic real-input color) are corrected to a bright 38;2;224;222;244,
preserving the truecolor payload-skip parser intent.

* no-mistakes(review): Preserve dark shell prompt safety

* no-mistakes(review): Harden erased shell prompt classification

* no-mistakes(document): Document shared composer ghost extraction

* no-mistakes(lint): Normalize tmux comment punctuation

* fix(spawn): make tmux window handling robust under non-default config (#134)

* test: isolate session-start suite from ambient harness markers (#432)

* fix(session-start): isolate harness env markers in suite runner

Neutralize CLAUDECODE, PI_CODING_AGENT, and GROK_AGENT in
run_session_start so ambient interactive shells cannot override the
suite's fake ps harness (local-vs-CI split on the pi supervision case).

* no-mistakes(document): Correct Pi marker documentation

* fix(teardown): retry transient index locks during worktree return (#435)

* fix(teardown): retry treehouse return on transient index.lock

Killed crew git ops can leave a short-lived worktree index.lock that
makes treehouse return fail. Retry on that error signature with a
bounded wait (env-overridable), never force-delete a live lock, and
only then fall back to the existing provably-stale cleanup path.

* no-mistakes(review): Harden teardown retry configuration

* no-mistakes(document): Document teardown index-lock retry behavior

* no-mistakes(lint): Fix empty shell variable assignments

* fix: complete brief help and consolidate documentation (#438)

* docs: de-feature the scripts.md and CONTRIBUTING test inventories

Slice 1 of the documentation redundancy cleanup wave (firstmate scope).

docs/scripts.md: every row is now one purpose clause; script headers
are the declared owner of behavior, flags, and contracts. Coverage
stays 61/61 scripts; bytes drop 19,922 -> 7,958.

CONTRIBUTING.md: the 54-row per-test inventory is gone; contributors
discover tests by listing tests/*.test.sh and reading each script's
own header, and gated tests print their own skip gates. The run
commands, symlink assertions, and watcher smoke line are unchanged.
Lines drop 135 -> 84 (18,797 -> 7,831 bytes).

Two facts that existed only as inventory rows moved into their
owners' headers first: fm-brief.sh's paused-vs-blocked scaffold
distinction and fm-session-start.sh's Pi extension-loaded check.

No instruction-surface or behavior change; AGENTS.md untouched.

* no-mistakes(review): Captain, fix brief help and Grok test discovery

* no-mistakes(review): Captain: document Grok lock-holder test coverage

* fix: detect Git and centralize backend configuration (#445)

* docs: consolidate universal backend contracts into configuration.md

Slice 2 of the documentation redundancy cleanup wave (firstmate scope).

docs/configuration.md is now the declared single owner of three
universal contracts, each with an explicit ownership sentence:
- the universal toolchain list (Toolchain), now also carrying the
  per-tool purpose clauses that previously lived only in the tmux guide;
- the task-selector vocabulary (Runtime backend);
- the tasks-axi compatibility definition (Backlog backend).

The five backend guides' prerequisites replace their verbatim
universal-requirements parentheticals (5 full copies) with a pointer
plus only backend-specific items; zellij/cmux selector restatements
and architecture.md's partial copy become pointers or are dropped;
CONTRIBUTING's compatibility sentence becomes a pointer; two
near-verbatim orca-bootstrap restatements (configuration.md Runtime
backend, orca guide) collapse into the Toolchain owner copy.

Backend-specific setup, behavior, target-string shapes, and every
empirical verification record are untouched. AGENTS.md untouched
(slice 3).

* docs: include git and GitHub auth in the toolchain owner list

The review flagged that the new universal-toolchain owner omitted git
and GitHub authentication while every backend guide now defers its
prerequisites here; bootstrap's NEEDS_GH_AUTH check makes them real
universal requirements.

* no-mistakes(review): Detect Git in bootstrap toolchain

* no-mistakes(document): Clarify GitHub CLI and centralize selector documentation

* feat(daemon): add backend-independent wedge alerts (#444)

* feat(daemon): backend-independent active alert for the wedge alarm

When away-mode injection wedges past max-defer, inject_wedge_alarm only
actively signalled via the tmux status-line, which is skipped on non-tmux
backends. A wedged claude-on-herdr primary left only the passive
state/.subsuper-inject-wedged marker (2026-07-10 overnight incident).

Add a config-gated active alert (config/wedge-alarm, local/gitignored;
FM_WEDGE_ALARM_CHANNEL) that reaches the captain even when every pane and
its status-line is unreadable: an OS-level macOS notification (osascript),
a herdr notification, or a captain-supplied command. Default-on (auto) so
the alarm is never silent; each channel best-effort, degrading to the next
and never crashing the daemon loop. The tmux flash and durable marker stay.

The OS notifiers route through a single FM_WEDGE_ALARM_EXEC seam. When the
daemon is sourced (only tests do this; production execs it) the seam
defaults to "discard", and tests/wake-helpers.sh points it at a recorder,
so it is structurally impossible for any test to post a real notification.

Channels verified once manually on macOS 26.5.2 / herdr 0.7.3; see
docs/wedge-alarm.md.

* no-mistakes(review): Bound wedge alarm notifier execution

* no-mistakes(review): Captain: harden wedge alarm notifier safety

* no-mistakes(review): Captain: harden wedge alarm test notifier isolation

* no-mistakes(review): Captain: harden wedge alarm throttling

* no-mistakes(review): Redact wedge alarm directive logs

* no-mistakes(review): Harden wedge alarm notifier safety

* no-mistakes(review): Track notifier process groups through cleanup

* no-mistakes(document): Document wedge-alarm active alert behavior

* docs: centralize firstmate operating contracts (#447)

* docs(agents): extract conditional AGENTS.md material to owned homes

Slice 3 of the documentation redundancy cleanup wave (firstmate scope):
the always-loaded instruction surface drops from 941 lines / 116,733
bytes (~29k tokens per session per fleet member) to 785 / 91,353
(~22.8k tokens), moving only audit-identified conditional and
situational material while preserving every load-bearing invariant at
its trigger point via the inline-stub pattern.

Moves, each to one declared owner plus an inline stub:
- section 3's bootstrap output-line handbook (~44 lines) -> new
  agent-only bootstrap-diagnostics skill, added to the section 13
  trigger index; the detect-consent-install rule and the
  do-not-dispatch gate stay inline as safety-critical.
- section 4's crew-dispatch JSON schema and field semantics ->
  docs/configuration.md 'Crew dispatch profiles' (pointer direction
  flipped); the intake procedure, precedence, backstop, and
  never-select-unverified rules stay inline.
- section 4's quota-balanced algorithm -> bin/fm-dispatch-select.sh
  header (now the declared owner; usage() converted to the dynamic
  header extraction pattern PR #438 established for fm-brief.sh).
- section 7's spawn resolution narrative and example sprawl ->
  bin/fm-spawn.sh header; the isolated-worktree assertion, refusal-is-
  a-blocker rule, and post-spawn duties stay inline.
- section 7's teardown landed-work mechanics -> bin/fm-teardown.sh
  header (section 1's containment pointer retargeted); the fork benign
  case and never-force rule stay inline.
- section 8's watcher classification narrative -> docs/architecture.md
  'Event-driven supervision' (already the owner); every operative rule
  (one live cycle, no turn ends blind, drain first, wake ladder,
  never-pkill, guard responses) stays inline.
- sections 3/4/6/7 secondmate sync, propagation, schema, and handoff
  restatements -> secondmate-provisioning skill, now the declared
  owner including the literal-file inheritance nuance.
- section 14's X-mode cadence mechanism -> docs/configuration.md
  'X mode (.env)', closing issue #363; activation semantics, the
  fmx-respond trigger, and the terminal-wake final-follow-up duty
  stay inline.

CLAUDE.md stays a symlink; no behavior or test change.

* no-mistakes(document): Centralize contract-owner documentation

* fix(cmux): close last workspace during teardown (#449)

* fix(cmux): close the last/selected workspace in a window at teardown

cmux keeps every window at >=1 workspace, so close-workspace on the only
workspace in a window silently no-ops (returns OK, workspace stays), and a
window holding a live session cannot be closed over the control socket.
That left a selected task workspace open at teardown (the last workspace
in a window is always the selected one).

Add fm_backend_cmux_window_of_workspace and have fm_backend_cmux_kill
create a throwaway default sibling in the target's window before closing
when the target is the last workspace there, so the close lands; the
window keeps a fresh default workspace (cmux's own "closed the last tab"
outcome). Non-last teardown closes directly, as before.

Cover both kill branches plus the helper with fake-CLI unit tests, add a
real-cmux window/count detection smoke assertion, and record the
empirical evidence in docs/cmux-backend.md.

* no-mistakes(review): Derive cmux count from membership snapshot

* no-mistakes(document): Document cmux last-workspace teardown behavior

* fix: recover orphaned packed-refs locks during fleet sync (#453)

* fix(fleet-sync): recover from an orphaned packed-refs.lock

A git ref rewrite (fetch --prune, pack-refs, branch -D) killed after
creating .git/packed-refs.lock but before renaming it - e.g. bootstrap's
timed-out fleet-sync kill or teardown's process kills - leaves a lock that
makes the next sync's fetch fail with "Unable to create
'...packed-refs.lock': File ex…
vipentti pushed a commit to vipentti/firstmate that referenced this pull request Aug 5, 2026
* fix(afk): make the away-mode daemon backend-aware for herdr

bin/fm-supervise-daemon.sh discovered its supervisor pane and injected
via raw tmux calls only, so /afk failed outright on a herdr-based
fleet (TMUX_PANE unset, firstmate:0 fallback unresolvable).

Discovery now resolves backend (tmux|herdr) and target independently,
mirroring fm-backend.sh's own runtime auto-detection, with an explicit
FM_SUPERVISOR_BACKEND override alongside the existing FM_SUPERVISOR_TARGET.
zellij/orca refuse loudly at startup instead of misapplying tmux
primitives. Injection (pane-exists probe, busy-guard, composer-guard,
verified submit) now dispatches through bin/fm-backend.sh's generic
primitives, adding a new fm_backend_composer_state dispatcher; the
tmux path is byte-identical to before. Also fixes a pre-existing bug
in fm_backend_target_exists's herdr arm (missing --session, so it
silently misrouted once more than one herdr server was running) found
while verifying this end to end against a real isolated herdr session.

Classification, batching, max-defer, the marker contract, locks, and
wake-queue handling are unchanged - this is a transport-layer fix.

* no-mistakes(review): Corroborate Herdr idle busy state

* no-mistakes(review): Stabilize Herdr daemon startup wait

* no-mistakes(review): Captain, route cmux composer and update AFK docs

* no-mistakes(document): Document AFK supervisor backend support
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