feat(bin): require an explicit per-task delivery contract - #1563
Merged
Merged
Conversation
A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions.
…omotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode.
AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly.
The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper.
Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires.
kaku-san
added a commit
to kaku-san/firstmate
that referenced
this pull request
Aug 3, 2026
…d CI sharding (#2) * perf: shard portable serial tests across CI runners (kunchenguid#1544) * perf(ci): shard the portable serial behavior lane across runners The Behavior portable serial job ran all 69 scripts of the serial remainder on one runner. The measured serial sum on run 30725985757 was 1143762 ms (19m04s) against a 20-minute timeout, so the job intermittently reached the cap and was cancelled with every step passing. Setup is only about 7s, so the cost is entirely test wall time. Split the lane into four separate-runner shards. Each shard is still strictly serial, and separate runners mean no two of these stateful scripts ever share a machine, so the split needs no concurrency isolation proof. Assignment is longest-processing-time bin packing over measured per-script duration hints, balancing every shard to 285941 ms (~4m46s) of expected work, and the timeout tightens from 20 to 15 minutes. bin/fm-test-run.sh owns the shard count and refuses a lane whose "ofN" disagrees with it, while ci.yml derives the same count from strategy.job-total rather than a literal, so changing it in either file alone fails the lane loudly instead of leaving part of the required suite unrun. --check-coverage additionally proves the shards are non-empty, disjoint, and exactly equal to the serial lane. No test is weakened, skipped, or removed. Also replace the wall-clock sleeps in the --jobs scheduler test fixture with an explicit signal handshake between the fixtures. The old 0.5s-versus-0.05s race failed on a loaded machine; the handshake passes under sustained CPU saturation. * no-mistakes(review): Correct portable serial shard balance evidence * no-mistakes(document): Document portable serial shard evidence accurately * fix(bin): correct session lock and attached watcher supervision (kunchenguid#1545) * fix(bin): identify harness sessions by path and report delivered wakes Two supervision faults, both reported by a contributor and both open on the default branch. Fault 1: the Stop auto-arm never claims the home. fm_harness_ancestry_pid() matched only the basename of `ps -o comm=`, and Claude Code's native installer names the per-session executable by its version (.../share/claude/versions/ 2.1.220), so that basename identifies nothing. Three real failure shapes follow: a version-named session is missed entirely and the hook exits 0 with the epoch never written (unconditional on Linux, where procps reports the kernel exec name and ignores argv[0]); a claude-named daemon that directly parents sessions wins the outermost-contiguous-claude rule ahead of the session itself; and a session that is both version-named and daemon-parented has its live lock reclaimed as stale and rewritten to the shared daemon pid, corrupting the home's ownership record. Harness identity now also reads whole components of the executable path and of argv[0], which is what both platforms still carry. Matching whole components only keeps that widening safe: bin/fm-claude-stop-autoarm.sh and ~/.claude/hooks scripts have no "claude" component. Ownership is then decided against the session's whole contiguous harness ancestry rather than one chosen pid, which is the honest form of the question the library already documents ("does the current process descend from that same harness?"). That subsumes the outermost-pid rule for Claude's nested bg-spare worker chain instead of reverting it, and lets a daemon-parented session recognize its own lock. Lock acquisition still writes the outermost pid of the run, the only pid that lives as long as the session. Fault 2: an attached arm reports a delivered cycle as FAILED. The watcher prints its one reason line to its own stdout, so only the arm that forked it can read that line; an arm that attached observes nothing but a released lock and called a completely successful cycle "cycle ended without an actionable reason". No supervision event was lost - the durable queue held it - but every harness protocol reads that line as "supervision is down" and directs a manual re-arm. The arm now resolves an unobservable close against the durable wake queue, which records every wake before the watcher prints it and whose sequence counter never rewinds, not even across a drain. A cycle the queue proves delivered a wake reports that wake and exits 0; a cycle whose records a handling turn already drained reports the delivery without inventing a reason line; only a cycle that delivered nothing is still the typed nonzero failure. Fixing it in the arm covers codex, opencode, pi, grok and kimi, not just the Claude Stop path. Regressions: tests/fm-session-lock-ancestry.test.sh pins both platforms' ps semantics behind a deterministic process table and runs the real Stop auto-arm in version-named, daemon-parented, and combined real process trees, each orphaned so the walk cannot escape the fixture. tests/fm-watch-arm.test.sh drives a real watcher and a real attached arm through a real wake. Every fault case fails on the previous code. * no-mistakes(review): Bind watcher delivery records to process identity * no-mistakes(review): Return validated watcher identity atomically * no-mistakes(review): Track watcher successors by PID and identity * no-mistakes(document): Consolidate watcher arm-cycle documentation ownership * fix(bin): harden Claude supervision auto-arm recovery (kunchenguid#1495) * fix(supervision): harden Claude auto-arm failure handling * no-mistakes(review): Guarantee automatic retry after Claude auto-arm failures * no-mistakes(review): Gate attended fail-open on verified supervision failure * no-mistakes(document): Document Claude auto-arm retry and guard scope * no-mistakes: apply CI fixes * fix(supervision): make Claude fail-open progression monotonic * no-mistakes(review): Preserve auto-arm failure episodes until verified watcher recovery * no-mistakes(review): Linearize auto-arm failure progression across existing locks * no-mistakes(review): Linearize positive recovery across shared failure episode lock * no-mistakes(review): Scope Claude recovery contention to Claude guard mode * no-mistakes(document): Align supervision auto-arm documentation * no-mistakes(review): Preserve actionable wakes despite healthy successors * no-mistakes(document): Refresh supervision auto-arm documentation * feat(bin): require an explicit per-task delivery contract (kunchenguid#1563) * feat(bin): require an explicit ship delivery mode in fm-brief A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions. * feat(bin): require an explicit ship delivery contract at spawn and promotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode. * docs: record the explicit per-task delivery contract AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly. * test: pass ship delivery flags per call site in the Herdr launcher e2e The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper. * test: pass the ship delivery contract in the secondmate suites Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires. * no-mistakes(document): correct per-task delivery mode and CI shard docs --------- Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com> Co-authored-by: Your Name <you@example.com>
JohnEdwardNorton
added a commit
to ignidus/firstmate
that referenced
this pull request
Aug 3, 2026
Pulls in: - fix(bin): correct session lock and attached watcher supervision (kunchenguid#1545) - fix(bin): harden Claude supervision auto-arm recovery (kunchenguid#1495) - fix(bin): retire terminal process events and surface queued wakes (kunchenguid#1500) - feat(bin): require an explicit per-task delivery contract (kunchenguid#1563) - perf: shard portable serial tests across CI runners (kunchenguid#1544)
allstargg
added a commit
to allstargg/firstmate
that referenced
this pull request
Aug 3, 2026
* fix(herdr): place workers in the launching workspace (#1328)
* fix(herdr): place workers in the launching agent's exact workspace
Herdr enforces no workspace-label uniqueness, and spawn resolved its
container by taking the FIRST workspace whose label matched the home
label. With two workspaces both labeled "firstmate", a worker launched
from the second one was created in the first, so it appeared in a
different space than the Firstmate the captain was watching.
Reproduced end to end on Herdr 0.7.5 protocol 17 by running the real
bin/fm-spawn.sh inside a launcher pane in the second "firstmate"
workspace: the worker landed in w1 while its launcher was in w2, with an
unrelated third workspace focused throughout, which also rules out any
dependence on the focused workspace.
Placement now binds to the launching process's own Herdr identity. Herdr
injects HERDR_PANE_ID, HERDR_SESSION, and HERDR_SOCKET_PATH into every
process it manages a pane for, and fm_backend_herdr_launcher_identity
resolves that pane's current owning tab and workspace live from Herdr,
cross-checking the pane against its tab and confirming the workspace
exists exactly once in the session. The injected HERDR_TAB_ID and
HERDR_WORKSPACE_ID are creation-time snapshots and are deliberately not
read as current identity. Labels are no longer placement authority.
A claimed parent identity that is unreadable, contradictory, stale, or
from another named session or Herdr server stops the spawn before any
worker endpoint exists, rather than degrading to a label search. A
launcher with no Herdr ancestry has no workspace to inherit and keeps
the per-home labeled container, which must now resolve to exactly one
workspace; two same-labeled candidates refuse instead of adopting
either. A --secondmate launch keeps standing up that home's own
workspace by design.
With presentation spaces enabled, the projected child is created and
bound under that same exact parent and anchors its ordering on it, so a
duplicated home label no longer makes the layout ambiguous. Projection,
focus restoration, restart binding, and quarantine rules are unchanged,
and children are never collapsed into the parent. tmux, Zellij, cmux,
Orca, and the away-mode daemon terminal were each inspected and are not
affected: none resolves a container by searching mutable labels.
tests/fm-backend-herdr-launcher-workspace-e2e.test.sh drives the real
spawn and teardown against an isolated Herdr lab, with its headline case
running fm-spawn.sh inside a real Herdr pane so the identity comes from
Herdr's own injection. The refusal matrix and the ordering anchor are
covered deterministically in tests/fm-backend-herdr.test.sh.
Eight existing real-Herdr suites inherited the developer terminal's own
Herdr pane into their isolated lab sessions, which the new cross-session
check correctly refuses. tests/herdr-test-safety.sh now owns
herdr_forget_inherited_pane and those suites call it, so what they assert
no longer depends on where they were launched from.
Two unrelated fixes found along the way. tests/fm-secondmate-harness.test.sh
had the same class of environment leak through CLAUDECODE, which outranks
PI_CODING_AGENT in bin/fm-harness.sh and made its pi-signed ancestry case
resolve "claude" whenever the suite ran inside Claude Code. And
fm-spawn.sh's usage() printed a fixed line range that had already been
truncating its own help mid-sentence.
* no-mistakes(review): Enforce exact Herdr launcher and projection identity
* no-mistakes(document): Document exact Herdr launcher workspace placement
* fix(calm): refine Calm working boat animation (#1339)
* feat(calm): replace Pi's working row with an animated ship while Calm is on
While Calm is active and one logical agent run is under way, Calm now hides
Pi's built-in working row and renders a small two-row SSHHIP-derived boat in
its place. When Calm is off, Pi's stock working row is left untouched.
The presentation uses only public Pi extension API: setWorkingVisible(false)
plus a temporary setWidget() component whose render(width) owns the responsive
geometry and whose timer requests a TUI render. Visibility follows agent_start
through agent_settled, so the boat does not flicker between tool calls,
automatic continuations, retries, or compaction inside the same run, and
settle, abort, and failure all reach the same cleanup.
fm-calm.ts stays the sole owner of the presentation choice and the only caller
of setWorkingVisible(); the new lib owns the sprite geometry and widget.
* no-mistakes(review): Guarded Calm-off lifecycle visibility writes; focused tests pass
* no-mistakes(test): Fixed Calm E2E wait to include tmux scrollback
* no-mistakes(document): Document Calm working boat behavior
* no-mistakes: apply CI fixes
* feat(calm): slow the Calm boat, animate blue water, and make the sail directional
The boat now moves one column every 880ms while a bounded fixed-cell water phase
advances every 220ms, so the water ripples several times between boat steps and
the presentation reads as calm. One scheduler drives both clocks and disposing
the widget stops them together; ticks rather than wall-clock timestamps drive
every state change, so tests seek animation time exactly.
Colors are standard ANSI foreground codes instead of theme lookups: blue for
every water cell and yellow for the complete boat, each run closed with a
default-foreground reset so nothing bleeds into padding or later frames. ANSI
bytes never enter geometry, so visible width stays exact.
The mainsail is directional and trails aft of the mast: <| travelling right and
|> travelling left. Direction reverses the moment the boat lands on an endpoint,
so the endpoint frame already shows the new heading and no frame at or after a
bounce shows the previous sail.
* test(calm): wait for the Ctrl+O expansion redraw this block asserts
* docs(calm): record the revised working-presentation verification evidence
* no-mistakes(document): Fix Calm feasibility document EOF whitespace
* fix(dispatch): preflight candidate auth before quota escalation (#1349)
* fix(dispatch): scope candidate authentication to its own surface
A locally expired timestamp in one credential store was reported to the
captain as a sign-out, including for dispatch candidates that never read
that store. A `harness=pi, model=xai/grok-*` candidate authenticates
through Pi's own xAI credential, but the only Grok quota reading
available was gated on the standalone Grok CLI's separate token, whose
expiry clock drifts independently. The always-loaded intake rule then
turned that unreadable quota into a mandatory captain escalation.
Add `bin/fm-auth-preflight.sh` as the deterministic owner of the parts
that must not depend on agent memory: it resolves a tuple's
authentication surface from quota-axi's own emitted auth sources rather
than from a harness or model name, so another harness's CLI can never
gate a candidate that does not use it. A vendor CLI is launched only
when the tuple's own harness owns the credential store under test and a
non-destructive discovery command is registered for it, which today is
`grok models` alone. That probe runs at most once with stdin closed and
a hard timeout, reads its verdict from the first stdout line because the
command exits 0 either way, treats unrecognized output as indeterminate,
and never invokes login, logout, or the interactive TUI. Quota is read
at most twice, and unknown headroom never makes a candidate ineligible
on its own.
Update the dispatch procedure to match: usable authentication with
unmeasurable headroom stays eligible at lower preference with the
unknown disclosed, and stop-and-report is reserved for unresolved
authentication, an unresolved relationship, or malformed configuration.
Record that Grok's `credits.remaining` is a prepaid balance rather than
window headroom.
Gate quota-axi at 0.1.16 in bootstrap, the first build reporting
per-credential auth sources. A stale install previously passed the
presence check silently, which is why a fix published two days earlier
was still not in effect.
Replace the orphaned quota-array-dispatch fixtures, which encoded a
`provider: "xai"` shape the tool never emits and had no consumer, with
fixtures shaped like real 0.1.16 output that the new suite drives the
script against. The suite asserts the verdict and, separately, which
vendor CLIs were launched, so a Pi/xAI candidate reaching the Grok CLI
fails. Map `tests/fixtures/<dir>` to its consuming suite so a fixture
change selects the right tests instead of refusing.
* refactor(bootstrap): give the quota-axi floor one owner
The floor was stated twice - once in bootstrap's gate and once inline in
the auth preflight - so bumping it needed two edits that could drift.
Move it to bin/fm-quota-axi-lib.sh alongside its rationale, matching the
existing tasks-axi library, and derive the comparison from the constant
so the number appears exactly once. Bootstrap turns a failing check into
the operator diagnostic; the preflight refuses to emit an unscoped
verdict. Map the new library to both consuming suites so a bump re-runs
them, and record that any usable source means the surface authenticates.
* no-mistakes(review): Captain: bound quota checks and removed Python dependency
* no-mistakes(review): Captain: enforce conservative headroom and exact preflight retry
* no-mistakes(review): Captain: preserve OpenCode eligibility without auth-surface guessing
* no-mistakes(review): Captain: reject malformed OpenCode model relationships
* no-mistakes(review): Captain: exempt verified unmodeled tuples from intake escalation
* no-mistakes(document): Updated dispatch authentication documentation
* no-mistakes: apply CI fixes
* feat(x-mode): reconcile promised public replies deterministically (#1350)
* feat(x-mode): reconcile promised public replies deterministically
A promised final reply in an X or Discord thread was only kept while the
primary remembered it. Compaction or restart erased that memory, so a typed
public-followup obligation could sit at pending-work after its PR merged and
the original thread never got its reply.
Make the promise durable state instead:
- bin/fm-public-followup-emit.sh reports a typed terminal work result (source
home, work id, generation, outcome, safe deliverables, bounded public-safe
text) into the owning home's private inbox. The event id is derived from
that identity tuple, so duplicate reports and restart replay converge with
no coordination, and nothing ever parses a free-form done: sentence.
- bin/fm-public-followup.sh registers a commitment, reconciles events through
tasks-axi public-followup, and runs the idempotent delivery sequence
(begin-delivery with the payload hash, post, record the posted receipt or a
typed error) against the stored platform and opaque thread binding. A
delivery interrupted between post and receipt refuses rather than risk a
second public reply.
- Session start surfaces unresolved commitments from disk, the existing relay
poll surfaces a new terminal-result set once, and teardown refuses while
this home still owes a public reply for that exact work.
tasks-axi public-followup remains the only owner of the obligation state
machine, state/x-context/ the only owner of the private request context, and
fm-x-reply.sh the only thing that posts. Its new optional --receipt-file is
the one addition there, so a caller can record how many messages were sent.
A home that never opted into the myfirstmate relay gates out on a single
[ -f "$FM_HOME/.env" ] test: no tasks-axi call, no backlog or context scan,
no output, and no artifact. Evidence in docs/verification/public-followup.md.
* no-mistakes(review): Hardened public-followup reconciliation and ownership guards
* no-mistakes(review): Hardened typed terminal cleanup and receipt reconciliation
* no-mistakes(review): Automated typed-delivery cleanup and strict backlog validation
* no-mistakes(review): Fail-closed parent resolution and registration-safe delivery
* no-mistakes(review): Harden relay gating and validate secondmate bindings
* no-mistakes(review): Use owner-aware single-gate teardown protection
* no-mistakes(document): Correct public-followup documentation drift
* no-mistakes(lint): Quote done literals to fix ShellCheck warnings
* no-mistakes: apply CI fixes
* feat(bin): replace busy heuristics with semantic lifecycle state (#1327)
* feat: add semantic busy-state contract owner and event writer
One owner (bin/fm-busy-lib.sh) for the captain-approved semantic
busy-state redesign: a per-task gen-bound record written only by
bin/fm-busy-event.sh, per-harness trusted-source classification with
explicit source attribution, busy/idle/unknown/dead semantics where
missing, malformed, stale, or untrusted semantic data is unknown -
never idle - and endpoint death is the only process-level override.
The Grok-only rendered-tail fallback and the standalone-Kimi
verification gate live behind the same classifier.
* feat: arm busy-state at spawn and convert Pi to the semantic extension path
fm-spawn arms the busy-state contract for converted adapters and seeds
busy/fm-spawn (the launch brief is a submitted turn). The Pi/pi-signed
per-task extension now reports agent_start -> busy and agent_settled ->
idle confirmed by ctx.isIdle(), covering auto-retries, compaction
retries, tool loops, and queued continuations, while turn_end stays a
wake notification touch. Teardown removes the new record, gen sidecar,
and lock. Live-verified on Pi 0.82.0: seed -> agent-start busy ->
agent-settled idle with the marker still touched.
* feat: convert OpenCode to the semantic session.status plugin path
The per-task plugin (renamed .opencode/plugins/fm-busy-state.js) now
classifies from OpenCode's semantic session.status events - busy and
retry are active, idle is inactive - latched to the worker's own
session so a subagent child session can never clear the worker's busy
state. The session.idle marker touch stays a wake notification.
Teardown removes both the new and the legacy plugin filenames.
Live-verified on OpenCode 1.17.18 in a real TUI pane: seed ->
session-busy -> session-status-idle.
* feat: convert Claude to the full lifecycle hooks path
The per-task settings.local.json now wires UserPromptSubmit -> busy
and Stop, StopFailure, and SessionEnd -> idle, so API-error and
shutdown turn ends can never strand a busy record; Stop keeps the
turn-ended notification touch. A refused (stale-gen) event exits 0 and
stays silent so Claude's own lifecycle is never broken. Live-verified
on Claude Code 2.1.220: UserPromptSubmit fires for the argv launch
prompt, Stop closes each turn, a mid-stream Escape interrupt fires no
closing hook, and the firstmate-controlled idle/fm-interrupt clear
resolves it.
* feat: gate Codex busy state behind verified semantic sources
The approved contract prefers Codex's app-server turn lifecycle with
capability negotiation and sanctions its lifecycle hooks as the
intermediate. Live probes on codex-cli 0.145.0 show neither is usable
for a pane worker: the app-server daemon is unreachable for a TUI
thread and refuses to start outside the managed standalone install,
and firstmate-written project hooks never fired (interactive with
directory trust granted, and exec, both with
--dangerously-bypass-hook-trust) while global hooks fired in the same
runs. Codex therefore classifies unknown codex-unverified behind an
explicit probe rather than falling back to idle or footer text, and
fm-spawn installs no unverified Codex wiring.
* feat: gate standalone Kimi busy state on live verification
Standalone Kimi has no installed binary here, so per the approved
contract its semantic path stays guarded and it classifies unknown
kimi-unverified rather than idle - and never from its locale-sensitive
moon-phase spinner, which the redesign forbids inventing as a state
source. The gate records the preferred source order (Wire prompt
request lifetime, which brackets a turn and reports cancellation, then
the documented hooks including Interrupt because Stop does not fire on
interrupts) and the exact evidence required to open it. Arming without
wiring would seed a busy record nothing could clear, so both land
together behind the same gate.
* feat: route busy consumers through the contract and drop the global OR
The watcher, crew-state reader, and away-mode daemon now decide busy
state through bin/fm-busy-lib.sh: only an exact busy verdict counts as
working, and unknown never becomes working or a silent idle, so a crew
whose semantic state is missing, malformed, stale, or unverified
surfaces instead of being absorbed. Crew-state reports the producing
source in its detail. The watcher's global OR regex default is gone;
Grok keeps its isolated fallback inside the contract. The daemon's
supervisor-pane reader stays rendered-text - that pane is not a
recorded task - but is now scoped to firstmate's own detected harness
instead of every vendor signature. Secondmate pending-reply
observation is deliberately unchanged and documented as a
delivery-confirmation signal, not task state.
* docs: point busy-state documentation at the single contract owner
Adds a maintainer-architecture section naming bin/fm-busy-lib.sh as
the owner of what busy means, with per-adapter sources, the
unknown-never-idle rule, the endpoint-death override, and the two
rendered-text readers that deliberately stay outside the contract.
Replaces the stale regex-first prose in architecture, tmux-backend,
herdr-backend, and configuration; converts the harness-adapters
per-harness rows from UI signatures to the semantic source each
harness uses; and records the live verification evidence, including
why Codex and standalone Kimi stay unknown.
* fix: arm away-launch signal handlers before acquiring the lifecycle lock
fm_afk_launch_main acquired its lock and only then installed the EXIT,
INT, and TERM traps. A signal arriving in that window terminated the
process by default action and left the lock directory behind, which
blocks the next away-mode launch until the stale-owner reclaim path
clears it. The release helper only removes a lock this process owns,
so the handlers are now armed first. The accompanying test also killed
the child whether or not the lock had appeared and sampled cleanup the
instant wait returned; it now requires the lock, then allows a bounded
settle, so it proves the guarantee instead of racing it.
* test: align fleet, Kimi, lifecycle, and detection suites with the contract
The fleet snapshot and wake-daemon lifecycle fixtures now prove a
working crew through its own semantic busy-state record instead of
rendered pane text, which is what those consumers read. The Kimi
watcher test asserts the approved contract directly: a standalone Kimi
task classifies unknown rather than matching its moon-phase spinner,
while Grok's isolated fallback still classifies only Grok. The
pi-signed detection cases clear ambient harness markers, fixing a
pre-existing failure where the running session's own CLAUDECODE
outranked the fixture's marker.
* fix: stop teardown from deleting a project's own Codex hooks file
An intermediate revision wired Codex through a firstmate-written
<worktree>/.codex/hooks.json, and teardown removed it alongside the
other generated wiring. The Codex wiring was dropped when its probes
came back unverified, so that removal now targets a file firstmate
never creates - and a project may legitimately track its own
.codex/hooks.json, which teardown would then delete from a pooled
worktree.
* fix: keep busy-record parsing from disturbing its sourcing caller
The record parser split fields with set -- under a temporary noglob,
which clobbers a sourcing caller's positional parameters and restores
glob expansion even when the caller had disabled it. The watcher, the
daemon, and the crew-state reader all source this library, so it now
reads fields with read -a, which never globs and never touches caller
state.
* docs: state exactly which Claude hook paths were reproduced live
The busy-state record listed all four wired Claude hooks in the source
column, which could read as a claim that every one fired during the
pass. UserPromptSubmit and Stop did; StopFailure and SessionEnd are
wired from hook names confirmed present in the installed binary, but
the abnormal turn ends they cover were not reproduced.
* test: let reset_fakes own the crew-state busy-text fixture lifecycle
The Grok fallback case set FM_FAKE_BUSY_TEXT and cleared it inline, so
the variable's lifetime was owned by one test rather than by the
shared reset that every other fake already uses.
* no-mistakes(review): Fix semantic busy-state lifecycle races
* no-mistakes(review): Make busy-state retirement idempotent
* no-mistakes(review): Enforce semantic state boundaries for status and injection
* no-mistakes(review): Restore harness-scoped away-mode busy guard
* no-mistakes(document): Refresh semantic busy-state documentation
* no-mistakes: apply CI fixes
* fix: preserve Calm boat continuity across working periods (#1356)
* fix(calm): resume working boat from frozen column across runs
Keep one extension-owned boat animation for the Pi session so settling
freezes column and direction, the next working period resumes there
without hidden-time jumps, and only a fresh session resets to the left edge.
* no-mistakes(review): Freeze Calm boat from last rendered state
* no-mistakes(document): Document Calm boat continuity contract
* fix: restore evidence-based dispatch eligibility (#1358)
* fix(dispatch): judge candidate provider relations instead of rejecting them
Firstmate deterministically dropped supported Pi candidates in the
openai-codex family. bin/fm-auth-preflight.sh resolved a harness=pi tuple's
credential surface by constructing the source id `pi:<model-prefix>`, so
`pi + openai-codex/gpt-5.6-terra` looked for a `pi:openai-codex` source. That
source does not exist, because Pi's Codex family authenticates through the
Codex store quota-axi already lists as `auth-json`/`cli-rpc`. The tuple
returned `eligible=no reason=surface-unresolved` while the Pi catalog listed
the model and the Codex provider reported fresh, usable credentials with 64
effective percent remaining on its all-model scope.
The prefix construction was only ever valid where Pi holds its own credential
(`pi:xai`, `pi:kimi-coding`), which is why every previously configured Pi tuple
resolved and the defect stayed hidden until a Codex-family Pi model was
configured.
Retire dispatch eligibility from deterministic shell. The dispatching first
mate now establishes model support and provider family from each harness's
authoritative catalog, applies quota at the granularity the vendor supplies,
and shows that reasoning. Provider-level and all-model evidence bounds every
model established in that family; a named-model window bounds only its own
model. Missing model-level quota, a missing auth source, unmeasurable headroom,
and unmodeled authentication are disclosed uncertainty. Only concrete
contradictory evidence blocks a candidate.
Replace the preflight with bin/fm-vendor-auth-probe.sh, which keeps the
captain's approved bounded probe envelope without any routing knowledge: it
takes no harness, model, or provider, reads no quota, renders no verdict, and
holds only a fixed-argv safety allowlist. Its behavior suite proves the absent
identity surface, the untouched quota, the uniform exit status, the fixed argv
with stdin closed, and a real bound even when the configured bound is zero.
Also fixed along the way: a zero FM_*_TIMEOUT silently removed the hard bound,
the pinned Grok version had drifted to 0.2.117, and --changed selection refused
outright on any deleted bin/ script.
AGENTS.md section 4 and quota-array-dispatch own the corrected policy,
harness-adapters gets the catalog-responsibility correction, and
docs/verification/dispatch-auth.md records the 2026-07-30 evidence on
Pi 0.82.0, quota-axi 0.1.16, and grok 0.2.117.
* no-mistakes(review): Reject all-zero vendor probe timeouts
* docs: define captain instruction precedence (#1362)
* docs: add captain-authorized inherent red-check merge exception
Keep the default red-PR ban and own one always-loaded exception in the
merge-authority section: captain-explicit PR or bounded batch plus exact
check, only when the failure is inherent to the selected delivery path.
Yolo cannot activate it; final head and the full current check suite must
be verified; other substantive failures remain non-waivable.
* docs: replace narrow red-check exception with captain precedence
Supersede the inherent failing-check merge exception with one always-loaded
Firstmate-local rule: a current explicit concrete captain instruction
overrides a conflicting Firstmate-written standing rule only within exact
scope, never above platform/system/developer instructions. Keep the ordinary
red-PR default and yolo boundary; point section 7 at the section 1 owner.
* docs: define validation supersession sequence (#1407)
* fix: give validation-time captain overrides a supersession sequence
The Validate section let a captain instruction that completely
invalidates the work being validated keep the same task and worker, but
never said how: the adjacent rule flatly bans hand-editing, committing,
aborting, or restarting during an active run with no carve-out, so a
worker facing full invalidation had no sanctioned path forward.
Add the missing sequence: cancel through no-mistakes axi's abort
command, confirm the run has stopped through axi status, recover branch
ownership through axi sync's guarded recovery, only then replace the
obsolete work, and validate once against the final head. The existing
ban on hand-editing an active run now cross-references this sequence
instead of contradicting it.
* no-mistakes(review): Make validation custody recovery conditional
* no-mistakes(document): Clarify validation supersession abort exception
* fix: keep obsolete pipeline commits out of the superseded deliverable
The review-applied fix made custody recovery conditional on
branch_sync.next_action.code, but left an open gap: recovering custody
settles who owns the branch, not what content ships. As written, a
worker could recover an obsolete run's branch and build the
replacement on top of its now-irrelevant commits instead of from the
correct pre-invalidation base, carrying obsolete content into the
final deliverable.
Make that explicit: custody recovery settles ownership, not content,
so the worker replaces obsolete work from the correct base and keeps
the obsolete run's commits out of what gets validated and shipped.
* no-mistakes(test): Restore minimal pre-invalidation replacement instruction
* fix: dedupe redundant "replace the obsolete work" restatement
Line 309 already says the worker replaces the obsolete work from the
correct pre-invalidation base, excluding the obsolete commits. The
closing sentence restated "replace the obsolete work" again before
gating the final validation run, layering the same fact twice instead
of stating it once.
Trim the closing sentence to just the ownership gate and the
single-run-against-final-head requirement it uniquely adds.
* fix: bind backend overrides to exact-task authority (#1413)
* fix: bind explicit --backend to exact-task authority
A Herdr-backed second mate carried a prior one-task --backend tmux
exception forward by analogy, so its child landed in tmux and never
appeared under the second mate in Herdr. Runtime detection was correct;
the authority surface was not.
docs/configuration.md now owns that an explicit --backend is authorized
only for that exact task. AGENTS.md and fm-spawn help point there.
* no-mistakes(document): Consolidate backend selection authorization documentation
* fix(herdr): prevent focus flashes during projected workspace cleanup (#1229)
* fix: remove projected workspaces through Herdr's focus-preserving pane-death path
Herdr 0.7.5's explicit close of a workspace-emptying last pane moves the
attached client's focus to a neighbor workspace, flashing the captain's
whole window and routing in-flight keystrokes to the wrong pane until
Firstmate's exact-tab restore masks it 56-197 ms later.
Teardown and cleanup now plan a workspace-emptying close as a focus-safe
removal: verify the close empties the workspace, reposition the doomed
workspace behind the focused one through the verified workspace.move
transport when it sits before a non-last focused workspace, prove the pane
holds one lone idle shell, and end that shell so Herdr removes the emptied
workspace through its focus-preserving pane-death path. Any ambiguity or
failure falls back to the plain close behind the existing restore backstop,
and fm_backend_herdr_kill applies the same plan for non-projected removals.
Two conditions proven on real hardware are encoded in the adapter: BSD ps
reports a login shell's comm as "-zsh", and an idle shell transiently
hosts a prompt helper right after a workspace.move relayout, absorbed by a
bounded strict-sample settle window in the idle-shell proof, now the single
owner shared with session-start cleanup.
An isolated-lab regression reproduces the raw steal on 0.7.5 and proves the
plan removes a doomed workspace with zero wrong-focus samples and no
corrective focus; unit fixtures cover the position, edge, ambiguity, move
and kill failure, escalation, and transient-helper cases. Upstream fixes
(#1877 explicit close, #1912 pane death) are merged but unreleased; once
released the plan degrades to a harmless reorder-then-remove.
* no-mistakes(review): Confirm pane death from structured not-found responses
* no-mistakes(review): Serialize Herdr kills and sample focus continuously
* no-mistakes(review): Synchronize Herdr focus evidence output
* no-mistakes(review): Refuse unlocked Herdr pane closes
* no-mistakes(document): Correct Herdr focus-safety documentation
* no-mistakes: apply CI fixes
* fix: never erase a Herdr task's records while its pane survives a refused close
A transient presentation-lock contention could produce a completed teardown
while the exact Herdr pane stayed alive as an unowned restored shell: the
kill refused the unlocked close (correctly), returned success, the warning
was suppressed, and cleanup erased the task's status, turn-end, and
metadata records after the isolated copy had already been returned.
Teardown now acquires the named-session presentation lock before anything
destructive: a contended lock refuses up front while the isolated copy, the
task branch, every durable record, and the endpoint are all intact for a
plain rerun, and the projected and flat close paths both run under that one
held lock instead of acquiring their own. Durable records are erased only
once the exact pane is confirmed gone through its structured presence; a
refused, skipped, or failed close retains every record with a visible,
retryable error, and after a skipped close (unresolvable lock path) only a
structured pane_not_found counts as gone - unknown never does.
The teardown regression drives a live contending lock holder end to end:
the refusal touches nothing (no worktree return, no branch drop, no close
attempt), and the retry after release returns the copy, closes the pane
under the lock, and removes the records. The unconfirmed projected close
now refuses with records retained, and the structured-presence gate has a
strict/default unit matrix.
* no-mistakes(review): Require structured pane-not-found before Herdr record removal
* no-mistakes(document): Correct Herdr record-retention verification date
* fix: refuse ambiguity, revalidate SIGKILL ownership, and roll back failed removals
Three accepted-contract corrections from the post-CI personal review of the
Herdr keep-spaces focus-flash mitigation.
Ambiguous endpoint identity no longer counts as a confirmed-gone pane: a
missing or malformed target refuses record removal in the structured
presence gate, and teardown treats missing confirmation machinery as a
refusal instead of skipping the gate, so only an exact structured
pane_not_found ever erases durable task records.
The pane-death SIGKILL escalation re-reads the exact pane's process
information and refuses to signal unless the same shell pid still passes
the strict bare-idle ownership proof, so a pid that exited and was reused
by an unrelated process is never signaled; the refused escalation falls
back to the plain close with the unrelated process untouched.
A reposition whose removal is not confirmed no longer outlives the attempt:
the emptying-close plan records the verified pre-move order and original
index whenever it invokes the mover, and both close owners restore the
exact original workspace order through a second verified move, under the
same held session lock, before reporting the close as failed.
Each defect was reproduced first: the unit matrix documented malformed
identity as gone, the PID-reuse regression showed SIGKILL reaching a
disowned pid, and the rollback regression showed a single unrestored move.
Teardown-level regressions cover unparseable presence retention alongside
the strict identity matrix.
* no-mistakes(review): Require confirmed Herdr removal and resolvable teardown locks
* no-mistakes(review): Enforce structured Herdr closes and teardown preflight
* no-mistakes(review): Preflight explicit Herdr close confirmation helper
* no-mistakes(document): Document Herdr rollback failure semantics
* no-mistakes(review): Captain, harden recursive Herdr teardown safety
* no-mistakes(document): Document recursive Herdr teardown evidence
* fix: retain nested secondmate home when a recursive child cleanup fails
Captain-decided Option A correction for nm-askuser-flash-r6, found during
complete-diff rereview of the merged head.
cleanup_firstmate_home_children's recursive secondmate branch called
itself for a nested child's home without checking the result, then
unconditionally removed that home right after. remove_firstmate_home
ends in an unconditional recursive delete with no check for leftover
records, so a nested secondmate whose own Herdr grandchild failed its
confirmed-gone check would have its entire home - retained grandchild
records included - erased by the very next line.
Guard the recursive call the same way every other fallible call in this
function already is: || return 1, skipping remove_firstmate_home and
leaving the nested home and its records for a safe rerun.
Empirically, fm-teardown.sh's set -eu already halted the script on the
prior unguarded call before reaching removal (verified by hand with the
guard reverted, under both this session's bash and stock macOS bash
3.2) - the reachable behavior was already correct. The explicit guard
is still applied exactly as decided: it matches every sibling call site
in the function, and it stops the correctness of this path depending on
errexit's well-known fragility under refactors (a wrapping if/&&, or a
future subshell) rather than on an explicit check.
Adds a teardown-level regression building on the existing direct-child
Herdr fixtures: a top-level secondmate contains a nested secondmate,
whose own Herdr child's close goes unconfirmed. Proves through the
public fm-teardown.sh interface that the nested home, the nested
secondmate's own record, and the grandchild's metadata and status all
survive, and that the top-level secondmate's record survives too.
* no-mistakes(document): Document nested Herdr teardown retention
* fix: prioritize completion runway in quota-aware dispatch (#1431)
* fix(dispatch): prioritize quota completion runway
* no-mistakes(document): Document completion-aware quota runway selection
* fix(bin): preserve full task contract in no-mistakes intent (#1447)
* Preserve task contract in no-mistakes intent
* no-mistakes(review): Preserve complete current task contract in no-mistakes intent
* fix(bin): parse punctuated secondmate registry entries safely (#1452)
* fix: centralize secondmate registry parsing
* no-mistakes(review): Centralize secondmate registry binding validation
* no-mistakes(review): Harden registry EOF and symlink validation
* no-mistakes(review): Reject unreadable registries before parsing
* no-mistakes(document): Document punctuation-safe secondmate registry validation
* no-mistakes: apply CI fixes
* feat(bin): add durable process-event supervision (#1483)
* feat(procevent): supervise long-polling sources into durable events
Firstmate had no way to wait on a blocking external process without holding
a conversational turn. Add a domain-neutral process-to-event runner plus a
thin adapter around the currently published `lavish-axi poll` interface:
canonical physical source identity, one machine-wide owner per source, direct
argv execution, and durable 0600 result capture before any event referencing
it is published on the existing wake queue. No second notifier, no polling
control plane, and no retry machinery.
A captured result with no durable handled acknowledgement stays eligible for
bounded re-announcement across any number of drains and restarts. Draining a
wake before acting on it and then starting a replacement session resurfaces
the same exact source and sequence, and never puts result payload text in an
event line. `fm-procevent.sh handled <source-id> <sequence>` is the only thing
that stops re-announcement: generation-keyed, private, path-safe, durable, and
atomically idempotent, so a paired external effect gated on its first-time
versus repeat report is never authorized twice.
An acknowledgement is refused unless matching captured result and adapter
records already exist, so a premature or mistyped call cannot suppress a
future result.
The source side is unchanged and still lossy: the published poll clears
feedback destructively before returning it, so a result lost in that window
is unrecoverable. This is never at-least-once, no-loss, or lossless, and the
handled acknowledgement is not a generic exactly-once effect either - a crash
between an external effect and its acknowledgement can still repeat that
effect on replay.
Integrate registered sources with watcher supervision, the guards, and
recoverable secondmate teardown across nested homes, and cover source
identity, lifecycle races, supervision, restart handling, and cleanup safety
with regressions.
* no-mistakes(review): Prevent Lavish prompt text from spoofing missing sessions
* no-mistakes(review): Serialize publication and secure handled acknowledgements
* no-mistakes(document): Document hardened process-event acknowledgement guarantees
* fix(procevent): never reclaim a source whose owned group still runs
A runner is its own process group leader and starts the blocking source in
that group, but the claim records only the leader PID and its identity. If the
leader died while the source child kept running, the missing PID was
classified stale: reconciliation released the claim and started a second
runner while the old blocking source was still consuming the same canonical
source. For the Lavish adapter that means two destructive long polls racing on
one review session, so it is not harmless process litter. It also contradicted
the documented promise that ownership is never released until the whole group
is gone.
Ownership state now distinguishes a generation that is really gone from one
whose leader crashed with its group still alive. Reconcile stops that
surviving group and releases its exact generation before starting any
replacement, and keeps the claim for a later cycle when it cannot prove the
group stopped or another home owns it. Acquisition and `start` treat the same
state as held rather than reclaimable.
Signalling that group is safe precisely because only an absent leader reaches
this state. A reused PID leaves the leader alive, so the identity comparison
still classifies it stale or uncertain and no group signal follows, which
keeps the existing PID-reuse refusal intact.
Add a public-interface regression for the exact crash cut - SIGKILL only the
leader, prove the child group survives, reconcile, and prove the old group is
gone with no second source running - plus its counterexample that a generation
with no leader and no surviving group is still reclaimed. Update the runner
help, operating documentation, skill, and verification record where they
described reclaim in terms of the leader alone.
* no-mistakes(review): Enforce runner group ownership and detect poller overlap
* no-mistakes(review): Isolate runner groups from unrelated caller processes
* no-mistakes(document): Document isolated process-event runner launch
* no-mistakes(lint): Suppress Perl literal ShellCheck false positive
* fix(bin): retire terminal process events and surface queued wakes (#1500)
* fix(bin): deliver process-event results and retire ended sources
Two defects reproduced during a real Lavish adapter session.
One human `Send & End` produced four captured results: the real feedback,
then recurring empty ended sessions. The generic runner had no way to learn
a source was finished, so every reconcile restarted a poll that returned
immediately. The runner now asks the source's own adapter -
`fm-procevent-<adapter>.sh terminal <result-file>` - and on exit 0 alone
re-proves ownership, drops the registration, and releases its own claim
under one source boundary. Terminal knowledge stays adapter-owned: for
Lavish that is an ended session, a missing session, and the final feedback
delivery the published poll marks with `session_ended`. An adapter with no
terminal command keeps its source armed exactly as before. Capture before
publication, captured-result durability, queued wake durability, bounded
re-announcement, handled deduplication, one-owner ownership, and explicit
idempotent retirement are all unchanged.
A captured result queued its `check` wake durably, but a healthy watcher
with a fresh beacon never delivered it; the result surfaced only after a
manual drain. Publication happens outside the watcher (in the runner) or
unconditionally (in reconcile), so the watcher had no newly actionable
signal to report and never reached its rewake path. It now reports a
queued-but-unsurfaced process-event record through the same actionable exit
every other wake uses, deduplicated by the same `.seen-*` marker discipline
the signal scan uses, so the record is always durable before it is
suppressed. The durable queue remains the authority and no second notifier,
poller, timer, queue, or adapter-specific wake path is added.
Regressions cover both, driven end to end: an armed Lavish source against a
stand-in for the published poll polls once, captures once, publishes one
distinct event, and retires itself; two fixture adapters prove the terminal
decision follows the adapter alone; and a real capture plus a real watcher
prove one proactive wake before any drain, with no duplicate wake while the
record stays queued or after it is acknowledged.
* no-mistakes(review): Harden process-event retirement and proactive delivery
* no-mistakes(review): Route process-event delivery through shared wake owner
* no-mistakes(document): Clarify process-event delivery and retirement documentation
* no-mistakes(lint): Fix ShellCheck control-flow warnings
* no-mistakes(lint): Fix wake output status lint warning
* perf: shard portable serial tests across CI runners (#1544)
* perf(ci): shard the portable serial behavior lane across runners
The Behavior portable serial job ran all 69 scripts of the serial
remainder on one runner. The measured serial sum on run 30725985757 was
1143762 ms (19m04s) against a 20-minute timeout, so the job intermittently
reached the cap and was cancelled with every step passing. Setup is only
about 7s, so the cost is entirely test wall time.
Split the lane into four separate-runner shards. Each shard is still
strictly serial, and separate runners mean no two of these stateful
scripts ever share a machine, so the split needs no concurrency isolation
proof. Assignment is longest-processing-time bin packing over measured
per-script duration hints, balancing every shard to 285941 ms (~4m46s) of
expected work, and the timeout tightens from 20 to 15 minutes.
bin/fm-test-run.sh owns the shard count and refuses a lane whose "ofN"
disagrees with it, while ci.yml derives the same count from
strategy.job-total rather than a literal, so changing it in either file
alone fails the lane loudly instead of leaving part of the required suite
unrun. --check-coverage additionally proves the shards are non-empty,
disjoint, and exactly equal to the serial lane. No test is weakened,
skipped, or removed.
Also replace the wall-clock sleeps in the --jobs scheduler test fixture
with an explicit signal handshake between the fixtures. The old
0.5s-versus-0.05s race failed on a loaded machine; the handshake passes
under sustained CPU saturation.
* no-mistakes(review): Correct portable serial shard balance evidence
* no-mistakes(document): Document portable serial shard evidence accurately
* fix(bin): correct session lock and attached watcher supervision (#1545)
* fix(bin): identify harness sessions by path and report delivered wakes
Two supervision faults, both reported by a contributor and both open on the
default branch.
Fault 1: the Stop auto-arm never claims the home. fm_harness_ancestry_pid()
matched only the basename of `ps -o comm=`, and Claude Code's native installer
names the per-session executable by its version (.../share/claude/versions/
2.1.220), so that basename identifies nothing. Three real failure shapes follow:
a version-named session is missed entirely and the hook exits 0 with the epoch
never written (unconditional on Linux, where procps reports the kernel exec name
and ignores argv[0]); a claude-named daemon that directly parents sessions wins
the outermost-contiguous-claude rule ahead of the session itself; and a session
that is both version-named and daemon-parented has its live lock reclaimed as
stale and rewritten to the shared daemon pid, corrupting the home's ownership
record.
Harness identity now also reads whole components of the executable path and of
argv[0], which is what both platforms still carry. Matching whole components
only keeps that widening safe: bin/fm-claude-stop-autoarm.sh and ~/.claude/hooks
scripts have no "claude" component. Ownership is then decided against the
session's whole contiguous harness ancestry rather than one chosen pid, which is
the honest form of the question the library already documents ("does the current
process descend from that same harness?"). That subsumes the outermost-pid rule
for Claude's nested bg-spare worker chain instead of reverting it, and lets a
daemon-parented session recognize its own lock. Lock acquisition still writes the
outermost pid of the run, the only pid that lives as long as the session.
Fault 2: an attached arm reports a delivered cycle as FAILED. The watcher prints
its one reason line to its own stdout, so only the arm that forked it can read
that line; an arm that attached observes nothing but a released lock and called a
completely successful cycle "cycle ended without an actionable reason". No
supervision event was lost - the durable queue held it - but every harness
protocol reads that line as "supervision is down" and directs a manual re-arm.
The arm now resolves an unobservable close against the durable wake queue, which
records every wake before the watcher prints it and whose sequence counter never
rewinds, not even across a drain. A cycle the queue proves delivered a wake
reports that wake and exits 0; a cycle whose records a handling turn already
drained reports the delivery without inventing a reason line; only a cycle that
delivered nothing is still the typed nonzero failure. Fixing it in the arm covers
codex, opencode, pi, grok and kimi, not just the Claude Stop path.
Regressions: tests/fm-session-lock-ancestry.test.sh pins both platforms' ps
semantics behind a deterministic process table and runs the real Stop auto-arm in
version-named, daemon-parented, and combined real process trees, each orphaned so
the walk cannot escape the fixture. tests/fm-watch-arm.test.sh drives a real
watcher and a real attached arm through a real wake. Every fault case fails on
the previous code.
* no-mistakes(review): Bind watcher delivery records to process identity
* no-mistakes(review): Return validated watcher identity atomically
* no-mistakes(review): Track watcher successors by PID and identity
* no-mistakes(document): Consolidate watcher arm-cycle documentation ownership
* fix(bin): harden Claude supervision auto-arm recovery (#1495)
* fix(supervision): harden Claude auto-arm failure handling
* no-mistakes(review): Guarantee automatic retry after Claude auto-arm failures
* no-mistakes(review): Gate attended fail-open on verified supervision failure
* no-mistakes(document): Document Claude auto-arm retry and guard scope
* no-mistakes: apply CI fixes
* fix(supervision): make Claude fail-open progression monotonic
* no-mistakes(review): Preserve auto-arm failure episodes until verified watcher recovery
* no-mistakes(review): Linearize auto-arm failure progression across existing locks
* no-mistakes(review): Linearize positive recovery across shared failure episode lock
* no-mistakes(review): Scope Claude recovery contention to Claude guard mode
* no-mistakes(document): Align supervision auto-arm documentation
* no-mistakes(review): Preserve actionable wakes despite healthy successors
* no-mistakes(document): Refresh supervision auto-arm documentation
* feat(bin): require an explicit per-task delivery contract (#1563)
* feat(bin): require an explicit ship delivery mode in fm-brief
A ship brief's definition of done was shaped by a silent per-project registry
lookup, so an adjusted brief and the task's recorded delivery could disagree and
no one had to decide anything per task.
fm-brief now requires --mode on ship scaffolds, validates it against the closed
set, refuses the conditional no-mistakes-prod-only registry policy as a task
mode, and records the choice as a fixed machine-readable "Delivery contract:
mode=<mode>" line that fm-spawn can check. --mode is refused on scout and
secondmate scaffolds, and --yolo is refused outright because the worker never
owns approval decisions.
* feat(bin): require an explicit ship delivery contract at spawn and promotion
fm-spawn resolved every ship and scout task's mode and yolo from the project
registry, so the delivery posture was never a per-task decision and could
contradict the brief the worker was about to follow.
fm-spawn now requires --mode and --yolo on ship spawns, validates both against
their closed sets, and reads the brief's recorded delivery contract line and
refuses a mismatch before any endpoint exists; a brief scaffolded before that
line existed warns once and launches on the flag. A batch carries one shared
contract that each pair still checks against its own brief. Scout and secondmate
spawns refuse the flags, and a scout now records no mode or yolo at all, which
teardown and the snapshot already tolerate. When the explicit mode carries less
rigor than the project's standing posture, a deviation notice is printed and the
spawn continues, so the registry stays advisory rather than an enforced default.
fm-promote requires the same two flags, because a scout carries no posture to
inherit, and writes them into the task record with the kind flip.
fm-project-mode keeps its one registry parser for the mechanical consumers that
have no task in hand, accepts the conditional no-mistakes-prod-only annotation
and maps it to its most rigorous leg for them, and grows --raw so the deviation
notice can tell a conditional policy apart from a flat mode.
* docs: record the explicit per-task delivery contract
AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at
intake, including the surface classification for a no-mistakes-prod-only project
and the unregistered-project fallback, and the project-management skill defines
that conditional policy as a registration-time posture with its defaults and
initialization consequences. The registry blurb, script table, and architecture
section follow: the registry records the captain's standing posture, and task
delivery is decided per task and passed explicitly.
* test: pass ship delivery flags per call site in the Herdr launcher e2e
The shared spawn helper also launches a secondmate, which refuses the flags, so
the contract belongs at each ship call site rather than inside the helper.
* test: pass the ship delivery contract in the secondmate suites
Both suites scaffold or spawn an ordinary ship task as the control case for a
secondmate assertion, so each needs the explicit contract the ship path now
requires.
* feat(spawn): propagate a native W3C traceparent to spawned agents
Add a default-off capability that resolves one W3C traceparent for a task,
injects it into the agent's pane shell as the TRACEPARENT environment
variable immediately before launch, and records the identical value as
traceparent= in state/<id>.meta, so an external observer that explicitly
reads that env value or meta field can correlate a worker, a Secondmate, and
their nested children into one trace with no collector, storage, UI, or
vendor coupling.
TRACEPARENT as an environment variable is a firstmate convention carrying a
W3C-formatted value: W3C Trace Context standardizes the header, not an env
var, and OpenTelemetry SDKs do not read it automatically, so a downstream
must consume it deliberately; this feature parents no SDK span by itself.
Identity is per task, not per spawn: the carrier is minted with random ids on
the first spawn, adopted as a child (fresh span, same trace) for a nested
spawn whose parent already holds one, and reused verbatim from the meta on
relaunch, so a task keeps one stable logical identity across restarts. A
malformed or all-zero inherited value is treated as absent and roots a fresh
trace. A new root is sampled (01) - a sampling decision a downstream
parent-based sampler honors, not a guarantee that any collector stores a
span, and firstmate emits no spans; a child preserves the inherited flag.
Trust boundary: a firstmate-minted root is random and reads no prompt, path,
task prose, credential, or arbitrary environment key. An inherited
TRACEPARENT is opaque caller-controlled data - up to 24 bytes of id passed
through after syntax validation - so whoever set it controls those bytes, a
bounded fixed-width channel rather than a general content or secret channel.
The feature adds no OTEL_* variable, no tracestate, and no arbitrary
environment injection; it runs no configurable or arbitrary command, only the
fixed local od and tr (resolved from PATH) to read a few bytes of entropy - a
small local pipeline with no network or watchdog and no hard latency
guarantee. Any entropy or validation failure that returns omits the carrier
without aborting the spawn. A default-off spawn leaves the generated meta and
launch environment unchanged.
Enablement is default-off (config/trace-context, or FM_TRACE_CONTEXT where a
non-empty value overrides and unset or empty defers to the file) and is
propagated into secondmate homes, taking effect at each agent's next launch:
a Secondmate launched or relaunched after enablement carries the primary
trace into its nested workers, while an already-running Secondmate roots new
traces for its own workers until relaunched. Injection reuses the existing
GOTMPDIR channel, so all spawn backends and harnesses and the ship, scout,
and secondmate paths are covered.
Covered by a pure-library suite and a spawn-path integration test (fake tmux
plus a real worktree, hermetic against ambient FM_TRACE_CONTEXT) proving the
recorded and injected carriers are identical and sent before launch, that
default-off writes and injects neither, that a relaunch reuses the recorded
carrier, and that an explicit FM_TRACE_CONTEXT overrides the file both ways;
plus a source-owner inheritance test proving trace-context propagates and
absence-mirrors through propagate_inheritable_config.
Documentation follows the repository documentation-audiences contract:
docs/trace-context.md is maintainer-architecture rationale, the configuration
schema lives in docs/configuration.md, and the repeatable test evidence is
separated into docs/verification/trace-context.md (maintainer-verification),
registered in docs/documentation-audiences.json.
* fix(spawn): propagate the effective trace-context decision to secondmates
FM_TRACE_CONTEXT overrode trace context only in the process that read it. A
newly launched secondmate decided enablement from the inherited
config/trace-context file alone, so the override did not cross the
primary-to-secondmate boundary: FM_TRACE_CONTEXT=off with the file present left
the secondmate's nested workers traced (a broken kill switch), and
FM_TRACE_CONTEXT=on with the file absent left them untraced despite the
inherited carrier.
Deliver the primary's effective decision to a newly launched secondmate as a
normalized on/off FM_TRACE_CONTEXT in the launch prefix, so a FM_TRACE_CONTEXT
override governs the nested primary -> secondmate -> worker chain both ways, not
just the copied file. The value is bounded to the literal on/off and does not
broaden environment injection; the already-running secondmate boundary is
unchanged.
Add a genuine two-level spawn regression that drives fm-spawn twice with the
exact environment the primary injects into the secondmate and proves both
divergent directions end to end. Correct the documentation that implied
secondmate coverage on every backend, since orca and cmux reject secondmate
spawns, and refresh the verification evidence for the new assertion count.
* no-mistakes(review): Clarify Secondmate trace-context launch snapshots
* no-mistakes(document): Correct trace-context documentation ownership and relaunch semantics
* fix(spawn): resolve the trace-context decision once for carrier and snapshot
The effective trace-context decision was read twice per spawn: once inside
fm_trace_context_resolve for the recorded carrier, and again for the secondmate
FM_TRACE_CONTEXT launch snapshot. A config-file change between the two reads
could pair a carrier with the opposite enable state - an injected carrier with
an off snapshot, or no carrier with an on snapshot.
Freeze the effective on/off decision once, drive the carrier resolution under
that frozen FM_TRACE_CONTEXT so it cannot independently re-read the file, and
reuse the same frozen decision for the secondmate launch snapshot. Add a
spawn-path regression that drives the file-decided path and proves the recorded
carrier and the delivered snapshot always agree, and refresh the verification
evidence for the new assertion count.
* no-mistakes(review): Preserve legacy Secondmate trace boundary
* no-mistakes(document): Correct trace-context verification comparison base
* no-mistakes(review): Captain, prevent failed trace delivery metadata claims
* no-mistakes(review): Captain, align trace-context tests and verification evidence
* no-mistakes(document): Correct trace-context verification evidence
* no-mistakes(lint): Suppress intentional ShellCheck literal-dollar warnings
* no-mistakes(review): Captain: freeze trace context at session start
* no-mistakes(test): Captain: stabilize scheduler test and document Kimi trace coverage
* no-mistakes(document): Document trace-context safety boundaries
* fix(trace): fail off on stale session snapshots
Publish each home session decision atomically through a same-directory temporary file and bind it to the current session lock. A replacement failure can no longer leave an earlier on decision active in a later session; missing, stale, malformed, or unpublishable state defaults safely to off.
Add regressions for read-only replacement and failed publication, update spawn and session-start fixtures for the lock-bound format, and refresh the architecture and verification records.
* no-mistakes(review): Fix trace spawn failure independence and duplicate safety
* no-mistakes(document): Refresh trace-context documentation and verification
* no-mistakes(review): Clear partial backend input after failed trace submission
* no-mistakes(review): Stop unsafe trace delivery before launch append
* no-mistakes(document): Document unsafe trace delivery handling
* fix(trace): bound each trace to one routed task, never the routing agent
A persistent Secondmate holds its launch-time TRACEPARENT in the process
environment for its whole life, and routed requests never replace it, so
resolving new-task carriers from the ambient environment chained every
routed task into one ever-growing trace per Secondmate with distinct
parent ids. Resolve now reuses the task's recorded carrier or mints a
fresh sampled root, never reading ambient TRACEPARENT, so each routed
task is its own trace boundary while relaunch, recovery, and
scout-to-ship promotion keep one stable per-task identity.
The spawn regression models the reviewed scenario exactly: two unrelated
tasks spawned sequentially through one persistent Secondmate environment
record and inject distinct trace ids, adopt nothing from the Secondmate's
carrier, and a relaunch of the first task reuses its original carrier
verbatim.
* docs(trace): define the per-task trace boundary
The design contract is one task per trace: a persistent Secondmate is
routing infrastructure with its own agent identity, never a shared trace
root for the unrelated tasks routed through it. Root/recovery semantics
replace the removed child-inheritance path, the sampling and safety
sections drop inherited-carrier language because ambient TRACEPARENT is
never read, and the verification page records the refreshed suite
inventories including the two-task Secondmate boundary regression.
* test(trace): adopt the explicit per-task delivery contract in spawn fixtures
Rebasing onto current main brings the explicit per-task delivery contract:
ship spawns now require --mode and --yolo instead of resolving them from the
project registry. The trace spawn fixtures pass the same explicit contract
canonical spawn tests use, preserving the per-task trace boundary coverage
unchanged, and the verification page records the refreshed comparison base.
---------
Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
3 tasks
nbost130
pushed a commit
to nbost130/firstmate
that referenced
this pull request
Aug 5, 2026
…d#1563) * feat(bin): require an explicit ship delivery mode in fm-brief A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions. * feat(bin): require an explicit ship delivery contract at spawn and promotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode. * docs: record the explicit per-task delivery contract AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly. * test: pass ship delivery flags per call site in the Herdr launcher e2e The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper. * test: pass the ship delivery contract in the secondmate suites Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires.
withally
added a commit
to withally/firstmate
that referenced
this pull request
Aug 5, 2026
…fork (#13) * perf: shard portable serial tests across CI runners (kunchenguid#1544) * perf(ci): shard the portable serial behavior lane across runners The Behavior portable serial job ran all 69 scripts of the serial remainder on one runner. The measured serial sum on run 30725985757 was 1143762 ms (19m04s) against a 20-minute timeout, so the job intermittently reached the cap and was cancelled with every step passing. Setup is only about 7s, so the cost is entirely test wall time. Split the lane into four separate-runner shards. Each shard is still strictly serial, and separate runners mean no two of these stateful scripts ever share a machine, so the split needs no concurrency isolation proof. Assignment is longest-processing-time bin packing over measured per-script duration hints, balancing every shard to 285941 ms (~4m46s) of expected work, and the timeout tightens from 20 to 15 minutes. bin/fm-test-run.sh owns the shard count and refuses a lane whose "ofN" disagrees with it, while ci.yml derives the same count from strategy.job-total rather than a literal, so changing it in either file alone fails the lane loudly instead of leaving part of the required suite unrun. --check-coverage additionally proves the shards are non-empty, disjoint, and exactly equal to the serial lane. No test is weakened, skipped, or removed. Also replace the wall-clock sleeps in the --jobs scheduler test fixture with an explicit signal handshake between the fixtures. The old 0.5s-versus-0.05s race failed on a loaded machine; the handshake passes under sustained CPU saturation. * no-mistakes(review): Correct portable serial shard balance evidence * no-mistakes(document): Document portable serial shard evidence accurately * fix(bin): correct session lock and attached watcher supervision (kunchenguid#1545) * fix(bin): identify harness sessions by path and report delivered wakes Two supervision faults, both reported by a contributor and both open on the default branch. Fault 1: the Stop auto-arm never claims the home. fm_harness_ancestry_pid() matched only the basename of `ps -o comm=`, and Claude Code's native installer names the per-session executable by its version (.../share/claude/versions/ 2.1.220), so that basename identifies nothing. Three real failure shapes follow: a version-named session is missed entirely and the hook exits 0 with the epoch never written (unconditional on Linux, where procps reports the kernel exec name and ignores argv[0]); a claude-named daemon that directly parents sessions wins the outermost-contiguous-claude rule ahead of the session itself; and a session that is both version-named and daemon-parented has its live lock reclaimed as stale and rewritten to the shared daemon pid, corrupting the home's ownership record. Harness identity now also reads whole components of the executable path and of argv[0], which is what both platforms still carry. Matching whole components only keeps that widening safe: bin/fm-claude-stop-autoarm.sh and ~/.claude/hooks scripts have no "claude" component. Ownership is then decided against the session's whole contiguous harness ancestry rather than one chosen pid, which is the honest form of the question the library already documents ("does the current process descend from that same harness?"). That subsumes the outermost-pid rule for Claude's nested bg-spare worker chain instead of reverting it, and lets a daemon-parented session recognize its own lock. Lock acquisition still writes the outermost pid of the run, the only pid that lives as long as the session. Fault 2: an attached arm reports a delivered cycle as FAILED. The watcher prints its one reason line to its own stdout, so only the arm that forked it can read that line; an arm that attached observes nothing but a released lock and called a completely successful cycle "cycle ended without an actionable reason". No supervision event was lost - the durable queue held it - but every harness protocol reads that line as "supervision is down" and directs a manual re-arm. The arm now resolves an unobservable close against the durable wake queue, which records every wake before the watcher prints it and whose sequence counter never rewinds, not even across a drain. A cycle the queue proves delivered a wake reports that wake and exits 0; a cycle whose records a handling turn already drained reports the delivery without inventing a reason line; only a cycle that delivered nothing is still the typed nonzero failure. Fixing it in the arm covers codex, opencode, pi, grok and kimi, not just the Claude Stop path. Regressions: tests/fm-session-lock-ancestry.test.sh pins both platforms' ps semantics behind a deterministic process table and runs the real Stop auto-arm in version-named, daemon-parented, and combined real process trees, each orphaned so the walk cannot escape the fixture. tests/fm-watch-arm.test.sh drives a real watcher and a real attached arm through a real wake. Every fault case fails on the previous code. * no-mistakes(review): Bind watcher delivery records to process identity * no-mistakes(review): Return validated watcher identity atomically * no-mistakes(review): Track watcher successors by PID and identity * no-mistakes(document): Consolidate watcher arm-cycle documentation ownership * fix(bin): harden Claude supervision auto-arm recovery (kunchenguid#1495) * fix(supervision): harden Claude auto-arm failure handling * no-mistakes(review): Guarantee automatic retry after Claude auto-arm failures * no-mistakes(review): Gate attended fail-open on verified supervision failure * no-mistakes(document): Document Claude auto-arm retry and guard scope * no-mistakes: apply CI fixes * fix(supervision): make Claude fail-open progression monotonic * no-mistakes(review): Preserve auto-arm failure episodes until verified watcher recovery * no-mistakes(review): Linearize auto-arm failure progression across existing locks * no-mistakes(review): Linearize positive recovery across shared failure episode lock * no-mistakes(review): Scope Claude recovery contention to Claude guard mode * no-mistakes(document): Align supervision auto-arm documentation * no-mistakes(review): Preserve actionable wakes despite healthy successors * no-mistakes(document): Refresh supervision auto-arm documentation * feat(bin): require an explicit per-task delivery contract (kunchenguid#1563) * feat(bin): require an explicit ship delivery mode in fm-brief A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions. * feat(bin): require an explicit ship delivery contract at spawn and promotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode. * docs: record the explicit per-task delivery contract AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly. * test: pass ship delivery flags per call site in the Herdr launcher e2e The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper. * test: pass the ship delivery contract in the secondmate suites Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires. * feat(bin): support remote secondmate homes (kunchenguid#1576) * Add generic remote secondmate transport * Add routed remote secondmate replies * Add remote outbox backlog handoff * Integrate remote secondmate lifecycle * no-mistakes(review): Fix remote snapshot and handoff races * no-mistakes(review): Serialize remote home provisioning transactions * no-mistakes(review): Harden remote lifecycle transaction boundaries * no-mistakes(review): Serialize remote lifecycle mutations and fail closed * no-mistakes(review): Close remote lifecycle and file race windows * no-mistakes(review): Serialize remote reply retirement and inheritance * no-mistakes(review): Harden remote transfer integrity and recovery * no-mistakes(review): Serialize remote respawn with registry retirement * no-mistakes(document): Document remote bootstrap convergence accurately * no-mistakes(document): Clarify skipped remote secondmate mutations * no-mistakes(lint): Resolve remote script ShellCheck warnings * no-mistakes: apply CI fixes * feat: add per-task trace context propagation (kunchenguid#995) * feat(spawn): propagate a native W3C traceparent to spawned agents Add a default-off capability that resolves one W3C traceparent for a task, injects it into the agent's pane shell as the TRACEPARENT environment variable immediately before launch, and records the identical value as traceparent= in state/<id>.meta, so an external observer that explicitly reads that env value or meta field can correlate a worker, a Secondmate, and their nested children into one trace with no collector, storage, UI, or vendor coupling. TRACEPARENT as an environment variable is a firstmate convention carrying a W3C-formatted value: W3C Trace Context standardizes the header, not an env var, and OpenTelemetry SDKs do not read it automatically, so a downstream must consume it deliberately; this feature parents no SDK span by itself. Identity is per task, not per spawn: the carrier is minted with random ids on the first spawn, adopted as a child (fresh span, same trace) for a nested spawn whose parent already holds one, and reused verbatim from the meta on relaunch, so a task keeps one stable logical identity across restarts. A malformed or all-zero inherited value is treated as absent and roots a fresh trace. A new root is sampled (01) - a sampling decision a downstream parent-based sampler honors, not a guarantee that any collector stores a span, and firstmate emits no spans; a child preserves the inherited flag. Trust boundary: a firstmate-minted root is random and reads no prompt, path, task prose, credential, or arbitrary environment key. An inherited TRACEPARENT is opaque caller-controlled data - up to 24 bytes of id passed through after syntax validation - so whoever set it controls those bytes, a bounded fixed-width channel rather than a general content or secret channel. The feature adds no OTEL_* variable, no tracestate, and no arbitrary environment injection; it runs no configurable or arbitrary command, only the fixed local od and tr (resolved from PATH) to read a few bytes of entropy - a small local pipeline with no network or watchdog and no hard latency guarantee. Any entropy or validation failure that returns omits the carrier without aborting the spawn. A default-off spawn leaves the generated meta and launch environment unchanged. Enablement is default-off (config/trace-context, or FM_TRACE_CONTEXT where a non-empty value overrides and unset or empty defers to the file) and is propagated into secondmate homes, taking effect at each agent's next launch: a Secondmate launched or relaunched after enablement carries the primary trace into its nested workers, while an already-running Secondmate roots new traces for its own workers until relaunched. Injection reuses the existing GOTMPDIR channel, so all spawn backends and harnesses and the ship, scout, and secondmate paths are covered. Covered by a pure-library suite and a spawn-path integration test (fake tmux plus a real worktree, hermetic against ambient FM_TRACE_CONTEXT) proving the recorded and injected carriers are identical and sent before launch, that default-off writes and injects neither, that a relaunch reuses the recorded carrier, and that an explicit FM_TRACE_CONTEXT overrides the file both ways; plus a source-owner inheritance test proving trace-context propagates and absence-mirrors through propagate_inheritable_config. Documentation follows the repository documentation-audiences contract: docs/trace-context.md is maintainer-architecture rationale, the configuration schema lives in docs/configuration.md, and the repeatable test evidence is separated into docs/verification/trace-context.md (maintainer-verification), registered in docs/documentation-audiences.json. * fix(spawn): propagate the effective trace-context decision to secondmates FM_TRACE_CONTEXT overrode trace context only in the process that read it. A newly launched secondmate decided enablement from the inherited config/trace-context file alone, so the override did not cross the primary-to-secondmate boundary: FM_TRACE_CONTEXT=off with the file present left the secondmate's nested workers traced (a broken kill switch), and FM_TRACE_CONTEXT=on with the file absent left them untraced despite the inherited carrier. Deliver the primary's effective decision to a newly launched secondmate as a normalized on/off FM_TRACE_CONTEXT in the launch prefix, so a FM_TRACE_CONTEXT override governs the nested primary -> secondmate -> worker chain both ways, not just the copied file. The value is bounded to the literal on/off and does not broaden environment injection; the already-running secondmate boundary is unchanged. Add a genuine two-level spawn regression that drives fm-spawn twice with the exact environment the primary injects into the secondmate and proves both divergent directions end to end. Correct the documentation that implied secondmate coverage on every backend, since orca and cmux reject secondmate spawns, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Clarify Secondmate trace-context launch snapshots * no-mistakes(document): Correct trace-context documentation ownership and relaunch semantics * fix(spawn): resolve the trace-context decision once for carrier and snapshot The effective trace-context decision was read twice per spawn: once inside fm_trace_context_resolve for the recorded carrier, and again for the secondmate FM_TRACE_CONTEXT launch snapshot. A config-file change between the two reads could pair a carrier with the opposite enable state - an injected carrier with an off snapshot, or no carrier with an on snapshot. Freeze the effective on/off decision once, drive the carrier resolution under that frozen FM_TRACE_CONTEXT so it cannot independently re-read the file, and reuse the same frozen decision for the secondmate launch snapshot. Add a spawn-path regression that drives the file-decided path and proves the recorded carrier and the delivered snapshot always agree, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Preserve legacy Secondmate trace boundary * no-mistakes(document): Correct trace-context verification comparison base * no-mistakes(review): Captain, prevent failed trace delivery metadata claims * no-mistakes(review): Captain, align trace-context tests and verification evidence * no-mistakes(document): Correct trace-context verification evidence * no-mistakes(lint): Suppress intentional ShellCheck literal-dollar warnings * no-mistakes(review): Captain: freeze trace context at session start * no-mistakes(test): Captain: stabilize scheduler test and document Kimi trace coverage * no-mistakes(document): Document trace-context safety boundaries * fix(trace): fail off on stale session snapshots Publish each home session decision atomically through a same-directory temporary file and bind it to the current session lock. A replacement failure can no longer leave an earlier on decision active in a later session; missing, stale, malformed, or unpublishable state defaults safely to off. Add regressions for read-only replacement and failed publication, update spawn and session-start fixtures for the lock-bound format, and refresh the architecture and verification records. * no-mistakes(review): Fix trace spawn failure independence and duplicate safety * no-mistakes(document): Refresh trace-context documentation and verification * no-mistakes(review): Clear partial backend input after failed trace submission * no-mistakes(review): Stop unsafe trace delivery before launch append * no-mistakes(document): Document unsafe trace delivery handling * fix(trace): bound each trace to one routed task, never the routing agent A persistent Secondmate holds its launch-time TRACEPARENT in the process environment for its whole life, and routed requests never replace it, so resolving new-task carriers from the ambient environment chained every routed task into one ever-growing trace per Secondmate with distinct parent ids. Resolve now reuses the task's recorded carrier or mints a fresh sampled root, never reading ambient TRACEPARENT, so each routed task is its own trace boundary while relaunch, recovery, and scout-to-ship promotion keep one stable per-task identity. The spawn regression models the reviewed scenario exactly: two unrelated tasks spawned sequentially through one persistent Secondmate environment record and inject distinct trace ids, adopt nothing from the Secondmate's carrier, and a relaunch of the first task reuses its original carrier verbatim. * docs(trace): define the per-task trace boundary The design contract is one task per trace: a persistent Secondmate is routing infrastructure with its own agent identity, never a shared trace root for the unrelated tasks routed through it. Root/recovery semantics replace the removed child-inheritance path, the sampling and safety sections drop inherited-carrier language because ambient TRACEPARENT is never read, and the verification page records the refreshed suite inventories including the two-task Secondmate boundary regression. * test(trace): adopt the explicit per-task delivery contract in spawn fixtures Rebasing onto current main brings the explicit per-task delivery contract: ship spawns now require --mode and --yolo instead of resolving them from the project registry. The trace spawn fixtures pass the same explicit contract canonical spawn tests use, preserving the per-task trace boundary coverage unchanged, and the verification page records the refreshed comparison base. * fix(bin): harden tmux agent liveness across harnesses (kunchenguid#1577) * fix(bin): classify tmux agent liveness independent of process titles `fm_backend_tmux_agent_state` attributed a pane solely from `#{pane_current_command}`, which is a process TITLE a harness can rewrite, not a structural fact. Claude Code 2.1.220 reports its version string there, so a live Claude endpoint classified `ambiguous`: the session-start secondmate liveness sweep could no longer see it, and any consumer that gates on a positive classification refuses outright. Read a second, independent name source: the kernel `comm` of every process in the pane tty's foreground process group. Either source naming a verified harness yields `alive`, because a false `dead` is the one verdict that can start a duplicate agent on a live worktree. Scoping to the foreground process group rather than the pane's descendants keeps a harness-named background process from faking an agent, and covers multi-process launchers (the Pi Launcher path) without a special case. Verified on 2026-08-03 against all seven adapters running for real on tmux 3.6a / macOS 26.5.2 arm64: claude 2.1.220, codex-cli 0.146.0, opencode 1.18.11, pi 0.82.0, pi-signed 0.82.0, grok 0.2.118, kimi 0.31.1 all classify `alive`, each attributed by a source independent of its title. Two tests, because they fail for different reasons: - tests/fm-tmux-agent-liveness.test.sh pins the logic with real processes and no harness, so it runs everywhere CI runs tmux. It drives the two name sources apart on purpose and asserts the divergence, so no case can go quietly vacuous. - tests/fm-harness-liveness-drift-live-e2e.test.sh relaunches every installed harness and fails naming the harness and version when one stops being attributed by a title-independent source. AGENTS.md section 4 carries the resulting standing rule, and firstmate-coding-guidelines owns how to satisfy it. * no-mistakes: apply CI fixes * docs: move the harness-dependent-check policy out of AGENTS.md The standing rule was stated in AGENTS.md section 4 with the mechanics in firstmate-coding-guidelines, which split one contract across two owners and charged every session for a rule that only fires when firstmate's own harness-dependent code is being changed. firstmate-coding-guidelines is now the single owner of both the rule and how to satisfy it: real-harness proof required, that proof authorized to spend tokens, structural signals preferred over vendor-rendered surfaces, and a guard that fails loudly naming the harness and version where a surface signal is unavoidable. No inline stub is left behind, because AGENTS.md already carries the load trigger for that skill in sections 7 and 13, so it is read before any change to firstmate's shared tracked material. Also records the cross-platform lesson the pipeline caught in the portable regression, and corrects that file's header: the divergence assertion lives on the version-string case, which diverges on both supported platforms, rather than on every case. * no-mistakes(review): Harden tmux liveness identity and drift validation * no-mistakes(document): Clarify cross-platform tmux liveness documentation * feat(bin): propagate trace context to remote secondmates (kunchenguid#1609) * feat(bin): trace remote secondmate routes and unify the inherit allowlist Per-task W3C trace context (kunchenguid#995) resolved and injected its carrier only at the local spawn path. A remote secondmate is routed through spawn_remote_secondmate, which returns long before that site and wrote its own metadata block, so a remote secondmate stayed silently untraced even with the capability enabled. The parent home still owns that task's identity, because it holds the metadata an observer reads. It now resolves the carrier against the task's own meta under its own frozen decision - reused verbatim on relaunch, freshly rooted otherwise, never adopting the parent process's ambient TRACEPARENT - and hands it to the configured host through a new fm-spawn --traceparent argument, accepted only for a secondmate launch and only as a strict W3C value. The remote host exports it at the same unconditional pre-launch site and reports back the carrier its endpoint actually holds, which the parent records, so an already-alive endpoint reports the identity its agent really received rather than one the parent merely intended. Disabled remains byte-identical and off. The remote inherit path also carried its own hardcoded copy of the inheritable config set, already drifted from FM_INHERITABLE_CONFIG by trace-context. Both remote ends now derive from that one declaration, so a future item cannot be sent by one side and refused by the other, and session-scoped enablement items are skipped on live convergence exactly as the local path skips them. Also fixes a latent stderr leak: an absent session lock printed a raw redirect failure, which the new remote resolve site made visible. Adds tests/fm-remote-secondmate-trace-context.test.sh, driving the real parent -> fm-on -> remote entrypoint -> control -> remote fm-spawn chain over the deterministic SSH boundary and reading the carrier back from the remote pane's own log. * no-mistakes(document): Clarify remote trace and allowlist contracts * feat(bin): preflight remote runtime tool paths (kunchenguid#1623) * feat(bin): widen the remote runtime PATH and add a remote doctor preflight The fixed remote entrypoint hard-coded a four-directory PATH, so a remote account whose tools live under nix or a per-user profile could not run basic Firstmate work without a login shell. The entrypoint now composes its child PATH from the code root's bin, the account's ~/.local/bin, the common package-manager directories that actually exist on the host, and the portable system tail, deduplicated and in a fixed order, still under env -i with the same variable allowlist and no shell command string. fm-remote-doctor.sh reports that exact PATH by inheriting it from its own entrypoint launch rather than recomposing it, so the ordering keeps one owner. It is read-only, reports where each required and optional tool resolved, and exits non-zero naming every required tool that did not. Remote seeding runs it as a preflight before anything is created on the host and restores the registry when it fails. * no-mistakes(review): Harden remote git authorization and missing-tool diagnostics * no-mistakes(document): Document remote PATH doctor and safe shims * no-mistakes(lint): Fix ShellCheck findings in remote path tests * no-mistakes(lint): Suppress exported fixture's false-positive ShellCheck warning * feat: gate remote second mates on Herdr readiness (kunchenguid#1639) * feat(bin): gate remote second mates on herdr readiness A remote second mate now always runs on the Herdr backend, whose server belongs to the host's GUI login session and therefore outlives the SSH connections that supervise it. fm-spawn's remote route forces that backend and the host-local control script refuses any other, so the requirement cannot be dropped from either side. fm-remote-doctor.sh becomes the single owner of what "ready" means. It keeps its PATH and tool reporting from kunchenguid#1623 and adds the Herdr, Aqua LaunchAgent, GUI-session, server-reachability, and entrypoint-symlink checks, tagging each gap fixable: or human: with the exact operator step. --fix closes only the automatable gaps - writing and loading the Aqua-scoped dev.firstmate.herdr launch agent, starting the server where no launch agent applies, and recreating the entrypoint symlink - then re-derives every check from the host, so a human gap is never presented as fixed. It never creates a login session, writes an auto-login password, or touches FileVault. Remote seed, remote spawn, and the startup liveness relaunch all run the same check, repair, re-check sequence through one shared library and fail closed with the doctor's own gap text. Recovery inherits the gate because it respawns through the same route. Tests drive the real doctor against a controlled account fixture with a private HOME, a state-backed launchctl, and a fake herdr, and prove the dangerous actions are never attempted. The remote lifecycle suites gain a stateful Herdr CLI fixture and answer the readiness gate at the SSH boundary, so they never inspect or repair the runner's own account. * no-mistakes(review): Validate launch-agent contract and confirm Herdr startup * no-mistakes(review): Validate loaded launch-agent contract before readiness * no-mistakes(review): Refuse legacy remote backends without altering routes * no-mistakes(review): Clarify conditional remote readiness repair sequence * no-mistakes(review): Repair remote readiness before liveness probing * no-mistakes(review): Preserve unknown seeds and reject legacy liveness * no-mistakes(document): docs: clarify remote Herdr backend ownership * fix: isolate remote secondmates in shared Herdr session (kunchenguid#1659) * Pin remote secondmates to fm-remote * no-mistakes(review): Fail closed on legacy remote Herdr endpoints * no-mistakes(review): Isolate fm-remote launch agent from interactive default * no-mistakes(document): Document shared remote Herdr retirement safety * feat: route remote commands through an Aqua job worker (kunchenguid#1660) * feat: run remote commands through Aqua job worker * no-mistakes(review): Enforce remote job deadlines and safe worker shutdown * no-mistakes(review): Refresh stale workers and harden dependency-free supervision * no-mistakes(review): Harden worker ownership recovery and shutdown quarantine * no-mistakes(review): Fix doctor bootstrap, harness repair, and output draining * no-mistakes(review): Probe doctor tools through authenticated worker bootstrap * no-mistakes(review): Refresh stale workers before doctor tool probes * no-mistakes(review): Recover stopped quarantines and extend job deadlines * no-mistakes(review): Separate queue and execution timeout windows * no-mistakes(review): Supervise Linux worker crashes and bind root identity * no-mistakes(review): Resolve authorized Nix profile bin links * no-mistakes(review): Clarify Nix path resolution documentation * no-mistakes(review): Harden PATH safety and nvm selection * no-mistakes(review): Honor nvm system defaults and refresh doctor digest * no-mistakes(review): Keep workers ready during active jobs * no-mistakes(review): Bound pre-execution validation by job timeout * no-mistakes(document): Clarify remote worker documentation * no-mistakes(lint): Fix remote worker ShellCheck diagnostics * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * fix: clarify remote doctor bootstrap path (kunchenguid#1691) * fix(bin): bound remote SSH dead-peer detection (kunchenguid#1699) * fix(remote): arm SSH dead-peer detection in fm-on.sh A vanished remote host mid-poll (a reboot, a dropped link) left ssh blocked indefinitely on a half-open TCP connection, because fm-on.sh's ssh invocation had no ServerAliveInterval/ServerAliveCountMax. This wedged the remote-reply ferry: fm-procevent.sh's runner blocked inside the ssh child and never reached its own no-result -> claim-release -> reconcile re-arm self-healing path, which otherwise already handles a nonzero exit with empty output correctly. Recovery required a manual retire and re-arm. Arm ServerAliveInterval=15 and ServerAliveCountMax=3 by default (bounded ~45s detection window), both overridable via FM_SSH_ALIVE_INTERVAL and FM_SSH_ALIVE_COUNT_MAX. This is a transport- level fix in fm-on.sh, so it covers every remote command routed through it, not just the reply ferry. The remote sshd answers keepalive probes independently of whatever the remote command is doing, so a legitimately long-but-alive command (a 55s poll, a clone, the doctor) is never falsely killed - only a truly vanished peer trips it, turning that case into a bounded, detectable ssh failure (exit 255) instead of an indefinite hang. Extends tests/fm-on.test.sh with a behavioral regression asserting a bounded, positive ServerAliveInterval/ServerAliveCountMax on the real ssh argv captured through the FM_SSH_BIN process seam, plus coverage that both are env-overridable. * no-mistakes(document): Document SSH dead-peer detection ownership * fix: report stale AXI tools during bootstrap (kunchenguid#1701) * feat(bootstrap): gate stale axi CLIs at the floors firstmate actually uses Add gh-axi 0.1.29 floor so bare --squash PR merges stop failing quietly on older builds. Raise tasks-axi to FM_TASKS_AXI_MIN=0.2.2 (multi-id mv) while keeping feature probes. Keep quota-axi at 0.1.16 after verifying schema 3 and per-model availability already ship there; runway remains optional. * no-mistakes(document): Clarify AXI compatibility documentation ownership * fix: prevent false watcher-down alarms in Claude sessions (kunchenguid#1661) * fix(guard): stop false watcher-down alarm mid-turn under Claude auto-arm bin/fm-guard.sh derived its watcher-health verdict from fm_watcher_healthy, which requires a live watcher process holding the home lock. Under the Claude Stop-hook auto-arm supervision model the watcher is armed at each turn end and exits on its wake, so it runs only between turns. Every guarded command run mid-turn therefore found no live watcher and printed the "WATCHER DOWN - SUPERVISION IS OFF" banner even though supervision was healthy. Because the episode key was derived from the beacon mtime (which the between-turns watcher advances every poll), the full banner re-printed on essentially every command, and the message always blamed a "fresh beacon" that was in fact fresh. Make the pull guard's health check model-aware via a new fm_watcher_supervision_verdict in bin/fm-wake-lib.sh: - Under the auto-arm model a beacon fresh within FM_GUARD_GRACE is healthy even with no live watcher process; only a beacon stale beyond grace (or absent) is a genuine lapse and alarms. - Under every persistent-watcher harness (codex foreground checkpoint, opencode/pi/grok background arm, tmux, unknown) a live identity-matched watcher with a fresh beacon is still required, unchanged. The banner now names the true failing condition, a missing live watcher process versus a genuinely stale beacon, instead of always blaming the beacon, and the once-per-episode dedup keys on that condition rather than the beacon mtime so a genuine lapse announces once and does not re-print each turn. The turn-end guard keeps the strict fm_watcher_healthy check because it fires at the turn boundary, where the auto-arm brings a fresh watcher up and it cooperates with that arm. fm_watcher_healthy itself is unchanged, so the arm layer's start/attach/replace decisions are unaffected. Tests in tests/fm-guard-stale-banner.test.sh cover the auto-arm healthy fresh-beacon-without-a-watcher case, the auto-arm stale-beacon alarm and its stable episode, the true-reason banner wording, and the reason-keyed episode surviving a beacon mtime change; existing persistent-model cases are pinned to that model. * no-mistakes(review): Pin secondmate supervision model to launched harness * no-mistakes(document): Align watcher documentation with model-aware supervision health * test: prevent fixture temporary directory leaks (kunchenguid#1704) * fix(tests): stop fixture-tempdir helper from self-deleting under command substitution fm_test_tmproot is almost always called as `TMP_ROOT=$(fm_test_tmproot prefix)`, which forks a subshell to capture its stdout. The old implementation set its EXIT cleanup trap inside that call, so the trap fired - and deleted the fixture root - the instant the subshell exited, before the real caller's own EXIT trap was ever installed. Every test using the documented call pattern leaked its fixture root on every run; two suites had already independently discovered and worked around this with ad-hoc mktemp calls. Registration now goes through a $$-keyed registry file instead of in-process state, since $$ resolves to the invoking shell's PID even inside the subshell. The real cleanup trap is armed once at source time (always the real caller, never a subshell) for EXIT, INT, and TERM. A best-effort orphan sweep on next source reaps marked fixture roots old enough to be from a killed prior run. Simplifies the two existing ad-hoc workarounds (fm-procevent.test.sh, wake-helpers.sh) back onto the shared helper now that it works correctly. * no-mistakes(review): Preserve live fixtures during orphan reaping * no-mistakes(review): Harden fixture ownership against PID reuse * no-mistakes(review): Secure cleanup registry against path precreation * no-mistakes(review): Make fixture registration transactional * no-mistakes(document): Documentation already matches fixture cleanup behavior * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * feat(herdr): enable presentation spaces by default (kunchenguid#1708) * feat(herdr): default presentation spaces on with an explicit opt-out Herdr's disposable one-task presentation workspace was opt-in through the presence of local config/herdr-presentation-spaces. It is now on by default, and a home opts out by writing "off" into that same file. Values are read with the whole-file whitespace-stripped convention the other scalar config items already use, plus case folding. An absent file, an empty file, and "on" all resolve on; only "off" opts out; an unrecognized value warns and keeps the default rather than failing a spawn over a purely visual setting. The empty file is exactly the historical opt-in form, so every home that had already enabled the projection stays enabled with no migration step, and no previously enabled home can be turned off by the flip. Because absence now means on at both ends, secondmate inheritance needs no item-specific convergence: mirroring an absent primary file converges a secondmate to the same default-on rather than turning its projection off, and only an explicit primary opt-out propagates the opt-out. The gate itself moves into fm_backend_herdr_presentation_enabled in the Herdr adapter so the semantics have one owner that regressions can exercise directly. * no-mistakes(document): Document Herdr default-on presentation safety --------- Co-authored-by: kunchenguid <kun-1@kunchenguid.com> * fix(bin): surface fleet-wide open decisions on every wake drain (kunchenguid#1711) * fix: surface consolidated open decisions on every wake-drain A needs-decision or blocked event buried under later, unrelated status appends was only ever shown via the last-line wake annotation, so a still-open captain decision could go silently missed even though status_open_decisions (fm-classify-lib.sh) already folds the whole status stream correctly and fleet-snapshot/bearings already reuse it. Wire that same fold into bin/fm-wake-drain.sh: a new fleet-wide scan_open_decisions wrapper scans every state/<id>.status, and fm-wake-drain.sh prints a separate, bounded OPEN DECISIONS section on every drain (including the empty-queue fast path), so session-start and every wake-handling turn surface it for free without duplicating the open/resolved fold itself. Heartbeat wakes drain through the same script, so this covers that surface too. Also tighten status_open_decisions' file guard to skip an unreadable status file instead of leaking a bash redirection error, now that a fleet-wide directory scan can reach files a single targeted read would not. * no-mistakes(review): Prevent status symlinks leaking open decisions * fix: drop unbounded perl subprocess from status symlink guard The review step's own symlink-safety auto-fix (O_NOFOLLOW read via a perl subprocess) forked one perl process per status file scanned by the new fleet-wide open-decisions scan, with no cap - inflating fm-wake-drain.sh's total external-read cost from 8 (the existing annotation read_cap) to 18 in the enrichment-caps regression test. The plain [ -L "$f" ] check already rejects any status file that is itself a symlink before any read happens, which is exactly what the new regression test exercises and is the same defense level the sibling scan_captain_relevant_statuses/last_status_line already rely on elsewhere in this file (no O_NOFOLLOW). Drop the subprocess-based nofollow read and keep the cheap builtin guard. * no-mistakes(document): Document actionable fleet-wide open decision drains * fix(bin): abort parked runs and reap leaked processes before teardown (kunchenguid#1710) * fix(bin): abort orphaned no-mistakes runs and reap leaked processes at teardown Teardown could remove a task's worker while its no-mistakes pipeline run was still parked at a gate, leaving an orphaned run holding a fleet slot indefinitely (observed 2026-08-03: runs parked 7h39m and parked at a post-CI approval gate). It could also leave backgrounded/disowned descendant processes rooted under the worktree or tasktmp surviving reparented to init (observed: two `go test` binaries pinning CPU for hours with no live task meta to attribute them to). Add two coupled pre-teardown steps, both scoped to this task's exact branch/head or worktree/tasktmp so they can never touch another task's run or processes: - conclude_task_no_mistakes_run aborts a run parked at a gate via `no-mistakes axi abort`, cd'd into the exact worktree so the daemon resolves the run itself rather than teardown naming a --run id. - reap_task_worktree_processes sweeps for processes whose cwd is under the worktree or tasktmp (via `lsof -a -d cwd`) and TERM/KILLs them. Both run before any worktree return, branch delete, or backend kill, and are idempotent on a retried teardown. The branch+head attribution logic is factored out of bin/fm-crew-state.sh into the new shared bin/fm-nm-run-lib.sh so both scripts use the same ownership contract. * no-mistakes(review): Fail closed on incomplete teardown cleanup * no-mistakes(review): Bind teardown cleanup to verified run and process identities * no-mistakes(review): Require confirmed aborts and convergent identity-safe process reaping * no-mistakes(review): Handle process exits during teardown identity checks * no-mistakes(review): Restore teardown library in hermetic gotmp fixtures * no-mistakes(document): Document teardown run attribution and timeout * no-mistakes(lint): Rename shell variable conflicting with done keyword * no-mistakes: apply CI fixes * fix: resolve fm-remote-entrypoint.sh SCRIPT_DIR through a PATH symlink (kunchenguid#1709) The script installs as a symlink under ~/.local/bin. Taking dirname of the symlink itself (instead of its real target) pointed SCRIPT_DIR at ~/.local/bin, breaking sourcing of the sibling fm-remote-job-lib.sh. Resolve the real path first, preferring python3's os.path.realpath, then realpath, falling back to the raw BASH_SOURCE on hosts with neither. * no-mistakes(review): restore identity-bound actionable close fallback and C collation * no-mistakes(document): refresh serial shard table and trace-context inheritance docs * no-mistakes: apply CI fixes --------- Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com> Co-authored-by: Gavin <51008413+allstargg@users.noreply.github.com> Co-authored-by: Christopher McKay <101884182+karotkriss@users.noreply.github.com> Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
vipentti
pushed a commit
to vipentti/firstmate
that referenced
this pull request
Aug 5, 2026
…d#1563) * feat(bin): require an explicit ship delivery mode in fm-brief A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions. * feat(bin): require an explicit ship delivery contract at spawn and promotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode. * docs: record the explicit per-task delivery contract AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly. * test: pass ship delivery flags per call site in the Herdr launcher e2e The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper. * test: pass the ship delivery contract in the secondmate suites Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires.
sbracewell64
added a commit
to sbracewell64/firstmate
that referenced
this pull request
Aug 5, 2026
…ks (#39) * feat(bin): require an explicit per-task delivery contract (kunchenguid#1563) * feat(bin): require an explicit ship delivery mode in fm-brief A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions. * feat(bin): require an explicit ship delivery contract at spawn and promotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode. * docs: record the explicit per-task delivery contract AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly. * test: pass ship delivery flags per call site in the Herdr launcher e2e The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper. * test: pass the ship delivery contract in the secondmate suites Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires. * feat(bin): support remote secondmate homes (kunchenguid#1576) * Add generic remote secondmate transport * Add routed remote secondmate replies * Add remote outbox backlog handoff * Integrate remote secondmate lifecycle * no-mistakes(review): Fix remote snapshot and handoff races * no-mistakes(review): Serialize remote home provisioning transactions * no-mistakes(review): Harden remote lifecycle transaction boundaries * no-mistakes(review): Serialize remote lifecycle mutations and fail closed * no-mistakes(review): Close remote lifecycle and file race windows * no-mistakes(review): Serialize remote reply retirement and inheritance * no-mistakes(review): Harden remote transfer integrity and recovery * no-mistakes(review): Serialize remote respawn with registry retirement * no-mistakes(document): Document remote bootstrap convergence accurately * no-mistakes(document): Clarify skipped remote secondmate mutations * no-mistakes(lint): Resolve remote script ShellCheck warnings * no-mistakes: apply CI fixes * feat: add per-task trace context propagation (kunchenguid#995) * feat(spawn): propagate a native W3C traceparent to spawned agents Add a default-off capability that resolves one W3C traceparent for a task, injects it into the agent's pane shell as the TRACEPARENT environment variable immediately before launch, and records the identical value as traceparent= in state/<id>.meta, so an external observer that explicitly reads that env value or meta field can correlate a worker, a Secondmate, and their nested children into one trace with no collector, storage, UI, or vendor coupling. TRACEPARENT as an environment variable is a firstmate convention carrying a W3C-formatted value: W3C Trace Context standardizes the header, not an env var, and OpenTelemetry SDKs do not read it automatically, so a downstream must consume it deliberately; this feature parents no SDK span by itself. Identity is per task, not per spawn: the carrier is minted with random ids on the first spawn, adopted as a child (fresh span, same trace) for a nested spawn whose parent already holds one, and reused verbatim from the meta on relaunch, so a task keeps one stable logical identity across restarts. A malformed or all-zero inherited value is treated as absent and roots a fresh trace. A new root is sampled (01) - a sampling decision a downstream parent-based sampler honors, not a guarantee that any collector stores a span, and firstmate emits no spans; a child preserves the inherited flag. Trust boundary: a firstmate-minted root is random and reads no prompt, path, task prose, credential, or arbitrary environment key. An inherited TRACEPARENT is opaque caller-controlled data - up to 24 bytes of id passed through after syntax validation - so whoever set it controls those bytes, a bounded fixed-width channel rather than a general content or secret channel. The feature adds no OTEL_* variable, no tracestate, and no arbitrary environment injection; it runs no configurable or arbitrary command, only the fixed local od and tr (resolved from PATH) to read a few bytes of entropy - a small local pipeline with no network or watchdog and no hard latency guarantee. Any entropy or validation failure that returns omits the carrier without aborting the spawn. A default-off spawn leaves the generated meta and launch environment unchanged. Enablement is default-off (config/trace-context, or FM_TRACE_CONTEXT where a non-empty value overrides and unset or empty defers to the file) and is propagated into secondmate homes, taking effect at each agent's next launch: a Secondmate launched or relaunched after enablement carries the primary trace into its nested workers, while an already-running Secondmate roots new traces for its own workers until relaunched. Injection reuses the existing GOTMPDIR channel, so all spawn backends and harnesses and the ship, scout, and secondmate paths are covered. Covered by a pure-library suite and a spawn-path integration test (fake tmux plus a real worktree, hermetic against ambient FM_TRACE_CONTEXT) proving the recorded and injected carriers are identical and sent before launch, that default-off writes and injects neither, that a relaunch reuses the recorded carrier, and that an explicit FM_TRACE_CONTEXT overrides the file both ways; plus a source-owner inheritance test proving trace-context propagates and absence-mirrors through propagate_inheritable_config. Documentation follows the repository documentation-audiences contract: docs/trace-context.md is maintainer-architecture rationale, the configuration schema lives in docs/configuration.md, and the repeatable test evidence is separated into docs/verification/trace-context.md (maintainer-verification), registered in docs/documentation-audiences.json. * fix(spawn): propagate the effective trace-context decision to secondmates FM_TRACE_CONTEXT overrode trace context only in the process that read it. A newly launched secondmate decided enablement from the inherited config/trace-context file alone, so the override did not cross the primary-to-secondmate boundary: FM_TRACE_CONTEXT=off with the file present left the secondmate's nested workers traced (a broken kill switch), and FM_TRACE_CONTEXT=on with the file absent left them untraced despite the inherited carrier. Deliver the primary's effective decision to a newly launched secondmate as a normalized on/off FM_TRACE_CONTEXT in the launch prefix, so a FM_TRACE_CONTEXT override governs the nested primary -> secondmate -> worker chain both ways, not just the copied file. The value is bounded to the literal on/off and does not broaden environment injection; the already-running secondmate boundary is unchanged. Add a genuine two-level spawn regression that drives fm-spawn twice with the exact environment the primary injects into the secondmate and proves both divergent directions end to end. Correct the documentation that implied secondmate coverage on every backend, since orca and cmux reject secondmate spawns, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Clarify Secondmate trace-context launch snapshots * no-mistakes(document): Correct trace-context documentation ownership and relaunch semantics * fix(spawn): resolve the trace-context decision once for carrier and snapshot The effective trace-context decision was read twice per spawn: once inside fm_trace_context_resolve for the recorded carrier, and again for the secondmate FM_TRACE_CONTEXT launch snapshot. A config-file change between the two reads could pair a carrier with the opposite enable state - an injected carrier with an off snapshot, or no carrier with an on snapshot. Freeze the effective on/off decision once, drive the carrier resolution under that frozen FM_TRACE_CONTEXT so it cannot independently re-read the file, and reuse the same frozen decision for the secondmate launch snapshot. Add a spawn-path regression that drives the file-decided path and proves the recorded carrier and the delivered snapshot always agree, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Preserve legacy Secondmate trace boundary * no-mistakes(document): Correct trace-context verification comparison base * no-mistakes(review): Captain, prevent failed trace delivery metadata claims * no-mistakes(review): Captain, align trace-context tests and verification evidence * no-mistakes(document): Correct trace-context verification evidence * no-mistakes(lint): Suppress intentional ShellCheck literal-dollar warnings * no-mistakes(review): Captain: freeze trace context at session start * no-mistakes(test): Captain: stabilize scheduler test and document Kimi trace coverage * no-mistakes(document): Document trace-context safety boundaries * fix(trace): fail off on stale session snapshots Publish each home session decision atomically through a same-directory temporary file and bind it to the current session lock. A replacement failure can no longer leave an earlier on decision active in a later session; missing, stale, malformed, or unpublishable state defaults safely to off. Add regressions for read-only replacement and failed publication, update spawn and session-start fixtures for the lock-bound format, and refresh the architecture and verification records. * no-mistakes(review): Fix trace spawn failure independence and duplicate safety * no-mistakes(document): Refresh trace-context documentation and verification * no-mistakes(review): Clear partial backend input after failed trace submission * no-mistakes(review): Stop unsafe trace delivery before launch append * no-mistakes(document): Document unsafe trace delivery handling * fix(trace): bound each trace to one routed task, never the routing agent A persistent Secondmate holds its launch-time TRACEPARENT in the process environment for its whole life, and routed requests never replace it, so resolving new-task carriers from the ambient environment chained every routed task into one ever-growing trace per Secondmate with distinct parent ids. Resolve now reuses the task's recorded carrier or mints a fresh sampled root, never reading ambient TRACEPARENT, so each routed task is its own trace boundary while relaunch, recovery, and scout-to-ship promotion keep one stable per-task identity. The spawn regression models the reviewed scenario exactly: two unrelated tasks spawned sequentially through one persistent Secondmate environment record and inject distinct trace ids, adopt nothing from the Secondmate's carrier, and a relaunch of the first task reuses its original carrier verbatim. * docs(trace): define the per-task trace boundary The design contract is one task per trace: a persistent Secondmate is routing infrastructure with its own agent identity, never a shared trace root for the unrelated tasks routed through it. Root/recovery semantics replace the removed child-inheritance path, the sampling and safety sections drop inherited-carrier language because ambient TRACEPARENT is never read, and the verification page records the refreshed suite inventories including the two-task Secondmate boundary regression. * test(trace): adopt the explicit per-task delivery contract in spawn fixtures Rebasing onto current main brings the explicit per-task delivery contract: ship spawns now require --mode and --yolo instead of resolving them from the project registry. The trace spawn fixtures pass the same explicit contract canonical spawn tests use, preserving the per-task trace boundary coverage unchanged, and the verification page records the refreshed comparison base. * fix(bin): harden tmux agent liveness across harnesses (kunchenguid#1577) * fix(bin): classify tmux agent liveness independent of process titles `fm_backend_tmux_agent_state` attributed a pane solely from `#{pane_current_command}`, which is a process TITLE a harness can rewrite, not a structural fact. Claude Code 2.1.220 reports its version string there, so a live Claude endpoint classified `ambiguous`: the session-start secondmate liveness sweep could no longer see it, and any consumer that gates on a positive classification refuses outright. Read a second, independent name source: the kernel `comm` of every process in the pane tty's foreground process group. Either source naming a verified harness yields `alive`, because a false `dead` is the one verdict that can start a duplicate agent on a live worktree. Scoping to the foreground process group rather than the pane's descendants keeps a harness-named background process from faking an agent, and covers multi-process launchers (the Pi Launcher path) without a special case. Verified on 2026-08-03 against all seven adapters running for real on tmux 3.6a / macOS 26.5.2 arm64: claude 2.1.220, codex-cli 0.146.0, opencode 1.18.11, pi 0.82.0, pi-signed 0.82.0, grok 0.2.118, kimi 0.31.1 all classify `alive`, each attributed by a source independent of its title. Two tests, because they fail for different reasons: - tests/fm-tmux-agent-liveness.test.sh pins the logic with real processes and no harness, so it runs everywhere CI runs tmux. It drives the two name sources apart on purpose and asserts the divergence, so no case can go quietly vacuous. - tests/fm-harness-liveness-drift-live-e2e.test.sh relaunches every installed harness and fails naming the harness and version when one stops being attributed by a title-independent source. AGENTS.md section 4 carries the resulting standing rule, and firstmate-coding-guidelines owns how to satisfy it. * no-mistakes: apply CI fixes * docs: move the harness-dependent-check policy out of AGENTS.md The standing rule was stated in AGENTS.md section 4 with the mechanics in firstmate-coding-guidelines, which split one contract across two owners and charged every session for a rule that only fires when firstmate's own harness-dependent code is being changed. firstmate-coding-guidelines is now the single owner of both the rule and how to satisfy it: real-harness proof required, that proof authorized to spend tokens, structural signals preferred over vendor-rendered surfaces, and a guard that fails loudly naming the harness and version where a surface signal is unavoidable. No inline stub is left behind, because AGENTS.md already carries the load trigger for that skill in sections 7 and 13, so it is read before any change to firstmate's shared tracked material. Also records the cross-platform lesson the pipeline caught in the portable regression, and corrects that file's header: the divergence assertion lives on the version-string case, which diverges on both supported platforms, rather than on every case. * no-mistakes(review): Harden tmux liveness identity and drift validation * no-mistakes(document): Clarify cross-platform tmux liveness documentation * feat(bin): propagate trace context to remote secondmates (kunchenguid#1609) * feat(bin): trace remote secondmate routes and unify the inherit allowlist Per-task W3C trace context (kunchenguid#995) resolved and injected its carrier only at the local spawn path. A remote secondmate is routed through spawn_remote_secondmate, which returns long before that site and wrote its own metadata block, so a remote secondmate stayed silently untraced even with the capability enabled. The parent home still owns that task's identity, because it holds the metadata an observer reads. It now resolves the carrier against the task's own meta under its own frozen decision - reused verbatim on relaunch, freshly rooted otherwise, never adopting the parent process's ambient TRACEPARENT - and hands it to the configured host through a new fm-spawn --traceparent argument, accepted only for a secondmate launch and only as a strict W3C value. The remote host exports it at the same unconditional pre-launch site and reports back the carrier its endpoint actually holds, which the parent records, so an already-alive endpoint reports the identity its agent really received rather than one the parent merely intended. Disabled remains byte-identical and off. The remote inherit path also carried its own hardcoded copy of the inheritable config set, already drifted from FM_INHERITABLE_CONFIG by trace-context. Both remote ends now derive from that one declaration, so a future item cannot be sent by one side and refused by the other, and session-scoped enablement items are skipped on live convergence exactly as the local path skips them. Also fixes a latent stderr leak: an absent session lock printed a raw redirect failure, which the new remote resolve site made visible. Adds tests/fm-remote-secondmate-trace-context.test.sh, driving the real parent -> fm-on -> remote entrypoint -> control -> remote fm-spawn chain over the deterministic SSH boundary and reading the carrier back from the remote pane's own log. * no-mistakes(document): Clarify remote trace and allowlist contracts * feat(bin): preflight remote runtime tool paths (kunchenguid#1623) * feat(bin): widen the remote runtime PATH and add a remote doctor preflight The fixed remote entrypoint hard-coded a four-directory PATH, so a remote account whose tools live under nix or a per-user profile could not run basic Firstmate work without a login shell. The entrypoint now composes its child PATH from the code root's bin, the account's ~/.local/bin, the common package-manager directories that actually exist on the host, and the portable system tail, deduplicated and in a fixed order, still under env -i with the same variable allowlist and no shell command string. fm-remote-doctor.sh reports that exact PATH by inheriting it from its own entrypoint launch rather than recomposing it, so the ordering keeps one owner. It is read-only, reports where each required and optional tool resolved, and exits non-zero naming every required tool that did not. Remote seeding runs it as a preflight before anything is created on the host and restores the registry when it fails. * no-mistakes(review): Harden remote git authorization and missing-tool diagnostics * no-mistakes(document): Document remote PATH doctor and safe shims * no-mistakes(lint): Fix ShellCheck findings in remote path tests * no-mistakes(lint): Suppress exported fixture's false-positive ShellCheck warning * feat: gate remote second mates on Herdr readiness (kunchenguid#1639) * feat(bin): gate remote second mates on herdr readiness A remote second mate now always runs on the Herdr backend, whose server belongs to the host's GUI login session and therefore outlives the SSH connections that supervise it. fm-spawn's remote route forces that backend and the host-local control script refuses any other, so the requirement cannot be dropped from either side. fm-remote-doctor.sh becomes the single owner of what "ready" means. It keeps its PATH and tool reporting from kunchenguid#1623 and adds the Herdr, Aqua LaunchAgent, GUI-session, server-reachability, and entrypoint-symlink checks, tagging each gap fixable: or human: with the exact operator step. --fix closes only the automatable gaps - writing and loading the Aqua-scoped dev.firstmate.herdr launch agent, starting the server where no launch agent applies, and recreating the entrypoint symlink - then re-derives every check from the host, so a human gap is never presented as fixed. It never creates a login session, writes an auto-login password, or touches FileVault. Remote seed, remote spawn, and the startup liveness relaunch all run the same check, repair, re-check sequence through one shared library and fail closed with the doctor's own gap text. Recovery inherits the gate because it respawns through the same route. Tests drive the real doctor against a controlled account fixture with a private HOME, a state-backed launchctl, and a fake herdr, and prove the dangerous actions are never attempted. The remote lifecycle suites gain a stateful Herdr CLI fixture and answer the readiness gate at the SSH boundary, so they never inspect or repair the runner's own account. * no-mistakes(review): Validate launch-agent contract and confirm Herdr startup * no-mistakes(review): Validate loaded launch-agent contract before readiness * no-mistakes(review): Refuse legacy remote backends without altering routes * no-mistakes(review): Clarify conditional remote readiness repair sequence * no-mistakes(review): Repair remote readiness before liveness probing * no-mistakes(review): Preserve unknown seeds and reject legacy liveness * no-mistakes(document): docs: clarify remote Herdr backend ownership * fix: isolate remote secondmates in shared Herdr session (kunchenguid#1659) * Pin remote secondmates to fm-remote * no-mistakes(review): Fail closed on legacy remote Herdr endpoints * no-mistakes(review): Isolate fm-remote launch agent from interactive default * no-mistakes(document): Document shared remote Herdr retirement safety * feat: route remote commands through an Aqua job worker (kunchenguid#1660) * feat: run remote commands through Aqua job worker * no-mistakes(review): Enforce remote job deadlines and safe worker shutdown * no-mistakes(review): Refresh stale workers and harden dependency-free supervision * no-mistakes(review): Harden worker ownership recovery and shutdown quarantine * no-mistakes(review): Fix doctor bootstrap, harness repair, and output draining * no-mistakes(review): Probe doctor tools through authenticated worker bootstrap * no-mistakes(review): Refresh stale workers before doctor tool probes * no-mistakes(review): Recover stopped quarantines and extend job deadlines * no-mistakes(review): Separate queue and execution timeout windows * no-mistakes(review): Supervise Linux worker crashes and bind root identity * no-mistakes(review): Resolve authorized Nix profile bin links * no-mistakes(review): Clarify Nix path resolution documentation * no-mistakes(review): Harden PATH safety and nvm selection * no-mistakes(review): Honor nvm system defaults and refresh doctor digest * no-mistakes(review): Keep workers ready during active jobs * no-mistakes(review): Bound pre-execution validation by job timeout * no-mistakes(document): Clarify remote worker documentation * no-mistakes(lint): Fix remote worker ShellCheck diagnostics * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * fix: clarify remote doctor bootstrap path (kunchenguid#1691) * fix(bin): bound remote SSH dead-peer detection (kunchenguid#1699) * fix(remote): arm SSH dead-peer detection in fm-on.sh A vanished remote host mid-poll (a reboot, a dropped link) left ssh blocked indefinitely on a half-open TCP connection, because fm-on.sh's ssh invocation had no ServerAliveInterval/ServerAliveCountMax. This wedged the remote-reply ferry: fm-procevent.sh's runner blocked inside the ssh child and never reached its own no-result -> claim-release -> reconcile re-arm self-healing path, which otherwise already handles a nonzero exit with empty output correctly. Recovery required a manual retire and re-arm. Arm ServerAliveInterval=15 and ServerAliveCountMax=3 by default (bounded ~45s detection window), both overridable via FM_SSH_ALIVE_INTERVAL and FM_SSH_ALIVE_COUNT_MAX. This is a transport- level fix in fm-on.sh, so it covers every remote command routed through it, not just the reply ferry. The remote sshd answers keepalive probes independently of whatever the remote command is doing, so a legitimately long-but-alive command (a 55s poll, a clone, the doctor) is never falsely killed - only a truly vanished peer trips it, turning that case into a bounded, detectable ssh failure (exit 255) instead of an indefinite hang. Extends tests/fm-on.test.sh with a behavioral regression asserting a bounded, positive ServerAliveInterval/ServerAliveCountMax on the real ssh argv captured through the FM_SSH_BIN process seam, plus coverage that both are env-overridable. * no-mistakes(document): Document SSH dead-peer detection ownership * fix: report stale AXI tools during bootstrap (kunchenguid#1701) * feat(bootstrap): gate stale axi CLIs at the floors firstmate actually uses Add gh-axi 0.1.29 floor so bare --squash PR merges stop failing quietly on older builds. Raise tasks-axi to FM_TASKS_AXI_MIN=0.2.2 (multi-id mv) while keeping feature probes. Keep quota-axi at 0.1.16 after verifying schema 3 and per-model availability already ship there; runway remains optional. * no-mistakes(document): Clarify AXI compatibility documentation ownership * fix: prevent false watcher-down alarms in Claude sessions (kunchenguid#1661) * fix(guard): stop false watcher-down alarm mid-turn under Claude auto-arm bin/fm-guard.sh derived its watcher-health verdict from fm_watcher_healthy, which requires a live watcher process holding the home lock. Under the Claude Stop-hook auto-arm supervision model the watcher is armed at each turn end and exits on its wake, so it runs only between turns. Every guarded command run mid-turn therefore found no live watcher and printed the "WATCHER DOWN - SUPERVISION IS OFF" banner even though supervision was healthy. Because the episode key was derived from the beacon mtime (which the between-turns watcher advances every poll), the full banner re-printed on essentially every command, and the message always blamed a "fresh beacon" that was in fact fresh. Make the pull guard's health check model-aware via a new fm_watcher_supervision_verdict in bin/fm-wake-lib.sh: - Under the auto-arm model a beacon fresh within FM_GUARD_GRACE is healthy even with no live watcher process; only a beacon stale beyond grace (or absent) is a genuine lapse and alarms. - Under every persistent-watcher harness (codex foreground checkpoint, opencode/pi/grok background arm, tmux, unknown) a live identity-matched watcher with a fresh beacon is still required, unchanged. The banner now names the true failing condition, a missing live watcher process versus a genuinely stale beacon, instead of always blaming the beacon, and the once-per-episode dedup keys on that condition rather than the beacon mtime so a genuine lapse announces once and does not re-print each turn. The turn-end guard keeps the strict fm_watcher_healthy check because it fires at the turn boundary, where the auto-arm brings a fresh watcher up and it cooperates with that arm. fm_watcher_healthy itself is unchanged, so the arm layer's start/attach/replace decisions are unaffected. Tests in tests/fm-guard-stale-banner.test.sh cover the auto-arm healthy fresh-beacon-without-a-watcher case, the auto-arm stale-beacon alarm and its stable episode, the true-reason banner wording, and the reason-keyed episode surviving a beacon mtime change; existing persistent-model cases are pinned to that model. * no-mistakes(review): Pin secondmate supervision model to launched harness * no-mistakes(document): Align watcher documentation with model-aware supervision health * test: prevent fixture temporary directory leaks (kunchenguid#1704) * fix(tests): stop fixture-tempdir helper from self-deleting under command substitution fm_test_tmproot is almost always called as `TMP_ROOT=$(fm_test_tmproot prefix)`, which forks a subshell to capture its stdout. The old implementation set its EXIT cleanup trap inside that call, so the trap fired - and deleted the fixture root - the instant the subshell exited, before the real caller's own EXIT trap was ever installed. Every test using the documented call pattern leaked its fixture root on every run; two suites had already independently discovered and worked around this with ad-hoc mktemp calls. Registration now goes through a $$-keyed registry file instead of in-process state, since $$ resolves to the invoking shell's PID even inside the subshell. The real cleanup trap is armed once at source time (always the real caller, never a subshell) for EXIT, INT, and TERM. A best-effort orphan sweep on next source reaps marked fixture roots old enough to be from a killed prior run. Simplifies the two existing ad-hoc workarounds (fm-procevent.test.sh, wake-helpers.sh) back onto the shared helper now that it works correctly. * no-mistakes(review): Preserve live fixtures during orphan reaping * no-mistakes(review): Harden fixture ownership against PID reuse * no-mistakes(review): Secure cleanup registry against path precreation * no-mistakes(review): Make fixture registration transactional * no-mistakes(document): Documentation already matches fixture cleanup behavior * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * feat(herdr): enable presentation spaces by default (kunchenguid#1708) * feat(herdr): default presentation spaces on with an explicit opt-out Herdr's disposable one-task presentation workspace was opt-in through the presence of local config/herdr-presentation-spaces. It is now on by default, and a home opts out by writing "off" into that same file. Values are read with the whole-file whitespace-stripped convention the other scalar config items already use, plus case folding. An absent file, an empty file, and "on" all resolve on; only "off" opts out; an unrecognized value warns and keeps the default rather than failing a spawn over a purely visual setting. The empty file is exactly the historical opt-in form, so every home that had already enabled the projection stays enabled with no migration step, and no previously enabled home can be turned off by the flip. Because absence now means on at both ends, secondmate inheritance needs no item-specific convergence: mirroring an absent primary file converges a secondmate to the same default-on rather than turning its projection off, and only an explicit primary opt-out propagates the opt-out. The gate itself moves into fm_backend_herdr_presentation_enabled in the Herdr adapter so the semantics have one owner that regressions can exercise directly. * no-mistakes(document): Document Herdr default-on presentation safety --------- Co-authored-by: kunchenguid <kun-1@kunchenguid.com> * fix(bin): surface fleet-wide open decisions on every wake drain (kunchenguid#1711) * fix: surface consolidated open decisions on every wake-drain A needs-decision or blocked event buried under later, unrelated status appends was only ever shown via the last-line wake annotation, so a still-open captain decision could go silently missed even though status_open_decisions (fm-classify-lib.sh) already folds the whole status stream correctly and fleet-snapshot/bearings already reuse it. Wire that same fold into bin/fm-wake-drain.sh: a new fleet-wide scan_open_decisions wrapper scans every state/<id>.status, and fm-wake-drain.sh prints a separate, bounded OPEN DECISIONS section on every drain (including the empty-queue fast path), so session-start and every wake-handling turn surface it for free without duplicating the open/resolved fold itself. Heartbeat wakes drain through the same script, so this covers that surface too. Also tighten status_open_decisions' file guard to skip an unreadable status file instead of leaking a bash redirection error, now that a fleet-wide directory scan can reach files a single targeted read would not. * no-mistakes(review): Prevent status symlinks leaking open decisions * fix: drop unbounded perl subprocess from status symlink guard The review step's own symlink-safety auto-fix (O_NOFOLLOW read via a perl subprocess) forked one perl process per status file scanned by the new fleet-wide open-decisions scan, with no cap - inflating fm-wake-drain.sh's total external-read cost from 8 (the existing annotation read_cap) to 18 in the enrichment-caps regression test. The plain [ -L "$f" ] check already rejects any status file that is itself a symlink before any read happens, which is exactly what the new regression test exercises and is the same defense level the sibling scan_captain_relevant_statuses/last_status_line already rely on elsewhere in this file (no O_NOFOLLOW). Drop the subprocess-based nofollow read and keep the cheap builtin guard. * no-mistakes(document): Document actionable fleet-wide open decision drains * fix(bin): abort parked runs and reap leaked processes before teardown (kunchenguid#1710) * fix(bin): abort orphaned no-mistakes runs and reap leaked processes at teardown Teardown could remove a task's worker while its no-mistakes pipeline run was still parked at a gate, leaving an orphaned run holding a fleet slot indefinitely (observed 2026-08-03: runs parked 7h39m and parked at a post-CI approval gate). It could also leave backgrounded/disowned descendant processes rooted under the worktree or tasktmp surviving reparented to init (observed: two `go test` binaries pinning CPU for hours with no live task meta to attribute them to). Add two coupled pre-teardown steps, both scoped to this task's exact branch/head or worktree/tasktmp so they can never touch another task's run or processes: - conclude_task_no_mistakes_run aborts a run parked at a gate via `no-mistakes axi abort`, cd'd into the exact worktree so the daemon resolves the run itself rather than teardown naming a --run id. - reap_task_worktree_processes sweeps for processes whose cwd is under the worktree or tasktmp (via `lsof -a -d cwd`) and TERM/KILLs them. Both run before any worktree return, branch delete, or backend kill, and are idempotent on a retried teardown. The branch+head attribution logic is factored out of bin/fm-crew-state.sh into the new shared bin/fm-nm-run-lib.sh so both scripts use the same ownership contract. * no-mistakes(review): Fail closed on incomplete teardown cleanup * no-mistakes(review): Bind teardown cleanup to verified run and process identities * no-mistakes(review): Require confirmed aborts and convergent identity-safe process reaping * no-mistakes(review): Handle process exits during teardown identity checks * no-mistakes(review): Restore teardown library in hermetic gotmp fixtures * no-mistakes(document): Document teardown run attribution and timeout * no-mistakes(lint): Rename shell variable conflicting with done keyword * no-mistakes: apply CI fixes * test: give fork scaffolding tests the delivery contract upstream now requires Upstream kunchenguid#1563 made --mode mandatory for ship briefs and --mode plus --yolo mandatory for ship spawns, and updated the tests it shipped with. Three fork tests were written after the fork diverged, against the pre-kunchenguid#1563 interfaces, so they called the scaffolds with no delivery contract. Merging the two trunks put those callers in front of the new requirement and they refused before reaching the behavior each test exists to pin. Git could not see this: the requirement and the callers live in different files, so both sides merged clean and the breakage only showed up when the suite ran. Upstream's contract is kept exactly as it is - it is a deliberate safety gate, and AGENTS.md section 7 depends on the brief and the spawn refusing to guess. Each fork test instead states its mode explicitly, so every assertion it was written to make still runs. The standing-worker-rules variants each state their own mode, because those cases assert mode-specific prose and fm-brief no longer reads the registry. Verified: all three fail on the merge commit and pass here, with fm-brief, fm-launch-lib and fm-model-zero-budget green. --------- Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com> Co-authored-by: Gavin <51008413+allstargg@users.noreply.github.com> Co-authored-by: Christopher McKay <101884182+karotkriss@users.noreply.github.com> Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
josh-padnick
added a commit
to josh-padnick/firstmate
that referenced
this pull request
Aug 5, 2026
* fix(bin): retire terminal process events and surface queued wakes (#1500)
* fix(bin): deliver process-event results and retire ended sources
Two defects reproduced during a real Lavish adapter session.
One human `Send & End` produced four captured results: the real feedback,
then recurring empty ended sessions. The generic runner had no way to learn
a source was finished, so every reconcile restarted a poll that returned
immediately. The runner now asks the source's own adapter -
`fm-procevent-<adapter>.sh terminal <result-file>` - and on exit 0 alone
re-proves ownership, drops the registration, and releases its own claim
under one source boundary. Terminal knowledge stays adapter-owned: for
Lavish that is an ended session, a missing session, and the final feedback
delivery the published poll marks with `session_ended`. An adapter with no
terminal command keeps its source armed exactly as before. Capture before
publication, captured-result durability, queued wake durability, bounded
re-announcement, handled deduplication, one-owner ownership, and explicit
idempotent retirement are all unchanged.
A captured result queued its `check` wake durably, but a healthy watcher
with a fresh beacon never delivered it; the result surfaced only after a
manual drain. Publication happens outside the watcher (in the runner) or
unconditionally (in reconcile), so the watcher had no newly actionable
signal to report and never reached its rewake path. It now reports a
queued-but-unsurfaced process-event record through the same actionable exit
every other wake uses, deduplicated by the same `.seen-*` marker discipline
the signal scan uses, so the record is always durable before it is
suppressed. The durable queue remains the authority and no second notifier,
poller, timer, queue, or adapter-specific wake path is added.
Regressions cover both, driven end to end: an armed Lavish source against a
stand-in for the published poll polls once, captures once, publishes one
distinct event, and retires itself; two fixture adapters prove the terminal
decision follows the adapter alone; and a real capture plus a real watcher
prove one proactive wake before any drain, with no duplicate wake while the
record stays queued or after it is acknowledged.
* no-mistakes(review): Harden process-event retirement and proactive delivery
* no-mistakes(review): Route process-event delivery through shared wake owner
* no-mistakes(document): Clarify process-event delivery and retirement documentation
* no-mistakes(lint): Fix ShellCheck control-flow warnings
* no-mistakes(lint): Fix wake output status lint warning
* perf: shard portable serial tests across CI runners (#1544)
* perf(ci): shard the portable serial behavior lane across runners
The Behavior portable serial job ran all 69 scripts of the serial
remainder on one runner. The measured serial sum on run 30725985757 was
1143762 ms (19m04s) against a 20-minute timeout, so the job intermittently
reached the cap and was cancelled with every step passing. Setup is only
about 7s, so the cost is entirely test wall time.
Split the lane into four separate-runner shards. Each shard is still
strictly serial, and separate runners mean no two of these stateful
scripts ever share a machine, so the split needs no concurrency isolation
proof. Assignment is longest-processing-time bin packing over measured
per-script duration hints, balancing every shard to 285941 ms (~4m46s) of
expected work, and the timeout tightens from 20 to 15 minutes.
bin/fm-test-run.sh owns the shard count and refuses a lane whose "ofN"
disagrees with it, while ci.yml derives the same count from
strategy.job-total rather than a literal, so changing it in either file
alone fails the lane loudly instead of leaving part of the required suite
unrun. --check-coverage additionally proves the shards are non-empty,
disjoint, and exactly equal to the serial lane. No test is weakened,
skipped, or removed.
Also replace the wall-clock sleeps in the --jobs scheduler test fixture
with an explicit signal handshake between the fixtures. The old
0.5s-versus-0.05s race failed on a loaded machine; the handshake passes
under sustained CPU saturation.
* no-mistakes(review): Correct portable serial shard balance evidence
* no-mistakes(document): Document portable serial shard evidence accurately
* fix(bin): correct session lock and attached watcher supervision (#1545)
* fix(bin): identify harness sessions by path and report delivered wakes
Two supervision faults, both reported by a contributor and both open on the
default branch.
Fault 1: the Stop auto-arm never claims the home. fm_harness_ancestry_pid()
matched only the basename of `ps -o comm=`, and Claude Code's native installer
names the per-session executable by its version (.../share/claude/versions/
2.1.220), so that basename identifies nothing. Three real failure shapes follow:
a version-named session is missed entirely and the hook exits 0 with the epoch
never written (unconditional on Linux, where procps reports the kernel exec name
and ignores argv[0]); a claude-named daemon that directly parents sessions wins
the outermost-contiguous-claude rule ahead of the session itself; and a session
that is both version-named and daemon-parented has its live lock reclaimed as
stale and rewritten to the shared daemon pid, corrupting the home's ownership
record.
Harness identity now also reads whole components of the executable path and of
argv[0], which is what both platforms still carry. Matching whole components
only keeps that widening safe: bin/fm-claude-stop-autoarm.sh and ~/.claude/hooks
scripts have no "claude" component. Ownership is then decided against the
session's whole contiguous harness ancestry rather than one chosen pid, which is
the honest form of the question the library already documents ("does the current
process descend from that same harness?"). That subsumes the outermost-pid rule
for Claude's nested bg-spare worker chain instead of reverting it, and lets a
daemon-parented session recognize its own lock. Lock acquisition still writes the
outermost pid of the run, the only pid that lives as long as the session.
Fault 2: an attached arm reports a delivered cycle as FAILED. The watcher prints
its one reason line to its own stdout, so only the arm that forked it can read
that line; an arm that attached observes nothing but a released lock and called a
completely successful cycle "cycle ended without an actionable reason". No
supervision event was lost - the durable queue held it - but every harness
protocol reads that line as "supervision is down" and directs a manual re-arm.
The arm now resolves an unobservable close against the durable wake queue, which
records every wake before the watcher prints it and whose sequence counter never
rewinds, not even across a drain. A cycle the queue proves delivered a wake
reports that wake and exits 0; a cycle whose records a handling turn already
drained reports the delivery without inventing a reason line; only a cycle that
delivered nothing is still the typed nonzero failure. Fixing it in the arm covers
codex, opencode, pi, grok and kimi, not just the Claude Stop path.
Regressions: tests/fm-session-lock-ancestry.test.sh pins both platforms' ps
semantics behind a deterministic process table and runs the real Stop auto-arm in
version-named, daemon-parented, and combined real process trees, each orphaned so
the walk cannot escape the fixture. tests/fm-watch-arm.test.sh drives a real
watcher and a real attached arm through a real wake. Every fault case fails on
the previous code.
* no-mistakes(review): Bind watcher delivery records to process identity
* no-mistakes(review): Return validated watcher identity atomically
* no-mistakes(review): Track watcher successors by PID and identity
* no-mistakes(document): Consolidate watcher arm-cycle documentation ownership
* fix(bin): harden Claude supervision auto-arm recovery (#1495)
* fix(supervision): harden Claude auto-arm failure handling
* no-mistakes(review): Guarantee automatic retry after Claude auto-arm failures
* no-mistakes(review): Gate attended fail-open on verified supervision failure
* no-mistakes(document): Document Claude auto-arm retry and guard scope
* no-mistakes: apply CI fixes
* fix(supervision): make Claude fail-open progression monotonic
* no-mistakes(review): Preserve auto-arm failure episodes until verified watcher recovery
* no-mistakes(review): Linearize auto-arm failure progression across existing locks
* no-mistakes(review): Linearize positive recovery across shared failure episode lock
* no-mistakes(review): Scope Claude recovery contention to Claude guard mode
* no-mistakes(document): Align supervision auto-arm documentation
* no-mistakes(review): Preserve actionable wakes despite healthy successors
* no-mistakes(document): Refresh supervision auto-arm documentation
* feat(bin): require an explicit per-task delivery contract (#1563)
* feat(bin): require an explicit ship delivery mode in fm-brief
A ship brief's definition of done was shaped by a silent per-project registry
lookup, so an adjusted brief and the task's recorded delivery could disagree and
no one had to decide anything per task.
fm-brief now requires --mode on ship scaffolds, validates it against the closed
set, refuses the conditional no-mistakes-prod-only registry policy as a task
mode, and records the choice as a fixed machine-readable "Delivery contract:
mode=<mode>" line that fm-spawn can check. --mode is refused on scout and
secondmate scaffolds, and --yolo is refused outright because the worker never
owns approval decisions.
* feat(bin): require an explicit ship delivery contract at spawn and promotion
fm-spawn resolved every ship and scout task's mode and yolo from the project
registry, so the delivery posture was never a per-task decision and could
contradict the brief the worker was about to follow.
fm-spawn now requires --mode and --yolo on ship spawns, validates both against
their closed sets, and reads the brief's recorded delivery contract line and
refuses a mismatch before any endpoint exists; a brief scaffolded before that
line existed warns once and launches on the flag. A batch carries one shared
contract that each pair still checks against its own brief. Scout and secondmate
spawns refuse the flags, and a scout now records no mode or yolo at all, which
teardown and the snapshot already tolerate. When the explicit mode carries less
rigor than the project's standing posture, a deviation notice is printed and the
spawn continues, so the registry stays advisory rather than an enforced default.
fm-promote requires the same two flags, because a scout carries no posture to
inherit, and writes them into the task record with the kind flip.
fm-project-mode keeps its one registry parser for the mechanical consumers that
have no task in hand, accepts the conditional no-mistakes-prod-only annotation
and maps it to its most rigorous leg for them, and grows --raw so the deviation
notice can tell a conditional policy apart from a flat mode.
* docs: record the explicit per-task delivery contract
AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at
intake, including the surface classification for a no-mistakes-prod-only project
and the unregistered-project fallback, and the project-management skill defines
that conditional policy as a registration-time posture with its defaults and
initialization consequences. The registry blurb, script table, and architecture
section follow: the registry records the captain's standing posture, and task
delivery is decided per task and passed explicitly.
* test: pass ship delivery flags per call site in the Herdr launcher e2e
The shared spawn helper also launches a secondmate, which refuses the flags, so
the contract belongs at each ship call site rather than inside the helper.
* test: pass the ship delivery contract in the secondmate suites
Both suites scaffold or spawn an ordinary ship task as the control case for a
secondmate assertion, so each needs the explicit contract the ship path now
requires.
* feat(bin): support remote secondmate homes (#1576)
* Add generic remote secondmate transport
* Add routed remote secondmate replies
* Add remote outbox backlog handoff
* Integrate remote secondmate lifecycle
* no-mistakes(review): Fix remote snapshot and handoff races
* no-mistakes(review): Serialize remote home provisioning transactions
* no-mistakes(review): Harden remote lifecycle transaction boundaries
* no-mistakes(review): Serialize remote lifecycle mutations and fail closed
* no-mistakes(review): Close remote lifecycle and file race windows
* no-mistakes(review): Serialize remote reply retirement and inheritance
* no-mistakes(review): Harden remote transfer integrity and recovery
* no-mistakes(review): Serialize remote respawn with registry retirement
* no-mistakes(document): Document remote bootstrap convergence accurately
* no-mistakes(document): Clarify skipped remote secondmate mutations
* no-mistakes(lint): Resolve remote script ShellCheck warnings
* no-mistakes: apply CI fixes
* feat: add per-task trace context propagation (#995)
* feat(spawn): propagate a native W3C traceparent to spawned agents
Add a default-off capability that resolves one W3C traceparent for a task,
injects it into the agent's pane shell as the TRACEPARENT environment
variable immediately before launch, and records the identical value as
traceparent= in state/<id>.meta, so an external observer that explicitly
reads that env value or meta field can correlate a worker, a Secondmate, and
their nested children into one trace with no collector, storage, UI, or
vendor coupling.
TRACEPARENT as an environment variable is a firstmate convention carrying a
W3C-formatted value: W3C Trace Context standardizes the header, not an env
var, and OpenTelemetry SDKs do not read it automatically, so a downstream
must consume it deliberately; this feature parents no SDK span by itself.
Identity is per task, not per spawn: the carrier is minted with random ids on
the first spawn, adopted as a child (fresh span, same trace) for a nested
spawn whose parent already holds one, and reused verbatim from the meta on
relaunch, so a task keeps one stable logical identity across restarts. A
malformed or all-zero inherited value is treated as absent and roots a fresh
trace. A new root is sampled (01) - a sampling decision a downstream
parent-based sampler honors, not a guarantee that any collector stores a
span, and firstmate emits no spans; a child preserves the inherited flag.
Trust boundary: a firstmate-minted root is random and reads no prompt, path,
task prose, credential, or arbitrary environment key. An inherited
TRACEPARENT is opaque caller-controlled data - up to 24 bytes of id passed
through after syntax validation - so whoever set it controls those bytes, a
bounded fixed-width channel rather than a general content or secret channel.
The feature adds no OTEL_* variable, no tracestate, and no arbitrary
environment injection; it runs no configurable or arbitrary command, only the
fixed local od and tr (resolved from PATH) to read a few bytes of entropy - a
small local pipeline with no network or watchdog and no hard latency
guarantee. Any entropy or validation failure that returns omits the carrier
without aborting the spawn. A default-off spawn leaves the generated meta and
launch environment unchanged.
Enablement is default-off (config/trace-context, or FM_TRACE_CONTEXT where a
non-empty value overrides and unset or empty defers to the file) and is
propagated into secondmate homes, taking effect at each agent's next launch:
a Secondmate launched or relaunched after enablement carries the primary
trace into its nested workers, while an already-running Secondmate roots new
traces for its own workers until relaunched. Injection reuses the existing
GOTMPDIR channel, so all spawn backends and harnesses and the ship, scout,
and secondmate paths are covered.
Covered by a pure-library suite and a spawn-path integration test (fake tmux
plus a real worktree, hermetic against ambient FM_TRACE_CONTEXT) proving the
recorded and injected carriers are identical and sent before launch, that
default-off writes and injects neither, that a relaunch reuses the recorded
carrier, and that an explicit FM_TRACE_CONTEXT overrides the file both ways;
plus a source-owner inheritance test proving trace-context propagates and
absence-mirrors through propagate_inheritable_config.
Documentation follows the repository documentation-audiences contract:
docs/trace-context.md is maintainer-architecture rationale, the configuration
schema lives in docs/configuration.md, and the repeatable test evidence is
separated into docs/verification/trace-context.md (maintainer-verification),
registered in docs/documentation-audiences.json.
* fix(spawn): propagate the effective trace-context decision to secondmates
FM_TRACE_CONTEXT overrode trace context only in the process that read it. A
newly launched secondmate decided enablement from the inherited
config/trace-context file alone, so the override did not cross the
primary-to-secondmate boundary: FM_TRACE_CONTEXT=off with the file present left
the secondmate's nested workers traced (a broken kill switch), and
FM_TRACE_CONTEXT=on with the file absent left them untraced despite the
inherited carrier.
Deliver the primary's effective decision to a newly launched secondmate as a
normalized on/off FM_TRACE_CONTEXT in the launch prefix, so a FM_TRACE_CONTEXT
override governs the nested primary -> secondmate -> worker chain both ways, not
just the copied file. The value is bounded to the literal on/off and does not
broaden environment injection; the already-running secondmate boundary is
unchanged.
Add a genuine two-level spawn regression that drives fm-spawn twice with the
exact environment the primary injects into the secondmate and proves both
divergent directions end to end. Correct the documentation that implied
secondmate coverage on every backend, since orca and cmux reject secondmate
spawns, and refresh the verification evidence for the new assertion count.
* no-mistakes(review): Clarify Secondmate trace-context launch snapshots
* no-mistakes(document): Correct trace-context documentation ownership and relaunch semantics
* fix(spawn): resolve the trace-context decision once for carrier and snapshot
The effective trace-context decision was read twice per spawn: once inside
fm_trace_context_resolve for the recorded carrier, and again for the secondmate
FM_TRACE_CONTEXT launch snapshot. A config-file change between the two reads
could pair a carrier with the opposite enable state - an injected carrier with
an off snapshot, or no carrier with an on snapshot.
Freeze the effective on/off decision once, drive the carrier resolution under
that frozen FM_TRACE_CONTEXT so it cannot independently re-read the file, and
reuse the same frozen decision for the secondmate launch snapshot. Add a
spawn-path regression that drives the file-decided path and proves the recorded
carrier and the delivered snapshot always agree, and refresh the verification
evidence for the new assertion count.
* no-mistakes(review): Preserve legacy Secondmate trace boundary
* no-mistakes(document): Correct trace-context verification comparison base
* no-mistakes(review): Captain, prevent failed trace delivery metadata claims
* no-mistakes(review): Captain, align trace-context tests and verification evidence
* no-mistakes(document): Correct trace-context verification evidence
* no-mistakes(lint): Suppress intentional ShellCheck literal-dollar warnings
* no-mistakes(review): Captain: freeze trace context at session start
* no-mistakes(test): Captain: stabilize scheduler test and document Kimi trace coverage
* no-mistakes(document): Document trace-context safety boundaries
* fix(trace): fail off on stale session snapshots
Publish each home session decision atomically through a same-directory temporary file and bind it to the current session lock. A replacement failure can no longer leave an earlier on decision active in a later session; missing, stale, malformed, or unpublishable state defaults safely to off.
Add regressions for read-only replacement and failed publication, update spawn and session-start fixtures for the lock-bound format, and refresh the architecture and verification records.
* no-mistakes(review): Fix trace spawn failure independence and duplicate safety
* no-mistakes(document): Refresh trace-context documentation and verification
* no-mistakes(review): Clear partial backend input after failed trace submission
* no-mistakes(review): Stop unsafe trace delivery before launch append
* no-mistakes(document): Document unsafe trace delivery handling
* fix(trace): bound each trace to one routed task, never the routing agent
A persistent Secondmate holds its launch-time TRACEPARENT in the process
environment for its whole life, and routed requests never replace it, so
resolving new-task carriers from the ambient environment chained every
routed task into one ever-growing trace per Secondmate with distinct
parent ids. Resolve now reuses the task's recorded carrier or mints a
fresh sampled root, never reading ambient TRACEPARENT, so each routed
task is its own trace boundary while relaunch, recovery, and
scout-to-ship promotion keep one stable per-task identity.
The spawn regression models the reviewed scenario exactly: two unrelated
tasks spawned sequentially through one persistent Secondmate environment
record and inject distinct trace ids, adopt nothing from the Secondmate's
carrier, and a relaunch of the first task reuses its original carrier
verbatim.
* docs(trace): define the per-task trace boundary
The design contract is one task per trace: a persistent Secondmate is
routing infrastructure with its own agent identity, never a shared trace
root for the unrelated tasks routed through it. Root/recovery semantics
replace the removed child-inheritance path, the sampling and safety
sections drop inherited-carrier language because ambient TRACEPARENT is
never read, and the verification page records the refreshed suite
inventories including the two-task Secondmate boundary regression.
* test(trace): adopt the explicit per-task delivery contract in spawn fixtures
Rebasing onto current main brings the explicit per-task delivery contract:
ship spawns now require --mode and --yolo instead of resolving them from the
project registry. The trace spawn fixtures pass the same explicit contract
canonical spawn tests use, preserving the per-task trace boundary coverage
unchanged, and the verification page records the refreshed comparison base.
* fix(bin): harden tmux agent liveness across harnesses (#1577)
* fix(bin): classify tmux agent liveness independent of process titles
`fm_backend_tmux_agent_state` attributed a pane solely from
`#{pane_current_command}`, which is a process TITLE a harness can rewrite,
not a structural fact. Claude Code 2.1.220 reports its version string there,
so a live Claude endpoint classified `ambiguous`: the session-start secondmate
liveness sweep could no longer see it, and any consumer that gates on a
positive classification refuses outright.
Read a second, independent name source: the kernel `comm` of every process in
the pane tty's foreground process group. Either source naming a verified
harness yields `alive`, because a false `dead` is the one verdict that can
start a duplicate agent on a live worktree. Scoping to the foreground process
group rather than the pane's descendants keeps a harness-named background
process from faking an agent, and covers multi-process launchers (the Pi
Launcher path) without a special case.
Verified on 2026-08-03 against all seven adapters running for real on tmux
3.6a / macOS 26.5.2 arm64: claude 2.1.220, codex-cli 0.146.0, opencode
1.18.11, pi 0.82.0, pi-signed 0.82.0, grok 0.2.118, kimi 0.31.1 all classify
`alive`, each attributed by a source independent of its title.
Two tests, because they fail for different reasons:
- tests/fm-tmux-agent-liveness.test.sh pins the logic with real processes and
no harness, so it runs everywhere CI runs tmux. It drives the two name
sources apart on purpose and asserts the divergence, so no case can go
quietly vacuous.
- tests/fm-harness-liveness-drift-live-e2e.test.sh relaunches every installed
harness and fails naming the harness and version when one stops being
attributed by a title-independent source.
AGENTS.md section 4 carries the resulting standing rule, and
firstmate-coding-guidelines owns how to satisfy it.
* no-mistakes: apply CI fixes
* docs: move the harness-dependent-check policy out of AGENTS.md
The standing rule was stated in AGENTS.md section 4 with the mechanics in
firstmate-coding-guidelines, which split one contract across two owners and
charged every session for a rule that only fires when firstmate's own
harness-dependent code is being changed.
firstmate-coding-guidelines is now the single owner of both the rule and how
to satisfy it: real-harness proof required, that proof authorized to spend
tokens, structural signals preferred over vendor-rendered surfaces, and a
guard that fails loudly naming the harness and version where a surface signal
is unavoidable. No inline stub is left behind, because AGENTS.md already
carries the load trigger for that skill in sections 7 and 13, so it is read
before any change to firstmate's shared tracked material.
Also records the cross-platform lesson the pipeline caught in the portable
regression, and corrects that file's header: the divergence assertion lives
on the version-string case, which diverges on both supported platforms,
rather than on every case.
* no-mistakes(review): Harden tmux liveness identity and drift validation
* no-mistakes(document): Clarify cross-platform tmux liveness documentation
* feat(bin): propagate trace context to remote secondmates (#1609)
* feat(bin): trace remote secondmate routes and unify the inherit allowlist
Per-task W3C trace context (#995) resolved and injected its carrier only at
the local spawn path. A remote secondmate is routed through
spawn_remote_secondmate, which returns long before that site and wrote its own
metadata block, so a remote secondmate stayed silently untraced even with the
capability enabled.
The parent home still owns that task's identity, because it holds the metadata
an observer reads. It now resolves the carrier against the task's own meta
under its own frozen decision - reused verbatim on relaunch, freshly rooted
otherwise, never adopting the parent process's ambient TRACEPARENT - and hands
it to the configured host through a new fm-spawn --traceparent argument,
accepted only for a secondmate launch and only as a strict W3C value. The
remote host exports it at the same unconditional pre-launch site and reports
back the carrier its endpoint actually holds, which the parent records, so an
already-alive endpoint reports the identity its agent really received rather
than one the parent merely intended. Disabled remains byte-identical and off.
The remote inherit path also carried its own hardcoded copy of the inheritable
config set, already drifted from FM_INHERITABLE_CONFIG by trace-context. Both
remote ends now derive from that one declaration, so a future item cannot be
sent by one side and refused by the other, and session-scoped enablement items
are skipped on live convergence exactly as the local path skips them.
Also fixes a latent stderr leak: an absent session lock printed a raw redirect
failure, which the new remote resolve site made visible.
Adds tests/fm-remote-secondmate-trace-context.test.sh, driving the real
parent -> fm-on -> remote entrypoint -> control -> remote fm-spawn chain over
the deterministic SSH boundary and reading the carrier back from the remote
pane's own log.
* no-mistakes(document): Clarify remote trace and allowlist contracts
* feat(bin): preflight remote runtime tool paths (#1623)
* feat(bin): widen the remote runtime PATH and add a remote doctor preflight
The fixed remote entrypoint hard-coded a four-directory PATH, so a remote
account whose tools live under nix or a per-user profile could not run basic
Firstmate work without a login shell. The entrypoint now composes its child
PATH from the code root's bin, the account's ~/.local/bin, the common
package-manager directories that actually exist on the host, and the portable
system tail, deduplicated and in a fixed order, still under env -i with the
same variable allowlist and no shell command string.
fm-remote-doctor.sh reports that exact PATH by inheriting it from its own
entrypoint launch rather than recomposing it, so the ordering keeps one owner.
It is read-only, reports where each required and optional tool resolved, and
exits non-zero naming every required tool that did not. Remote seeding runs it
as a preflight before anything is created on the host and restores the registry
when it fails.
* no-mistakes(review): Harden remote git authorization and missing-tool diagnostics
* no-mistakes(document): Document remote PATH doctor and safe shims
* no-mistakes(lint): Fix ShellCheck findings in remote path tests
* no-mistakes(lint): Suppress exported fixture's false-positive ShellCheck warning
* feat: gate remote second mates on Herdr readiness (#1639)
* feat(bin): gate remote second mates on herdr readiness
A remote second mate now always runs on the Herdr backend, whose server
belongs to the host's GUI login session and therefore outlives the SSH
connections that supervise it. fm-spawn's remote route forces that backend
and the host-local control script refuses any other, so the requirement
cannot be dropped from either side.
fm-remote-doctor.sh becomes the single owner of what "ready" means. It keeps
its PATH and tool reporting from #1623 and adds the Herdr, Aqua LaunchAgent,
GUI-session, server-reachability, and entrypoint-symlink checks, tagging each
gap fixable: or human: with the exact operator step. --fix closes only the
automatable gaps - writing and loading the Aqua-scoped dev.firstmate.herdr
launch agent, starting the server where no launch agent applies, and
recreating the entrypoint symlink - then re-derives every check from the host,
so a human gap is never presented as fixed. It never creates a login session,
writes an auto-login password, or touches FileVault.
Remote seed, remote spawn, and the startup liveness relaunch all run the same
check, repair, re-check sequence through one shared library and fail closed
with the doctor's own gap text. Recovery inherits the gate because it respawns
through the same route.
Tests drive the real doctor against a controlled account fixture with a
private HOME, a state-backed launchctl, and a fake herdr, and prove the
dangerous actions are never attempted. The remote lifecycle suites gain a
stateful Herdr CLI fixture and answer the readiness gate at the SSH boundary,
so they never inspect or repair the runner's own account.
* no-mistakes(review): Validate launch-agent contract and confirm Herdr startup
* no-mistakes(review): Validate loaded launch-agent contract before readiness
* no-mistakes(review): Refuse legacy remote backends without altering routes
* no-mistakes(review): Clarify conditional remote readiness repair sequence
* no-mistakes(review): Repair remote readiness before liveness probing
* no-mistakes(review): Preserve unknown seeds and reject legacy liveness
* no-mistakes(document): docs: clarify remote Herdr backend ownership
* fix: isolate remote secondmates in shared Herdr session (#1659)
* Pin remote secondmates to fm-remote
* no-mistakes(review): Fail closed on legacy remote Herdr endpoints
* no-mistakes(review): Isolate fm-remote launch agent from interactive default
* no-mistakes(document): Document shared remote Herdr retirement safety
* feat: route remote commands through an Aqua job worker (#1660)
* feat: run remote commands through Aqua job worker
* no-mistakes(review): Enforce remote job deadlines and safe worker shutdown
* no-mistakes(review): Refresh stale workers and harden dependency-free supervision
* no-mistakes(review): Harden worker ownership recovery and shutdown quarantine
* no-mistakes(review): Fix doctor bootstrap, harness repair, and output draining
* no-mistakes(review): Probe doctor tools through authenticated worker bootstrap
* no-mistakes(review): Refresh stale workers before doctor tool probes
* no-mistakes(review): Recover stopped quarantines and extend job deadlines
* no-mistakes(review): Separate queue and execution timeout windows
* no-mistakes(review): Supervise Linux worker crashes and bind root identity
* no-mistakes(review): Resolve authorized Nix profile bin links
* no-mistakes(review): Clarify Nix path resolution documentation
* no-mistakes(review): Harden PATH safety and nvm selection
* no-mistakes(review): Honor nvm system defaults and refresh doctor digest
* no-mistakes(review): Keep workers ready during active jobs
* no-mistakes(review): Bound pre-execution validation by job timeout
* no-mistakes(document): Clarify remote worker documentation
* no-mistakes(lint): Fix remote worker ShellCheck diagnostics
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
* fix: clarify remote doctor bootstrap path (#1691)
* fix(bin): bound remote SSH dead-peer detection (#1699)
* fix(remote): arm SSH dead-peer detection in fm-on.sh
A vanished remote host mid-poll (a reboot, a dropped link) left ssh
blocked indefinitely on a half-open TCP connection, because fm-on.sh's
ssh invocation had no ServerAliveInterval/ServerAliveCountMax. This
wedged the remote-reply ferry: fm-procevent.sh's runner blocked inside
the ssh child and never reached its own no-result -> claim-release ->
reconcile re-arm self-healing path, which otherwise already handles a
nonzero exit with empty output correctly. Recovery required a manual
retire and re-arm.
Arm ServerAliveInterval=15 and ServerAliveCountMax=3 by default
(bounded ~45s detection window), both overridable via
FM_SSH_ALIVE_INTERVAL and FM_SSH_ALIVE_COUNT_MAX. This is a transport-
level fix in fm-on.sh, so it covers every remote command routed
through it, not just the reply ferry. The remote sshd answers
keepalive probes independently of whatever the remote command is
doing, so a legitimately long-but-alive command (a 55s poll, a clone,
the doctor) is never falsely killed - only a truly vanished peer trips
it, turning that case into a bounded, detectable ssh failure (exit
255) instead of an indefinite hang.
Extends tests/fm-on.test.sh with a behavioral regression asserting a
bounded, positive ServerAliveInterval/ServerAliveCountMax on the real
ssh argv captured through the FM_SSH_BIN process seam, plus coverage
that both are env-overridable.
* no-mistakes(document): Document SSH dead-peer detection ownership
* fix: report stale AXI tools during bootstrap (#1701)
* feat(bootstrap): gate stale axi CLIs at the floors firstmate actually uses
Add gh-axi 0.1.29 floor so bare --squash PR merges stop failing quietly on
older builds. Raise tasks-axi to FM_TASKS_AXI_MIN=0.2.2 (multi-id mv) while
keeping feature probes. Keep quota-axi at 0.1.16 after verifying schema 3
and per-model availability already ship there; runway remains optional.
* no-mistakes(document): Clarify AXI compatibility documentation ownership
* fix: prevent false watcher-down alarms in Claude sessions (#1661)
* fix(guard): stop false watcher-down alarm mid-turn under Claude auto-arm
bin/fm-guard.sh derived its watcher-health verdict from fm_watcher_healthy,
which requires a live watcher process holding the home lock. Under the Claude
Stop-hook auto-arm supervision model the watcher is armed at each turn end and
exits on its wake, so it runs only between turns. Every guarded command run
mid-turn therefore found no live watcher and printed the "WATCHER DOWN -
SUPERVISION IS OFF" banner even though supervision was healthy. Because the
episode key was derived from the beacon mtime (which the between-turns watcher
advances every poll), the full banner re-printed on essentially every command,
and the message always blamed a "fresh beacon" that was in fact fresh.
Make the pull guard's health check model-aware via a new
fm_watcher_supervision_verdict in bin/fm-wake-lib.sh:
- Under the auto-arm model a beacon fresh within FM_GUARD_GRACE is healthy even
with no live watcher process; only a beacon stale beyond grace (or absent) is
a genuine lapse and alarms.
- Under every persistent-watcher harness (codex foreground checkpoint,
opencode/pi/grok background arm, tmux, unknown) a live identity-matched
watcher with a fresh beacon is still required, unchanged.
The banner now names the true failing condition, a missing live watcher process
versus a genuinely stale beacon, instead of always blaming the beacon, and the
once-per-episode dedup keys on that condition rather than the beacon mtime so a
genuine lapse announces once and does not re-print each turn.
The turn-end guard keeps the strict fm_watcher_healthy check because it fires at
the turn boundary, where the auto-arm brings a fresh watcher up and it
cooperates with that arm. fm_watcher_healthy itself is unchanged, so the arm
layer's start/attach/replace decisions are unaffected.
Tests in tests/fm-guard-stale-banner.test.sh cover the auto-arm healthy
fresh-beacon-without-a-watcher case, the auto-arm stale-beacon alarm and its
stable episode, the true-reason banner wording, and the reason-keyed episode
surviving a beacon mtime change; existing persistent-model cases are pinned to
that model.
* no-mistakes(review): Pin secondmate supervision model to launched harness
* no-mistakes(document): Align watcher documentation with model-aware supervision health
* test: prevent fixture temporary directory leaks (#1704)
* fix(tests): stop fixture-tempdir helper from self-deleting under command substitution
fm_test_tmproot is almost always called as `TMP_ROOT=$(fm_test_tmproot prefix)`,
which forks a subshell to capture its stdout. The old implementation set its
EXIT cleanup trap inside that call, so the trap fired - and deleted the fixture
root - the instant the subshell exited, before the real caller's own EXIT trap
was ever installed. Every test using the documented call pattern leaked its
fixture root on every run; two suites had already independently discovered and
worked around this with ad-hoc mktemp calls.
Registration now goes through a $$-keyed registry file instead of in-process
state, since $$ resolves to the invoking shell's PID even inside the
subshell. The real cleanup trap is armed once at source time (always the real
caller, never a subshell) for EXIT, INT, and TERM. A best-effort orphan sweep
on next source reaps marked fixture roots old enough to be from a killed prior
run.
Simplifies the two existing ad-hoc workarounds (fm-procevent.test.sh,
wake-helpers.sh) back onto the shared helper now that it works correctly.
* no-mistakes(review): Preserve live fixtures during orphan reaping
* no-mistakes(review): Harden fixture ownership against PID reuse
* no-mistakes(review): Secure cleanup registry against path precreation
* no-mistakes(review): Make fixture registration transactional
* no-mistakes(document): Documentation already matches fixture cleanup behavior
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
* feat(herdr): enable presentation spaces by default (#1708)
* feat(herdr): default presentation spaces on with an explicit opt-out
Herdr's disposable one-task presentation workspace was opt-in through the
presence of local config/herdr-presentation-spaces. It is now on by default,
and a home opts out by writing "off" into that same file.
Values are read with the whole-file whitespace-stripped convention the other
scalar config items already use, plus case folding. An absent file, an empty
file, and "on" all resolve on; only "off" opts out; an unrecognized value warns
and keeps the default rather than failing a spawn over a purely visual setting.
The empty file is exactly the historical opt-in form, so every home that had
already enabled the projection stays enabled with no migration step, and no
previously enabled home can be turned off by the flip.
Because absence now means on at both ends, secondmate inheritance needs no
item-specific convergence: mirroring an absent primary file converges a
secondmate to the same default-on rather than turning its projection off, and
only an explicit primary opt-out propagates the opt-out.
The gate itself moves into fm_backend_herdr_presentation_enabled in the Herdr
adapter so the semantics have one owner that regressions can exercise directly.
* no-mistakes(document): Document Herdr default-on presentation safety
---------
Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
* fix(bin): surface fleet-wide open decisions on every wake drain (#1711)
* fix: surface consolidated open decisions on every wake-drain
A needs-decision or blocked event buried under later, unrelated status
appends was only ever shown via the last-line wake annotation, so a
still-open captain decision could go silently missed even though
status_open_decisions (fm-classify-lib.sh) already folds the whole
status stream correctly and fleet-snapshot/bearings already reuse it.
Wire that same fold into bin/fm-wake-drain.sh: a new fleet-wide
scan_open_decisions wrapper scans every state/<id>.status, and
fm-wake-drain.sh prints a separate, bounded OPEN DECISIONS section on
every drain (including the empty-queue fast path), so session-start
and every wake-handling turn surface it for free without duplicating
the open/resolved fold itself. Heartbeat wakes drain through the same
script, so this covers that surface too.
Also tighten status_open_decisions' file guard to skip an unreadable
status file instead of leaking a bash redirection error, now that a
fleet-wide directory scan can reach files a single targeted read
would not.
* no-mistakes(review): Prevent status symlinks leaking open decisions
* fix: drop unbounded perl subprocess from status symlink guard
The review step's own symlink-safety auto-fix (O_NOFOLLOW read via a
perl subprocess) forked one perl process per status file scanned by
the new fleet-wide open-decisions scan, with no cap - inflating
fm-wake-drain.sh's total external-read cost from 8 (the existing
annotation read_cap) to 18 in the enrichment-caps regression test.
The plain [ -L "$f" ] check already rejects any status file that is
itself a symlink before any read happens, which is exactly what the
new regression test exercises and is the same defense level the
sibling scan_captain_relevant_statuses/last_status_line already rely
on elsewhere in this file (no O_NOFOLLOW). Drop the subprocess-based
nofollow read and keep the cheap builtin guard.
* no-mistakes(document): Document actionable fleet-wide open decision drains
* fix(bin): abort parked runs and reap leaked processes before teardown (#1710)
* fix(bin): abort orphaned no-mistakes runs and reap leaked processes at teardown
Teardown could remove a task's worker while its no-mistakes pipeline run was
still parked at a gate, leaving an orphaned run holding a fleet slot
indefinitely (observed 2026-08-03: runs parked 7h39m and parked at a
post-CI approval gate). It could also leave backgrounded/disowned
descendant processes rooted under the worktree or tasktmp surviving
reparented to init (observed: two `go test` binaries pinning CPU for
hours with no live task meta to attribute them to).
Add two coupled pre-teardown steps, both scoped to this task's exact
branch/head or worktree/tasktmp so they can never touch another task's
run or processes:
- conclude_task_no_mistakes_run aborts a run parked at a gate via
`no-mistakes axi abort`, cd'd into the exact worktree so the daemon
resolves the run itself rather than teardown naming a --run id.
- reap_task_worktree_processes sweeps for processes whose cwd is under
the worktree or tasktmp (via `lsof -a -d cwd`) and TERM/KILLs them.
Both run before any worktree return, branch delete, or backend kill,
and are idempotent on a retried teardown. The branch+head attribution
logic is factored out of bin/fm-crew-state.sh into the new shared
bin/fm-nm-run-lib.sh so both scripts use the same ownership contract.
* no-mistakes(review): Fail closed on incomplete teardown cleanup
* no-mistakes(review): Bind teardown cleanup to verified run and process identities
* no-mistakes(review): Require confirmed aborts and convergent identity-safe process reaping
* no-mistakes(review): Handle process exits during teardown identity checks
* no-mistakes(review): Restore teardown library in hermetic gotmp fixtures
* no-mistakes(document): Document teardown run attribution and timeout
* no-mistakes(lint): Rename shell variable conflicting with done keyword
* no-mistakes: apply CI fixes
* fix: resolve fm-remote-entrypoint.sh SCRIPT_DIR through a PATH symlink (#1709)
The script installs as a symlink under ~/.local/bin. Taking dirname of
the symlink itself (instead of its real target) pointed SCRIPT_DIR at
~/.local/bin, breaking sourcing of the sibling fm-remote-job-lib.sh.
Resolve the real path first, preferring python3's os.path.realpath,
then realpath, falling back to the raw BASH_SOURCE on hosts with
neither.
* fix(pi): gate Calm built-in overrides by activation state (#1724)
* fix(pi): stop Calm claiming a built-in tool name another extension owns
fm-calm.ts claimed bash/read/edit/write/grep/find/ls unconditionally at
extension load, regardless of whether Calm was on. Pi resolves two
extensions registering the same built-in name by first-registered-wins
with no merge and no unregister call, and Calm's project-local
.pi/extensions/ position beats any global or CLI-configured extension,
so a user who never even enabled Calm could have their own bash/read/etc
override silently replaced.
Captain-approved plan implemented:
- Registration is now gated on config/calm already being "on" at load
time. A Calm-off session or reload registers nothing, so a non-Calm
user never contests a name. This stays synchronous during the
factory's own load, not deferred to session_start: /reload (and
ctx.newSession/fork/switchSession) render the restored transcript from
a pre-session_start snapshot of the tool registry, so a deferred claim
would miss that render - confirmed by tests/fm-calm-pi-extension
.test.sh's hidden-block-geometry E2E when trialed.
- The first time Calm turns on in a session that started off
(activateBuiltInsIfNeeded, from the /calm command handler), Calm calls
pi.getAllTools() - safe only once every extension has finished loading,
unlike the load-time path above - to see whether a different extension
already owns a name, and skips claiming only that one, leaving it and
its owning extension fully intact and callable.
- A contested name found this way prints a prominent ctx.ui.notify()
warning naming the tool, plus a console diagnostic.
- reportBuiltInLosses() remains the backstop for the one case neither of
the above can reach: a session that starts or reloads with Calm already
on, where the registry snapshot is taken before Calm gets any chance to
check ownership. A symlink-safe realpath comparison avoids misreporting
Calm's own registration as foreign when its path crosses a symlink
(macOS /tmp, /var).
Confirmed, bounded trade-off: the very first time a session that started
Calm-off turns Calm on, tool-call rows already on screen from before that
toggle do not retroactively collapse, because Pi never lets an extension
re-point an already-rendered row at a definition registered later. Every
session after that first toggle starts with the preference already on and
takes the synchronous load-time path, so the guarantee is intact from
then on. docs/calm.md and the file's own header document this in full.
tests/fm-calm-pi-extension.test.sh gains test_builtin_gate_load_time
(config/calm off registers nothing, on registers all 7 synchronously at
load) and test_calm_activation_collision_and_regression_bound (first
activation claims every uncontested built-in, leaves a foreign bash tool
fully intact and callable, warns and logs the contested name, and locks
in the documented pre-activation bound against real ToolExecutionComponent
rendering). test_rendering_and_session_lifecycle and the live interactive
E2E are updated for the new gate-at-load and first-activation-bound
contract.
* no-mistakes(document): Document Calm tool collision boundaries
* no-mistakes: apply CI fixes
* fix(bin): persist secondmate parent bindings for cleanup (#1727)
* fix(bin): give secondmate homes a durable parent binding record
Finished-worker cleanup on a remote second mate refused forever with
"cannot resolve the primary home ... durable parent binding". The
remote launch hands the child the remote code checkout as its parent
home (fm-spawn.sh's sole writer of FM_PUBLIC_FOLLOWUP_PRIMARY_HOME
receives FM_HOME=$FM_ROOT from fm-remote-secondmate-control.sh's
host-local launch), and that path can never carry the parent's real
records, so the guard refused unconditionally once relay looked active
anywhere on that host.
fm-home-seed.sh and fm-remote-home-provision.sh now write a durable
.fm-secondmate-parent record next to the .fm-secondmate-home identity
marker, naming the home's route to its parent as local (with the real
parent path) or remote (with the parent's SSH alias for diagnostics
only). fm-teardown.sh's cleanup gate reads it: a remote parent is out
of scope for the delegated-promise check (the whole promised-public-
reply subsystem is same-filesystem by construction, so a remote parent
can never hold one), while a token committed directly to the child's
own .env file - never the process environment - still refuses, so an
unrelated export in the remote host's login shell can no longer mask
in. For a local secondmate, the durable parent_home now also backs up
the launch-time env var, closing a silent fail-open where a restart
that dropped the launch prefix made the guard treat a genuinely active
parent relay as off.
Regression coverage drives the real remote route (SSH boundary + Herdr
fixture) and real fm-home-seed.sh seeding rather than hand-crafted
markers.
* no-mistakes(review): Captain: fail closed on unsafe durable parent records
* no-mistakes(review): Captain: enforce durable parent binding commit protocol
* no-mistakes(review): Captain: publish local parent binding before identity
* no-mistakes(review): Captain: refuse conflicting local parent bindings
* no-mistakes(review): Captain: reject non-regular secondmate seed leaves
* no-mistakes(review): Captain: enforce unique durable parent bindings
* no-mistakes(review): Captain: reject route-incompatible durable parent fields
* no-mistakes(document): Document durable secondmate parent bindings
* no-mistakes(lint): Fix secondmate parent parser ShellCheck warnings
* no-mistakes: apply CI fixes
* feat(bin): enforce latest AXI-family tool floors (#1733)
* feat(bin): gate lavish-axi at its session_ended floor in bootstrap
bin/fm-procevent-lavish.sh decides that a human "Send & End" review is
terminal by reading session_ended from the poll response's leading session
block. That field first shipped in lavish-axi 0.1.35, so an older installed
build silently leaves every ended review source armed forever and captures
an empty ended result on each later cycle. The same release is what makes a
plain reopen refuse a session the human deliberately ended.
Add LAVISH_AXI_MIN=0.1.35 to the existing axi-family floor structure in
bin/fm-bootstrap.sh, reusing tool_version_at_least and the same MISSING
diagnostic gh-axi already emits, so an incompatible build is reported as an
upgrade request before any review surface is armed. Later lavish-axi
releases only add artifact-authoring surface the adapter never reads, so
the floor is the feature-introduction point rather than latest.
Fixtures that stubbed lavish-axi as a bare exit-0 tool would now be read as
unparseable builds, so tests/lib.sh gains fm_fake_version_tool and every
bootstrap-running suite uses it for lavish-axi.
* no-mistakes(review): Clarify lavish-axi version floor rationale
* no-mistakes: apply CI fixes
* feat(bin): set axi-family floors to current latest under the bump policy
The axi-family bootstrap floors are the CURRENT LATEST published version of
each tool, captain-bumped periodically to move the whole fleet onto the
newest axi tools. They are not the minimum feature-introduced version. The
earlier lavish-axi work set a feature-minimum floor, which is the opposite
of this policy, so replace it along with the older feature-minimum rationale
carried by tasks-axi and quota-axi.
State the policy explicitly in bin/fm-bootstrap.sh's header, which owns it,
and in each per-tool floor owner, so no future change argues a floor back
down to the earliest release that happens to satisfy some behavior. Remove
the lavish-axi session_ended and upstream-PR citation, the tasks-axi
multi-ID-mv minimum argument, and the quota-axi credential-source argument
as floor rationale; the tasks-axi feature probes remain as a separate
defense-in-depth concern.
Floors: lavish-axi 0.1.45 (was 0.1.35), tasks-axi 0.2.4 (was 0.2.2),
quota-axi 0.1.17 (was 0.1.16), gh-axi 0.1.29 unchanged and already latest.
Each was verified against the tool's current published version.
The mechanism is unchanged: the same shared version helper and the same
MISSING diagnostic path. The below-fires and at-or-above-silent regression
rows move to the new floors, keeping each boundary genuine by pinning the
patch immediately below each floor rather than a version that was only
below the old one. Fleet fixtures move to the new floors so a bootstrap-
running suite is not reported as an out-of-date build.
Three operator-facing backlog handoff and receipt errors named "0.2.2+"
while the enforced floor moved, so they now point at the floor's owner
instead of duplicating a version number that drifts.
* no-mistakes(review): Centralize AXI floor policy beside constants
* no-mistakes(review): Clarify bootstrap boundary test comment
* no-mistakes(document): Centralize AXI floor policy rationale
* fix(bin): bound open decision scans with incremental cursors (#1737)
* fix(bin): bound OPEN DECISIONS scan cost with a per-status-file cursor
The fleet-wide OPEN DECISIONS scan added in #1711 re-reads and refolds
every task's entire lifetime status log on every drain, so its cost
grows unbounded with total log size. Add status_open_decisions_incremental
and scan_open_decisions_incremental to fm-classify-lib.sh: they persist a
per-status-file byte cursor plus the folded open-decision set, and fold
only newly appended bytes on each call, reusing status_open_decisions'
exact fold-line rule (extracted into _fm_decision_fold_line) so the two
strategies can never disagree on what is open. A missing or invalidated
cursor (new task, truncated/rewritten/shrunk log) falls back to a full
re-fold. bin/fm-wake-drain.sh now calls the incremental wrapper instead
of the whole-file scan.
* fix(bin): add O(1) rotation detection and read-failure guarding to the cursor fold
Add the two pieces the incremental open-decisions cursor was missing,
scoped to this repo's actual status-file usage (create-once, append-only,
never replaced or rewritten in place):
- An O(1) device+inode identity check (one stat call) alongside the
existing size-shrink check, so a status file replaced/rotated/recreated
at the same path is detected and falls back to a full re-fold, even
when the replacement is the same size. A same-inode, same-size,
in-place byte edit is a deliberately accepted gap: no code path in
this repo ever does that to a status file.
- Checked reads: a stat/wc/tail failure is a genuine I/O error, not
"the file is empty" - it now reports the already-trusted persisted
open set unchanged instead of risking a silent invalidation.
Both stay O(1) plus new bytes per call, matching the cursor's bounded-
cost design; no content hashing or pending-fragment machinery.
* no-mistakes(review): Preserve cursor state across failed incremental reads
* no-mistakes(review): Refold status when cursor cache reads fail
* no-mistakes(document): Document cursor-backed open-decision scanning
* no-mistakes: apply CI fixes
* fix(watch): stop alarming for panes that are parked, finished, or unprovable (#2)
Three separate paths turned normal supervision into captain-visible noise.
Each is fixed against evidence measured from a live home, and each suppression
is state-justified and self-clearing rather than a blanket mute.
1. False "watcher FAILED - cycle ended without an actionable reason".
Only the arm that forked a watcher receives that watcher's printed wake reason.
Every other arm attached to the same singleton sees just the lock, so when the
watcher delivered an actionable wake and exited, an attached arm observed only
"the holder went away" - and its successor cannot exist until the model's next
turn re-arms. That was reported as supervision being down: 93 of 102 attached
cycles in one live home ended that way, and all 43 of that home's watcher-FAILED
alarms were this same false line, none a real outage.
The durable wake queue is the evidence both arms share, and its monotonic
.wake-queue.seq survives drains. An attached arm now reprints a still-pending
delivery so the handling turn still happens, reports an already-drained one as a
handoff without inventing a second wake, and keeps the FAILED line for a cycle
that queued nothing.
2. A parked fleet re-alarming on pane churn.
The stale suppressor is the pane HASH, so a pane sitting on done: or
needs-decision: re-surfaced once per distinct hash with no cadence bound at all.
Any fleet-wide re-render - a resize, a workspace interaction, a new pane joining
the layout - invalidates every hash at once. Live: six parked panes changed hash
inside a ten-second window and six stale wakes followed over four minutes, each
one a full model turn carrying nothing the captain had not already been shown.
A pane parked on a result already delivered to firstmate is now absorbed and
re-surfaced only when that status line changes or once per
FM_TERMINAL_RESURFACE_SECS, the same treatment declared pauses already had.
Suppression is tied to the delivered status, never to the pane, so any new status
line surfaces at once and the heartbeat backstop still recovers a missed one.
3. Repeat wedge escalations on evidence nobody has.
fm-busy-lib.sh is explicit that missing, malformed, or unverified semantic data
is unknown and must never be promoted to either pole, but the stale path
collapsed unknown and a proven idle into one "not busy" boolean and repeated the
possible-wedge alarm on both at the same cadence. A worker whose harness has no
semantic turn source reports unknown for its whole turn, so a multi-minute build
climbed to demand-deep-inspection on the schedule a genuinely idle pane does.
Repeat escalations for a stale pane with no positive evidence now stretch by
FM_STALE_ESCALATE_UNKNOWN_MULT while its verdict is unknown. First surfaces are
untouched, the ladder still fires, and the provably-working timers keep the base
cadence so frozen-run detection is unchanged.
Measured on a synthetic fleet hit by one fleet-wide re-render, before -> after:
3 parked workers 3 -> 0 wakes, 8 parked workers 8 -> 0, 13 parked workers 13 -> 0.
Five regression tests, each sensitivity-proven to fail against the prior code.
---------
Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
Co-authored-by: Gavin <51008413+allstargg@users.noreply.github.com>
Co-authored-by: Christopher McKay <101884182+karotkriss@users.noreply.github.com>
Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
trillium
pushed a commit
to trillium/firstmate
that referenced
this pull request
Aug 6, 2026
…d#1563) * feat(bin): require an explicit ship delivery mode in fm-brief A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions. * feat(bin): require an explicit ship delivery contract at spawn and promotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode. * docs: record the explicit per-task delivery contract AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly. * test: pass ship delivery flags per call site in the Herdr launcher e2e The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper. * test: pass the ship delivery contract in the secondmate suites Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires.
sparkus
pushed a commit
to sparkus/firstmate
that referenced
this pull request
Aug 7, 2026
…d#1563) * feat(bin): require an explicit ship delivery mode in fm-brief A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions. * feat(bin): require an explicit ship delivery contract at spawn and promotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode. * docs: record the explicit per-task delivery contract AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly. * test: pass ship delivery flags per call site in the Herdr launcher e2e The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper. * test: pass the ship delivery contract in the secondmate suites Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires.
huynhtandat223
pushed a commit
to huynhtandat223/firstmate
that referenced
this pull request
Aug 7, 2026
…d#1563) * feat(bin): require an explicit ship delivery mode in fm-brief A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions. * feat(bin): require an explicit ship delivery contract at spawn and promotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode. * docs: record the explicit per-task delivery contract AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly. * test: pass ship delivery flags per call site in the Herdr launcher e2e The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper. * test: pass the ship delivery contract in the secondmate suites Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires.
Ospeto
added a commit
to Ospeto/firstmate
that referenced
this pull request
Aug 8, 2026
* fix(bin): preserve full task contract in no-mistakes intent (#1447)
* Preserve task contract in no-mistakes intent
* no-mistakes(review): Preserve complete current task contract in no-mistakes intent
* fix(bin): parse punctuated secondmate registry entries safely (#1452)
* fix: centralize secondmate registry parsing
* no-mistakes(review): Centralize secondmate registry binding validation
* no-mistakes(review): Harden registry EOF and symlink validation
* no-mistakes(review): Reject unreadable registries before parsing
* no-mistakes(document): Document punctuation-safe secondmate registry validation
* no-mistakes: apply CI fixes
* feat(bin): add durable process-event supervision (#1483)
* feat(procevent): supervise long-polling sources into durable events
Firstmate had no way to wait on a blocking external process without holding
a conversational turn. Add a domain-neutral process-to-event runner plus a
thin adapter around the currently published `lavish-axi poll` interface:
canonical physical source identity, one machine-wide owner per source, direct
argv execution, and durable 0600 result capture before any event referencing
it is published on the existing wake queue. No second notifier, no polling
control plane, and no retry machinery.
A captured result with no durable handled acknowledgement stays eligible for
bounded re-announcement across any number of drains and restarts. Draining a
wake before acting on it and then starting a replacement session resurfaces
the same exact source and sequence, and never puts result payload text in an
event line. `fm-procevent.sh handled <source-id> <sequence>` is the only thing
that stops re-announcement: generation-keyed, private, path-safe, durable, and
atomically idempotent, so a paired external effect gated on its first-time
versus repeat report is never authorized twice.
An acknowledgement is refused unless matching captured result and adapter
records already exist, so a premature or mistyped call cannot suppress a
future result.
The source side is unchanged and still lossy: the published poll clears
feedback destructively before returning it, so a result lost in that window
is unrecoverable. This is never at-least-once, no-loss, or lossless, and the
handled acknowledgement is not a generic exactly-once effect either - a crash
between an external effect and its acknowledgement can still repeat that
effect on replay.
Integrate registered sources with watcher supervision, the guards, and
recoverable secondmate teardown across nested homes, and cover source
identity, lifecycle races, supervision, restart handling, and cleanup safety
with regressions.
* no-mistakes(review): Prevent Lavish prompt text from spoofing missing sessions
* no-mistakes(review): Serialize publication and secure handled acknowledgements
* no-mistakes(document): Document hardened process-event acknowledgement guarantees
* fix(procevent): never reclaim a source whose owned group still runs
A runner is its own process group leader and starts the blocking source in
that group, but the claim records only the leader PID and its identity. If the
leader died while the source child kept running, the missing PID was
classified stale: reconciliation released the claim and started a second
runner while the old blocking source was still consuming the same canonical
source. For the Lavish adapter that means two destructive long polls racing on
one review session, so it is not harmless process litter. It also contradicted
the documented promise that ownership is never released until the whole group
is gone.
Ownership state now distinguishes a generation that is really gone from one
whose leader crashed with its group still alive. Reconcile stops that
surviving group and releases its exact generation before starting any
replacement, and keeps the claim for a later cycle when it cannot prove the
group stopped or another home owns it. Acquisition and `start` treat the same
state as held rather than reclaimable.
Signalling that group is safe precisely because only an absent leader reaches
this state. A reused PID leaves the leader alive, so the identity comparison
still classifies it stale or uncertain and no group signal follows, which
keeps the existing PID-reuse refusal intact.
Add a public-interface regression for the exact crash cut - SIGKILL only the
leader, prove the child group survives, reconcile, and prove the old group is
gone with no second source running - plus its counterexample that a generation
with no leader and no surviving group is still reclaimed. Update the runner
help, operating documentation, skill, and verification record where they
described reclaim in terms of the leader alone.
* no-mistakes(review): Enforce runner group ownership and detect poller overlap
* no-mistakes(review): Isolate runner groups from unrelated caller processes
* no-mistakes(document): Document isolated process-event runner launch
* no-mistakes(lint): Suppress Perl literal ShellCheck false positive
* fix(bin): retire terminal process events and surface queued wakes (#1500)
* fix(bin): deliver process-event results and retire ended sources
Two defects reproduced during a real Lavish adapter session.
One human `Send & End` produced four captured results: the real feedback,
then recurring empty ended sessions. The generic runner had no way to learn
a source was finished, so every reconcile restarted a poll that returned
immediately. The runner now asks the source's own adapter -
`fm-procevent-<adapter>.sh terminal <result-file>` - and on exit 0 alone
re-proves ownership, drops the registration, and releases its own claim
under one source boundary. Terminal knowledge stays adapter-owned: for
Lavish that is an ended session, a missing session, and the final feedback
delivery the published poll marks with `session_ended`. An adapter with no
terminal command keeps its source armed exactly as before. Capture before
publication, captured-result durability, queued wake durability, bounded
re-announcement, handled deduplication, one-owner ownership, and explicit
idempotent retirement are all unchanged.
A captured result queued its `check` wake durably, but a healthy watcher
with a fresh beacon never delivered it; the result surfaced only after a
manual drain. Publication happens outside the watcher (in the runner) or
unconditionally (in reconcile), so the watcher had no newly actionable
signal to report and never reached its rewake path. It now reports a
queued-but-unsurfaced process-event record through the same actionable exit
every other wake uses, deduplicated by the same `.seen-*` marker discipline
the signal scan uses, so the record is always durable before it is
suppressed. The durable queue remains the authority and no second notifier,
poller, timer, queue, or adapter-specific wake path is added.
Regressions cover both, driven end to end: an armed Lavish source against a
stand-in for the published poll polls once, captures once, publishes one
distinct event, and retires itself; two fixture adapters prove the terminal
decision follows the adapter alone; and a real capture plus a real watcher
prove one proactive wake before any drain, with no duplicate wake while the
record stays queued or after it is acknowledged.
* no-mistakes(review): Harden process-event retirement and proactive delivery
* no-mistakes(review): Route process-event delivery through shared wake owner
* no-mistakes(document): Clarify process-event delivery and retirement documentation
* no-mistakes(lint): Fix ShellCheck control-flow warnings
* no-mistakes(lint): Fix wake output status lint warning
* perf: shard portable serial tests across CI runners (#1544)
* perf(ci): shard the portable serial behavior lane across runners
The Behavior portable serial job ran all 69 scripts of the serial
remainder on one runner. The measured serial sum on run 30725985757 was
1143762 ms (19m04s) against a 20-minute timeout, so the job intermittently
reached the cap and was cancelled with every step passing. Setup is only
about 7s, so the cost is entirely test wall time.
Split the lane into four separate-runner shards. Each shard is still
strictly serial, and separate runners mean no two of these stateful
scripts ever share a machine, so the split needs no concurrency isolation
proof. Assignment is longest-processing-time bin packing over measured
per-script duration hints, balancing every shard to 285941 ms (~4m46s) of
expected work, and the timeout tightens from 20 to 15 minutes.
bin/fm-test-run.sh owns the shard count and refuses a lane whose "ofN"
disagrees with it, while ci.yml derives the same count from
strategy.job-total rather than a literal, so changing it in either file
alone fails the lane loudly instead of leaving part of the required suite
unrun. --check-coverage additionally proves the shards are non-empty,
disjoint, and exactly equal to the serial lane. No test is weakened,
skipped, or removed.
Also replace the wall-clock sleeps in the --jobs scheduler test fixture
with an explicit signal handshake between the fixtures. The old
0.5s-versus-0.05s race failed on a loaded machine; the handshake passes
under sustained CPU saturation.
* no-mistakes(review): Correct portable serial shard balance evidence
* no-mistakes(document): Document portable serial shard evidence accurately
* fix(bin): correct session lock and attached watcher supervision (#1545)
* fix(bin): identify harness sessions by path and report delivered wakes
Two supervision faults, both reported by a contributor and both open on the
default branch.
Fault 1: the Stop auto-arm never claims the home. fm_harness_ancestry_pid()
matched only the basename of `ps -o comm=`, and Claude Code's native installer
names the per-session executable by its version (.../share/claude/versions/
2.1.220), so that basename identifies nothing. Three real failure shapes follow:
a version-named session is missed entirely and the hook exits 0 with the epoch
never written (unconditional on Linux, where procps reports the kernel exec name
and ignores argv[0]); a claude-named daemon that directly parents sessions wins
the outermost-contiguous-claude rule ahead of the session itself; and a session
that is both version-named and daemon-parented has its live lock reclaimed as
stale and rewritten to the shared daemon pid, corrupting the home's ownership
record.
Harness identity now also reads whole components of the executable path and of
argv[0], which is what both platforms still carry. Matching whole components
only keeps that widening safe: bin/fm-claude-stop-autoarm.sh and ~/.claude/hooks
scripts have no "claude" component. Ownership is then decided against the
session's whole contiguous harness ancestry rather than one chosen pid, which is
the honest form of the question the library already documents ("does the current
process descend from that same harness?"). That subsumes the outermost-pid rule
for Claude's nested bg-spare worker chain instead of reverting it, and lets a
daemon-parented session recognize its own lock. Lock acquisition still writes the
outermost pid of the run, the only pid that lives as long as the session.
Fault 2: an attached arm reports a delivered cycle as FAILED. The watcher prints
its one reason line to its own stdout, so only the arm that forked it can read
that line; an arm that attached observes nothing but a released lock and called a
completely successful cycle "cycle ended without an actionable reason". No
supervision event was lost - the durable queue held it - but every harness
protocol reads that line as "supervision is down" and directs a manual re-arm.
The arm now resolves an unobservable close against the durable wake queue, which
records every wake before the watcher prints it and whose sequence counter never
rewinds, not even across a drain. A cycle the queue proves delivered a wake
reports that wake and exits 0; a cycle whose records a handling turn already
drained reports the delivery without inventing a reason line; only a cycle that
delivered nothing is still the typed nonzero failure. Fixing it in the arm covers
codex, opencode, pi, grok and kimi, not just the Claude Stop path.
Regressions: tests/fm-session-lock-ancestry.test.sh pins both platforms' ps
semantics behind a deterministic process table and runs the real Stop auto-arm in
version-named, daemon-parented, and combined real process trees, each orphaned so
the walk cannot escape the fixture. tests/fm-watch-arm.test.sh drives a real
watcher and a real attached arm through a real wake. Every fault case fails on
the previous code.
* no-mistakes(review): Bind watcher delivery records to process identity
* no-mistakes(review): Return validated watcher identity atomically
* no-mistakes(review): Track watcher successors by PID and identity
* no-mistakes(document): Consolidate watcher arm-cycle documentation ownership
* fix(bin): harden Claude supervision auto-arm recovery (#1495)
* fix(supervision): harden Claude auto-arm failure handling
* no-mistakes(review): Guarantee automatic retry after Claude auto-arm failures
* no-mistakes(review): Gate attended fail-open on verified supervision failure
* no-mistakes(document): Document Claude auto-arm retry and guard scope
* no-mistakes: apply CI fixes
* fix(supervision): make Claude fail-open progression monotonic
* no-mistakes(review): Preserve auto-arm failure episodes until verified watcher recovery
* no-mistakes(review): Linearize auto-arm failure progression across existing locks
* no-mistakes(review): Linearize positive recovery across shared failure episode lock
* no-mistakes(review): Scope Claude recovery contention to Claude guard mode
* no-mistakes(document): Align supervision auto-arm documentation
* no-mistakes(review): Preserve actionable wakes despite healthy successors
* no-mistakes(document): Refresh supervision auto-arm documentation
* feat(bin): require an explicit per-task delivery contract (#1563)
* feat(bin): require an explicit ship delivery mode in fm-brief
A ship brief's definition of done was shaped by a silent per-project registry
lookup, so an adjusted brief and the task's recorded delivery could disagree and
no one had to decide anything per task.
fm-brief now requires --mode on ship scaffolds, validates it against the closed
set, refuses the conditional no-mistakes-prod-only registry policy as a task
mode, and records the choice as a fixed machine-readable "Delivery contract:
mode=<mode>" line that fm-spawn can check. --mode is refused on scout and
secondmate scaffolds, and --yolo is refused outright because the worker never
owns approval decisions.
* feat(bin): require an explicit ship delivery contract at spawn and promotion
fm-spawn resolved every ship and scout task's mode and yolo from the project
registry, so the delivery posture was never a per-task decision and could
contradict the brief the worker was about to follow.
fm-spawn now requires --mode and --yolo on ship spawns, validates both against
their closed sets, and reads the brief's recorded delivery contract line and
refuses a mismatch before any endpoint exists; a brief scaffolded before that
line existed warns once and launches on the flag. A batch carries one shared
contract that each pair still checks against its own brief. Scout and secondmate
spawns refuse the flags, and a scout now records no mode or yolo at all, which
teardown and the snapshot already tolerate. When the explicit mode carries less
rigor than the project's standing posture, a deviation notice is printed and the
spawn continues, so the registry stays advisory rather than an enforced default.
fm-promote requires the same two flags, because a scout carries no posture to
inherit, and writes them into the task record with the kind flip.
fm-project-mode keeps its one registry parser for the mechanical consumers that
have no task in hand, accepts the conditional no-mistakes-prod-only annotation
and maps it to its most rigorous leg for them, and grows --raw so the deviation
notice can tell a conditional policy apart from a flat mode.
* docs: record the explicit per-task delivery contract
AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at
intake, including the surface classification for a no-mistakes-prod-only project
and the unregistered-project fallback, and the project-management skill defines
that conditional policy as a registration-time posture with its defaults and
initialization consequences. The registry blurb, script table, and architecture
section follow: the registry records the captain's standing posture, and task
delivery is decided per task and passed explicitly.
* test: pass ship delivery flags per call site in the Herdr launcher e2e
The shared spawn helper also launches a secondmate, which refuses the flags, so
the contract belongs at each ship call site rather than inside the helper.
* test: pass the ship delivery contract in the secondmate suites
Both suites scaffold or spawn an ordinary ship task as the control case for a
secondmate assertion, so each needs the explicit contract the ship path now
requires.
* feat(bin): support remote secondmate homes (#1576)
* Add generic remote secondmate transport
* Add routed remote secondmate replies
* Add remote outbox backlog handoff
* Integrate remote secondmate lifecycle
* no-mistakes(review): Fix remote snapshot and handoff races
* no-mistakes(review): Serialize remote home provisioning transactions
* no-mistakes(review): Harden remote lifecycle transaction boundaries
* no-mistakes(review): Serialize remote lifecycle mutations and fail closed
* no-mistakes(review): Close remote lifecycle and file race windows
* no-mistakes(review): Serialize remote reply retirement and inheritance
* no-mistakes(review): Harden remote transfer integrity and recovery
* no-mistakes(review): Serialize remote respawn with registry retirement
* no-mistakes(document): Document remote bootstrap convergence accurately
* no-mistakes(document): Clarify skipped remote secondmate mutations
* no-mistakes(lint): Resolve remote script ShellCheck warnings
* no-mistakes: apply CI fixes
* feat: add per-task trace context propagation (#995)
* feat(spawn): propagate a native W3C traceparent to spawned agents
Add a default-off capability that resolves one W3C traceparent for a task,
injects it into the agent's pane shell as the TRACEPARENT environment
variable immediately before launch, and records the identical value as
traceparent= in state/<id>.meta, so an external observer that explicitly
reads that env value or meta field can correlate a worker, a Secondmate, and
their nested children into one trace with no collector, storage, UI, or
vendor coupling.
TRACEPARENT as an environment variable is a firstmate convention carrying a
W3C-formatted value: W3C Trace Context standardizes the header, not an env
var, and OpenTelemetry SDKs do not read it automatically, so a downstream
must consume it deliberately; this feature parents no SDK span by itself.
Identity is per task, not per spawn: the carrier is minted with random ids on
the first spawn, adopted as a child (fresh span, same trace) for a nested
spawn whose parent already holds one, and reused verbatim from the meta on
relaunch, so a task keeps one stable logical identity across restarts. A
malformed or all-zero inherited value is treated as absent and roots a fresh
trace. A new root is sampled (01) - a sampling decision a downstream
parent-based sampler honors, not a guarantee that any collector stores a
span, and firstmate emits no spans; a child preserves the inherited flag.
Trust boundary: a firstmate-minted root is random and reads no prompt, path,
task prose, credential, or arbitrary environment key. An inherited
TRACEPARENT is opaque caller-controlled data - up to 24 bytes of id passed
through after syntax validation - so whoever set it controls those bytes, a
bounded fixed-width channel rather than a general content or secret channel.
The feature adds no OTEL_* variable, no tracestate, and no arbitrary
environment injection; it runs no configurable or arbitrary command, only the
fixed local od and tr (resolved from PATH) to read a few bytes of entropy - a
small local pipeline with no network or watchdog and no hard latency
guarantee. Any entropy or validation failure that returns omits the carrier
without aborting the spawn. A default-off spawn leaves the generated meta and
launch environment unchanged.
Enablement is default-off (config/trace-context, or FM_TRACE_CONTEXT where a
non-empty value overrides and unset or empty defers to the file) and is
propagated into secondmate homes, taking effect at each agent's next launch:
a Secondmate launched or relaunched after enablement carries the primary
trace into its nested workers, while an already-running Secondmate roots new
traces for its own workers until relaunched. Injection reuses the existing
GOTMPDIR channel, so all spawn backends and harnesses and the ship, scout,
and secondmate paths are covered.
Covered by a pure-library suite and a spawn-path integration test (fake tmux
plus a real worktree, hermetic against ambient FM_TRACE_CONTEXT) proving the
recorded and injected carriers are identical and sent before launch, that
default-off writes and injects neither, that a relaunch reuses the recorded
carrier, and that an explicit FM_TRACE_CONTEXT overrides the file both ways;
plus a source-owner inheritance test proving trace-context propagates and
absence-mirrors through propagate_inheritable_config.
Documentation follows the repository documentation-audiences contract:
docs/trace-context.md is maintainer-architecture rationale, the configuration
schema lives in docs/configuration.md, and the repeatable test evidence is
separated into docs/verification/trace-context.md (maintainer-verification),
registered in docs/documentation-audiences.json.
* fix(spawn): propagate the effective trace-context decision to secondmates
FM_TRACE_CONTEXT overrode trace context only in the process that read it. A
newly launched secondmate decided enablement from the inherited
config/trace-context file alone, so the override did not cross the
primary-to-secondmate boundary: FM_TRACE_CONTEXT=off with the file present left
the secondmate's nested workers traced (a broken kill switch), and
FM_TRACE_CONTEXT=on with the file absent left them untraced despite the
inherited carrier.
Deliver the primary's effective decision to a newly launched secondmate as a
normalized on/off FM_TRACE_CONTEXT in the launch prefix, so a FM_TRACE_CONTEXT
override governs the nested primary -> secondmate -> worker chain both ways, not
just the copied file. The value is bounded to the literal on/off and does not
broaden environment injection; the already-running secondmate boundary is
unchanged.
Add a genuine two-level spawn regression that drives fm-spawn twice with the
exact environment the primary injects into the secondmate and proves both
divergent directions end to end. Correct the documentation that implied
secondmate coverage on every backend, since orca and cmux reject secondmate
spawns, and refresh the verification evidence for the new assertion count.
* no-mistakes(review): Clarify Secondmate trace-context launch snapshots
* no-mistakes(document): Correct trace-context documentation ownership and relaunch semantics
* fix(spawn): resolve the trace-context decision once for carrier and snapshot
The effective trace-context decision was read twice per spawn: once inside
fm_trace_context_resolve for the recorded carrier, and again for the secondmate
FM_TRACE_CONTEXT launch snapshot. A config-file change between the two reads
could pair a carrier with the opposite enable state - an injected carrier with
an off snapshot, or no carrier with an on snapshot.
Freeze the effective on/off decision once, drive the carrier resolution under
that frozen FM_TRACE_CONTEXT so it cannot independently re-read the file, and
reuse the same frozen decision for the secondmate launch snapshot. Add a
spawn-path regression that drives the file-decided path and proves the recorded
carrier and the delivered snapshot always agree, and refresh the verification
evidence for the new assertion count.
* no-mistakes(review): Preserve legacy Secondmate trace boundary
* no-mistakes(document): Correct trace-context verification comparison base
* no-mistakes(review): Captain, prevent failed trace delivery metadata claims
* no-mistakes(review): Captain, align trace-context tests and verification evidence
* no-mistakes(document): Correct trace-context verification evidence
* no-mistakes(lint): Suppress intentional ShellCheck literal-dollar warnings
* no-mistakes(review): Captain: freeze trace context at session start
* no-mistakes(test): Captain: stabilize scheduler test and document Kimi trace coverage
* no-mistakes(document): Document trace-context safety boundaries
* fix(trace): fail off on stale session snapshots
Publish each home session decision atomically through a same-directory temporary file and bind it to the current session lock. A replacement failure can no longer leave an earlier on decision active in a later session; missing, stale, malformed, or unpublishable state defaults safely to off.
Add regressions for read-only replacement and failed publication, update spawn and session-start fixtures for the lock-bound format, and refresh the architecture and verification records.
* no-mistakes(review): Fix trace spawn failure independence and duplicate safety
* no-mistakes(document): Refresh trace-context documentation and verification
* no-mistakes(review): Clear partial backend input after failed trace submission
* no-mistakes(review): Stop unsafe trace delivery before launch append
* no-mistakes(document): Document unsafe trace delivery handling
* fix(trace): bound each trace to one routed task, never the routing agent
A persistent Secondmate holds its launch-time TRACEPARENT in the process
environment for its whole life, and routed requests never replace it, so
resolving new-task carriers from the ambient environment chained every
routed task into one ever-growing trace per Secondmate with distinct
parent ids. Resolve now reuses the task's recorded carrier or mints a
fresh sampled root, never reading ambient TRACEPARENT, so each routed
task is its own trace boundary while relaunch, recovery, and
scout-to-ship promotion keep one stable per-task identity.
The spawn regression models the reviewed scenario exactly: two unrelated
tasks spawned sequentially through one persistent Secondmate environment
record and inject distinct trace ids, adopt nothing from the Secondmate's
carrier, and a relaunch of the first task reuses its original carrier
verbatim.
* docs(trace): define the per-task trace boundary
The design contract is one task per trace: a persistent Secondmate is
routing infrastructure with its own agent identity, never a shared trace
root for the unrelated tasks routed through it. Root/recovery semantics
replace the removed child-inheritance path, the sampling and safety
sections drop inherited-carrier language because ambient TRACEPARENT is
never read, and the verification page records the refreshed suite
inventories including the two-task Secondmate boundary regression.
* test(trace): adopt the explicit per-task delivery contract in spawn fixtures
Rebasing onto current main brings the explicit per-task delivery contract:
ship spawns now require --mode and --yolo instead of resolving them from the
project registry. The trace spawn fixtures pass the same explicit contract
canonical spawn tests use, preserving the per-task trace boundary coverage
unchanged, and the verification page records the refreshed comparison base.
* fix(bin): harden tmux agent liveness across harnesses (#1577)
* fix(bin): classify tmux agent liveness independent of process titles
`fm_backend_tmux_agent_state` attributed a pane solely from
`#{pane_current_command}`, which is a process TITLE a harness can rewrite,
not a structural fact. Claude Code 2.1.220 reports its version string there,
so a live Claude endpoint classified `ambiguous`: the session-start secondmate
liveness sweep could no longer see it, and any consumer that gates on a
positive classification refuses outright.
Read a second, independent name source: the kernel `comm` of every process in
the pane tty's foreground process group. Either source naming a verified
harness yields `alive`, because a false `dead` is the one verdict that can
start a duplicate agent on a live worktree. Scoping to the foreground process
group rather than the pane's descendants keeps a harness-named background
process from faking an agent, and covers multi-process launchers (the Pi
Launcher path) without a special case.
Verified on 2026-08-03 against all seven adapters running for real on tmux
3.6a / macOS 26.5.2 arm64: claude 2.1.220, codex-cli 0.146.0, opencode
1.18.11, pi 0.82.0, pi-signed 0.82.0, grok 0.2.118, kimi 0.31.1 all classify
`alive`, each attributed by a source independent of its title.
Two tests, because they fail for different reasons:
- tests/fm-tmux-agent-liveness.test.sh pins the logic with real processes and
no harness, so it runs everywhere CI runs tmux. It drives the two name
sources apart on purpose and asserts the divergence, so no case can go
quietly vacuous.
- tests/fm-harness-liveness-drift-live-e2e.test.sh relaunches every installed
harness and fails naming the harness and version when one stops being
attributed by a title-independent source.
AGENTS.md section 4 carries the resulting standing rule, and
firstmate-coding-guidelines owns how to satisfy it.
* no-mistakes: apply CI fixes
* docs: move the harness-dependent-check policy out of AGENTS.md
The standing rule was stated in AGENTS.md section 4 with the mechanics in
firstmate-coding-guidelines, which split one contract across two owners and
charged every session for a rule that only fires when firstmate's own
harness-dependent code is being changed.
firstmate-coding-guidelines is now the single owner of both the rule and how
to satisfy it: real-harness proof required, that proof authorized to spend
tokens, structural signals preferred over vendor-rendered surfaces, and a
guard that fails loudly naming the harness and version where a surface signal
is unavoidable. No inline stub is left behind, because AGENTS.md already
carries the load trigger for that skill in sections 7 and 13, so it is read
before any change to firstmate's shared tracked material.
Also records the cross-platform lesson the pipeline caught in the portable
regression, and corrects that file's header: the divergence assertion lives
on the version-string case, which diverges on both supported platforms,
rather than on every case.
* no-mistakes(review): Harden tmux liveness identity and drift validation
* no-mistakes(document): Clarify cross-platform tmux liveness documentation
* feat(bin): propagate trace context to remote secondmates (#1609)
* feat(bin): trace remote secondmate routes and unify the inherit allowlist
Per-task W3C trace context (#995) resolved and injected its carrier only at
the local spawn path. A remote secondmate is routed through
spawn_remote_secondmate, which returns long before that site and wrote its own
metadata block, so a remote secondmate stayed silently untraced even with the
capability enabled.
The parent home still owns that task's identity, because it holds the metadata
an observer reads. It now resolves the carrier against the task's own meta
under its own frozen decision - reused verbatim on relaunch, freshly rooted
otherwise, never adopting the parent process's ambient TRACEPARENT - and hands
it to the configured host through a new fm-spawn --traceparent argument,
accepted only for a secondmate launch and only as a strict W3C value. The
remote host exports it at the same unconditional pre-launch site and reports
back the carrier its endpoint actually holds, which the parent records, so an
already-alive endpoint reports the identity its agent really received rather
than one the parent merely intended. Disabled remains byte-identical and off.
The remote inherit path also carried its own hardcoded copy of the inheritable
config set, already drifted from FM_INHERITABLE_CONFIG by trace-context. Both
remote ends now derive from that one declaration, so a future item cannot be
sent by one side and refused by the other, and session-scoped enablement items
are skipped on live convergence exactly as the local path skips them.
Also fixes a latent stderr leak: an absent session lock printed a raw redirect
failure, which the new remote resolve site made visible.
Adds tests/fm-remote-secondmate-trace-context.test.sh, driving the real
parent -> fm-on -> remote entrypoint -> control -> remote fm-spawn chain over
the deterministic SSH boundary and reading the carrier back from the remote
pane's own log.
* no-mistakes(document): Clarify remote trace and allowlist contracts
* feat(bin): preflight remote runtime tool paths (#1623)
* feat(bin): widen the remote runtime PATH and add a remote doctor preflight
The fixed remote entrypoint hard-coded a four-directory PATH, so a remote
account whose tools live under nix or a per-user profile could not run basic
Firstmate work without a login shell. The entrypoint now composes its child
PATH from the code root's bin, the account's ~/.local/bin, the common
package-manager directories that actually exist on the host, and the portable
system tail, deduplicated and in a fixed order, still under env -i with the
same variable allowlist and no shell command string.
fm-remote-doctor.sh reports that exact PATH by inheriting it from its own
entrypoint launch rather than recomposing it, so the ordering keeps one owner.
It is read-only, reports where each required and optional tool resolved, and
exits non-zero naming every required tool that did not. Remote seeding runs it
as a preflight before anything is created on the host and restores the registry
when it fails.
* no-mistakes(review): Harden remote git authorization and missing-tool diagnostics
* no-mistakes(document): Document remote PATH doctor and safe shims
* no-mistakes(lint): Fix ShellCheck findings in remote path tests
* no-mistakes(lint): Suppress exported fixture's false-positive ShellCheck warning
* feat: gate remote second mates on Herdr readiness (#1639)
* feat(bin): gate remote second mates on herdr readiness
A remote second mate now always runs on the Herdr backend, whose server
belongs to the host's GUI login session and therefore outlives the SSH
connections that supervise it. fm-spawn's remote route forces that backend
and the host-local control script refuses any other, so the requirement
cannot be dropped from either side.
fm-remote-doctor.sh becomes the single owner of what "ready" means. It keeps
its PATH and tool reporting from #1623 and adds the Herdr, Aqua LaunchAgent,
GUI-session, server-reachability, and entrypoint-symlink checks, tagging each
gap fixable: or human: with the exact operator step. --fix closes only the
automatable gaps - writing and loading the Aqua-scoped dev.firstmate.herdr
launch agent, starting the server where no launch agent applies, and
recreating the entrypoint symlink - then re-derives every check from the host,
so a human gap is never presented as fixed. It never creates a login session,
writes an auto-login password, or touches FileVault.
Remote seed, remote spawn, and the startup liveness relaunch all run the same
check, repair, re-check sequence through one shared library and fail closed
with the doctor's own gap text. Recovery inherits the gate because it respawns
through the same route.
Tests drive the real doctor against a controlled account fixture with a
private HOME, a state-backed launchctl, and a fake herdr, and prove the
dangerous actions are never attempted. The remote lifecycle suites gain a
stateful Herdr CLI fixture and answer the readiness gate at the SSH boundary,
so they never inspect or repair the runner's own account.
* no-mistakes(review): Validate launch-agent contract and confirm Herdr startup
* no-mistakes(review): Validate loaded launch-agent contract before readiness
* no-mistakes(review): Refuse legacy remote backends without altering routes
* no-mistakes(review): Clarify conditional remote readiness repair sequence
* no-mistakes(review): Repair remote readiness before liveness probing
* no-mistakes(review): Preserve unknown seeds and reject legacy liveness
* no-mistakes(document): docs: clarify remote Herdr backend ownership
* fix: isolate remote secondmates in shared Herdr session (#1659)
* Pin remote secondmates to fm-remote
* no-mistakes(review): Fail closed on legacy remote Herdr endpoints
* no-mistakes(review): Isolate fm-remote launch agent from interactive default
* no-mistakes(document): Document shared remote Herdr retirement safety
* feat: route remote commands through an Aqua job worker (#1660)
* feat: run remote commands through Aqua job worker
* no-mistakes(review): Enforce remote job deadlines and safe worker shutdown
* no-mistakes(review): Refresh stale workers and harden dependency-free supervision
* no-mistakes(review): Harden worker ownership recovery and shutdown quarantine
* no-mistakes(review): Fix doctor bootstrap, harness repair, and output draining
* no-mistakes(review): Probe doctor tools through authenticated worker bootstrap
* no-mistakes(review): Refresh stale workers before doctor tool probes
* no-mistakes(review): Recover stopped quarantines and extend job deadlines
* no-mistakes(review): Separate queue and execution timeout windows
* no-mistakes(review): Supervise Linux worker crashes and bind root identity
* no-mistakes(review): Resolve authorized Nix profile bin links
* no-mistakes(review): Clarify Nix path resolution documentation
* no-mistakes(review): Harden PATH safety and nvm selection
* no-mistakes(review): Honor nvm system defaults and refresh doctor digest
* no-mistakes(review): Keep workers ready during active jobs
* no-mistakes(review): Bound pre-execution validation by job timeout
* no-mistakes(document): Clarify remote worker documentation
* no-mistakes(lint): Fix remote worker ShellCheck diagnostics
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
* fix: clarify remote doctor bootstrap path (#1691)
* fix(bin): bound remote SSH dead-peer detection (#1699)
* fix(remote): arm SSH dead-peer detection in fm-on.sh
A vanished remote host mid-poll (a reboot, a dropped link) left ssh
blocked indefinitely on a half-open TCP connection, because fm-on.sh's
ssh invocation had no ServerAliveInterval/ServerAliveCountMax. This
wedged the remote-reply ferry: fm-procevent.sh's runner blocked inside
the ssh child and never reached its own no-result -> claim-release ->
reconcile re-arm self-healing path, which otherwise already handles a
nonzero exit with empty output correctly. Recovery required a manual
retire and re-arm.
Arm ServerAliveInterval=15 and ServerAliveCountMax=3 by default
(bounded ~45s detection window), both overridable via
FM_SSH_ALIVE_INTERVAL and FM_SSH_ALIVE_COUNT_MAX. This is a transport-
level fix in fm-on.sh, so it covers every remote command routed
through it, not just the reply ferry. The remote sshd answers
keepalive probes independently of whatever the remote command is
doing, so a legitimately long-but-alive command (a 55s poll, a clone,
the doctor) is never falsely killed - only a truly vanished peer trips
it, turning that case into a bounded, detectable ssh failure (exit
255) instead of an indefinite hang.
Extends tests/fm-on.test.sh with a behavioral regression asserting a
bounded, positive ServerAliveInterval/ServerAliveCountMax on the real
ssh argv captured through the FM_SSH_BIN process seam, plus coverage
that both are env-overridable.
* no-mistakes(document): Document SSH dead-peer detection ownership
* fix: report stale AXI tools during bootstrap (#1701)
* feat(bootstrap): gate stale axi CLIs at the floors firstmate actually uses
Add gh-axi 0.1.29 floor so bare --squash PR merges stop failing quietly on
older builds. Raise tasks-axi to FM_TASKS_AXI_MIN=0.2.2 (multi-id mv) while
keeping feature probes. Keep quota-axi at 0.1.16 after verifying schema 3
and per-model availability already ship there; runway remains optional.
* no-mistakes(document): Clarify AXI compatibility documentation ownership
* fix: prevent false watcher-down alarms in Claude sessions (#1661)
* fix(guard): stop false watcher-down alarm mid-turn under Claude auto-arm
bin/fm-guard.sh derived its watcher-health verdict from fm_watcher_healthy,
which requires a live watcher process holding the home lock. Under the Claude
Stop-hook auto-arm supervision model the watcher is armed at each turn end and
exits on its wake, so it runs only between turns. Every guarded command run
mid-turn therefore found no live watcher and printed the "WATCHER DOWN -
SUPERVISION IS OFF" banner even though supervision was healthy. Because the
episode key was derived from the beacon mtime (which the between-turns watcher
advances every poll), the full banner re-printed on essentially every command,
and the message always blamed a "fresh beacon" that was in fact fresh.
Make the pull guard's health check model-aware via a new
fm_watcher_supervision_verdict in bin/fm-wake-lib.sh:
- Under the auto-arm model a beacon fresh within FM_GUARD_GRACE is healthy even
with no live watcher process; only a beacon stale beyond grace (or absent) is
a genuine lapse and alarms.
- Under every persistent-watcher harness (codex foreground checkpoint,
opencode/pi/grok background arm, tmux, unknown) a live identity-matched
watcher with a fresh beacon is still required, unchanged.
The banner now names the true failing condition, a missing live watcher process
versus a genuinely stale beacon, instead of always blaming the beacon, and the
once-per-episode dedup keys on that condition rather than the beacon mtime so a
genuine lapse announces once and does not re-print each turn.
The turn-end guard keeps the strict fm_watcher_healthy check because it fires at
the turn boundary, where the auto-arm brings a fresh watcher up and it
cooperates with that arm. fm_watcher_healthy itself is unchanged, so the arm
layer's start/attach/replace decisions are unaffected.
Tests in tests/fm-guard-stale-banner.test.sh cover the auto-arm healthy
fresh-beacon-without-a-watcher case, the auto-arm stale-beacon alarm and its
stable episode, the true-reason banner wording, and the reason-keyed episode
surviving a beacon mtime change; existing persistent-model cases are pinned to
that model.
* no-mistakes(review): Pin secondmate supervision model to launched harness
* no-mistakes(document): Align watcher documentation with model-aware supervision health
* test: prevent fixture temporary directory leaks (#1704)
* fix(tests): stop fixture-tempdir helper from self-deleting under command substitution
fm_test_tmproot is almost always called as `TMP_ROOT=$(fm_test_tmproot prefix)`,
which forks a subshell to capture its stdout. The old implementation set its
EXIT cleanup trap inside that call, so the trap fired - and deleted the fixture
root - the instant the subshell exited, before the real caller's own EXIT trap
was ever installed. Every test using the documented call pattern leaked its
fixture root on every run; two suites had already independently discovered and
worked around this with ad-hoc mktemp calls.
Registration now goes through a $$-keyed registry file instead of in-process
state, since $$ resolves to the invoking shell's PID even inside the
subshell. The real cleanup trap is armed once at source time (always the real
caller, never a subshell) for EXIT, INT, and TERM. A best-effort orphan sweep
on next source reaps marked fixture roots old enough to be from a killed prior
run.
Simplifies the two existing ad-hoc workarounds (fm-procevent.test.sh,
wake-helpers.sh) back onto the shared helper now that it works correctly.
* no-mistakes(review): Preserve live fixtures during orphan reaping
* no-mistakes(review): Harden fixture ownership against PID reuse
* no-mistakes(review): Secure cleanup registry against path precreation
* no-mistakes(review): Make fixture registration transactional
* no-mistakes(document): Documentation already matches fixture cleanup behavior
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
* feat(herdr): enable presentation spaces by default (#1708)
* feat(herdr): default presentation spaces on with an explicit opt-out
Herdr's disposable one-task presentation workspace was opt-in through the
presence of local config/herdr-presentation-spaces. It is now on by default,
and a home opts out by writing "off" into that same file.
Values are read with the whole-file whitespace-stripped convention the other
scalar config items already use, plus case folding. An absent file, an empty
file, and "on" all resolve on; only "off" opts out; an unrecognized value warns
and keeps the default rather than failing a spawn over a purely visual setting.
The empty file is exactly the historical opt-in form, so every home that had
already enabled the projection stays enabled with no migration step, and no
previously enabled home can be turned off by the flip.
Because absence now means on at both ends, secondmate inheritance needs no
item-specific convergence: mirroring an absent primary file converges a
secondmate to the same default-on rather than turning its projection off, and
only an explicit primary opt-out propagates the opt-out.
The gate itself moves into fm_backend_herdr_presentation_enabled in the Herdr
adapter so the semantics have one owner that regressions can exercise directly.
* no-mistakes(document): Document Herdr default-on presentation safety
---------
Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
* fix(bin): surface fleet-wide open decisions on every wake drain (#1711)
* fix: surface consolidated open decisions on every wake-drain
A needs-decision or blocked event buried under later, unrelated status
appends was only ever shown via the last-line wake annotation, so a
still-open captain decision could go silently missed even though
status_open_decisions (fm-classify-lib.sh) already folds the whole
status stream correctly and fleet-snapshot/bearings already reuse it.
Wire that same fold into bin/fm-wake-drain.sh: a new fleet-wide
scan_open_decisions wrapper scans every state/<id>.status, and
fm-wake-drain.sh prints a separate, bounded OPEN DECISIONS section on
every drain (including the empty-queue fast path), so session-start
and every wake-handling turn surface it for free without duplicating
the open/resolved fold itself. Heartbeat wakes drain through the same
script, so this covers that surface too.
Also tighten status_open_decisions' file guard to skip an unreadable
status file instead of leaking a bash redirection error, now that a
fleet-wide directory scan can reach files a single targeted read
would not.
* no-mistakes(review): Prevent status symlinks leaking open decisions
* fix: drop unbounded perl subprocess from status symlink guard
The review step's own symlink-safety auto-fix (O_NOFOLLOW read via a
perl subprocess) forked one perl process per status file scanned by
the new fleet-wide open-decisions scan, with no cap - inflating
fm-wake-drain.sh's total external-read cost from 8 (the existing
annotation read_cap) to 18 in the enrichment-caps regression test.
The plain [ -L "$f" ] check already rejects any status file that is
itself a symlink before any read happens, which is exactly what the
new regression test exercises and is the same defense level the
sibling scan_captain_relevant_statuses/last_status_line already rely
on elsewhere in this file (no O_NOFOLLOW). Drop the subprocess-based
nofollow read and keep the cheap builtin guard.
* no-mistakes(document): Document actionable fleet-wide open decision drains
* fix(bin): abort parked runs and reap leaked processes before teardown (#1710)
* fix(bin): abort orphaned no-mistakes runs and reap leaked processes at teardown
Teardown could remove a task's worker while its no-mistakes pipeline run was
still parked at a gate, leaving an orphaned run holding a fleet slot
indefinitely (observed 2026-08-03: runs parked 7h39m and parked at a
post-CI approval gate). It could also leave backgrounded/disowned
descendant processes rooted under the worktree or tasktmp surviving
reparented to init (observed: two `go test` binaries pinning CPU for
hours with no live task meta to attribute them to).
Add two coupled pre-teardown steps, both scoped to this task's exact
branch/head or worktree/tasktmp so they can never touch another task's
run or processes:
- conclude_task_no_mistakes_run aborts a run parked at a gate via
`no-mistakes axi abort`, cd'd into the exact worktree so the daemon
resolves the run itself rather than teardown naming a --run id.
- reap_task_worktree_processes sweeps for processes whose cwd is under
the worktree or tasktmp (via `lsof -a -d cwd`) and TERM/KILLs them.
Both run before any worktree return, branch delete, or backend kill,
and are idempotent on a retried teardown. The branch+head attribution
logic is factored out of bin/fm-crew-state.sh into the new shared
bin/fm-nm-run-lib.sh so both scripts use the same ownership contract.
* no-mistakes(review): Fail closed on incomplete teardown cleanup
* no-mistakes(review): Bind teardown cleanup to verified run and process identities
* no-mistakes(review): Require confirmed aborts and convergent identity-safe process reaping
* no-mistakes(review): Handle process exits during teardown identity checks
* no-mistakes(review): Restore teardown library in hermetic gotmp fixtures
* no-mistakes(document): Document teardown run attribution and timeout
* no-mistakes(lint): Rename shell variable conflicting with done keyword
* no-mistakes: apply CI fixes
* fix: resolve fm-remote-entrypoint.sh SCRIPT_DIR through a PATH symlink (#1709)
The script installs as a symlink under ~/.local/bin. Taking dirname of
the symlink itself (instead of its real target) pointed SCRIPT_DIR at
~/.local/bin, breaking sourcing of the sibling fm-remote-job-lib.sh.
Resolve the real path first, preferring python3's os.path.realpath,
then realpath, falling back to the raw BASH_SOURCE on hosts with
neither.
* fix(pi): gate Calm built-in overrides by activation state (#1724)
* fix(pi): stop Calm claiming a built-in tool name another extension owns
fm-calm.ts claimed bash/read/edit/write/grep/find/ls unconditionally at
extension load, regardless of whether Calm was on. Pi resolves two
extensions registering the same built-in name by first-registered-wins
with no merge and no unregister call, and Calm's project-local
.pi/extensions/ position beats any global or CLI-configured extension,
so a user who never even enabled Calm could have their own bash/read/etc
override silently replaced.
Captain-approved plan implemented:
- Registration is now gated on config/calm already being "on" at load
time. A Calm-off session or reload registers nothing, so a non-Calm
user never contests a name. This stays synchronous during the
factory's own load, not deferred to session_start: /reload (and
ctx.newSession/fork/switchSession) render the restored transcript from
a pre-session_start snapshot of the tool registry, so a deferred claim
would miss that render - confirmed by tests/fm-calm-pi-extension
.test.sh's hidden-block-geometry E2E when trialed.
- The first time Calm turns on in a session that started off
(activateBuiltInsIfNeeded, from the /calm command handler), Calm calls
pi.getAllTools() - safe only once every extension has finished loading,
unlike the load-time path above - to see whether a different extension
already owns a name, and skips claiming only that one, leaving it and
its owning extension fully intact and callable.
- A contested name found this way prints a prominent ctx.ui.notify()
warning naming the tool, plus a console diagnostic.
- reportBuiltInLosses() remains the backstop for the one case neither of
the above can reach: a session that starts or reloads with Calm already
on, where the registry snapshot is taken before Calm gets any chance to
check ownership. A symlink-safe realpath comparison avoids misreporting
Calm's own registration as foreign when its path crosses a symlink
(macOS /tmp, /var).
Confirmed, bounded trade-off: the very first time a session that started
Calm-off turns Calm on, tool-call rows already on screen from before that
toggle do not retroactively collapse, because Pi never lets an extension
re-point an already-rendered row at a definition registered later. Every
session after that first toggle starts with the preference already on and
takes the synchronous load-time path, so the guarantee is intact from
then on. docs/calm.md and the file's own header document this in full.
tests/fm-calm-pi-extension.test.sh gains test_builtin_gate_load_time
(config/calm off registers nothing, on registers all 7 synchronously at
load) and test_calm_activation_collision_and_regression_bound (first
activation claims every uncontested built-in, leaves a foreign bash tool
fully intact and callable, warns and logs the contested name, and locks
in the documented pre-activation bound against real ToolExecutionComponent
rendering). test_rendering_and_session_lifecycle and the live interactive
E2E are updated for the new gate-at-load and first-activation-bound
contract.
* no-mistakes(document): Document Calm tool collision boundaries
* no-mistakes: apply CI fixes
* fix(bin): persist secondmate parent bindings for cleanup (#1727)
* fix(bin): give secondmate homes a durable parent binding record
Finished-worker cleanup on a remote second mate refused forever with
"cannot resolve the primary home ... durable parent binding". The
remote launch hands the child the remote code checkout as its parent
home (fm-spawn.sh's sole writer of FM_PUBLIC_FOLLOWUP_PRIMARY_HOME
receives FM_HOME=$FM_ROOT from fm-remote-secondmate-control.sh's
host-local launch), and that path can never carry the parent's real
records, so the guard refused unconditionally once relay looked active
anywhere on that host.
fm-home-seed.sh and fm-remote-home-provision.sh now write a durable
.fm-secondmate-parent record next to the .fm-secondmate-home identity
marker, naming the home's route to its parent as local (with the real
parent path) or remote (with the parent's SSH alias for diagnostics
only). fm-teardown.sh's cleanup gate reads it: a remote parent is out
of scope for the delegated-promise check (the whole promised-public-
reply subsystem is same-filesystem by construction, so a remote parent
can never hold one), while a token committed directly to the child's
own .env file - never the process environment - still refuses, so an
unrelated export in the remote host's login shell can no longer mask
in. For a local secondmate, the durable parent_home now also backs up
the launch-time env var, closing a silent fail-open where a restart
that dropped the launch prefix made the guard treat a genuinely active
parent relay as off.
Regression coverage drives the real remote route (SSH boundary + Herdr
fixture) and real fm-home-seed.sh seeding rather than hand-crafted
markers.
* no-mistakes(review): Captain: fail closed on unsafe durable parent records
* no-mistakes(review): Captain: enforce durable parent binding commit protocol
* no-mistakes(review): Captain: publish local parent binding before identity
* no-mistakes(review): Captain: refuse conflicting local parent bindings
* no-mistakes(review): Captain: reject non-regular secondmate seed leaves
* no-mistakes(review): Captain: enforce unique durable parent bindings
* no-mistakes(review): Captain: reject route-incompatible durable parent fields
* no-mistakes(document): Document durable secondmate parent bindings
* no-mistakes(lint): Fix secondmate parent parser ShellCheck warnings
* no-mistakes: apply CI fixes
* feat(bin): enforce latest AXI-family tool floors (#1733)
* feat(bin): gate lavish-axi at its session_ended floor in bootstrap
bin/fm-procevent-lavish.sh decides that a human "Send & End" review is
terminal by reading session_ended from the poll response's leading session
block. That field first shipped in lavish-axi 0.1.35, so an older installed
build silently leaves every ended review source armed forever and captures
an empty ended result on each later cycle. The same release is what makes a
plain reopen refuse a session the human deliberately ended.
Add LAVISH_AXI_MIN=0.1.35 to the existing axi-family floor structure in
bin/fm-bootstrap.sh, reusing tool_version_at_least and the same MISSING
diagnostic gh-axi already emits, so an incompatible build is reported as an
upgrade request before any review surface is armed. Later lavish-axi
releases only add artifact-authoring surface the adapter never reads, so
the floor is the feature-introduction point rather than latest.
Fixtures that stubbed lavish-axi as a bare exit-0 tool would now be read as
unparseable builds, so tests/lib.sh gains fm_fake_version_tool and every
bootstrap-running suite uses it for lavish-axi.
* no-mistakes(review): Clarify lavish-axi version floor rationale
* no-mistakes: apply CI fixes
* feat(bin): set axi-family floors to current latest under the bump policy
The axi-family bootstrap floors are the CURRENT LATEST published version of
each tool, captain-bumped periodically to move the whole fleet onto the
newest axi tools. They are not the minimum feature-introduced version. The
earlier lavish-axi work set a feature-minimum floor, which is the opposite
of this policy, so replace it along with the older feature-minimum rationale
carried by tasks-axi and quota-axi.
State the policy explicitly in bin/fm-bootstrap.sh's header, which owns it,
and in each per-tool floor owner, so no future change argues a floor back
down to the earliest release that happens to satisfy some behavior. Remove
the lavish-axi session_ended and upstream-PR citation, the tasks-axi
multi-ID-mv minimum argument, and the quota-axi credential-source argument
as floor rationale; the tasks-axi feature probes remain as a separate
defense-in-depth concern.
Floors: lavish-axi 0.1.45 (was 0.1.35), tasks-axi 0.2.4 (was 0.2.2),
quota-axi 0.1.17 (was 0.1.16), gh-axi 0.1.29 unchanged and already latest.
Each was verified against the tool's current published version.
The mechanism is unchanged: the same shared version helper and the same
MISSING diagnostic path. The below-fires and at-or-above-silent regression
rows move to the new floors, keeping each boundary genuine by pinning the
patch immediately below each floor rather than a version that was only
below the old one. Fleet fixtures move to the new floors so a bootstrap-
running suite is not reported as an out-of-date build.
Three operator-facing backlog handoff and receipt errors named "0.2.2+"
while the enforced floor moved, so they now point at the floor's owner
instead of duplicating a version number that drifts.
* no-mistakes(review): Centralize AXI floor policy beside constants
* no-mistakes(review): Clarify bootstrap boundary test comment
* no-mistakes(document): Centralize AXI floor policy rationale
* fix(bin): bound open decision scans with incremental cursors (#1737)
* fix(bin): bound OPEN DECISIONS scan cost with a per-status-file cursor
The fleet-wide OPEN DECISIONS scan added in #1711 re-reads and refolds
every task's entire lifetime status log on every drain, so its cost
grows unbounded with total log size. Add status_open_decisions_incremental
and scan_open_decisions_incremental to fm-classify-lib.sh: they persist a
per-status-file byte cursor plus the folded open-decision set, and fold
only newly appended bytes on each call, reusing status_open_decisions'
exact fold-line rule (extracted into _fm_decision_fold_line) so the two
strategies can never disagree on what is open. A missing or invalidated
cursor (new task, truncated/rewritten/shrunk log) falls back to a full
re-fold. bin/fm-wake-drain.sh now calls the incremental wrapper instead
of the whole-file scan.
* fix(bin): add O(1) rotation detection and read-failure guarding to the cursor fold
Add the two pieces the incremental open-decisions cursor was missing,
scoped to this repo's actual status-file usage (create-once, append-only,
never replaced or rewritten in place):
- An O(1) device+inode identity check (one stat call) alongside the
existing size-shrink check, so a status file replaced/rotated/recreated
at the same path is detected and falls back to a full re-fold, even
when the replacement is the same size. A same-inode, same-size,
in-place byte edit is a deliberately accepted gap: no code path in
this repo ever does that to a status file.
- Checked reads: a stat/wc/tail failure is a genuine I/O error, not
"the file is empty" - it now reports the already-trusted persisted
open set unchanged instead of risking a silent invalidation.
Both stay O(1) plus new bytes per call, matching the cursor's bounded-
cost design; no content hashing or pending-fragment machinery.
* no-mistakes(review): Preserve cursor state across failed incremental reads
* no-mistakes(review): Refold status when cursor cache reads fail
* no-mistakes(document): Document cursor-backed open-decision scanning
* no-mistakes: apply CI fixes
* fix(bin): prevent remote polls from blocking session startup (#1754)
* fix(bin): preempt remote reply long-polls for queued short jobs
Session start on a home with live remote second mates could stall silently
for many minutes: the single serial remote job worker ran each armed
fm-remote-delta-read.sh reply poll to its full 55s window while bootstrap's
short sync, inherit, state, and route commands sat queued behind it, and
non-FIFO queue pickup let re-armed polls keep winning the lane. Measured
end to end, a trivial short job took 31s behind one 30s poll window.
The worker now preempts a running preemptible job (the read-only, cursor-
anchored delta read is the only member of that class) as soon as a
non-preemptible job is queued, publishing exit 75 with emptied output -
byte-identical to the poll's own elapsed-window-with-no-data result - so
the parent runner takes its existing no-result path and the watcher re-arms
from the same cursor with nothing lost. The delta read translates SIGTERM
into that same exit after removing its staging directory. Sibling polls
never preempt each other, so two armed monitors cannot churn. The same
measured scenario now completes in 1s.
* no-mistakes(document): Clarify remote poll preemption documentation
* docs: present X mode as the X and Discord public surface (#1778)
Discord mentions already ride the same pairing-token opt-in, relay poll,
and platform-aware reply path as X mentions, but the docs still read as
X-only, so a stranger could not self-serve the Discord path.
Add the numbered turn-on steps to the X mode configuration reference,
pointing at the myfirstmate dashboard for account creation, bot install,
and token issuance rather than duplicating operator setup here, and drop
the X-only framing from the README bullet, the documentation index, and
the architecture overview.
* fix(bin): run session start deterministically from hooks (#1781)
* feat(bin): run session start deterministically on hook-capable harnesses
Session start relied on a native nudge that only asked the agent to run
bin/fm-session-start.sh, and an agent can defer that. Observed 2026-08-01:
an /ahoy-first session followed the recap path and did not take the helm
until a later request forced it.
Claude, Codex, and Pi now RUN the digest in their session-open hook through
the new bin/fm-sessionstart-run.sh, so the full ordered digest is in model
context before the first turn. That wrapper is the single owner of what a
session-open source means: startup and Pi's "new" take the helm, clear and
compact re-emit, resume/reload/fork delegate to the nudge, and an unreadable
source takes the helm because doing that redundantly is idempotent while
skipping it is the bug. Grok and OpenCode keep the nudge as the floor, since
neither can carry hook stdout into a model turn.
Because the hook now blocks session initialization, fm-session-start.sh
bounds itself first. Its steps are not all individually bounded - bootstrap's
gh auth probe, tool version probes, the backlog listing and per-task endpoint
reads are unbounded - so the whole digest runs as one bounded child (default
120s). Whatever it emitted before the bound survives, and the parent adds a
loud STARTUP TRUNCATED banner naming the stage that stalled and every stage
that never ran, still exiting 0.
--reemit skips only the sweeps startup already reconciled. It still re-verifies
lock ownership and still drains queued wakes, which arrived after startup and
are the turn's work. fm-bootstrap.sh gains FM_BOOTSTRAP_LOCKED so a re-emit
keeps repair ownership instead of deferring to a lock holder that is itself.
Also adds bin/fm-timeout-lib.sh as the single owner of bounded execution,
replacing three near-identical copies, and gives the ahoy skill a helm check
so a nudge-tier harness cannot recap before taking the helm.
Verified live on 2026-08-05 against Claude 2.1.222,…
This was referenced Aug 8, 2026
Ospeto
added a commit
to Ospeto/firstmate
that referenced
this pull request
Aug 9, 2026
* fix(bin): preserve full task contract in no-mistakes intent (#1447)
* Preserve task contract in no-mistakes intent
* no-mistakes(review): Preserve complete current task contract in no-mistakes intent
* fix(bin): parse punctuated secondmate registry entries safely (#1452)
* fix: centralize secondmate registry parsing
* no-mistakes(review): Centralize secondmate registry binding validation
* no-mistakes(review): Harden registry EOF and symlink validation
* no-mistakes(review): Reject unreadable registries before parsing
* no-mistakes(document): Document punctuation-safe secondmate registry validation
* no-mistakes: apply CI fixes
* feat(bin): add durable process-event supervision (#1483)
* feat(procevent): supervise long-polling sources into durable events
Firstmate had no way to wait on a blocking external process without holding
a conversational turn. Add a domain-neutral process-to-event runner plus a
thin adapter around the currently published `lavish-axi poll` interface:
canonical physical source identity, one machine-wide owner per source, direct
argv execution, and durable 0600 result capture before any event referencing
it is published on the existing wake queue. No second notifier, no polling
control plane, and no retry machinery.
A captured result with no durable handled acknowledgement stays eligible for
bounded re-announcement across any number of drains and restarts. Draining a
wake before acting on it and then starting a replacement session resurfaces
the same exact source and sequence, and never puts result payload text in an
event line. `fm-procevent.sh handled <source-id> <sequence>` is the only thing
that stops re-announcement: generation-keyed, private, path-safe, durable, and
atomically idempotent, so a paired external effect gated on its first-time
versus repeat report is never authorized twice.
An acknowledgement is refused unless matching captured result and adapter
records already exist, so a premature or mistyped call cannot suppress a
future result.
The source side is unchanged and still lossy: the published poll clears
feedback destructively before returning it, so a result lost in that window
is unrecoverable. This is never at-least-once, no-loss, or lossless, and the
handled acknowledgement is not a generic exactly-once effect either - a crash
between an external effect and its acknowledgement can still repeat that
effect on replay.
Integrate registered sources with watcher supervision, the guards, and
recoverable secondmate teardown across nested homes, and cover source
identity, lifecycle races, supervision, restart handling, and cleanup safety
with regressions.
* no-mistakes(review): Prevent Lavish prompt text from spoofing missing sessions
* no-mistakes(review): Serialize publication and secure handled acknowledgements
* no-mistakes(document): Document hardened process-event acknowledgement guarantees
* fix(procevent): never reclaim a source whose owned group still runs
A runner is its own process group leader and starts the blocking source in
that group, but the claim records only the leader PID and its identity. If the
leader died while the source child kept running, the missing PID was
classified stale: reconciliation released the claim and started a second
runner while the old blocking source was still consuming the same canonical
source. For the Lavish adapter that means two destructive long polls racing on
one review session, so it is not harmless process litter. It also contradicted
the documented promise that ownership is never released until the whole group
is gone.
Ownership state now distinguishes a generation that is really gone from one
whose leader crashed with its group still alive. Reconcile stops that
surviving group and releases its exact generation before starting any
replacement, and keeps the claim for a later cycle when it cannot prove the
group stopped or another home owns it. Acquisition and `start` treat the same
state as held rather than reclaimable.
Signalling that group is safe precisely because only an absent leader reaches
this state. A reused PID leaves the leader alive, so the identity comparison
still classifies it stale or uncertain and no group signal follows, which
keeps the existing PID-reuse refusal intact.
Add a public-interface regression for the exact crash cut - SIGKILL only the
leader, prove the child group survives, reconcile, and prove the old group is
gone with no second source running - plus its counterexample that a generation
with no leader and no surviving group is still reclaimed. Update the runner
help, operating documentation, skill, and verification record where they
described reclaim in terms of the leader alone.
* no-mistakes(review): Enforce runner group ownership and detect poller overlap
* no-mistakes(review): Isolate runner groups from unrelated caller processes
* no-mistakes(document): Document isolated process-event runner launch
* no-mistakes(lint): Suppress Perl literal ShellCheck false positive
* fix(bin): retire terminal process events and surface queued wakes (#1500)
* fix(bin): deliver process-event results and retire ended sources
Two defects reproduced during a real Lavish adapter session.
One human `Send & End` produced four captured results: the real feedback,
then recurring empty ended sessions. The generic runner had no way to learn
a source was finished, so every reconcile restarted a poll that returned
immediately. The runner now asks the source's own adapter -
`fm-procevent-<adapter>.sh terminal <result-file>` - and on exit 0 alone
re-proves ownership, drops the registration, and releases its own claim
under one source boundary. Terminal knowledge stays adapter-owned: for
Lavish that is an ended session, a missing session, and the final feedback
delivery the published poll marks with `session_ended`. An adapter with no
terminal command keeps its source armed exactly as before. Capture before
publication, captured-result durability, queued wake durability, bounded
re-announcement, handled deduplication, one-owner ownership, and explicit
idempotent retirement are all unchanged.
A captured result queued its `check` wake durably, but a healthy watcher
with a fresh beacon never delivered it; the result surfaced only after a
manual drain. Publication happens outside the watcher (in the runner) or
unconditionally (in reconcile), so the watcher had no newly actionable
signal to report and never reached its rewake path. It now reports a
queued-but-unsurfaced process-event record through the same actionable exit
every other wake uses, deduplicated by the same `.seen-*` marker discipline
the signal scan uses, so the record is always durable before it is
suppressed. The durable queue remains the authority and no second notifier,
poller, timer, queue, or adapter-specific wake path is added.
Regressions cover both, driven end to end: an armed Lavish source against a
stand-in for the published poll polls once, captures once, publishes one
distinct event, and retires itself; two fixture adapters prove the terminal
decision follows the adapter alone; and a real capture plus a real watcher
prove one proactive wake before any drain, with no duplicate wake while the
record stays queued or after it is acknowledged.
* no-mistakes(review): Harden process-event retirement and proactive delivery
* no-mistakes(review): Route process-event delivery through shared wake owner
* no-mistakes(document): Clarify process-event delivery and retirement documentation
* no-mistakes(lint): Fix ShellCheck control-flow warnings
* no-mistakes(lint): Fix wake output status lint warning
* perf: shard portable serial tests across CI runners (#1544)
* perf(ci): shard the portable serial behavior lane across runners
The Behavior portable serial job ran all 69 scripts of the serial
remainder on one runner. The measured serial sum on run 30725985757 was
1143762 ms (19m04s) against a 20-minute timeout, so the job intermittently
reached the cap and was cancelled with every step passing. Setup is only
about 7s, so the cost is entirely test wall time.
Split the lane into four separate-runner shards. Each shard is still
strictly serial, and separate runners mean no two of these stateful
scripts ever share a machine, so the split needs no concurrency isolation
proof. Assignment is longest-processing-time bin packing over measured
per-script duration hints, balancing every shard to 285941 ms (~4m46s) of
expected work, and the timeout tightens from 20 to 15 minutes.
bin/fm-test-run.sh owns the shard count and refuses a lane whose "ofN"
disagrees with it, while ci.yml derives the same count from
strategy.job-total rather than a literal, so changing it in either file
alone fails the lane loudly instead of leaving part of the required suite
unrun. --check-coverage additionally proves the shards are non-empty,
disjoint, and exactly equal to the serial lane. No test is weakened,
skipped, or removed.
Also replace the wall-clock sleeps in the --jobs scheduler test fixture
with an explicit signal handshake between the fixtures. The old
0.5s-versus-0.05s race failed on a loaded machine; the handshake passes
under sustained CPU saturation.
* no-mistakes(review): Correct portable serial shard balance evidence
* no-mistakes(document): Document portable serial shard evidence accurately
* fix(bin): correct session lock and attached watcher supervision (#1545)
* fix(bin): identify harness sessions by path and report delivered wakes
Two supervision faults, both reported by a contributor and both open on the
default branch.
Fault 1: the Stop auto-arm never claims the home. fm_harness_ancestry_pid()
matched only the basename of `ps -o comm=`, and Claude Code's native installer
names the per-session executable by its version (.../share/claude/versions/
2.1.220), so that basename identifies nothing. Three real failure shapes follow:
a version-named session is missed entirely and the hook exits 0 with the epoch
never written (unconditional on Linux, where procps reports the kernel exec name
and ignores argv[0]); a claude-named daemon that directly parents sessions wins
the outermost-contiguous-claude rule ahead of the session itself; and a session
that is both version-named and daemon-parented has its live lock reclaimed as
stale and rewritten to the shared daemon pid, corrupting the home's ownership
record.
Harness identity now also reads whole components of the executable path and of
argv[0], which is what both platforms still carry. Matching whole components
only keeps that widening safe: bin/fm-claude-stop-autoarm.sh and ~/.claude/hooks
scripts have no "claude" component. Ownership is then decided against the
session's whole contiguous harness ancestry rather than one chosen pid, which is
the honest form of the question the library already documents ("does the current
process descend from that same harness?"). That subsumes the outermost-pid rule
for Claude's nested bg-spare worker chain instead of reverting it, and lets a
daemon-parented session recognize its own lock. Lock acquisition still writes the
outermost pid of the run, the only pid that lives as long as the session.
Fault 2: an attached arm reports a delivered cycle as FAILED. The watcher prints
its one reason line to its own stdout, so only the arm that forked it can read
that line; an arm that attached observes nothing but a released lock and called a
completely successful cycle "cycle ended without an actionable reason". No
supervision event was lost - the durable queue held it - but every harness
protocol reads that line as "supervision is down" and directs a manual re-arm.
The arm now resolves an unobservable close against the durable wake queue, which
records every wake before the watcher prints it and whose sequence counter never
rewinds, not even across a drain. A cycle the queue proves delivered a wake
reports that wake and exits 0; a cycle whose records a handling turn already
drained reports the delivery without inventing a reason line; only a cycle that
delivered nothing is still the typed nonzero failure. Fixing it in the arm covers
codex, opencode, pi, grok and kimi, not just the Claude Stop path.
Regressions: tests/fm-session-lock-ancestry.test.sh pins both platforms' ps
semantics behind a deterministic process table and runs the real Stop auto-arm in
version-named, daemon-parented, and combined real process trees, each orphaned so
the walk cannot escape the fixture. tests/fm-watch-arm.test.sh drives a real
watcher and a real attached arm through a real wake. Every fault case fails on
the previous code.
* no-mistakes(review): Bind watcher delivery records to process identity
* no-mistakes(review): Return validated watcher identity atomically
* no-mistakes(review): Track watcher successors by PID and identity
* no-mistakes(document): Consolidate watcher arm-cycle documentation ownership
* fix(bin): harden Claude supervision auto-arm recovery (#1495)
* fix(supervision): harden Claude auto-arm failure handling
* no-mistakes(review): Guarantee automatic retry after Claude auto-arm failures
* no-mistakes(review): Gate attended fail-open on verified supervision failure
* no-mistakes(document): Document Claude auto-arm retry and guard scope
* no-mistakes: apply CI fixes
* fix(supervision): make Claude fail-open progression monotonic
* no-mistakes(review): Preserve auto-arm failure episodes until verified watcher recovery
* no-mistakes(review): Linearize auto-arm failure progression across existing locks
* no-mistakes(review): Linearize positive recovery across shared failure episode lock
* no-mistakes(review): Scope Claude recovery contention to Claude guard mode
* no-mistakes(document): Align supervision auto-arm documentation
* no-mistakes(review): Preserve actionable wakes despite healthy successors
* no-mistakes(document): Refresh supervision auto-arm documentation
* feat(bin): require an explicit per-task delivery contract (#1563)
* feat(bin): require an explicit ship delivery mode in fm-brief
A ship brief's definition of done was shaped by a silent per-project registry
lookup, so an adjusted brief and the task's recorded delivery could disagree and
no one had to decide anything per task.
fm-brief now requires --mode on ship scaffolds, validates it against the closed
set, refuses the conditional no-mistakes-prod-only registry policy as a task
mode, and records the choice as a fixed machine-readable "Delivery contract:
mode=<mode>" line that fm-spawn can check. --mode is refused on scout and
secondmate scaffolds, and --yolo is refused outright because the worker never
owns approval decisions.
* feat(bin): require an explicit ship delivery contract at spawn and promotion
fm-spawn resolved every ship and scout task's mode and yolo from the project
registry, so the delivery posture was never a per-task decision and could
contradict the brief the worker was about to follow.
fm-spawn now requires --mode and --yolo on ship spawns, validates both against
their closed sets, and reads the brief's recorded delivery contract line and
refuses a mismatch before any endpoint exists; a brief scaffolded before that
line existed warns once and launches on the flag. A batch carries one shared
contract that each pair still checks against its own brief. Scout and secondmate
spawns refuse the flags, and a scout now records no mode or yolo at all, which
teardown and the snapshot already tolerate. When the explicit mode carries less
rigor than the project's standing posture, a deviation notice is printed and the
spawn continues, so the registry stays advisory rather than an enforced default.
fm-promote requires the same two flags, because a scout carries no posture to
inherit, and writes them into the task record with the kind flip.
fm-project-mode keeps its one registry parser for the mechanical consumers that
have no task in hand, accepts the conditional no-mistakes-prod-only annotation
and maps it to its most rigorous leg for them, and grows --raw so the deviation
notice can tell a conditional policy apart from a flat mode.
* docs: record the explicit per-task delivery contract
AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at
intake, including the surface classification for a no-mistakes-prod-only project
and the unregistered-project fallback, and the project-management skill defines
that conditional policy as a registration-time posture with its defaults and
initialization consequences. The registry blurb, script table, and architecture
section follow: the registry records the captain's standing posture, and task
delivery is decided per task and passed explicitly.
* test: pass ship delivery flags per call site in the Herdr launcher e2e
The shared spawn helper also launches a secondmate, which refuses the flags, so
the contract belongs at each ship call site rather than inside the helper.
* test: pass the ship delivery contract in the secondmate suites
Both suites scaffold or spawn an ordinary ship task as the control case for a
secondmate assertion, so each needs the explicit contract the ship path now
requires.
* feat(bin): support remote secondmate homes (#1576)
* Add generic remote secondmate transport
* Add routed remote secondmate replies
* Add remote outbox backlog handoff
* Integrate remote secondmate lifecycle
* no-mistakes(review): Fix remote snapshot and handoff races
* no-mistakes(review): Serialize remote home provisioning transactions
* no-mistakes(review): Harden remote lifecycle transaction boundaries
* no-mistakes(review): Serialize remote lifecycle mutations and fail closed
* no-mistakes(review): Close remote lifecycle and file race windows
* no-mistakes(review): Serialize remote reply retirement and inheritance
* no-mistakes(review): Harden remote transfer integrity and recovery
* no-mistakes(review): Serialize remote respawn with registry retirement
* no-mistakes(document): Document remote bootstrap convergence accurately
* no-mistakes(document): Clarify skipped remote secondmate mutations
* no-mistakes(lint): Resolve remote script ShellCheck warnings
* no-mistakes: apply CI fixes
* feat: add per-task trace context propagation (#995)
* feat(spawn): propagate a native W3C traceparent to spawned agents
Add a default-off capability that resolves one W3C traceparent for a task,
injects it into the agent's pane shell as the TRACEPARENT environment
variable immediately before launch, and records the identical value as
traceparent= in state/<id>.meta, so an external observer that explicitly
reads that env value or meta field can correlate a worker, a Secondmate, and
their nested children into one trace with no collector, storage, UI, or
vendor coupling.
TRACEPARENT as an environment variable is a firstmate convention carrying a
W3C-formatted value: W3C Trace Context standardizes the header, not an env
var, and OpenTelemetry SDKs do not read it automatically, so a downstream
must consume it deliberately; this feature parents no SDK span by itself.
Identity is per task, not per spawn: the carrier is minted with random ids on
the first spawn, adopted as a child (fresh span, same trace) for a nested
spawn whose parent already holds one, and reused verbatim from the meta on
relaunch, so a task keeps one stable logical identity across restarts. A
malformed or all-zero inherited value is treated as absent and roots a fresh
trace. A new root is sampled (01) - a sampling decision a downstream
parent-based sampler honors, not a guarantee that any collector stores a
span, and firstmate emits no spans; a child preserves the inherited flag.
Trust boundary: a firstmate-minted root is random and reads no prompt, path,
task prose, credential, or arbitrary environment key. An inherited
TRACEPARENT is opaque caller-controlled data - up to 24 bytes of id passed
through after syntax validation - so whoever set it controls those bytes, a
bounded fixed-width channel rather than a general content or secret channel.
The feature adds no OTEL_* variable, no tracestate, and no arbitrary
environment injection; it runs no configurable or arbitrary command, only the
fixed local od and tr (resolved from PATH) to read a few bytes of entropy - a
small local pipeline with no network or watchdog and no hard latency
guarantee. Any entropy or validation failure that returns omits the carrier
without aborting the spawn. A default-off spawn leaves the generated meta and
launch environment unchanged.
Enablement is default-off (config/trace-context, or FM_TRACE_CONTEXT where a
non-empty value overrides and unset or empty defers to the file) and is
propagated into secondmate homes, taking effect at each agent's next launch:
a Secondmate launched or relaunched after enablement carries the primary
trace into its nested workers, while an already-running Secondmate roots new
traces for its own workers until relaunched. Injection reuses the existing
GOTMPDIR channel, so all spawn backends and harnesses and the ship, scout,
and secondmate paths are covered.
Covered by a pure-library suite and a spawn-path integration test (fake tmux
plus a real worktree, hermetic against ambient FM_TRACE_CONTEXT) proving the
recorded and injected carriers are identical and sent before launch, that
default-off writes and injects neither, that a relaunch reuses the recorded
carrier, and that an explicit FM_TRACE_CONTEXT overrides the file both ways;
plus a source-owner inheritance test proving trace-context propagates and
absence-mirrors through propagate_inheritable_config.
Documentation follows the repository documentation-audiences contract:
docs/trace-context.md is maintainer-architecture rationale, the configuration
schema lives in docs/configuration.md, and the repeatable test evidence is
separated into docs/verification/trace-context.md (maintainer-verification),
registered in docs/documentation-audiences.json.
* fix(spawn): propagate the effective trace-context decision to secondmates
FM_TRACE_CONTEXT overrode trace context only in the process that read it. A
newly launched secondmate decided enablement from the inherited
config/trace-context file alone, so the override did not cross the
primary-to-secondmate boundary: FM_TRACE_CONTEXT=off with the file present left
the secondmate's nested workers traced (a broken kill switch), and
FM_TRACE_CONTEXT=on with the file absent left them untraced despite the
inherited carrier.
Deliver the primary's effective decision to a newly launched secondmate as a
normalized on/off FM_TRACE_CONTEXT in the launch prefix, so a FM_TRACE_CONTEXT
override governs the nested primary -> secondmate -> worker chain both ways, not
just the copied file. The value is bounded to the literal on/off and does not
broaden environment injection; the already-running secondmate boundary is
unchanged.
Add a genuine two-level spawn regression that drives fm-spawn twice with the
exact environment the primary injects into the secondmate and proves both
divergent directions end to end. Correct the documentation that implied
secondmate coverage on every backend, since orca and cmux reject secondmate
spawns, and refresh the verification evidence for the new assertion count.
* no-mistakes(review): Clarify Secondmate trace-context launch snapshots
* no-mistakes(document): Correct trace-context documentation ownership and relaunch semantics
* fix(spawn): resolve the trace-context decision once for carrier and snapshot
The effective trace-context decision was read twice per spawn: once inside
fm_trace_context_resolve for the recorded carrier, and again for the secondmate
FM_TRACE_CONTEXT launch snapshot. A config-file change between the two reads
could pair a carrier with the opposite enable state - an injected carrier with
an off snapshot, or no carrier with an on snapshot.
Freeze the effective on/off decision once, drive the carrier resolution under
that frozen FM_TRACE_CONTEXT so it cannot independently re-read the file, and
reuse the same frozen decision for the secondmate launch snapshot. Add a
spawn-path regression that drives the file-decided path and proves the recorded
carrier and the delivered snapshot always agree, and refresh the verification
evidence for the new assertion count.
* no-mistakes(review): Preserve legacy Secondmate trace boundary
* no-mistakes(document): Correct trace-context verification comparison base
* no-mistakes(review): Captain, prevent failed trace delivery metadata claims
* no-mistakes(review): Captain, align trace-context tests and verification evidence
* no-mistakes(document): Correct trace-context verification evidence
* no-mistakes(lint): Suppress intentional ShellCheck literal-dollar warnings
* no-mistakes(review): Captain: freeze trace context at session start
* no-mistakes(test): Captain: stabilize scheduler test and document Kimi trace coverage
* no-mistakes(document): Document trace-context safety boundaries
* fix(trace): fail off on stale session snapshots
Publish each home session decision atomically through a same-directory temporary file and bind it to the current session lock. A replacement failure can no longer leave an earlier on decision active in a later session; missing, stale, malformed, or unpublishable state defaults safely to off.
Add regressions for read-only replacement and failed publication, update spawn and session-start fixtures for the lock-bound format, and refresh the architecture and verification records.
* no-mistakes(review): Fix trace spawn failure independence and duplicate safety
* no-mistakes(document): Refresh trace-context documentation and verification
* no-mistakes(review): Clear partial backend input after failed trace submission
* no-mistakes(review): Stop unsafe trace delivery before launch append
* no-mistakes(document): Document unsafe trace delivery handling
* fix(trace): bound each trace to one routed task, never the routing agent
A persistent Secondmate holds its launch-time TRACEPARENT in the process
environment for its whole life, and routed requests never replace it, so
resolving new-task carriers from the ambient environment chained every
routed task into one ever-growing trace per Secondmate with distinct
parent ids. Resolve now reuses the task's recorded carrier or mints a
fresh sampled root, never reading ambient TRACEPARENT, so each routed
task is its own trace boundary while relaunch, recovery, and
scout-to-ship promotion keep one stable per-task identity.
The spawn regression models the reviewed scenario exactly: two unrelated
tasks spawned sequentially through one persistent Secondmate environment
record and inject distinct trace ids, adopt nothing from the Secondmate's
carrier, and a relaunch of the first task reuses its original carrier
verbatim.
* docs(trace): define the per-task trace boundary
The design contract is one task per trace: a persistent Secondmate is
routing infrastructure with its own agent identity, never a shared trace
root for the unrelated tasks routed through it. Root/recovery semantics
replace the removed child-inheritance path, the sampling and safety
sections drop inherited-carrier language because ambient TRACEPARENT is
never read, and the verification page records the refreshed suite
inventories including the two-task Secondmate boundary regression.
* test(trace): adopt the explicit per-task delivery contract in spawn fixtures
Rebasing onto current main brings the explicit per-task delivery contract:
ship spawns now require --mode and --yolo instead of resolving them from the
project registry. The trace spawn fixtures pass the same explicit contract
canonical spawn tests use, preserving the per-task trace boundary coverage
unchanged, and the verification page records the refreshed comparison base.
* fix(bin): harden tmux agent liveness across harnesses (#1577)
* fix(bin): classify tmux agent liveness independent of process titles
`fm_backend_tmux_agent_state` attributed a pane solely from
`#{pane_current_command}`, which is a process TITLE a harness can rewrite,
not a structural fact. Claude Code 2.1.220 reports its version string there,
so a live Claude endpoint classified `ambiguous`: the session-start secondmate
liveness sweep could no longer see it, and any consumer that gates on a
positive classification refuses outright.
Read a second, independent name source: the kernel `comm` of every process in
the pane tty's foreground process group. Either source naming a verified
harness yields `alive`, because a false `dead` is the one verdict that can
start a duplicate agent on a live worktree. Scoping to the foreground process
group rather than the pane's descendants keeps a harness-named background
process from faking an agent, and covers multi-process launchers (the Pi
Launcher path) without a special case.
Verified on 2026-08-03 against all seven adapters running for real on tmux
3.6a / macOS 26.5.2 arm64: claude 2.1.220, codex-cli 0.146.0, opencode
1.18.11, pi 0.82.0, pi-signed 0.82.0, grok 0.2.118, kimi 0.31.1 all classify
`alive`, each attributed by a source independent of its title.
Two tests, because they fail for different reasons:
- tests/fm-tmux-agent-liveness.test.sh pins the logic with real processes and
no harness, so it runs everywhere CI runs tmux. It drives the two name
sources apart on purpose and asserts the divergence, so no case can go
quietly vacuous.
- tests/fm-harness-liveness-drift-live-e2e.test.sh relaunches every installed
harness and fails naming the harness and version when one stops being
attributed by a title-independent source.
AGENTS.md section 4 carries the resulting standing rule, and
firstmate-coding-guidelines owns how to satisfy it.
* no-mistakes: apply CI fixes
* docs: move the harness-dependent-check policy out of AGENTS.md
The standing rule was stated in AGENTS.md section 4 with the mechanics in
firstmate-coding-guidelines, which split one contract across two owners and
charged every session for a rule that only fires when firstmate's own
harness-dependent code is being changed.
firstmate-coding-guidelines is now the single owner of both the rule and how
to satisfy it: real-harness proof required, that proof authorized to spend
tokens, structural signals preferred over vendor-rendered surfaces, and a
guard that fails loudly naming the harness and version where a surface signal
is unavoidable. No inline stub is left behind, because AGENTS.md already
carries the load trigger for that skill in sections 7 and 13, so it is read
before any change to firstmate's shared tracked material.
Also records the cross-platform lesson the pipeline caught in the portable
regression, and corrects that file's header: the divergence assertion lives
on the version-string case, which diverges on both supported platforms,
rather than on every case.
* no-mistakes(review): Harden tmux liveness identity and drift validation
* no-mistakes(document): Clarify cross-platform tmux liveness documentation
* feat(bin): propagate trace context to remote secondmates (#1609)
* feat(bin): trace remote secondmate routes and unify the inherit allowlist
Per-task W3C trace context (#995) resolved and injected its carrier only at
the local spawn path. A remote secondmate is routed through
spawn_remote_secondmate, which returns long before that site and wrote its own
metadata block, so a remote secondmate stayed silently untraced even with the
capability enabled.
The parent home still owns that task's identity, because it holds the metadata
an observer reads. It now resolves the carrier against the task's own meta
under its own frozen decision - reused verbatim on relaunch, freshly rooted
otherwise, never adopting the parent process's ambient TRACEPARENT - and hands
it to the configured host through a new fm-spawn --traceparent argument,
accepted only for a secondmate launch and only as a strict W3C value. The
remote host exports it at the same unconditional pre-launch site and reports
back the carrier its endpoint actually holds, which the parent records, so an
already-alive endpoint reports the identity its agent really received rather
than one the parent merely intended. Disabled remains byte-identical and off.
The remote inherit path also carried its own hardcoded copy of the inheritable
config set, already drifted from FM_INHERITABLE_CONFIG by trace-context. Both
remote ends now derive from that one declaration, so a future item cannot be
sent by one side and refused by the other, and session-scoped enablement items
are skipped on live convergence exactly as the local path skips them.
Also fixes a latent stderr leak: an absent session lock printed a raw redirect
failure, which the new remote resolve site made visible.
Adds tests/fm-remote-secondmate-trace-context.test.sh, driving the real
parent -> fm-on -> remote entrypoint -> control -> remote fm-spawn chain over
the deterministic SSH boundary and reading the carrier back from the remote
pane's own log.
* no-mistakes(document): Clarify remote trace and allowlist contracts
* feat(bin): preflight remote runtime tool paths (#1623)
* feat(bin): widen the remote runtime PATH and add a remote doctor preflight
The fixed remote entrypoint hard-coded a four-directory PATH, so a remote
account whose tools live under nix or a per-user profile could not run basic
Firstmate work without a login shell. The entrypoint now composes its child
PATH from the code root's bin, the account's ~/.local/bin, the common
package-manager directories that actually exist on the host, and the portable
system tail, deduplicated and in a fixed order, still under env -i with the
same variable allowlist and no shell command string.
fm-remote-doctor.sh reports that exact PATH by inheriting it from its own
entrypoint launch rather than recomposing it, so the ordering keeps one owner.
It is read-only, reports where each required and optional tool resolved, and
exits non-zero naming every required tool that did not. Remote seeding runs it
as a preflight before anything is created on the host and restores the registry
when it fails.
* no-mistakes(review): Harden remote git authorization and missing-tool diagnostics
* no-mistakes(document): Document remote PATH doctor and safe shims
* no-mistakes(lint): Fix ShellCheck findings in remote path tests
* no-mistakes(lint): Suppress exported fixture's false-positive ShellCheck warning
* feat: gate remote second mates on Herdr readiness (#1639)
* feat(bin): gate remote second mates on herdr readiness
A remote second mate now always runs on the Herdr backend, whose server
belongs to the host's GUI login session and therefore outlives the SSH
connections that supervise it. fm-spawn's remote route forces that backend
and the host-local control script refuses any other, so the requirement
cannot be dropped from either side.
fm-remote-doctor.sh becomes the single owner of what "ready" means. It keeps
its PATH and tool reporting from #1623 and adds the Herdr, Aqua LaunchAgent,
GUI-session, server-reachability, and entrypoint-symlink checks, tagging each
gap fixable: or human: with the exact operator step. --fix closes only the
automatable gaps - writing and loading the Aqua-scoped dev.firstmate.herdr
launch agent, starting the server where no launch agent applies, and
recreating the entrypoint symlink - then re-derives every check from the host,
so a human gap is never presented as fixed. It never creates a login session,
writes an auto-login password, or touches FileVault.
Remote seed, remote spawn, and the startup liveness relaunch all run the same
check, repair, re-check sequence through one shared library and fail closed
with the doctor's own gap text. Recovery inherits the gate because it respawns
through the same route.
Tests drive the real doctor against a controlled account fixture with a
private HOME, a state-backed launchctl, and a fake herdr, and prove the
dangerous actions are never attempted. The remote lifecycle suites gain a
stateful Herdr CLI fixture and answer the readiness gate at the SSH boundary,
so they never inspect or repair the runner's own account.
* no-mistakes(review): Validate launch-agent contract and confirm Herdr startup
* no-mistakes(review): Validate loaded launch-agent contract before readiness
* no-mistakes(review): Refuse legacy remote backends without altering routes
* no-mistakes(review): Clarify conditional remote readiness repair sequence
* no-mistakes(review): Repair remote readiness before liveness probing
* no-mistakes(review): Preserve unknown seeds and reject legacy liveness
* no-mistakes(document): docs: clarify remote Herdr backend ownership
* fix: isolate remote secondmates in shared Herdr session (#1659)
* Pin remote secondmates to fm-remote
* no-mistakes(review): Fail closed on legacy remote Herdr endpoints
* no-mistakes(review): Isolate fm-remote launch agent from interactive default
* no-mistakes(document): Document shared remote Herdr retirement safety
* feat: route remote commands through an Aqua job worker (#1660)
* feat: run remote commands through Aqua job worker
* no-mistakes(review): Enforce remote job deadlines and safe worker shutdown
* no-mistakes(review): Refresh stale workers and harden dependency-free supervision
* no-mistakes(review): Harden worker ownership recovery and shutdown quarantine
* no-mistakes(review): Fix doctor bootstrap, harness repair, and output draining
* no-mistakes(review): Probe doctor tools through authenticated worker bootstrap
* no-mistakes(review): Refresh stale workers before doctor tool probes
* no-mistakes(review): Recover stopped quarantines and extend job deadlines
* no-mistakes(review): Separate queue and execution timeout windows
* no-mistakes(review): Supervise Linux worker crashes and bind root identity
* no-mistakes(review): Resolve authorized Nix profile bin links
* no-mistakes(review): Clarify Nix path resolution documentation
* no-mistakes(review): Harden PATH safety and nvm selection
* no-mistakes(review): Honor nvm system defaults and refresh doctor digest
* no-mistakes(review): Keep workers ready during active jobs
* no-mistakes(review): Bound pre-execution validation by job timeout
* no-mistakes(document): Clarify remote worker documentation
* no-mistakes(lint): Fix remote worker ShellCheck diagnostics
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
* fix: clarify remote doctor bootstrap path (#1691)
* fix(bin): bound remote SSH dead-peer detection (#1699)
* fix(remote): arm SSH dead-peer detection in fm-on.sh
A vanished remote host mid-poll (a reboot, a dropped link) left ssh
blocked indefinitely on a half-open TCP connection, because fm-on.sh's
ssh invocation had no ServerAliveInterval/ServerAliveCountMax. This
wedged the remote-reply ferry: fm-procevent.sh's runner blocked inside
the ssh child and never reached its own no-result -> claim-release ->
reconcile re-arm self-healing path, which otherwise already handles a
nonzero exit with empty output correctly. Recovery required a manual
retire and re-arm.
Arm ServerAliveInterval=15 and ServerAliveCountMax=3 by default
(bounded ~45s detection window), both overridable via
FM_SSH_ALIVE_INTERVAL and FM_SSH_ALIVE_COUNT_MAX. This is a transport-
level fix in fm-on.sh, so it covers every remote command routed
through it, not just the reply ferry. The remote sshd answers
keepalive probes independently of whatever the remote command is
doing, so a legitimately long-but-alive command (a 55s poll, a clone,
the doctor) is never falsely killed - only a truly vanished peer trips
it, turning that case into a bounded, detectable ssh failure (exit
255) instead of an indefinite hang.
Extends tests/fm-on.test.sh with a behavioral regression asserting a
bounded, positive ServerAliveInterval/ServerAliveCountMax on the real
ssh argv captured through the FM_SSH_BIN process seam, plus coverage
that both are env-overridable.
* no-mistakes(document): Document SSH dead-peer detection ownership
* fix: report stale AXI tools during bootstrap (#1701)
* feat(bootstrap): gate stale axi CLIs at the floors firstmate actually uses
Add gh-axi 0.1.29 floor so bare --squash PR merges stop failing quietly on
older builds. Raise tasks-axi to FM_TASKS_AXI_MIN=0.2.2 (multi-id mv) while
keeping feature probes. Keep quota-axi at 0.1.16 after verifying schema 3
and per-model availability already ship there; runway remains optional.
* no-mistakes(document): Clarify AXI compatibility documentation ownership
* fix: prevent false watcher-down alarms in Claude sessions (#1661)
* fix(guard): stop false watcher-down alarm mid-turn under Claude auto-arm
bin/fm-guard.sh derived its watcher-health verdict from fm_watcher_healthy,
which requires a live watcher process holding the home lock. Under the Claude
Stop-hook auto-arm supervision model the watcher is armed at each turn end and
exits on its wake, so it runs only between turns. Every guarded command run
mid-turn therefore found no live watcher and printed the "WATCHER DOWN -
SUPERVISION IS OFF" banner even though supervision was healthy. Because the
episode key was derived from the beacon mtime (which the between-turns watcher
advances every poll), the full banner re-printed on essentially every command,
and the message always blamed a "fresh beacon" that was in fact fresh.
Make the pull guard's health check model-aware via a new
fm_watcher_supervision_verdict in bin/fm-wake-lib.sh:
- Under the auto-arm model a beacon fresh within FM_GUARD_GRACE is healthy even
with no live watcher process; only a beacon stale beyond grace (or absent) is
a genuine lapse and alarms.
- Under every persistent-watcher harness (codex foreground checkpoint,
opencode/pi/grok background arm, tmux, unknown) a live identity-matched
watcher with a fresh beacon is still required, unchanged.
The banner now names the true failing condition, a missing live watcher process
versus a genuinely stale beacon, instead of always blaming the beacon, and the
once-per-episode dedup keys on that condition rather than the beacon mtime so a
genuine lapse announces once and does not re-print each turn.
The turn-end guard keeps the strict fm_watcher_healthy check because it fires at
the turn boundary, where the auto-arm brings a fresh watcher up and it
cooperates with that arm. fm_watcher_healthy itself is unchanged, so the arm
layer's start/attach/replace decisions are unaffected.
Tests in tests/fm-guard-stale-banner.test.sh cover the auto-arm healthy
fresh-beacon-without-a-watcher case, the auto-arm stale-beacon alarm and its
stable episode, the true-reason banner wording, and the reason-keyed episode
surviving a beacon mtime change; existing persistent-model cases are pinned to
that model.
* no-mistakes(review): Pin secondmate supervision model to launched harness
* no-mistakes(document): Align watcher documentation with model-aware supervision health
* test: prevent fixture temporary directory leaks (#1704)
* fix(tests): stop fixture-tempdir helper from self-deleting under command substitution
fm_test_tmproot is almost always called as `TMP_ROOT=$(fm_test_tmproot prefix)`,
which forks a subshell to capture its stdout. The old implementation set its
EXIT cleanup trap inside that call, so the trap fired - and deleted the fixture
root - the instant the subshell exited, before the real caller's own EXIT trap
was ever installed. Every test using the documented call pattern leaked its
fixture root on every run; two suites had already independently discovered and
worked around this with ad-hoc mktemp calls.
Registration now goes through a $$-keyed registry file instead of in-process
state, since $$ resolves to the invoking shell's PID even inside the
subshell. The real cleanup trap is armed once at source time (always the real
caller, never a subshell) for EXIT, INT, and TERM. A best-effort orphan sweep
on next source reaps marked fixture roots old enough to be from a killed prior
run.
Simplifies the two existing ad-hoc workarounds (fm-procevent.test.sh,
wake-helpers.sh) back onto the shared helper now that it works correctly.
* no-mistakes(review): Preserve live fixtures during orphan reaping
* no-mistakes(review): Harden fixture ownership against PID reuse
* no-mistakes(review): Secure cleanup registry against path precreation
* no-mistakes(review): Make fixture registration transactional
* no-mistakes(document): Documentation already matches fixture cleanup behavior
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
* feat(herdr): enable presentation spaces by default (#1708)
* feat(herdr): default presentation spaces on with an explicit opt-out
Herdr's disposable one-task presentation workspace was opt-in through the
presence of local config/herdr-presentation-spaces. It is now on by default,
and a home opts out by writing "off" into that same file.
Values are read with the whole-file whitespace-stripped convention the other
scalar config items already use, plus case folding. An absent file, an empty
file, and "on" all resolve on; only "off" opts out; an unrecognized value warns
and keeps the default rather than failing a spawn over a purely visual setting.
The empty file is exactly the historical opt-in form, so every home that had
already enabled the projection stays enabled with no migration step, and no
previously enabled home can be turned off by the flip.
Because absence now means on at both ends, secondmate inheritance needs no
item-specific convergence: mirroring an absent primary file converges a
secondmate to the same default-on rather than turning its projection off, and
only an explicit primary opt-out propagates the opt-out.
The gate itself moves into fm_backend_herdr_presentation_enabled in the Herdr
adapter so the semantics have one owner that regressions can exercise directly.
* no-mistakes(document): Document Herdr default-on presentation safety
---------
Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
* fix(bin): surface fleet-wide open decisions on every wake drain (#1711)
* fix: surface consolidated open decisions on every wake-drain
A needs-decision or blocked event buried under later, unrelated status
appends was only ever shown via the last-line wake annotation, so a
still-open captain decision could go silently missed even though
status_open_decisions (fm-classify-lib.sh) already folds the whole
status stream correctly and fleet-snapshot/bearings already reuse it.
Wire that same fold into bin/fm-wake-drain.sh: a new fleet-wide
scan_open_decisions wrapper scans every state/<id>.status, and
fm-wake-drain.sh prints a separate, bounded OPEN DECISIONS section on
every drain (including the empty-queue fast path), so session-start
and every wake-handling turn surface it for free without duplicating
the open/resolved fold itself. Heartbeat wakes drain through the same
script, so this covers that surface too.
Also tighten status_open_decisions' file guard to skip an unreadable
status file instead of leaking a bash redirection error, now that a
fleet-wide directory scan can reach files a single targeted read
would not.
* no-mistakes(review): Prevent status symlinks leaking open decisions
* fix: drop unbounded perl subprocess from status symlink guard
The review step's own symlink-safety auto-fix (O_NOFOLLOW read via a
perl subprocess) forked one perl process per status file scanned by
the new fleet-wide open-decisions scan, with no cap - inflating
fm-wake-drain.sh's total external-read cost from 8 (the existing
annotation read_cap) to 18 in the enrichment-caps regression test.
The plain [ -L "$f" ] check already rejects any status file that is
itself a symlink before any read happens, which is exactly what the
new regression test exercises and is the same defense level the
sibling scan_captain_relevant_statuses/last_status_line already rely
on elsewhere in this file (no O_NOFOLLOW). Drop the subprocess-based
nofollow read and keep the cheap builtin guard.
* no-mistakes(document): Document actionable fleet-wide open decision drains
* fix(bin): abort parked runs and reap leaked processes before teardown (#1710)
* fix(bin): abort orphaned no-mistakes runs and reap leaked processes at teardown
Teardown could remove a task's worker while its no-mistakes pipeline run was
still parked at a gate, leaving an orphaned run holding a fleet slot
indefinitely (observed 2026-08-03: runs parked 7h39m and parked at a
post-CI approval gate). It could also leave backgrounded/disowned
descendant processes rooted under the worktree or tasktmp surviving
reparented to init (observed: two `go test` binaries pinning CPU for
hours with no live task meta to attribute them to).
Add two coupled pre-teardown steps, both scoped to this task's exact
branch/head or worktree/tasktmp so they can never touch another task's
run or processes:
- conclude_task_no_mistakes_run aborts a run parked at a gate via
`no-mistakes axi abort`, cd'd into the exact worktree so the daemon
resolves the run itself rather than teardown naming a --run id.
- reap_task_worktree_processes sweeps for processes whose cwd is under
the worktree or tasktmp (via `lsof -a -d cwd`) and TERM/KILLs them.
Both run before any worktree return, branch delete, or backend kill,
and are idempotent on a retried teardown. The branch+head attribution
logic is factored out of bin/fm-crew-state.sh into the new shared
bin/fm-nm-run-lib.sh so both scripts use the same ownership contract.
* no-mistakes(review): Fail closed on incomplete teardown cleanup
* no-mistakes(review): Bind teardown cleanup to verified run and process identities
* no-mistakes(review): Require confirmed aborts and convergent identity-safe process reaping
* no-mistakes(review): Handle process exits during teardown identity checks
* no-mistakes(review): Restore teardown library in hermetic gotmp fixtures
* no-mistakes(document): Document teardown run attribution and timeout
* no-mistakes(lint): Rename shell variable conflicting with done keyword
* no-mistakes: apply CI fixes
* fix: resolve fm-remote-entrypoint.sh SCRIPT_DIR through a PATH symlink (#1709)
The script installs as a symlink under ~/.local/bin. Taking dirname of
the symlink itself (instead of its real target) pointed SCRIPT_DIR at
~/.local/bin, breaking sourcing of the sibling fm-remote-job-lib.sh.
Resolve the real path first, preferring python3's os.path.realpath,
then realpath, falling back to the raw BASH_SOURCE on hosts with
neither.
* fix(pi): gate Calm built-in overrides by activation state (#1724)
* fix(pi): stop Calm claiming a built-in tool name another extension owns
fm-calm.ts claimed bash/read/edit/write/grep/find/ls unconditionally at
extension load, regardless of whether Calm was on. Pi resolves two
extensions registering the same built-in name by first-registered-wins
with no merge and no unregister call, and Calm's project-local
.pi/extensions/ position beats any global or CLI-configured extension,
so a user who never even enabled Calm could have their own bash/read/etc
override silently replaced.
Captain-approved plan implemented:
- Registration is now gated on config/calm already being "on" at load
time. A Calm-off session or reload registers nothing, so a non-Calm
user never contests a name. This stays synchronous during the
factory's own load, not deferred to session_start: /reload (and
ctx.newSession/fork/switchSession) render the restored transcript from
a pre-session_start snapshot of the tool registry, so a deferred claim
would miss that render - confirmed by tests/fm-calm-pi-extension
.test.sh's hidden-block-geometry E2E when trialed.
- The first time Calm turns on in a session that started off
(activateBuiltInsIfNeeded, from the /calm command handler), Calm calls
pi.getAllTools() - safe only once every extension has finished loading,
unlike the load-time path above - to see whether a different extension
already owns a name, and skips claiming only that one, leaving it and
its owning extension fully intact and callable.
- A contested name found this way prints a prominent ctx.ui.notify()
warning naming the tool, plus a console diagnostic.
- reportBuiltInLosses() remains the backstop for the one case neither of
the above can reach: a session that starts or reloads with Calm already
on, where the registry snapshot is taken before Calm gets any chance to
check ownership. A symlink-safe realpath comparison avoids misreporting
Calm's own registration as foreign when its path crosses a symlink
(macOS /tmp, /var).
Confirmed, bounded trade-off: the very first time a session that started
Calm-off turns Calm on, tool-call rows already on screen from before that
toggle do not retroactively collapse, because Pi never lets an extension
re-point an already-rendered row at a definition registered later. Every
session after that first toggle starts with the preference already on and
takes the synchronous load-time path, so the guarantee is intact from
then on. docs/calm.md and the file's own header document this in full.
tests/fm-calm-pi-extension.test.sh gains test_builtin_gate_load_time
(config/calm off registers nothing, on registers all 7 synchronously at
load) and test_calm_activation_collision_and_regression_bound (first
activation claims every uncontested built-in, leaves a foreign bash tool
fully intact and callable, warns and logs the contested name, and locks
in the documented pre-activation bound against real ToolExecutionComponent
rendering). test_rendering_and_session_lifecycle and the live interactive
E2E are updated for the new gate-at-load and first-activation-bound
contract.
* no-mistakes(document): Document Calm tool collision boundaries
* no-mistakes: apply CI fixes
* fix(bin): persist secondmate parent bindings for cleanup (#1727)
* fix(bin): give secondmate homes a durable parent binding record
Finished-worker cleanup on a remote second mate refused forever with
"cannot resolve the primary home ... durable parent binding". The
remote launch hands the child the remote code checkout as its parent
home (fm-spawn.sh's sole writer of FM_PUBLIC_FOLLOWUP_PRIMARY_HOME
receives FM_HOME=$FM_ROOT from fm-remote-secondmate-control.sh's
host-local launch), and that path can never carry the parent's real
records, so the guard refused unconditionally once relay looked active
anywhere on that host.
fm-home-seed.sh and fm-remote-home-provision.sh now write a durable
.fm-secondmate-parent record next to the .fm-secondmate-home identity
marker, naming the home's route to its parent as local (with the real
parent path) or remote (with the parent's SSH alias for diagnostics
only). fm-teardown.sh's cleanup gate reads it: a remote parent is out
of scope for the delegated-promise check (the whole promised-public-
reply subsystem is same-filesystem by construction, so a remote parent
can never hold one), while a token committed directly to the child's
own .env file - never the process environment - still refuses, so an
unrelated export in the remote host's login shell can no longer mask
in. For a local secondmate, the durable parent_home now also backs up
the launch-time env var, closing a silent fail-open where a restart
that dropped the launch prefix made the guard treat a genuinely active
parent relay as off.
Regression coverage drives the real remote route (SSH boundary + Herdr
fixture) and real fm-home-seed.sh seeding rather than hand-crafted
markers.
* no-mistakes(review): Captain: fail closed on unsafe durable parent records
* no-mistakes(review): Captain: enforce durable parent binding commit protocol
* no-mistakes(review): Captain: publish local parent binding before identity
* no-mistakes(review): Captain: refuse conflicting local parent bindings
* no-mistakes(review): Captain: reject non-regular secondmate seed leaves
* no-mistakes(review): Captain: enforce unique durable parent bindings
* no-mistakes(review): Captain: reject route-incompatible durable parent fields
* no-mistakes(document): Document durable secondmate parent bindings
* no-mistakes(lint): Fix secondmate parent parser ShellCheck warnings
* no-mistakes: apply CI fixes
* feat(bin): enforce latest AXI-family tool floors (#1733)
* feat(bin): gate lavish-axi at its session_ended floor in bootstrap
bin/fm-procevent-lavish.sh decides that a human "Send & End" review is
terminal by reading session_ended from the poll response's leading session
block. That field first shipped in lavish-axi 0.1.35, so an older installed
build silently leaves every ended review source armed forever and captures
an empty ended result on each later cycle. The same release is what makes a
plain reopen refuse a session the human deliberately ended.
Add LAVISH_AXI_MIN=0.1.35 to the existing axi-family floor structure in
bin/fm-bootstrap.sh, reusing tool_version_at_least and the same MISSING
diagnostic gh-axi already emits, so an incompatible build is reported as an
upgrade request before any review surface is armed. Later lavish-axi
releases only add artifact-authoring surface the adapter never reads, so
the floor is the feature-introduction point rather than latest.
Fixtures that stubbed lavish-axi as a bare exit-0 tool would now be read as
unparseable builds, so tests/lib.sh gains fm_fake_version_tool and every
bootstrap-running suite uses it for lavish-axi.
* no-mistakes(review): Clarify lavish-axi version floor rationale
* no-mistakes: apply CI fixes
* feat(bin): set axi-family floors to current latest under the bump policy
The axi-family bootstrap floors are the CURRENT LATEST published version of
each tool, captain-bumped periodically to move the whole fleet onto the
newest axi tools. They are not the minimum feature-introduced version. The
earlier lavish-axi work set a feature-minimum floor, which is the opposite
of this policy, so replace it along with the older feature-minimum rationale
carried by tasks-axi and quota-axi.
State the policy explicitly in bin/fm-bootstrap.sh's header, which owns it,
and in each per-tool floor owner, so no future change argues a floor back
down to the earliest release that happens to satisfy some behavior. Remove
the lavish-axi session_ended and upstream-PR citation, the tasks-axi
multi-ID-mv minimum argument, and the quota-axi credential-source argument
as floor rationale; the tasks-axi feature probes remain as a separate
defense-in-depth concern.
Floors: lavish-axi 0.1.45 (was 0.1.35), tasks-axi 0.2.4 (was 0.2.2),
quota-axi 0.1.17 (was 0.1.16), gh-axi 0.1.29 unchanged and already latest.
Each was verified against the tool's current published version.
The mechanism is unchanged: the same shared version helper and the same
MISSING diagnostic path. The below-fires and at-or-above-silent regression
rows move to the new floors, keeping each boundary genuine by pinning the
patch immediately below each floor rather than a version that was only
below the old one. Fleet fixtures move to the new floors so a bootstrap-
running suite is not reported as an out-of-date build.
Three operator-facing backlog handoff and receipt errors named "0.2.2+"
while the enforced floor moved, so they now point at the floor's owner
instead of duplicating a version number that drifts.
* no-mistakes(review): Centralize AXI floor policy beside constants
* no-mistakes(review): Clarify bootstrap boundary test comment
* no-mistakes(document): Centralize AXI floor policy rationale
* fix(bin): bound open decision scans with incremental cursors (#1737)
* fix(bin): bound OPEN DECISIONS scan cost with a per-status-file cursor
The fleet-wide OPEN DECISIONS scan added in #1711 re-reads and refolds
every task's entire lifetime status log on every drain, so its cost
grows unbounded with total log size. Add status_open_decisions_incremental
and scan_open_decisions_incremental to fm-classify-lib.sh: they persist a
per-status-file byte cursor plus the folded open-decision set, and fold
only newly appended bytes on each call, reusing status_open_decisions'
exact fold-line rule (extracted into _fm_decision_fold_line) so the two
strategies can never disagree on what is open. A missing or invalidated
cursor (new task, truncated/rewritten/shrunk log) falls back to a full
re-fold. bin/fm-wake-drain.sh now calls the incremental wrapper instead
of the whole-file scan.
* fix(bin): add O(1) rotation detection and read-failure guarding to the cursor fold
Add the two pieces the incremental open-decisions cursor was missing,
scoped to this repo's actual status-file usage (create-once, append-only,
never replaced or rewritten in place):
- An O(1) device+inode identity check (one stat call) alongside the
existing size-shrink check, so a status file replaced/rotated/recreated
at the same path is detected and falls back to a full re-fold, even
when the replacement is the same size. A same-inode, same-size,
in-place byte edit is a deliberately accepted gap: no code path in
this repo ever does that to a status file.
- Checked reads: a stat/wc/tail failure is a genuine I/O error, not
"the file is empty" - it now reports the already-trusted persisted
open set unchanged instead of risking a silent invalidation.
Both stay O(1) plus new bytes per call, matching the cursor's bounded-
cost design; no content hashing or pending-fragment machinery.
* no-mistakes(review): Preserve cursor state across failed incremental reads
* no-mistakes(review): Refold status when cursor cache reads fail
* no-mistakes(document): Document cursor-backed open-decision scanning
* no-mistakes: apply CI fixes
* fix(bin): prevent remote polls from blocking session startup (#1754)
* fix(bin): preempt remote reply long-polls for queued short jobs
Session start on a home with live remote second mates could stall silently
for many minutes: the single serial remote job worker ran each armed
fm-remote-delta-read.sh reply poll to its full 55s window while bootstrap's
short sync, inherit, state, and route commands sat queued behind it, and
non-FIFO queue pickup let re-armed polls keep winning the lane. Measured
end to end, a trivial short job took 31s behind one 30s poll window.
The worker now preempts a running preemptible job (the read-only, cursor-
anchored delta read is the only member of that class) as soon as a
non-preemptible job is queued, publishing exit 75 with emptied output -
byte-identical to the poll's own elapsed-window-with-no-data result - so
the parent runner takes its existing no-result path and the watcher re-arms
from the same cursor with nothing lost. The delta read translates SIGTERM
into that same exit after removing its staging directory. Sibling polls
never preempt each other, so two armed monitors cannot churn. The same
measured scenario now completes in 1s.
* no-mistakes(document): Clarify remote poll preemption documentation
* docs: present X mode as the X and Discord public surface (#1778)
Discord mentions already ride the same pairing-token opt-in, relay poll,
and platform-aware reply path as X mentions, but the docs still read as
X-only, so a stranger could not self-serve the Discord path.
Add the numbered turn-on steps to the X mode configuration reference,
pointing at the myfirstmate dashboard for account creation, bot install,
and token issuance rather than duplicating operator setup here, and drop
the X-only framing from the README bullet, the documentation index, and
the architecture overview.
* fix(bin): run session start deterministically from hooks (#1781)
* feat(bin): run session start deterministically on hook-capable harnesses
Session start relied on a native nudge that only asked the agent to run
bin/fm-session-start.sh, and an agent can defer that. Observed 2026-08-01:
an /ahoy-first session followed the recap path and did not take the helm
until a later request forced it.
Claude, Codex, and Pi now RUN the digest in their session-open hook through
the new bin/fm-sessionstart-run.sh, so the full ordered digest is in model
context before the first turn. That wrapper is the single owner of what a
session-open source means: startup and Pi's "new" take the helm, clear and
compact re-emit, resume/reload/fork delegate to the nudge, and an unreadable
source takes the helm because doing that redundantly is idempotent while
skipping it is the bug. Grok and OpenCode keep the nudge as the floor, since
neither can carry hook stdout into a model turn.
Because the hook now blocks session initialization, fm-session-start.sh
bounds itself first. Its steps are not all individually bounded - bootstrap's
gh auth probe, tool version probes, the backlog listing and per-task endpoint
reads are unbounded - so the whole digest runs as one bounded child (default
120s). Whatever it emitted before the bound survives, and the parent adds a
loud STARTUP TRUNCATED banner naming the stage that stalled and every stage
that never ran, still exiting 0.
--reemit skips only the sweeps startup already reconciled. It still re-verifies
lock ownership and still drains queued wakes, which arrived after startup and
are the turn's work. fm-bootstrap.sh gains FM_BOOTSTRAP_LOCKED so a re-emit
keeps repair ownership instead of deferring to a lock holder that is itself.
Also adds bin/fm-timeout-lib.sh as the single owner of bounded execution,
replacing three near-identical copies, and gives the ahoy skill a helm check
so a nudge-tier harness cannot recap before taking the helm.
Verified live on 2026-08-05 against Claude…
blazingbunny
pushed a commit
to blazingbunny/firstmate
that referenced
this pull request
Aug 9, 2026
…d#1563) * feat(bin): require an explicit ship delivery mode in fm-brief A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions. * feat(bin): require an explicit ship delivery contract at spawn and promotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode. * docs: record the explicit per-task delivery contract AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly. * test: pass ship delivery flags per call site in the Herdr launcher e2e The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper. * test: pass the ship delivery contract in the secondmate suites Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires.
blazingbunny
added a commit
to blazingbunny/firstmate
that referenced
this pull request
Aug 9, 2026
) * docs: define captain instruction precedence (#1362) * docs: add captain-authorized inherent red-check merge exception Keep the default red-PR ban and own one always-loaded exception in the merge-authority section: captain-explicit PR or bounded batch plus exact check, only when the failure is inherent to the selected delivery path. Yolo cannot activate it; final head and the full current check suite must be verified; other substantive failures remain non-waivable. * docs: replace narrow red-check exception with captain precedence Supersede the inherent failing-check merge exception with one always-loaded Firstmate-local rule: a current explicit concrete captain instruction overrides a conflicting Firstmate-written standing rule only within exact scope, never above platform/system/developer instructions. Keep the ordinary red-PR default and yolo boundary; point section 7 at the section 1 owner. * docs: define validation supersession sequence (#1407) * fix: give validation-time captain overrides a supersession sequence The Validate section let a captain instruction that completely invalidates the work being validated keep the same task and worker, but never said how: the adjacent rule flatly bans hand-editing, committing, aborting, or restarting during an active run with no carve-out, so a worker facing full invalidation had no sanctioned path forward. Add the missing sequence: cancel through no-mistakes axi's abort command, confirm the run has stopped through axi status, recover branch ownership through axi sync's guarded recovery, only then replace the obsolete work, and validate once against the final head. The existing ban on hand-editing an active run now cross-references this sequence instead of contradicting it. * no-mistakes(review): Make validation custody recovery conditional * no-mistakes(document): Clarify validation supersession abort exception * fix: keep obsolete pipeline commits out of the superseded deliverable The review-applied fix made custody recovery conditional on branch_sync.next_action.code, but left an open gap: recovering custody settles who owns the branch, not what content ships. As written, a worker could recover an obsolete run's branch and build the replacement on top of its now-irrelevant commits instead of from the correct pre-invalidation base, carrying obsolete content into the final deliverable. Make that explicit: custody recovery settles ownership, not content, so the worker replaces obsolete work from the correct base and keeps the obsolete run's commits out of what gets validated and shipped. * no-mistakes(test): Restore minimal pre-invalidation replacement instruction * fix: dedupe redundant "replace the obsolete work" restatement Line 309 already says the worker replaces the obsolete work from the correct pre-invalidation base, excluding the obsolete commits. The closing sentence restated "replace the obsolete work" again before gating the final validation run, layering the same fact twice instead of stating it once. Trim the closing sentence to just the ownership gate and the single-run-against-final-head requirement it uniquely adds. * fix: bind backend overrides to exact-task authority (#1413) * fix: bind explicit --backend to exact-task authority A Herdr-backed second mate carried a prior one-task --backend tmux exception forward by analogy, so its child landed in tmux and never appeared under the second mate in Herdr. Runtime detection was correct; the authority surface was not. docs/configuration.md now owns that an explicit --backend is authorized only for that exact task. AGENTS.md and fm-spawn help point there. * no-mistakes(document): Consolidate backend selection authorization documentation * fix(herdr): prevent focus flashes during projected workspace cleanup (#1229) * fix: remove projected workspaces through Herdr's focus-preserving pane-death path Herdr 0.7.5's explicit close of a workspace-emptying last pane moves the attached client's focus to a neighbor workspace, flashing the captain's whole window and routing in-flight keystrokes to the wrong pane until Firstmate's exact-tab restore masks it 56-197 ms later. Teardown and cleanup now plan a workspace-emptying close as a focus-safe removal: verify the close empties the workspace, reposition the doomed workspace behind the focused one through the verified workspace.move transport when it sits before a non-last focused workspace, prove the pane holds one lone idle shell, and end that shell so Herdr removes the emptied workspace through its focus-preserving pane-death path. Any ambiguity or failure falls back to the plain close behind the existing restore backstop, and fm_backend_herdr_kill applies the same plan for non-projected removals. Two conditions proven on real hardware are encoded in the adapter: BSD ps reports a login shell's comm as "-zsh", and an idle shell transiently hosts a prompt helper right after a workspace.move relayout, absorbed by a bounded strict-sample settle window in the idle-shell proof, now the single owner shared with session-start cleanup. An isolated-lab regression reproduces the raw steal on 0.7.5 and proves the plan removes a doomed workspace with zero wrong-focus samples and no corrective focus; unit fixtures cover the position, edge, ambiguity, move and kill failure, escalation, and transient-helper cases. Upstream fixes (#1877 explicit close, #1912 pane death) are merged but unreleased; once released the plan degrades to a harmless reorder-then-remove. * no-mistakes(review): Confirm pane death from structured not-found responses * no-mistakes(review): Serialize Herdr kills and sample focus continuously * no-mistakes(review): Synchronize Herdr focus evidence output * no-mistakes(review): Refuse unlocked Herdr pane closes * no-mistakes(document): Correct Herdr focus-safety documentation * no-mistakes: apply CI fixes * fix: never erase a Herdr task's records while its pane survives a refused close A transient presentation-lock contention could produce a completed teardown while the exact Herdr pane stayed alive as an unowned restored shell: the kill refused the unlocked close (correctly), returned success, the warning was suppressed, and cleanup erased the task's status, turn-end, and metadata records after the isolated copy had already been returned. Teardown now acquires the named-session presentation lock before anything destructive: a contended lock refuses up front while the isolated copy, the task branch, every durable record, and the endpoint are all intact for a plain rerun, and the projected and flat close paths both run under that one held lock instead of acquiring their own. Durable records are erased only once the exact pane is confirmed gone through its structured presence; a refused, skipped, or failed close retains every record with a visible, retryable error, and after a skipped close (unresolvable lock path) only a structured pane_not_found counts as gone - unknown never does. The teardown regression drives a live contending lock holder end to end: the refusal touches nothing (no worktree return, no branch drop, no close attempt), and the retry after release returns the copy, closes the pane under the lock, and removes the records. The unconfirmed projected close now refuses with records retained, and the structured-presence gate has a strict/default unit matrix. * no-mistakes(review): Require structured pane-not-found before Herdr record removal * no-mistakes(document): Correct Herdr record-retention verification date * fix: refuse ambiguity, revalidate SIGKILL ownership, and roll back failed removals Three accepted-contract corrections from the post-CI personal review of the Herdr keep-spaces focus-flash mitigation. Ambiguous endpoint identity no longer counts as a confirmed-gone pane: a missing or malformed target refuses record removal in the structured presence gate, and teardown treats missing confirmation machinery as a refusal instead of skipping the gate, so only an exact structured pane_not_found ever erases durable task records. The pane-death SIGKILL escalation re-reads the exact pane's process information and refuses to signal unless the same shell pid still passes the strict bare-idle ownership proof, so a pid that exited and was reused by an unrelated process is never signaled; the refused escalation falls back to the plain close with the unrelated process untouched. A reposition whose removal is not confirmed no longer outlives the attempt: the emptying-close plan records the verified pre-move order and original index whenever it invokes the mover, and both close owners restore the exact original workspace order through a second verified move, under the same held session lock, before reporting the close as failed. Each defect was reproduced first: the unit matrix documented malformed identity as gone, the PID-reuse regression showed SIGKILL reaching a disowned pid, and the rollback regression showed a single unrestored move. Teardown-level regressions cover unparseable presence retention alongside the strict identity matrix. * no-mistakes(review): Require confirmed Herdr removal and resolvable teardown locks * no-mistakes(review): Enforce structured Herdr closes and teardown preflight * no-mistakes(review): Preflight explicit Herdr close confirmation helper * no-mistakes(document): Document Herdr rollback failure semantics * no-mistakes(review): Captain, harden recursive Herdr teardown safety * no-mistakes(document): Document recursive Herdr teardown evidence * fix: retain nested secondmate home when a recursive child cleanup fails Captain-decided Option A correction for nm-askuser-flash-r6, found during complete-diff rereview of the merged head. cleanup_firstmate_home_children's recursive secondmate branch called itself for a nested child's home without checking the result, then unconditionally removed that home right after. remove_firstmate_home ends in an unconditional recursive delete with no check for leftover records, so a nested secondmate whose own Herdr grandchild failed its confirmed-gone check would have its entire home - retained grandchild records included - erased by the very next line. Guard the recursive call the same way every other fallible call in this function already is: || return 1, skipping remove_firstmate_home and leaving the nested home and its records for a safe rerun. Empirically, fm-teardown.sh's set -eu already halted the script on the prior unguarded call before reaching removal (verified by hand with the guard reverted, under both this session's bash and stock macOS bash 3.2) - the reachable behavior was already correct. The explicit guard is still applied exactly as decided: it matches every sibling call site in the function, and it stops the correctness of this path depending on errexit's well-known fragility under refactors (a wrapping if/&&, or a future subshell) rather than on an explicit check. Adds a teardown-level regression building on the existing direct-child Herdr fixtures: a top-level secondmate contains a nested secondmate, whose own Herdr child's close goes unconfirmed. Proves through the public fm-teardown.sh interface that the nested home, the nested secondmate's own record, and the grandchild's metadata and status all survive, and that the top-level secondmate's record survives too. * no-mistakes(document): Document nested Herdr teardown retention * fix: prioritize completion runway in quota-aware dispatch (#1431) * fix(dispatch): prioritize quota completion runway * no-mistakes(document): Document completion-aware quota runway selection * fix(bin): preserve full task contract in no-mistakes intent (#1447) * Preserve task contract in no-mistakes intent * no-mistakes(review): Preserve complete current task contract in no-mistakes intent * fix(bin): parse punctuated secondmate registry entries safely (#1452) * fix: centralize secondmate registry parsing * no-mistakes(review): Centralize secondmate registry binding validation * no-mistakes(review): Harden registry EOF and symlink validation * no-mistakes(review): Reject unreadable registries before parsing * no-mistakes(document): Document punctuation-safe secondmate registry validation * no-mistakes: apply CI fixes * feat(bin): add durable process-event supervision (#1483) * feat(procevent): supervise long-polling sources into durable events Firstmate had no way to wait on a blocking external process without holding a conversational turn. Add a domain-neutral process-to-event runner plus a thin adapter around the currently published `lavish-axi poll` interface: canonical physical source identity, one machine-wide owner per source, direct argv execution, and durable 0600 result capture before any event referencing it is published on the existing wake queue. No second notifier, no polling control plane, and no retry machinery. A captured result with no durable handled acknowledgement stays eligible for bounded re-announcement across any number of drains and restarts. Draining a wake before acting on it and then starting a replacement session resurfaces the same exact source and sequence, and never puts result payload text in an event line. `fm-procevent.sh handled <source-id> <sequence>` is the only thing that stops re-announcement: generation-keyed, private, path-safe, durable, and atomically idempotent, so a paired external effect gated on its first-time versus repeat report is never authorized twice. An acknowledgement is refused unless matching captured result and adapter records already exist, so a premature or mistyped call cannot suppress a future result. The source side is unchanged and still lossy: the published poll clears feedback destructively before returning it, so a result lost in that window is unrecoverable. This is never at-least-once, no-loss, or lossless, and the handled acknowledgement is not a generic exactly-once effect either - a crash between an external effect and its acknowledgement can still repeat that effect on replay. Integrate registered sources with watcher supervision, the guards, and recoverable secondmate teardown across nested homes, and cover source identity, lifecycle races, supervision, restart handling, and cleanup safety with regressions. * no-mistakes(review): Prevent Lavish prompt text from spoofing missing sessions * no-mistakes(review): Serialize publication and secure handled acknowledgements * no-mistakes(document): Document hardened process-event acknowledgement guarantees * fix(procevent): never reclaim a source whose owned group still runs A runner is its own process group leader and starts the blocking source in that group, but the claim records only the leader PID and its identity. If the leader died while the source child kept running, the missing PID was classified stale: reconciliation released the claim and started a second runner while the old blocking source was still consuming the same canonical source. For the Lavish adapter that means two destructive long polls racing on one review session, so it is not harmless process litter. It also contradicted the documented promise that ownership is never released until the whole group is gone. Ownership state now distinguishes a generation that is really gone from one whose leader crashed with its group still alive. Reconcile stops that surviving group and releases its exact generation before starting any replacement, and keeps the claim for a later cycle when it cannot prove the group stopped or another home owns it. Acquisition and `start` treat the same state as held rather than reclaimable. Signalling that group is safe precisely because only an absent leader reaches this state. A reused PID leaves the leader alive, so the identity comparison still classifies it stale or uncertain and no group signal follows, which keeps the existing PID-reuse refusal intact. Add a public-interface regression for the exact crash cut - SIGKILL only the leader, prove the child group survives, reconcile, and prove the old group is gone with no second source running - plus its counterexample that a generation with no leader and no surviving group is still reclaimed. Update the runner help, operating documentation, skill, and verification record where they described reclaim in terms of the leader alone. * no-mistakes(review): Enforce runner group ownership and detect poller overlap * no-mistakes(review): Isolate runner groups from unrelated caller processes * no-mistakes(document): Document isolated process-event runner launch * no-mistakes(lint): Suppress Perl literal ShellCheck false positive * fix(bin): retire terminal process events and surface queued wakes (#1500) * fix(bin): deliver process-event results and retire ended sources Two defects reproduced during a real Lavish adapter session. One human `Send & End` produced four captured results: the real feedback, then recurring empty ended sessions. The generic runner had no way to learn a source was finished, so every reconcile restarted a poll that returned immediately. The runner now asks the source's own adapter - `fm-procevent-<adapter>.sh terminal <result-file>` - and on exit 0 alone re-proves ownership, drops the registration, and releases its own claim under one source boundary. Terminal knowledge stays adapter-owned: for Lavish that is an ended session, a missing session, and the final feedback delivery the published poll marks with `session_ended`. An adapter with no terminal command keeps its source armed exactly as before. Capture before publication, captured-result durability, queued wake durability, bounded re-announcement, handled deduplication, one-owner ownership, and explicit idempotent retirement are all unchanged. A captured result queued its `check` wake durably, but a healthy watcher with a fresh beacon never delivered it; the result surfaced only after a manual drain. Publication happens outside the watcher (in the runner) or unconditionally (in reconcile), so the watcher had no newly actionable signal to report and never reached its rewake path. It now reports a queued-but-unsurfaced process-event record through the same actionable exit every other wake uses, deduplicated by the same `.seen-*` marker discipline the signal scan uses, so the record is always durable before it is suppressed. The durable queue remains the authority and no second notifier, poller, timer, queue, or adapter-specific wake path is added. Regressions cover both, driven end to end: an armed Lavish source against a stand-in for the published poll polls once, captures once, publishes one distinct event, and retires itself; two fixture adapters prove the terminal decision follows the adapter alone; and a real capture plus a real watcher prove one proactive wake before any drain, with no duplicate wake while the record stays queued or after it is acknowledged. * no-mistakes(review): Harden process-event retirement and proactive delivery * no-mistakes(review): Route process-event delivery through shared wake owner * no-mistakes(document): Clarify process-event delivery and retirement documentation * no-mistakes(lint): Fix ShellCheck control-flow warnings * no-mistakes(lint): Fix wake output status lint warning * perf: shard portable serial tests across CI runners (#1544) * perf(ci): shard the portable serial behavior lane across runners The Behavior portable serial job ran all 69 scripts of the serial remainder on one runner. The measured serial sum on run 30725985757 was 1143762 ms (19m04s) against a 20-minute timeout, so the job intermittently reached the cap and was cancelled with every step passing. Setup is only about 7s, so the cost is entirely test wall time. Split the lane into four separate-runner shards. Each shard is still strictly serial, and separate runners mean no two of these stateful scripts ever share a machine, so the split needs no concurrency isolation proof. Assignment is longest-processing-time bin packing over measured per-script duration hints, balancing every shard to 285941 ms (~4m46s) of expected work, and the timeout tightens from 20 to 15 minutes. bin/fm-test-run.sh owns the shard count and refuses a lane whose "ofN" disagrees with it, while ci.yml derives the same count from strategy.job-total rather than a literal, so changing it in either file alone fails the lane loudly instead of leaving part of the required suite unrun. --check-coverage additionally proves the shards are non-empty, disjoint, and exactly equal to the serial lane. No test is weakened, skipped, or removed. Also replace the wall-clock sleeps in the --jobs scheduler test fixture with an explicit signal handshake between the fixtures. The old 0.5s-versus-0.05s race failed on a loaded machine; the handshake passes under sustained CPU saturation. * no-mistakes(review): Correct portable serial shard balance evidence * no-mistakes(document): Document portable serial shard evidence accurately * fix(bin): correct session lock and attached watcher supervision (#1545) * fix(bin): identify harness sessions by path and report delivered wakes Two supervision faults, both reported by a contributor and both open on the default branch. Fault 1: the Stop auto-arm never claims the home. fm_harness_ancestry_pid() matched only the basename of `ps -o comm=`, and Claude Code's native installer names the per-session executable by its version (.../share/claude/versions/ 2.1.220), so that basename identifies nothing. Three real failure shapes follow: a version-named session is missed entirely and the hook exits 0 with the epoch never written (unconditional on Linux, where procps reports the kernel exec name and ignores argv[0]); a claude-named daemon that directly parents sessions wins the outermost-contiguous-claude rule ahead of the session itself; and a session that is both version-named and daemon-parented has its live lock reclaimed as stale and rewritten to the shared daemon pid, corrupting the home's ownership record. Harness identity now also reads whole components of the executable path and of argv[0], which is what both platforms still carry. Matching whole components only keeps that widening safe: bin/fm-claude-stop-autoarm.sh and ~/.claude/hooks scripts have no "claude" component. Ownership is then decided against the session's whole contiguous harness ancestry rather than one chosen pid, which is the honest form of the question the library already documents ("does the current process descend from that same harness?"). That subsumes the outermost-pid rule for Claude's nested bg-spare worker chain instead of reverting it, and lets a daemon-parented session recognize its own lock. Lock acquisition still writes the outermost pid of the run, the only pid that lives as long as the session. Fault 2: an attached arm reports a delivered cycle as FAILED. The watcher prints its one reason line to its own stdout, so only the arm that forked it can read that line; an arm that attached observes nothing but a released lock and called a completely successful cycle "cycle ended without an actionable reason". No supervision event was lost - the durable queue held it - but every harness protocol reads that line as "supervision is down" and directs a manual re-arm. The arm now resolves an unobservable close against the durable wake queue, which records every wake before the watcher prints it and whose sequence counter never rewinds, not even across a drain. A cycle the queue proves delivered a wake reports that wake and exits 0; a cycle whose records a handling turn already drained reports the delivery without inventing a reason line; only a cycle that delivered nothing is still the typed nonzero failure. Fixing it in the arm covers codex, opencode, pi, grok and kimi, not just the Claude Stop path. Regressions: tests/fm-session-lock-ancestry.test.sh pins both platforms' ps semantics behind a deterministic process table and runs the real Stop auto-arm in version-named, daemon-parented, and combined real process trees, each orphaned so the walk cannot escape the fixture. tests/fm-watch-arm.test.sh drives a real watcher and a real attached arm through a real wake. Every fault case fails on the previous code. * no-mistakes(review): Bind watcher delivery records to process identity * no-mistakes(review): Return validated watcher identity atomically * no-mistakes(review): Track watcher successors by PID and identity * no-mistakes(document): Consolidate watcher arm-cycle documentation ownership * fix(bin): harden Claude supervision auto-arm recovery (#1495) * fix(supervision): harden Claude auto-arm failure handling * no-mistakes(review): Guarantee automatic retry after Claude auto-arm failures * no-mistakes(review): Gate attended fail-open on verified supervision failure * no-mistakes(document): Document Claude auto-arm retry and guard scope * no-mistakes: apply CI fixes * fix(supervision): make Claude fail-open progression monotonic * no-mistakes(review): Preserve auto-arm failure episodes until verified watcher recovery * no-mistakes(review): Linearize auto-arm failure progression across existing locks * no-mistakes(review): Linearize positive recovery across shared failure episode lock * no-mistakes(review): Scope Claude recovery contention to Claude guard mode * no-mistakes(document): Align supervision auto-arm documentation * no-mistakes(review): Preserve actionable wakes despite healthy successors * no-mistakes(document): Refresh supervision auto-arm documentation * feat(bin): require an explicit per-task delivery contract (#1563) * feat(bin): require an explicit ship delivery mode in fm-brief A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions. * feat(bin): require an explicit ship delivery contract at spawn and promotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode. * docs: record the explicit per-task delivery contract AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly. * test: pass ship delivery flags per call site in the Herdr launcher e2e The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper. * test: pass the ship delivery contract in the secondmate suites Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires. * feat(bin): support remote secondmate homes (#1576) * Add generic remote secondmate transport * Add routed remote secondmate replies * Add remote outbox backlog handoff * Integrate remote secondmate lifecycle * no-mistakes(review): Fix remote snapshot and handoff races * no-mistakes(review): Serialize remote home provisioning transactions * no-mistakes(review): Harden remote lifecycle transaction boundaries * no-mistakes(review): Serialize remote lifecycle mutations and fail closed * no-mistakes(review): Close remote lifecycle and file race windows * no-mistakes(review): Serialize remote reply retirement and inheritance * no-mistakes(review): Harden remote transfer integrity and recovery * no-mistakes(review): Serialize remote respawn with registry retirement * no-mistakes(document): Document remote bootstrap convergence accurately * no-mistakes(document): Clarify skipped remote secondmate mutations * no-mistakes(lint): Resolve remote script ShellCheck warnings * no-mistakes: apply CI fixes * feat: add per-task trace context propagation (#995) * feat(spawn): propagate a native W3C traceparent to spawned agents Add a default-off capability that resolves one W3C traceparent for a task, injects it into the agent's pane shell as the TRACEPARENT environment variable immediately before launch, and records the identical value as traceparent= in state/<id>.meta, so an external observer that explicitly reads that env value or meta field can correlate a worker, a Secondmate, and their nested children into one trace with no collector, storage, UI, or vendor coupling. TRACEPARENT as an environment variable is a firstmate convention carrying a W3C-formatted value: W3C Trace Context standardizes the header, not an env var, and OpenTelemetry SDKs do not read it automatically, so a downstream must consume it deliberately; this feature parents no SDK span by itself. Identity is per task, not per spawn: the carrier is minted with random ids on the first spawn, adopted as a child (fresh span, same trace) for a nested spawn whose parent already holds one, and reused verbatim from the meta on relaunch, so a task keeps one stable logical identity across restarts. A malformed or all-zero inherited value is treated as absent and roots a fresh trace. A new root is sampled (01) - a sampling decision a downstream parent-based sampler honors, not a guarantee that any collector stores a span, and firstmate emits no spans; a child preserves the inherited flag. Trust boundary: a firstmate-minted root is random and reads no prompt, path, task prose, credential, or arbitrary environment key. An inherited TRACEPARENT is opaque caller-controlled data - up to 24 bytes of id passed through after syntax validation - so whoever set it controls those bytes, a bounded fixed-width channel rather than a general content or secret channel. The feature adds no OTEL_* variable, no tracestate, and no arbitrary environment injection; it runs no configurable or arbitrary command, only the fixed local od and tr (resolved from PATH) to read a few bytes of entropy - a small local pipeline with no network or watchdog and no hard latency guarantee. Any entropy or validation failure that returns omits the carrier without aborting the spawn. A default-off spawn leaves the generated meta and launch environment unchanged. Enablement is default-off (config/trace-context, or FM_TRACE_CONTEXT where a non-empty value overrides and unset or empty defers to the file) and is propagated into secondmate homes, taking effect at each agent's next launch: a Secondmate launched or relaunched after enablement carries the primary trace into its nested workers, while an already-running Secondmate roots new traces for its own workers until relaunched. Injection reuses the existing GOTMPDIR channel, so all spawn backends and harnesses and the ship, scout, and secondmate paths are covered. Covered by a pure-library suite and a spawn-path integration test (fake tmux plus a real worktree, hermetic against ambient FM_TRACE_CONTEXT) proving the recorded and injected carriers are identical and sent before launch, that default-off writes and injects neither, that a relaunch reuses the recorded carrier, and that an explicit FM_TRACE_CONTEXT overrides the file both ways; plus a source-owner inheritance test proving trace-context propagates and absence-mirrors through propagate_inheritable_config. Documentation follows the repository documentation-audiences contract: docs/trace-context.md is maintainer-architecture rationale, the configuration schema lives in docs/configuration.md, and the repeatable test evidence is separated into docs/verification/trace-context.md (maintainer-verification), registered in docs/documentation-audiences.json. * fix(spawn): propagate the effective trace-context decision to secondmates FM_TRACE_CONTEXT overrode trace context only in the process that read it. A newly launched secondmate decided enablement from the inherited config/trace-context file alone, so the override did not cross the primary-to-secondmate boundary: FM_TRACE_CONTEXT=off with the file present left the secondmate's nested workers traced (a broken kill switch), and FM_TRACE_CONTEXT=on with the file absent left them untraced despite the inherited carrier. Deliver the primary's effective decision to a newly launched secondmate as a normalized on/off FM_TRACE_CONTEXT in the launch prefix, so a FM_TRACE_CONTEXT override governs the nested primary -> secondmate -> worker chain both ways, not just the copied file. The value is bounded to the literal on/off and does not broaden environment injection; the already-running secondmate boundary is unchanged. Add a genuine two-level spawn regression that drives fm-spawn twice with the exact environment the primary injects into the secondmate and proves both divergent directions end to end. Correct the documentation that implied secondmate coverage on every backend, since orca and cmux reject secondmate spawns, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Clarify Secondmate trace-context launch snapshots * no-mistakes(document): Correct trace-context documentation ownership and relaunch semantics * fix(spawn): resolve the trace-context decision once for carrier and snapshot The effective trace-context decision was read twice per spawn: once inside fm_trace_context_resolve for the recorded carrier, and again for the secondmate FM_TRACE_CONTEXT launch snapshot. A config-file change between the two reads could pair a carrier with the opposite enable state - an injected carrier with an off snapshot, or no carrier with an on snapshot. Freeze the effective on/off decision once, drive the carrier resolution under that frozen FM_TRACE_CONTEXT so it cannot independently re-read the file, and reuse the same frozen decision for the secondmate launch snapshot. Add a spawn-path regression that drives the file-decided path and proves the recorded carrier and the delivered snapshot always agree, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Preserve legacy Secondmate trace boundary * no-mistakes(document): Correct trace-context verification comparison base * no-mistakes(review): Captain, prevent failed trace delivery metadata claims * no-mistakes(review): Captain, align trace-context tests and verification evidence * no-mistakes(document): Correct trace-context verification evidence * no-mistakes(lint): Suppress intentional ShellCheck literal-dollar warnings * no-mistakes(review): Captain: freeze trace context at session start * no-mistakes(test): Captain: stabilize scheduler test and document Kimi trace coverage * no-mistakes(document): Document trace-context safety boundaries * fix(trace): fail off on stale session snapshots Publish each home session decision atomically through a same-directory temporary file and bind it to the current session lock. A replacement failure can no longer leave an earlier on decision active in a later session; missing, stale, malformed, or unpublishable state defaults safely to off. Add regressions for read-only replacement and failed publication, update spawn and session-start fixtures for the lock-bound format, and refresh the architecture and verification records. * no-mistakes(review): Fix trace spawn failure independence and duplicate safety * no-mistakes(document): Refresh trace-context documentation and verification * no-mistakes(review): Clear partial backend input after failed trace submission * no-mistakes(review): Stop unsafe trace delivery before launch append * no-mistakes(document): Document unsafe trace delivery handling * fix(trace): bound each trace to one routed task, never the routing agent A persistent Secondmate holds its launch-time TRACEPARENT in the process environment for its whole life, and routed requests never replace it, so resolving new-task carriers from the ambient environment chained every routed task into one ever-growing trace per Secondmate with distinct parent ids. Resolve now reuses the task's recorded carrier or mints a fresh sampled root, never reading ambient TRACEPARENT, so each routed task is its own trace boundary while relaunch, recovery, and scout-to-ship promotion keep one stable per-task identity. The spawn regression models the reviewed scenario exactly: two unrelated tasks spawned sequentially through one persistent Secondmate environment record and inject distinct trace ids, adopt nothing from the Secondmate's carrier, and a relaunch of the first task reuses its original carrier verbatim. * docs(trace): define the per-task trace boundary The design contract is one task per trace: a persistent Secondmate is routing infrastructure with its own agent identity, never a shared trace root for the unrelated tasks routed through it. Root/recovery semantics replace the removed child-inheritance path, the sampling and safety sections drop inherited-carrier language because ambient TRACEPARENT is never read, and the verification page records the refreshed suite inventories including the two-task Secondmate boundary regression. * test(trace): adopt the explicit per-task delivery contract in spawn fixtures Rebasing onto current main brings the explicit per-task delivery contract: ship spawns now require --mode and --yolo instead of resolving them from the project registry. The trace spawn fixtures pass the same explicit contract canonical spawn tests use, preserving the per-task trace boundary coverage unchanged, and the verification page records the refreshed comparison base. * fix(bin): harden tmux agent liveness across harnesses (#1577) * fix(bin): classify tmux agent liveness independent of process titles `fm_backend_tmux_agent_state` attributed a pane solely from `#{pane_current_command}`, which is a process TITLE a harness can rewrite, not a structural fact. Claude Code 2.1.220 reports its version string there, so a live Claude endpoint classified `ambiguous`: the session-start secondmate liveness sweep could no longer see it, and any consumer that gates on a positive classification refuses outright. Read a second, independent name source: the kernel `comm` of every process in the pane tty's foreground process group. Either source naming a verified harness yields `alive`, because a false `dead` is the one verdict that can start a duplicate agent on a live worktree. Scoping to the foreground process group rather than the pane's descendants keeps a harness-named background process from faking an agent, and covers multi-process launchers (the Pi Launcher path) without a special case. Verified on 2026-08-03 against all seven adapters running for real on tmux 3.6a / macOS 26.5.2 arm64: claude 2.1.220, codex-cli 0.146.0, opencode 1.18.11, pi 0.82.0, pi-signed 0.82.0, grok 0.2.118, kimi 0.31.1 all classify `alive`, each attributed by a source independent of its title. Two tests, because they fail for different reasons: - tests/fm-tmux-agent-liveness.test.sh pins the logic with real processes and no harness, so it runs everywhere CI runs tmux. It drives the two name sources apart on purpose and asserts the divergence, so no case can go quietly vacuous. - tests/fm-harness-liveness-drift-live-e2e.test.sh relaunches every installed harness and fails naming the harness and version when one stops being attributed by a title-independent source. AGENTS.md section 4 carries the resulting standing rule, and firstmate-coding-guidelines owns how to satisfy it. * no-mistakes: apply CI fixes * docs: move the harness-dependent-check policy out of AGENTS.md The standing rule was stated in AGENTS.md section 4 with the mechanics in firstmate-coding-guidelines, which split one contract across two owners and charged every session for a rule that only fires when firstmate's own harness-dependent code is being changed. firstmate-coding-guidelines is now the single owner of both the rule and how to satisfy it: real-harness proof required, that proof authorized to spend tokens, structural signals preferred over vendor-rendered surfaces, and a guard that fails loudly naming the harness and version where a surface signal is unavoidable. No inline stub is left behind, because AGENTS.md already carries the load trigger for that skill in sections 7 and 13, so it is read before any change to firstmate's shared tracked material. Also records the cross-platform lesson the pipeline caught in the portable regression, and corrects that file's header: the divergence assertion lives on the version-string case, which diverges on both supported platforms, rather than on every case. * no-mistakes(review): Harden tmux liveness identity and drift validation * no-mistakes(document): Clarify cross-platform tmux liveness documentation * feat(bin): propagate trace context to remote secondmates (#1609) * feat(bin): trace remote secondmate routes and unify the inherit allowlist Per-task W3C trace context (#995) resolved and injected its carrier only at the local spawn path. A remote secondmate is routed through spawn_remote_secondmate, which returns long before that site and wrote its own metadata block, so a remote secondmate stayed silently untraced even with the capability enabled. The parent home still owns that task's identity, because it holds the metadata an observer reads. It now resolves the carrier against the task's own meta under its own frozen decision - reused verbatim on relaunch, freshly rooted otherwise, never adopting the parent process's ambient TRACEPARENT - and hands it to the configured host through a new fm-spawn --traceparent argument, accepted only for a secondmate launch and only as a strict W3C value. The remote host exports it at the same unconditional pre-launch site and reports back the carrier its endpoint actually holds, which the parent records, so an already-alive endpoint reports the identity its agent really received rather than one the parent merely intended. Disabled remains byte-identical and off. The remote inherit path also carried its own hardcoded copy of the inheritable config set, already drifted from FM_INHERITABLE_CONFIG by trace-context. Both remote ends now derive from that one declaration, so a future item cannot be sent by one side and refused by the other, and session-scoped enablement items are skipped on live convergence exactly as the local path skips them. Also fixes a latent stderr leak: an absent session lock printed a raw redirect failure, which the new remote resolve site made visible. Adds tests/fm-remote-secondmate-trace-context.test.sh, driving the real parent -> fm-on -> remote entrypoint -> control -> remote fm-spawn chain over the deterministic SSH boundary and reading the carrier back from the remote pane's own log. * no-mistakes(document): Clarify remote trace and allowlist contracts * feat(bin): preflight remote runtime tool paths (#1623) * feat(bin): widen the remote runtime PATH and add a remote doctor preflight The fixed remote entrypoint hard-coded a four-directory PATH, so a remote account whose tools live under nix or a per-user profile could not run basic Firstmate work without a login shell. The entrypoint now composes its child PATH from the code root's bin, the account's ~/.local/bin, the common package-manager directories that actually exist on the host, and the portable system tail, deduplicated and in a fixed order, still under env -i with the same variable allowlist and no shell command string. fm-remote-doctor.sh reports that exact PATH by inheriting it from its own entrypoint launch rather than recomposing it, so the ordering keeps one owner. It is read-only, reports where each required and optional tool resolved, and exits non-zero naming every required tool that did not. Remote seeding runs it as a preflight before anything is created on the host and restores the registry when it fails. * no-mistakes(review): Harden remote git authorization and missing-tool diagnostics * no-mistakes(document): Document remote PATH doctor and safe shims * no-mistakes(lint): Fix ShellCheck findings in remote path tests * no-mistakes(lint): Suppress exported fixture's false-positive ShellCheck warning * feat: gate remote second mates on Herdr readiness (#1639) * feat(bin): gate remote second mates on herdr readiness A remote second mate now always runs on the Herdr backend, whose server belongs to the host's GUI login session and therefore outlives the SSH connections that supervise it. fm-spawn's remote route forces that backend and the host-local control script refuses any other, so the requirement cannot be dropped from either side. fm-remote-doctor.sh becomes the single owner of what "ready" means. It keeps its PATH and tool reporting from #1623 and adds the Herdr, Aqua LaunchAgent, GUI-session, server-reachability, and entrypoint-symlink checks, tagging each gap fixable: or human: with the exact operator step. --fix closes only the automatable gaps - writing and loading the Aqua-scoped dev.firstmate.herdr launch agent, starting the server where no launch agent applies, and recreating the entrypoint symlink - then re-derives every check from the host, so a human gap is never presented as fixed. It never creates a login session, writes an auto-login password, or touches FileVault. Remote seed, remote spawn, and the startup liveness relaunch all run the same check, repair, re-check sequence through one shared library and fail closed with the doctor's own gap text. Recovery inherits the gate because it respawns through the same route. Tests drive the real doctor against a controlled account fixture with a private HOME, a state-backed launchctl, and a fake herdr, and prove the dangerous actions are never attempted. The remote lifecycle suites gain a stateful Herdr CLI fixture and answer the readiness gate at the SSH boundary, so they never inspect or repair the runner's own account. * no-mistakes(review): Validate launch-agent contract and confirm Herdr startup * no-mistakes(review): Validate loaded launch-agent contract before readiness * no-mistakes(review): Refuse legacy remote backends without altering routes * no-mistakes(review): Clarify conditional remote readiness repair sequence * no-mistakes(review): Repair remote readiness before liveness probing * no-mistakes(review): Preserve unknown seeds and reject legacy liveness * no-mistakes(document): docs: clarify remote Herdr backend ownership * fix: isolate remote secondmates in shared Herdr session (#1659) * Pin remote secondmates to fm-remote * no-mistakes(review): Fail closed on legacy remote Herdr endpoints * no-mistakes(review): Isolate fm-remote launch agent from interactive default * no-mistakes(document): Document shared remote Herdr retirement safety * feat: route remote commands through an Aqua job worker (#1660) * feat: run remote commands through Aqua job worker * no-mistakes(review): Enforce remote job deadlines and safe worker shutdown * no-mistakes(review): Refresh stale workers and harden dependency-free supervision * no-mistakes(review): Harden worker ownership recovery and shutdown quarantine * no-mistakes(review): Fix doctor bootstrap, harness repair, and output draining * no-mistakes(review): Probe doctor tools through authenticated worker bootstrap * no-mistakes(review): Refresh stale workers before doctor tool probes * no-mistakes(review): Recover stopped quarantines and extend job deadlines * no-mistakes(review): Separate queue and execution timeout windows * no-mistakes(review): Supervise Linux worker crashes and bind root identity * no-mistakes(review): Resolve authorized Nix profile bin links * no-mistakes(review): Clarify Nix path resolution documentation * no-mistakes(review): Harden PATH safety and nvm selection * no-mistakes(review): Honor nvm system defaults and refresh doctor digest * no-mistakes(review): Keep workers ready during active jobs * no-mistakes(review): Bound pre-execution validation by job timeout * no-mistakes(document): Clarify remote worker documentation * no-mistakes(lint): Fix remote worker ShellCheck diagnostics * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * fix: clarify remote doctor bootstrap path (#1691) * fix(bin): bound remote SSH dead-peer detection (#1699) * fix(remote): arm SSH dead-peer detection in fm-on.sh A vanished remote host mid-poll (a reboot, a dropped link) left ssh blocked indefinitely on a half-open TCP connection, because fm-on.sh's ssh invocation had no ServerAliveInterval/ServerAliveCountMax. This wedged the remote-reply ferry: fm-procevent.sh's runner blocked inside the ssh child and never reached its own no-result -> claim-release -> reconcile re-arm self-healing path, which otherwise already handles a nonzero exit with empty output correctly. Recovery required a manual retire and re-arm. Arm ServerAliveInterval=15 and ServerAliveCountMax=3 by default (bounded ~45s detection window), both overridable via FM_SSH_ALIVE_INTERVAL and FM_SSH_ALIVE_COUNT_MAX. This is a transport- level fix in fm-on.sh, so it covers every remote command routed through it, not just the reply ferry. The remote sshd answers keepalive probes independently of whatever the remote command is doing, so a legitimately long-but-alive command (a 55s poll, a clone, the doctor) is never falsely killed - only a truly vanished peer trips it, turning that case into a bounded, detectable ssh failure (exit 255) instead of an indefinite hang. Extends tests/fm-on.test.sh with a behavioral regression asserting a bounded, positive ServerAliveInterval/ServerAliveCountMax on the real ssh argv captured through the FM_SSH_BIN process seam, plus coverage that both are env-overridable. * no-mistakes(document): Document SSH dead-peer detection ownership * fix: report stale AXI tools during bootstrap (#1701) * feat(bootstrap): gate stale axi CLIs at the floors firstmate actually uses Add gh-axi 0.1.29 floor so bare --squash PR merges stop failing quietly on older builds. Raise tasks-axi to FM_TASKS_AXI_MIN=0.2.2 (multi-id mv) while keeping feature probes. Keep quota-axi at 0.1.16 after verifying schema 3 and per-model availability already ship there; runway remains optional. * no-mistakes(document): Clarify AXI compatibility documentation ownership * fix: prevent false watcher-down alarms in Claude sessions (#1661) * fix(guard): stop false watcher-down alarm mid-turn under Claude auto-arm bin/fm-guard.sh derived its watcher-health verdict from fm_watcher_healthy, which requires a live watcher process holding the home lock. Under the Claude Stop-hook auto-arm supervision model the watcher is armed at each turn end and exits on its wake, so it runs only between turns. Every guarded command run mid-turn therefore found no live watcher and printed the "WATCHER DOWN - SUPERVISION IS OFF" banner even though supervision was healthy. Because the episode key was derived from the beacon mtime (which the between-turns watcher advances every poll), the full banner re-printed on essentially every command, and the message always blamed a "fresh beacon" that was in fact fresh. Make the pull guard's health check model-aware via a new fm_watcher_supervision_verdict in bin/fm-wake-lib.sh: - Under the auto-arm model a beacon fresh within FM_GUARD_GRACE is healthy even with no live watcher process; only a beacon stale beyond grace (or absent) is a genuine lapse and alarms. - Under every persistent-watcher harness (codex foreground checkpoint, opencode/pi/grok background arm, tmux, unknown) a live identity-matched watcher with a fresh beacon is still required, unchanged. The banner now names the true failing condition, a missing live watcher process versus a genuinely stale beacon, instead of always blaming the beacon, and the once-per-episode dedup keys on that condition rather than the beacon mtime so a genuine lapse announces once and does not re-print each turn. The turn-end guard keeps the strict fm_watcher_healthy check because it fires at the turn boundary, where the auto-arm brings a fresh watcher up and it cooperates with that arm. fm_watcher_healthy itself is unchanged, so the arm layer's start/attach/replace decisions are unaffected. Tests in tests/fm-guard-stale-banner.test.sh cover the auto-arm healthy fresh-beacon-without-a-watcher case, the auto-arm stale-beacon alarm and its stable episode, the true-reason banner wording, and the reason-keyed episode surviving a beacon mtime change; existing persistent-model cases are pinned to that model. * no-mistakes(review): Pin secondmate supervision model to launched harness * no-mistakes(document): Align watcher documentation with model-aware supervision health * test: prevent fixture temporary directory leaks (#1704) * fix(tests): stop fixture-tempdir helper from self-deleting under command substitution fm_test_tmproot is almost always called as `TMP_ROOT=$(fm_test_tmproot prefix)`, which forks a subshell to capture its stdout. The old implementation set its EXIT cleanup trap inside that call, so the trap fired - and deleted the fixture root - the instant the subshell exited, before the real caller's own EXIT trap was ever installed. Every test using the documented call pattern leaked its fixture root on every run; two suites had already independently discovered and worked around this with ad-hoc mktemp calls. Registration now goes through a $$-keyed registry file instead of in-process state, since $$ resolves to the invoking shell's PID even inside the subshell. The real cleanup trap is armed once at source time (always the real caller, never a subshell) for EXIT, INT, and TERM. A best-effort orphan sweep on next source reaps marked fixture roots old enough to be from a killed prior run. Simplifies the two existing ad-hoc workarounds (fm-procevent.test.sh, wake-helpers.sh) back onto the shared helper now that it works correctly. * no-mistakes(review): Preserve live fixtures during orphan reaping * no-mistakes(review): Harden fixture ownership against PID reuse * no-mistakes(review): Secure cleanup registry against path precreation * no-mistakes(review): Make fixture registration transactional * no-mistakes(document): Documentation already matches fixture cleanup behavior * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * feat(herdr): enable presentation spaces by default (#1708) * feat(herdr): default presentation spaces on with an explicit opt-out Herdr's disposable one-task presentation workspace was opt-in through the presence of local config/herdr-presentation-spaces. It is now on by default, and a home opts out by writing "off" into that same file. Values are read with the whole-file whitespace-stripped convention the other scalar config items already use, plus case folding. An absent file, an empty file, and "on" all resolve on; only "off" opts out; an unrecognized value warns and keeps the default rather than failing a spawn over a purely visual setting. The empty file is exactly the historical opt-in form, so every home that had already enabled the projection stays enabled with no migration step, and no previously enabled home can be turned off by the flip. Because absence now means on at both ends, secondmate inheritance needs no item-specific convergence: mirroring an absent primary file converges a secondmate to the same default-on rather than turning its projection off, and only an explicit primary opt-out propagates the opt-out. The gate itself moves into fm_backend_herdr_presentation_enabled in the Herdr adapter so the semantics have one owner that regressions can exercise directly. * no-mistakes(document): Document Herdr default-on presentation safety --------- Co-authored-by: kunchenguid <kun-1@kunchenguid.com> * fix(bin): surface fleet-wide open decisions on every wake drain (#1711) * fix: surface consolidated open decisions on every wake-drain A needs-decision or blocked event buried under later, unrelated status appends was only ever shown via the last-line wake annotation, so a still-open captain decision could go silently missed even though status_open_decisions (fm-classify-lib.sh) already folds the whole status stream correctly and fleet-snapshot/bearings already reuse it. Wire that same fold into bin/fm-wake-drain.sh: a new fleet-wide scan_open_decisions wrapper scans every state/<id>.status, and fm-wake-drain.sh prints a separate, bounded OPEN DECISIONS section on every drain (including the empty-queue fast path), so session-start and every wake-handling turn surface it for free without duplicating the open/resolved fold itself. Heartbeat wakes drain through the same script, so this covers that surface too. Also tighten status_open_decisions' file guard to skip an unreadable status file instead of leaking a bash redirection error, now that a fleet-wide directory scan can reach files a single targeted read would not. * no-mistakes(review): Prevent status symlinks leaking open decisions * fix: drop unbounded perl subprocess from status symlink guard The review step's own symlink-safety auto-fix (O_NOFOLLOW read via a perl subprocess) forked one perl process per status file scanned by the new fleet-wide open-decisions scan, with no cap - inflating fm-wake-drain.sh's total external-read cost from 8 (the existing annotation read_cap) to 18 in the enrichment-caps regression test. The plain [ -L "$f" ] check already rejects any status file that is itself a symlink before any read happens, which is exactly what the new regression test exercises and is the same defense level the sibling scan_captain_relevant_statuses/last_status_line already rely on elsewhere in this file (no O_NOFOLLOW). Drop the subprocess-based nofollow read and keep the cheap builtin guard. * no-mistakes(document): Document actionable fleet-wide open decision drains * fix(bin): abort parked runs and reap leaked processes before teardown (#1710) * fix(bin): abort orphaned no-mistakes runs and reap leaked processes at teardown Teardown could remove a task's worker while its no-mistakes pipeline run was still parked at a gate, leaving an orphaned run holding a fleet slot indefinitely (observed 2026-08-03: runs parked 7h39m and parked at a post-CI approval gate). It could also leave backgrounded/disowned descendant processes rooted under the worktree or tasktmp surviving reparented to init (observed: two `go test` binaries pinning CPU for hours with no live task meta to attribute them to). Add two coupled pre-teardown steps, both scoped to this task's exact branch/head or worktree/tasktmp so they can never touch another task's run or processes: - conclude_task_no_mistakes_run aborts a run parked at a gate via `no-mistakes axi abort`, cd'd into the exact worktree so the daemon resolves the run itself rather than teardown naming a --run id. - reap_task_worktree_processes sweeps for processes whose cwd is under the worktree or tasktmp (via `lsof -a -d cwd`) and TERM/KILLs them. Both run before any worktree return, branch delete, or backend kill, and are idempotent on a retried teardown. The branch+head attribution logic is factored out of bin/fm-crew-state.sh into the new shared bin/fm-nm-run-lib.sh so both scripts use the same ownership contract. * no-mistakes(review): Fail closed on incomplete teardown cleanup * no-mistakes(review): Bind teardown cleanup to verified run and process identities * no-mistakes(review): Require confirmed aborts and convergent identity-safe process reaping * no-mistakes(review): Handle process exits during teardown identity checks * no-mistakes(review): Restore teardown library in hermetic gotmp fixtures * no-mistakes(document): Document teardown run attribution and timeout * no-mistakes(lint): Rename shell variable conflicting with done keyword * no-mistakes: apply CI fixes * fix: resolve fm-remote-entrypoint.sh SCRIPT_DIR through a PATH symlink (#1709) The script installs as a symlink under ~/.local/bin. Taking dirname of the symlink itself (instead of its real target) pointed SCRIPT_DIR at ~/.local/bin, breaking sourcing of the sibling fm-remote-job-lib.sh. Resolve the real path first, preferring python3's os.path.realpath, then realpath, falling back to the raw BASH_SOURCE on hosts with neither. * fix(pi): gate Calm built-in overrides by activation state (#1724) * fix(pi): stop Calm claiming a built-in tool name another extension owns fm-calm.ts claimed bash/read/edit/write/grep/find/ls unconditionally at extension load, regardless of whether Calm was on. Pi resolves two extensions registering the same built-in name by first-registered-wins with no merge and no unregister call, and Calm's project-local .pi/extensions/ position beats any global or CLI-configured extension, so a user who never even enabled Calm could have their own bash/read/etc override silently replaced. Captain-approved plan implemented: - Registration is now gated on config/calm already being "on" at load time. A Calm-off session or reload registers nothing, so a non-Calm user never contests a name. This stays synchronous during the factory's own load, not deferred to session_start: /reload (and ctx.newSession/fork/switchSession) render the restored transcript from a pre-session_start snapshot of the tool registry, so a deferred claim would miss that render - confirmed by tests/fm-calm-pi-extension .test.sh's hidden-block-geometry E2E when trialed. - The first time Calm turns on in a session that started off (activateBuiltInsIfNeeded, from the /calm command handler), Calm calls pi.getAllTools() - safe only once every extension has finished loading, unlike the load-time path above - to see whether a different extension already owns a name, and skips claiming only that one, leaving it and its owning extension fully intact and callable. - A contested name found this way prints a prominent ctx.ui.notify() warning naming the tool, plus a console diagnostic. - reportBuiltInLosses() remains the backstop for the one case neither of the above can reach: a session that starts or reloads with Calm already on, where the registry snapshot is taken before Calm gets any chance to check ownership. A symlink-safe realpath comparison avoids misreporting Calm's own registration as foreign when its path crosses a symlink (macOS /tmp, /var). Confirmed, bounded trade-off: the very first time a session that started Calm-off turns Calm on, tool-call rows already on screen from before that toggle do not retroactively collapse, because Pi never lets an extension re-point an already-rendered row at a definition registered later. Every session after that first toggle starts with the preference already on and takes the synchronous load-time path, so the guarantee is intact from then on. docs/calm.md and the file's own header document this in full. tests/fm-calm-pi-extension.test.sh gains test_builtin_gate_load_time (config/calm off registers nothing, on registers all 7 synchronously at load) and test_calm_activation_collision_and_regression_bound (first activation claims every uncontested built-in, leaves a foreign bash tool fully intact and callable, warns and logs the contested name, and locks in the documented pre-activation bound against real ToolExecutionComponent rendering). test_rendering_and_session_lifecycle and the live interactive E2E are updated for the new gate-at-load and first-activation-bound contract. * no-mistakes(document): Document Calm tool collision boundaries * no-mistakes: apply CI fixes * fix(bin): persist secondmate parent bindings for cl…
sbracewell64
added a commit
to sbracewell64/firstmate
that referenced
this pull request
Aug 9, 2026
…ks (#39) * feat(bin): require an explicit per-task delivery contract (kunchenguid#1563) * feat(bin): require an explicit ship delivery mode in fm-brief A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions. * feat(bin): require an explicit ship delivery contract at spawn and promotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode. * docs: record the explicit per-task delivery contract AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly. * test: pass ship delivery flags per call site in the Herdr launcher e2e The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper. * test: pass the ship delivery contract in the secondmate suites Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires. * feat(bin): support remote secondmate homes (kunchenguid#1576) * Add generic remote secondmate transport * Add routed remote secondmate replies * Add remote outbox backlog handoff * Integrate remote secondmate lifecycle * no-mistakes(review): Fix remote snapshot and handoff races * no-mistakes(review): Serialize remote home provisioning transactions * no-mistakes(review): Harden remote lifecycle transaction boundaries * no-mistakes(review): Serialize remote lifecycle mutations and fail closed * no-mistakes(review): Close remote lifecycle and file race windows * no-mistakes(review): Serialize remote reply retirement and inheritance * no-mistakes(review): Harden remote transfer integrity and recovery * no-mistakes(review): Serialize remote respawn with registry retirement * no-mistakes(document): Document remote bootstrap convergence accurately * no-mistakes(document): Clarify skipped remote secondmate mutations * no-mistakes(lint): Resolve remote script ShellCheck warnings * no-mistakes: apply CI fixes * feat: add per-task trace context propagation (kunchenguid#995) * feat(spawn): propagate a native W3C traceparent to spawned agents Add a default-off capability that resolves one W3C traceparent for a task, injects it into the agent's pane shell as the TRACEPARENT environment variable immediately before launch, and records the identical value as traceparent= in state/<id>.meta, so an external observer that explicitly reads that env value or meta field can correlate a worker, a Secondmate, and their nested children into one trace with no collector, storage, UI, or vendor coupling. TRACEPARENT as an environment variable is a firstmate convention carrying a W3C-formatted value: W3C Trace Context standardizes the header, not an env var, and OpenTelemetry SDKs do not read it automatically, so a downstream must consume it deliberately; this feature parents no SDK span by itself. Identity is per task, not per spawn: the carrier is minted with random ids on the first spawn, adopted as a child (fresh span, same trace) for a nested spawn whose parent already holds one, and reused verbatim from the meta on relaunch, so a task keeps one stable logical identity across restarts. A malformed or all-zero inherited value is treated as absent and roots a fresh trace. A new root is sampled (01) - a sampling decision a downstream parent-based sampler honors, not a guarantee that any collector stores a span, and firstmate emits no spans; a child preserves the inherited flag. Trust boundary: a firstmate-minted root is random and reads no prompt, path, task prose, credential, or arbitrary environment key. An inherited TRACEPARENT is opaque caller-controlled data - up to 24 bytes of id passed through after syntax validation - so whoever set it controls those bytes, a bounded fixed-width channel rather than a general content or secret channel. The feature adds no OTEL_* variable, no tracestate, and no arbitrary environment injection; it runs no configurable or arbitrary command, only the fixed local od and tr (resolved from PATH) to read a few bytes of entropy - a small local pipeline with no network or watchdog and no hard latency guarantee. Any entropy or validation failure that returns omits the carrier without aborting the spawn. A default-off spawn leaves the generated meta and launch environment unchanged. Enablement is default-off (config/trace-context, or FM_TRACE_CONTEXT where a non-empty value overrides and unset or empty defers to the file) and is propagated into secondmate homes, taking effect at each agent's next launch: a Secondmate launched or relaunched after enablement carries the primary trace into its nested workers, while an already-running Secondmate roots new traces for its own workers until relaunched. Injection reuses the existing GOTMPDIR channel, so all spawn backends and harnesses and the ship, scout, and secondmate paths are covered. Covered by a pure-library suite and a spawn-path integration test (fake tmux plus a real worktree, hermetic against ambient FM_TRACE_CONTEXT) proving the recorded and injected carriers are identical and sent before launch, that default-off writes and injects neither, that a relaunch reuses the recorded carrier, and that an explicit FM_TRACE_CONTEXT overrides the file both ways; plus a source-owner inheritance test proving trace-context propagates and absence-mirrors through propagate_inheritable_config. Documentation follows the repository documentation-audiences contract: docs/trace-context.md is maintainer-architecture rationale, the configuration schema lives in docs/configuration.md, and the repeatable test evidence is separated into docs/verification/trace-context.md (maintainer-verification), registered in docs/documentation-audiences.json. * fix(spawn): propagate the effective trace-context decision to secondmates FM_TRACE_CONTEXT overrode trace context only in the process that read it. A newly launched secondmate decided enablement from the inherited config/trace-context file alone, so the override did not cross the primary-to-secondmate boundary: FM_TRACE_CONTEXT=off with the file present left the secondmate's nested workers traced (a broken kill switch), and FM_TRACE_CONTEXT=on with the file absent left them untraced despite the inherited carrier. Deliver the primary's effective decision to a newly launched secondmate as a normalized on/off FM_TRACE_CONTEXT in the launch prefix, so a FM_TRACE_CONTEXT override governs the nested primary -> secondmate -> worker chain both ways, not just the copied file. The value is bounded to the literal on/off and does not broaden environment injection; the already-running secondmate boundary is unchanged. Add a genuine two-level spawn regression that drives fm-spawn twice with the exact environment the primary injects into the secondmate and proves both divergent directions end to end. Correct the documentation that implied secondmate coverage on every backend, since orca and cmux reject secondmate spawns, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Clarify Secondmate trace-context launch snapshots * no-mistakes(document): Correct trace-context documentation ownership and relaunch semantics * fix(spawn): resolve the trace-context decision once for carrier and snapshot The effective trace-context decision was read twice per spawn: once inside fm_trace_context_resolve for the recorded carrier, and again for the secondmate FM_TRACE_CONTEXT launch snapshot. A config-file change between the two reads could pair a carrier with the opposite enable state - an injected carrier with an off snapshot, or no carrier with an on snapshot. Freeze the effective on/off decision once, drive the carrier resolution under that frozen FM_TRACE_CONTEXT so it cannot independently re-read the file, and reuse the same frozen decision for the secondmate launch snapshot. Add a spawn-path regression that drives the file-decided path and proves the recorded carrier and the delivered snapshot always agree, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Preserve legacy Secondmate trace boundary * no-mistakes(document): Correct trace-context verification comparison base * no-mistakes(review): Captain, prevent failed trace delivery metadata claims * no-mistakes(review): Captain, align trace-context tests and verification evidence * no-mistakes(document): Correct trace-context verification evidence * no-mistakes(lint): Suppress intentional ShellCheck literal-dollar warnings * no-mistakes(review): Captain: freeze trace context at session start * no-mistakes(test): Captain: stabilize scheduler test and document Kimi trace coverage * no-mistakes(document): Document trace-context safety boundaries * fix(trace): fail off on stale session snapshots Publish each home session decision atomically through a same-directory temporary file and bind it to the current session lock. A replacement failure can no longer leave an earlier on decision active in a later session; missing, stale, malformed, or unpublishable state defaults safely to off. Add regressions for read-only replacement and failed publication, update spawn and session-start fixtures for the lock-bound format, and refresh the architecture and verification records. * no-mistakes(review): Fix trace spawn failure independence and duplicate safety * no-mistakes(document): Refresh trace-context documentation and verification * no-mistakes(review): Clear partial backend input after failed trace submission * no-mistakes(review): Stop unsafe trace delivery before launch append * no-mistakes(document): Document unsafe trace delivery handling * fix(trace): bound each trace to one routed task, never the routing agent A persistent Secondmate holds its launch-time TRACEPARENT in the process environment for its whole life, and routed requests never replace it, so resolving new-task carriers from the ambient environment chained every routed task into one ever-growing trace per Secondmate with distinct parent ids. Resolve now reuses the task's recorded carrier or mints a fresh sampled root, never reading ambient TRACEPARENT, so each routed task is its own trace boundary while relaunch, recovery, and scout-to-ship promotion keep one stable per-task identity. The spawn regression models the reviewed scenario exactly: two unrelated tasks spawned sequentially through one persistent Secondmate environment record and inject distinct trace ids, adopt nothing from the Secondmate's carrier, and a relaunch of the first task reuses its original carrier verbatim. * docs(trace): define the per-task trace boundary The design contract is one task per trace: a persistent Secondmate is routing infrastructure with its own agent identity, never a shared trace root for the unrelated tasks routed through it. Root/recovery semantics replace the removed child-inheritance path, the sampling and safety sections drop inherited-carrier language because ambient TRACEPARENT is never read, and the verification page records the refreshed suite inventories including the two-task Secondmate boundary regression. * test(trace): adopt the explicit per-task delivery contract in spawn fixtures Rebasing onto current main brings the explicit per-task delivery contract: ship spawns now require --mode and --yolo instead of resolving them from the project registry. The trace spawn fixtures pass the same explicit contract canonical spawn tests use, preserving the per-task trace boundary coverage unchanged, and the verification page records the refreshed comparison base. * fix(bin): harden tmux agent liveness across harnesses (kunchenguid#1577) * fix(bin): classify tmux agent liveness independent of process titles `fm_backend_tmux_agent_state` attributed a pane solely from `#{pane_current_command}`, which is a process TITLE a harness can rewrite, not a structural fact. Claude Code 2.1.220 reports its version string there, so a live Claude endpoint classified `ambiguous`: the session-start secondmate liveness sweep could no longer see it, and any consumer that gates on a positive classification refuses outright. Read a second, independent name source: the kernel `comm` of every process in the pane tty's foreground process group. Either source naming a verified harness yields `alive`, because a false `dead` is the one verdict that can start a duplicate agent on a live worktree. Scoping to the foreground process group rather than the pane's descendants keeps a harness-named background process from faking an agent, and covers multi-process launchers (the Pi Launcher path) without a special case. Verified on 2026-08-03 against all seven adapters running for real on tmux 3.6a / macOS 26.5.2 arm64: claude 2.1.220, codex-cli 0.146.0, opencode 1.18.11, pi 0.82.0, pi-signed 0.82.0, grok 0.2.118, kimi 0.31.1 all classify `alive`, each attributed by a source independent of its title. Two tests, because they fail for different reasons: - tests/fm-tmux-agent-liveness.test.sh pins the logic with real processes and no harness, so it runs everywhere CI runs tmux. It drives the two name sources apart on purpose and asserts the divergence, so no case can go quietly vacuous. - tests/fm-harness-liveness-drift-live-e2e.test.sh relaunches every installed harness and fails naming the harness and version when one stops being attributed by a title-independent source. AGENTS.md section 4 carries the resulting standing rule, and firstmate-coding-guidelines owns how to satisfy it. * no-mistakes: apply CI fixes * docs: move the harness-dependent-check policy out of AGENTS.md The standing rule was stated in AGENTS.md section 4 with the mechanics in firstmate-coding-guidelines, which split one contract across two owners and charged every session for a rule that only fires when firstmate's own harness-dependent code is being changed. firstmate-coding-guidelines is now the single owner of both the rule and how to satisfy it: real-harness proof required, that proof authorized to spend tokens, structural signals preferred over vendor-rendered surfaces, and a guard that fails loudly naming the harness and version where a surface signal is unavoidable. No inline stub is left behind, because AGENTS.md already carries the load trigger for that skill in sections 7 and 13, so it is read before any change to firstmate's shared tracked material. Also records the cross-platform lesson the pipeline caught in the portable regression, and corrects that file's header: the divergence assertion lives on the version-string case, which diverges on both supported platforms, rather than on every case. * no-mistakes(review): Harden tmux liveness identity and drift validation * no-mistakes(document): Clarify cross-platform tmux liveness documentation * feat(bin): propagate trace context to remote secondmates (kunchenguid#1609) * feat(bin): trace remote secondmate routes and unify the inherit allowlist Per-task W3C trace context (kunchenguid#995) resolved and injected its carrier only at the local spawn path. A remote secondmate is routed through spawn_remote_secondmate, which returns long before that site and wrote its own metadata block, so a remote secondmate stayed silently untraced even with the capability enabled. The parent home still owns that task's identity, because it holds the metadata an observer reads. It now resolves the carrier against the task's own meta under its own frozen decision - reused verbatim on relaunch, freshly rooted otherwise, never adopting the parent process's ambient TRACEPARENT - and hands it to the configured host through a new fm-spawn --traceparent argument, accepted only for a secondmate launch and only as a strict W3C value. The remote host exports it at the same unconditional pre-launch site and reports back the carrier its endpoint actually holds, which the parent records, so an already-alive endpoint reports the identity its agent really received rather than one the parent merely intended. Disabled remains byte-identical and off. The remote inherit path also carried its own hardcoded copy of the inheritable config set, already drifted from FM_INHERITABLE_CONFIG by trace-context. Both remote ends now derive from that one declaration, so a future item cannot be sent by one side and refused by the other, and session-scoped enablement items are skipped on live convergence exactly as the local path skips them. Also fixes a latent stderr leak: an absent session lock printed a raw redirect failure, which the new remote resolve site made visible. Adds tests/fm-remote-secondmate-trace-context.test.sh, driving the real parent -> fm-on -> remote entrypoint -> control -> remote fm-spawn chain over the deterministic SSH boundary and reading the carrier back from the remote pane's own log. * no-mistakes(document): Clarify remote trace and allowlist contracts * feat(bin): preflight remote runtime tool paths (kunchenguid#1623) * feat(bin): widen the remote runtime PATH and add a remote doctor preflight The fixed remote entrypoint hard-coded a four-directory PATH, so a remote account whose tools live under nix or a per-user profile could not run basic Firstmate work without a login shell. The entrypoint now composes its child PATH from the code root's bin, the account's ~/.local/bin, the common package-manager directories that actually exist on the host, and the portable system tail, deduplicated and in a fixed order, still under env -i with the same variable allowlist and no shell command string. fm-remote-doctor.sh reports that exact PATH by inheriting it from its own entrypoint launch rather than recomposing it, so the ordering keeps one owner. It is read-only, reports where each required and optional tool resolved, and exits non-zero naming every required tool that did not. Remote seeding runs it as a preflight before anything is created on the host and restores the registry when it fails. * no-mistakes(review): Harden remote git authorization and missing-tool diagnostics * no-mistakes(document): Document remote PATH doctor and safe shims * no-mistakes(lint): Fix ShellCheck findings in remote path tests * no-mistakes(lint): Suppress exported fixture's false-positive ShellCheck warning * feat: gate remote second mates on Herdr readiness (kunchenguid#1639) * feat(bin): gate remote second mates on herdr readiness A remote second mate now always runs on the Herdr backend, whose server belongs to the host's GUI login session and therefore outlives the SSH connections that supervise it. fm-spawn's remote route forces that backend and the host-local control script refuses any other, so the requirement cannot be dropped from either side. fm-remote-doctor.sh becomes the single owner of what "ready" means. It keeps its PATH and tool reporting from kunchenguid#1623 and adds the Herdr, Aqua LaunchAgent, GUI-session, server-reachability, and entrypoint-symlink checks, tagging each gap fixable: or human: with the exact operator step. --fix closes only the automatable gaps - writing and loading the Aqua-scoped dev.firstmate.herdr launch agent, starting the server where no launch agent applies, and recreating the entrypoint symlink - then re-derives every check from the host, so a human gap is never presented as fixed. It never creates a login session, writes an auto-login password, or touches FileVault. Remote seed, remote spawn, and the startup liveness relaunch all run the same check, repair, re-check sequence through one shared library and fail closed with the doctor's own gap text. Recovery inherits the gate because it respawns through the same route. Tests drive the real doctor against a controlled account fixture with a private HOME, a state-backed launchctl, and a fake herdr, and prove the dangerous actions are never attempted. The remote lifecycle suites gain a stateful Herdr CLI fixture and answer the readiness gate at the SSH boundary, so they never inspect or repair the runner's own account. * no-mistakes(review): Validate launch-agent contract and confirm Herdr startup * no-mistakes(review): Validate loaded launch-agent contract before readiness * no-mistakes(review): Refuse legacy remote backends without altering routes * no-mistakes(review): Clarify conditional remote readiness repair sequence * no-mistakes(review): Repair remote readiness before liveness probing * no-mistakes(review): Preserve unknown seeds and reject legacy liveness * no-mistakes(document): docs: clarify remote Herdr backend ownership * fix: isolate remote secondmates in shared Herdr session (kunchenguid#1659) * Pin remote secondmates to fm-remote * no-mistakes(review): Fail closed on legacy remote Herdr endpoints * no-mistakes(review): Isolate fm-remote launch agent from interactive default * no-mistakes(document): Document shared remote Herdr retirement safety * feat: route remote commands through an Aqua job worker (kunchenguid#1660) * feat: run remote commands through Aqua job worker * no-mistakes(review): Enforce remote job deadlines and safe worker shutdown * no-mistakes(review): Refresh stale workers and harden dependency-free supervision * no-mistakes(review): Harden worker ownership recovery and shutdown quarantine * no-mistakes(review): Fix doctor bootstrap, harness repair, and output draining * no-mistakes(review): Probe doctor tools through authenticated worker bootstrap * no-mistakes(review): Refresh stale workers before doctor tool probes * no-mistakes(review): Recover stopped quarantines and extend job deadlines * no-mistakes(review): Separate queue and execution timeout windows * no-mistakes(review): Supervise Linux worker crashes and bind root identity * no-mistakes(review): Resolve authorized Nix profile bin links * no-mistakes(review): Clarify Nix path resolution documentation * no-mistakes(review): Harden PATH safety and nvm selection * no-mistakes(review): Honor nvm system defaults and refresh doctor digest * no-mistakes(review): Keep workers ready during active jobs * no-mistakes(review): Bound pre-execution validation by job timeout * no-mistakes(document): Clarify remote worker documentation * no-mistakes(lint): Fix remote worker ShellCheck diagnostics * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * fix: clarify remote doctor bootstrap path (kunchenguid#1691) * fix(bin): bound remote SSH dead-peer detection (kunchenguid#1699) * fix(remote): arm SSH dead-peer detection in fm-on.sh A vanished remote host mid-poll (a reboot, a dropped link) left ssh blocked indefinitely on a half-open TCP connection, because fm-on.sh's ssh invocation had no ServerAliveInterval/ServerAliveCountMax. This wedged the remote-reply ferry: fm-procevent.sh's runner blocked inside the ssh child and never reached its own no-result -> claim-release -> reconcile re-arm self-healing path, which otherwise already handles a nonzero exit with empty output correctly. Recovery required a manual retire and re-arm. Arm ServerAliveInterval=15 and ServerAliveCountMax=3 by default (bounded ~45s detection window), both overridable via FM_SSH_ALIVE_INTERVAL and FM_SSH_ALIVE_COUNT_MAX. This is a transport- level fix in fm-on.sh, so it covers every remote command routed through it, not just the reply ferry. The remote sshd answers keepalive probes independently of whatever the remote command is doing, so a legitimately long-but-alive command (a 55s poll, a clone, the doctor) is never falsely killed - only a truly vanished peer trips it, turning that case into a bounded, detectable ssh failure (exit 255) instead of an indefinite hang. Extends tests/fm-on.test.sh with a behavioral regression asserting a bounded, positive ServerAliveInterval/ServerAliveCountMax on the real ssh argv captured through the FM_SSH_BIN process seam, plus coverage that both are env-overridable. * no-mistakes(document): Document SSH dead-peer detection ownership * fix: report stale AXI tools during bootstrap (kunchenguid#1701) * feat(bootstrap): gate stale axi CLIs at the floors firstmate actually uses Add gh-axi 0.1.29 floor so bare --squash PR merges stop failing quietly on older builds. Raise tasks-axi to FM_TASKS_AXI_MIN=0.2.2 (multi-id mv) while keeping feature probes. Keep quota-axi at 0.1.16 after verifying schema 3 and per-model availability already ship there; runway remains optional. * no-mistakes(document): Clarify AXI compatibility documentation ownership * fix: prevent false watcher-down alarms in Claude sessions (kunchenguid#1661) * fix(guard): stop false watcher-down alarm mid-turn under Claude auto-arm bin/fm-guard.sh derived its watcher-health verdict from fm_watcher_healthy, which requires a live watcher process holding the home lock. Under the Claude Stop-hook auto-arm supervision model the watcher is armed at each turn end and exits on its wake, so it runs only between turns. Every guarded command run mid-turn therefore found no live watcher and printed the "WATCHER DOWN - SUPERVISION IS OFF" banner even though supervision was healthy. Because the episode key was derived from the beacon mtime (which the between-turns watcher advances every poll), the full banner re-printed on essentially every command, and the message always blamed a "fresh beacon" that was in fact fresh. Make the pull guard's health check model-aware via a new fm_watcher_supervision_verdict in bin/fm-wake-lib.sh: - Under the auto-arm model a beacon fresh within FM_GUARD_GRACE is healthy even with no live watcher process; only a beacon stale beyond grace (or absent) is a genuine lapse and alarms. - Under every persistent-watcher harness (codex foreground checkpoint, opencode/pi/grok background arm, tmux, unknown) a live identity-matched watcher with a fresh beacon is still required, unchanged. The banner now names the true failing condition, a missing live watcher process versus a genuinely stale beacon, instead of always blaming the beacon, and the once-per-episode dedup keys on that condition rather than the beacon mtime so a genuine lapse announces once and does not re-print each turn. The turn-end guard keeps the strict fm_watcher_healthy check because it fires at the turn boundary, where the auto-arm brings a fresh watcher up and it cooperates with that arm. fm_watcher_healthy itself is unchanged, so the arm layer's start/attach/replace decisions are unaffected. Tests in tests/fm-guard-stale-banner.test.sh cover the auto-arm healthy fresh-beacon-without-a-watcher case, the auto-arm stale-beacon alarm and its stable episode, the true-reason banner wording, and the reason-keyed episode surviving a beacon mtime change; existing persistent-model cases are pinned to that model. * no-mistakes(review): Pin secondmate supervision model to launched harness * no-mistakes(document): Align watcher documentation with model-aware supervision health * test: prevent fixture temporary directory leaks (kunchenguid#1704) * fix(tests): stop fixture-tempdir helper from self-deleting under command substitution fm_test_tmproot is almost always called as `TMP_ROOT=$(fm_test_tmproot prefix)`, which forks a subshell to capture its stdout. The old implementation set its EXIT cleanup trap inside that call, so the trap fired - and deleted the fixture root - the instant the subshell exited, before the real caller's own EXIT trap was ever installed. Every test using the documented call pattern leaked its fixture root on every run; two suites had already independently discovered and worked around this with ad-hoc mktemp calls. Registration now goes through a $$-keyed registry file instead of in-process state, since $$ resolves to the invoking shell's PID even inside the subshell. The real cleanup trap is armed once at source time (always the real caller, never a subshell) for EXIT, INT, and TERM. A best-effort orphan sweep on next source reaps marked fixture roots old enough to be from a killed prior run. Simplifies the two existing ad-hoc workarounds (fm-procevent.test.sh, wake-helpers.sh) back onto the shared helper now that it works correctly. * no-mistakes(review): Preserve live fixtures during orphan reaping * no-mistakes(review): Harden fixture ownership against PID reuse * no-mistakes(review): Secure cleanup registry against path precreation * no-mistakes(review): Make fixture registration transactional * no-mistakes(document): Documentation already matches fixture cleanup behavior * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * feat(herdr): enable presentation spaces by default (kunchenguid#1708) * feat(herdr): default presentation spaces on with an explicit opt-out Herdr's disposable one-task presentation workspace was opt-in through the presence of local config/herdr-presentation-spaces. It is now on by default, and a home opts out by writing "off" into that same file. Values are read with the whole-file whitespace-stripped convention the other scalar config items already use, plus case folding. An absent file, an empty file, and "on" all resolve on; only "off" opts out; an unrecognized value warns and keeps the default rather than failing a spawn over a purely visual setting. The empty file is exactly the historical opt-in form, so every home that had already enabled the projection stays enabled with no migration step, and no previously enabled home can be turned off by the flip. Because absence now means on at both ends, secondmate inheritance needs no item-specific convergence: mirroring an absent primary file converges a secondmate to the same default-on rather than turning its projection off, and only an explicit primary opt-out propagates the opt-out. The gate itself moves into fm_backend_herdr_presentation_enabled in the Herdr adapter so the semantics have one owner that regressions can exercise directly. * no-mistakes(document): Document Herdr default-on presentation safety --------- Co-authored-by: kunchenguid <kun-1@kunchenguid.com> * fix(bin): surface fleet-wide open decisions on every wake drain (kunchenguid#1711) * fix: surface consolidated open decisions on every wake-drain A needs-decision or blocked event buried under later, unrelated status appends was only ever shown via the last-line wake annotation, so a still-open captain decision could go silently missed even though status_open_decisions (fm-classify-lib.sh) already folds the whole status stream correctly and fleet-snapshot/bearings already reuse it. Wire that same fold into bin/fm-wake-drain.sh: a new fleet-wide scan_open_decisions wrapper scans every state/<id>.status, and fm-wake-drain.sh prints a separate, bounded OPEN DECISIONS section on every drain (including the empty-queue fast path), so session-start and every wake-handling turn surface it for free without duplicating the open/resolved fold itself. Heartbeat wakes drain through the same script, so this covers that surface too. Also tighten status_open_decisions' file guard to skip an unreadable status file instead of leaking a bash redirection error, now that a fleet-wide directory scan can reach files a single targeted read would not. * no-mistakes(review): Prevent status symlinks leaking open decisions * fix: drop unbounded perl subprocess from status symlink guard The review step's own symlink-safety auto-fix (O_NOFOLLOW read via a perl subprocess) forked one perl process per status file scanned by the new fleet-wide open-decisions scan, with no cap - inflating fm-wake-drain.sh's total external-read cost from 8 (the existing annotation read_cap) to 18 in the enrichment-caps regression test. The plain [ -L "$f" ] check already rejects any status file that is itself a symlink before any read happens, which is exactly what the new regression test exercises and is the same defense level the sibling scan_captain_relevant_statuses/last_status_line already rely on elsewhere in this file (no O_NOFOLLOW). Drop the subprocess-based nofollow read and keep the cheap builtin guard. * no-mistakes(document): Document actionable fleet-wide open decision drains * fix(bin): abort parked runs and reap leaked processes before teardown (kunchenguid#1710) * fix(bin): abort orphaned no-mistakes runs and reap leaked processes at teardown Teardown could remove a task's worker while its no-mistakes pipeline run was still parked at a gate, leaving an orphaned run holding a fleet slot indefinitely (observed 2026-08-03: runs parked 7h39m and parked at a post-CI approval gate). It could also leave backgrounded/disowned descendant processes rooted under the worktree or tasktmp surviving reparented to init (observed: two `go test` binaries pinning CPU for hours with no live task meta to attribute them to). Add two coupled pre-teardown steps, both scoped to this task's exact branch/head or worktree/tasktmp so they can never touch another task's run or processes: - conclude_task_no_mistakes_run aborts a run parked at a gate via `no-mistakes axi abort`, cd'd into the exact worktree so the daemon resolves the run itself rather than teardown naming a --run id. - reap_task_worktree_processes sweeps for processes whose cwd is under the worktree or tasktmp (via `lsof -a -d cwd`) and TERM/KILLs them. Both run before any worktree return, branch delete, or backend kill, and are idempotent on a retried teardown. The branch+head attribution logic is factored out of bin/fm-crew-state.sh into the new shared bin/fm-nm-run-lib.sh so both scripts use the same ownership contract. * no-mistakes(review): Fail closed on incomplete teardown cleanup * no-mistakes(review): Bind teardown cleanup to verified run and process identities * no-mistakes(review): Require confirmed aborts and convergent identity-safe process reaping * no-mistakes(review): Handle process exits during teardown identity checks * no-mistakes(review): Restore teardown library in hermetic gotmp fixtures * no-mistakes(document): Document teardown run attribution and timeout * no-mistakes(lint): Rename shell variable conflicting with done keyword * no-mistakes: apply CI fixes * test: give fork scaffolding tests the delivery contract upstream now requires Upstream kunchenguid#1563 made --mode mandatory for ship briefs and --mode plus --yolo mandatory for ship spawns, and updated the tests it shipped with. Three fork tests were written after the fork diverged, against the pre-kunchenguid#1563 interfaces, so they called the scaffolds with no delivery contract. Merging the two trunks put those callers in front of the new requirement and they refused before reaching the behavior each test exists to pin. Git could not see this: the requirement and the callers live in different files, so both sides merged clean and the breakage only showed up when the suite ran. Upstream's contract is kept exactly as it is - it is a deliberate safety gate, and AGENTS.md section 7 depends on the brief and the spawn refusing to guess. Each fork test instead states its mode explicitly, so every assertion it was written to make still runs. The standing-worker-rules variants each state their own mode, because those cases assert mode-specific prose and fm-brief no longer reads the registry. Verified: all three fail on the merge commit and pass here, with fm-brief, fm-launch-lib and fm-model-zero-budget green. --------- Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com> Co-authored-by: Gavin <51008413+allstargg@users.noreply.github.com> Co-authored-by: Christopher McKay <101884182+karotkriss@users.noreply.github.com> Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
sbracewell64
added a commit
to sbracewell64/firstmate
that referenced
this pull request
Aug 9, 2026
Squash-reconcile of the fork trunk (48 commits through ed376cf, fork PRs #8-#48 plus CI mirrors) rebased onto upstream main at 74230fc, resolving 72 conflicted files. The fork trunk carries: the fm-launch-lib.sh one-owner launch refactor, the slot-base/contribution-target task base contract (fm-task-base-lib.sh), the zero-budget model registry, fleet admission control stages 0/1, the wake-outcome ledger, the LoopSpec canonical representation, the research-approved-work corpus scanner, the 70% compaction doctrine, from-firstmate steer markers, the fleet launcher menu and Windows-to-WSL bridge, forge-verified merge gates, and the fleet-view per-argument cap fix. Conflict-resolution decisions a reviewer cannot see from the diff alone: - Where the fork carried an older in-flight import of upstream work (delivery contracts kunchenguid#1563, remote secondmate homes kunchenguid#1576, trace context kunchenguid#995, and every add/add remote-secondmate file), upstream's landed and further-evolved version wins outright; nothing fork-specific lived in those copies (verified blob-by-blob against upstream history). - Upstream's --relaunch lifecycle control plane and the fork's task base contract both survive: base derivation and the brief base-contract guard are gated to fresh spawns (RELAUNCH=0) because a relaunch reuses the recorded worktree and bases, and the trunk may have moved since. - Launch commands stay one-owner in bin/fm-launch-lib.sh: upstream's Muse Code adapter is ported into the library (template arm, model flag, effort mapping with max->ultra) instead of resurrecting the inline fm-spawn template, and fm-spawn's post-template FM_PI_HARNESS prefix is dropped because the library's pi templates carry the marker themselves. - Herdr presentation spaces keep upstream's 0.8.0-floor semantics (config off/on/empty, two-argument fm_backend_herdr_presentation_enabled) across spawn, config inheritance, docs, and tests. - fm-pr-check derives its per-task lock from state/<id>.meta explicitly so upstream's kunchenguid#1568 meta locking coexists with the fork's #34 landing records (fm_meta_lock_path rejects a .landing path). - tests/lib.sh keeps both the fork's identity-verified background-process reaper and upstream's fm_fake_version_tool fixture. - Merged prose follows upstream's X-mode -> Relay rename. Pre-existing on both parents, not introduced here: tests/fm-calm-pi-extension.test.sh fails under system Node 22 (ERR_UNKNOWN_FILE_EXTENSION), verified identical on the base commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sbracewell64
added a commit
to sbracewell64/firstmate
that referenced
this pull request
Aug 9, 2026
…ks (#39) * feat(bin): require an explicit per-task delivery contract (kunchenguid#1563) * feat(bin): require an explicit ship delivery mode in fm-brief A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions. * feat(bin): require an explicit ship delivery contract at spawn and promotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode. * docs: record the explicit per-task delivery contract AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly. * test: pass ship delivery flags per call site in the Herdr launcher e2e The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper. * test: pass the ship delivery contract in the secondmate suites Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires. * feat(bin): support remote secondmate homes (kunchenguid#1576) * Add generic remote secondmate transport * Add routed remote secondmate replies * Add remote outbox backlog handoff * Integrate remote secondmate lifecycle * no-mistakes(review): Fix remote snapshot and handoff races * no-mistakes(review): Serialize remote home provisioning transactions * no-mistakes(review): Harden remote lifecycle transaction boundaries * no-mistakes(review): Serialize remote lifecycle mutations and fail closed * no-mistakes(review): Close remote lifecycle and file race windows * no-mistakes(review): Serialize remote reply retirement and inheritance * no-mistakes(review): Harden remote transfer integrity and recovery * no-mistakes(review): Serialize remote respawn with registry retirement * no-mistakes(document): Document remote bootstrap convergence accurately * no-mistakes(document): Clarify skipped remote secondmate mutations * no-mistakes(lint): Resolve remote script ShellCheck warnings * no-mistakes: apply CI fixes * feat: add per-task trace context propagation (kunchenguid#995) * feat(spawn): propagate a native W3C traceparent to spawned agents Add a default-off capability that resolves one W3C traceparent for a task, injects it into the agent's pane shell as the TRACEPARENT environment variable immediately before launch, and records the identical value as traceparent= in state/<id>.meta, so an external observer that explicitly reads that env value or meta field can correlate a worker, a Secondmate, and their nested children into one trace with no collector, storage, UI, or vendor coupling. TRACEPARENT as an environment variable is a firstmate convention carrying a W3C-formatted value: W3C Trace Context standardizes the header, not an env var, and OpenTelemetry SDKs do not read it automatically, so a downstream must consume it deliberately; this feature parents no SDK span by itself. Identity is per task, not per spawn: the carrier is minted with random ids on the first spawn, adopted as a child (fresh span, same trace) for a nested spawn whose parent already holds one, and reused verbatim from the meta on relaunch, so a task keeps one stable logical identity across restarts. A malformed or all-zero inherited value is treated as absent and roots a fresh trace. A new root is sampled (01) - a sampling decision a downstream parent-based sampler honors, not a guarantee that any collector stores a span, and firstmate emits no spans; a child preserves the inherited flag. Trust boundary: a firstmate-minted root is random and reads no prompt, path, task prose, credential, or arbitrary environment key. An inherited TRACEPARENT is opaque caller-controlled data - up to 24 bytes of id passed through after syntax validation - so whoever set it controls those bytes, a bounded fixed-width channel rather than a general content or secret channel. The feature adds no OTEL_* variable, no tracestate, and no arbitrary environment injection; it runs no configurable or arbitrary command, only the fixed local od and tr (resolved from PATH) to read a few bytes of entropy - a small local pipeline with no network or watchdog and no hard latency guarantee. Any entropy or validation failure that returns omits the carrier without aborting the spawn. A default-off spawn leaves the generated meta and launch environment unchanged. Enablement is default-off (config/trace-context, or FM_TRACE_CONTEXT where a non-empty value overrides and unset or empty defers to the file) and is propagated into secondmate homes, taking effect at each agent's next launch: a Secondmate launched or relaunched after enablement carries the primary trace into its nested workers, while an already-running Secondmate roots new traces for its own workers until relaunched. Injection reuses the existing GOTMPDIR channel, so all spawn backends and harnesses and the ship, scout, and secondmate paths are covered. Covered by a pure-library suite and a spawn-path integration test (fake tmux plus a real worktree, hermetic against ambient FM_TRACE_CONTEXT) proving the recorded and injected carriers are identical and sent before launch, that default-off writes and injects neither, that a relaunch reuses the recorded carrier, and that an explicit FM_TRACE_CONTEXT overrides the file both ways; plus a source-owner inheritance test proving trace-context propagates and absence-mirrors through propagate_inheritable_config. Documentation follows the repository documentation-audiences contract: docs/trace-context.md is maintainer-architecture rationale, the configuration schema lives in docs/configuration.md, and the repeatable test evidence is separated into docs/verification/trace-context.md (maintainer-verification), registered in docs/documentation-audiences.json. * fix(spawn): propagate the effective trace-context decision to secondmates FM_TRACE_CONTEXT overrode trace context only in the process that read it. A newly launched secondmate decided enablement from the inherited config/trace-context file alone, so the override did not cross the primary-to-secondmate boundary: FM_TRACE_CONTEXT=off with the file present left the secondmate's nested workers traced (a broken kill switch), and FM_TRACE_CONTEXT=on with the file absent left them untraced despite the inherited carrier. Deliver the primary's effective decision to a newly launched secondmate as a normalized on/off FM_TRACE_CONTEXT in the launch prefix, so a FM_TRACE_CONTEXT override governs the nested primary -> secondmate -> worker chain both ways, not just the copied file. The value is bounded to the literal on/off and does not broaden environment injection; the already-running secondmate boundary is unchanged. Add a genuine two-level spawn regression that drives fm-spawn twice with the exact environment the primary injects into the secondmate and proves both divergent directions end to end. Correct the documentation that implied secondmate coverage on every backend, since orca and cmux reject secondmate spawns, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Clarify Secondmate trace-context launch snapshots * no-mistakes(document): Correct trace-context documentation ownership and relaunch semantics * fix(spawn): resolve the trace-context decision once for carrier and snapshot The effective trace-context decision was read twice per spawn: once inside fm_trace_context_resolve for the recorded carrier, and again for the secondmate FM_TRACE_CONTEXT launch snapshot. A config-file change between the two reads could pair a carrier with the opposite enable state - an injected carrier with an off snapshot, or no carrier with an on snapshot. Freeze the effective on/off decision once, drive the carrier resolution under that frozen FM_TRACE_CONTEXT so it cannot independently re-read the file, and reuse the same frozen decision for the secondmate launch snapshot. Add a spawn-path regression that drives the file-decided path and proves the recorded carrier and the delivered snapshot always agree, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Preserve legacy Secondmate trace boundary * no-mistakes(document): Correct trace-context verification comparison base * no-mistakes(review): Captain, prevent failed trace delivery metadata claims * no-mistakes(review): Captain, align trace-context tests and verification evidence * no-mistakes(document): Correct trace-context verification evidence * no-mistakes(lint): Suppress intentional ShellCheck literal-dollar warnings * no-mistakes(review): Captain: freeze trace context at session start * no-mistakes(test): Captain: stabilize scheduler test and document Kimi trace coverage * no-mistakes(document): Document trace-context safety boundaries * fix(trace): fail off on stale session snapshots Publish each home session decision atomically through a same-directory temporary file and bind it to the current session lock. A replacement failure can no longer leave an earlier on decision active in a later session; missing, stale, malformed, or unpublishable state defaults safely to off. Add regressions for read-only replacement and failed publication, update spawn and session-start fixtures for the lock-bound format, and refresh the architecture and verification records. * no-mistakes(review): Fix trace spawn failure independence and duplicate safety * no-mistakes(document): Refresh trace-context documentation and verification * no-mistakes(review): Clear partial backend input after failed trace submission * no-mistakes(review): Stop unsafe trace delivery before launch append * no-mistakes(document): Document unsafe trace delivery handling * fix(trace): bound each trace to one routed task, never the routing agent A persistent Secondmate holds its launch-time TRACEPARENT in the process environment for its whole life, and routed requests never replace it, so resolving new-task carriers from the ambient environment chained every routed task into one ever-growing trace per Secondmate with distinct parent ids. Resolve now reuses the task's recorded carrier or mints a fresh sampled root, never reading ambient TRACEPARENT, so each routed task is its own trace boundary while relaunch, recovery, and scout-to-ship promotion keep one stable per-task identity. The spawn regression models the reviewed scenario exactly: two unrelated tasks spawned sequentially through one persistent Secondmate environment record and inject distinct trace ids, adopt nothing from the Secondmate's carrier, and a relaunch of the first task reuses its original carrier verbatim. * docs(trace): define the per-task trace boundary The design contract is one task per trace: a persistent Secondmate is routing infrastructure with its own agent identity, never a shared trace root for the unrelated tasks routed through it. Root/recovery semantics replace the removed child-inheritance path, the sampling and safety sections drop inherited-carrier language because ambient TRACEPARENT is never read, and the verification page records the refreshed suite inventories including the two-task Secondmate boundary regression. * test(trace): adopt the explicit per-task delivery contract in spawn fixtures Rebasing onto current main brings the explicit per-task delivery contract: ship spawns now require --mode and --yolo instead of resolving them from the project registry. The trace spawn fixtures pass the same explicit contract canonical spawn tests use, preserving the per-task trace boundary coverage unchanged, and the verification page records the refreshed comparison base. * fix(bin): harden tmux agent liveness across harnesses (kunchenguid#1577) * fix(bin): classify tmux agent liveness independent of process titles `fm_backend_tmux_agent_state` attributed a pane solely from `#{pane_current_command}`, which is a process TITLE a harness can rewrite, not a structural fact. Claude Code 2.1.220 reports its version string there, so a live Claude endpoint classified `ambiguous`: the session-start secondmate liveness sweep could no longer see it, and any consumer that gates on a positive classification refuses outright. Read a second, independent name source: the kernel `comm` of every process in the pane tty's foreground process group. Either source naming a verified harness yields `alive`, because a false `dead` is the one verdict that can start a duplicate agent on a live worktree. Scoping to the foreground process group rather than the pane's descendants keeps a harness-named background process from faking an agent, and covers multi-process launchers (the Pi Launcher path) without a special case. Verified on 2026-08-03 against all seven adapters running for real on tmux 3.6a / macOS 26.5.2 arm64: claude 2.1.220, codex-cli 0.146.0, opencode 1.18.11, pi 0.82.0, pi-signed 0.82.0, grok 0.2.118, kimi 0.31.1 all classify `alive`, each attributed by a source independent of its title. Two tests, because they fail for different reasons: - tests/fm-tmux-agent-liveness.test.sh pins the logic with real processes and no harness, so it runs everywhere CI runs tmux. It drives the two name sources apart on purpose and asserts the divergence, so no case can go quietly vacuous. - tests/fm-harness-liveness-drift-live-e2e.test.sh relaunches every installed harness and fails naming the harness and version when one stops being attributed by a title-independent source. AGENTS.md section 4 carries the resulting standing rule, and firstmate-coding-guidelines owns how to satisfy it. * no-mistakes: apply CI fixes * docs: move the harness-dependent-check policy out of AGENTS.md The standing rule was stated in AGENTS.md section 4 with the mechanics in firstmate-coding-guidelines, which split one contract across two owners and charged every session for a rule that only fires when firstmate's own harness-dependent code is being changed. firstmate-coding-guidelines is now the single owner of both the rule and how to satisfy it: real-harness proof required, that proof authorized to spend tokens, structural signals preferred over vendor-rendered surfaces, and a guard that fails loudly naming the harness and version where a surface signal is unavoidable. No inline stub is left behind, because AGENTS.md already carries the load trigger for that skill in sections 7 and 13, so it is read before any change to firstmate's shared tracked material. Also records the cross-platform lesson the pipeline caught in the portable regression, and corrects that file's header: the divergence assertion lives on the version-string case, which diverges on both supported platforms, rather than on every case. * no-mistakes(review): Harden tmux liveness identity and drift validation * no-mistakes(document): Clarify cross-platform tmux liveness documentation * feat(bin): propagate trace context to remote secondmates (kunchenguid#1609) * feat(bin): trace remote secondmate routes and unify the inherit allowlist Per-task W3C trace context (kunchenguid#995) resolved and injected its carrier only at the local spawn path. A remote secondmate is routed through spawn_remote_secondmate, which returns long before that site and wrote its own metadata block, so a remote secondmate stayed silently untraced even with the capability enabled. The parent home still owns that task's identity, because it holds the metadata an observer reads. It now resolves the carrier against the task's own meta under its own frozen decision - reused verbatim on relaunch, freshly rooted otherwise, never adopting the parent process's ambient TRACEPARENT - and hands it to the configured host through a new fm-spawn --traceparent argument, accepted only for a secondmate launch and only as a strict W3C value. The remote host exports it at the same unconditional pre-launch site and reports back the carrier its endpoint actually holds, which the parent records, so an already-alive endpoint reports the identity its agent really received rather than one the parent merely intended. Disabled remains byte-identical and off. The remote inherit path also carried its own hardcoded copy of the inheritable config set, already drifted from FM_INHERITABLE_CONFIG by trace-context. Both remote ends now derive from that one declaration, so a future item cannot be sent by one side and refused by the other, and session-scoped enablement items are skipped on live convergence exactly as the local path skips them. Also fixes a latent stderr leak: an absent session lock printed a raw redirect failure, which the new remote resolve site made visible. Adds tests/fm-remote-secondmate-trace-context.test.sh, driving the real parent -> fm-on -> remote entrypoint -> control -> remote fm-spawn chain over the deterministic SSH boundary and reading the carrier back from the remote pane's own log. * no-mistakes(document): Clarify remote trace and allowlist contracts * feat(bin): preflight remote runtime tool paths (kunchenguid#1623) * feat(bin): widen the remote runtime PATH and add a remote doctor preflight The fixed remote entrypoint hard-coded a four-directory PATH, so a remote account whose tools live under nix or a per-user profile could not run basic Firstmate work without a login shell. The entrypoint now composes its child PATH from the code root's bin, the account's ~/.local/bin, the common package-manager directories that actually exist on the host, and the portable system tail, deduplicated and in a fixed order, still under env -i with the same variable allowlist and no shell command string. fm-remote-doctor.sh reports that exact PATH by inheriting it from its own entrypoint launch rather than recomposing it, so the ordering keeps one owner. It is read-only, reports where each required and optional tool resolved, and exits non-zero naming every required tool that did not. Remote seeding runs it as a preflight before anything is created on the host and restores the registry when it fails. * no-mistakes(review): Harden remote git authorization and missing-tool diagnostics * no-mistakes(document): Document remote PATH doctor and safe shims * no-mistakes(lint): Fix ShellCheck findings in remote path tests * no-mistakes(lint): Suppress exported fixture's false-positive ShellCheck warning * feat: gate remote second mates on Herdr readiness (kunchenguid#1639) * feat(bin): gate remote second mates on herdr readiness A remote second mate now always runs on the Herdr backend, whose server belongs to the host's GUI login session and therefore outlives the SSH connections that supervise it. fm-spawn's remote route forces that backend and the host-local control script refuses any other, so the requirement cannot be dropped from either side. fm-remote-doctor.sh becomes the single owner of what "ready" means. It keeps its PATH and tool reporting from kunchenguid#1623 and adds the Herdr, Aqua LaunchAgent, GUI-session, server-reachability, and entrypoint-symlink checks, tagging each gap fixable: or human: with the exact operator step. --fix closes only the automatable gaps - writing and loading the Aqua-scoped dev.firstmate.herdr launch agent, starting the server where no launch agent applies, and recreating the entrypoint symlink - then re-derives every check from the host, so a human gap is never presented as fixed. It never creates a login session, writes an auto-login password, or touches FileVault. Remote seed, remote spawn, and the startup liveness relaunch all run the same check, repair, re-check sequence through one shared library and fail closed with the doctor's own gap text. Recovery inherits the gate because it respawns through the same route. Tests drive the real doctor against a controlled account fixture with a private HOME, a state-backed launchctl, and a fake herdr, and prove the dangerous actions are never attempted. The remote lifecycle suites gain a stateful Herdr CLI fixture and answer the readiness gate at the SSH boundary, so they never inspect or repair the runner's own account. * no-mistakes(review): Validate launch-agent contract and confirm Herdr startup * no-mistakes(review): Validate loaded launch-agent contract before readiness * no-mistakes(review): Refuse legacy remote backends without altering routes * no-mistakes(review): Clarify conditional remote readiness repair sequence * no-mistakes(review): Repair remote readiness before liveness probing * no-mistakes(review): Preserve unknown seeds and reject legacy liveness * no-mistakes(document): docs: clarify remote Herdr backend ownership * fix: isolate remote secondmates in shared Herdr session (kunchenguid#1659) * Pin remote secondmates to fm-remote * no-mistakes(review): Fail closed on legacy remote Herdr endpoints * no-mistakes(review): Isolate fm-remote launch agent from interactive default * no-mistakes(document): Document shared remote Herdr retirement safety * feat: route remote commands through an Aqua job worker (kunchenguid#1660) * feat: run remote commands through Aqua job worker * no-mistakes(review): Enforce remote job deadlines and safe worker shutdown * no-mistakes(review): Refresh stale workers and harden dependency-free supervision * no-mistakes(review): Harden worker ownership recovery and shutdown quarantine * no-mistakes(review): Fix doctor bootstrap, harness repair, and output draining * no-mistakes(review): Probe doctor tools through authenticated worker bootstrap * no-mistakes(review): Refresh stale workers before doctor tool probes * no-mistakes(review): Recover stopped quarantines and extend job deadlines * no-mistakes(review): Separate queue and execution timeout windows * no-mistakes(review): Supervise Linux worker crashes and bind root identity * no-mistakes(review): Resolve authorized Nix profile bin links * no-mistakes(review): Clarify Nix path resolution documentation * no-mistakes(review): Harden PATH safety and nvm selection * no-mistakes(review): Honor nvm system defaults and refresh doctor digest * no-mistakes(review): Keep workers ready during active jobs * no-mistakes(review): Bound pre-execution validation by job timeout * no-mistakes(document): Clarify remote worker documentation * no-mistakes(lint): Fix remote worker ShellCheck diagnostics * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * fix: clarify remote doctor bootstrap path (kunchenguid#1691) * fix(bin): bound remote SSH dead-peer detection (kunchenguid#1699) * fix(remote): arm SSH dead-peer detection in fm-on.sh A vanished remote host mid-poll (a reboot, a dropped link) left ssh blocked indefinitely on a half-open TCP connection, because fm-on.sh's ssh invocation had no ServerAliveInterval/ServerAliveCountMax. This wedged the remote-reply ferry: fm-procevent.sh's runner blocked inside the ssh child and never reached its own no-result -> claim-release -> reconcile re-arm self-healing path, which otherwise already handles a nonzero exit with empty output correctly. Recovery required a manual retire and re-arm. Arm ServerAliveInterval=15 and ServerAliveCountMax=3 by default (bounded ~45s detection window), both overridable via FM_SSH_ALIVE_INTERVAL and FM_SSH_ALIVE_COUNT_MAX. This is a transport- level fix in fm-on.sh, so it covers every remote command routed through it, not just the reply ferry. The remote sshd answers keepalive probes independently of whatever the remote command is doing, so a legitimately long-but-alive command (a 55s poll, a clone, the doctor) is never falsely killed - only a truly vanished peer trips it, turning that case into a bounded, detectable ssh failure (exit 255) instead of an indefinite hang. Extends tests/fm-on.test.sh with a behavioral regression asserting a bounded, positive ServerAliveInterval/ServerAliveCountMax on the real ssh argv captured through the FM_SSH_BIN process seam, plus coverage that both are env-overridable. * no-mistakes(document): Document SSH dead-peer detection ownership * fix: report stale AXI tools during bootstrap (kunchenguid#1701) * feat(bootstrap): gate stale axi CLIs at the floors firstmate actually uses Add gh-axi 0.1.29 floor so bare --squash PR merges stop failing quietly on older builds. Raise tasks-axi to FM_TASKS_AXI_MIN=0.2.2 (multi-id mv) while keeping feature probes. Keep quota-axi at 0.1.16 after verifying schema 3 and per-model availability already ship there; runway remains optional. * no-mistakes(document): Clarify AXI compatibility documentation ownership * fix: prevent false watcher-down alarms in Claude sessions (kunchenguid#1661) * fix(guard): stop false watcher-down alarm mid-turn under Claude auto-arm bin/fm-guard.sh derived its watcher-health verdict from fm_watcher_healthy, which requires a live watcher process holding the home lock. Under the Claude Stop-hook auto-arm supervision model the watcher is armed at each turn end and exits on its wake, so it runs only between turns. Every guarded command run mid-turn therefore found no live watcher and printed the "WATCHER DOWN - SUPERVISION IS OFF" banner even though supervision was healthy. Because the episode key was derived from the beacon mtime (which the between-turns watcher advances every poll), the full banner re-printed on essentially every command, and the message always blamed a "fresh beacon" that was in fact fresh. Make the pull guard's health check model-aware via a new fm_watcher_supervision_verdict in bin/fm-wake-lib.sh: - Under the auto-arm model a beacon fresh within FM_GUARD_GRACE is healthy even with no live watcher process; only a beacon stale beyond grace (or absent) is a genuine lapse and alarms. - Under every persistent-watcher harness (codex foreground checkpoint, opencode/pi/grok background arm, tmux, unknown) a live identity-matched watcher with a fresh beacon is still required, unchanged. The banner now names the true failing condition, a missing live watcher process versus a genuinely stale beacon, instead of always blaming the beacon, and the once-per-episode dedup keys on that condition rather than the beacon mtime so a genuine lapse announces once and does not re-print each turn. The turn-end guard keeps the strict fm_watcher_healthy check because it fires at the turn boundary, where the auto-arm brings a fresh watcher up and it cooperates with that arm. fm_watcher_healthy itself is unchanged, so the arm layer's start/attach/replace decisions are unaffected. Tests in tests/fm-guard-stale-banner.test.sh cover the auto-arm healthy fresh-beacon-without-a-watcher case, the auto-arm stale-beacon alarm and its stable episode, the true-reason banner wording, and the reason-keyed episode surviving a beacon mtime change; existing persistent-model cases are pinned to that model. * no-mistakes(review): Pin secondmate supervision model to launched harness * no-mistakes(document): Align watcher documentation with model-aware supervision health * test: prevent fixture temporary directory leaks (kunchenguid#1704) * fix(tests): stop fixture-tempdir helper from self-deleting under command substitution fm_test_tmproot is almost always called as `TMP_ROOT=$(fm_test_tmproot prefix)`, which forks a subshell to capture its stdout. The old implementation set its EXIT cleanup trap inside that call, so the trap fired - and deleted the fixture root - the instant the subshell exited, before the real caller's own EXIT trap was ever installed. Every test using the documented call pattern leaked its fixture root on every run; two suites had already independently discovered and worked around this with ad-hoc mktemp calls. Registration now goes through a $$-keyed registry file instead of in-process state, since $$ resolves to the invoking shell's PID even inside the subshell. The real cleanup trap is armed once at source time (always the real caller, never a subshell) for EXIT, INT, and TERM. A best-effort orphan sweep on next source reaps marked fixture roots old enough to be from a killed prior run. Simplifies the two existing ad-hoc workarounds (fm-procevent.test.sh, wake-helpers.sh) back onto the shared helper now that it works correctly. * no-mistakes(review): Preserve live fixtures during orphan reaping * no-mistakes(review): Harden fixture ownership against PID reuse * no-mistakes(review): Secure cleanup registry against path precreation * no-mistakes(review): Make fixture registration transactional * no-mistakes(document): Documentation already matches fixture cleanup behavior * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * feat(herdr): enable presentation spaces by default (kunchenguid#1708) * feat(herdr): default presentation spaces on with an explicit opt-out Herdr's disposable one-task presentation workspace was opt-in through the presence of local config/herdr-presentation-spaces. It is now on by default, and a home opts out by writing "off" into that same file. Values are read with the whole-file whitespace-stripped convention the other scalar config items already use, plus case folding. An absent file, an empty file, and "on" all resolve on; only "off" opts out; an unrecognized value warns and keeps the default rather than failing a spawn over a purely visual setting. The empty file is exactly the historical opt-in form, so every home that had already enabled the projection stays enabled with no migration step, and no previously enabled home can be turned off by the flip. Because absence now means on at both ends, secondmate inheritance needs no item-specific convergence: mirroring an absent primary file converges a secondmate to the same default-on rather than turning its projection off, and only an explicit primary opt-out propagates the opt-out. The gate itself moves into fm_backend_herdr_presentation_enabled in the Herdr adapter so the semantics have one owner that regressions can exercise directly. * no-mistakes(document): Document Herdr default-on presentation safety --------- Co-authored-by: kunchenguid <kun-1@kunchenguid.com> * fix(bin): surface fleet-wide open decisions on every wake drain (kunchenguid#1711) * fix: surface consolidated open decisions on every wake-drain A needs-decision or blocked event buried under later, unrelated status appends was only ever shown via the last-line wake annotation, so a still-open captain decision could go silently missed even though status_open_decisions (fm-classify-lib.sh) already folds the whole status stream correctly and fleet-snapshot/bearings already reuse it. Wire that same fold into bin/fm-wake-drain.sh: a new fleet-wide scan_open_decisions wrapper scans every state/<id>.status, and fm-wake-drain.sh prints a separate, bounded OPEN DECISIONS section on every drain (including the empty-queue fast path), so session-start and every wake-handling turn surface it for free without duplicating the open/resolved fold itself. Heartbeat wakes drain through the same script, so this covers that surface too. Also tighten status_open_decisions' file guard to skip an unreadable status file instead of leaking a bash redirection error, now that a fleet-wide directory scan can reach files a single targeted read would not. * no-mistakes(review): Prevent status symlinks leaking open decisions * fix: drop unbounded perl subprocess from status symlink guard The review step's own symlink-safety auto-fix (O_NOFOLLOW read via a perl subprocess) forked one perl process per status file scanned by the new fleet-wide open-decisions scan, with no cap - inflating fm-wake-drain.sh's total external-read cost from 8 (the existing annotation read_cap) to 18 in the enrichment-caps regression test. The plain [ -L "$f" ] check already rejects any status file that is itself a symlink before any read happens, which is exactly what the new regression test exercises and is the same defense level the sibling scan_captain_relevant_statuses/last_status_line already rely on elsewhere in this file (no O_NOFOLLOW). Drop the subprocess-based nofollow read and keep the cheap builtin guard. * no-mistakes(document): Document actionable fleet-wide open decision drains * fix(bin): abort parked runs and reap leaked processes before teardown (kunchenguid#1710) * fix(bin): abort orphaned no-mistakes runs and reap leaked processes at teardown Teardown could remove a task's worker while its no-mistakes pipeline run was still parked at a gate, leaving an orphaned run holding a fleet slot indefinitely (observed 2026-08-03: runs parked 7h39m and parked at a post-CI approval gate). It could also leave backgrounded/disowned descendant processes rooted under the worktree or tasktmp surviving reparented to init (observed: two `go test` binaries pinning CPU for hours with no live task meta to attribute them to). Add two coupled pre-teardown steps, both scoped to this task's exact branch/head or worktree/tasktmp so they can never touch another task's run or processes: - conclude_task_no_mistakes_run aborts a run parked at a gate via `no-mistakes axi abort`, cd'd into the exact worktree so the daemon resolves the run itself rather than teardown naming a --run id. - reap_task_worktree_processes sweeps for processes whose cwd is under the worktree or tasktmp (via `lsof -a -d cwd`) and TERM/KILLs them. Both run before any worktree return, branch delete, or backend kill, and are idempotent on a retried teardown. The branch+head attribution logic is factored out of bin/fm-crew-state.sh into the new shared bin/fm-nm-run-lib.sh so both scripts use the same ownership contract. * no-mistakes(review): Fail closed on incomplete teardown cleanup * no-mistakes(review): Bind teardown cleanup to verified run and process identities * no-mistakes(review): Require confirmed aborts and convergent identity-safe process reaping * no-mistakes(review): Handle process exits during teardown identity checks * no-mistakes(review): Restore teardown library in hermetic gotmp fixtures * no-mistakes(document): Document teardown run attribution and timeout * no-mistakes(lint): Rename shell variable conflicting with done keyword * no-mistakes: apply CI fixes * test: give fork scaffolding tests the delivery contract upstream now requires Upstream kunchenguid#1563 made --mode mandatory for ship briefs and --mode plus --yolo mandatory for ship spawns, and updated the tests it shipped with. Three fork tests were written after the fork diverged, against the pre-kunchenguid#1563 interfaces, so they called the scaffolds with no delivery contract. Merging the two trunks put those callers in front of the new requirement and they refused before reaching the behavior each test exists to pin. Git could not see this: the requirement and the callers live in different files, so both sides merged clean and the breakage only showed up when the suite ran. Upstream's contract is kept exactly as it is - it is a deliberate safety gate, and AGENTS.md section 7 depends on the brief and the spawn refusing to guess. Each fork test instead states its mode explicitly, so every assertion it was written to make still runs. The standing-worker-rules variants each state their own mode, because those cases assert mode-specific prose and fm-brief no longer reads the registry. Verified: all three fail on the merge commit and pass here, with fm-brief, fm-launch-lib and fm-model-zero-budget green. --------- Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com> Co-authored-by: Gavin <51008413+allstargg@users.noreply.github.com> Co-authored-by: Christopher McKay <101884182+karotkriss@users.noreply.github.com> Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
sbracewell64
added a commit
to sbracewell64/firstmate
that referenced
this pull request
Aug 10, 2026
…ks (#39) * feat(bin): require an explicit per-task delivery contract (kunchenguid#1563) * feat(bin): require an explicit ship delivery mode in fm-brief A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions. * feat(bin): require an explicit ship delivery contract at spawn and promotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode. * docs: record the explicit per-task delivery contract AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly. * test: pass ship delivery flags per call site in the Herdr launcher e2e The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper. * test: pass the ship delivery contract in the secondmate suites Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires. * feat(bin): support remote secondmate homes (kunchenguid#1576) * Add generic remote secondmate transport * Add routed remote secondmate replies * Add remote outbox backlog handoff * Integrate remote secondmate lifecycle * no-mistakes(review): Fix remote snapshot and handoff races * no-mistakes(review): Serialize remote home provisioning transactions * no-mistakes(review): Harden remote lifecycle transaction boundaries * no-mistakes(review): Serialize remote lifecycle mutations and fail closed * no-mistakes(review): Close remote lifecycle and file race windows * no-mistakes(review): Serialize remote reply retirement and inheritance * no-mistakes(review): Harden remote transfer integrity and recovery * no-mistakes(review): Serialize remote respawn with registry retirement * no-mistakes(document): Document remote bootstrap convergence accurately * no-mistakes(document): Clarify skipped remote secondmate mutations * no-mistakes(lint): Resolve remote script ShellCheck warnings * no-mistakes: apply CI fixes * feat: add per-task trace context propagation (kunchenguid#995) * feat(spawn): propagate a native W3C traceparent to spawned agents Add a default-off capability that resolves one W3C traceparent for a task, injects it into the agent's pane shell as the TRACEPARENT environment variable immediately before launch, and records the identical value as traceparent= in state/<id>.meta, so an external observer that explicitly reads that env value or meta field can correlate a worker, a Secondmate, and their nested children into one trace with no collector, storage, UI, or vendor coupling. TRACEPARENT as an environment variable is a firstmate convention carrying a W3C-formatted value: W3C Trace Context standardizes the header, not an env var, and OpenTelemetry SDKs do not read it automatically, so a downstream must consume it deliberately; this feature parents no SDK span by itself. Identity is per task, not per spawn: the carrier is minted with random ids on the first spawn, adopted as a child (fresh span, same trace) for a nested spawn whose parent already holds one, and reused verbatim from the meta on relaunch, so a task keeps one stable logical identity across restarts. A malformed or all-zero inherited value is treated as absent and roots a fresh trace. A new root is sampled (01) - a sampling decision a downstream parent-based sampler honors, not a guarantee that any collector stores a span, and firstmate emits no spans; a child preserves the inherited flag. Trust boundary: a firstmate-minted root is random and reads no prompt, path, task prose, credential, or arbitrary environment key. An inherited TRACEPARENT is opaque caller-controlled data - up to 24 bytes of id passed through after syntax validation - so whoever set it controls those bytes, a bounded fixed-width channel rather than a general content or secret channel. The feature adds no OTEL_* variable, no tracestate, and no arbitrary environment injection; it runs no configurable or arbitrary command, only the fixed local od and tr (resolved from PATH) to read a few bytes of entropy - a small local pipeline with no network or watchdog and no hard latency guarantee. Any entropy or validation failure that returns omits the carrier without aborting the spawn. A default-off spawn leaves the generated meta and launch environment unchanged. Enablement is default-off (config/trace-context, or FM_TRACE_CONTEXT where a non-empty value overrides and unset or empty defers to the file) and is propagated into secondmate homes, taking effect at each agent's next launch: a Secondmate launched or relaunched after enablement carries the primary trace into its nested workers, while an already-running Secondmate roots new traces for its own workers until relaunched. Injection reuses the existing GOTMPDIR channel, so all spawn backends and harnesses and the ship, scout, and secondmate paths are covered. Covered by a pure-library suite and a spawn-path integration test (fake tmux plus a real worktree, hermetic against ambient FM_TRACE_CONTEXT) proving the recorded and injected carriers are identical and sent before launch, that default-off writes and injects neither, that a relaunch reuses the recorded carrier, and that an explicit FM_TRACE_CONTEXT overrides the file both ways; plus a source-owner inheritance test proving trace-context propagates and absence-mirrors through propagate_inheritable_config. Documentation follows the repository documentation-audiences contract: docs/trace-context.md is maintainer-architecture rationale, the configuration schema lives in docs/configuration.md, and the repeatable test evidence is separated into docs/verification/trace-context.md (maintainer-verification), registered in docs/documentation-audiences.json. * fix(spawn): propagate the effective trace-context decision to secondmates FM_TRACE_CONTEXT overrode trace context only in the process that read it. A newly launched secondmate decided enablement from the inherited config/trace-context file alone, so the override did not cross the primary-to-secondmate boundary: FM_TRACE_CONTEXT=off with the file present left the secondmate's nested workers traced (a broken kill switch), and FM_TRACE_CONTEXT=on with the file absent left them untraced despite the inherited carrier. Deliver the primary's effective decision to a newly launched secondmate as a normalized on/off FM_TRACE_CONTEXT in the launch prefix, so a FM_TRACE_CONTEXT override governs the nested primary -> secondmate -> worker chain both ways, not just the copied file. The value is bounded to the literal on/off and does not broaden environment injection; the already-running secondmate boundary is unchanged. Add a genuine two-level spawn regression that drives fm-spawn twice with the exact environment the primary injects into the secondmate and proves both divergent directions end to end. Correct the documentation that implied secondmate coverage on every backend, since orca and cmux reject secondmate spawns, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Clarify Secondmate trace-context launch snapshots * no-mistakes(document): Correct trace-context documentation ownership and relaunch semantics * fix(spawn): resolve the trace-context decision once for carrier and snapshot The effective trace-context decision was read twice per spawn: once inside fm_trace_context_resolve for the recorded carrier, and again for the secondmate FM_TRACE_CONTEXT launch snapshot. A config-file change between the two reads could pair a carrier with the opposite enable state - an injected carrier with an off snapshot, or no carrier with an on snapshot. Freeze the effective on/off decision once, drive the carrier resolution under that frozen FM_TRACE_CONTEXT so it cannot independently re-read the file, and reuse the same frozen decision for the secondmate launch snapshot. Add a spawn-path regression that drives the file-decided path and proves the recorded carrier and the delivered snapshot always agree, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Preserve legacy Secondmate trace boundary * no-mistakes(document): Correct trace-context verification comparison base * no-mistakes(review): Captain, prevent failed trace delivery metadata claims * no-mistakes(review): Captain, align trace-context tests and verification evidence * no-mistakes(document): Correct trace-context verification evidence * no-mistakes(lint): Suppress intentional ShellCheck literal-dollar warnings * no-mistakes(review): Captain: freeze trace context at session start * no-mistakes(test): Captain: stabilize scheduler test and document Kimi trace coverage * no-mistakes(document): Document trace-context safety boundaries * fix(trace): fail off on stale session snapshots Publish each home session decision atomically through a same-directory temporary file and bind it to the current session lock. A replacement failure can no longer leave an earlier on decision active in a later session; missing, stale, malformed, or unpublishable state defaults safely to off. Add regressions for read-only replacement and failed publication, update spawn and session-start fixtures for the lock-bound format, and refresh the architecture and verification records. * no-mistakes(review): Fix trace spawn failure independence and duplicate safety * no-mistakes(document): Refresh trace-context documentation and verification * no-mistakes(review): Clear partial backend input after failed trace submission * no-mistakes(review): Stop unsafe trace delivery before launch append * no-mistakes(document): Document unsafe trace delivery handling * fix(trace): bound each trace to one routed task, never the routing agent A persistent Secondmate holds its launch-time TRACEPARENT in the process environment for its whole life, and routed requests never replace it, so resolving new-task carriers from the ambient environment chained every routed task into one ever-growing trace per Secondmate with distinct parent ids. Resolve now reuses the task's recorded carrier or mints a fresh sampled root, never reading ambient TRACEPARENT, so each routed task is its own trace boundary while relaunch, recovery, and scout-to-ship promotion keep one stable per-task identity. The spawn regression models the reviewed scenario exactly: two unrelated tasks spawned sequentially through one persistent Secondmate environment record and inject distinct trace ids, adopt nothing from the Secondmate's carrier, and a relaunch of the first task reuses its original carrier verbatim. * docs(trace): define the per-task trace boundary The design contract is one task per trace: a persistent Secondmate is routing infrastructure with its own agent identity, never a shared trace root for the unrelated tasks routed through it. Root/recovery semantics replace the removed child-inheritance path, the sampling and safety sections drop inherited-carrier language because ambient TRACEPARENT is never read, and the verification page records the refreshed suite inventories including the two-task Secondmate boundary regression. * test(trace): adopt the explicit per-task delivery contract in spawn fixtures Rebasing onto current main brings the explicit per-task delivery contract: ship spawns now require --mode and --yolo instead of resolving them from the project registry. The trace spawn fixtures pass the same explicit contract canonical spawn tests use, preserving the per-task trace boundary coverage unchanged, and the verification page records the refreshed comparison base. * fix(bin): harden tmux agent liveness across harnesses (kunchenguid#1577) * fix(bin): classify tmux agent liveness independent of process titles `fm_backend_tmux_agent_state` attributed a pane solely from `#{pane_current_command}`, which is a process TITLE a harness can rewrite, not a structural fact. Claude Code 2.1.220 reports its version string there, so a live Claude endpoint classified `ambiguous`: the session-start secondmate liveness sweep could no longer see it, and any consumer that gates on a positive classification refuses outright. Read a second, independent name source: the kernel `comm` of every process in the pane tty's foreground process group. Either source naming a verified harness yields `alive`, because a false `dead` is the one verdict that can start a duplicate agent on a live worktree. Scoping to the foreground process group rather than the pane's descendants keeps a harness-named background process from faking an agent, and covers multi-process launchers (the Pi Launcher path) without a special case. Verified on 2026-08-03 against all seven adapters running for real on tmux 3.6a / macOS 26.5.2 arm64: claude 2.1.220, codex-cli 0.146.0, opencode 1.18.11, pi 0.82.0, pi-signed 0.82.0, grok 0.2.118, kimi 0.31.1 all classify `alive`, each attributed by a source independent of its title. Two tests, because they fail for different reasons: - tests/fm-tmux-agent-liveness.test.sh pins the logic with real processes and no harness, so it runs everywhere CI runs tmux. It drives the two name sources apart on purpose and asserts the divergence, so no case can go quietly vacuous. - tests/fm-harness-liveness-drift-live-e2e.test.sh relaunches every installed harness and fails naming the harness and version when one stops being attributed by a title-independent source. AGENTS.md section 4 carries the resulting standing rule, and firstmate-coding-guidelines owns how to satisfy it. * no-mistakes: apply CI fixes * docs: move the harness-dependent-check policy out of AGENTS.md The standing rule was stated in AGENTS.md section 4 with the mechanics in firstmate-coding-guidelines, which split one contract across two owners and charged every session for a rule that only fires when firstmate's own harness-dependent code is being changed. firstmate-coding-guidelines is now the single owner of both the rule and how to satisfy it: real-harness proof required, that proof authorized to spend tokens, structural signals preferred over vendor-rendered surfaces, and a guard that fails loudly naming the harness and version where a surface signal is unavoidable. No inline stub is left behind, because AGENTS.md already carries the load trigger for that skill in sections 7 and 13, so it is read before any change to firstmate's shared tracked material. Also records the cross-platform lesson the pipeline caught in the portable regression, and corrects that file's header: the divergence assertion lives on the version-string case, which diverges on both supported platforms, rather than on every case. * no-mistakes(review): Harden tmux liveness identity and drift validation * no-mistakes(document): Clarify cross-platform tmux liveness documentation * feat(bin): propagate trace context to remote secondmates (kunchenguid#1609) * feat(bin): trace remote secondmate routes and unify the inherit allowlist Per-task W3C trace context (kunchenguid#995) resolved and injected its carrier only at the local spawn path. A remote secondmate is routed through spawn_remote_secondmate, which returns long before that site and wrote its own metadata block, so a remote secondmate stayed silently untraced even with the capability enabled. The parent home still owns that task's identity, because it holds the metadata an observer reads. It now resolves the carrier against the task's own meta under its own frozen decision - reused verbatim on relaunch, freshly rooted otherwise, never adopting the parent process's ambient TRACEPARENT - and hands it to the configured host through a new fm-spawn --traceparent argument, accepted only for a secondmate launch and only as a strict W3C value. The remote host exports it at the same unconditional pre-launch site and reports back the carrier its endpoint actually holds, which the parent records, so an already-alive endpoint reports the identity its agent really received rather than one the parent merely intended. Disabled remains byte-identical and off. The remote inherit path also carried its own hardcoded copy of the inheritable config set, already drifted from FM_INHERITABLE_CONFIG by trace-context. Both remote ends now derive from that one declaration, so a future item cannot be sent by one side and refused by the other, and session-scoped enablement items are skipped on live convergence exactly as the local path skips them. Also fixes a latent stderr leak: an absent session lock printed a raw redirect failure, which the new remote resolve site made visible. Adds tests/fm-remote-secondmate-trace-context.test.sh, driving the real parent -> fm-on -> remote entrypoint -> control -> remote fm-spawn chain over the deterministic SSH boundary and reading the carrier back from the remote pane's own log. * no-mistakes(document): Clarify remote trace and allowlist contracts * feat(bin): preflight remote runtime tool paths (kunchenguid#1623) * feat(bin): widen the remote runtime PATH and add a remote doctor preflight The fixed remote entrypoint hard-coded a four-directory PATH, so a remote account whose tools live under nix or a per-user profile could not run basic Firstmate work without a login shell. The entrypoint now composes its child PATH from the code root's bin, the account's ~/.local/bin, the common package-manager directories that actually exist on the host, and the portable system tail, deduplicated and in a fixed order, still under env -i with the same variable allowlist and no shell command string. fm-remote-doctor.sh reports that exact PATH by inheriting it from its own entrypoint launch rather than recomposing it, so the ordering keeps one owner. It is read-only, reports where each required and optional tool resolved, and exits non-zero naming every required tool that did not. Remote seeding runs it as a preflight before anything is created on the host and restores the registry when it fails. * no-mistakes(review): Harden remote git authorization and missing-tool diagnostics * no-mistakes(document): Document remote PATH doctor and safe shims * no-mistakes(lint): Fix ShellCheck findings in remote path tests * no-mistakes(lint): Suppress exported fixture's false-positive ShellCheck warning * feat: gate remote second mates on Herdr readiness (kunchenguid#1639) * feat(bin): gate remote second mates on herdr readiness A remote second mate now always runs on the Herdr backend, whose server belongs to the host's GUI login session and therefore outlives the SSH connections that supervise it. fm-spawn's remote route forces that backend and the host-local control script refuses any other, so the requirement cannot be dropped from either side. fm-remote-doctor.sh becomes the single owner of what "ready" means. It keeps its PATH and tool reporting from kunchenguid#1623 and adds the Herdr, Aqua LaunchAgent, GUI-session, server-reachability, and entrypoint-symlink checks, tagging each gap fixable: or human: with the exact operator step. --fix closes only the automatable gaps - writing and loading the Aqua-scoped dev.firstmate.herdr launch agent, starting the server where no launch agent applies, and recreating the entrypoint symlink - then re-derives every check from the host, so a human gap is never presented as fixed. It never creates a login session, writes an auto-login password, or touches FileVault. Remote seed, remote spawn, and the startup liveness relaunch all run the same check, repair, re-check sequence through one shared library and fail closed with the doctor's own gap text. Recovery inherits the gate because it respawns through the same route. Tests drive the real doctor against a controlled account fixture with a private HOME, a state-backed launchctl, and a fake herdr, and prove the dangerous actions are never attempted. The remote lifecycle suites gain a stateful Herdr CLI fixture and answer the readiness gate at the SSH boundary, so they never inspect or repair the runner's own account. * no-mistakes(review): Validate launch-agent contract and confirm Herdr startup * no-mistakes(review): Validate loaded launch-agent contract before readiness * no-mistakes(review): Refuse legacy remote backends without altering routes * no-mistakes(review): Clarify conditional remote readiness repair sequence * no-mistakes(review): Repair remote readiness before liveness probing * no-mistakes(review): Preserve unknown seeds and reject legacy liveness * no-mistakes(document): docs: clarify remote Herdr backend ownership * fix: isolate remote secondmates in shared Herdr session (kunchenguid#1659) * Pin remote secondmates to fm-remote * no-mistakes(review): Fail closed on legacy remote Herdr endpoints * no-mistakes(review): Isolate fm-remote launch agent from interactive default * no-mistakes(document): Document shared remote Herdr retirement safety * feat: route remote commands through an Aqua job worker (kunchenguid#1660) * feat: run remote commands through Aqua job worker * no-mistakes(review): Enforce remote job deadlines and safe worker shutdown * no-mistakes(review): Refresh stale workers and harden dependency-free supervision * no-mistakes(review): Harden worker ownership recovery and shutdown quarantine * no-mistakes(review): Fix doctor bootstrap, harness repair, and output draining * no-mistakes(review): Probe doctor tools through authenticated worker bootstrap * no-mistakes(review): Refresh stale workers before doctor tool probes * no-mistakes(review): Recover stopped quarantines and extend job deadlines * no-mistakes(review): Separate queue and execution timeout windows * no-mistakes(review): Supervise Linux worker crashes and bind root identity * no-mistakes(review): Resolve authorized Nix profile bin links * no-mistakes(review): Clarify Nix path resolution documentation * no-mistakes(review): Harden PATH safety and nvm selection * no-mistakes(review): Honor nvm system defaults and refresh doctor digest * no-mistakes(review): Keep workers ready during active jobs * no-mistakes(review): Bound pre-execution validation by job timeout * no-mistakes(document): Clarify remote worker documentation * no-mistakes(lint): Fix remote worker ShellCheck diagnostics * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * fix: clarify remote doctor bootstrap path (kunchenguid#1691) * fix(bin): bound remote SSH dead-peer detection (kunchenguid#1699) * fix(remote): arm SSH dead-peer detection in fm-on.sh A vanished remote host mid-poll (a reboot, a dropped link) left ssh blocked indefinitely on a half-open TCP connection, because fm-on.sh's ssh invocation had no ServerAliveInterval/ServerAliveCountMax. This wedged the remote-reply ferry: fm-procevent.sh's runner blocked inside the ssh child and never reached its own no-result -> claim-release -> reconcile re-arm self-healing path, which otherwise already handles a nonzero exit with empty output correctly. Recovery required a manual retire and re-arm. Arm ServerAliveInterval=15 and ServerAliveCountMax=3 by default (bounded ~45s detection window), both overridable via FM_SSH_ALIVE_INTERVAL and FM_SSH_ALIVE_COUNT_MAX. This is a transport- level fix in fm-on.sh, so it covers every remote command routed through it, not just the reply ferry. The remote sshd answers keepalive probes independently of whatever the remote command is doing, so a legitimately long-but-alive command (a 55s poll, a clone, the doctor) is never falsely killed - only a truly vanished peer trips it, turning that case into a bounded, detectable ssh failure (exit 255) instead of an indefinite hang. Extends tests/fm-on.test.sh with a behavioral regression asserting a bounded, positive ServerAliveInterval/ServerAliveCountMax on the real ssh argv captured through the FM_SSH_BIN process seam, plus coverage that both are env-overridable. * no-mistakes(document): Document SSH dead-peer detection ownership * fix: report stale AXI tools during bootstrap (kunchenguid#1701) * feat(bootstrap): gate stale axi CLIs at the floors firstmate actually uses Add gh-axi 0.1.29 floor so bare --squash PR merges stop failing quietly on older builds. Raise tasks-axi to FM_TASKS_AXI_MIN=0.2.2 (multi-id mv) while keeping feature probes. Keep quota-axi at 0.1.16 after verifying schema 3 and per-model availability already ship there; runway remains optional. * no-mistakes(document): Clarify AXI compatibility documentation ownership * fix: prevent false watcher-down alarms in Claude sessions (kunchenguid#1661) * fix(guard): stop false watcher-down alarm mid-turn under Claude auto-arm bin/fm-guard.sh derived its watcher-health verdict from fm_watcher_healthy, which requires a live watcher process holding the home lock. Under the Claude Stop-hook auto-arm supervision model the watcher is armed at each turn end and exits on its wake, so it runs only between turns. Every guarded command run mid-turn therefore found no live watcher and printed the "WATCHER DOWN - SUPERVISION IS OFF" banner even though supervision was healthy. Because the episode key was derived from the beacon mtime (which the between-turns watcher advances every poll), the full banner re-printed on essentially every command, and the message always blamed a "fresh beacon" that was in fact fresh. Make the pull guard's health check model-aware via a new fm_watcher_supervision_verdict in bin/fm-wake-lib.sh: - Under the auto-arm model a beacon fresh within FM_GUARD_GRACE is healthy even with no live watcher process; only a beacon stale beyond grace (or absent) is a genuine lapse and alarms. - Under every persistent-watcher harness (codex foreground checkpoint, opencode/pi/grok background arm, tmux, unknown) a live identity-matched watcher with a fresh beacon is still required, unchanged. The banner now names the true failing condition, a missing live watcher process versus a genuinely stale beacon, instead of always blaming the beacon, and the once-per-episode dedup keys on that condition rather than the beacon mtime so a genuine lapse announces once and does not re-print each turn. The turn-end guard keeps the strict fm_watcher_healthy check because it fires at the turn boundary, where the auto-arm brings a fresh watcher up and it cooperates with that arm. fm_watcher_healthy itself is unchanged, so the arm layer's start/attach/replace decisions are unaffected. Tests in tests/fm-guard-stale-banner.test.sh cover the auto-arm healthy fresh-beacon-without-a-watcher case, the auto-arm stale-beacon alarm and its stable episode, the true-reason banner wording, and the reason-keyed episode surviving a beacon mtime change; existing persistent-model cases are pinned to that model. * no-mistakes(review): Pin secondmate supervision model to launched harness * no-mistakes(document): Align watcher documentation with model-aware supervision health * test: prevent fixture temporary directory leaks (kunchenguid#1704) * fix(tests): stop fixture-tempdir helper from self-deleting under command substitution fm_test_tmproot is almost always called as `TMP_ROOT=$(fm_test_tmproot prefix)`, which forks a subshell to capture its stdout. The old implementation set its EXIT cleanup trap inside that call, so the trap fired - and deleted the fixture root - the instant the subshell exited, before the real caller's own EXIT trap was ever installed. Every test using the documented call pattern leaked its fixture root on every run; two suites had already independently discovered and worked around this with ad-hoc mktemp calls. Registration now goes through a $$-keyed registry file instead of in-process state, since $$ resolves to the invoking shell's PID even inside the subshell. The real cleanup trap is armed once at source time (always the real caller, never a subshell) for EXIT, INT, and TERM. A best-effort orphan sweep on next source reaps marked fixture roots old enough to be from a killed prior run. Simplifies the two existing ad-hoc workarounds (fm-procevent.test.sh, wake-helpers.sh) back onto the shared helper now that it works correctly. * no-mistakes(review): Preserve live fixtures during orphan reaping * no-mistakes(review): Harden fixture ownership against PID reuse * no-mistakes(review): Secure cleanup registry against path precreation * no-mistakes(review): Make fixture registration transactional * no-mistakes(document): Documentation already matches fixture cleanup behavior * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * feat(herdr): enable presentation spaces by default (kunchenguid#1708) * feat(herdr): default presentation spaces on with an explicit opt-out Herdr's disposable one-task presentation workspace was opt-in through the presence of local config/herdr-presentation-spaces. It is now on by default, and a home opts out by writing "off" into that same file. Values are read with the whole-file whitespace-stripped convention the other scalar config items already use, plus case folding. An absent file, an empty file, and "on" all resolve on; only "off" opts out; an unrecognized value warns and keeps the default rather than failing a spawn over a purely visual setting. The empty file is exactly the historical opt-in form, so every home that had already enabled the projection stays enabled with no migration step, and no previously enabled home can be turned off by the flip. Because absence now means on at both ends, secondmate inheritance needs no item-specific convergence: mirroring an absent primary file converges a secondmate to the same default-on rather than turning its projection off, and only an explicit primary opt-out propagates the opt-out. The gate itself moves into fm_backend_herdr_presentation_enabled in the Herdr adapter so the semantics have one owner that regressions can exercise directly. * no-mistakes(document): Document Herdr default-on presentation safety --------- Co-authored-by: kunchenguid <kun-1@kunchenguid.com> * fix(bin): surface fleet-wide open decisions on every wake drain (kunchenguid#1711) * fix: surface consolidated open decisions on every wake-drain A needs-decision or blocked event buried under later, unrelated status appends was only ever shown via the last-line wake annotation, so a still-open captain decision could go silently missed even though status_open_decisions (fm-classify-lib.sh) already folds the whole status stream correctly and fleet-snapshot/bearings already reuse it. Wire that same fold into bin/fm-wake-drain.sh: a new fleet-wide scan_open_decisions wrapper scans every state/<id>.status, and fm-wake-drain.sh prints a separate, bounded OPEN DECISIONS section on every drain (including the empty-queue fast path), so session-start and every wake-handling turn surface it for free without duplicating the open/resolved fold itself. Heartbeat wakes drain through the same script, so this covers that surface too. Also tighten status_open_decisions' file guard to skip an unreadable status file instead of leaking a bash redirection error, now that a fleet-wide directory scan can reach files a single targeted read would not. * no-mistakes(review): Prevent status symlinks leaking open decisions * fix: drop unbounded perl subprocess from status symlink guard The review step's own symlink-safety auto-fix (O_NOFOLLOW read via a perl subprocess) forked one perl process per status file scanned by the new fleet-wide open-decisions scan, with no cap - inflating fm-wake-drain.sh's total external-read cost from 8 (the existing annotation read_cap) to 18 in the enrichment-caps regression test. The plain [ -L "$f" ] check already rejects any status file that is itself a symlink before any read happens, which is exactly what the new regression test exercises and is the same defense level the sibling scan_captain_relevant_statuses/last_status_line already rely on elsewhere in this file (no O_NOFOLLOW). Drop the subprocess-based nofollow read and keep the cheap builtin guard. * no-mistakes(document): Document actionable fleet-wide open decision drains * fix(bin): abort parked runs and reap leaked processes before teardown (kunchenguid#1710) * fix(bin): abort orphaned no-mistakes runs and reap leaked processes at teardown Teardown could remove a task's worker while its no-mistakes pipeline run was still parked at a gate, leaving an orphaned run holding a fleet slot indefinitely (observed 2026-08-03: runs parked 7h39m and parked at a post-CI approval gate). It could also leave backgrounded/disowned descendant processes rooted under the worktree or tasktmp surviving reparented to init (observed: two `go test` binaries pinning CPU for hours with no live task meta to attribute them to). Add two coupled pre-teardown steps, both scoped to this task's exact branch/head or worktree/tasktmp so they can never touch another task's run or processes: - conclude_task_no_mistakes_run aborts a run parked at a gate via `no-mistakes axi abort`, cd'd into the exact worktree so the daemon resolves the run itself rather than teardown naming a --run id. - reap_task_worktree_processes sweeps for processes whose cwd is under the worktree or tasktmp (via `lsof -a -d cwd`) and TERM/KILLs them. Both run before any worktree return, branch delete, or backend kill, and are idempotent on a retried teardown. The branch+head attribution logic is factored out of bin/fm-crew-state.sh into the new shared bin/fm-nm-run-lib.sh so both scripts use the same ownership contract. * no-mistakes(review): Fail closed on incomplete teardown cleanup * no-mistakes(review): Bind teardown cleanup to verified run and process identities * no-mistakes(review): Require confirmed aborts and convergent identity-safe process reaping * no-mistakes(review): Handle process exits during teardown identity checks * no-mistakes(review): Restore teardown library in hermetic gotmp fixtures * no-mistakes(document): Document teardown run attribution and timeout * no-mistakes(lint): Rename shell variable conflicting with done keyword * no-mistakes: apply CI fixes * test: give fork scaffolding tests the delivery contract upstream now requires Upstream kunchenguid#1563 made --mode mandatory for ship briefs and --mode plus --yolo mandatory for ship spawns, and updated the tests it shipped with. Three fork tests were written after the fork diverged, against the pre-kunchenguid#1563 interfaces, so they called the scaffolds with no delivery contract. Merging the two trunks put those callers in front of the new requirement and they refused before reaching the behavior each test exists to pin. Git could not see this: the requirement and the callers live in different files, so both sides merged clean and the breakage only showed up when the suite ran. Upstream's contract is kept exactly as it is - it is a deliberate safety gate, and AGENTS.md section 7 depends on the brief and the spawn refusing to guess. Each fork test instead states its mode explicitly, so every assertion it was written to make still runs. The standing-worker-rules variants each state their own mode, because those cases assert mode-specific prose and fm-brief no longer reads the registry. Verified: all three fail on the merge commit and pass here, with fm-brief, fm-launch-lib and fm-model-zero-budget green. --------- Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com> Co-authored-by: Gavin <51008413+allstargg@users.noreply.github.com> Co-authored-by: Christopher McKay <101884182+karotkriss@users.noreply.github.com> Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
yelenplays
added a commit
to yelenplays/firstmate
that referenced
this pull request
Aug 10, 2026
…#2) * fix: answer session ownership as identity, not harness category fm_session_lock_owned_by_self() compared the recorded lock pid against the NEAREST harness-named ancestor, which asks a category question ("is my closest harness ancestor the owner?") where the lock records an identity ("this session owns this home"). Claude Code 2.1.220 interposes a daemon, a pty host, and a bg-spare helper between every hook and the session, so the walk stopped at the helper and a session stopped recognising its own lock. Two consequences, both reproduced: - The Claude Stop auto-arm exited at the identity gate on every firing, before writing a byte, so state/.claude-autoarm.lock never appeared and watcher continuity silently depended on manual re-arming. - bin/fm-lock.sh refused a session its own home, naming that session's own pid as "another live firstmate session". Ownership is now chain membership: the recorded pid counts when it appears anywhere in the current ancestry. The hop budget moves from 8 to 12 because the session now sits six or more parents above a hook. fm_harness_ancestry_pid(), which mints the lock value, gets the companion change: it returns the outermost pid of the uninterrupted harness-named run rather than the nearest one, so a lock acquired after the helper tree appears records the session instead of a helper that dies with the next generation. The run deliberately stops at the first non-harness parent, so a genuinely separate parent session - reachable only across a multiplexer server or a plain shell - stays outside this session's identity. bin/fm-lock.sh now refuses only a lock recorded outside this session's ancestry, which also keeps a home readable when its lock was minted by a different adapter or an older firstmate on the same session. fm-sessionstart-nudge.sh had a second full copy of this contract. It already asked the identity question but at the old depth, so it is consolidated onto the shared owner rather than left to drift. Fail-closed behaviour is unchanged: an unresolvable ancestry, a malformed lock, and a lock held by an unrelated live session all still refuse. Deliberately not addressed: the "cycle ended without an actionable reason" reporting defect. Those reports are currently the loudest signal that continuity is broken, so the identity gate lands first. * docs: record session identity evidence under Claude Code 2.1.220 The existing auto-arm evidence was measured on 2.1.219, before the helper tree existed, so it does not cover the ancestry depth the identity predicate now has to survive. * no-mistakes(review): mint the session, not a shared harness ancestor * no-mistakes(document): note peer-runtime ancestry budgets in session-lock lib header * fix(brief): name the PR gate before the commit in no-mistakes briefs Four crewmates in a row (2026-07-26/27) reported `done:` as soon as their implementation was committed with green project tests, never starting the pipeline. Each needed a steer, and each pipeline run then found real defects. They were not ignoring the brief - they were following it. The generated no-mistakes Definition of done opened with "The task is complete only when committed on your branch", offered a `done: {summary}` gate right there, and deferred the pipeline to a later firstmate instruction. The real gate, `done: PR {url} checks green`, sat fourteen lines below. A worker that stops reading at its first stop condition stops at the commit. Invert it: the PR with green checks is the first and only stop condition, the commit is explicitly an intermediate step, and the worker starts the pipeline itself instead of waiting to be told. The section gets shorter, not longer - the defect was the order, not a shortage of text. Scoped to the no-mistakes branch; direct-PR and local-only genuinely do complete at the commit and are untouched. The ask-user prohibition, the `--yes` prohibition, the shared-daemon rule, and the worktree-isolation assertion are preserved verbatim. AGENTS.md's validate step is updated to match: firstmate's harness-correct trigger becomes the fallback for a worker that did not start validation. Add a regression test pinning the order, the absence of the earlier `done:` gate, and the preserved safety wording. Nothing checked this before, which is why it went unnoticed through four repetitions. * feat(decisions): add the structured decision record A captain decision was one prose field carrying the question, the options, the recommendation and the evidence at once. Measured on the live backlog on 2026-07-29: 116 open captain decisions, hold reason min 60, median 309, max 1457 characters, against a collapsed notification budget of about 80 and a decision card question budget of about 52. Nothing could render that, and nothing should have to parse prose to find the options. Add seven authored fields alongside the prose, which keeps its exact meaning as the why disclosure: question, consequence, recommend, options[], expires_at, sensitivity, and safe_preview. fm-classify-lib.sh owns the wire form, the field names and the ceilings, and gains the status-line record block, its parsers and decision_record_validate. fm-decision-hold.sh accepts the fields on hold, stores them in the hold body, carries them into a resolved hold, and gains record (fields or JSON) and status-line (compose and validate). status_line_note strips the block, so every existing consumer keeps showing the same human summary. The change is strictly additive. The block sits after the first colon of a status line, so the verb parser, the key parser, the decision fold, the watcher and the away-mode daemon are untouched, and a decision registered without record options behaves exactly as before and is never rewritten. The ceilings are enforced rather than documented, because a field that overflows cannot be rendered at all. The first option may not be destructive, since that is the action an Apple Watch double tap fires with no confirmation. sensitivity classifies and grants nothing: AGENTS.md section 7 and ask-user-authority keep owning who may answer what, and a regression asserts no script outside the record owner and its grammar reads it. safe_preview is the only field that may leave the trusted session, so it is refused when it carries a link, address, handle, path, identifier, amount, opaque token, invisible separator, or a phrase lifted out of the why prose. * fix(spawn): merge the Claude turn-end hook into a project's own settings fm-spawn installed the Claude crewmate turn-end hook by writing <worktree>/.claude/settings.local.json with `cat >`, replacing whatever the project kept there. Reproduced end to end: a worktree whose settings file carried three pre-approved permission entries was left as a single hooks object with the permissions gone. Some vaults track that file, so a crewmate touching it or any broad commit could land the deletion in the captain's own repository, and the diff would read as ordinary agent configuration. bin/fm-claude-worktree-hook.sh now owns that file's contract for both ends of the task lifecycle: - install merges one Stop hook into the existing document, preserving every other key and every hook the project already had, including its own Stop hooks. - A missing or empty file has nothing to preserve and is simply written. A malformed, symlinked, or structurally surprising file is refused with no write at all, so an unreadable file is never silently replaced; fm-spawn turns that refusal into a refused spawn naming the file to repair. - install records what it found, so remove restores the original bytes when nothing else changed the file, keeps settings the task added, and deletes only a file (and .claude directory) firstmate itself created. - Only a settings file firstmate created is added to the worktree's info/exclude; a project's own file stays in git's view. fm-teardown removes the hook before it inspects the worktree for uncommitted work, because a hook merged into a tracked settings file would otherwise read as the crewmate's own unlanded change and refuse a teardown of a clean worktree. A refused teardown reinstalls the hook so a still-running crewmate keeps signalling turn ends. The other harnesses were checked and do not have this defect: opencode, grok, and kimi each write only firstmate-reserved names in the worktree (.opencode/plugins/fm-turn-end.js, .fm-grok-turnend, .fm-kimi-turnend), pi writes outside the worktree entirely, and codex writes nothing. Kimi's global config edit already goes through its own marker-region installer. Tests: tests/fm-claude-worktree-settings.test.sh drives the real fm-spawn and fm-teardown against real git worktrees, covering pre-existing permissions and hooks, absent/empty/malformed/symlinked files, byte-exact restoration, and a tracked settings file surviving a full spawn-to-teardown round trip. * fix(supervision): guard channel-armed idle homes on every primary harness An X-mode-armed home with no task in flight still needs a live supervision cycle so an incoming mention can wake it, which AGENTS.md section 14 states as a contract. Both guards contradicted it. bin/fm-guard.sh exited before the watcher-down banner whenever the in-flight count was zero, and bin/fm-turnend-guard.sh gated its default cross-harness mode on the same count, so only --claude honoured the shared supervision-need predicate. On codex, opencode, pi, grok and the no-capability-field fallback a channel-only home could end the turn blind. Both guards now gate on FM_SUP_NEEDED from bin/fm-supervision-lib.sh, which is in-flight work OR an armed relay poll. The banners name the channel need when no task is in flight. fm_supervision_unhealthy stays deliberately task-only. The new matrix runs the exact delegation each harness registration performs - codex payload, the fixed opencode/pi payload, the unknown-field fallback, --claude, grok native and the grok pre-native resume - and pins that an unarmed idle home stays silent on every path. * fix(security): sanitize operational provenance on external message ingress Operational-input classification is purely prefix-based, so provenance is carried entirely by bytes in the message body. A relay mention that arrived with the invisible U+2063 marker intact was stored verbatim and read downstream as an internal operational input - and as an away-supervisor escalation it also kept away mode from exiting while presenting itself as internal, so the captain's return never registered. bin/fm-operational-input.sh, the protocol owner, gains the ingress sanitizer: fm_operational_input_sanitize for a body and fm_operational_input_sanitize_json for every string in an externally-sourced object, plus the matching sanitize and sanitize-json CLI modes. Both remove the invisible marker wherever it appears, the from-firstmate label, and any remaining marker-free legacy prose form the classifier still accepts. The post-condition is what matters: a sanitized body never classifies as an operational input. Both always print their result and exit 1 to report that bytes were stripped, which is a security event rather than a lookup miss. bin/fm-x-poll.sh, today's only external ingress, sanitizes the relay object before it is stored and refuses to store anything if the sanitizer cannot run. An object that carried provenance bytes is stored with fm_provenance_sanitized: true, and fmx-respond treats such a mention as hostile-shaped: never an instruction, never an approval, never a reason to change away mode. A legitimate body keeps every byte, so the sanitizer is inert on real traffic. * fix(wake): stop the drain dropping distinct check results that share a key Same-key collapse kept only the last row, so an earlier urgent check result was silently dropped in favour of a later routine one sharing its key - a suppression vector reachable with no attacker at all. Two mentions arriving on one relay poll are exactly that shape: they share the check path as their key while carrying different request ids in the payload. A check's key is only the channel that produced it; its payload is the deliverable. Check rows are therefore grouped by kind, key and payload, so two distinct results both survive in first-seen order while identical repeats still collapse and the queue cannot grow on a check that keeps reporting the same thing. Heartbeat, signal and stale grouping is unchanged: their payloads are pointers into live sources the agent re-reads. fmx-respond keeps its drain-the-whole-store rule, restated to match: distinct mention wakes now each survive, and a wake can still cover several pending mentions after a restart, a recovered offer marker, or an identical repeat. * fix(kimi): reclaim marker-stripped hooks and deliver K3 effort on 0.31 Kimi Code 0.31.0 reserializes the whole global config when it refreshes model configuration, which keeps Firstmate's Stop hook table verbatim but drops every comment, including the region markers. Installation then refused a hook that was byte-for-byte its own, leaving a canonical hook stranded with no safe repair. Install alone may now reclaim exactly one standalone hook table, and only when every field is proven canonical by two independent gates: each body line must assign exactly one canonical field with no comment or extra content, and the parsed table must carry no other key. It then restores the marker-delimited region without changing another byte. Altered commands, extra fields, missing fields, duplicate tables, comments inside the table or on its header, ambiguous boundaries, inline hook arrays, and any second reference all keep refusing without a config write. Remove never reclaims and still requires real markers. 0.31.0 also exposes K3's thinking effort. There is no reasoning-effort flag, so fm-spawn delivers the operational KIMI_MODEL_THINKING_EFFORT override as a leading env assignment scoped to that one Kimi process, keeping the verified adapter path intact. The override bypasses Kimi's own supportEfforts check, so only the levels K3 advertises (low, high, max) are passed; medium and xhigh stay in task metadata and never reach the wire. * test: make the jobs scheduler refill proof ordered instead of timed The fixture proved slot refill by racing a 0.5s sleep against the replacement worker starting, which false-failed under concurrent test load and reproduced on a clean base. The long worker now blocks until the replacement signals that it started, so the ordering is proven rather than timed, and a scheduler that waited for the oldest worker still fails through the same assertion after a bounded wait. The later runs in the same case only need a proven-set worker that succeeds, so the coordination fixture is dropped before them. * feat(skills): bridge Firstmate to the pinned Matt Pocock skills (Phase 1) Load the original installed skills instead of paraphrasing them, and close the hazards a globally discoverable skill suite creates inside a worker. - bin/fm-skill-path.sh resolves one exact installed plugin skill from the Claude plugin registry, verifying identity before printing anything: the manifest names the plugin and agrees with the registry version, the skill is declared by this version, front matter names the skill, and no symlink or traversal escapes the install root. Refusals print no path and carry distinct exit codes, so a caller can say the honest thing about why a workflow is unavailable. Resolution reads only the plugin registry, so a stale copy under ~/.agents/skills can never satisfy a request. - .agents/skills/grill-intake adapts grilling, grill-with-docs and domain-modeling for intake sharpening. It restates no upstream procedure: it resolves and reads the originals, and states only the Firstmate bindings - who interviews, when a fact is looked up instead of asked, and that glossary and decision records are captured immediately but landed by a crewmate through the project's delivery path. - Generated briefs gain three defensive rules: a worker never starts an interview skill and returns decisions through firstmate; no skill displaces the selected delivery path or no-mistakes' sole review ownership; and a scout's delegated research lands only in its report, with no unsupervised nested agent. - AGENTS.md gains two lines: the narrow grill-intake trigger, and that skill-dependent work must dispatch to a harness that can load the skill and fail honestly when none can. harness-adapters records which harness discovers what. Nothing upstream is copied, vendored, symlinked, or paraphrased, so the workflows improve whenever the plugin does. Colocated tests cover resolution, support-file resolution, a custom CLAUDE_CONFIG_DIR, paths with spaces, and refusal of missing, disabled, ambiguous, tampered, symlinked and malformed installs; brief tests assert each safety rule lands only in the task shapes whose hazard it addresses. * docs(verification): date the stale-copy collision evidence The Matt-derived copies under ~/.agents/skills were removed after the probe ran, so state the comparison as a dated observation and make explicit that the resolver's guarantee never depended on that directory being empty. * feat(skills): reach the Matt Pocock skills from non-Claude runtimes The suite is installed once, as the SHA-pinned Claude plugin mattpocock-skills 1.2.0. Runtimes that discover skills elsewhere had no route to it, and the twelve vendored copies that used to stand in for one had gone stale. Copying the bodies again would recreate that failure, so nothing here copies a skill body. bin/fm-matt-skill.sh resolves one declared skill from the Claude plugin registry and prints the installed original, preceded by an identity header carrying the author, licence, upstream repository, resolved version and commit, and the SHA-256 of the file it read. It refuses without printing anything on stdout when the plugin is absent, disabled, moved, swapped, symlinked, or declares a different skill, so a caller can treat any output as original bytes. Version drift is reported under a PLUGIN CHANGED banner rather than refused, because refusing on every upstream bump would turn each release into an outage and bring back the sync chore this replaces; --require-validated-pin restores the strict reading. Resolution delegates to bin/fm-skill-path.sh whenever that script is present, which is the repo's single owner of plugin-skill resolution. The inline fallback covers only this one plugin and exists because that owner has not landed yet. Deleting it once the owner is available changes no generated pointer, only the resolved_by line. bin/fm-matt-pointers.sh installs one small pointer per declared skill where a runtime looks. Each pointer names Matt Pocock, the MIT licence, the upstream repository, the plugin and the validated version, mirrors the upstream invocation flags so a user-invoked skill is never promoted, carries a firstmate-pointer marker and a generation date so an audit can tell it from a hand-copied directory, and instructs a hard stop rather than any fallback when the load fails. A destination directory without that marker is never written to and never removed. docs/verification/matt-pocock-runtime-reach.md records the live evidence, including the finding that grok 0.2.114 already reaches the plugin natively and needs no bridge, while kimi and codex do not. * fix(tests): stop the pi-signed identity case inheriting the runner's harness markers test_pi_signed_detection_and_session_lock_identity drives bin/fm-harness.sh with a fake ps and PI_CODING_AGENT=true, but never clears the ambient harness family markers. fm-harness.sh answers CLAUDECODE before PI_CODING_AGENT, so every one of those cases resolves "claude" and the first assertion fails whenever the suite is run from inside a Claude Code session. Verified against an untouched upstream tree: the case fails there too, so this is a pre-existing environment dependency, not a merge regression. Clears CLAUDECODE and GROK_AGENT for the fm-harness.sh invocations, the same way test_dash_leading_process_names_are_basename_operands already does immediately below. The session-lock assertions in the same case need no change; they source the library directly and never read those markers. * perf: shard portable serial tests across CI runners (#1544) * perf(ci): shard the portable serial behavior lane across runners The Behavior portable serial job ran all 69 scripts of the serial remainder on one runner. The measured serial sum on run 30725985757 was 1143762 ms (19m04s) against a 20-minute timeout, so the job intermittently reached the cap and was cancelled with every step passing. Setup is only about 7s, so the cost is entirely test wall time. Split the lane into four separate-runner shards. Each shard is still strictly serial, and separate runners mean no two of these stateful scripts ever share a machine, so the split needs no concurrency isolation proof. Assignment is longest-processing-time bin packing over measured per-script duration hints, balancing every shard to 285941 ms (~4m46s) of expected work, and the timeout tightens from 20 to 15 minutes. bin/fm-test-run.sh owns the shard count and refuses a lane whose "ofN" disagrees with it, while ci.yml derives the same count from strategy.job-total rather than a literal, so changing it in either file alone fails the lane loudly instead of leaving part of the required suite unrun. --check-coverage additionally proves the shards are non-empty, disjoint, and exactly equal to the serial lane. No test is weakened, skipped, or removed. Also replace the wall-clock sleeps in the --jobs scheduler test fixture with an explicit signal handshake between the fixtures. The old 0.5s-versus-0.05s race failed on a loaded machine; the handshake passes under sustained CPU saturation. * no-mistakes(review): Correct portable serial shard balance evidence * no-mistakes(document): Document portable serial shard evidence accurately * fix(bin): correct session lock and attached watcher supervision (#1545) * fix(bin): identify harness sessions by path and report delivered wakes Two supervision faults, both reported by a contributor and both open on the default branch. Fault 1: the Stop auto-arm never claims the home. fm_harness_ancestry_pid() matched only the basename of `ps -o comm=`, and Claude Code's native installer names the per-session executable by its version (.../share/claude/versions/ 2.1.220), so that basename identifies nothing. Three real failure shapes follow: a version-named session is missed entirely and the hook exits 0 with the epoch never written (unconditional on Linux, where procps reports the kernel exec name and ignores argv[0]); a claude-named daemon that directly parents sessions wins the outermost-contiguous-claude rule ahead of the session itself; and a session that is both version-named and daemon-parented has its live lock reclaimed as stale and rewritten to the shared daemon pid, corrupting the home's ownership record. Harness identity now also reads whole components of the executable path and of argv[0], which is what both platforms still carry. Matching whole components only keeps that widening safe: bin/fm-claude-stop-autoarm.sh and ~/.claude/hooks scripts have no "claude" component. Ownership is then decided against the session's whole contiguous harness ancestry rather than one chosen pid, which is the honest form of the question the library already documents ("does the current process descend from that same harness?"). That subsumes the outermost-pid rule for Claude's nested bg-spare worker chain instead of reverting it, and lets a daemon-parented session recognize its own lock. Lock acquisition still writes the outermost pid of the run, the only pid that lives as long as the session. Fault 2: an attached arm reports a delivered cycle as FAILED. The watcher prints its one reason line to its own stdout, so only the arm that forked it can read that line; an arm that attached observes nothing but a released lock and called a completely successful cycle "cycle ended without an actionable reason". No supervision event was lost - the durable queue held it - but every harness protocol reads that line as "supervision is down" and directs a manual re-arm. The arm now resolves an unobservable close against the durable wake queue, which records every wake before the watcher prints it and whose sequence counter never rewinds, not even across a drain. A cycle the queue proves delivered a wake reports that wake and exits 0; a cycle whose records a handling turn already drained reports the delivery without inventing a reason line; only a cycle that delivered nothing is still the typed nonzero failure. Fixing it in the arm covers codex, opencode, pi, grok and kimi, not just the Claude Stop path. Regressions: tests/fm-session-lock-ancestry.test.sh pins both platforms' ps semantics behind a deterministic process table and runs the real Stop auto-arm in version-named, daemon-parented, and combined real process trees, each orphaned so the walk cannot escape the fixture. tests/fm-watch-arm.test.sh drives a real watcher and a real attached arm through a real wake. Every fault case fails on the previous code. * no-mistakes(review): Bind watcher delivery records to process identity * no-mistakes(review): Return validated watcher identity atomically * no-mistakes(review): Track watcher successors by PID and identity * no-mistakes(document): Consolidate watcher arm-cycle documentation ownership * fix(bin): harden Claude supervision auto-arm recovery (#1495) * fix(supervision): harden Claude auto-arm failure handling * no-mistakes(review): Guarantee automatic retry after Claude auto-arm failures * no-mistakes(review): Gate attended fail-open on verified supervision failure * no-mistakes(document): Document Claude auto-arm retry and guard scope * no-mistakes: apply CI fixes * fix(supervision): make Claude fail-open progression monotonic * no-mistakes(review): Preserve auto-arm failure episodes until verified watcher recovery * no-mistakes(review): Linearize auto-arm failure progression across existing locks * no-mistakes(review): Linearize positive recovery across shared failure episode lock * no-mistakes(review): Scope Claude recovery contention to Claude guard mode * no-mistakes(document): Align supervision auto-arm documentation * no-mistakes(review): Preserve actionable wakes despite healthy successors * no-mistakes(document): Refresh supervision auto-arm documentation * feat(bin): require an explicit per-task delivery contract (#1563) * feat(bin): require an explicit ship delivery mode in fm-brief A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions. * feat(bin): require an explicit ship delivery contract at spawn and promotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode. * docs: record the explicit per-task delivery contract AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly. * test: pass ship delivery flags per call site in the Herdr launcher e2e The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper. * test: pass the ship delivery contract in the secondmate suites Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires. * feat(bin): support remote secondmate homes (#1576) * Add generic remote secondmate transport * Add routed remote secondmate replies * Add remote outbox backlog handoff * Integrate remote secondmate lifecycle * no-mistakes(review): Fix remote snapshot and handoff races * no-mistakes(review): Serialize remote home provisioning transactions * no-mistakes(review): Harden remote lifecycle transaction boundaries * no-mistakes(review): Serialize remote lifecycle mutations and fail closed * no-mistakes(review): Close remote lifecycle and file race windows * no-mistakes(review): Serialize remote reply retirement and inheritance * no-mistakes(review): Harden remote transfer integrity and recovery * no-mistakes(review): Serialize remote respawn with registry retirement * no-mistakes(document): Document remote bootstrap convergence accurately * no-mistakes(document): Clarify skipped remote secondmate mutations * no-mistakes(lint): Resolve remote script ShellCheck warnings * no-mistakes: apply CI fixes * feat: add per-task trace context propagation (#995) * feat(spawn): propagate a native W3C traceparent to spawned agents Add a default-off capability that resolves one W3C traceparent for a task, injects it into the agent's pane shell as the TRACEPARENT environment variable immediately before launch, and records the identical value as traceparent= in state/<id>.meta, so an external observer that explicitly reads that env value or meta field can correlate a worker, a Secondmate, and their nested children into one trace with no collector, storage, UI, or vendor coupling. TRACEPARENT as an environment variable is a firstmate convention carrying a W3C-formatted value: W3C Trace Context standardizes the header, not an env var, and OpenTelemetry SDKs do not read it automatically, so a downstream must consume it deliberately; this feature parents no SDK span by itself. Identity is per task, not per spawn: the carrier is minted with random ids on the first spawn, adopted as a child (fresh span, same trace) for a nested spawn whose parent already holds one, and reused verbatim from the meta on relaunch, so a task keeps one stable logical identity across restarts. A malformed or all-zero inherited value is treated as absent and roots a fresh trace. A new root is sampled (01) - a sampling decision a downstream parent-based sampler honors, not a guarantee that any collector stores a span, and firstmate emits no spans; a child preserves the inherited flag. Trust boundary: a firstmate-minted root is random and reads no prompt, path, task prose, credential, or arbitrary environment key. An inherited TRACEPARENT is opaque caller-controlled data - up to 24 bytes of id passed through after syntax validation - so whoever set it controls those bytes, a bounded fixed-width channel rather than a general content or secret channel. The feature adds no OTEL_* variable, no tracestate, and no arbitrary environment injection; it runs no configurable or arbitrary command, only the fixed local od and tr (resolved from PATH) to read a few bytes of entropy - a small local pipeline with no network or watchdog and no hard latency guarantee. Any entropy or validation failure that returns omits the carrier without aborting the spawn. A default-off spawn leaves the generated meta and launch environment unchanged. Enablement is default-off (config/trace-context, or FM_TRACE_CONTEXT where a non-empty value overrides and unset or empty defers to the file) and is propagated into secondmate homes, taking effect at each agent's next launch: a Secondmate launched or relaunched after enablement carries the primary trace into its nested workers, while an already-running Secondmate roots new traces for its own workers until relaunched. Injection reuses the existing GOTMPDIR channel, so all spawn backends and harnesses and the ship, scout, and secondmate paths are covered. Covered by a pure-library suite and a spawn-path integration test (fake tmux plus a real worktree, hermetic against ambient FM_TRACE_CONTEXT) proving the recorded and injected carriers are identical and sent before launch, that default-off writes and injects neither, that a relaunch reuses the recorded carrier, and that an explicit FM_TRACE_CONTEXT overrides the file both ways; plus a source-owner inheritance test proving trace-context propagates and absence-mirrors through propagate_inheritable_config. Documentation follows the repository documentation-audiences contract: docs/trace-context.md is maintainer-architecture rationale, the configuration schema lives in docs/configuration.md, and the repeatable test evidence is separated into docs/verification/trace-context.md (maintainer-verification), registered in docs/documentation-audiences.json. * fix(spawn): propagate the effective trace-context decision to secondmates FM_TRACE_CONTEXT overrode trace context only in the process that read it. A newly launched secondmate decided enablement from the inherited config/trace-context file alone, so the override did not cross the primary-to-secondmate boundary: FM_TRACE_CONTEXT=off with the file present left the secondmate's nested workers traced (a broken kill switch), and FM_TRACE_CONTEXT=on with the file absent left them untraced despite the inherited carrier. Deliver the primary's effective decision to a newly launched secondmate as a normalized on/off FM_TRACE_CONTEXT in the launch prefix, so a FM_TRACE_CONTEXT override governs the nested primary -> secondmate -> worker chain both ways, not just the copied file. The value is bounded to the literal on/off and does not broaden environment injection; the already-running secondmate boundary is unchanged. Add a genuine two-level spawn regression that drives fm-spawn twice with the exact environment the primary injects into the secondmate and proves both divergent directions end to end. Correct the documentation that implied secondmate coverage on every backend, since orca and cmux reject secondmate spawns, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Clarify Secondmate trace-context launch snapshots * no-mistakes(document): Correct trace-context documentation ownership and relaunch semantics * fix(spawn): resolve the trace-context decision once for carrier and snapshot The effective trace-context decision was read twice per spawn: once inside fm_trace_context_resolve for the recorded carrier, and again for the secondmate FM_TRACE_CONTEXT launch snapshot. A config-file change between the two reads could pair a carrier with the opposite enable state - an injected carrier with an off snapshot, or no carrier with an on snapshot. Freeze the effective on/off decision once, drive the carrier resolution under that frozen FM_TRACE_CONTEXT so it cannot independently re-read the file, and reuse the same frozen decision for the secondmate launch snapshot. Add a spawn-path regression that drives the file-decided path and proves the recorded carrier and the delivered snapshot always agree, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Preserve legacy Secondmate trace boundary * no-mistakes(document): Correct trace-context verification comparison base * no-mistakes(review): Captain, prevent failed trace delivery metadata claims * no-mistakes(review): Captain, align trace-context tests and verification evidence * no-mistakes(document): Correct trace-context verification evidence * no-mistakes(lint): Suppress intentional ShellCheck literal-dollar warnings * no-mistakes(review): Captain: freeze trace context at session start * no-mistakes(test): Captain: stabilize scheduler test and document Kimi trace coverage * no-mistakes(document): Document trace-context safety boundaries * fix(trace): fail off on stale session snapshots Publish each home session decision atomically through a same-directory temporary file and bind it to the current session lock. A replacement failure can no longer leave an earlier on decision active in a later session; missing, stale, malformed, or unpublishable state defaults safely to off. Add regressions for read-only replacement and failed publication, update spawn and session-start fixtures for the lock-bound format, and refresh the architecture and verification records. * no-mistakes(review): Fix trace spawn failure independence and duplicate safety * no-mistakes(document): Refresh trace-context documentation and verification * no-mistakes(review): Clear partial backend input after failed trace submission * no-mistakes(review): Stop unsafe trace delivery before launch append * no-mistakes(document): Document unsafe trace delivery handling * fix(trace): bound each trace to one routed task, never the routing agent A persistent Secondmate holds its launch-time TRACEPARENT in the process environment for its whole life, and routed requests never replace it, so resolving new-task carriers from the ambient environment chained every routed task into one ever-growing trace per Secondmate with distinct parent ids. Resolve now reuses the task's recorded carrier or mints a fresh sampled root, never reading ambient TRACEPARENT, so each routed task is its own trace boundary while relaunch, recovery, and scout-to-ship promotion keep one stable per-task identity. The spawn regression models the reviewed scenario exactly: two unrelated tasks spawned sequentially through one persistent Secondmate environment record and inject distinct trace ids, adopt nothing from the Secondmate's carrier, and a relaunch of the first task reuses its original carrier verbatim. * docs(trace): define the per-task trace boundary The design contract is one task per trace: a persistent Secondmate is routing infrastructure with its own agent identity, never a shared trace root for the unrelated tasks routed through it. Root/recovery semantics replace the removed child-inheritance path, the sampling and safety sections drop inherited-carrier language because ambient TRACEPARENT is never read, and the verification page records the refreshed suite inventories including the two-task Secondmate boundary regression. * test(trace): adopt the explicit per-task delivery contract in spawn fixtures Rebasing onto current main brings the explicit per-task delivery contract: ship spawns now require --mode and --yolo instead of resolving them from the project registry. The trace spawn fixtures pass the same explicit contract canonical spawn tests use, preserving the per-task trace boundary coverage unchanged, and the verification page records the refreshed comparison base. * fix(bin): harden tmux agent liveness across harnesses (#1577) * fix(bin): classify tmux agent liveness independent of process titles `fm_backend_tmux_agent_state` attributed a pane solely from `#{pane_current_command}`, which is a process TITLE a harness can rewrite, not a structural fact. Claude Code 2.1.220 reports its version string there, so a live Claude endpoint classified `ambiguous`: the session-start secondmate liveness sweep could no longer see it, and any consumer that gates on a positive classification refuses outright. Read a second, independent name source: the kernel `comm` of every process in the pane tty's foreground process group. Either source naming a verified harness yields `alive`, because a false `dead` is the one verdict that can start a duplicate agent on a live worktree. Scoping to the foreground process group rather than the pane's descendants keeps a harness-named background process from faking an agent, and covers multi-process launchers (the Pi Launcher path) without a special case. Verified on 2026-08-03 against all seven adapters running for real on tmux 3.6a / macOS 26.5.2 arm64: claude 2.1.220, codex-cli 0.146.0, opencode 1.18.11, pi 0.82.0, pi-signed 0.82.0, grok 0.2.118, kimi 0.31.1 all classify `alive`, each attributed by a source independent of its title. Two tests, because they fail for different reasons: - tests/fm-tmux-agent-liveness.test.sh pins the logic with real processes and no harness, so it runs everywhere CI runs tmux. It drives the two name sources apart on purpose and asserts the divergence, so no case can go quietly vacuous. - tests/fm-harness-liveness-drift-live-e2e.test.sh relaunches every installed harness and fails naming the harness and version when one stops being attributed by a title-independent source. AGENTS.md section 4 carries the resulting standing rule, and firstmate-coding-guidelines owns how to satisfy it. * no-mistakes: apply CI fixes * docs: move the harness-dependent-check policy out of AGENTS.md The standing rule was stated in AGENTS.md section 4 with the mechanics in firstmate-coding-guidelines, which split one contract across two owners and charged every session for a rule that only fires when firstmate's own harness-dependent code is being changed. firstmate-coding-guidelines is now the single owner of both the rule and how to satisfy it: real-harness proof required, that proof authorized to spend tokens, structural signals preferred over vendor-rendered surfaces, and a guard that fails loudly naming the harness and version where a surface signal is unavoidable. No inline stub is left behind, because AGENTS.md already carries the load trigger for that skill in sections 7 and 13, so it is read before any change to firstmate's shared tracked material. Also records the cross-platform lesson the pipeline caught in the portable regression, and corrects that file's header: the divergence assertion lives on the version-string case, which diverges on both supported platforms, rather than on every case. * no-mistakes(review): Harden tmux liveness identity and drift validation * no-mistakes(document): Clarify cross-platform tmux liveness documentation * feat(bin): propagate trace context to remote secondmates (#1609) * feat(bin): trace remote secondmate routes and unify the inherit allowlist Per-task W3C trace context (#995) resolved and injected its carrier only at the local spawn path. A remote secondmate is routed through spawn_remote_secondmate, which returns long before that site and wrote its own metadata block, so a remote secondmate stayed silently untraced even with the capability enabled. The parent home still owns that task's identity, because it holds the metadata an observer reads. It now resolves the carrier against the task's own meta under its own frozen decision - reused verbatim on relaunch, freshly rooted otherwise, never adopting the parent process's ambient TRACEPARENT - and hands it to the configured host through a new fm-spawn --traceparent argument, accepted only for a secondmate launch and only as a strict W3C value. The remote host exports it at the same unconditional pre-launch site and reports back the carrier its endpoint actually holds, which the parent records, so an already-alive endpoint reports the identity its agent really received rather than one the parent merely intended. Disabled remains byte-identical and off. The remote inherit path also carried its own hardcoded copy of the inheritable config set, already drifted from FM_INHERITABLE_CONFIG by trace-context. Both remote ends now derive from that one declaration, so a future item cannot be sent by one side and refused by the other, and session-scoped enablement items are skipped on live convergence exactly as the local path skips them. Also fixes a latent stderr leak: an absent session lock printed a raw redirect failure, which the new remote resolve site made visible. Adds tests/fm-remote-secondmate-trace-context.test.sh, driving the real parent -> fm-on -> remote entrypoint -> control -> remote fm-spawn chain over the deterministic SSH boundary and reading the carrier back from the remote pane's own log. * no-mistakes(document): Clarify remote trace and allowlist contracts * feat(bin): preflight remote runtime tool paths (#1623) * feat(bin): widen the remote runtime PATH and add a remote doctor preflight The fixed remote entrypoint hard-coded a four-directory PATH, so a remote account whose tools live under nix or a per-user profile could not run basic Firstmate work without a login shell. The entrypoint now composes its child PATH from the code root's bin, the account's ~/.local/bin, the common package-manager directories that actually exist on the host, and the portable system tail, deduplicated and in a fixed order, still under env -i with the same variable allowlist and no shell command string. fm-remote-doctor.sh reports that exact PATH by inheriting it from its own entrypoint launch rather than recomposing it, so the ordering keeps one owner. It is read-only, reports where each required and optional tool resolved, and exits non-zero naming every required tool that did not. Remote seeding runs it as a preflight before anything is created on the host and restores the registry when it fails. * no-mistakes(review): Harden remote git authorization and missing-tool diagnostics * no-mistakes(document): Document remote PATH doctor and safe shims * no-mistakes(lint): Fix ShellCheck findings in remote path tests * no-mistakes(lint): Suppress exported fixture's false-positive ShellCheck warning * feat: gate remote second mates on Herdr readiness (#1639) * feat(bin): gate remote second mates on herdr readiness A remote second mate now always runs on the Herdr backend, whose server belongs to the host's GUI login session and therefore outlives the SSH connections that supervise it. fm-spawn's remote route forces that backend and the host-local control script refuses any other, so the requirement cannot be dropped from either side. fm-remote-doctor.sh becomes the single owner of what "ready" means. It keeps its PATH and tool reporting from #1623 and adds the Herdr, Aqua LaunchAgent, GUI-session, server-reachability, and entrypoint-symlink checks, tagging each gap fixable: or human: with the exact operator step. --fix closes only the automatable gaps - writing and loading the Aqua-scoped dev.firstmate.herdr launch agent, starting the server where no launch agent applies, and recreating the entrypoint symlink - then re-derives every check from the host, so a human gap is never presented as fixed. It never creates a login session, writes an auto-login password, or touches FileVault. Remote seed, remote spawn, and the startup liveness relaunch all run the same check, repair, re-check sequence through one shared library and fail closed with the doctor's own gap text. Recovery inherits the gate because it respawns through the same route. Tests drive the real doctor against a controlled account fixture with a private HOME, a state-backed launchctl, and a fake herdr, and prove the dangerous actions are never attempted. The remote lifecycle suites gain a stateful Herdr CLI fixture and answer the readiness gate at the SSH boundary, so they never inspect or repair the runner's own account. * no-mistakes(review): Validate launch-agent contract and confirm Herdr startup * no-mistakes(review): Validate loaded launch-agent contract before readiness * no-mistakes(review): Refuse legacy remote backends without altering routes * no-mistakes(review): Clarify conditional remote readiness repair sequence * no-mistakes(review): Repair remote readiness before liveness probing * no-mistakes(review): Preserve unknown seeds and reject legacy liveness * no-mistakes(document): docs: clarify remote Herdr backend ownership * fix: isolate remote secondmates in shared Herdr session (#1659) * Pin remote secondmates to fm-remote * no-mistakes(review): Fail closed on legacy remote Herdr endpoints * no-mistakes(review): Isolate fm-remote launch agent from interactive default * no-mistakes(document): Document shared remote Herdr retirement safety * feat: route remote commands through an Aqua job worker (#1660) * feat: run remote commands through Aqua job worker * no-mistakes(review): Enforce remote job deadlines and safe worker shutdown * no-mistakes(review): Refresh stale workers and harden dependency-free supervision * no-mistakes(review): Harden worker ownership recovery and shutdown quarantine * no-mistakes(review): Fix doctor bootstrap, harness repair, and output draining * no-mistakes(review): Probe doctor tools through authenticated worker bootstrap * no-mistakes(review): Refresh stale workers before doctor tool probes * no-mistakes(review): Recover stopped quarantines and extend job deadlines * no-mistakes(review): Separate queue and execution timeout windows * no-mistakes(review): Supervise Linux worker crashes and bind root identity * no-mistakes(review): Resolve authorized Nix profile bin links * no-mistakes(review): Clarify Nix path resolution documentation * no-mistakes(review): Harden PATH safety and nvm selection * no-mistakes(review): Honor nvm system defaults and refresh doctor digest * no-mistakes(review): Keep workers ready during active jobs * no-mistakes(review): Bound pre-execution validation by job timeout * no-mistakes(document): Clarify remote worker documentation * no-mistakes(lint): Fix remote worker ShellCheck diagnostics * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * fix: clarify remote doctor bootstrap path (#1691) * fix(bin): bound remote SSH dead-peer detection (#1699) * fix(remote): arm SSH dead-peer detection in fm-on.sh A vanished remote host mid-poll (a reboot, a dropped link) left ssh blocked indefinitely on a half-open TCP connection, because fm-on.sh's ssh invocation had no ServerAliveInterval/ServerAliveCountMax. This wedged the remote-reply ferry: fm-procevent.sh's runner blocked inside the ssh child and never reached its own no-result -> claim-release -> reconcile re-arm self-healing path, which otherwise already handles a nonzero exit with empty output correctly. Recovery required a manual retire and re-arm. Arm ServerAliveInterval=15 and ServerAliveCountMax=3 by default (bounded ~45s detection window), both overridable via FM_SSH_ALIVE_INTERVAL and FM_SSH_ALIVE_COUNT_MAX. This is a transport- level fix in fm-on.sh, so it covers every remote command routed through it, not just the reply ferry. The remote sshd answers keepalive probes independently of whatever the remote command is doing, so a legitimately long-but-alive command (a 55s poll, a clone, the doctor) is never falsely killed - only a truly vanished peer trips it, turning that case into a bounded, detectable ssh failure (exit 255) instead of an indefinite hang. Extends tests/fm-on.test.sh with a behavioral regression asserting a bounded, positive ServerAliveInterval/ServerAliveCountMax on the real ssh argv captured through the FM_SSH_BIN process seam, plus coverage that both are env-overridable. * no-mistakes(document): Document SSH dead-peer detection ownership * fix: report stale AXI tools during bootstrap (#1701) * feat(bootstrap): gate stale axi CLIs at the floors firstmate actually uses Add gh-axi 0.1.29 floor so bare --squash PR merges stop failing quietly on older builds. Raise tasks-axi to FM_TASKS_AXI_MIN=0.2.2 (multi-id mv) while keeping feature probes. Keep quota-axi at 0.1.16 after verifying schema 3 and per-model availability already ship there; runway remains optional. * no-mistakes(document): Clarify AXI compatibility documentation ownership * fix: prevent false watcher-down alarms in Claude sessions (#1661) * fix(guard): stop false watcher-down alarm mid-turn under Claude auto-arm bin/fm-guard.sh derived its watcher-health verdict from fm_watcher_healthy, which requires a live watcher process holding the home lock. Under the Claude Stop-hook auto-arm supervision model the watcher is armed at each turn end and exits on its wake, so it runs only between turns. Every guarded command run mid-turn therefore found no live watcher and printed the "WATCHER DOWN - SUPERVISION IS OFF" banner even though supervision was healthy. Because the episode key was derived from the beacon mtime (which the between-turns watcher advances every poll), the full banner re-printed on essentially every command, and the message always blamed a "fresh beacon" that was in fact fresh. Make the pull guard's health check model-aware via a new fm_watcher_supervision_verdict in bin/fm-wake-lib.sh: - Under the auto-arm model a beacon fresh within FM_GUARD_GRACE is healthy even with no live watcher process; only a beacon stale beyond grace (or absent) is a genuine lapse and alarms. - Under every persistent-watcher harness (codex foreground checkpoint, opencode/pi/grok background arm, tmux, unknown) a live identity-matched watcher with a fresh beacon is still required, unchanged. The banner now names the true failing condition, a missing live watcher process versus a genuinely stale beacon, instead of always blaming the beacon, and the once-per-episode dedup keys on that condition rather than the beacon mtime so a genuine lapse announces once and does not re-print each turn. The turn-end guard keeps the strict fm_watcher_healthy check because it fires at the turn boundary, where the auto-arm brings a fresh watcher up and it cooperates with that arm. fm_watcher_healthy itself is unchanged, so the arm layer's start/attach/replace decisions are unaffected. Tests in tests/fm-guard-stale-banner.test.sh cover the auto-arm healthy fresh-beacon-without-a-watcher case, the auto-arm stale-beacon alarm and its stable episode, the true-reason banner wording, and the reason-keyed episode surviving a beacon mtime change; existing persistent-model cases are pinned to that model. * no-mistakes(review): Pin secondmate supervision model to launched harness * no-mistakes(document): Align watcher documentation with model-aware supervision health * test: prevent fixture temporary directory leaks (#1704) * fix(tests): stop fixture-tempdir helper from self-deleting under command substitution fm_test_tmproot is almost always called as `TMP_ROOT=$(fm_test_tmproot prefix)`, which forks a subshell to capture its stdout. The old implementation set its EXIT cleanup trap inside that call, so the trap fired - and deleted the fixture root - the instant the subshell exited, before the real caller's own EXIT trap was ever installed. Every test using the documented call pattern leaked its fixture root on every run; two suites had already independently discovered and worked around this with ad-hoc mktemp calls. Registration now goes through a $$-keyed registry file instead of in-process state, since $$ resolves to the invoking shell's PID even inside the subshell. The real cleanup trap is armed once at source time (always the real caller, never a subshell) for EXIT, INT, and TERM. A best-effort orphan sweep on next source reaps marked fixture roots old enough to be from a killed prior run. Simplifies the two existing ad-hoc workarounds (fm-procevent.test.sh, wake-helpers.sh) back onto the shared helper now that it works correctly. * no-mistakes(review): Preserve live fixtures during orphan reaping * no-mistakes(review): Harden fixture ownership against PID reuse * no-mistakes(review): Secure cleanup registry against path precreation * no-mistakes(review): Make fixture registration transactional * no-mistakes(document): Documentation already matches fixture cleanup behavior * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * feat(herdr): enable presentation spaces by default (#1708) * feat(herdr): default presentation spaces on with an explicit opt-out Herdr's disposable one-task presentation workspace was opt-in through the presence of local config/herdr-presentation-spaces. It is now on by default, and a home opts out by writing "off" into that same file. Values are read with the whole-file whitespace-stripped convention the other scalar config items already use, plus case folding. An absent file, an empty file, and "on" all resolve on; only "off" opts out; an unrecognized value warns and keeps the default rather than failing a spawn over a purely visual setting. The empty file is exactly the historical opt-in form, so every home that had already enabled the projection stays enabled with no migration step, and no previously enabled home can be turned off by the flip. Because absence now means on at both ends, secondmate inheritance needs no item-specific convergence: mirroring an absent primary file converges a secondmate to the same default-on rather than turning its projection off, and only an explicit primary opt-out propagates the opt-out. The gate itself moves into fm_backend_herdr_presentation_enabled in the Herdr adapter so the semantics have one owner that regressions can exercise directly. * no-mistakes(document): Document Herdr default-on presentation safety --------- Co-authored-by: kunchenguid <kun-1@kunchenguid.com> * fix(bin): surface fleet-wide open decisions on every wake drain (#1711) * fix: surface consolidated open decisions on every wake-drain A needs-decision or blocked event buried under later, unrelated status appends was only ever shown via the last-line wake annotation, so a still-open captain decision could go silently missed even though status_open_decisions (fm-classify-lib.sh) already folds the whole status stream correctly and fleet-snapshot/bearings already reuse it. Wire that same fold into bin/fm-wake-drain.sh: a new fleet-wide scan_open_decisions wrapper scans every state/<id>.status, and fm-wake-drain.sh prints a separate, bounded OPEN DECISIONS section on every drain (including the empty-queue fast path), so session-start and every wake-handling turn surface it for free without duplicating the open/resolved fold itself. Heartbeat wakes drain through the same script, so this covers that surface too. Also tighten status_open_decisions' file guard to skip an unreadable status file instead of leaking a bash redirection error, now that a fleet-wide directory scan can reach files a single targeted read would not. * no-mistakes(review): Prevent status symlinks leaking open decisions * fix: drop unbounded perl subprocess from status symlink guard The review step's own symlink-safety auto-fix (O_NOFOLLOW read via a perl subprocess) forked one perl process per status file scanned by the new fleet-wide open-decisions scan, with no cap - inflating fm-wake-drain.sh's total external-read cost from 8 (the existing annotation read_cap) to 18 in the enrichment-caps regression test. The plain [ -L "$f" ] check already rejects any status file that is itself a symlink before any read happens, which is exactly what the new regression test exercises and is the same defense level the sibling scan_captain_relevant_statuses/last_status_line already rely on elsewhere in this file (no O_NOFOLLOW). Drop the subprocess-based nofollow read and keep the cheap builtin guard. * no-mistakes(document): Document actionable fleet-wide open decision drains * fix(bin): abort parked runs and reap leaked processes before teardown (#1710) * fix(bin): abort orphaned no-mistakes runs and reap leaked processes at teardown Teardown could remove a task's worker while its no-mistakes pipeline run was still parked at a gate, leaving an orphaned run holding a fleet slot indefinitely (observed 2026-08-03: runs parked 7h39m and parked at a post-CI approval gate). It could also leave backgrounded/disowned descendant processes rooted under the worktree or tasktmp surviving reparented to init (observed: two `go test` binaries pinning CPU for hours with no live task meta to attribute them to). Add two coupled pre-teardown steps, both scoped to this task's exact branch/head or worktree/tasktmp so they can never touch another task's run or processes: - conclude_task_no_mistakes_run aborts a run parked at a gate via `no-mistakes axi abort`, cd'd into the exact worktree so the daemon resolves the run itself rather than teardown naming a --run id. - reap_task_worktree_processes sweeps for processes whose cwd is under the worktree or tasktmp (via `lsof -a -d cwd`) and TERM/KILLs them. Both run before any worktree return, branch delete, or backend kill, and are idempotent on a retried teardown. The branch+head attribution logic is factored out of bin/fm-crew-state.sh into the new shared bin/fm-nm-run-lib.sh so both scripts use the same ownership contract. * no-mistakes(review): Fail closed on incomplete teardown cleanup * no-mistakes(review): Bind teardown cleanup to verified run and process identities * no-mistakes(review): Require confirmed aborts and convergent identity-safe process reaping * no-mistakes(review): Handle process exits during teardown identity checks * no-mistakes(review): Restore teardown library in hermetic gotmp fixtures * no-mistakes(document): Document teardown run attribution and timeout * no-mistakes(lint): Rename shell variable conflicting with done keyword * no-mistakes: apply CI fixes * fix: resolve fm-remote-entrypoint.sh SCRIPT_DIR through a PATH symlink (#1709) The script installs as a symlink under ~/.local/bin. Taking dirname of the symlink itself (instead of its real target) pointed SCRIPT_DIR at ~/.local/bin, breaking sourcing of the sibling fm-remote-job-lib.sh. Resolve the real path first, preferring python3's os.path.realpath, then realpath, falling back to the raw BASH_SOURCE on hosts with neither. * fix(pi): gate Calm built-in overrides by activation state (#1724) * fix(pi): stop Calm claiming a built-in tool name another extension owns fm-calm.ts claimed bash/read/edit/write/grep/find/ls unconditionally at extension load, regardless of whether Calm was on. Pi resolves two extensions registering the same built-in name by first-registered-wins with no merge and no unregister call, and Calm's project-local .pi/extensions/ position beats any global or CLI-configured extension, so a user who never even enabled Calm could have their own bash/read/etc override silently replaced. Captain-approved plan implemented: - Registration is now gated on config/calm already being "on" at load time. A Calm-off session or reload registers nothing, so a non-Calm user never contests a name. This stays synchronous during the factory's own load, not deferred to session_start: /reload (and ctx.newSession/fork/switchSession) render the restored transcript from a pre-session_start snapshot of the tool registry, so a deferred claim would miss that render - confirmed by tests/fm-calm-pi-extension .test.sh's hidden-block-geometry E2E when trialed. - The first time Calm turns on in a session that started off (activateBuiltInsIfNeeded, from the /calm command handler), Calm calls pi.getAllTools() - safe only once every extension has finished loading, unlike the load-time path above - to see whether a different extension already owns a name, and skips claiming only that one, leaving it and its owning extension fully intact and callable. - A contested name found this way prints a prominent ctx.ui.notify() warning naming the tool, plus a console diagnostic. - reportBuiltInLosses() remains the backstop for the one case neither of the above can reach: a session that starts or reloads with Calm already on, where the registry snapshot is taken before Calm gets any chance to check ownership. A symlink-safe realpath comparison avoids misreporting Calm's own registration as foreign when its path crosses a symlink (macOS /tmp, /var). Confirmed, bounded trade-off: the very first time a session that started Calm-off turns Calm on, tool-call rows already on screen from before that toggle do not retr…
sbracewell64
added a commit
to sbracewell64/firstmate
that referenced
this pull request
Aug 11, 2026
…ks (#39) * feat(bin): require an explicit per-task delivery contract (kunchenguid#1563) * feat(bin): require an explicit ship delivery mode in fm-brief A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions. * feat(bin): require an explicit ship delivery contract at spawn and promotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode. * docs: record the explicit per-task delivery contract AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly. * test: pass ship delivery flags per call site in the Herdr launcher e2e The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper. * test: pass the ship delivery contract in the secondmate suites Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires. * feat(bin): support remote secondmate homes (kunchenguid#1576) * Add generic remote secondmate transport * Add routed remote secondmate replies * Add remote outbox backlog handoff * Integrate remote secondmate lifecycle * no-mistakes(review): Fix remote snapshot and handoff races * no-mistakes(review): Serialize remote home provisioning transactions * no-mistakes(review): Harden remote lifecycle transaction boundaries * no-mistakes(review): Serialize remote lifecycle mutations and fail closed * no-mistakes(review): Close remote lifecycle and file race windows * no-mistakes(review): Serialize remote reply retirement and inheritance * no-mistakes(review): Harden remote transfer integrity and recovery * no-mistakes(review): Serialize remote respawn with registry retirement * no-mistakes(document): Document remote bootstrap convergence accurately * no-mistakes(document): Clarify skipped remote secondmate mutations * no-mistakes(lint): Resolve remote script ShellCheck warnings * no-mistakes: apply CI fixes * feat: add per-task trace context propagation (kunchenguid#995) * feat(spawn): propagate a native W3C traceparent to spawned agents Add a default-off capability that resolves one W3C traceparent for a task, injects it into the agent's pane shell as the TRACEPARENT environment variable immediately before launch, and records the identical value as traceparent= in state/<id>.meta, so an external observer that explicitly reads that env value or meta field can correlate a worker, a Secondmate, and their nested children into one trace with no collector, storage, UI, or vendor coupling. TRACEPARENT as an environment variable is a firstmate convention carrying a W3C-formatted value: W3C Trace Context standardizes the header, not an env var, and OpenTelemetry SDKs do not read it automatically, so a downstream must consume it deliberately; this feature parents no SDK span by itself. Identity is per task, not per spawn: the carrier is minted with random ids on the first spawn, adopted as a child (fresh span, same trace) for a nested spawn whose parent already holds one, and reused verbatim from the meta on relaunch, so a task keeps one stable logical identity across restarts. A malformed or all-zero inherited value is treated as absent and roots a fresh trace. A new root is sampled (01) - a sampling decision a downstream parent-based sampler honors, not a guarantee that any collector stores a span, and firstmate emits no spans; a child preserves the inherited flag. Trust boundary: a firstmate-minted root is random and reads no prompt, path, task prose, credential, or arbitrary environment key. An inherited TRACEPARENT is opaque caller-controlled data - up to 24 bytes of id passed through after syntax validation - so whoever set it controls those bytes, a bounded fixed-width channel rather than a general content or secret channel. The feature adds no OTEL_* variable, no tracestate, and no arbitrary environment injection; it runs no configurable or arbitrary command, only the fixed local od and tr (resolved from PATH) to read a few bytes of entropy - a small local pipeline with no network or watchdog and no hard latency guarantee. Any entropy or validation failure that returns omits the carrier without aborting the spawn. A default-off spawn leaves the generated meta and launch environment unchanged. Enablement is default-off (config/trace-context, or FM_TRACE_CONTEXT where a non-empty value overrides and unset or empty defers to the file) and is propagated into secondmate homes, taking effect at each agent's next launch: a Secondmate launched or relaunched after enablement carries the primary trace into its nested workers, while an already-running Secondmate roots new traces for its own workers until relaunched. Injection reuses the existing GOTMPDIR channel, so all spawn backends and harnesses and the ship, scout, and secondmate paths are covered. Covered by a pure-library suite and a spawn-path integration test (fake tmux plus a real worktree, hermetic against ambient FM_TRACE_CONTEXT) proving the recorded and injected carriers are identical and sent before launch, that default-off writes and injects neither, that a relaunch reuses the recorded carrier, and that an explicit FM_TRACE_CONTEXT overrides the file both ways; plus a source-owner inheritance test proving trace-context propagates and absence-mirrors through propagate_inheritable_config. Documentation follows the repository documentation-audiences contract: docs/trace-context.md is maintainer-architecture rationale, the configuration schema lives in docs/configuration.md, and the repeatable test evidence is separated into docs/verification/trace-context.md (maintainer-verification), registered in docs/documentation-audiences.json. * fix(spawn): propagate the effective trace-context decision to secondmates FM_TRACE_CONTEXT overrode trace context only in the process that read it. A newly launched secondmate decided enablement from the inherited config/trace-context file alone, so the override did not cross the primary-to-secondmate boundary: FM_TRACE_CONTEXT=off with the file present left the secondmate's nested workers traced (a broken kill switch), and FM_TRACE_CONTEXT=on with the file absent left them untraced despite the inherited carrier. Deliver the primary's effective decision to a newly launched secondmate as a normalized on/off FM_TRACE_CONTEXT in the launch prefix, so a FM_TRACE_CONTEXT override governs the nested primary -> secondmate -> worker chain both ways, not just the copied file. The value is bounded to the literal on/off and does not broaden environment injection; the already-running secondmate boundary is unchanged. Add a genuine two-level spawn regression that drives fm-spawn twice with the exact environment the primary injects into the secondmate and proves both divergent directions end to end. Correct the documentation that implied secondmate coverage on every backend, since orca and cmux reject secondmate spawns, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Clarify Secondmate trace-context launch snapshots * no-mistakes(document): Correct trace-context documentation ownership and relaunch semantics * fix(spawn): resolve the trace-context decision once for carrier and snapshot The effective trace-context decision was read twice per spawn: once inside fm_trace_context_resolve for the recorded carrier, and again for the secondmate FM_TRACE_CONTEXT launch snapshot. A config-file change between the two reads could pair a carrier with the opposite enable state - an injected carrier with an off snapshot, or no carrier with an on snapshot. Freeze the effective on/off decision once, drive the carrier resolution under that frozen FM_TRACE_CONTEXT so it cannot independently re-read the file, and reuse the same frozen decision for the secondmate launch snapshot. Add a spawn-path regression that drives the file-decided path and proves the recorded carrier and the delivered snapshot always agree, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Preserve legacy Secondmate trace boundary * no-mistakes(document): Correct trace-context verification comparison base * no-mistakes(review): Captain, prevent failed trace delivery metadata claims * no-mistakes(review): Captain, align trace-context tests and verification evidence * no-mistakes(document): Correct trace-context verification evidence * no-mistakes(lint): Suppress intentional ShellCheck literal-dollar warnings * no-mistakes(review): Captain: freeze trace context at session start * no-mistakes(test): Captain: stabilize scheduler test and document Kimi trace coverage * no-mistakes(document): Document trace-context safety boundaries * fix(trace): fail off on stale session snapshots Publish each home session decision atomically through a same-directory temporary file and bind it to the current session lock. A replacement failure can no longer leave an earlier on decision active in a later session; missing, stale, malformed, or unpublishable state defaults safely to off. Add regressions for read-only replacement and failed publication, update spawn and session-start fixtures for the lock-bound format, and refresh the architecture and verification records. * no-mistakes(review): Fix trace spawn failure independence and duplicate safety * no-mistakes(document): Refresh trace-context documentation and verification * no-mistakes(review): Clear partial backend input after failed trace submission * no-mistakes(review): Stop unsafe trace delivery before launch append * no-mistakes(document): Document unsafe trace delivery handling * fix(trace): bound each trace to one routed task, never the routing agent A persistent Secondmate holds its launch-time TRACEPARENT in the process environment for its whole life, and routed requests never replace it, so resolving new-task carriers from the ambient environment chained every routed task into one ever-growing trace per Secondmate with distinct parent ids. Resolve now reuses the task's recorded carrier or mints a fresh sampled root, never reading ambient TRACEPARENT, so each routed task is its own trace boundary while relaunch, recovery, and scout-to-ship promotion keep one stable per-task identity. The spawn regression models the reviewed scenario exactly: two unrelated tasks spawned sequentially through one persistent Secondmate environment record and inject distinct trace ids, adopt nothing from the Secondmate's carrier, and a relaunch of the first task reuses its original carrier verbatim. * docs(trace): define the per-task trace boundary The design contract is one task per trace: a persistent Secondmate is routing infrastructure with its own agent identity, never a shared trace root for the unrelated tasks routed through it. Root/recovery semantics replace the removed child-inheritance path, the sampling and safety sections drop inherited-carrier language because ambient TRACEPARENT is never read, and the verification page records the refreshed suite inventories including the two-task Secondmate boundary regression. * test(trace): adopt the explicit per-task delivery contract in spawn fixtures Rebasing onto current main brings the explicit per-task delivery contract: ship spawns now require --mode and --yolo instead of resolving them from the project registry. The trace spawn fixtures pass the same explicit contract canonical spawn tests use, preserving the per-task trace boundary coverage unchanged, and the verification page records the refreshed comparison base. * fix(bin): harden tmux agent liveness across harnesses (kunchenguid#1577) * fix(bin): classify tmux agent liveness independent of process titles `fm_backend_tmux_agent_state` attributed a pane solely from `#{pane_current_command}`, which is a process TITLE a harness can rewrite, not a structural fact. Claude Code 2.1.220 reports its version string there, so a live Claude endpoint classified `ambiguous`: the session-start secondmate liveness sweep could no longer see it, and any consumer that gates on a positive classification refuses outright. Read a second, independent name source: the kernel `comm` of every process in the pane tty's foreground process group. Either source naming a verified harness yields `alive`, because a false `dead` is the one verdict that can start a duplicate agent on a live worktree. Scoping to the foreground process group rather than the pane's descendants keeps a harness-named background process from faking an agent, and covers multi-process launchers (the Pi Launcher path) without a special case. Verified on 2026-08-03 against all seven adapters running for real on tmux 3.6a / macOS 26.5.2 arm64: claude 2.1.220, codex-cli 0.146.0, opencode 1.18.11, pi 0.82.0, pi-signed 0.82.0, grok 0.2.118, kimi 0.31.1 all classify `alive`, each attributed by a source independent of its title. Two tests, because they fail for different reasons: - tests/fm-tmux-agent-liveness.test.sh pins the logic with real processes and no harness, so it runs everywhere CI runs tmux. It drives the two name sources apart on purpose and asserts the divergence, so no case can go quietly vacuous. - tests/fm-harness-liveness-drift-live-e2e.test.sh relaunches every installed harness and fails naming the harness and version when one stops being attributed by a title-independent source. AGENTS.md section 4 carries the resulting standing rule, and firstmate-coding-guidelines owns how to satisfy it. * no-mistakes: apply CI fixes * docs: move the harness-dependent-check policy out of AGENTS.md The standing rule was stated in AGENTS.md section 4 with the mechanics in firstmate-coding-guidelines, which split one contract across two owners and charged every session for a rule that only fires when firstmate's own harness-dependent code is being changed. firstmate-coding-guidelines is now the single owner of both the rule and how to satisfy it: real-harness proof required, that proof authorized to spend tokens, structural signals preferred over vendor-rendered surfaces, and a guard that fails loudly naming the harness and version where a surface signal is unavoidable. No inline stub is left behind, because AGENTS.md already carries the load trigger for that skill in sections 7 and 13, so it is read before any change to firstmate's shared tracked material. Also records the cross-platform lesson the pipeline caught in the portable regression, and corrects that file's header: the divergence assertion lives on the version-string case, which diverges on both supported platforms, rather than on every case. * no-mistakes(review): Harden tmux liveness identity and drift validation * no-mistakes(document): Clarify cross-platform tmux liveness documentation * feat(bin): propagate trace context to remote secondmates (kunchenguid#1609) * feat(bin): trace remote secondmate routes and unify the inherit allowlist Per-task W3C trace context (kunchenguid#995) resolved and injected its carrier only at the local spawn path. A remote secondmate is routed through spawn_remote_secondmate, which returns long before that site and wrote its own metadata block, so a remote secondmate stayed silently untraced even with the capability enabled. The parent home still owns that task's identity, because it holds the metadata an observer reads. It now resolves the carrier against the task's own meta under its own frozen decision - reused verbatim on relaunch, freshly rooted otherwise, never adopting the parent process's ambient TRACEPARENT - and hands it to the configured host through a new fm-spawn --traceparent argument, accepted only for a secondmate launch and only as a strict W3C value. The remote host exports it at the same unconditional pre-launch site and reports back the carrier its endpoint actually holds, which the parent records, so an already-alive endpoint reports the identity its agent really received rather than one the parent merely intended. Disabled remains byte-identical and off. The remote inherit path also carried its own hardcoded copy of the inheritable config set, already drifted from FM_INHERITABLE_CONFIG by trace-context. Both remote ends now derive from that one declaration, so a future item cannot be sent by one side and refused by the other, and session-scoped enablement items are skipped on live convergence exactly as the local path skips them. Also fixes a latent stderr leak: an absent session lock printed a raw redirect failure, which the new remote resolve site made visible. Adds tests/fm-remote-secondmate-trace-context.test.sh, driving the real parent -> fm-on -> remote entrypoint -> control -> remote fm-spawn chain over the deterministic SSH boundary and reading the carrier back from the remote pane's own log. * no-mistakes(document): Clarify remote trace and allowlist contracts * feat(bin): preflight remote runtime tool paths (kunchenguid#1623) * feat(bin): widen the remote runtime PATH and add a remote doctor preflight The fixed remote entrypoint hard-coded a four-directory PATH, so a remote account whose tools live under nix or a per-user profile could not run basic Firstmate work without a login shell. The entrypoint now composes its child PATH from the code root's bin, the account's ~/.local/bin, the common package-manager directories that actually exist on the host, and the portable system tail, deduplicated and in a fixed order, still under env -i with the same variable allowlist and no shell command string. fm-remote-doctor.sh reports that exact PATH by inheriting it from its own entrypoint launch rather than recomposing it, so the ordering keeps one owner. It is read-only, reports where each required and optional tool resolved, and exits non-zero naming every required tool that did not. Remote seeding runs it as a preflight before anything is created on the host and restores the registry when it fails. * no-mistakes(review): Harden remote git authorization and missing-tool diagnostics * no-mistakes(document): Document remote PATH doctor and safe shims * no-mistakes(lint): Fix ShellCheck findings in remote path tests * no-mistakes(lint): Suppress exported fixture's false-positive ShellCheck warning * feat: gate remote second mates on Herdr readiness (kunchenguid#1639) * feat(bin): gate remote second mates on herdr readiness A remote second mate now always runs on the Herdr backend, whose server belongs to the host's GUI login session and therefore outlives the SSH connections that supervise it. fm-spawn's remote route forces that backend and the host-local control script refuses any other, so the requirement cannot be dropped from either side. fm-remote-doctor.sh becomes the single owner of what "ready" means. It keeps its PATH and tool reporting from kunchenguid#1623 and adds the Herdr, Aqua LaunchAgent, GUI-session, server-reachability, and entrypoint-symlink checks, tagging each gap fixable: or human: with the exact operator step. --fix closes only the automatable gaps - writing and loading the Aqua-scoped dev.firstmate.herdr launch agent, starting the server where no launch agent applies, and recreating the entrypoint symlink - then re-derives every check from the host, so a human gap is never presented as fixed. It never creates a login session, writes an auto-login password, or touches FileVault. Remote seed, remote spawn, and the startup liveness relaunch all run the same check, repair, re-check sequence through one shared library and fail closed with the doctor's own gap text. Recovery inherits the gate because it respawns through the same route. Tests drive the real doctor against a controlled account fixture with a private HOME, a state-backed launchctl, and a fake herdr, and prove the dangerous actions are never attempted. The remote lifecycle suites gain a stateful Herdr CLI fixture and answer the readiness gate at the SSH boundary, so they never inspect or repair the runner's own account. * no-mistakes(review): Validate launch-agent contract and confirm Herdr startup * no-mistakes(review): Validate loaded launch-agent contract before readiness * no-mistakes(review): Refuse legacy remote backends without altering routes * no-mistakes(review): Clarify conditional remote readiness repair sequence * no-mistakes(review): Repair remote readiness before liveness probing * no-mistakes(review): Preserve unknown seeds and reject legacy liveness * no-mistakes(document): docs: clarify remote Herdr backend ownership * fix: isolate remote secondmates in shared Herdr session (kunchenguid#1659) * Pin remote secondmates to fm-remote * no-mistakes(review): Fail closed on legacy remote Herdr endpoints * no-mistakes(review): Isolate fm-remote launch agent from interactive default * no-mistakes(document): Document shared remote Herdr retirement safety * feat: route remote commands through an Aqua job worker (kunchenguid#1660) * feat: run remote commands through Aqua job worker * no-mistakes(review): Enforce remote job deadlines and safe worker shutdown * no-mistakes(review): Refresh stale workers and harden dependency-free supervision * no-mistakes(review): Harden worker ownership recovery and shutdown quarantine * no-mistakes(review): Fix doctor bootstrap, harness repair, and output draining * no-mistakes(review): Probe doctor tools through authenticated worker bootstrap * no-mistakes(review): Refresh stale workers before doctor tool probes * no-mistakes(review): Recover stopped quarantines and extend job deadlines * no-mistakes(review): Separate queue and execution timeout windows * no-mistakes(review): Supervise Linux worker crashes and bind root identity * no-mistakes(review): Resolve authorized Nix profile bin links * no-mistakes(review): Clarify Nix path resolution documentation * no-mistakes(review): Harden PATH safety and nvm selection * no-mistakes(review): Honor nvm system defaults and refresh doctor digest * no-mistakes(review): Keep workers ready during active jobs * no-mistakes(review): Bound pre-execution validation by job timeout * no-mistakes(document): Clarify remote worker documentation * no-mistakes(lint): Fix remote worker ShellCheck diagnostics * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * fix: clarify remote doctor bootstrap path (kunchenguid#1691) * fix(bin): bound remote SSH dead-peer detection (kunchenguid#1699) * fix(remote): arm SSH dead-peer detection in fm-on.sh A vanished remote host mid-poll (a reboot, a dropped link) left ssh blocked indefinitely on a half-open TCP connection, because fm-on.sh's ssh invocation had no ServerAliveInterval/ServerAliveCountMax. This wedged the remote-reply ferry: fm-procevent.sh's runner blocked inside the ssh child and never reached its own no-result -> claim-release -> reconcile re-arm self-healing path, which otherwise already handles a nonzero exit with empty output correctly. Recovery required a manual retire and re-arm. Arm ServerAliveInterval=15 and ServerAliveCountMax=3 by default (bounded ~45s detection window), both overridable via FM_SSH_ALIVE_INTERVAL and FM_SSH_ALIVE_COUNT_MAX. This is a transport- level fix in fm-on.sh, so it covers every remote command routed through it, not just the reply ferry. The remote sshd answers keepalive probes independently of whatever the remote command is doing, so a legitimately long-but-alive command (a 55s poll, a clone, the doctor) is never falsely killed - only a truly vanished peer trips it, turning that case into a bounded, detectable ssh failure (exit 255) instead of an indefinite hang. Extends tests/fm-on.test.sh with a behavioral regression asserting a bounded, positive ServerAliveInterval/ServerAliveCountMax on the real ssh argv captured through the FM_SSH_BIN process seam, plus coverage that both are env-overridable. * no-mistakes(document): Document SSH dead-peer detection ownership * fix: report stale AXI tools during bootstrap (kunchenguid#1701) * feat(bootstrap): gate stale axi CLIs at the floors firstmate actually uses Add gh-axi 0.1.29 floor so bare --squash PR merges stop failing quietly on older builds. Raise tasks-axi to FM_TASKS_AXI_MIN=0.2.2 (multi-id mv) while keeping feature probes. Keep quota-axi at 0.1.16 after verifying schema 3 and per-model availability already ship there; runway remains optional. * no-mistakes(document): Clarify AXI compatibility documentation ownership * fix: prevent false watcher-down alarms in Claude sessions (kunchenguid#1661) * fix(guard): stop false watcher-down alarm mid-turn under Claude auto-arm bin/fm-guard.sh derived its watcher-health verdict from fm_watcher_healthy, which requires a live watcher process holding the home lock. Under the Claude Stop-hook auto-arm supervision model the watcher is armed at each turn end and exits on its wake, so it runs only between turns. Every guarded command run mid-turn therefore found no live watcher and printed the "WATCHER DOWN - SUPERVISION IS OFF" banner even though supervision was healthy. Because the episode key was derived from the beacon mtime (which the between-turns watcher advances every poll), the full banner re-printed on essentially every command, and the message always blamed a "fresh beacon" that was in fact fresh. Make the pull guard's health check model-aware via a new fm_watcher_supervision_verdict in bin/fm-wake-lib.sh: - Under the auto-arm model a beacon fresh within FM_GUARD_GRACE is healthy even with no live watcher process; only a beacon stale beyond grace (or absent) is a genuine lapse and alarms. - Under every persistent-watcher harness (codex foreground checkpoint, opencode/pi/grok background arm, tmux, unknown) a live identity-matched watcher with a fresh beacon is still required, unchanged. The banner now names the true failing condition, a missing live watcher process versus a genuinely stale beacon, instead of always blaming the beacon, and the once-per-episode dedup keys on that condition rather than the beacon mtime so a genuine lapse announces once and does not re-print each turn. The turn-end guard keeps the strict fm_watcher_healthy check because it fires at the turn boundary, where the auto-arm brings a fresh watcher up and it cooperates with that arm. fm_watcher_healthy itself is unchanged, so the arm layer's start/attach/replace decisions are unaffected. Tests in tests/fm-guard-stale-banner.test.sh cover the auto-arm healthy fresh-beacon-without-a-watcher case, the auto-arm stale-beacon alarm and its stable episode, the true-reason banner wording, and the reason-keyed episode surviving a beacon mtime change; existing persistent-model cases are pinned to that model. * no-mistakes(review): Pin secondmate supervision model to launched harness * no-mistakes(document): Align watcher documentation with model-aware supervision health * test: prevent fixture temporary directory leaks (kunchenguid#1704) * fix(tests): stop fixture-tempdir helper from self-deleting under command substitution fm_test_tmproot is almost always called as `TMP_ROOT=$(fm_test_tmproot prefix)`, which forks a subshell to capture its stdout. The old implementation set its EXIT cleanup trap inside that call, so the trap fired - and deleted the fixture root - the instant the subshell exited, before the real caller's own EXIT trap was ever installed. Every test using the documented call pattern leaked its fixture root on every run; two suites had already independently discovered and worked around this with ad-hoc mktemp calls. Registration now goes through a $$-keyed registry file instead of in-process state, since $$ resolves to the invoking shell's PID even inside the subshell. The real cleanup trap is armed once at source time (always the real caller, never a subshell) for EXIT, INT, and TERM. A best-effort orphan sweep on next source reaps marked fixture roots old enough to be from a killed prior run. Simplifies the two existing ad-hoc workarounds (fm-procevent.test.sh, wake-helpers.sh) back onto the shared helper now that it works correctly. * no-mistakes(review): Preserve live fixtures during orphan reaping * no-mistakes(review): Harden fixture ownership against PID reuse * no-mistakes(review): Secure cleanup registry against path precreation * no-mistakes(review): Make fixture registration transactional * no-mistakes(document): Documentation already matches fixture cleanup behavior * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * feat(herdr): enable presentation spaces by default (kunchenguid#1708) * feat(herdr): default presentation spaces on with an explicit opt-out Herdr's disposable one-task presentation workspace was opt-in through the presence of local config/herdr-presentation-spaces. It is now on by default, and a home opts out by writing "off" into that same file. Values are read with the whole-file whitespace-stripped convention the other scalar config items already use, plus case folding. An absent file, an empty file, and "on" all resolve on; only "off" opts out; an unrecognized value warns and keeps the default rather than failing a spawn over a purely visual setting. The empty file is exactly the historical opt-in form, so every home that had already enabled the projection stays enabled with no migration step, and no previously enabled home can be turned off by the flip. Because absence now means on at both ends, secondmate inheritance needs no item-specific convergence: mirroring an absent primary file converges a secondmate to the same default-on rather than turning its projection off, and only an explicit primary opt-out propagates the opt-out. The gate itself moves into fm_backend_herdr_presentation_enabled in the Herdr adapter so the semantics have one owner that regressions can exercise directly. * no-mistakes(document): Document Herdr default-on presentation safety --------- Co-authored-by: kunchenguid <kun-1@kunchenguid.com> * fix(bin): surface fleet-wide open decisions on every wake drain (kunchenguid#1711) * fix: surface consolidated open decisions on every wake-drain A needs-decision or blocked event buried under later, unrelated status appends was only ever shown via the last-line wake annotation, so a still-open captain decision could go silently missed even though status_open_decisions (fm-classify-lib.sh) already folds the whole status stream correctly and fleet-snapshot/bearings already reuse it. Wire that same fold into bin/fm-wake-drain.sh: a new fleet-wide scan_open_decisions wrapper scans every state/<id>.status, and fm-wake-drain.sh prints a separate, bounded OPEN DECISIONS section on every drain (including the empty-queue fast path), so session-start and every wake-handling turn surface it for free without duplicating the open/resolved fold itself. Heartbeat wakes drain through the same script, so this covers that surface too. Also tighten status_open_decisions' file guard to skip an unreadable status file instead of leaking a bash redirection error, now that a fleet-wide directory scan can reach files a single targeted read would not. * no-mistakes(review): Prevent status symlinks leaking open decisions * fix: drop unbounded perl subprocess from status symlink guard The review step's own symlink-safety auto-fix (O_NOFOLLOW read via a perl subprocess) forked one perl process per status file scanned by the new fleet-wide open-decisions scan, with no cap - inflating fm-wake-drain.sh's total external-read cost from 8 (the existing annotation read_cap) to 18 in the enrichment-caps regression test. The plain [ -L "$f" ] check already rejects any status file that is itself a symlink before any read happens, which is exactly what the new regression test exercises and is the same defense level the sibling scan_captain_relevant_statuses/last_status_line already rely on elsewhere in this file (no O_NOFOLLOW). Drop the subprocess-based nofollow read and keep the cheap builtin guard. * no-mistakes(document): Document actionable fleet-wide open decision drains * fix(bin): abort parked runs and reap leaked processes before teardown (kunchenguid#1710) * fix(bin): abort orphaned no-mistakes runs and reap leaked processes at teardown Teardown could remove a task's worker while its no-mistakes pipeline run was still parked at a gate, leaving an orphaned run holding a fleet slot indefinitely (observed 2026-08-03: runs parked 7h39m and parked at a post-CI approval gate). It could also leave backgrounded/disowned descendant processes rooted under the worktree or tasktmp surviving reparented to init (observed: two `go test` binaries pinning CPU for hours with no live task meta to attribute them to). Add two coupled pre-teardown steps, both scoped to this task's exact branch/head or worktree/tasktmp so they can never touch another task's run or processes: - conclude_task_no_mistakes_run aborts a run parked at a gate via `no-mistakes axi abort`, cd'd into the exact worktree so the daemon resolves the run itself rather than teardown naming a --run id. - reap_task_worktree_processes sweeps for processes whose cwd is under the worktree or tasktmp (via `lsof -a -d cwd`) and TERM/KILLs them. Both run before any worktree return, branch delete, or backend kill, and are idempotent on a retried teardown. The branch+head attribution logic is factored out of bin/fm-crew-state.sh into the new shared bin/fm-nm-run-lib.sh so both scripts use the same ownership contract. * no-mistakes(review): Fail closed on incomplete teardown cleanup * no-mistakes(review): Bind teardown cleanup to verified run and process identities * no-mistakes(review): Require confirmed aborts and convergent identity-safe process reaping * no-mistakes(review): Handle process exits during teardown identity checks * no-mistakes(review): Restore teardown library in hermetic gotmp fixtures * no-mistakes(document): Document teardown run attribution and timeout * no-mistakes(lint): Rename shell variable conflicting with done keyword * no-mistakes: apply CI fixes * test: give fork scaffolding tests the delivery contract upstream now requires Upstream kunchenguid#1563 made --mode mandatory for ship briefs and --mode plus --yolo mandatory for ship spawns, and updated the tests it shipped with. Three fork tests were written after the fork diverged, against the pre-kunchenguid#1563 interfaces, so they called the scaffolds with no delivery contract. Merging the two trunks put those callers in front of the new requirement and they refused before reaching the behavior each test exists to pin. Git could not see this: the requirement and the callers live in different files, so both sides merged clean and the breakage only showed up when the suite ran. Upstream's contract is kept exactly as it is - it is a deliberate safety gate, and AGENTS.md section 7 depends on the brief and the spawn refusing to guess. Each fork test instead states its mode explicitly, so every assertion it was written to make still runs. The standing-worker-rules variants each state their own mode, because those cases assert mode-specific prose and fm-brief no longer reads the registry. Verified: all three fail on the merge commit and pass here, with fm-brief, fm-launch-lib and fm-model-zero-budget green. --------- Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com> Co-authored-by: Gavin <51008413+allstargg@users.noreply.github.com> Co-authored-by: Christopher McKay <101884182+karotkriss@users.noreply.github.com> Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
sbracewell64
added a commit
to sbracewell64/firstmate
that referenced
this pull request
Aug 11, 2026
…ks (#39) * feat(bin): require an explicit per-task delivery contract (kunchenguid#1563) * feat(bin): require an explicit ship delivery mode in fm-brief A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions. * feat(bin): require an explicit ship delivery contract at spawn and promotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode. * docs: record the explicit per-task delivery contract AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly. * test: pass ship delivery flags per call site in the Herdr launcher e2e The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper. * test: pass the ship delivery contract in the secondmate suites Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires. * feat(bin): support remote secondmate homes (kunchenguid#1576) * Add generic remote secondmate transport * Add routed remote secondmate replies * Add remote outbox backlog handoff * Integrate remote secondmate lifecycle * no-mistakes(review): Fix remote snapshot and handoff races * no-mistakes(review): Serialize remote home provisioning transactions * no-mistakes(review): Harden remote lifecycle transaction boundaries * no-mistakes(review): Serialize remote lifecycle mutations and fail closed * no-mistakes(review): Close remote lifecycle and file race windows * no-mistakes(review): Serialize remote reply retirement and inheritance * no-mistakes(review): Harden remote transfer integrity and recovery * no-mistakes(review): Serialize remote respawn with registry retirement * no-mistakes(document): Document remote bootstrap convergence accurately * no-mistakes(document): Clarify skipped remote secondmate mutations * no-mistakes(lint): Resolve remote script ShellCheck warnings * no-mistakes: apply CI fixes * feat: add per-task trace context propagation (kunchenguid#995) * feat(spawn): propagate a native W3C traceparent to spawned agents Add a default-off capability that resolves one W3C traceparent for a task, injects it into the agent's pane shell as the TRACEPARENT environment variable immediately before launch, and records the identical value as traceparent= in state/<id>.meta, so an external observer that explicitly reads that env value or meta field can correlate a worker, a Secondmate, and their nested children into one trace with no collector, storage, UI, or vendor coupling. TRACEPARENT as an environment variable is a firstmate convention carrying a W3C-formatted value: W3C Trace Context standardizes the header, not an env var, and OpenTelemetry SDKs do not read it automatically, so a downstream must consume it deliberately; this feature parents no SDK span by itself. Identity is per task, not per spawn: the carrier is minted with random ids on the first spawn, adopted as a child (fresh span, same trace) for a nested spawn whose parent already holds one, and reused verbatim from the meta on relaunch, so a task keeps one stable logical identity across restarts. A malformed or all-zero inherited value is treated as absent and roots a fresh trace. A new root is sampled (01) - a sampling decision a downstream parent-based sampler honors, not a guarantee that any collector stores a span, and firstmate emits no spans; a child preserves the inherited flag. Trust boundary: a firstmate-minted root is random and reads no prompt, path, task prose, credential, or arbitrary environment key. An inherited TRACEPARENT is opaque caller-controlled data - up to 24 bytes of id passed through after syntax validation - so whoever set it controls those bytes, a bounded fixed-width channel rather than a general content or secret channel. The feature adds no OTEL_* variable, no tracestate, and no arbitrary environment injection; it runs no configurable or arbitrary command, only the fixed local od and tr (resolved from PATH) to read a few bytes of entropy - a small local pipeline with no network or watchdog and no hard latency guarantee. Any entropy or validation failure that returns omits the carrier without aborting the spawn. A default-off spawn leaves the generated meta and launch environment unchanged. Enablement is default-off (config/trace-context, or FM_TRACE_CONTEXT where a non-empty value overrides and unset or empty defers to the file) and is propagated into secondmate homes, taking effect at each agent's next launch: a Secondmate launched or relaunched after enablement carries the primary trace into its nested workers, while an already-running Secondmate roots new traces for its own workers until relaunched. Injection reuses the existing GOTMPDIR channel, so all spawn backends and harnesses and the ship, scout, and secondmate paths are covered. Covered by a pure-library suite and a spawn-path integration test (fake tmux plus a real worktree, hermetic against ambient FM_TRACE_CONTEXT) proving the recorded and injected carriers are identical and sent before launch, that default-off writes and injects neither, that a relaunch reuses the recorded carrier, and that an explicit FM_TRACE_CONTEXT overrides the file both ways; plus a source-owner inheritance test proving trace-context propagates and absence-mirrors through propagate_inheritable_config. Documentation follows the repository documentation-audiences contract: docs/trace-context.md is maintainer-architecture rationale, the configuration schema lives in docs/configuration.md, and the repeatable test evidence is separated into docs/verification/trace-context.md (maintainer-verification), registered in docs/documentation-audiences.json. * fix(spawn): propagate the effective trace-context decision to secondmates FM_TRACE_CONTEXT overrode trace context only in the process that read it. A newly launched secondmate decided enablement from the inherited config/trace-context file alone, so the override did not cross the primary-to-secondmate boundary: FM_TRACE_CONTEXT=off with the file present left the secondmate's nested workers traced (a broken kill switch), and FM_TRACE_CONTEXT=on with the file absent left them untraced despite the inherited carrier. Deliver the primary's effective decision to a newly launched secondmate as a normalized on/off FM_TRACE_CONTEXT in the launch prefix, so a FM_TRACE_CONTEXT override governs the nested primary -> secondmate -> worker chain both ways, not just the copied file. The value is bounded to the literal on/off and does not broaden environment injection; the already-running secondmate boundary is unchanged. Add a genuine two-level spawn regression that drives fm-spawn twice with the exact environment the primary injects into the secondmate and proves both divergent directions end to end. Correct the documentation that implied secondmate coverage on every backend, since orca and cmux reject secondmate spawns, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Clarify Secondmate trace-context launch snapshots * no-mistakes(document): Correct trace-context documentation ownership and relaunch semantics * fix(spawn): resolve the trace-context decision once for carrier and snapshot The effective trace-context decision was read twice per spawn: once inside fm_trace_context_resolve for the recorded carrier, and again for the secondmate FM_TRACE_CONTEXT launch snapshot. A config-file change between the two reads could pair a carrier with the opposite enable state - an injected carrier with an off snapshot, or no carrier with an on snapshot. Freeze the effective on/off decision once, drive the carrier resolution under that frozen FM_TRACE_CONTEXT so it cannot independently re-read the file, and reuse the same frozen decision for the secondmate launch snapshot. Add a spawn-path regression that drives the file-decided path and proves the recorded carrier and the delivered snapshot always agree, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Preserve legacy Secondmate trace boundary * no-mistakes(document): Correct trace-context verification comparison base * no-mistakes(review): Captain, prevent failed trace delivery metadata claims * no-mistakes(review): Captain, align trace-context tests and verification evidence * no-mistakes(document): Correct trace-context verification evidence * no-mistakes(lint): Suppress intentional ShellCheck literal-dollar warnings * no-mistakes(review): Captain: freeze trace context at session start * no-mistakes(test): Captain: stabilize scheduler test and document Kimi trace coverage * no-mistakes(document): Document trace-context safety boundaries * fix(trace): fail off on stale session snapshots Publish each home session decision atomically through a same-directory temporary file and bind it to the current session lock. A replacement failure can no longer leave an earlier on decision active in a later session; missing, stale, malformed, or unpublishable state defaults safely to off. Add regressions for read-only replacement and failed publication, update spawn and session-start fixtures for the lock-bound format, and refresh the architecture and verification records. * no-mistakes(review): Fix trace spawn failure independence and duplicate safety * no-mistakes(document): Refresh trace-context documentation and verification * no-mistakes(review): Clear partial backend input after failed trace submission * no-mistakes(review): Stop unsafe trace delivery before launch append * no-mistakes(document): Document unsafe trace delivery handling * fix(trace): bound each trace to one routed task, never the routing agent A persistent Secondmate holds its launch-time TRACEPARENT in the process environment for its whole life, and routed requests never replace it, so resolving new-task carriers from the ambient environment chained every routed task into one ever-growing trace per Secondmate with distinct parent ids. Resolve now reuses the task's recorded carrier or mints a fresh sampled root, never reading ambient TRACEPARENT, so each routed task is its own trace boundary while relaunch, recovery, and scout-to-ship promotion keep one stable per-task identity. The spawn regression models the reviewed scenario exactly: two unrelated tasks spawned sequentially through one persistent Secondmate environment record and inject distinct trace ids, adopt nothing from the Secondmate's carrier, and a relaunch of the first task reuses its original carrier verbatim. * docs(trace): define the per-task trace boundary The design contract is one task per trace: a persistent Secondmate is routing infrastructure with its own agent identity, never a shared trace root for the unrelated tasks routed through it. Root/recovery semantics replace the removed child-inheritance path, the sampling and safety sections drop inherited-carrier language because ambient TRACEPARENT is never read, and the verification page records the refreshed suite inventories including the two-task Secondmate boundary regression. * test(trace): adopt the explicit per-task delivery contract in spawn fixtures Rebasing onto current main brings the explicit per-task delivery contract: ship spawns now require --mode and --yolo instead of resolving them from the project registry. The trace spawn fixtures pass the same explicit contract canonical spawn tests use, preserving the per-task trace boundary coverage unchanged, and the verification page records the refreshed comparison base. * fix(bin): harden tmux agent liveness across harnesses (kunchenguid#1577) * fix(bin): classify tmux agent liveness independent of process titles `fm_backend_tmux_agent_state` attributed a pane solely from `#{pane_current_command}`, which is a process TITLE a harness can rewrite, not a structural fact. Claude Code 2.1.220 reports its version string there, so a live Claude endpoint classified `ambiguous`: the session-start secondmate liveness sweep could no longer see it, and any consumer that gates on a positive classification refuses outright. Read a second, independent name source: the kernel `comm` of every process in the pane tty's foreground process group. Either source naming a verified harness yields `alive`, because a false `dead` is the one verdict that can start a duplicate agent on a live worktree. Scoping to the foreground process group rather than the pane's descendants keeps a harness-named background process from faking an agent, and covers multi-process launchers (the Pi Launcher path) without a special case. Verified on 2026-08-03 against all seven adapters running for real on tmux 3.6a / macOS 26.5.2 arm64: claude 2.1.220, codex-cli 0.146.0, opencode 1.18.11, pi 0.82.0, pi-signed 0.82.0, grok 0.2.118, kimi 0.31.1 all classify `alive`, each attributed by a source independent of its title. Two tests, because they fail for different reasons: - tests/fm-tmux-agent-liveness.test.sh pins the logic with real processes and no harness, so it runs everywhere CI runs tmux. It drives the two name sources apart on purpose and asserts the divergence, so no case can go quietly vacuous. - tests/fm-harness-liveness-drift-live-e2e.test.sh relaunches every installed harness and fails naming the harness and version when one stops being attributed by a title-independent source. AGENTS.md section 4 carries the resulting standing rule, and firstmate-coding-guidelines owns how to satisfy it. * no-mistakes: apply CI fixes * docs: move the harness-dependent-check policy out of AGENTS.md The standing rule was stated in AGENTS.md section 4 with the mechanics in firstmate-coding-guidelines, which split one contract across two owners and charged every session for a rule that only fires when firstmate's own harness-dependent code is being changed. firstmate-coding-guidelines is now the single owner of both the rule and how to satisfy it: real-harness proof required, that proof authorized to spend tokens, structural signals preferred over vendor-rendered surfaces, and a guard that fails loudly naming the harness and version where a surface signal is unavoidable. No inline stub is left behind, because AGENTS.md already carries the load trigger for that skill in sections 7 and 13, so it is read before any change to firstmate's shared tracked material. Also records the cross-platform lesson the pipeline caught in the portable regression, and corrects that file's header: the divergence assertion lives on the version-string case, which diverges on both supported platforms, rather than on every case. * no-mistakes(review): Harden tmux liveness identity and drift validation * no-mistakes(document): Clarify cross-platform tmux liveness documentation * feat(bin): propagate trace context to remote secondmates (kunchenguid#1609) * feat(bin): trace remote secondmate routes and unify the inherit allowlist Per-task W3C trace context (kunchenguid#995) resolved and injected its carrier only at the local spawn path. A remote secondmate is routed through spawn_remote_secondmate, which returns long before that site and wrote its own metadata block, so a remote secondmate stayed silently untraced even with the capability enabled. The parent home still owns that task's identity, because it holds the metadata an observer reads. It now resolves the carrier against the task's own meta under its own frozen decision - reused verbatim on relaunch, freshly rooted otherwise, never adopting the parent process's ambient TRACEPARENT - and hands it to the configured host through a new fm-spawn --traceparent argument, accepted only for a secondmate launch and only as a strict W3C value. The remote host exports it at the same unconditional pre-launch site and reports back the carrier its endpoint actually holds, which the parent records, so an already-alive endpoint reports the identity its agent really received rather than one the parent merely intended. Disabled remains byte-identical and off. The remote inherit path also carried its own hardcoded copy of the inheritable config set, already drifted from FM_INHERITABLE_CONFIG by trace-context. Both remote ends now derive from that one declaration, so a future item cannot be sent by one side and refused by the other, and session-scoped enablement items are skipped on live convergence exactly as the local path skips them. Also fixes a latent stderr leak: an absent session lock printed a raw redirect failure, which the new remote resolve site made visible. Adds tests/fm-remote-secondmate-trace-context.test.sh, driving the real parent -> fm-on -> remote entrypoint -> control -> remote fm-spawn chain over the deterministic SSH boundary and reading the carrier back from the remote pane's own log. * no-mistakes(document): Clarify remote trace and allowlist contracts * feat(bin): preflight remote runtime tool paths (kunchenguid#1623) * feat(bin): widen the remote runtime PATH and add a remote doctor preflight The fixed remote entrypoint hard-coded a four-directory PATH, so a remote account whose tools live under nix or a per-user profile could not run basic Firstmate work without a login shell. The entrypoint now composes its child PATH from the code root's bin, the account's ~/.local/bin, the common package-manager directories that actually exist on the host, and the portable system tail, deduplicated and in a fixed order, still under env -i with the same variable allowlist and no shell command string. fm-remote-doctor.sh reports that exact PATH by inheriting it from its own entrypoint launch rather than recomposing it, so the ordering keeps one owner. It is read-only, reports where each required and optional tool resolved, and exits non-zero naming every required tool that did not. Remote seeding runs it as a preflight before anything is created on the host and restores the registry when it fails. * no-mistakes(review): Harden remote git authorization and missing-tool diagnostics * no-mistakes(document): Document remote PATH doctor and safe shims * no-mistakes(lint): Fix ShellCheck findings in remote path tests * no-mistakes(lint): Suppress exported fixture's false-positive ShellCheck warning * feat: gate remote second mates on Herdr readiness (kunchenguid#1639) * feat(bin): gate remote second mates on herdr readiness A remote second mate now always runs on the Herdr backend, whose server belongs to the host's GUI login session and therefore outlives the SSH connections that supervise it. fm-spawn's remote route forces that backend and the host-local control script refuses any other, so the requirement cannot be dropped from either side. fm-remote-doctor.sh becomes the single owner of what "ready" means. It keeps its PATH and tool reporting from kunchenguid#1623 and adds the Herdr, Aqua LaunchAgent, GUI-session, server-reachability, and entrypoint-symlink checks, tagging each gap fixable: or human: with the exact operator step. --fix closes only the automatable gaps - writing and loading the Aqua-scoped dev.firstmate.herdr launch agent, starting the server where no launch agent applies, and recreating the entrypoint symlink - then re-derives every check from the host, so a human gap is never presented as fixed. It never creates a login session, writes an auto-login password, or touches FileVault. Remote seed, remote spawn, and the startup liveness relaunch all run the same check, repair, re-check sequence through one shared library and fail closed with the doctor's own gap text. Recovery inherits the gate because it respawns through the same route. Tests drive the real doctor against a controlled account fixture with a private HOME, a state-backed launchctl, and a fake herdr, and prove the dangerous actions are never attempted. The remote lifecycle suites gain a stateful Herdr CLI fixture and answer the readiness gate at the SSH boundary, so they never inspect or repair the runner's own account. * no-mistakes(review): Validate launch-agent contract and confirm Herdr startup * no-mistakes(review): Validate loaded launch-agent contract before readiness * no-mistakes(review): Refuse legacy remote backends without altering routes * no-mistakes(review): Clarify conditional remote readiness repair sequence * no-mistakes(review): Repair remote readiness before liveness probing * no-mistakes(review): Preserve unknown seeds and reject legacy liveness * no-mistakes(document): docs: clarify remote Herdr backend ownership * fix: isolate remote secondmates in shared Herdr session (kunchenguid#1659) * Pin remote secondmates to fm-remote * no-mistakes(review): Fail closed on legacy remote Herdr endpoints * no-mistakes(review): Isolate fm-remote launch agent from interactive default * no-mistakes(document): Document shared remote Herdr retirement safety * feat: route remote commands through an Aqua job worker (kunchenguid#1660) * feat: run remote commands through Aqua job worker * no-mistakes(review): Enforce remote job deadlines and safe worker shutdown * no-mistakes(review): Refresh stale workers and harden dependency-free supervision * no-mistakes(review): Harden worker ownership recovery and shutdown quarantine * no-mistakes(review): Fix doctor bootstrap, harness repair, and output draining * no-mistakes(review): Probe doctor tools through authenticated worker bootstrap * no-mistakes(review): Refresh stale workers before doctor tool probes * no-mistakes(review): Recover stopped quarantines and extend job deadlines * no-mistakes(review): Separate queue and execution timeout windows * no-mistakes(review): Supervise Linux worker crashes and bind root identity * no-mistakes(review): Resolve authorized Nix profile bin links * no-mistakes(review): Clarify Nix path resolution documentation * no-mistakes(review): Harden PATH safety and nvm selection * no-mistakes(review): Honor nvm system defaults and refresh doctor digest * no-mistakes(review): Keep workers ready during active jobs * no-mistakes(review): Bound pre-execution validation by job timeout * no-mistakes(document): Clarify remote worker documentation * no-mistakes(lint): Fix remote worker ShellCheck diagnostics * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * fix: clarify remote doctor bootstrap path (kunchenguid#1691) * fix(bin): bound remote SSH dead-peer detection (kunchenguid#1699) * fix(remote): arm SSH dead-peer detection in fm-on.sh A vanished remote host mid-poll (a reboot, a dropped link) left ssh blocked indefinitely on a half-open TCP connection, because fm-on.sh's ssh invocation had no ServerAliveInterval/ServerAliveCountMax. This wedged the remote-reply ferry: fm-procevent.sh's runner blocked inside the ssh child and never reached its own no-result -> claim-release -> reconcile re-arm self-healing path, which otherwise already handles a nonzero exit with empty output correctly. Recovery required a manual retire and re-arm. Arm ServerAliveInterval=15 and ServerAliveCountMax=3 by default (bounded ~45s detection window), both overridable via FM_SSH_ALIVE_INTERVAL and FM_SSH_ALIVE_COUNT_MAX. This is a transport- level fix in fm-on.sh, so it covers every remote command routed through it, not just the reply ferry. The remote sshd answers keepalive probes independently of whatever the remote command is doing, so a legitimately long-but-alive command (a 55s poll, a clone, the doctor) is never falsely killed - only a truly vanished peer trips it, turning that case into a bounded, detectable ssh failure (exit 255) instead of an indefinite hang. Extends tests/fm-on.test.sh with a behavioral regression asserting a bounded, positive ServerAliveInterval/ServerAliveCountMax on the real ssh argv captured through the FM_SSH_BIN process seam, plus coverage that both are env-overridable. * no-mistakes(document): Document SSH dead-peer detection ownership * fix: report stale AXI tools during bootstrap (kunchenguid#1701) * feat(bootstrap): gate stale axi CLIs at the floors firstmate actually uses Add gh-axi 0.1.29 floor so bare --squash PR merges stop failing quietly on older builds. Raise tasks-axi to FM_TASKS_AXI_MIN=0.2.2 (multi-id mv) while keeping feature probes. Keep quota-axi at 0.1.16 after verifying schema 3 and per-model availability already ship there; runway remains optional. * no-mistakes(document): Clarify AXI compatibility documentation ownership * fix: prevent false watcher-down alarms in Claude sessions (kunchenguid#1661) * fix(guard): stop false watcher-down alarm mid-turn under Claude auto-arm bin/fm-guard.sh derived its watcher-health verdict from fm_watcher_healthy, which requires a live watcher process holding the home lock. Under the Claude Stop-hook auto-arm supervision model the watcher is armed at each turn end and exits on its wake, so it runs only between turns. Every guarded command run mid-turn therefore found no live watcher and printed the "WATCHER DOWN - SUPERVISION IS OFF" banner even though supervision was healthy. Because the episode key was derived from the beacon mtime (which the between-turns watcher advances every poll), the full banner re-printed on essentially every command, and the message always blamed a "fresh beacon" that was in fact fresh. Make the pull guard's health check model-aware via a new fm_watcher_supervision_verdict in bin/fm-wake-lib.sh: - Under the auto-arm model a beacon fresh within FM_GUARD_GRACE is healthy even with no live watcher process; only a beacon stale beyond grace (or absent) is a genuine lapse and alarms. - Under every persistent-watcher harness (codex foreground checkpoint, opencode/pi/grok background arm, tmux, unknown) a live identity-matched watcher with a fresh beacon is still required, unchanged. The banner now names the true failing condition, a missing live watcher process versus a genuinely stale beacon, instead of always blaming the beacon, and the once-per-episode dedup keys on that condition rather than the beacon mtime so a genuine lapse announces once and does not re-print each turn. The turn-end guard keeps the strict fm_watcher_healthy check because it fires at the turn boundary, where the auto-arm brings a fresh watcher up and it cooperates with that arm. fm_watcher_healthy itself is unchanged, so the arm layer's start/attach/replace decisions are unaffected. Tests in tests/fm-guard-stale-banner.test.sh cover the auto-arm healthy fresh-beacon-without-a-watcher case, the auto-arm stale-beacon alarm and its stable episode, the true-reason banner wording, and the reason-keyed episode surviving a beacon mtime change; existing persistent-model cases are pinned to that model. * no-mistakes(review): Pin secondmate supervision model to launched harness * no-mistakes(document): Align watcher documentation with model-aware supervision health * test: prevent fixture temporary directory leaks (kunchenguid#1704) * fix(tests): stop fixture-tempdir helper from self-deleting under command substitution fm_test_tmproot is almost always called as `TMP_ROOT=$(fm_test_tmproot prefix)`, which forks a subshell to capture its stdout. The old implementation set its EXIT cleanup trap inside that call, so the trap fired - and deleted the fixture root - the instant the subshell exited, before the real caller's own EXIT trap was ever installed. Every test using the documented call pattern leaked its fixture root on every run; two suites had already independently discovered and worked around this with ad-hoc mktemp calls. Registration now goes through a $$-keyed registry file instead of in-process state, since $$ resolves to the invoking shell's PID even inside the subshell. The real cleanup trap is armed once at source time (always the real caller, never a subshell) for EXIT, INT, and TERM. A best-effort orphan sweep on next source reaps marked fixture roots old enough to be from a killed prior run. Simplifies the two existing ad-hoc workarounds (fm-procevent.test.sh, wake-helpers.sh) back onto the shared helper now that it works correctly. * no-mistakes(review): Preserve live fixtures during orphan reaping * no-mistakes(review): Harden fixture ownership against PID reuse * no-mistakes(review): Secure cleanup registry against path precreation * no-mistakes(review): Make fixture registration transactional * no-mistakes(document): Documentation already matches fixture cleanup behavior * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * feat(herdr): enable presentation spaces by default (kunchenguid#1708) * feat(herdr): default presentation spaces on with an explicit opt-out Herdr's disposable one-task presentation workspace was opt-in through the presence of local config/herdr-presentation-spaces. It is now on by default, and a home opts out by writing "off" into that same file. Values are read with the whole-file whitespace-stripped convention the other scalar config items already use, plus case folding. An absent file, an empty file, and "on" all resolve on; only "off" opts out; an unrecognized value warns and keeps the default rather than failing a spawn over a purely visual setting. The empty file is exactly the historical opt-in form, so every home that had already enabled the projection stays enabled with no migration step, and no previously enabled home can be turned off by the flip. Because absence now means on at both ends, secondmate inheritance needs no item-specific convergence: mirroring an absent primary file converges a secondmate to the same default-on rather than turning its projection off, and only an explicit primary opt-out propagates the opt-out. The gate itself moves into fm_backend_herdr_presentation_enabled in the Herdr adapter so the semantics have one owner that regressions can exercise directly. * no-mistakes(document): Document Herdr default-on presentation safety --------- Co-authored-by: kunchenguid <kun-1@kunchenguid.com> * fix(bin): surface fleet-wide open decisions on every wake drain (kunchenguid#1711) * fix: surface consolidated open decisions on every wake-drain A needs-decision or blocked event buried under later, unrelated status appends was only ever shown via the last-line wake annotation, so a still-open captain decision could go silently missed even though status_open_decisions (fm-classify-lib.sh) already folds the whole status stream correctly and fleet-snapshot/bearings already reuse it. Wire that same fold into bin/fm-wake-drain.sh: a new fleet-wide scan_open_decisions wrapper scans every state/<id>.status, and fm-wake-drain.sh prints a separate, bounded OPEN DECISIONS section on every drain (including the empty-queue fast path), so session-start and every wake-handling turn surface it for free without duplicating the open/resolved fold itself. Heartbeat wakes drain through the same script, so this covers that surface too. Also tighten status_open_decisions' file guard to skip an unreadable status file instead of leaking a bash redirection error, now that a fleet-wide directory scan can reach files a single targeted read would not. * no-mistakes(review): Prevent status symlinks leaking open decisions * fix: drop unbounded perl subprocess from status symlink guard The review step's own symlink-safety auto-fix (O_NOFOLLOW read via a perl subprocess) forked one perl process per status file scanned by the new fleet-wide open-decisions scan, with no cap - inflating fm-wake-drain.sh's total external-read cost from 8 (the existing annotation read_cap) to 18 in the enrichment-caps regression test. The plain [ -L "$f" ] check already rejects any status file that is itself a symlink before any read happens, which is exactly what the new regression test exercises and is the same defense level the sibling scan_captain_relevant_statuses/last_status_line already rely on elsewhere in this file (no O_NOFOLLOW). Drop the subprocess-based nofollow read and keep the cheap builtin guard. * no-mistakes(document): Document actionable fleet-wide open decision drains * fix(bin): abort parked runs and reap leaked processes before teardown (kunchenguid#1710) * fix(bin): abort orphaned no-mistakes runs and reap leaked processes at teardown Teardown could remove a task's worker while its no-mistakes pipeline run was still parked at a gate, leaving an orphaned run holding a fleet slot indefinitely (observed 2026-08-03: runs parked 7h39m and parked at a post-CI approval gate). It could also leave backgrounded/disowned descendant processes rooted under the worktree or tasktmp surviving reparented to init (observed: two `go test` binaries pinning CPU for hours with no live task meta to attribute them to). Add two coupled pre-teardown steps, both scoped to this task's exact branch/head or worktree/tasktmp so they can never touch another task's run or processes: - conclude_task_no_mistakes_run aborts a run parked at a gate via `no-mistakes axi abort`, cd'd into the exact worktree so the daemon resolves the run itself rather than teardown naming a --run id. - reap_task_worktree_processes sweeps for processes whose cwd is under the worktree or tasktmp (via `lsof -a -d cwd`) and TERM/KILLs them. Both run before any worktree return, branch delete, or backend kill, and are idempotent on a retried teardown. The branch+head attribution logic is factored out of bin/fm-crew-state.sh into the new shared bin/fm-nm-run-lib.sh so both scripts use the same ownership contract. * no-mistakes(review): Fail closed on incomplete teardown cleanup * no-mistakes(review): Bind teardown cleanup to verified run and process identities * no-mistakes(review): Require confirmed aborts and convergent identity-safe process reaping * no-mistakes(review): Handle process exits during teardown identity checks * no-mistakes(review): Restore teardown library in hermetic gotmp fixtures * no-mistakes(document): Document teardown run attribution and timeout * no-mistakes(lint): Rename shell variable conflicting with done keyword * no-mistakes: apply CI fixes * test: give fork scaffolding tests the delivery contract upstream now requires Upstream kunchenguid#1563 made --mode mandatory for ship briefs and --mode plus --yolo mandatory for ship spawns, and updated the tests it shipped with. Three fork tests were written after the fork diverged, against the pre-kunchenguid#1563 interfaces, so they called the scaffolds with no delivery contract. Merging the two trunks put those callers in front of the new requirement and they refused before reaching the behavior each test exists to pin. Git could not see this: the requirement and the callers live in different files, so both sides merged clean and the breakage only showed up when the suite ran. Upstream's contract is kept exactly as it is - it is a deliberate safety gate, and AGENTS.md section 7 depends on the brief and the spawn refusing to guess. Each fork test instead states its mode explicitly, so every assertion it was written to make still runs. The standing-worker-rules variants each state their own mode, because those cases assert mode-specific prose and fm-brief no longer reads the registry. Verified: all three fail on the merge commit and pass here, with fm-brief, fm-launch-lib and fm-model-zero-budget green. --------- Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com> Co-authored-by: Gavin <51008413+allstargg@users.noreply.github.com> Co-authored-by: Christopher McKay <101884182+karotkriss@users.noreply.github.com> Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
4mb1t10n
pushed a commit
to 4mb1t10n/firstmate
that referenced
this pull request
Aug 11, 2026
…d#1563) * feat(bin): require an explicit ship delivery mode in fm-brief A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions. * feat(bin): require an explicit ship delivery contract at spawn and promotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode. * docs: record the explicit per-task delivery contract AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly. * test: pass ship delivery flags per call site in the Herdr launcher e2e The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper. * test: pass the ship delivery contract in the secondmate suites Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires.
levelupself
added a commit
to levelupself/firstmate
that referenced
this pull request
Aug 13, 2026
* feat(bin): require an explicit per-task delivery contract (#1563)
* feat(bin): require an explicit ship delivery mode in fm-brief
A ship brief's definition of done was shaped by a silent per-project registry
lookup, so an adjusted brief and the task's recorded delivery could disagree and
no one had to decide anything per task.
fm-brief now requires --mode on ship scaffolds, validates it against the closed
set, refuses the conditional no-mistakes-prod-only registry policy as a task
mode, and records the choice as a fixed machine-readable "Delivery contract:
mode=<mode>" line that fm-spawn can check. --mode is refused on scout and
secondmate scaffolds, and --yolo is refused outright because the worker never
owns approval decisions.
* feat(bin): require an explicit ship delivery contract at spawn and promotion
fm-spawn resolved every ship and scout task's mode and yolo from the project
registry, so the delivery posture was never a per-task decision and could
contradict the brief the worker was about to follow.
fm-spawn now requires --mode and --yolo on ship spawns, validates both against
their closed sets, and reads the brief's recorded delivery contract line and
refuses a mismatch before any endpoint exists; a brief scaffolded before that
line existed warns once and launches on the flag. A batch carries one shared
contract that each pair still checks against its own brief. Scout and secondmate
spawns refuse the flags, and a scout now records no mode or yolo at all, which
teardown and the snapshot already tolerate. When the explicit mode carries less
rigor than the project's standing posture, a deviation notice is printed and the
spawn continues, so the registry stays advisory rather than an enforced default.
fm-promote requires the same two flags, because a scout carries no posture to
inherit, and writes them into the task record with the kind flip.
fm-project-mode keeps its one registry parser for the mechanical consumers that
have no task in hand, accepts the conditional no-mistakes-prod-only annotation
and maps it to its most rigorous leg for them, and grows --raw so the deviation
notice can tell a conditional policy apart from a flat mode.
* docs: record the explicit per-task delivery contract
AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at
intake, including the surface classification for a no-mistakes-prod-only project
and the unregistered-project fallback, and the project-management skill defines
that conditional policy as a registration-time posture with its defaults and
initialization consequences. The registry blurb, script table, and architecture
section follow: the registry records the captain's standing posture, and task
delivery is decided per task and passed explicitly.
* test: pass ship delivery flags per call site in the Herdr launcher e2e
The shared spawn helper also launches a secondmate, which refuses the flags, so
the contract belongs at each ship call site rather than inside the helper.
* test: pass the ship delivery contract in the secondmate suites
Both suites scaffold or spawn an ordinary ship task as the control case for a
secondmate assertion, so each needs the explicit contract the ship path now
requires.
* feat(bin): support remote secondmate homes (#1576)
* Add generic remote secondmate transport
* Add routed remote secondmate replies
* Add remote outbox backlog handoff
* Integrate remote secondmate lifecycle
* no-mistakes(review): Fix remote snapshot and handoff races
* no-mistakes(review): Serialize remote home provisioning transactions
* no-mistakes(review): Harden remote lifecycle transaction boundaries
* no-mistakes(review): Serialize remote lifecycle mutations and fail closed
* no-mistakes(review): Close remote lifecycle and file race windows
* no-mistakes(review): Serialize remote reply retirement and inheritance
* no-mistakes(review): Harden remote transfer integrity and recovery
* no-mistakes(review): Serialize remote respawn with registry retirement
* no-mistakes(document): Document remote bootstrap convergence accurately
* no-mistakes(document): Clarify skipped remote secondmate mutations
* no-mistakes(lint): Resolve remote script ShellCheck warnings
* no-mistakes: apply CI fixes
* feat: add per-task trace context propagation (#995)
* feat(spawn): propagate a native W3C traceparent to spawned agents
Add a default-off capability that resolves one W3C traceparent for a task,
injects it into the agent's pane shell as the TRACEPARENT environment
variable immediately before launch, and records the identical value as
traceparent= in state/<id>.meta, so an external observer that explicitly
reads that env value or meta field can correlate a worker, a Secondmate, and
their nested children into one trace with no collector, storage, UI, or
vendor coupling.
TRACEPARENT as an environment variable is a firstmate convention carrying a
W3C-formatted value: W3C Trace Context standardizes the header, not an env
var, and OpenTelemetry SDKs do not read it automatically, so a downstream
must consume it deliberately; this feature parents no SDK span by itself.
Identity is per task, not per spawn: the carrier is minted with random ids on
the first spawn, adopted as a child (fresh span, same trace) for a nested
spawn whose parent already holds one, and reused verbatim from the meta on
relaunch, so a task keeps one stable logical identity across restarts. A
malformed or all-zero inherited value is treated as absent and roots a fresh
trace. A new root is sampled (01) - a sampling decision a downstream
parent-based sampler honors, not a guarantee that any collector stores a
span, and firstmate emits no spans; a child preserves the inherited flag.
Trust boundary: a firstmate-minted root is random and reads no prompt, path,
task prose, credential, or arbitrary environment key. An inherited
TRACEPARENT is opaque caller-controlled data - up to 24 bytes of id passed
through after syntax validation - so whoever set it controls those bytes, a
bounded fixed-width channel rather than a general content or secret channel.
The feature adds no OTEL_* variable, no tracestate, and no arbitrary
environment injection; it runs no configurable or arbitrary command, only the
fixed local od and tr (resolved from PATH) to read a few bytes of entropy - a
small local pipeline with no network or watchdog and no hard latency
guarantee. Any entropy or validation failure that returns omits the carrier
without aborting the spawn. A default-off spawn leaves the generated meta and
launch environment unchanged.
Enablement is default-off (config/trace-context, or FM_TRACE_CONTEXT where a
non-empty value overrides and unset or empty defers to the file) and is
propagated into secondmate homes, taking effect at each agent's next launch:
a Secondmate launched or relaunched after enablement carries the primary
trace into its nested workers, while an already-running Secondmate roots new
traces for its own workers until relaunched. Injection reuses the existing
GOTMPDIR channel, so all spawn backends and harnesses and the ship, scout,
and secondmate paths are covered.
Covered by a pure-library suite and a spawn-path integration test (fake tmux
plus a real worktree, hermetic against ambient FM_TRACE_CONTEXT) proving the
recorded and injected carriers are identical and sent before launch, that
default-off writes and injects neither, that a relaunch reuses the recorded
carrier, and that an explicit FM_TRACE_CONTEXT overrides the file both ways;
plus a source-owner inheritance test proving trace-context propagates and
absence-mirrors through propagate_inheritable_config.
Documentation follows the repository documentation-audiences contract:
docs/trace-context.md is maintainer-architecture rationale, the configuration
schema lives in docs/configuration.md, and the repeatable test evidence is
separated into docs/verification/trace-context.md (maintainer-verification),
registered in docs/documentation-audiences.json.
* fix(spawn): propagate the effective trace-context decision to secondmates
FM_TRACE_CONTEXT overrode trace context only in the process that read it. A
newly launched secondmate decided enablement from the inherited
config/trace-context file alone, so the override did not cross the
primary-to-secondmate boundary: FM_TRACE_CONTEXT=off with the file present left
the secondmate's nested workers traced (a broken kill switch), and
FM_TRACE_CONTEXT=on with the file absent left them untraced despite the
inherited carrier.
Deliver the primary's effective decision to a newly launched secondmate as a
normalized on/off FM_TRACE_CONTEXT in the launch prefix, so a FM_TRACE_CONTEXT
override governs the nested primary -> secondmate -> worker chain both ways, not
just the copied file. The value is bounded to the literal on/off and does not
broaden environment injection; the already-running secondmate boundary is
unchanged.
Add a genuine two-level spawn regression that drives fm-spawn twice with the
exact environment the primary injects into the secondmate and proves both
divergent directions end to end. Correct the documentation that implied
secondmate coverage on every backend, since orca and cmux reject secondmate
spawns, and refresh the verification evidence for the new assertion count.
* no-mistakes(review): Clarify Secondmate trace-context launch snapshots
* no-mistakes(document): Correct trace-context documentation ownership and relaunch semantics
* fix(spawn): resolve the trace-context decision once for carrier and snapshot
The effective trace-context decision was read twice per spawn: once inside
fm_trace_context_resolve for the recorded carrier, and again for the secondmate
FM_TRACE_CONTEXT launch snapshot. A config-file change between the two reads
could pair a carrier with the opposite enable state - an injected carrier with
an off snapshot, or no carrier with an on snapshot.
Freeze the effective on/off decision once, drive the carrier resolution under
that frozen FM_TRACE_CONTEXT so it cannot independently re-read the file, and
reuse the same frozen decision for the secondmate launch snapshot. Add a
spawn-path regression that drives the file-decided path and proves the recorded
carrier and the delivered snapshot always agree, and refresh the verification
evidence for the new assertion count.
* no-mistakes(review): Preserve legacy Secondmate trace boundary
* no-mistakes(document): Correct trace-context verification comparison base
* no-mistakes(review): Captain, prevent failed trace delivery metadata claims
* no-mistakes(review): Captain, align trace-context tests and verification evidence
* no-mistakes(document): Correct trace-context verification evidence
* no-mistakes(lint): Suppress intentional ShellCheck literal-dollar warnings
* no-mistakes(review): Captain: freeze trace context at session start
* no-mistakes(test): Captain: stabilize scheduler test and document Kimi trace coverage
* no-mistakes(document): Document trace-context safety boundaries
* fix(trace): fail off on stale session snapshots
Publish each home session decision atomically through a same-directory temporary file and bind it to the current session lock. A replacement failure can no longer leave an earlier on decision active in a later session; missing, stale, malformed, or unpublishable state defaults safely to off.
Add regressions for read-only replacement and failed publication, update spawn and session-start fixtures for the lock-bound format, and refresh the architecture and verification records.
* no-mistakes(review): Fix trace spawn failure independence and duplicate safety
* no-mistakes(document): Refresh trace-context documentation and verification
* no-mistakes(review): Clear partial backend input after failed trace submission
* no-mistakes(review): Stop unsafe trace delivery before launch append
* no-mistakes(document): Document unsafe trace delivery handling
* fix(trace): bound each trace to one routed task, never the routing agent
A persistent Secondmate holds its launch-time TRACEPARENT in the process
environment for its whole life, and routed requests never replace it, so
resolving new-task carriers from the ambient environment chained every
routed task into one ever-growing trace per Secondmate with distinct
parent ids. Resolve now reuses the task's recorded carrier or mints a
fresh sampled root, never reading ambient TRACEPARENT, so each routed
task is its own trace boundary while relaunch, recovery, and
scout-to-ship promotion keep one stable per-task identity.
The spawn regression models the reviewed scenario exactly: two unrelated
tasks spawned sequentially through one persistent Secondmate environment
record and inject distinct trace ids, adopt nothing from the Secondmate's
carrier, and a relaunch of the first task reuses its original carrier
verbatim.
* docs(trace): define the per-task trace boundary
The design contract is one task per trace: a persistent Secondmate is
routing infrastructure with its own agent identity, never a shared trace
root for the unrelated tasks routed through it. Root/recovery semantics
replace the removed child-inheritance path, the sampling and safety
sections drop inherited-carrier language because ambient TRACEPARENT is
never read, and the verification page records the refreshed suite
inventories including the two-task Secondmate boundary regression.
* test(trace): adopt the explicit per-task delivery contract in spawn fixtures
Rebasing onto current main brings the explicit per-task delivery contract:
ship spawns now require --mode and --yolo instead of resolving them from the
project registry. The trace spawn fixtures pass the same explicit contract
canonical spawn tests use, preserving the per-task trace boundary coverage
unchanged, and the verification page records the refreshed comparison base.
* fix(bin): harden tmux agent liveness across harnesses (#1577)
* fix(bin): classify tmux agent liveness independent of process titles
`fm_backend_tmux_agent_state` attributed a pane solely from
`#{pane_current_command}`, which is a process TITLE a harness can rewrite,
not a structural fact. Claude Code 2.1.220 reports its version string there,
so a live Claude endpoint classified `ambiguous`: the session-start secondmate
liveness sweep could no longer see it, and any consumer that gates on a
positive classification refuses outright.
Read a second, independent name source: the kernel `comm` of every process in
the pane tty's foreground process group. Either source naming a verified
harness yields `alive`, because a false `dead` is the one verdict that can
start a duplicate agent on a live worktree. Scoping to the foreground process
group rather than the pane's descendants keeps a harness-named background
process from faking an agent, and covers multi-process launchers (the Pi
Launcher path) without a special case.
Verified on 2026-08-03 against all seven adapters running for real on tmux
3.6a / macOS 26.5.2 arm64: claude 2.1.220, codex-cli 0.146.0, opencode
1.18.11, pi 0.82.0, pi-signed 0.82.0, grok 0.2.118, kimi 0.31.1 all classify
`alive`, each attributed by a source independent of its title.
Two tests, because they fail for different reasons:
- tests/fm-tmux-agent-liveness.test.sh pins the logic with real processes and
no harness, so it runs everywhere CI runs tmux. It drives the two name
sources apart on purpose and asserts the divergence, so no case can go
quietly vacuous.
- tests/fm-harness-liveness-drift-live-e2e.test.sh relaunches every installed
harness and fails naming the harness and version when one stops being
attributed by a title-independent source.
AGENTS.md section 4 carries the resulting standing rule, and
firstmate-coding-guidelines owns how to satisfy it.
* no-mistakes: apply CI fixes
* docs: move the harness-dependent-check policy out of AGENTS.md
The standing rule was stated in AGENTS.md section 4 with the mechanics in
firstmate-coding-guidelines, which split one contract across two owners and
charged every session for a rule that only fires when firstmate's own
harness-dependent code is being changed.
firstmate-coding-guidelines is now the single owner of both the rule and how
to satisfy it: real-harness proof required, that proof authorized to spend
tokens, structural signals preferred over vendor-rendered surfaces, and a
guard that fails loudly naming the harness and version where a surface signal
is unavoidable. No inline stub is left behind, because AGENTS.md already
carries the load trigger for that skill in sections 7 and 13, so it is read
before any change to firstmate's shared tracked material.
Also records the cross-platform lesson the pipeline caught in the portable
regression, and corrects that file's header: the divergence assertion lives
on the version-string case, which diverges on both supported platforms,
rather than on every case.
* no-mistakes(review): Harden tmux liveness identity and drift validation
* no-mistakes(document): Clarify cross-platform tmux liveness documentation
* feat(bin): propagate trace context to remote secondmates (#1609)
* feat(bin): trace remote secondmate routes and unify the inherit allowlist
Per-task W3C trace context (#995) resolved and injected its carrier only at
the local spawn path. A remote secondmate is routed through
spawn_remote_secondmate, which returns long before that site and wrote its own
metadata block, so a remote secondmate stayed silently untraced even with the
capability enabled.
The parent home still owns that task's identity, because it holds the metadata
an observer reads. It now resolves the carrier against the task's own meta
under its own frozen decision - reused verbatim on relaunch, freshly rooted
otherwise, never adopting the parent process's ambient TRACEPARENT - and hands
it to the configured host through a new fm-spawn --traceparent argument,
accepted only for a secondmate launch and only as a strict W3C value. The
remote host exports it at the same unconditional pre-launch site and reports
back the carrier its endpoint actually holds, which the parent records, so an
already-alive endpoint reports the identity its agent really received rather
than one the parent merely intended. Disabled remains byte-identical and off.
The remote inherit path also carried its own hardcoded copy of the inheritable
config set, already drifted from FM_INHERITABLE_CONFIG by trace-context. Both
remote ends now derive from that one declaration, so a future item cannot be
sent by one side and refused by the other, and session-scoped enablement items
are skipped on live convergence exactly as the local path skips them.
Also fixes a latent stderr leak: an absent session lock printed a raw redirect
failure, which the new remote resolve site made visible.
Adds tests/fm-remote-secondmate-trace-context.test.sh, driving the real
parent -> fm-on -> remote entrypoint -> control -> remote fm-spawn chain over
the deterministic SSH boundary and reading the carrier back from the remote
pane's own log.
* no-mistakes(document): Clarify remote trace and allowlist contracts
* feat(bin): preflight remote runtime tool paths (#1623)
* feat(bin): widen the remote runtime PATH and add a remote doctor preflight
The fixed remote entrypoint hard-coded a four-directory PATH, so a remote
account whose tools live under nix or a per-user profile could not run basic
Firstmate work without a login shell. The entrypoint now composes its child
PATH from the code root's bin, the account's ~/.local/bin, the common
package-manager directories that actually exist on the host, and the portable
system tail, deduplicated and in a fixed order, still under env -i with the
same variable allowlist and no shell command string.
fm-remote-doctor.sh reports that exact PATH by inheriting it from its own
entrypoint launch rather than recomposing it, so the ordering keeps one owner.
It is read-only, reports where each required and optional tool resolved, and
exits non-zero naming every required tool that did not. Remote seeding runs it
as a preflight before anything is created on the host and restores the registry
when it fails.
* no-mistakes(review): Harden remote git authorization and missing-tool diagnostics
* no-mistakes(document): Document remote PATH doctor and safe shims
* no-mistakes(lint): Fix ShellCheck findings in remote path tests
* no-mistakes(lint): Suppress exported fixture's false-positive ShellCheck warning
* feat: gate remote second mates on Herdr readiness (#1639)
* feat(bin): gate remote second mates on herdr readiness
A remote second mate now always runs on the Herdr backend, whose server
belongs to the host's GUI login session and therefore outlives the SSH
connections that supervise it. fm-spawn's remote route forces that backend
and the host-local control script refuses any other, so the requirement
cannot be dropped from either side.
fm-remote-doctor.sh becomes the single owner of what "ready" means. It keeps
its PATH and tool reporting from #1623 and adds the Herdr, Aqua LaunchAgent,
GUI-session, server-reachability, and entrypoint-symlink checks, tagging each
gap fixable: or human: with the exact operator step. --fix closes only the
automatable gaps - writing and loading the Aqua-scoped dev.firstmate.herdr
launch agent, starting the server where no launch agent applies, and
recreating the entrypoint symlink - then re-derives every check from the host,
so a human gap is never presented as fixed. It never creates a login session,
writes an auto-login password, or touches FileVault.
Remote seed, remote spawn, and the startup liveness relaunch all run the same
check, repair, re-check sequence through one shared library and fail closed
with the doctor's own gap text. Recovery inherits the gate because it respawns
through the same route.
Tests drive the real doctor against a controlled account fixture with a
private HOME, a state-backed launchctl, and a fake herdr, and prove the
dangerous actions are never attempted. The remote lifecycle suites gain a
stateful Herdr CLI fixture and answer the readiness gate at the SSH boundary,
so they never inspect or repair the runner's own account.
* no-mistakes(review): Validate launch-agent contract and confirm Herdr startup
* no-mistakes(review): Validate loaded launch-agent contract before readiness
* no-mistakes(review): Refuse legacy remote backends without altering routes
* no-mistakes(review): Clarify conditional remote readiness repair sequence
* no-mistakes(review): Repair remote readiness before liveness probing
* no-mistakes(review): Preserve unknown seeds and reject legacy liveness
* no-mistakes(document): docs: clarify remote Herdr backend ownership
* fix: isolate remote secondmates in shared Herdr session (#1659)
* Pin remote secondmates to fm-remote
* no-mistakes(review): Fail closed on legacy remote Herdr endpoints
* no-mistakes(review): Isolate fm-remote launch agent from interactive default
* no-mistakes(document): Document shared remote Herdr retirement safety
* feat: route remote commands through an Aqua job worker (#1660)
* feat: run remote commands through Aqua job worker
* no-mistakes(review): Enforce remote job deadlines and safe worker shutdown
* no-mistakes(review): Refresh stale workers and harden dependency-free supervision
* no-mistakes(review): Harden worker ownership recovery and shutdown quarantine
* no-mistakes(review): Fix doctor bootstrap, harness repair, and output draining
* no-mistakes(review): Probe doctor tools through authenticated worker bootstrap
* no-mistakes(review): Refresh stale workers before doctor tool probes
* no-mistakes(review): Recover stopped quarantines and extend job deadlines
* no-mistakes(review): Separate queue and execution timeout windows
* no-mistakes(review): Supervise Linux worker crashes and bind root identity
* no-mistakes(review): Resolve authorized Nix profile bin links
* no-mistakes(review): Clarify Nix path resolution documentation
* no-mistakes(review): Harden PATH safety and nvm selection
* no-mistakes(review): Honor nvm system defaults and refresh doctor digest
* no-mistakes(review): Keep workers ready during active jobs
* no-mistakes(review): Bound pre-execution validation by job timeout
* no-mistakes(document): Clarify remote worker documentation
* no-mistakes(lint): Fix remote worker ShellCheck diagnostics
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
* fix: clarify remote doctor bootstrap path (#1691)
* fix(bin): bound remote SSH dead-peer detection (#1699)
* fix(remote): arm SSH dead-peer detection in fm-on.sh
A vanished remote host mid-poll (a reboot, a dropped link) left ssh
blocked indefinitely on a half-open TCP connection, because fm-on.sh's
ssh invocation had no ServerAliveInterval/ServerAliveCountMax. This
wedged the remote-reply ferry: fm-procevent.sh's runner blocked inside
the ssh child and never reached its own no-result -> claim-release ->
reconcile re-arm self-healing path, which otherwise already handles a
nonzero exit with empty output correctly. Recovery required a manual
retire and re-arm.
Arm ServerAliveInterval=15 and ServerAliveCountMax=3 by default
(bounded ~45s detection window), both overridable via
FM_SSH_ALIVE_INTERVAL and FM_SSH_ALIVE_COUNT_MAX. This is a transport-
level fix in fm-on.sh, so it covers every remote command routed
through it, not just the reply ferry. The remote sshd answers
keepalive probes independently of whatever the remote command is
doing, so a legitimately long-but-alive command (a 55s poll, a clone,
the doctor) is never falsely killed - only a truly vanished peer trips
it, turning that case into a bounded, detectable ssh failure (exit
255) instead of an indefinite hang.
Extends tests/fm-on.test.sh with a behavioral regression asserting a
bounded, positive ServerAliveInterval/ServerAliveCountMax on the real
ssh argv captured through the FM_SSH_BIN process seam, plus coverage
that both are env-overridable.
* no-mistakes(document): Document SSH dead-peer detection ownership
* fix: report stale AXI tools during bootstrap (#1701)
* feat(bootstrap): gate stale axi CLIs at the floors firstmate actually uses
Add gh-axi 0.1.29 floor so bare --squash PR merges stop failing quietly on
older builds. Raise tasks-axi to FM_TASKS_AXI_MIN=0.2.2 (multi-id mv) while
keeping feature probes. Keep quota-axi at 0.1.16 after verifying schema 3
and per-model availability already ship there; runway remains optional.
* no-mistakes(document): Clarify AXI compatibility documentation ownership
* fix: prevent false watcher-down alarms in Claude sessions (#1661)
* fix(guard): stop false watcher-down alarm mid-turn under Claude auto-arm
bin/fm-guard.sh derived its watcher-health verdict from fm_watcher_healthy,
which requires a live watcher process holding the home lock. Under the Claude
Stop-hook auto-arm supervision model the watcher is armed at each turn end and
exits on its wake, so it runs only between turns. Every guarded command run
mid-turn therefore found no live watcher and printed the "WATCHER DOWN -
SUPERVISION IS OFF" banner even though supervision was healthy. Because the
episode key was derived from the beacon mtime (which the between-turns watcher
advances every poll), the full banner re-printed on essentially every command,
and the message always blamed a "fresh beacon" that was in fact fresh.
Make the pull guard's health check model-aware via a new
fm_watcher_supervision_verdict in bin/fm-wake-lib.sh:
- Under the auto-arm model a beacon fresh within FM_GUARD_GRACE is healthy even
with no live watcher process; only a beacon stale beyond grace (or absent) is
a genuine lapse and alarms.
- Under every persistent-watcher harness (codex foreground checkpoint,
opencode/pi/grok background arm, tmux, unknown) a live identity-matched
watcher with a fresh beacon is still required, unchanged.
The banner now names the true failing condition, a missing live watcher process
versus a genuinely stale beacon, instead of always blaming the beacon, and the
once-per-episode dedup keys on that condition rather than the beacon mtime so a
genuine lapse announces once and does not re-print each turn.
The turn-end guard keeps the strict fm_watcher_healthy check because it fires at
the turn boundary, where the auto-arm brings a fresh watcher up and it
cooperates with that arm. fm_watcher_healthy itself is unchanged, so the arm
layer's start/attach/replace decisions are unaffected.
Tests in tests/fm-guard-stale-banner.test.sh cover the auto-arm healthy
fresh-beacon-without-a-watcher case, the auto-arm stale-beacon alarm and its
stable episode, the true-reason banner wording, and the reason-keyed episode
surviving a beacon mtime change; existing persistent-model cases are pinned to
that model.
* no-mistakes(review): Pin secondmate supervision model to launched harness
* no-mistakes(document): Align watcher documentation with model-aware supervision health
* test: prevent fixture temporary directory leaks (#1704)
* fix(tests): stop fixture-tempdir helper from self-deleting under command substitution
fm_test_tmproot is almost always called as `TMP_ROOT=$(fm_test_tmproot prefix)`,
which forks a subshell to capture its stdout. The old implementation set its
EXIT cleanup trap inside that call, so the trap fired - and deleted the fixture
root - the instant the subshell exited, before the real caller's own EXIT trap
was ever installed. Every test using the documented call pattern leaked its
fixture root on every run; two suites had already independently discovered and
worked around this with ad-hoc mktemp calls.
Registration now goes through a $$-keyed registry file instead of in-process
state, since $$ resolves to the invoking shell's PID even inside the
subshell. The real cleanup trap is armed once at source time (always the real
caller, never a subshell) for EXIT, INT, and TERM. A best-effort orphan sweep
on next source reaps marked fixture roots old enough to be from a killed prior
run.
Simplifies the two existing ad-hoc workarounds (fm-procevent.test.sh,
wake-helpers.sh) back onto the shared helper now that it works correctly.
* no-mistakes(review): Preserve live fixtures during orphan reaping
* no-mistakes(review): Harden fixture ownership against PID reuse
* no-mistakes(review): Secure cleanup registry against path precreation
* no-mistakes(review): Make fixture registration transactional
* no-mistakes(document): Documentation already matches fixture cleanup behavior
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
* feat(herdr): enable presentation spaces by default (#1708)
* feat(herdr): default presentation spaces on with an explicit opt-out
Herdr's disposable one-task presentation workspace was opt-in through the
presence of local config/herdr-presentation-spaces. It is now on by default,
and a home opts out by writing "off" into that same file.
Values are read with the whole-file whitespace-stripped convention the other
scalar config items already use, plus case folding. An absent file, an empty
file, and "on" all resolve on; only "off" opts out; an unrecognized value warns
and keeps the default rather than failing a spawn over a purely visual setting.
The empty file is exactly the historical opt-in form, so every home that had
already enabled the projection stays enabled with no migration step, and no
previously enabled home can be turned off by the flip.
Because absence now means on at both ends, secondmate inheritance needs no
item-specific convergence: mirroring an absent primary file converges a
secondmate to the same default-on rather than turning its projection off, and
only an explicit primary opt-out propagates the opt-out.
The gate itself moves into fm_backend_herdr_presentation_enabled in the Herdr
adapter so the semantics have one owner that regressions can exercise directly.
* no-mistakes(document): Document Herdr default-on presentation safety
---------
Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
* fix(bin): surface fleet-wide open decisions on every wake drain (#1711)
* fix: surface consolidated open decisions on every wake-drain
A needs-decision or blocked event buried under later, unrelated status
appends was only ever shown via the last-line wake annotation, so a
still-open captain decision could go silently missed even though
status_open_decisions (fm-classify-lib.sh) already folds the whole
status stream correctly and fleet-snapshot/bearings already reuse it.
Wire that same fold into bin/fm-wake-drain.sh: a new fleet-wide
scan_open_decisions wrapper scans every state/<id>.status, and
fm-wake-drain.sh prints a separate, bounded OPEN DECISIONS section on
every drain (including the empty-queue fast path), so session-start
and every wake-handling turn surface it for free without duplicating
the open/resolved fold itself. Heartbeat wakes drain through the same
script, so this covers that surface too.
Also tighten status_open_decisions' file guard to skip an unreadable
status file instead of leaking a bash redirection error, now that a
fleet-wide directory scan can reach files a single targeted read
would not.
* no-mistakes(review): Prevent status symlinks leaking open decisions
* fix: drop unbounded perl subprocess from status symlink guard
The review step's own symlink-safety auto-fix (O_NOFOLLOW read via a
perl subprocess) forked one perl process per status file scanned by
the new fleet-wide open-decisions scan, with no cap - inflating
fm-wake-drain.sh's total external-read cost from 8 (the existing
annotation read_cap) to 18 in the enrichment-caps regression test.
The plain [ -L "$f" ] check already rejects any status file that is
itself a symlink before any read happens, which is exactly what the
new regression test exercises and is the same defense level the
sibling scan_captain_relevant_statuses/last_status_line already rely
on elsewhere in this file (no O_NOFOLLOW). Drop the subprocess-based
nofollow read and keep the cheap builtin guard.
* no-mistakes(document): Document actionable fleet-wide open decision drains
* fix(bin): abort parked runs and reap leaked processes before teardown (#1710)
* fix(bin): abort orphaned no-mistakes runs and reap leaked processes at teardown
Teardown could remove a task's worker while its no-mistakes pipeline run was
still parked at a gate, leaving an orphaned run holding a fleet slot
indefinitely (observed 2026-08-03: runs parked 7h39m and parked at a
post-CI approval gate). It could also leave backgrounded/disowned
descendant processes rooted under the worktree or tasktmp surviving
reparented to init (observed: two `go test` binaries pinning CPU for
hours with no live task meta to attribute them to).
Add two coupled pre-teardown steps, both scoped to this task's exact
branch/head or worktree/tasktmp so they can never touch another task's
run or processes:
- conclude_task_no_mistakes_run aborts a run parked at a gate via
`no-mistakes axi abort`, cd'd into the exact worktree so the daemon
resolves the run itself rather than teardown naming a --run id.
- reap_task_worktree_processes sweeps for processes whose cwd is under
the worktree or tasktmp (via `lsof -a -d cwd`) and TERM/KILLs them.
Both run before any worktree return, branch delete, or backend kill,
and are idempotent on a retried teardown. The branch+head attribution
logic is factored out of bin/fm-crew-state.sh into the new shared
bin/fm-nm-run-lib.sh so both scripts use the same ownership contract.
* no-mistakes(review): Fail closed on incomplete teardown cleanup
* no-mistakes(review): Bind teardown cleanup to verified run and process identities
* no-mistakes(review): Require confirmed aborts and convergent identity-safe process reaping
* no-mistakes(review): Handle process exits during teardown identity checks
* no-mistakes(review): Restore teardown library in hermetic gotmp fixtures
* no-mistakes(document): Document teardown run attribution and timeout
* no-mistakes(lint): Rename shell variable conflicting with done keyword
* no-mistakes: apply CI fixes
* fix: resolve fm-remote-entrypoint.sh SCRIPT_DIR through a PATH symlink (#1709)
The script installs as a symlink under ~/.local/bin. Taking dirname of
the symlink itself (instead of its real target) pointed SCRIPT_DIR at
~/.local/bin, breaking sourcing of the sibling fm-remote-job-lib.sh.
Resolve the real path first, preferring python3's os.path.realpath,
then realpath, falling back to the raw BASH_SOURCE on hosts with
neither.
* fix(pi): gate Calm built-in overrides by activation state (#1724)
* fix(pi): stop Calm claiming a built-in tool name another extension owns
fm-calm.ts claimed bash/read/edit/write/grep/find/ls unconditionally at
extension load, regardless of whether Calm was on. Pi resolves two
extensions registering the same built-in name by first-registered-wins
with no merge and no unregister call, and Calm's project-local
.pi/extensions/ position beats any global or CLI-configured extension,
so a user who never even enabled Calm could have their own bash/read/etc
override silently replaced.
Captain-approved plan implemented:
- Registration is now gated on config/calm already being "on" at load
time. A Calm-off session or reload registers nothing, so a non-Calm
user never contests a name. This stays synchronous during the
factory's own load, not deferred to session_start: /reload (and
ctx.newSession/fork/switchSession) render the restored transcript from
a pre-session_start snapshot of the tool registry, so a deferred claim
would miss that render - confirmed by tests/fm-calm-pi-extension
.test.sh's hidden-block-geometry E2E when trialed.
- The first time Calm turns on in a session that started off
(activateBuiltInsIfNeeded, from the /calm command handler), Calm calls
pi.getAllTools() - safe only once every extension has finished loading,
unlike the load-time path above - to see whether a different extension
already owns a name, and skips claiming only that one, leaving it and
its owning extension fully intact and callable.
- A contested name found this way prints a prominent ctx.ui.notify()
warning naming the tool, plus a console diagnostic.
- reportBuiltInLosses() remains the backstop for the one case neither of
the above can reach: a session that starts or reloads with Calm already
on, where the registry snapshot is taken before Calm gets any chance to
check ownership. A symlink-safe realpath comparison avoids misreporting
Calm's own registration as foreign when its path crosses a symlink
(macOS /tmp, /var).
Confirmed, bounded trade-off: the very first time a session that started
Calm-off turns Calm on, tool-call rows already on screen from before that
toggle do not retroactively collapse, because Pi never lets an extension
re-point an already-rendered row at a definition registered later. Every
session after that first toggle starts with the preference already on and
takes the synchronous load-time path, so the guarantee is intact from
then on. docs/calm.md and the file's own header document this in full.
tests/fm-calm-pi-extension.test.sh gains test_builtin_gate_load_time
(config/calm off registers nothing, on registers all 7 synchronously at
load) and test_calm_activation_collision_and_regression_bound (first
activation claims every uncontested built-in, leaves a foreign bash tool
fully intact and callable, warns and logs the contested name, and locks
in the documented pre-activation bound against real ToolExecutionComponent
rendering). test_rendering_and_session_lifecycle and the live interactive
E2E are updated for the new gate-at-load and first-activation-bound
contract.
* no-mistakes(document): Document Calm tool collision boundaries
* no-mistakes: apply CI fixes
* fix(bin): persist secondmate parent bindings for cleanup (#1727)
* fix(bin): give secondmate homes a durable parent binding record
Finished-worker cleanup on a remote second mate refused forever with
"cannot resolve the primary home ... durable parent binding". The
remote launch hands the child the remote code checkout as its parent
home (fm-spawn.sh's sole writer of FM_PUBLIC_FOLLOWUP_PRIMARY_HOME
receives FM_HOME=$FM_ROOT from fm-remote-secondmate-control.sh's
host-local launch), and that path can never carry the parent's real
records, so the guard refused unconditionally once relay looked active
anywhere on that host.
fm-home-seed.sh and fm-remote-home-provision.sh now write a durable
.fm-secondmate-parent record next to the .fm-secondmate-home identity
marker, naming the home's route to its parent as local (with the real
parent path) or remote (with the parent's SSH alias for diagnostics
only). fm-teardown.sh's cleanup gate reads it: a remote parent is out
of scope for the delegated-promise check (the whole promised-public-
reply subsystem is same-filesystem by construction, so a remote parent
can never hold one), while a token committed directly to the child's
own .env file - never the process environment - still refuses, so an
unrelated export in the remote host's login shell can no longer mask
in. For a local secondmate, the durable parent_home now also backs up
the launch-time env var, closing a silent fail-open where a restart
that dropped the launch prefix made the guard treat a genuinely active
parent relay as off.
Regression coverage drives the real remote route (SSH boundary + Herdr
fixture) and real fm-home-seed.sh seeding rather than hand-crafted
markers.
* no-mistakes(review): Captain: fail closed on unsafe durable parent records
* no-mistakes(review): Captain: enforce durable parent binding commit protocol
* no-mistakes(review): Captain: publish local parent binding before identity
* no-mistakes(review): Captain: refuse conflicting local parent bindings
* no-mistakes(review): Captain: reject non-regular secondmate seed leaves
* no-mistakes(review): Captain: enforce unique durable parent bindings
* no-mistakes(review): Captain: reject route-incompatible durable parent fields
* no-mistakes(document): Document durable secondmate parent bindings
* no-mistakes(lint): Fix secondmate parent parser ShellCheck warnings
* no-mistakes: apply CI fixes
* feat(bin): enforce latest AXI-family tool floors (#1733)
* feat(bin): gate lavish-axi at its session_ended floor in bootstrap
bin/fm-procevent-lavish.sh decides that a human "Send & End" review is
terminal by reading session_ended from the poll response's leading session
block. That field first shipped in lavish-axi 0.1.35, so an older installed
build silently leaves every ended review source armed forever and captures
an empty ended result on each later cycle. The same release is what makes a
plain reopen refuse a session the human deliberately ended.
Add LAVISH_AXI_MIN=0.1.35 to the existing axi-family floor structure in
bin/fm-bootstrap.sh, reusing tool_version_at_least and the same MISSING
diagnostic gh-axi already emits, so an incompatible build is reported as an
upgrade request before any review surface is armed. Later lavish-axi
releases only add artifact-authoring surface the adapter never reads, so
the floor is the feature-introduction point rather than latest.
Fixtures that stubbed lavish-axi as a bare exit-0 tool would now be read as
unparseable builds, so tests/lib.sh gains fm_fake_version_tool and every
bootstrap-running suite uses it for lavish-axi.
* no-mistakes(review): Clarify lavish-axi version floor rationale
* no-mistakes: apply CI fixes
* feat(bin): set axi-family floors to current latest under the bump policy
The axi-family bootstrap floors are the CURRENT LATEST published version of
each tool, captain-bumped periodically to move the whole fleet onto the
newest axi tools. They are not the minimum feature-introduced version. The
earlier lavish-axi work set a feature-minimum floor, which is the opposite
of this policy, so replace it along with the older feature-minimum rationale
carried by tasks-axi and quota-axi.
State the policy explicitly in bin/fm-bootstrap.sh's header, which owns it,
and in each per-tool floor owner, so no future change argues a floor back
down to the earliest release that happens to satisfy some behavior. Remove
the lavish-axi session_ended and upstream-PR citation, the tasks-axi
multi-ID-mv minimum argument, and the quota-axi credential-source argument
as floor rationale; the tasks-axi feature probes remain as a separate
defense-in-depth concern.
Floors: lavish-axi 0.1.45 (was 0.1.35), tasks-axi 0.2.4 (was 0.2.2),
quota-axi 0.1.17 (was 0.1.16), gh-axi 0.1.29 unchanged and already latest.
Each was verified against the tool's current published version.
The mechanism is unchanged: the same shared version helper and the same
MISSING diagnostic path. The below-fires and at-or-above-silent regression
rows move to the new floors, keeping each boundary genuine by pinning the
patch immediately below each floor rather than a version that was only
below the old one. Fleet fixtures move to the new floors so a bootstrap-
running suite is not reported as an out-of-date build.
Three operator-facing backlog handoff and receipt errors named "0.2.2+"
while the enforced floor moved, so they now point at the floor's owner
instead of duplicating a version number that drifts.
* no-mistakes(review): Centralize AXI floor policy beside constants
* no-mistakes(review): Clarify bootstrap boundary test comment
* no-mistakes(document): Centralize AXI floor policy rationale
* fix(bin): bound open decision scans with incremental cursors (#1737)
* fix(bin): bound OPEN DECISIONS scan cost with a per-status-file cursor
The fleet-wide OPEN DECISIONS scan added in #1711 re-reads and refolds
every task's entire lifetime status log on every drain, so its cost
grows unbounded with total log size. Add status_open_decisions_incremental
and scan_open_decisions_incremental to fm-classify-lib.sh: they persist a
per-status-file byte cursor plus the folded open-decision set, and fold
only newly appended bytes on each call, reusing status_open_decisions'
exact fold-line rule (extracted into _fm_decision_fold_line) so the two
strategies can never disagree on what is open. A missing or invalidated
cursor (new task, truncated/rewritten/shrunk log) falls back to a full
re-fold. bin/fm-wake-drain.sh now calls the incremental wrapper instead
of the whole-file scan.
* fix(bin): add O(1) rotation detection and read-failure guarding to the cursor fold
Add the two pieces the incremental open-decisions cursor was missing,
scoped to this repo's actual status-file usage (create-once, append-only,
never replaced or rewritten in place):
- An O(1) device+inode identity check (one stat call) alongside the
existing size-shrink check, so a status file replaced/rotated/recreated
at the same path is detected and falls back to a full re-fold, even
when the replacement is the same size. A same-inode, same-size,
in-place byte edit is a deliberately accepted gap: no code path in
this repo ever does that to a status file.
- Checked reads: a stat/wc/tail failure is a genuine I/O error, not
"the file is empty" - it now reports the already-trusted persisted
open set unchanged instead of risking a silent invalidation.
Both stay O(1) plus new bytes per call, matching the cursor's bounded-
cost design; no content hashing or pending-fragment machinery.
* no-mistakes(review): Preserve cursor state across failed incremental reads
* no-mistakes(review): Refold status when cursor cache reads fail
* no-mistakes(document): Document cursor-backed open-decision scanning
* no-mistakes: apply CI fixes
* fix(bin): prevent remote polls from blocking session startup (#1754)
* fix(bin): preempt remote reply long-polls for queued short jobs
Session start on a home with live remote second mates could stall silently
for many minutes: the single serial remote job worker ran each armed
fm-remote-delta-read.sh reply poll to its full 55s window while bootstrap's
short sync, inherit, state, and route commands sat queued behind it, and
non-FIFO queue pickup let re-armed polls keep winning the lane. Measured
end to end, a trivial short job took 31s behind one 30s poll window.
The worker now preempts a running preemptible job (the read-only, cursor-
anchored delta read is the only member of that class) as soon as a
non-preemptible job is queued, publishing exit 75 with emptied output -
byte-identical to the poll's own elapsed-window-with-no-data result - so
the parent runner takes its existing no-result path and the watcher re-arms
from the same cursor with nothing lost. The delta read translates SIGTERM
into that same exit after removing its staging directory. Sibling polls
never preempt each other, so two armed monitors cannot churn. The same
measured scenario now completes in 1s.
* no-mistakes(document): Clarify remote poll preemption documentation
* docs: present X mode as the X and Discord public surface (#1778)
Discord mentions already ride the same pairing-token opt-in, relay poll,
and platform-aware reply path as X mentions, but the docs still read as
X-only, so a stranger could not self-serve the Discord path.
Add the numbered turn-on steps to the X mode configuration reference,
pointing at the myfirstmate dashboard for account creation, bot install,
and token issuance rather than duplicating operator setup here, and drop
the X-only framing from the README bullet, the documentation index, and
the architecture overview.
* fix(bin): run session start deterministically from hooks (#1781)
* feat(bin): run session start deterministically on hook-capable harnesses
Session start relied on a native nudge that only asked the agent to run
bin/fm-session-start.sh, and an agent can defer that. Observed 2026-08-01:
an /ahoy-first session followed the recap path and did not take the helm
until a later request forced it.
Claude, Codex, and Pi now RUN the digest in their session-open hook through
the new bin/fm-sessionstart-run.sh, so the full ordered digest is in model
context before the first turn. That wrapper is the single owner of what a
session-open source means: startup and Pi's "new" take the helm, clear and
compact re-emit, resume/reload/fork delegate to the nudge, and an unreadable
source takes the helm because doing that redundantly is idempotent while
skipping it is the bug. Grok and OpenCode keep the nudge as the floor, since
neither can carry hook stdout into a model turn.
Because the hook now blocks session initialization, fm-session-start.sh
bounds itself first. Its steps are not all individually bounded - bootstrap's
gh auth probe, tool version probes, the backlog listing and per-task endpoint
reads are unbounded - so the whole digest runs as one bounded child (default
120s). Whatever it emitted before the bound survives, and the parent adds a
loud STARTUP TRUNCATED banner naming the stage that stalled and every stage
that never ran, still exiting 0.
--reemit skips only the sweeps startup already reconciled. It still re-verifies
lock ownership and still drains queued wakes, which arrived after startup and
are the turn's work. fm-bootstrap.sh gains FM_BOOTSTRAP_LOCKED so a re-emit
keeps repair ownership instead of deferring to a lock holder that is itself.
Also adds bin/fm-timeout-lib.sh as the single owner of bounded execution,
replacing three near-identical copies, and gives the ahoy skill a helm check
so a nudge-tier harness cannot recap before taking the helm.
Verified live on 2026-08-05 against Claude 2.1.222, Codex 0.146.0, and Pi
0.82.0; docs/verification/supervision.md records the per-harness source
vocabulary, the two named gaps, and the refresh command.
* no-mistakes(review): Harden session-start completion, timeout, and Pi delivery
* no-mistakes(review): Harden completion ownership and portable timeout escalation
* no-mistakes(review): Normalize watchdog KILL exits without masking command status
* no-mistakes(review): Guarantee startup bounds and align harness delivery tiers
* no-mistakes(test): Fix Pi session-start live verification fixture
* no-mistakes(document): Align session-start documentation with deterministic hooks
* no-mistakes(lint): Silence intentional child-shell expansion lint warning
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
* fix: rename X mode to Relay in user-facing docs (#1784)
* docs: rename the user-facing product name to Relay
The public-mention integration gated by the `.env` pairing token is now
called Relay across user-facing prose, covering X and Discord alike
instead of implying a single network.
Renames the product-name strings only: README, docs, the captain-facing
skill descriptions, and the AGENTS.md operating prose, including the
`X mode (.env)` and `Optional X mode` headings and every link anchor
that pointed at them. AGENTS.md section 14 carries a one-line bridge
note so the older name and the unchanged identifier spellings stay
discoverable.
Internal identifiers are untouched: `FMX_*`, `config/x-mode.env`,
`state/x-*`, `bin/fm-x-*`, the `fmx-respond` skill path,
`__FM_X_MODE_ENV__`, and `x-mode-error`. Platform references to X and
Discord as networks stay as they are, and the bootstrap-diagnostics
entry still quotes bootstrap's emitted `FMX: X mode on/off` line
verbatim because `bin/` output is out of scope for this pass.
* no-mistakes(review): Complete Relay prose rename in maintained docs
* no-mistakes: apply CI fixes
* feat: add Muse Code crewmate adapter (#1786)
* feat(harness): add a verified muse crewmate adapter
Muse Code joins the fleet as a crewmate/scout adapter, verified live against
Muse Code 0.1.0-R708.1 in an isolated lab.
Detection matches the anchored prefix muse-bin*, because the installed launcher
execs a version-suffixed binary whose name changes on every auto-update and
whose install path carries no muse component to fall back on. The same identity
is taught to the tmux liveness classifier, without which a healthy muse pane
would have read as a dead endpoint.
Busy state folds muse's own durable session event log, bound per task by a
sessions-root/worktree sidecar. It is a pull source with no writer, so nothing
is armed and no record is ever seeded. The fold is anchored on the full run
lifecycle prefix so muse's nested cleanup "terminal" payloads cannot settle an
in-flight run, and it is depth-bounded so muse's native sub-agent logs cannot be
mistaken for the parent's. The idle half stays gated: an open run proves busy,
but a settled log reads unknown until a credentialed multi-step run proves one
turn stays inside one run.
Two findings corrected the scout report. The exec-only
--no-foreign-personal-context flag is rejected by the interactive TUI, so the
privacy control that actually reaches a pane worker is
MUSE_EXPERIMENTAL_FOREIGN_PERSONAL_CONTEXT_KILL, verified to drop the operator's
foreign personal rules while keeping the project's own AGENTS.md. And an
unauthenticated muse pane never exits, it waits on a device-code prompt, so
credentials are a spawn preflight rather than a screen check.
muse is refused for secondmates: it has no primary supervision protocol and its
hook dialect rejects the reawakening handlers that protocol needs.
Per the captain's decision, auto-update is not pinned, and the credentialed
multi-step smoke is deferred with an explicit checklist in
docs/verification/muse.md.
* no-mistakes(review): Accept Muse dispatch profiles and shared efforts
* no-mistakes(review): Bind Muse busy state to current session
* no-mistakes(review): Compare Muse workspace bindings literally
* no-mistakes(review): Harden Muse worker credentials and live signal verification
* no-mistakes(review): Cache Muse session bindings and clarify worker credentials
* no-mistakes(review): Clear Muse marker inheritance and normalize interrupt aliases
* no-mistakes(review): Verify Muse glyph effective foreground color
* no-mistakes(review): Harden Muse XDG paths, session cache, and glyph parsing
* no-mistakes(document): Document Muse adapter boundaries
* fix(herdr): require 0.8.0 for default presentation spaces (#1787)
* fix(herdr): floor default-on presentation spaces at Herdr 0.8.0
Default-on presentation projection turns every crewmate teardown into a
workspace-emptying removal. The focus-safe removal plan avoids Herdr's
focus-stealing explicit close only while the doomed pane's shell can be proved
lone, childless, and idle; a persistent child of that shell (gitstatusd, a
zsh-async worker, direnv) fails that proof permanently and forces the plain
close, which on every release before Herdr 0.8.0 moves the captain's active
workspace for ~140ms on each teardown.
Gate the unconfigured default behind a Herdr 0.8.0 floor. At or above it,
project as before; below it, fall back to the flat per-home layout with one
warning per home per detected release naming the version and the upgrade. An
explicit "on" - including the historical empty opt-in file - is still honored
below the floor, so a deliberate opt-in is never silently downgraded.
The floor reads two independent signals from the client's own status, either of
which can establish a supported release: the protocol number and the release
core of the version string. Measured against the real release binaries, no build
lacking both upstream focus fixes reaches protocol 19 and every pre-fix build
tops out at 17, so protocol 19 is a safe structural expression of the floor. A
release that reports neither signal readably is treated as unsupported rather
than guessed at.
Also:
- Correct the adapter comment claiming the mitigation "stays safe without any
version gate". That holds for the pane-death route only; the plain-close
fallback is reachable precisely on the releases where it is unsafe.
- Stop discarding the projected-close helper's stderr at teardown, so a refused
or failed focus restore is visible instead of silent. The close stays
non-fatal; the presence gate still decides record removal.
- Add Part C to the focus-flash regression: a doomed pane whose shell holds a
persistent child, in the geometry where the closing workspace's right
neighbour is not the anchor. That is the fallback branch the suite could not
structurally reach. On 0.7.5 it observes a bounded four-sample wrong-focus
window restored exactly; on 0.8.0 it observes none. It also cross-checks its
own measurement against the floor classifier, so a drifted protocol mapping
fails loudly.
- Make the projection suite's unconfigured-home case release-aware, so the whole
real-Herdr lane passes on both the CI-pinned 0.7.4 and 0.8.0.
- Add an opt-in live guard that re-measures the release-to-protocol mapping
against the pinned upstream binaries.
The immediate no-code mitigation for a home that cannot upgrade remains writing
"off" into config/herdr-presentation-spaces.
* no-mistakes(review): Pin Herdr live-guard digests across supported platforms
* no-mistakes(review): Document authorized Herdr cleanup containment
* no-mistakes(review): Harden Herdr warning marker publication
* no-mistakes(review): Honor running Herdr server presentation floor
* no-mistakes(review): Recheck Herdr floor after server ensure
* no-mistakes(review): Refresh 0.7.5 and 0.8.0 focus transcripts
* no-mistakes(review): Route Herdr floor probe through lab session
* no-mistakes(document): Align Herdr floor documentation and comments
* no-mistakes(lint): Document Herdr presentation out-parameter consumer
* fix(bin): classify settled Muse session logs as idle (#1788)
* fix(muse): trust the settled session log as idle
The credentialed multi-step smoke on Muse Code 0.1.0-R708.1 answered the one
question the idle half was held back for: one real 75-second tool-loop turn with
23 tool batches stays inside exactly one run started/terminal pair, and an
Escape mid tool loop closes that run as cancelled rather than leaving the turn to
continue in another run. A settled log is therefore a finished turn, not a pause
between the runs of one turn.
Remove fm_busy_muse_idle_verified and FM_BUSY_MUSE_IDLE_VERIFIED_VERSIONS
outright rather than pinning them to a version: the session log's own metadata
carries only semver 0.1.0 and a build sha, so a version allowlist could not
actually match the running build and would be false precision. A settled log now
classifies idle, an open run still classifies busy, and only a resolution
failure - no binding, no matching log, an unreadable or run-free log - stays
unknown.
Record the evidence in docs/verification/muse.md, including the run-scoped grep
the counts must use, and keep the post-upgrade re-check guidance.
* no-mistakes(review): Document Muse idle trust and remove stale gate reference
* no-mistakes(document): Clarify Muse idle verification ownership
* docs(agents): read the persisted digest when only a preview is shown (#1794)
* fix: preserve fleet state in truncated session-start digests (#1798)
* feat(session-start): order the startup digest for truncation safety and bound its bulk
The digest is delivered through a harness that truncates an oversized payload
from the tail, and it really has been truncated: a 70KB digest arrived as lines
1-435 of 578, cutting off eight lines before the live-task inventory. That
session took the helm without ever seeing which tasks were live or where their
endpoints were.
Three changes, one file's worth of composition:
- FLEET STATE is emitted before CONTEXT, so a truncated tail drops curated
memory - stable session to session, already governed by a captain-set budget,
recoverable with one targeted read - instead of live fleet identity. The
LOCK/BOOTSTRAP/WAKE-QUEUE safety preamble keeps its order. The read-once
contract moves out of the closing reminder into its own section ahead of both,
and now names the condition that voids it: a stage the truncation banner
reports as never emitted.
- Status-tail lines are capped per line, reusing the cut the wake digest's OPEN
DECISIONS section already applies. An observed tail line ran 865 characters
and nothing bounded it. The cut and its marker now live in one place,
bin/fm-line-cap-lib.sh, so the two digests cannot drift apart; each task's
full status log path is still printed beside its tail.
- The backlog listing is composed as a recovery input: done rows are never
listed, every in-flight, held, and blocked row is shown in full with its hold
and blocked-by metadata, and only the dispatchable-now listing is bounded -
with an exact remainder count and the command that shows the rest.
FM_SESSION_START_QUEUED_LIMIT (default 20) replaces
FM_SESSION_START_BACKLOG_LIMIT, which bounded the whole listing
indiscriminately and so could drop a held or blocked row.
Tests exercise the real digest output: section ordering with the preamble
pinned, the per-line cap and its marker, and the backlog composition including
the remainder counters on both the tasks-axi and manual paths.
* no-mistakes(document): Clarify digest source recovery comments
* feat(send): close answered decisions at answer time via --resolve-key (#1842)
A captain decision opened by a keyed needs-decision:/blocked: status line
orphaned as permanently open whenever the answer kicked off work: the
worker's next event is working [key=<workstream>] in a different key
namespace, so no resolved [key=<decision>] ever landed and the OPEN
DECISIONS fold kept listing the answered decision forever.
Remove the writer-dependency at its source: the answering firstmate
already holds the decision key when it sends the answer, so fm-send's new
--resolve-key flag (repeatable) appends the closing resolved line to this
home's own state/<id>.status after the submit is confirmed. The close is
a local ledger append for crewmates, local secondmates, and remote
secondmates alike - a remote mate's escalations reach this ledger through
the parent-replies ingest, so only the answer message crosses the
transport.
Safety: each named key must currently be open per the authoritative
status_open_decisions fold or fm-send refuses before sending; a failed or
unconfirmed send never closes a key; an append failure after a delivered
answer exits nonzero with the manual close command so the decision
re-surfaces instead of silently vanishing; a send without the flag closes
nothing, and working:/done: still never clear a captain decision.
Complementary fixes: the wake-drain OPEN DECISIONS section prints the
answer-with-close command hint at the moment of use; brief scaffolds
separate resolved's two duties (keyed-phase end vs decision closure) and
state that a done:/working: line never closes a decision even when the
answer started that work, keeping worker self-close for blockers that
clear without a firstmate reply; AGENTS.md and docs/architecture.md carry
the one-line pointers to the fm-send contract.
* fix(bin): seed remote secondmates from supplied origins (#1836)
* feat(secondmate): seed a remote home from a supplied project origin
Remote seeding required a local projects/<name> clone purely to read
`git remote get-url origin` into the provisioning manifest, so setting up
a remote second mate forced disposable clones and no-mistakes inits in the
primary home for projects that home has no reason to hold.
Firstmate now resolves the origin itself and names it as <project>=<origin-url>.
The seed validates and transports what it is given, and the receiving host
…
kyokosawada
added a commit
to kyokosawada/firstmate
that referenced
this pull request
Aug 14, 2026
Ship briefs have required --mode, and ship spawns --mode and --yolo, since 4ee4a0a (kunchenguid#1563), which is already in this fork's merge base. Local test cases added afterwards call fm-brief.sh and fm-spawn.sh without them, so they exited 1 on the argument gate and never reached the behavior they assert. They have never passed on this fork. Supply the missing arguments rather than relaxing the gate: the contract is deliberate and separately covered by test_ship_mode_is_explicit_not_registry, which asserts the scaffold must not consult the registry at all. No production behavior changes. fm-brief.test.sh: every ship invocation now names its mode, and the two loops that encode a flag per case carry it there so their scout entries stay untouched. brief-ws-yolo-w6 uses local-only because it asserts that mode. fm-task-repos.test.sh: run_spawn_case passes mode and yolo for every case, and the batch refusal case passes them so it reaches its own --also-project diagnostic instead of stopping on the mode gate.
bramdokman
added a commit
to bramdokman/firstmate
that referenced
this pull request
Aug 15, 2026
* docs: define captain instruction precedence (#1362)
* docs: add captain-authorized inherent red-check merge exception
Keep the default red-PR ban and own one always-loaded exception in the
merge-authority section: captain-explicit PR or bounded batch plus exact
check, only when the failure is inherent to the selected delivery path.
Yolo cannot activate it; final head and the full current check suite must
be verified; other substantive failures remain non-waivable.
* docs: replace narrow red-check exception with captain precedence
Supersede the inherent failing-check merge exception with one always-loaded
Firstmate-local rule: a current explicit concrete captain instruction
overrides a conflicting Firstmate-written standing rule only within exact
scope, never above platform/system/developer instructions. Keep the ordinary
red-PR default and yolo boundary; point section 7 at the section 1 owner.
* docs: define validation supersession sequence (#1407)
* fix: give validation-time captain overrides a supersession sequence
The Validate section let a captain instruction that completely
invalidates the work being validated keep the same task and worker, but
never said how: the adjacent rule flatly bans hand-editing, committing,
aborting, or restarting during an active run with no carve-out, so a
worker facing full invalidation had no sanctioned path forward.
Add the missing sequence: cancel through no-mistakes axi's abort
command, confirm the run has stopped through axi status, recover branch
ownership through axi sync's guarded recovery, only then replace the
obsolete work, and validate once against the final head. The existing
ban on hand-editing an active run now cross-references this sequence
instead of contradicting it.
* no-mistakes(review): Make validation custody recovery conditional
* no-mistakes(document): Clarify validation supersession abort exception
* fix: keep obsolete pipeline commits out of the superseded deliverable
The review-applied fix made custody recovery conditional on
branch_sync.next_action.code, but left an open gap: recovering custody
settles who owns the branch, not what content ships. As written, a
worker could recover an obsolete run's branch and build the
replacement on top of its now-irrelevant commits instead of from the
correct pre-invalidation base, carrying obsolete content into the
final deliverable.
Make that explicit: custody recovery settles ownership, not content,
so the worker replaces obsolete work from the correct base and keeps
the obsolete run's commits out of what gets validated and shipped.
* no-mistakes(test): Restore minimal pre-invalidation replacement instruction
* fix: dedupe redundant "replace the obsolete work" restatement
Line 309 already says the worker replaces the obsolete work from the
correct pre-invalidation base, excluding the obsolete commits. The
closing sentence restated "replace the obsolete work" again before
gating the final validation run, layering the same fact twice instead
of stating it once.
Trim the closing sentence to just the ownership gate and the
single-run-against-final-head requirement it uniquely adds.
* fix: bind backend overrides to exact-task authority (#1413)
* fix: bind explicit --backend to exact-task authority
A Herdr-backed second mate carried a prior one-task --backend tmux
exception forward by analogy, so its child landed in tmux and never
appeared under the second mate in Herdr. Runtime detection was correct;
the authority surface was not.
docs/configuration.md now owns that an explicit --backend is authorized
only for that exact task. AGENTS.md and fm-spawn help point there.
* no-mistakes(document): Consolidate backend selection authorization documentation
* fix(herdr): prevent focus flashes during projected workspace cleanup (#1229)
* fix: remove projected workspaces through Herdr's focus-preserving pane-death path
Herdr 0.7.5's explicit close of a workspace-emptying last pane moves the
attached client's focus to a neighbor workspace, flashing the captain's
whole window and routing in-flight keystrokes to the wrong pane until
Firstmate's exact-tab restore masks it 56-197 ms later.
Teardown and cleanup now plan a workspace-emptying close as a focus-safe
removal: verify the close empties the workspace, reposition the doomed
workspace behind the focused one through the verified workspace.move
transport when it sits before a non-last focused workspace, prove the pane
holds one lone idle shell, and end that shell so Herdr removes the emptied
workspace through its focus-preserving pane-death path. Any ambiguity or
failure falls back to the plain close behind the existing restore backstop,
and fm_backend_herdr_kill applies the same plan for non-projected removals.
Two conditions proven on real hardware are encoded in the adapter: BSD ps
reports a login shell's comm as "-zsh", and an idle shell transiently
hosts a prompt helper right after a workspace.move relayout, absorbed by a
bounded strict-sample settle window in the idle-shell proof, now the single
owner shared with session-start cleanup.
An isolated-lab regression reproduces the raw steal on 0.7.5 and proves the
plan removes a doomed workspace with zero wrong-focus samples and no
corrective focus; unit fixtures cover the position, edge, ambiguity, move
and kill failure, escalation, and transient-helper cases. Upstream fixes
(#1877 explicit close, #1912 pane death) are merged but unreleased; once
released the plan degrades to a harmless reorder-then-remove.
* no-mistakes(review): Confirm pane death from structured not-found responses
* no-mistakes(review): Serialize Herdr kills and sample focus continuously
* no-mistakes(review): Synchronize Herdr focus evidence output
* no-mistakes(review): Refuse unlocked Herdr pane closes
* no-mistakes(document): Correct Herdr focus-safety documentation
* no-mistakes: apply CI fixes
* fix: never erase a Herdr task's records while its pane survives a refused close
A transient presentation-lock contention could produce a completed teardown
while the exact Herdr pane stayed alive as an unowned restored shell: the
kill refused the unlocked close (correctly), returned success, the warning
was suppressed, and cleanup erased the task's status, turn-end, and
metadata records after the isolated copy had already been returned.
Teardown now acquires the named-session presentation lock before anything
destructive: a contended lock refuses up front while the isolated copy, the
task branch, every durable record, and the endpoint are all intact for a
plain rerun, and the projected and flat close paths both run under that one
held lock instead of acquiring their own. Durable records are erased only
once the exact pane is confirmed gone through its structured presence; a
refused, skipped, or failed close retains every record with a visible,
retryable error, and after a skipped close (unresolvable lock path) only a
structured pane_not_found counts as gone - unknown never does.
The teardown regression drives a live contending lock holder end to end:
the refusal touches nothing (no worktree return, no branch drop, no close
attempt), and the retry after release returns the copy, closes the pane
under the lock, and removes the records. The unconfirmed projected close
now refuses with records retained, and the structured-presence gate has a
strict/default unit matrix.
* no-mistakes(review): Require structured pane-not-found before Herdr record removal
* no-mistakes(document): Correct Herdr record-retention verification date
* fix: refuse ambiguity, revalidate SIGKILL ownership, and roll back failed removals
Three accepted-contract corrections from the post-CI personal review of the
Herdr keep-spaces focus-flash mitigation.
Ambiguous endpoint identity no longer counts as a confirmed-gone pane: a
missing or malformed target refuses record removal in the structured
presence gate, and teardown treats missing confirmation machinery as a
refusal instead of skipping the gate, so only an exact structured
pane_not_found ever erases durable task records.
The pane-death SIGKILL escalation re-reads the exact pane's process
information and refuses to signal unless the same shell pid still passes
the strict bare-idle ownership proof, so a pid that exited and was reused
by an unrelated process is never signaled; the refused escalation falls
back to the plain close with the unrelated process untouched.
A reposition whose removal is not confirmed no longer outlives the attempt:
the emptying-close plan records the verified pre-move order and original
index whenever it invokes the mover, and both close owners restore the
exact original workspace order through a second verified move, under the
same held session lock, before reporting the close as failed.
Each defect was reproduced first: the unit matrix documented malformed
identity as gone, the PID-reuse regression showed SIGKILL reaching a
disowned pid, and the rollback regression showed a single unrestored move.
Teardown-level regressions cover unparseable presence retention alongside
the strict identity matrix.
* no-mistakes(review): Require confirmed Herdr removal and resolvable teardown locks
* no-mistakes(review): Enforce structured Herdr closes and teardown preflight
* no-mistakes(review): Preflight explicit Herdr close confirmation helper
* no-mistakes(document): Document Herdr rollback failure semantics
* no-mistakes(review): Captain, harden recursive Herdr teardown safety
* no-mistakes(document): Document recursive Herdr teardown evidence
* fix: retain nested secondmate home when a recursive child cleanup fails
Captain-decided Option A correction for nm-askuser-flash-r6, found during
complete-diff rereview of the merged head.
cleanup_firstmate_home_children's recursive secondmate branch called
itself for a nested child's home without checking the result, then
unconditionally removed that home right after. remove_firstmate_home
ends in an unconditional recursive delete with no check for leftover
records, so a nested secondmate whose own Herdr grandchild failed its
confirmed-gone check would have its entire home - retained grandchild
records included - erased by the very next line.
Guard the recursive call the same way every other fallible call in this
function already is: || return 1, skipping remove_firstmate_home and
leaving the nested home and its records for a safe rerun.
Empirically, fm-teardown.sh's set -eu already halted the script on the
prior unguarded call before reaching removal (verified by hand with the
guard reverted, under both this session's bash and stock macOS bash
3.2) - the reachable behavior was already correct. The explicit guard
is still applied exactly as decided: it matches every sibling call site
in the function, and it stops the correctness of this path depending on
errexit's well-known fragility under refactors (a wrapping if/&&, or a
future subshell) rather than on an explicit check.
Adds a teardown-level regression building on the existing direct-child
Herdr fixtures: a top-level secondmate contains a nested secondmate,
whose own Herdr child's close goes unconfirmed. Proves through the
public fm-teardown.sh interface that the nested home, the nested
secondmate's own record, and the grandchild's metadata and status all
survive, and that the top-level secondmate's record survives too.
* no-mistakes(document): Document nested Herdr teardown retention
* fix: prioritize completion runway in quota-aware dispatch (#1431)
* fix(dispatch): prioritize quota completion runway
* no-mistakes(document): Document completion-aware quota runway selection
* fix(bin): preserve full task contract in no-mistakes intent (#1447)
* Preserve task contract in no-mistakes intent
* no-mistakes(review): Preserve complete current task contract in no-mistakes intent
* fix(bin): parse punctuated secondmate registry entries safely (#1452)
* fix: centralize secondmate registry parsing
* no-mistakes(review): Centralize secondmate registry binding validation
* no-mistakes(review): Harden registry EOF and symlink validation
* no-mistakes(review): Reject unreadable registries before parsing
* no-mistakes(document): Document punctuation-safe secondmate registry validation
* no-mistakes: apply CI fixes
* feat(bin): add durable process-event supervision (#1483)
* feat(procevent): supervise long-polling sources into durable events
Firstmate had no way to wait on a blocking external process without holding
a conversational turn. Add a domain-neutral process-to-event runner plus a
thin adapter around the currently published `lavish-axi poll` interface:
canonical physical source identity, one machine-wide owner per source, direct
argv execution, and durable 0600 result capture before any event referencing
it is published on the existing wake queue. No second notifier, no polling
control plane, and no retry machinery.
A captured result with no durable handled acknowledgement stays eligible for
bounded re-announcement across any number of drains and restarts. Draining a
wake before acting on it and then starting a replacement session resurfaces
the same exact source and sequence, and never puts result payload text in an
event line. `fm-procevent.sh handled <source-id> <sequence>` is the only thing
that stops re-announcement: generation-keyed, private, path-safe, durable, and
atomically idempotent, so a paired external effect gated on its first-time
versus repeat report is never authorized twice.
An acknowledgement is refused unless matching captured result and adapter
records already exist, so a premature or mistyped call cannot suppress a
future result.
The source side is unchanged and still lossy: the published poll clears
feedback destructively before returning it, so a result lost in that window
is unrecoverable. This is never at-least-once, no-loss, or lossless, and the
handled acknowledgement is not a generic exactly-once effect either - a crash
between an external effect and its acknowledgement can still repeat that
effect on replay.
Integrate registered sources with watcher supervision, the guards, and
recoverable secondmate teardown across nested homes, and cover source
identity, lifecycle races, supervision, restart handling, and cleanup safety
with regressions.
* no-mistakes(review): Prevent Lavish prompt text from spoofing missing sessions
* no-mistakes(review): Serialize publication and secure handled acknowledgements
* no-mistakes(document): Document hardened process-event acknowledgement guarantees
* fix(procevent): never reclaim a source whose owned group still runs
A runner is its own process group leader and starts the blocking source in
that group, but the claim records only the leader PID and its identity. If the
leader died while the source child kept running, the missing PID was
classified stale: reconciliation released the claim and started a second
runner while the old blocking source was still consuming the same canonical
source. For the Lavish adapter that means two destructive long polls racing on
one review session, so it is not harmless process litter. It also contradicted
the documented promise that ownership is never released until the whole group
is gone.
Ownership state now distinguishes a generation that is really gone from one
whose leader crashed with its group still alive. Reconcile stops that
surviving group and releases its exact generation before starting any
replacement, and keeps the claim for a later cycle when it cannot prove the
group stopped or another home owns it. Acquisition and `start` treat the same
state as held rather than reclaimable.
Signalling that group is safe precisely because only an absent leader reaches
this state. A reused PID leaves the leader alive, so the identity comparison
still classifies it stale or uncertain and no group signal follows, which
keeps the existing PID-reuse refusal intact.
Add a public-interface regression for the exact crash cut - SIGKILL only the
leader, prove the child group survives, reconcile, and prove the old group is
gone with no second source running - plus its counterexample that a generation
with no leader and no surviving group is still reclaimed. Update the runner
help, operating documentation, skill, and verification record where they
described reclaim in terms of the leader alone.
* no-mistakes(review): Enforce runner group ownership and detect poller overlap
* no-mistakes(review): Isolate runner groups from unrelated caller processes
* no-mistakes(document): Document isolated process-event runner launch
* no-mistakes(lint): Suppress Perl literal ShellCheck false positive
* fix(bin): retire terminal process events and surface queued wakes (#1500)
* fix(bin): deliver process-event results and retire ended sources
Two defects reproduced during a real Lavish adapter session.
One human `Send & End` produced four captured results: the real feedback,
then recurring empty ended sessions. The generic runner had no way to learn
a source was finished, so every reconcile restarted a poll that returned
immediately. The runner now asks the source's own adapter -
`fm-procevent-<adapter>.sh terminal <result-file>` - and on exit 0 alone
re-proves ownership, drops the registration, and releases its own claim
under one source boundary. Terminal knowledge stays adapter-owned: for
Lavish that is an ended session, a missing session, and the final feedback
delivery the published poll marks with `session_ended`. An adapter with no
terminal command keeps its source armed exactly as before. Capture before
publication, captured-result durability, queued wake durability, bounded
re-announcement, handled deduplication, one-owner ownership, and explicit
idempotent retirement are all unchanged.
A captured result queued its `check` wake durably, but a healthy watcher
with a fresh beacon never delivered it; the result surfaced only after a
manual drain. Publication happens outside the watcher (in the runner) or
unconditionally (in reconcile), so the watcher had no newly actionable
signal to report and never reached its rewake path. It now reports a
queued-but-unsurfaced process-event record through the same actionable exit
every other wake uses, deduplicated by the same `.seen-*` marker discipline
the signal scan uses, so the record is always durable before it is
suppressed. The durable queue remains the authority and no second notifier,
poller, timer, queue, or adapter-specific wake path is added.
Regressions cover both, driven end to end: an armed Lavish source against a
stand-in for the published poll polls once, captures once, publishes one
distinct event, and retires itself; two fixture adapters prove the terminal
decision follows the adapter alone; and a real capture plus a real watcher
prove one proactive wake before any drain, with no duplicate wake while the
record stays queued or after it is acknowledged.
* no-mistakes(review): Harden process-event retirement and proactive delivery
* no-mistakes(review): Route process-event delivery through shared wake owner
* no-mistakes(document): Clarify process-event delivery and retirement documentation
* no-mistakes(lint): Fix ShellCheck control-flow warnings
* no-mistakes(lint): Fix wake output status lint warning
* perf: shard portable serial tests across CI runners (#1544)
* perf(ci): shard the portable serial behavior lane across runners
The Behavior portable serial job ran all 69 scripts of the serial
remainder on one runner. The measured serial sum on run 30725985757 was
1143762 ms (19m04s) against a 20-minute timeout, so the job intermittently
reached the cap and was cancelled with every step passing. Setup is only
about 7s, so the cost is entirely test wall time.
Split the lane into four separate-runner shards. Each shard is still
strictly serial, and separate runners mean no two of these stateful
scripts ever share a machine, so the split needs no concurrency isolation
proof. Assignment is longest-processing-time bin packing over measured
per-script duration hints, balancing every shard to 285941 ms (~4m46s) of
expected work, and the timeout tightens from 20 to 15 minutes.
bin/fm-test-run.sh owns the shard count and refuses a lane whose "ofN"
disagrees with it, while ci.yml derives the same count from
strategy.job-total rather than a literal, so changing it in either file
alone fails the lane loudly instead of leaving part of the required suite
unrun. --check-coverage additionally proves the shards are non-empty,
disjoint, and exactly equal to the serial lane. No test is weakened,
skipped, or removed.
Also replace the wall-clock sleeps in the --jobs scheduler test fixture
with an explicit signal handshake between the fixtures. The old
0.5s-versus-0.05s race failed on a loaded machine; the handshake passes
under sustained CPU saturation.
* no-mistakes(review): Correct portable serial shard balance evidence
* no-mistakes(document): Document portable serial shard evidence accurately
* fix(bin): correct session lock and attached watcher supervision (#1545)
* fix(bin): identify harness sessions by path and report delivered wakes
Two supervision faults, both reported by a contributor and both open on the
default branch.
Fault 1: the Stop auto-arm never claims the home. fm_harness_ancestry_pid()
matched only the basename of `ps -o comm=`, and Claude Code's native installer
names the per-session executable by its version (.../share/claude/versions/
2.1.220), so that basename identifies nothing. Three real failure shapes follow:
a version-named session is missed entirely and the hook exits 0 with the epoch
never written (unconditional on Linux, where procps reports the kernel exec name
and ignores argv[0]); a claude-named daemon that directly parents sessions wins
the outermost-contiguous-claude rule ahead of the session itself; and a session
that is both version-named and daemon-parented has its live lock reclaimed as
stale and rewritten to the shared daemon pid, corrupting the home's ownership
record.
Harness identity now also reads whole components of the executable path and of
argv[0], which is what both platforms still carry. Matching whole components
only keeps that widening safe: bin/fm-claude-stop-autoarm.sh and ~/.claude/hooks
scripts have no "claude" component. Ownership is then decided against the
session's whole contiguous harness ancestry rather than one chosen pid, which is
the honest form of the question the library already documents ("does the current
process descend from that same harness?"). That subsumes the outermost-pid rule
for Claude's nested bg-spare worker chain instead of reverting it, and lets a
daemon-parented session recognize its own lock. Lock acquisition still writes the
outermost pid of the run, the only pid that lives as long as the session.
Fault 2: an attached arm reports a delivered cycle as FAILED. The watcher prints
its one reason line to its own stdout, so only the arm that forked it can read
that line; an arm that attached observes nothing but a released lock and called a
completely successful cycle "cycle ended without an actionable reason". No
supervision event was lost - the durable queue held it - but every harness
protocol reads that line as "supervision is down" and directs a manual re-arm.
The arm now resolves an unobservable close against the durable wake queue, which
records every wake before the watcher prints it and whose sequence counter never
rewinds, not even across a drain. A cycle the queue proves delivered a wake
reports that wake and exits 0; a cycle whose records a handling turn already
drained reports the delivery without inventing a reason line; only a cycle that
delivered nothing is still the typed nonzero failure. Fixing it in the arm covers
codex, opencode, pi, grok and kimi, not just the Claude Stop path.
Regressions: tests/fm-session-lock-ancestry.test.sh pins both platforms' ps
semantics behind a deterministic process table and runs the real Stop auto-arm in
version-named, daemon-parented, and combined real process trees, each orphaned so
the walk cannot escape the fixture. tests/fm-watch-arm.test.sh drives a real
watcher and a real attached arm through a real wake. Every fault case fails on
the previous code.
* no-mistakes(review): Bind watcher delivery records to process identity
* no-mistakes(review): Return validated watcher identity atomically
* no-mistakes(review): Track watcher successors by PID and identity
* no-mistakes(document): Consolidate watcher arm-cycle documentation ownership
* fix(bin): harden Claude supervision auto-arm recovery (#1495)
* fix(supervision): harden Claude auto-arm failure handling
* no-mistakes(review): Guarantee automatic retry after Claude auto-arm failures
* no-mistakes(review): Gate attended fail-open on verified supervision failure
* no-mistakes(document): Document Claude auto-arm retry and guard scope
* no-mistakes: apply CI fixes
* fix(supervision): make Claude fail-open progression monotonic
* no-mistakes(review): Preserve auto-arm failure episodes until verified watcher recovery
* no-mistakes(review): Linearize auto-arm failure progression across existing locks
* no-mistakes(review): Linearize positive recovery across shared failure episode lock
* no-mistakes(review): Scope Claude recovery contention to Claude guard mode
* no-mistakes(document): Align supervision auto-arm documentation
* no-mistakes(review): Preserve actionable wakes despite healthy successors
* no-mistakes(document): Refresh supervision auto-arm documentation
* feat(bin): require an explicit per-task delivery contract (#1563)
* feat(bin): require an explicit ship delivery mode in fm-brief
A ship brief's definition of done was shaped by a silent per-project registry
lookup, so an adjusted brief and the task's recorded delivery could disagree and
no one had to decide anything per task.
fm-brief now requires --mode on ship scaffolds, validates it against the closed
set, refuses the conditional no-mistakes-prod-only registry policy as a task
mode, and records the choice as a fixed machine-readable "Delivery contract:
mode=<mode>" line that fm-spawn can check. --mode is refused on scout and
secondmate scaffolds, and --yolo is refused outright because the worker never
owns approval decisions.
* feat(bin): require an explicit ship delivery contract at spawn and promotion
fm-spawn resolved every ship and scout task's mode and yolo from the project
registry, so the delivery posture was never a per-task decision and could
contradict the brief the worker was about to follow.
fm-spawn now requires --mode and --yolo on ship spawns, validates both against
their closed sets, and reads the brief's recorded delivery contract line and
refuses a mismatch before any endpoint exists; a brief scaffolded before that
line existed warns once and launches on the flag. A batch carries one shared
contract that each pair still checks against its own brief. Scout and secondmate
spawns refuse the flags, and a scout now records no mode or yolo at all, which
teardown and the snapshot already tolerate. When the explicit mode carries less
rigor than the project's standing posture, a deviation notice is printed and the
spawn continues, so the registry stays advisory rather than an enforced default.
fm-promote requires the same two flags, because a scout carries no posture to
inherit, and writes them into the task record with the kind flip.
fm-project-mode keeps its one registry parser for the mechanical consumers that
have no task in hand, accepts the conditional no-mistakes-prod-only annotation
and maps it to its most rigorous leg for them, and grows --raw so the deviation
notice can tell a conditional policy apart from a flat mode.
* docs: record the explicit per-task delivery contract
AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at
intake, including the surface classification for a no-mistakes-prod-only project
and the unregistered-project fallback, and the project-management skill defines
that conditional policy as a registration-time posture with its defaults and
initialization consequences. The registry blurb, script table, and architecture
section follow: the registry records the captain's standing posture, and task
delivery is decided per task and passed explicitly.
* test: pass ship delivery flags per call site in the Herdr launcher e2e
The shared spawn helper also launches a secondmate, which refuses the flags, so
the contract belongs at each ship call site rather than inside the helper.
* test: pass the ship delivery contract in the secondmate suites
Both suites scaffold or spawn an ordinary ship task as the control case for a
secondmate assertion, so each needs the explicit contract the ship path now
requires.
* feat(bin): support remote secondmate homes (#1576)
* Add generic remote secondmate transport
* Add routed remote secondmate replies
* Add remote outbox backlog handoff
* Integrate remote secondmate lifecycle
* no-mistakes(review): Fix remote snapshot and handoff races
* no-mistakes(review): Serialize remote home provisioning transactions
* no-mistakes(review): Harden remote lifecycle transaction boundaries
* no-mistakes(review): Serialize remote lifecycle mutations and fail closed
* no-mistakes(review): Close remote lifecycle and file race windows
* no-mistakes(review): Serialize remote reply retirement and inheritance
* no-mistakes(review): Harden remote transfer integrity and recovery
* no-mistakes(review): Serialize remote respawn with registry retirement
* no-mistakes(document): Document remote bootstrap convergence accurately
* no-mistakes(document): Clarify skipped remote secondmate mutations
* no-mistakes(lint): Resolve remote script ShellCheck warnings
* no-mistakes: apply CI fixes
* feat: add per-task trace context propagation (#995)
* feat(spawn): propagate a native W3C traceparent to spawned agents
Add a default-off capability that resolves one W3C traceparent for a task,
injects it into the agent's pane shell as the TRACEPARENT environment
variable immediately before launch, and records the identical value as
traceparent= in state/<id>.meta, so an external observer that explicitly
reads that env value or meta field can correlate a worker, a Secondmate, and
their nested children into one trace with no collector, storage, UI, or
vendor coupling.
TRACEPARENT as an environment variable is a firstmate convention carrying a
W3C-formatted value: W3C Trace Context standardizes the header, not an env
var, and OpenTelemetry SDKs do not read it automatically, so a downstream
must consume it deliberately; this feature parents no SDK span by itself.
Identity is per task, not per spawn: the carrier is minted with random ids on
the first spawn, adopted as a child (fresh span, same trace) for a nested
spawn whose parent already holds one, and reused verbatim from the meta on
relaunch, so a task keeps one stable logical identity across restarts. A
malformed or all-zero inherited value is treated as absent and roots a fresh
trace. A new root is sampled (01) - a sampling decision a downstream
parent-based sampler honors, not a guarantee that any collector stores a
span, and firstmate emits no spans; a child preserves the inherited flag.
Trust boundary: a firstmate-minted root is random and reads no prompt, path,
task prose, credential, or arbitrary environment key. An inherited
TRACEPARENT is opaque caller-controlled data - up to 24 bytes of id passed
through after syntax validation - so whoever set it controls those bytes, a
bounded fixed-width channel rather than a general content or secret channel.
The feature adds no OTEL_* variable, no tracestate, and no arbitrary
environment injection; it runs no configurable or arbitrary command, only the
fixed local od and tr (resolved from PATH) to read a few bytes of entropy - a
small local pipeline with no network or watchdog and no hard latency
guarantee. Any entropy or validation failure that returns omits the carrier
without aborting the spawn. A default-off spawn leaves the generated meta and
launch environment unchanged.
Enablement is default-off (config/trace-context, or FM_TRACE_CONTEXT where a
non-empty value overrides and unset or empty defers to the file) and is
propagated into secondmate homes, taking effect at each agent's next launch:
a Secondmate launched or relaunched after enablement carries the primary
trace into its nested workers, while an already-running Secondmate roots new
traces for its own workers until relaunched. Injection reuses the existing
GOTMPDIR channel, so all spawn backends and harnesses and the ship, scout,
and secondmate paths are covered.
Covered by a pure-library suite and a spawn-path integration test (fake tmux
plus a real worktree, hermetic against ambient FM_TRACE_CONTEXT) proving the
recorded and injected carriers are identical and sent before launch, that
default-off writes and injects neither, that a relaunch reuses the recorded
carrier, and that an explicit FM_TRACE_CONTEXT overrides the file both ways;
plus a source-owner inheritance test proving trace-context propagates and
absence-mirrors through propagate_inheritable_config.
Documentation follows the repository documentation-audiences contract:
docs/trace-context.md is maintainer-architecture rationale, the configuration
schema lives in docs/configuration.md, and the repeatable test evidence is
separated into docs/verification/trace-context.md (maintainer-verification),
registered in docs/documentation-audiences.json.
* fix(spawn): propagate the effective trace-context decision to secondmates
FM_TRACE_CONTEXT overrode trace context only in the process that read it. A
newly launched secondmate decided enablement from the inherited
config/trace-context file alone, so the override did not cross the
primary-to-secondmate boundary: FM_TRACE_CONTEXT=off with the file present left
the secondmate's nested workers traced (a broken kill switch), and
FM_TRACE_CONTEXT=on with the file absent left them untraced despite the
inherited carrier.
Deliver the primary's effective decision to a newly launched secondmate as a
normalized on/off FM_TRACE_CONTEXT in the launch prefix, so a FM_TRACE_CONTEXT
override governs the nested primary -> secondmate -> worker chain both ways, not
just the copied file. The value is bounded to the literal on/off and does not
broaden environment injection; the already-running secondmate boundary is
unchanged.
Add a genuine two-level spawn regression that drives fm-spawn twice with the
exact environment the primary injects into the secondmate and proves both
divergent directions end to end. Correct the documentation that implied
secondmate coverage on every backend, since orca and cmux reject secondmate
spawns, and refresh the verification evidence for the new assertion count.
* no-mistakes(review): Clarify Secondmate trace-context launch snapshots
* no-mistakes(document): Correct trace-context documentation ownership and relaunch semantics
* fix(spawn): resolve the trace-context decision once for carrier and snapshot
The effective trace-context decision was read twice per spawn: once inside
fm_trace_context_resolve for the recorded carrier, and again for the secondmate
FM_TRACE_CONTEXT launch snapshot. A config-file change between the two reads
could pair a carrier with the opposite enable state - an injected carrier with
an off snapshot, or no carrier with an on snapshot.
Freeze the effective on/off decision once, drive the carrier resolution under
that frozen FM_TRACE_CONTEXT so it cannot independently re-read the file, and
reuse the same frozen decision for the secondmate launch snapshot. Add a
spawn-path regression that drives the file-decided path and proves the recorded
carrier and the delivered snapshot always agree, and refresh the verification
evidence for the new assertion count.
* no-mistakes(review): Preserve legacy Secondmate trace boundary
* no-mistakes(document): Correct trace-context verification comparison base
* no-mistakes(review): Captain, prevent failed trace delivery metadata claims
* no-mistakes(review): Captain, align trace-context tests and verification evidence
* no-mistakes(document): Correct trace-context verification evidence
* no-mistakes(lint): Suppress intentional ShellCheck literal-dollar warnings
* no-mistakes(review): Captain: freeze trace context at session start
* no-mistakes(test): Captain: stabilize scheduler test and document Kimi trace coverage
* no-mistakes(document): Document trace-context safety boundaries
* fix(trace): fail off on stale session snapshots
Publish each home session decision atomically through a same-directory temporary file and bind it to the current session lock. A replacement failure can no longer leave an earlier on decision active in a later session; missing, stale, malformed, or unpublishable state defaults safely to off.
Add regressions for read-only replacement and failed publication, update spawn and session-start fixtures for the lock-bound format, and refresh the architecture and verification records.
* no-mistakes(review): Fix trace spawn failure independence and duplicate safety
* no-mistakes(document): Refresh trace-context documentation and verification
* no-mistakes(review): Clear partial backend input after failed trace submission
* no-mistakes(review): Stop unsafe trace delivery before launch append
* no-mistakes(document): Document unsafe trace delivery handling
* fix(trace): bound each trace to one routed task, never the routing agent
A persistent Secondmate holds its launch-time TRACEPARENT in the process
environment for its whole life, and routed requests never replace it, so
resolving new-task carriers from the ambient environment chained every
routed task into one ever-growing trace per Secondmate with distinct
parent ids. Resolve now reuses the task's recorded carrier or mints a
fresh sampled root, never reading ambient TRACEPARENT, so each routed
task is its own trace boundary while relaunch, recovery, and
scout-to-ship promotion keep one stable per-task identity.
The spawn regression models the reviewed scenario exactly: two unrelated
tasks spawned sequentially through one persistent Secondmate environment
record and inject distinct trace ids, adopt nothing from the Secondmate's
carrier, and a relaunch of the first task reuses its original carrier
verbatim.
* docs(trace): define the per-task trace boundary
The design contract is one task per trace: a persistent Secondmate is
routing infrastructure with its own agent identity, never a shared trace
root for the unrelated tasks routed through it. Root/recovery semantics
replace the removed child-inheritance path, the sampling and safety
sections drop inherited-carrier language because ambient TRACEPARENT is
never read, and the verification page records the refreshed suite
inventories including the two-task Secondmate boundary regression.
* test(trace): adopt the explicit per-task delivery contract in spawn fixtures
Rebasing onto current main brings the explicit per-task delivery contract:
ship spawns now require --mode and --yolo instead of resolving them from the
project registry. The trace spawn fixtures pass the same explicit contract
canonical spawn tests use, preserving the per-task trace boundary coverage
unchanged, and the verification page records the refreshed comparison base.
* fix(bin): harden tmux agent liveness across harnesses (#1577)
* fix(bin): classify tmux agent liveness independent of process titles
`fm_backend_tmux_agent_state` attributed a pane solely from
`#{pane_current_command}`, which is a process TITLE a harness can rewrite,
not a structural fact. Claude Code 2.1.220 reports its version string there,
so a live Claude endpoint classified `ambiguous`: the session-start secondmate
liveness sweep could no longer see it, and any consumer that gates on a
positive classification refuses outright.
Read a second, independent name source: the kernel `comm` of every process in
the pane tty's foreground process group. Either source naming a verified
harness yields `alive`, because a false `dead` is the one verdict that can
start a duplicate agent on a live worktree. Scoping to the foreground process
group rather than the pane's descendants keeps a harness-named background
process from faking an agent, and covers multi-process launchers (the Pi
Launcher path) without a special case.
Verified on 2026-08-03 against all seven adapters running for real on tmux
3.6a / macOS 26.5.2 arm64: claude 2.1.220, codex-cli 0.146.0, opencode
1.18.11, pi 0.82.0, pi-signed 0.82.0, grok 0.2.118, kimi 0.31.1 all classify
`alive`, each attributed by a source independent of its title.
Two tests, because they fail for different reasons:
- tests/fm-tmux-agent-liveness.test.sh pins the logic with real processes and
no harness, so it runs everywhere CI runs tmux. It drives the two name
sources apart on purpose and asserts the divergence, so no case can go
quietly vacuous.
- tests/fm-harness-liveness-drift-live-e2e.test.sh relaunches every installed
harness and fails naming the harness and version when one stops being
attributed by a title-independent source.
AGENTS.md section 4 carries the resulting standing rule, and
firstmate-coding-guidelines owns how to satisfy it.
* no-mistakes: apply CI fixes
* docs: move the harness-dependent-check policy out of AGENTS.md
The standing rule was stated in AGENTS.md section 4 with the mechanics in
firstmate-coding-guidelines, which split one contract across two owners and
charged every session for a rule that only fires when firstmate's own
harness-dependent code is being changed.
firstmate-coding-guidelines is now the single owner of both the rule and how
to satisfy it: real-harness proof required, that proof authorized to spend
tokens, structural signals preferred over vendor-rendered surfaces, and a
guard that fails loudly naming the harness and version where a surface signal
is unavoidable. No inline stub is left behind, because AGENTS.md already
carries the load trigger for that skill in sections 7 and 13, so it is read
before any change to firstmate's shared tracked material.
Also records the cross-platform lesson the pipeline caught in the portable
regression, and corrects that file's header: the divergence assertion lives
on the version-string case, which diverges on both supported platforms,
rather than on every case.
* no-mistakes(review): Harden tmux liveness identity and drift validation
* no-mistakes(document): Clarify cross-platform tmux liveness documentation
* feat(bin): propagate trace context to remote secondmates (#1609)
* feat(bin): trace remote secondmate routes and unify the inherit allowlist
Per-task W3C trace context (#995) resolved and injected its carrier only at
the local spawn path. A remote secondmate is routed through
spawn_remote_secondmate, which returns long before that site and wrote its own
metadata block, so a remote secondmate stayed silently untraced even with the
capability enabled.
The parent home still owns that task's identity, because it holds the metadata
an observer reads. It now resolves the carrier against the task's own meta
under its own frozen decision - reused verbatim on relaunch, freshly rooted
otherwise, never adopting the parent process's ambient TRACEPARENT - and hands
it to the configured host through a new fm-spawn --traceparent argument,
accepted only for a secondmate launch and only as a strict W3C value. The
remote host exports it at the same unconditional pre-launch site and reports
back the carrier its endpoint actually holds, which the parent records, so an
already-alive endpoint reports the identity its agent really received rather
than one the parent merely intended. Disabled remains byte-identical and off.
The remote inherit path also carried its own hardcoded copy of the inheritable
config set, already drifted from FM_INHERITABLE_CONFIG by trace-context. Both
remote ends now derive from that one declaration, so a future item cannot be
sent by one side and refused by the other, and session-scoped enablement items
are skipped on live convergence exactly as the local path skips them.
Also fixes a latent stderr leak: an absent session lock printed a raw redirect
failure, which the new remote resolve site made visible.
Adds tests/fm-remote-secondmate-trace-context.test.sh, driving the real
parent -> fm-on -> remote entrypoint -> control -> remote fm-spawn chain over
the deterministic SSH boundary and reading the carrier back from the remote
pane's own log.
* no-mistakes(document): Clarify remote trace and allowlist contracts
* feat(bin): preflight remote runtime tool paths (#1623)
* feat(bin): widen the remote runtime PATH and add a remote doctor preflight
The fixed remote entrypoint hard-coded a four-directory PATH, so a remote
account whose tools live under nix or a per-user profile could not run basic
Firstmate work without a login shell. The entrypoint now composes its child
PATH from the code root's bin, the account's ~/.local/bin, the common
package-manager directories that actually exist on the host, and the portable
system tail, deduplicated and in a fixed order, still under env -i with the
same variable allowlist and no shell command string.
fm-remote-doctor.sh reports that exact PATH by inheriting it from its own
entrypoint launch rather than recomposing it, so the ordering keeps one owner.
It is read-only, reports where each required and optional tool resolved, and
exits non-zero naming every required tool that did not. Remote seeding runs it
as a preflight before anything is created on the host and restores the registry
when it fails.
* no-mistakes(review): Harden remote git authorization and missing-tool diagnostics
* no-mistakes(document): Document remote PATH doctor and safe shims
* no-mistakes(lint): Fix ShellCheck findings in remote path tests
* no-mistakes(lint): Suppress exported fixture's false-positive ShellCheck warning
* feat: gate remote second mates on Herdr readiness (#1639)
* feat(bin): gate remote second mates on herdr readiness
A remote second mate now always runs on the Herdr backend, whose server
belongs to the host's GUI login session and therefore outlives the SSH
connections that supervise it. fm-spawn's remote route forces that backend
and the host-local control script refuses any other, so the requirement
cannot be dropped from either side.
fm-remote-doctor.sh becomes the single owner of what "ready" means. It keeps
its PATH and tool reporting from #1623 and adds the Herdr, Aqua LaunchAgent,
GUI-session, server-reachability, and entrypoint-symlink checks, tagging each
gap fixable: or human: with the exact operator step. --fix closes only the
automatable gaps - writing and loading the Aqua-scoped dev.firstmate.herdr
launch agent, starting the server where no launch agent applies, and
recreating the entrypoint symlink - then re-derives every check from the host,
so a human gap is never presented as fixed. It never creates a login session,
writes an auto-login password, or touches FileVault.
Remote seed, remote spawn, and the startup liveness relaunch all run the same
check, repair, re-check sequence through one shared library and fail closed
with the doctor's own gap text. Recovery inherits the gate because it respawns
through the same route.
Tests drive the real doctor against a controlled account fixture with a
private HOME, a state-backed launchctl, and a fake herdr, and prove the
dangerous actions are never attempted. The remote lifecycle suites gain a
stateful Herdr CLI fixture and answer the readiness gate at the SSH boundary,
so they never inspect or repair the runner's own account.
* no-mistakes(review): Validate launch-agent contract and confirm Herdr startup
* no-mistakes(review): Validate loaded launch-agent contract before readiness
* no-mistakes(review): Refuse legacy remote backends without altering routes
* no-mistakes(review): Clarify conditional remote readiness repair sequence
* no-mistakes(review): Repair remote readiness before liveness probing
* no-mistakes(review): Preserve unknown seeds and reject legacy liveness
* no-mistakes(document): docs: clarify remote Herdr backend ownership
* fix: isolate remote secondmates in shared Herdr session (#1659)
* Pin remote secondmates to fm-remote
* no-mistakes(review): Fail closed on legacy remote Herdr endpoints
* no-mistakes(review): Isolate fm-remote launch agent from interactive default
* no-mistakes(document): Document shared remote Herdr retirement safety
* feat: route remote commands through an Aqua job worker (#1660)
* feat: run remote commands through Aqua job worker
* no-mistakes(review): Enforce remote job deadlines and safe worker shutdown
* no-mistakes(review): Refresh stale workers and harden dependency-free supervision
* no-mistakes(review): Harden worker ownership recovery and shutdown quarantine
* no-mistakes(review): Fix doctor bootstrap, harness repair, and output draining
* no-mistakes(review): Probe doctor tools through authenticated worker bootstrap
* no-mistakes(review): Refresh stale workers before doctor tool probes
* no-mistakes(review): Recover stopped quarantines and extend job deadlines
* no-mistakes(review): Separate queue and execution timeout windows
* no-mistakes(review): Supervise Linux worker crashes and bind root identity
* no-mistakes(review): Resolve authorized Nix profile bin links
* no-mistakes(review): Clarify Nix path resolution documentation
* no-mistakes(review): Harden PATH safety and nvm selection
* no-mistakes(review): Honor nvm system defaults and refresh doctor digest
* no-mistakes(review): Keep workers ready during active jobs
* no-mistakes(review): Bound pre-execution validation by job timeout
* no-mistakes(document): Clarify remote worker documentation
* no-mistakes(lint): Fix remote worker ShellCheck diagnostics
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
* fix: clarify remote doctor bootstrap path (#1691)
* fix(bin): gate delivery reports on a real PR and genuinely-run checks (#2)
* fix(bin): require a real PR and genuinely run checks before done
Workers on claude, codex and opencode/GLM reported "done: committed <sha>"
with no PR at all, and "checks green" on a fork PR whose workflow runs sat
at action_required so nothing had run. Both read the absence of a visible
failure as success, and the generated definition of done invited it: the
direct-PR contract opened by declaring the task complete once committed,
and no mode said what a green check set has to look like.
The PR-based modes now carry a short evidence gate: a real PR URL must
exist, and "checks green" may describe only checks that actually ran and
passed, naming the exact shape that fooled two workers - `gh pr checks`
printing nothing, or listing only skipped, queued or pending runs. A
missing PR is a blocker, not a done report, so the gate adds no new way to
declare success. The no-mistakes first report is now labelled a handoff
rather than completion, and local-only is untouched because it has no PR
by definition.
Delivery-mode semantics, the worktree-isolation assertion, the Herdr lab
gate and every other safety clause are unchanged, with behavioural tests
over the generated brief for each mode.
* no-mistakes(review): scope delivery evidence gate to PR-naming completion claim
---------
Co-authored-by: Claus (AI Assistant) <claus@providenceit.nl>
* feat(teardown): verify a merged PR's issues actually closed (#3)
* feat(teardown): verify a merged PR's issues actually closed
GitHub silently ignores some closing keywords, so a merged PR can leave its
issue open with no error or signal; the work lands on the default branch
while the board still misrepresents the state. Separately, many PRs carry no
parseable closing reference at all.
Add bin/fm-issue-closure.sh: a best-effort post-merge check that re-derives
the candidate issues a merged GitHub PR was meant to close - from the PR
body's closing-keyword grammar, GitHub's own closingIssuesReferences
(commit-message refs included), and optionally the task brief - checks each
candidate's state, and reports any GitHub left OPEN. It never auto-closes
(an outward-facing human decision), never blocks its caller (always exits 0),
reports lookup failures separately, and stays silent for PRs that close
nothing. GitLab merge requests are out of scope.
Invoke it from bin/fm-teardown.sh's post-merge path (alongside fleet-sync,
guarded by `|| true`), so firstmate sees the report in teardown output and
can surface the discrepancy.
* no-mistakes(review): skip PR-numbered closure candidates via gh api; drop dead helper
* no-mistakes(review): migrate teardown test gh mock to gh api issue lookup
* no-mistakes(document): add fm-issue-closure.sh to bin toolbelt inventory
* no-mistakes(review): accept colon closing-keyword form; fix doc wording
* no-mistakes(document): document teardown's merged-PR issue-closure step in script header
---------
Co-authored-by: Claus (AI Assistant) <claus@providenceit.nl>
* fix(teardown): stop refusing teardown of work that already landed (#4)
* fix(teardown): stop reporting landed commits as unpushed
Teardown refused two demonstrably-safe worktrees in one day, both from
comparisons that could not answer the question they were asked.
A worktree can hold ZERO remote-tracking refs for its own branch - a fresh
clone, a pruned ref, a worktree that never fetched. `git log HEAD --not
--remotes` then has nothing to compare against and reads every commit as
unpushed. That is absence of evidence, not evidence of absence, so the
remotes are now asked directly with `git ls-remote`, a read-only lookup that
writes nothing to the worktree. Teardown still never fetches to answer this:
a fetch would make a network failure indistinguishable from a properly
pushed branch, turning a safe refusal into an unsafe proceed.
A squash merge rewrites the branch into one commit, so no local hash exists
on the base even though every change does. When hash and per-commit patch-id
comparisons come up empty, the FILES at stake are now compared against the PR
head and against the PR's own merge commit - the merge commit rather than the
current default branch, because the base branch keeps moving and that motion
is not the task's work. A 3-way merge against the default branch that cannot
run falls through to the same comparison instead of being read as unlanded.
The file comparison covers what the ref itself contributed, not only what the
branch changed: an unpushed local commit that reverts merged work touches no
file relative to the branch's own base, and comparing only the branch's files
would call that revert landed and discard it.
Nothing weakens the guard. Every check returns "landed" only on positive
proof; an unreachable remote, a failed PR lookup, an unavailable merge
commit, a quoted filename, and a comparison that fails to run are all
recorded as unestablished and still refuse. Refusals now list exactly which
evidence source could not settle the question, so an operator can verify the
named gap instead of reaching for --force.
* no-mistakes(review): close rename-detection false allow and clarify refusal evidence notes
* no-mistakes(document): document FM_LS_REMOTE_TIMEOUT_SECS in configuration env inventory
---------
Co-authored-by: Claus (AI Assistant) <claus@providenceit.nl>
* feat(spawn): bootstrap project task environments before worker launch (#6)
* feat(spawn): bootstrap project task environments
* no-mistakes(review): bound and isolate task bootstrap, release aborted spawns
* no-mistakes(review): enforce bootstrap bound everywhere, return only validated worktrees
* no-mistakes(document): scope failed-spawn cleanup claims to their actual backends
* no-mistakes(lint): use env for bootstrap runner override in test
---------
Co-authored-by: Claus (AI Assistant) <claus@providenceit.nl>
* fix(herdr): confirm fast Herdr deliveries with a bounded submit postcondition (#7)
* fix(send): confirm fast Herdr deliveries
* no-mistakes(review): widen Herdr transcript postcondition window; restore wait fixtures
* no-mistakes(review): bite tall-turn regression; strip capture ANSI once
* no-mistakes(document): document Herdr fast-turn submit postcondition and pending diagnostics
---------
Co-authored-by: Claus (AI Assistant) <claus@providenceit.nl>
---------
Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
Co-authored-by: Gavin <51008413+allstargg@users.noreply.github.com>
Co-authored-by: Claus (AI Assistant) <claus@providenceit.nl>
levelupself
added a commit
to levelupself/firstmate
that referenced
this pull request
Aug 16, 2026
…ts preserved) (#58) * feat(bin): require an explicit per-task delivery contract (#1563) * feat(bin): require an explicit ship delivery mode in fm-brief A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions. * feat(bin): require an explicit ship delivery contract at spawn and promotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode. * docs: record the explicit per-task delivery contract AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly. * test: pass ship delivery flags per call site in the Herdr launcher e2e The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper. * test: pass the ship delivery contract in the secondmate suites Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires. * feat(bin): support remote secondmate homes (#1576) * Add generic remote secondmate transport * Add routed remote secondmate replies * Add remote outbox backlog handoff * Integrate remote secondmate lifecycle * no-mistakes(review): Fix remote snapshot and handoff races * no-mistakes(review): Serialize remote home provisioning transactions * no-mistakes(review): Harden remote lifecycle transaction boundaries * no-mistakes(review): Serialize remote lifecycle mutations and fail closed * no-mistakes(review): Close remote lifecycle and file race windows * no-mistakes(review): Serialize remote reply retirement and inheritance * no-mistakes(review): Harden remote transfer integrity and recovery * no-mistakes(review): Serialize remote respawn with registry retirement * no-mistakes(document): Document remote bootstrap convergence accurately * no-mistakes(document): Clarify skipped remote secondmate mutations * no-mistakes(lint): Resolve remote script ShellCheck warnings * no-mistakes: apply CI fixes * feat: add per-task trace context propagation (#995) * feat(spawn): propagate a native W3C traceparent to spawned agents Add a default-off capability that resolves one W3C traceparent for a task, injects it into the agent's pane shell as the TRACEPARENT environment variable immediately before launch, and records the identical value as traceparent= in state/<id>.meta, so an external observer that explicitly reads that env value or meta field can correlate a worker, a Secondmate, and their nested children into one trace with no collector, storage, UI, or vendor coupling. TRACEPARENT as an environment variable is a firstmate convention carrying a W3C-formatted value: W3C Trace Context standardizes the header, not an env var, and OpenTelemetry SDKs do not read it automatically, so a downstream must consume it deliberately; this feature parents no SDK span by itself. Identity is per task, not per spawn: the carrier is minted with random ids on the first spawn, adopted as a child (fresh span, same trace) for a nested spawn whose parent already holds one, and reused verbatim from the meta on relaunch, so a task keeps one stable logical identity across restarts. A malformed or all-zero inherited value is treated as absent and roots a fresh trace. A new root is sampled (01) - a sampling decision a downstream parent-based sampler honors, not a guarantee that any collector stores a span, and firstmate emits no spans; a child preserves the inherited flag. Trust boundary: a firstmate-minted root is random and reads no prompt, path, task prose, credential, or arbitrary environment key. An inherited TRACEPARENT is opaque caller-controlled data - up to 24 bytes of id passed through after syntax validation - so whoever set it controls those bytes, a bounded fixed-width channel rather than a general content or secret channel. The feature adds no OTEL_* variable, no tracestate, and no arbitrary environment injection; it runs no configurable or arbitrary command, only the fixed local od and tr (resolved from PATH) to read a few bytes of entropy - a small local pipeline with no network or watchdog and no hard latency guarantee. Any entropy or validation failure that returns omits the carrier without aborting the spawn. A default-off spawn leaves the generated meta and launch environment unchanged. Enablement is default-off (config/trace-context, or FM_TRACE_CONTEXT where a non-empty value overrides and unset or empty defers to the file) and is propagated into secondmate homes, taking effect at each agent's next launch: a Secondmate launched or relaunched after enablement carries the primary trace into its nested workers, while an already-running Secondmate roots new traces for its own workers until relaunched. Injection reuses the existing GOTMPDIR channel, so all spawn backends and harnesses and the ship, scout, and secondmate paths are covered. Covered by a pure-library suite and a spawn-path integration test (fake tmux plus a real worktree, hermetic against ambient FM_TRACE_CONTEXT) proving the recorded and injected carriers are identical and sent before launch, that default-off writes and injects neither, that a relaunch reuses the recorded carrier, and that an explicit FM_TRACE_CONTEXT overrides the file both ways; plus a source-owner inheritance test proving trace-context propagates and absence-mirrors through propagate_inheritable_config. Documentation follows the repository documentation-audiences contract: docs/trace-context.md is maintainer-architecture rationale, the configuration schema lives in docs/configuration.md, and the repeatable test evidence is separated into docs/verification/trace-context.md (maintainer-verification), registered in docs/documentation-audiences.json. * fix(spawn): propagate the effective trace-context decision to secondmates FM_TRACE_CONTEXT overrode trace context only in the process that read it. A newly launched secondmate decided enablement from the inherited config/trace-context file alone, so the override did not cross the primary-to-secondmate boundary: FM_TRACE_CONTEXT=off with the file present left the secondmate's nested workers traced (a broken kill switch), and FM_TRACE_CONTEXT=on with the file absent left them untraced despite the inherited carrier. Deliver the primary's effective decision to a newly launched secondmate as a normalized on/off FM_TRACE_CONTEXT in the launch prefix, so a FM_TRACE_CONTEXT override governs the nested primary -> secondmate -> worker chain both ways, not just the copied file. The value is bounded to the literal on/off and does not broaden environment injection; the already-running secondmate boundary is unchanged. Add a genuine two-level spawn regression that drives fm-spawn twice with the exact environment the primary injects into the secondmate and proves both divergent directions end to end. Correct the documentation that implied secondmate coverage on every backend, since orca and cmux reject secondmate spawns, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Clarify Secondmate trace-context launch snapshots * no-mistakes(document): Correct trace-context documentation ownership and relaunch semantics * fix(spawn): resolve the trace-context decision once for carrier and snapshot The effective trace-context decision was read twice per spawn: once inside fm_trace_context_resolve for the recorded carrier, and again for the secondmate FM_TRACE_CONTEXT launch snapshot. A config-file change between the two reads could pair a carrier with the opposite enable state - an injected carrier with an off snapshot, or no carrier with an on snapshot. Freeze the effective on/off decision once, drive the carrier resolution under that frozen FM_TRACE_CONTEXT so it cannot independently re-read the file, and reuse the same frozen decision for the secondmate launch snapshot. Add a spawn-path regression that drives the file-decided path and proves the recorded carrier and the delivered snapshot always agree, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Preserve legacy Secondmate trace boundary * no-mistakes(document): Correct trace-context verification comparison base * no-mistakes(review): Captain, prevent failed trace delivery metadata claims * no-mistakes(review): Captain, align trace-context tests and verification evidence * no-mistakes(document): Correct trace-context verification evidence * no-mistakes(lint): Suppress intentional ShellCheck literal-dollar warnings * no-mistakes(review): Captain: freeze trace context at session start * no-mistakes(test): Captain: stabilize scheduler test and document Kimi trace coverage * no-mistakes(document): Document trace-context safety boundaries * fix(trace): fail off on stale session snapshots Publish each home session decision atomically through a same-directory temporary file and bind it to the current session lock. A replacement failure can no longer leave an earlier on decision active in a later session; missing, stale, malformed, or unpublishable state defaults safely to off. Add regressions for read-only replacement and failed publication, update spawn and session-start fixtures for the lock-bound format, and refresh the architecture and verification records. * no-mistakes(review): Fix trace spawn failure independence and duplicate safety * no-mistakes(document): Refresh trace-context documentation and verification * no-mistakes(review): Clear partial backend input after failed trace submission * no-mistakes(review): Stop unsafe trace delivery before launch append * no-mistakes(document): Document unsafe trace delivery handling * fix(trace): bound each trace to one routed task, never the routing agent A persistent Secondmate holds its launch-time TRACEPARENT in the process environment for its whole life, and routed requests never replace it, so resolving new-task carriers from the ambient environment chained every routed task into one ever-growing trace per Secondmate with distinct parent ids. Resolve now reuses the task's recorded carrier or mints a fresh sampled root, never reading ambient TRACEPARENT, so each routed task is its own trace boundary while relaunch, recovery, and scout-to-ship promotion keep one stable per-task identity. The spawn regression models the reviewed scenario exactly: two unrelated tasks spawned sequentially through one persistent Secondmate environment record and inject distinct trace ids, adopt nothing from the Secondmate's carrier, and a relaunch of the first task reuses its original carrier verbatim. * docs(trace): define the per-task trace boundary The design contract is one task per trace: a persistent Secondmate is routing infrastructure with its own agent identity, never a shared trace root for the unrelated tasks routed through it. Root/recovery semantics replace the removed child-inheritance path, the sampling and safety sections drop inherited-carrier language because ambient TRACEPARENT is never read, and the verification page records the refreshed suite inventories including the two-task Secondmate boundary regression. * test(trace): adopt the explicit per-task delivery contract in spawn fixtures Rebasing onto current main brings the explicit per-task delivery contract: ship spawns now require --mode and --yolo instead of resolving them from the project registry. The trace spawn fixtures pass the same explicit contract canonical spawn tests use, preserving the per-task trace boundary coverage unchanged, and the verification page records the refreshed comparison base. * fix(bin): harden tmux agent liveness across harnesses (#1577) * fix(bin): classify tmux agent liveness independent of process titles `fm_backend_tmux_agent_state` attributed a pane solely from `#{pane_current_command}`, which is a process TITLE a harness can rewrite, not a structural fact. Claude Code 2.1.220 reports its version string there, so a live Claude endpoint classified `ambiguous`: the session-start secondmate liveness sweep could no longer see it, and any consumer that gates on a positive classification refuses outright. Read a second, independent name source: the kernel `comm` of every process in the pane tty's foreground process group. Either source naming a verified harness yields `alive`, because a false `dead` is the one verdict that can start a duplicate agent on a live worktree. Scoping to the foreground process group rather than the pane's descendants keeps a harness-named background process from faking an agent, and covers multi-process launchers (the Pi Launcher path) without a special case. Verified on 2026-08-03 against all seven adapters running for real on tmux 3.6a / macOS 26.5.2 arm64: claude 2.1.220, codex-cli 0.146.0, opencode 1.18.11, pi 0.82.0, pi-signed 0.82.0, grok 0.2.118, kimi 0.31.1 all classify `alive`, each attributed by a source independent of its title. Two tests, because they fail for different reasons: - tests/fm-tmux-agent-liveness.test.sh pins the logic with real processes and no harness, so it runs everywhere CI runs tmux. It drives the two name sources apart on purpose and asserts the divergence, so no case can go quietly vacuous. - tests/fm-harness-liveness-drift-live-e2e.test.sh relaunches every installed harness and fails naming the harness and version when one stops being attributed by a title-independent source. AGENTS.md section 4 carries the resulting standing rule, and firstmate-coding-guidelines owns how to satisfy it. * no-mistakes: apply CI fixes * docs: move the harness-dependent-check policy out of AGENTS.md The standing rule was stated in AGENTS.md section 4 with the mechanics in firstmate-coding-guidelines, which split one contract across two owners and charged every session for a rule that only fires when firstmate's own harness-dependent code is being changed. firstmate-coding-guidelines is now the single owner of both the rule and how to satisfy it: real-harness proof required, that proof authorized to spend tokens, structural signals preferred over vendor-rendered surfaces, and a guard that fails loudly naming the harness and version where a surface signal is unavoidable. No inline stub is left behind, because AGENTS.md already carries the load trigger for that skill in sections 7 and 13, so it is read before any change to firstmate's shared tracked material. Also records the cross-platform lesson the pipeline caught in the portable regression, and corrects that file's header: the divergence assertion lives on the version-string case, which diverges on both supported platforms, rather than on every case. * no-mistakes(review): Harden tmux liveness identity and drift validation * no-mistakes(document): Clarify cross-platform tmux liveness documentation * feat(bin): propagate trace context to remote secondmates (#1609) * feat(bin): trace remote secondmate routes and unify the inherit allowlist Per-task W3C trace context (#995) resolved and injected its carrier only at the local spawn path. A remote secondmate is routed through spawn_remote_secondmate, which returns long before that site and wrote its own metadata block, so a remote secondmate stayed silently untraced even with the capability enabled. The parent home still owns that task's identity, because it holds the metadata an observer reads. It now resolves the carrier against the task's own meta under its own frozen decision - reused verbatim on relaunch, freshly rooted otherwise, never adopting the parent process's ambient TRACEPARENT - and hands it to the configured host through a new fm-spawn --traceparent argument, accepted only for a secondmate launch and only as a strict W3C value. The remote host exports it at the same unconditional pre-launch site and reports back the carrier its endpoint actually holds, which the parent records, so an already-alive endpoint reports the identity its agent really received rather than one the parent merely intended. Disabled remains byte-identical and off. The remote inherit path also carried its own hardcoded copy of the inheritable config set, already drifted from FM_INHERITABLE_CONFIG by trace-context. Both remote ends now derive from that one declaration, so a future item cannot be sent by one side and refused by the other, and session-scoped enablement items are skipped on live convergence exactly as the local path skips them. Also fixes a latent stderr leak: an absent session lock printed a raw redirect failure, which the new remote resolve site made visible. Adds tests/fm-remote-secondmate-trace-context.test.sh, driving the real parent -> fm-on -> remote entrypoint -> control -> remote fm-spawn chain over the deterministic SSH boundary and reading the carrier back from the remote pane's own log. * no-mistakes(document): Clarify remote trace and allowlist contracts * feat(bin): preflight remote runtime tool paths (#1623) * feat(bin): widen the remote runtime PATH and add a remote doctor preflight The fixed remote entrypoint hard-coded a four-directory PATH, so a remote account whose tools live under nix or a per-user profile could not run basic Firstmate work without a login shell. The entrypoint now composes its child PATH from the code root's bin, the account's ~/.local/bin, the common package-manager directories that actually exist on the host, and the portable system tail, deduplicated and in a fixed order, still under env -i with the same variable allowlist and no shell command string. fm-remote-doctor.sh reports that exact PATH by inheriting it from its own entrypoint launch rather than recomposing it, so the ordering keeps one owner. It is read-only, reports where each required and optional tool resolved, and exits non-zero naming every required tool that did not. Remote seeding runs it as a preflight before anything is created on the host and restores the registry when it fails. * no-mistakes(review): Harden remote git authorization and missing-tool diagnostics * no-mistakes(document): Document remote PATH doctor and safe shims * no-mistakes(lint): Fix ShellCheck findings in remote path tests * no-mistakes(lint): Suppress exported fixture's false-positive ShellCheck warning * feat: gate remote second mates on Herdr readiness (#1639) * feat(bin): gate remote second mates on herdr readiness A remote second mate now always runs on the Herdr backend, whose server belongs to the host's GUI login session and therefore outlives the SSH connections that supervise it. fm-spawn's remote route forces that backend and the host-local control script refuses any other, so the requirement cannot be dropped from either side. fm-remote-doctor.sh becomes the single owner of what "ready" means. It keeps its PATH and tool reporting from #1623 and adds the Herdr, Aqua LaunchAgent, GUI-session, server-reachability, and entrypoint-symlink checks, tagging each gap fixable: or human: with the exact operator step. --fix closes only the automatable gaps - writing and loading the Aqua-scoped dev.firstmate.herdr launch agent, starting the server where no launch agent applies, and recreating the entrypoint symlink - then re-derives every check from the host, so a human gap is never presented as fixed. It never creates a login session, writes an auto-login password, or touches FileVault. Remote seed, remote spawn, and the startup liveness relaunch all run the same check, repair, re-check sequence through one shared library and fail closed with the doctor's own gap text. Recovery inherits the gate because it respawns through the same route. Tests drive the real doctor against a controlled account fixture with a private HOME, a state-backed launchctl, and a fake herdr, and prove the dangerous actions are never attempted. The remote lifecycle suites gain a stateful Herdr CLI fixture and answer the readiness gate at the SSH boundary, so they never inspect or repair the runner's own account. * no-mistakes(review): Validate launch-agent contract and confirm Herdr startup * no-mistakes(review): Validate loaded launch-agent contract before readiness * no-mistakes(review): Refuse legacy remote backends without altering routes * no-mistakes(review): Clarify conditional remote readiness repair sequence * no-mistakes(review): Repair remote readiness before liveness probing * no-mistakes(review): Preserve unknown seeds and reject legacy liveness * no-mistakes(document): docs: clarify remote Herdr backend ownership * fix: isolate remote secondmates in shared Herdr session (#1659) * Pin remote secondmates to fm-remote * no-mistakes(review): Fail closed on legacy remote Herdr endpoints * no-mistakes(review): Isolate fm-remote launch agent from interactive default * no-mistakes(document): Document shared remote Herdr retirement safety * feat: route remote commands through an Aqua job worker (#1660) * feat: run remote commands through Aqua job worker * no-mistakes(review): Enforce remote job deadlines and safe worker shutdown * no-mistakes(review): Refresh stale workers and harden dependency-free supervision * no-mistakes(review): Harden worker ownership recovery and shutdown quarantine * no-mistakes(review): Fix doctor bootstrap, harness repair, and output draining * no-mistakes(review): Probe doctor tools through authenticated worker bootstrap * no-mistakes(review): Refresh stale workers before doctor tool probes * no-mistakes(review): Recover stopped quarantines and extend job deadlines * no-mistakes(review): Separate queue and execution timeout windows * no-mistakes(review): Supervise Linux worker crashes and bind root identity * no-mistakes(review): Resolve authorized Nix profile bin links * no-mistakes(review): Clarify Nix path resolution documentation * no-mistakes(review): Harden PATH safety and nvm selection * no-mistakes(review): Honor nvm system defaults and refresh doctor digest * no-mistakes(review): Keep workers ready during active jobs * no-mistakes(review): Bound pre-execution validation by job timeout * no-mistakes(document): Clarify remote worker documentation * no-mistakes(lint): Fix remote worker ShellCheck diagnostics * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * fix: clarify remote doctor bootstrap path (#1691) * fix(bin): bound remote SSH dead-peer detection (#1699) * fix(remote): arm SSH dead-peer detection in fm-on.sh A vanished remote host mid-poll (a reboot, a dropped link) left ssh blocked indefinitely on a half-open TCP connection, because fm-on.sh's ssh invocation had no ServerAliveInterval/ServerAliveCountMax. This wedged the remote-reply ferry: fm-procevent.sh's runner blocked inside the ssh child and never reached its own no-result -> claim-release -> reconcile re-arm self-healing path, which otherwise already handles a nonzero exit with empty output correctly. Recovery required a manual retire and re-arm. Arm ServerAliveInterval=15 and ServerAliveCountMax=3 by default (bounded ~45s detection window), both overridable via FM_SSH_ALIVE_INTERVAL and FM_SSH_ALIVE_COUNT_MAX. This is a transport- level fix in fm-on.sh, so it covers every remote command routed through it, not just the reply ferry. The remote sshd answers keepalive probes independently of whatever the remote command is doing, so a legitimately long-but-alive command (a 55s poll, a clone, the doctor) is never falsely killed - only a truly vanished peer trips it, turning that case into a bounded, detectable ssh failure (exit 255) instead of an indefinite hang. Extends tests/fm-on.test.sh with a behavioral regression asserting a bounded, positive ServerAliveInterval/ServerAliveCountMax on the real ssh argv captured through the FM_SSH_BIN process seam, plus coverage that both are env-overridable. * no-mistakes(document): Document SSH dead-peer detection ownership * fix: report stale AXI tools during bootstrap (#1701) * feat(bootstrap): gate stale axi CLIs at the floors firstmate actually uses Add gh-axi 0.1.29 floor so bare --squash PR merges stop failing quietly on older builds. Raise tasks-axi to FM_TASKS_AXI_MIN=0.2.2 (multi-id mv) while keeping feature probes. Keep quota-axi at 0.1.16 after verifying schema 3 and per-model availability already ship there; runway remains optional. * no-mistakes(document): Clarify AXI compatibility documentation ownership * fix: prevent false watcher-down alarms in Claude sessions (#1661) * fix(guard): stop false watcher-down alarm mid-turn under Claude auto-arm bin/fm-guard.sh derived its watcher-health verdict from fm_watcher_healthy, which requires a live watcher process holding the home lock. Under the Claude Stop-hook auto-arm supervision model the watcher is armed at each turn end and exits on its wake, so it runs only between turns. Every guarded command run mid-turn therefore found no live watcher and printed the "WATCHER DOWN - SUPERVISION IS OFF" banner even though supervision was healthy. Because the episode key was derived from the beacon mtime (which the between-turns watcher advances every poll), the full banner re-printed on essentially every command, and the message always blamed a "fresh beacon" that was in fact fresh. Make the pull guard's health check model-aware via a new fm_watcher_supervision_verdict in bin/fm-wake-lib.sh: - Under the auto-arm model a beacon fresh within FM_GUARD_GRACE is healthy even with no live watcher process; only a beacon stale beyond grace (or absent) is a genuine lapse and alarms. - Under every persistent-watcher harness (codex foreground checkpoint, opencode/pi/grok background arm, tmux, unknown) a live identity-matched watcher with a fresh beacon is still required, unchanged. The banner now names the true failing condition, a missing live watcher process versus a genuinely stale beacon, instead of always blaming the beacon, and the once-per-episode dedup keys on that condition rather than the beacon mtime so a genuine lapse announces once and does not re-print each turn. The turn-end guard keeps the strict fm_watcher_healthy check because it fires at the turn boundary, where the auto-arm brings a fresh watcher up and it cooperates with that arm. fm_watcher_healthy itself is unchanged, so the arm layer's start/attach/replace decisions are unaffected. Tests in tests/fm-guard-stale-banner.test.sh cover the auto-arm healthy fresh-beacon-without-a-watcher case, the auto-arm stale-beacon alarm and its stable episode, the true-reason banner wording, and the reason-keyed episode surviving a beacon mtime change; existing persistent-model cases are pinned to that model. * no-mistakes(review): Pin secondmate supervision model to launched harness * no-mistakes(document): Align watcher documentation with model-aware supervision health * test: prevent fixture temporary directory leaks (#1704) * fix(tests): stop fixture-tempdir helper from self-deleting under command substitution fm_test_tmproot is almost always called as `TMP_ROOT=$(fm_test_tmproot prefix)`, which forks a subshell to capture its stdout. The old implementation set its EXIT cleanup trap inside that call, so the trap fired - and deleted the fixture root - the instant the subshell exited, before the real caller's own EXIT trap was ever installed. Every test using the documented call pattern leaked its fixture root on every run; two suites had already independently discovered and worked around this with ad-hoc mktemp calls. Registration now goes through a $$-keyed registry file instead of in-process state, since $$ resolves to the invoking shell's PID even inside the subshell. The real cleanup trap is armed once at source time (always the real caller, never a subshell) for EXIT, INT, and TERM. A best-effort orphan sweep on next source reaps marked fixture roots old enough to be from a killed prior run. Simplifies the two existing ad-hoc workarounds (fm-procevent.test.sh, wake-helpers.sh) back onto the shared helper now that it works correctly. * no-mistakes(review): Preserve live fixtures during orphan reaping * no-mistakes(review): Harden fixture ownership against PID reuse * no-mistakes(review): Secure cleanup registry against path precreation * no-mistakes(review): Make fixture registration transactional * no-mistakes(document): Documentation already matches fixture cleanup behavior * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * feat(herdr): enable presentation spaces by default (#1708) * feat(herdr): default presentation spaces on with an explicit opt-out Herdr's disposable one-task presentation workspace was opt-in through the presence of local config/herdr-presentation-spaces. It is now on by default, and a home opts out by writing "off" into that same file. Values are read with the whole-file whitespace-stripped convention the other scalar config items already use, plus case folding. An absent file, an empty file, and "on" all resolve on; only "off" opts out; an unrecognized value warns and keeps the default rather than failing a spawn over a purely visual setting. The empty file is exactly the historical opt-in form, so every home that had already enabled the projection stays enabled with no migration step, and no previously enabled home can be turned off by the flip. Because absence now means on at both ends, secondmate inheritance needs no item-specific convergence: mirroring an absent primary file converges a secondmate to the same default-on rather than turning its projection off, and only an explicit primary opt-out propagates the opt-out. The gate itself moves into fm_backend_herdr_presentation_enabled in the Herdr adapter so the semantics have one owner that regressions can exercise directly. * no-mistakes(document): Document Herdr default-on presentation safety --------- Co-authored-by: kunchenguid <kun-1@kunchenguid.com> * fix(bin): surface fleet-wide open decisions on every wake drain (#1711) * fix: surface consolidated open decisions on every wake-drain A needs-decision or blocked event buried under later, unrelated status appends was only ever shown via the last-line wake annotation, so a still-open captain decision could go silently missed even though status_open_decisions (fm-classify-lib.sh) already folds the whole status stream correctly and fleet-snapshot/bearings already reuse it. Wire that same fold into bin/fm-wake-drain.sh: a new fleet-wide scan_open_decisions wrapper scans every state/<id>.status, and fm-wake-drain.sh prints a separate, bounded OPEN DECISIONS section on every drain (including the empty-queue fast path), so session-start and every wake-handling turn surface it for free without duplicating the open/resolved fold itself. Heartbeat wakes drain through the same script, so this covers that surface too. Also tighten status_open_decisions' file guard to skip an unreadable status file instead of leaking a bash redirection error, now that a fleet-wide directory scan can reach files a single targeted read would not. * no-mistakes(review): Prevent status symlinks leaking open decisions * fix: drop unbounded perl subprocess from status symlink guard The review step's own symlink-safety auto-fix (O_NOFOLLOW read via a perl subprocess) forked one perl process per status file scanned by the new fleet-wide open-decisions scan, with no cap - inflating fm-wake-drain.sh's total external-read cost from 8 (the existing annotation read_cap) to 18 in the enrichment-caps regression test. The plain [ -L "$f" ] check already rejects any status file that is itself a symlink before any read happens, which is exactly what the new regression test exercises and is the same defense level the sibling scan_captain_relevant_statuses/last_status_line already rely on elsewhere in this file (no O_NOFOLLOW). Drop the subprocess-based nofollow read and keep the cheap builtin guard. * no-mistakes(document): Document actionable fleet-wide open decision drains * fix(bin): abort parked runs and reap leaked processes before teardown (#1710) * fix(bin): abort orphaned no-mistakes runs and reap leaked processes at teardown Teardown could remove a task's worker while its no-mistakes pipeline run was still parked at a gate, leaving an orphaned run holding a fleet slot indefinitely (observed 2026-08-03: runs parked 7h39m and parked at a post-CI approval gate). It could also leave backgrounded/disowned descendant processes rooted under the worktree or tasktmp surviving reparented to init (observed: two `go test` binaries pinning CPU for hours with no live task meta to attribute them to). Add two coupled pre-teardown steps, both scoped to this task's exact branch/head or worktree/tasktmp so they can never touch another task's run or processes: - conclude_task_no_mistakes_run aborts a run parked at a gate via `no-mistakes axi abort`, cd'd into the exact worktree so the daemon resolves the run itself rather than teardown naming a --run id. - reap_task_worktree_processes sweeps for processes whose cwd is under the worktree or tasktmp (via `lsof -a -d cwd`) and TERM/KILLs them. Both run before any worktree return, branch delete, or backend kill, and are idempotent on a retried teardown. The branch+head attribution logic is factored out of bin/fm-crew-state.sh into the new shared bin/fm-nm-run-lib.sh so both scripts use the same ownership contract. * no-mistakes(review): Fail closed on incomplete teardown cleanup * no-mistakes(review): Bind teardown cleanup to verified run and process identities * no-mistakes(review): Require confirmed aborts and convergent identity-safe process reaping * no-mistakes(review): Handle process exits during teardown identity checks * no-mistakes(review): Restore teardown library in hermetic gotmp fixtures * no-mistakes(document): Document teardown run attribution and timeout * no-mistakes(lint): Rename shell variable conflicting with done keyword * no-mistakes: apply CI fixes * fix: resolve fm-remote-entrypoint.sh SCRIPT_DIR through a PATH symlink (#1709) The script installs as a symlink under ~/.local/bin. Taking dirname of the symlink itself (instead of its real target) pointed SCRIPT_DIR at ~/.local/bin, breaking sourcing of the sibling fm-remote-job-lib.sh. Resolve the real path first, preferring python3's os.path.realpath, then realpath, falling back to the raw BASH_SOURCE on hosts with neither. * fix(pi): gate Calm built-in overrides by activation state (#1724) * fix(pi): stop Calm claiming a built-in tool name another extension owns fm-calm.ts claimed bash/read/edit/write/grep/find/ls unconditionally at extension load, regardless of whether Calm was on. Pi resolves two extensions registering the same built-in name by first-registered-wins with no merge and no unregister call, and Calm's project-local .pi/extensions/ position beats any global or CLI-configured extension, so a user who never even enabled Calm could have their own bash/read/etc override silently replaced. Captain-approved plan implemented: - Registration is now gated on config/calm already being "on" at load time. A Calm-off session or reload registers nothing, so a non-Calm user never contests a name. This stays synchronous during the factory's own load, not deferred to session_start: /reload (and ctx.newSession/fork/switchSession) render the restored transcript from a pre-session_start snapshot of the tool registry, so a deferred claim would miss that render - confirmed by tests/fm-calm-pi-extension .test.sh's hidden-block-geometry E2E when trialed. - The first time Calm turns on in a session that started off (activateBuiltInsIfNeeded, from the /calm command handler), Calm calls pi.getAllTools() - safe only once every extension has finished loading, unlike the load-time path above - to see whether a different extension already owns a name, and skips claiming only that one, leaving it and its owning extension fully intact and callable. - A contested name found this way prints a prominent ctx.ui.notify() warning naming the tool, plus a console diagnostic. - reportBuiltInLosses() remains the backstop for the one case neither of the above can reach: a session that starts or reloads with Calm already on, where the registry snapshot is taken before Calm gets any chance to check ownership. A symlink-safe realpath comparison avoids misreporting Calm's own registration as foreign when its path crosses a symlink (macOS /tmp, /var). Confirmed, bounded trade-off: the very first time a session that started Calm-off turns Calm on, tool-call rows already on screen from before that toggle do not retroactively collapse, because Pi never lets an extension re-point an already-rendered row at a definition registered later. Every session after that first toggle starts with the preference already on and takes the synchronous load-time path, so the guarantee is intact from then on. docs/calm.md and the file's own header document this in full. tests/fm-calm-pi-extension.test.sh gains test_builtin_gate_load_time (config/calm off registers nothing, on registers all 7 synchronously at load) and test_calm_activation_collision_and_regression_bound (first activation claims every uncontested built-in, leaves a foreign bash tool fully intact and callable, warns and logs the contested name, and locks in the documented pre-activation bound against real ToolExecutionComponent rendering). test_rendering_and_session_lifecycle and the live interactive E2E are updated for the new gate-at-load and first-activation-bound contract. * no-mistakes(document): Document Calm tool collision boundaries * no-mistakes: apply CI fixes * fix(bin): persist secondmate parent bindings for cleanup (#1727) * fix(bin): give secondmate homes a durable parent binding record Finished-worker cleanup on a remote second mate refused forever with "cannot resolve the primary home ... durable parent binding". The remote launch hands the child the remote code checkout as its parent home (fm-spawn.sh's sole writer of FM_PUBLIC_FOLLOWUP_PRIMARY_HOME receives FM_HOME=$FM_ROOT from fm-remote-secondmate-control.sh's host-local launch), and that path can never carry the parent's real records, so the guard refused unconditionally once relay looked active anywhere on that host. fm-home-seed.sh and fm-remote-home-provision.sh now write a durable .fm-secondmate-parent record next to the .fm-secondmate-home identity marker, naming the home's route to its parent as local (with the real parent path) or remote (with the parent's SSH alias for diagnostics only). fm-teardown.sh's cleanup gate reads it: a remote parent is out of scope for the delegated-promise check (the whole promised-public- reply subsystem is same-filesystem by construction, so a remote parent can never hold one), while a token committed directly to the child's own .env file - never the process environment - still refuses, so an unrelated export in the remote host's login shell can no longer mask in. For a local secondmate, the durable parent_home now also backs up the launch-time env var, closing a silent fail-open where a restart that dropped the launch prefix made the guard treat a genuinely active parent relay as off. Regression coverage drives the real remote route (SSH boundary + Herdr fixture) and real fm-home-seed.sh seeding rather than hand-crafted markers. * no-mistakes(review): Captain: fail closed on unsafe durable parent records * no-mistakes(review): Captain: enforce durable parent binding commit protocol * no-mistakes(review): Captain: publish local parent binding before identity * no-mistakes(review): Captain: refuse conflicting local parent bindings * no-mistakes(review): Captain: reject non-regular secondmate seed leaves * no-mistakes(review): Captain: enforce unique durable parent bindings * no-mistakes(review): Captain: reject route-incompatible durable parent fields * no-mistakes(document): Document durable secondmate parent bindings * no-mistakes(lint): Fix secondmate parent parser ShellCheck warnings * no-mistakes: apply CI fixes * feat(bin): enforce latest AXI-family tool floors (#1733) * feat(bin): gate lavish-axi at its session_ended floor in bootstrap bin/fm-procevent-lavish.sh decides that a human "Send & End" review is terminal by reading session_ended from the poll response's leading session block. That field first shipped in lavish-axi 0.1.35, so an older installed build silently leaves every ended review source armed forever and captures an empty ended result on each later cycle. The same release is what makes a plain reopen refuse a session the human deliberately ended. Add LAVISH_AXI_MIN=0.1.35 to the existing axi-family floor structure in bin/fm-bootstrap.sh, reusing tool_version_at_least and the same MISSING diagnostic gh-axi already emits, so an incompatible build is reported as an upgrade request before any review surface is armed. Later lavish-axi releases only add artifact-authoring surface the adapter never reads, so the floor is the feature-introduction point rather than latest. Fixtures that stubbed lavish-axi as a bare exit-0 tool would now be read as unparseable builds, so tests/lib.sh gains fm_fake_version_tool and every bootstrap-running suite uses it for lavish-axi. * no-mistakes(review): Clarify lavish-axi version floor rationale * no-mistakes: apply CI fixes * feat(bin): set axi-family floors to current latest under the bump policy The axi-family bootstrap floors are the CURRENT LATEST published version of each tool, captain-bumped periodically to move the whole fleet onto the newest axi tools. They are not the minimum feature-introduced version. The earlier lavish-axi work set a feature-minimum floor, which is the opposite of this policy, so replace it along with the older feature-minimum rationale carried by tasks-axi and quota-axi. State the policy explicitly in bin/fm-bootstrap.sh's header, which owns it, and in each per-tool floor owner, so no future change argues a floor back down to the earliest release that happens to satisfy some behavior. Remove the lavish-axi session_ended and upstream-PR citation, the tasks-axi multi-ID-mv minimum argument, and the quota-axi credential-source argument as floor rationale; the tasks-axi feature probes remain as a separate defense-in-depth concern. Floors: lavish-axi 0.1.45 (was 0.1.35), tasks-axi 0.2.4 (was 0.2.2), quota-axi 0.1.17 (was 0.1.16), gh-axi 0.1.29 unchanged and already latest. Each was verified against the tool's current published version. The mechanism is unchanged: the same shared version helper and the same MISSING diagnostic path. The below-fires and at-or-above-silent regression rows move to the new floors, keeping each boundary genuine by pinning the patch immediately below each floor rather than a version that was only below the old one. Fleet fixtures move to the new floors so a bootstrap- running suite is not reported as an out-of-date build. Three operator-facing backlog handoff and receipt errors named "0.2.2+" while the enforced floor moved, so they now point at the floor's owner instead of duplicating a version number that drifts. * no-mistakes(review): Centralize AXI floor policy beside constants * no-mistakes(review): Clarify bootstrap boundary test comment * no-mistakes(document): Centralize AXI floor policy rationale * fix(bin): bound open decision scans with incremental cursors (#1737) * fix(bin): bound OPEN DECISIONS scan cost with a per-status-file cursor The fleet-wide OPEN DECISIONS scan added in #1711 re-reads and refolds every task's entire lifetime status log on every drain, so its cost grows unbounded with total log size. Add status_open_decisions_incremental and scan_open_decisions_incremental to fm-classify-lib.sh: they persist a per-status-file byte cursor plus the folded open-decision set, and fold only newly appended bytes on each call, reusing status_open_decisions' exact fold-line rule (extracted into _fm_decision_fold_line) so the two strategies can never disagree on what is open. A missing or invalidated cursor (new task, truncated/rewritten/shrunk log) falls back to a full re-fold. bin/fm-wake-drain.sh now calls the incremental wrapper instead of the whole-file scan. * fix(bin): add O(1) rotation detection and read-failure guarding to the cursor fold Add the two pieces the incremental open-decisions cursor was missing, scoped to this repo's actual status-file usage (create-once, append-only, never replaced or rewritten in place): - An O(1) device+inode identity check (one stat call) alongside the existing size-shrink check, so a status file replaced/rotated/recreated at the same path is detected and falls back to a full re-fold, even when the replacement is the same size. A same-inode, same-size, in-place byte edit is a deliberately accepted gap: no code path in this repo ever does that to a status file. - Checked reads: a stat/wc/tail failure is a genuine I/O error, not "the file is empty" - it now reports the already-trusted persisted open set unchanged instead of risking a silent invalidation. Both stay O(1) plus new bytes per call, matching the cursor's bounded- cost design; no content hashing or pending-fragment machinery. * no-mistakes(review): Preserve cursor state across failed incremental reads * no-mistakes(review): Refold status when cursor cache reads fail * no-mistakes(document): Document cursor-backed open-decision scanning * no-mistakes: apply CI fixes * fix(bin): prevent remote polls from blocking session startup (#1754) * fix(bin): preempt remote reply long-polls for queued short jobs Session start on a home with live remote second mates could stall silently for many minutes: the single serial remote job worker ran each armed fm-remote-delta-read.sh reply poll to its full 55s window while bootstrap's short sync, inherit, state, and route commands sat queued behind it, and non-FIFO queue pickup let re-armed polls keep winning the lane. Measured end to end, a trivial short job took 31s behind one 30s poll window. The worker now preempts a running preemptible job (the read-only, cursor- anchored delta read is the only member of that class) as soon as a non-preemptible job is queued, publishing exit 75 with emptied output - byte-identical to the poll's own elapsed-window-with-no-data result - so the parent runner takes its existing no-result path and the watcher re-arms from the same cursor with nothing lost. The delta read translates SIGTERM into that same exit after removing its staging directory. Sibling polls never preempt each other, so two armed monitors cannot churn. The same measured scenario now completes in 1s. * no-mistakes(document): Clarify remote poll preemption documentation * docs: present X mode as the X and Discord public surface (#1778) Discord mentions already ride the same pairing-token opt-in, relay poll, and platform-aware reply path as X mentions, but the docs still read as X-only, so a stranger could not self-serve the Discord path. Add the numbered turn-on steps to the X mode configuration reference, pointing at the myfirstmate dashboard for account creation, bot install, and token issuance rather than duplicating operator setup here, and drop the X-only framing from the README bullet, the documentation index, and the architecture overview. * fix(bin): run session start deterministically from hooks (#1781) * feat(bin): run session start deterministically on hook-capable harnesses Session start relied on a native nudge that only asked the agent to run bin/fm-session-start.sh, and an agent can defer that. Observed 2026-08-01: an /ahoy-first session followed the recap path and did not take the helm until a later request forced it. Claude, Codex, and Pi now RUN the digest in their session-open hook through the new bin/fm-sessionstart-run.sh, so the full ordered digest is in model context before the first turn. That wrapper is the single owner of what a session-open source means: startup and Pi's "new" take the helm, clear and compact re-emit, resume/reload/fork delegate to the nudge, and an unreadable source takes the helm because doing that redundantly is idempotent while skipping it is the bug. Grok and OpenCode keep the nudge as the floor, since neither can carry hook stdout into a model turn. Because the hook now blocks session initialization, fm-session-start.sh bounds itself first. Its steps are not all individually bounded - bootstrap's gh auth probe, tool version probes, the backlog listing and per-task endpoint reads are unbounded - so the whole digest runs as one bounded child (default 120s). Whatever it emitted before the bound survives, and the parent adds a loud STARTUP TRUNCATED banner naming the stage that stalled and every stage that never ran, still exiting 0. --reemit skips only the sweeps startup already reconciled. It still re-verifies lock ownership and still drains queued wakes, which arrived after startup and are the turn's work. fm-bootstrap.sh gains FM_BOOTSTRAP_LOCKED so a re-emit keeps repair ownership instead of deferring to a lock holder that is itself. Also adds bin/fm-timeout-lib.sh as the single owner of bounded execution, replacing three near-identical copies, and gives the ahoy skill a helm check so a nudge-tier harness cannot recap before taking the helm. Verified live on 2026-08-05 against Claude 2.1.222, Codex 0.146.0, and Pi 0.82.0; docs/verification/supervision.md records the per-harness source vocabulary, the two named gaps, and the refresh command. * no-mistakes(review): Harden session-start completion, timeout, and Pi delivery * no-mistakes(review): Harden completion ownership and portable timeout escalation * no-mistakes(review): Normalize watchdog KILL exits without masking command status * no-mistakes(review): Guarantee startup bounds and align harness delivery tiers * no-mistakes(test): Fix Pi session-start live verification fixture * no-mistakes(document): Align session-start documentation with deterministic hooks * no-mistakes(lint): Silence intentional child-shell expansion lint warning * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * fix: rename X mode to Relay in user-facing docs (#1784) * docs: rename the user-facing product name to Relay The public-mention integration gated by the `.env` pairing token is now called Relay across user-facing prose, covering X and Discord alike instead of implying a single network. Renames the product-name strings only: README, docs, the captain-facing skill descriptions, and the AGENTS.md operating prose, including the `X mode (.env)` and `Optional X mode` headings and every link anchor that pointed at them. AGENTS.md section 14 carries a one-line bridge note so the older name and the unchanged identifier spellings stay discoverable. Internal identifiers are untouched: `FMX_*`, `config/x-mode.env`, `state/x-*`, `bin/fm-x-*`, the `fmx-respond` skill path, `__FM_X_MODE_ENV__`, and `x-mode-error`. Platform references to X and Discord as networks stay as they are, and the bootstrap-diagnostics entry still quotes bootstrap's emitted `FMX: X mode on/off` line verbatim because `bin/` output is out of scope for this pass. * no-mistakes(review): Complete Relay prose rename in maintained docs * no-mistakes: apply CI fixes * feat: add Muse Code crewmate adapter (#1786) * feat(harness): add a verified muse crewmate adapter Muse Code joins the fleet as a crewmate/scout adapter, verified live against Muse Code 0.1.0-R708.1 in an isolated lab. Detection matches the anchored prefix muse-bin*, because the installed launcher execs a version-suffixed binary whose name changes on every auto-update and whose install path carries no muse component to fall back on. The same identity is taught to the tmux liveness classifier, without which a healthy muse pane would have read as a dead endpoint. Busy state folds muse's own durable session event log, bound per task by a sessions-root/worktree sidecar. It is a pull source with no writer, so nothing is armed and no record is ever seeded. The fold is anchored on the full run lifecycle prefix so muse's nested cleanup "terminal" payloads cannot settle an in-flight run, and it is depth-bounded so muse's native sub-agent logs cannot be mistaken for the parent's. The idle half stays gated: an open run proves busy, but a settled log reads unknown until a credentialed multi-step run proves one turn stays inside one run. Two findings corrected the scout report. The exec-only --no-foreign-personal-context flag is rejected by the interactive TUI, so the privacy control that actually reaches a pane worker is MUSE_EXPERIMENTAL_FOREIGN_PERSONAL_CONTEXT_KILL, verified to drop the operator's foreign personal rules while keeping the project's own AGENTS.md. And an unauthenticated muse pane never exits, it waits on a device-code prompt, so credentials are a spawn preflight rather than a screen check. muse is refused for secondmates: it has no primary supervision protocol and its hook dialect rejects the reawakening handlers that protocol needs. Per the captain's decision, auto-update is not pinned, and the credentialed multi-step smoke is deferred with an explicit checklist in docs/verification/muse.md. * no-mistakes(review): Accept Muse dispatch profiles and shared efforts * no-mistakes(review): Bind Muse busy state to current session * no-mistakes(review): Compare Muse workspace bindings literally * no-mistakes(review): Harden Muse worker credentials and live signal verification * no-mistakes(review): Cache Muse session bindings and clarify worker credentials * no-mistakes(review): Clear Muse marker inheritance and normalize interrupt aliases * no-mistakes(review): Verify Muse glyph effective foreground color * no-mistakes(review): Harden Muse XDG paths, session cache, and glyph parsing * no-mistakes(document): Document Muse adapter boundaries * fix(herdr): require 0.8.0 for default presentation spaces (#1787) * fix(herdr): floor default-on presentation spaces at Herdr 0.8.0 Default-on presentation projection turns every crewmate teardown into a workspace-emptying removal. The focus-safe removal plan avoids Herdr's focus-stealing explicit close only while the doomed pane's shell can be proved lone, childless, and idle; a persistent child of that shell (gitstatusd, a zsh-async worker, direnv) fails that proof permanently and forces the plain close, which on every release before Herdr 0.8.0 moves the captain's active workspace for ~140ms on each teardown. Gate the unconfigured default behind a Herdr 0.8.0 floor. At or above it, project as before; below it, fall back to the flat per-home layout with one warning per home per detected release naming the version and the upgrade. An explicit "on" - including the historical empty opt-in file - is still honored below the floor, so a deliberate opt-in is never silently downgraded. The floor reads two independent signals from the client's own status, either of which can establish a supported release: the protocol number and the release core of the version string. Measured against the real release binaries, no build lacking both upstream focus fixes reaches protocol 19 and every pre-fix build tops out at 17, so protocol 19 is a safe structural expression of the floor. A release that reports neither signal readably is treated as unsupported rather than guessed at. Also: - Correct the adapter comment claiming the mitigation "stays safe without any version gate". That holds for the pane-death route only; the plain-close fallback is reachable precisely on the releases where it is unsafe. - Stop discarding the projected-close helper's stderr at teardown, so a refused or failed focus restore is visible instead of silent. The close stays non-fatal; the presence gate still decides record removal. - Add Part C to the focus-flash regression: a doomed pane whose shell holds a persistent child, in the geometry where the closing workspace's right neighbour is not the anchor. That is the fallback branch the suite could not structurally reach. On 0.7.5 it observes a bounded four-sample wrong-focus window restored exactly; on 0.8.0 it observes none. It also cross-checks its own measurement against the floor classifier, so a drifted protocol mapping fails loudly. - Make the projection suite's unconfigured-home case release-aware, so the whole real-Herdr lane passes on both the CI-pinned 0.7.4 and 0.8.0. - Add an opt-in live guard that re-measures the release-to-protocol mapping against the pinned upstream binaries. The immediate no-code mitigation for a home that cannot upgrade remains writing "off" into config/herdr-presentation-spaces. * no-mistakes(review): Pin Herdr live-guard digests across supported platforms * no-mistakes(review): Document authorized Herdr cleanup containment * no-mistakes(review): Harden Herdr warning marker publication * no-mistakes(review): Honor running Herdr server presentation floor * no-mistakes(review): Recheck Herdr floor after server ensure * no-mistakes(review): Refresh 0.7.5 and 0.8.0 focus transcripts * no-mistakes(review): Route Herdr floor probe through lab session * no-mistakes(document): Align Herdr floor documentation and comments * no-mistakes(lint): Document Herdr presentation out-parameter consumer * fix(bin): classify settled Muse session logs as idle (#1788) * fix(muse): trust the settled session log as idle The credentialed multi-step smoke on Muse Code 0.1.0-R708.1 answered the one question the idle half was held back for: one real 75-second tool-loop turn with 23 tool batches stays inside exactly one run started/terminal pair, and an Escape mid tool loop closes that run as cancelled rather than leaving the turn to continue in another run. A settled log is therefore a finished turn, not a pause between the runs of one turn. Remove fm_busy_muse_idle_verified and FM_BUSY_MUSE_IDLE_VERIFIED_VERSIONS outright rather than pinning them to a version: the session log's own metadata carries only semver 0.1.0 and a build sha, so a version allowlist could not actually match the running build and would be false precision. A settled log now classifies idle, an open run still classifies busy, and only a resolution failure - no binding, no matching log, an unreadable or run-free log - stays unknown. Record the evidence in docs/verification/muse.md, including the run-scoped grep the counts must use, and keep the post-upgrade re-check guidance. * no-mistakes(review): Document Muse idle trust and remove stale gate reference * no-mistakes(document): Clarify Muse idle verification ownership * docs(agents): read the persisted digest when only a preview is shown (#1794) * fix: preserve fleet state in truncated session-start digests (#1798) * feat(session-start): order the startup digest for truncation safety and bound its bulk The digest is delivered through a harness that truncates an oversized payload from the tail, and it really has been truncated: a 70KB digest arrived as lines 1-435 of 578, cutting off eight lines before the live-task inventory. That session took the helm without ever seeing which tasks were live or where their endpoints were. Three changes, one file's worth of composition: - FLEET STATE is emitted before CONTEXT, so a truncated tail drops curated memory - stable session to session, already governed by a captain-set budget, recoverable with one targeted read - instead of live fleet identity. The LOCK/BOOTSTRAP/WAKE-QUEUE safety preamble keeps its order. The read-once contract moves out of the closing reminder into its own section ahead of both, and now names the condition that voids it: a stage the truncation banner reports as never emitted. - Status-tail lines are capped per line, reusing the cut the wake digest's OPEN DECISIONS section already applies. An observed tail line ran 865 characters and nothing bounded it. The cut and its marker now live in one place, bin/fm-line-cap-lib.sh, so the two digests cannot drift apart; each task's full status log path is still printed beside its tail. - The backlog listing is composed as a recovery input: done rows are never listed, every in-flight, held, and blocked row is shown in full with its hold and blocked-by metadata, and only the dispatchable-now listing is bounded - with an exact remainder count and the command that shows the rest. FM_SESSION_START_QUEUED_LIMIT (default 20) replaces FM_SESSION_START_BACKLOG_LIMIT, which bounded the whole listing indiscriminately and so could drop a held or blocked row. Tests exercise the real digest output: section ordering with the preamble pinned, the per-line cap and its marker, and the backlog composition including the remainder counters on both the tasks-axi and manual paths. * no-mistakes(document): Clarify digest source recovery comments * feat(send): close answered decisions at answer time via --resolve-key (#1842) A captain decision opened by a keyed needs-decision:/blocked: status line orphaned as permanently open whenever the answer kicked off work: the worker's next event is working [key=<workstream>] in a different key namespace, so no resolved [key=<decision>] ever landed and the OPEN DECISIONS fold kept listing the answered decision forever. Remove the writer-dependency at its source: the answering firstmate already holds the decision key when it sends the answer, so fm-send's new --resolve-key flag (repeatable) appends the closing resolved line to this home's own state/<id>.status after the submit is confirmed. The close is a local ledger append for crewmates, local secondmates, and remote secondmates alike - a remote mate's escalations reach this ledger through the parent-replies ingest, so only the answer message crosses the transport. Safety: each named key must currently be open per the authoritative status_open_decisions fold or fm-send refuses before sending; a failed or unconfirmed send never closes a key; an append failure after a delivered answer exits nonzero with the manual close command so the decision re-surfaces instead of silently vanishing; a send without the flag closes nothing, and working:/done: still never clear a captain decision. Complementary fixes: the wake-drain OPEN DECISIONS section prints the answer-with-close command hint at the moment of use; brief scaffolds separate resolved's two duties (keyed-phase end vs decision closure) and state that a done:/working: line never closes a decision even when the answer started that work, keeping worker self-close for blockers that clear without a firstmate reply; AGENTS.md and docs/architecture.md carry the one-line pointers to the fm-send contract. * fix(bin): seed remote secondmates from supplied origins (#1836) * feat(secondmate): seed a remote home from a supplied project origin Remote seeding required a local projects/<name> clone purely to read `git remote get-url origin` into the provisioning manifest, so setting up a remote second mate forced disposable clones and no-mistakes inits in the primary home for projects that home has no reason to hold. Firstmate now resolves the origin itself and names it as <project>=<origin-url>. The seed validates and transports what it is give…
This was referenced Aug 16, 2026
eduardstan
added a commit
to eduardstan/firstmate
that referenced
this pull request
Aug 17, 2026
…#2) * feat(bin): add durable process-event supervision (#1483) * feat(procevent): supervise long-polling sources into durable events Firstmate had no way to wait on a blocking external process without holding a conversational turn. Add a domain-neutral process-to-event runner plus a thin adapter around the currently published `lavish-axi poll` interface: canonical physical source identity, one machine-wide owner per source, direct argv execution, and durable 0600 result capture before any event referencing it is published on the existing wake queue. No second notifier, no polling control plane, and no retry machinery. A captured result with no durable handled acknowledgement stays eligible for bounded re-announcement across any number of drains and restarts. Draining a wake before acting on it and then starting a replacement session resurfaces the same exact source and sequence, and never puts result payload text in an event line. `fm-procevent.sh handled <source-id> <sequence>` is the only thing that stops re-announcement: generation-keyed, private, path-safe, durable, and atomically idempotent, so a paired external effect gated on its first-time versus repeat report is never authorized twice. An acknowledgement is refused unless matching captured result and adapter records already exist, so a premature or mistyped call cannot suppress a future result. The source side is unchanged and still lossy: the published poll clears feedback destructively before returning it, so a result lost in that window is unrecoverable. This is never at-least-once, no-loss, or lossless, and the handled acknowledgement is not a generic exactly-once effect either - a crash between an external effect and its acknowledgement can still repeat that effect on replay. Integrate registered sources with watcher supervision, the guards, and recoverable secondmate teardown across nested homes, and cover source identity, lifecycle races, supervision, restart handling, and cleanup safety with regressions. * no-mistakes(review): Prevent Lavish prompt text from spoofing missing sessions * no-mistakes(review): Serialize publication and secure handled acknowledgements * no-mistakes(document): Document hardened process-event acknowledgement guarantees * fix(procevent): never reclaim a source whose owned group still runs A runner is its own process group leader and starts the blocking source in that group, but the claim records only the leader PID and its identity. If the leader died while the source child kept running, the missing PID was classified stale: reconciliation released the claim and started a second runner while the old blocking source was still consuming the same canonical source. For the Lavish adapter that means two destructive long polls racing on one review session, so it is not harmless process litter. It also contradicted the documented promise that ownership is never released until the whole group is gone. Ownership state now distinguishes a generation that is really gone from one whose leader crashed with its group still alive. Reconcile stops that surviving group and releases its exact generation before starting any replacement, and keeps the claim for a later cycle when it cannot prove the group stopped or another home owns it. Acquisition and `start` treat the same state as held rather than reclaimable. Signalling that group is safe precisely because only an absent leader reaches this state. A reused PID leaves the leader alive, so the identity comparison still classifies it stale or uncertain and no group signal follows, which keeps the existing PID-reuse refusal intact. Add a public-interface regression for the exact crash cut - SIGKILL only the leader, prove the child group survives, reconcile, and prove the old group is gone with no second source running - plus its counterexample that a generation with no leader and no surviving group is still reclaimed. Update the runner help, operating documentation, skill, and verification record where they described reclaim in terms of the leader alone. * no-mistakes(review): Enforce runner group ownership and detect poller overlap * no-mistakes(review): Isolate runner groups from unrelated caller processes * no-mistakes(document): Document isolated process-event runner launch * no-mistakes(lint): Suppress Perl literal ShellCheck false positive * fix(bin): retire terminal process events and surface queued wakes (#1500) * fix(bin): deliver process-event results and retire ended sources Two defects reproduced during a real Lavish adapter session. One human `Send & End` produced four captured results: the real feedback, then recurring empty ended sessions. The generic runner had no way to learn a source was finished, so every reconcile restarted a poll that returned immediately. The runner now asks the source's own adapter - `fm-procevent-<adapter>.sh terminal <result-file>` - and on exit 0 alone re-proves ownership, drops the registration, and releases its own claim under one source boundary. Terminal knowledge stays adapter-owned: for Lavish that is an ended session, a missing session, and the final feedback delivery the published poll marks with `session_ended`. An adapter with no terminal command keeps its source armed exactly as before. Capture before publication, captured-result durability, queued wake durability, bounded re-announcement, handled deduplication, one-owner ownership, and explicit idempotent retirement are all unchanged. A captured result queued its `check` wake durably, but a healthy watcher with a fresh beacon never delivered it; the result surfaced only after a manual drain. Publication happens outside the watcher (in the runner) or unconditionally (in reconcile), so the watcher had no newly actionable signal to report and never reached its rewake path. It now reports a queued-but-unsurfaced process-event record through the same actionable exit every other wake uses, deduplicated by the same `.seen-*` marker discipline the signal scan uses, so the record is always durable before it is suppressed. The durable queue remains the authority and no second notifier, poller, timer, queue, or adapter-specific wake path is added. Regressions cover both, driven end to end: an armed Lavish source against a stand-in for the published poll polls once, captures once, publishes one distinct event, and retires itself; two fixture adapters prove the terminal decision follows the adapter alone; and a real capture plus a real watcher prove one proactive wake before any drain, with no duplicate wake while the record stays queued or after it is acknowledged. * no-mistakes(review): Harden process-event retirement and proactive delivery * no-mistakes(review): Route process-event delivery through shared wake owner * no-mistakes(document): Clarify process-event delivery and retirement documentation * no-mistakes(lint): Fix ShellCheck control-flow warnings * no-mistakes(lint): Fix wake output status lint warning * perf: shard portable serial tests across CI runners (#1544) * perf(ci): shard the portable serial behavior lane across runners The Behavior portable serial job ran all 69 scripts of the serial remainder on one runner. The measured serial sum on run 30725985757 was 1143762 ms (19m04s) against a 20-minute timeout, so the job intermittently reached the cap and was cancelled with every step passing. Setup is only about 7s, so the cost is entirely test wall time. Split the lane into four separate-runner shards. Each shard is still strictly serial, and separate runners mean no two of these stateful scripts ever share a machine, so the split needs no concurrency isolation proof. Assignment is longest-processing-time bin packing over measured per-script duration hints, balancing every shard to 285941 ms (~4m46s) of expected work, and the timeout tightens from 20 to 15 minutes. bin/fm-test-run.sh owns the shard count and refuses a lane whose "ofN" disagrees with it, while ci.yml derives the same count from strategy.job-total rather than a literal, so changing it in either file alone fails the lane loudly instead of leaving part of the required suite unrun. --check-coverage additionally proves the shards are non-empty, disjoint, and exactly equal to the serial lane. No test is weakened, skipped, or removed. Also replace the wall-clock sleeps in the --jobs scheduler test fixture with an explicit signal handshake between the fixtures. The old 0.5s-versus-0.05s race failed on a loaded machine; the handshake passes under sustained CPU saturation. * no-mistakes(review): Correct portable serial shard balance evidence * no-mistakes(document): Document portable serial shard evidence accurately * fix(bin): correct session lock and attached watcher supervision (#1545) * fix(bin): identify harness sessions by path and report delivered wakes Two supervision faults, both reported by a contributor and both open on the default branch. Fault 1: the Stop auto-arm never claims the home. fm_harness_ancestry_pid() matched only the basename of `ps -o comm=`, and Claude Code's native installer names the per-session executable by its version (.../share/claude/versions/ 2.1.220), so that basename identifies nothing. Three real failure shapes follow: a version-named session is missed entirely and the hook exits 0 with the epoch never written (unconditional on Linux, where procps reports the kernel exec name and ignores argv[0]); a claude-named daemon that directly parents sessions wins the outermost-contiguous-claude rule ahead of the session itself; and a session that is both version-named and daemon-parented has its live lock reclaimed as stale and rewritten to the shared daemon pid, corrupting the home's ownership record. Harness identity now also reads whole components of the executable path and of argv[0], which is what both platforms still carry. Matching whole components only keeps that widening safe: bin/fm-claude-stop-autoarm.sh and ~/.claude/hooks scripts have no "claude" component. Ownership is then decided against the session's whole contiguous harness ancestry rather than one chosen pid, which is the honest form of the question the library already documents ("does the current process descend from that same harness?"). That subsumes the outermost-pid rule for Claude's nested bg-spare worker chain instead of reverting it, and lets a daemon-parented session recognize its own lock. Lock acquisition still writes the outermost pid of the run, the only pid that lives as long as the session. Fault 2: an attached arm reports a delivered cycle as FAILED. The watcher prints its one reason line to its own stdout, so only the arm that forked it can read that line; an arm that attached observes nothing but a released lock and called a completely successful cycle "cycle ended without an actionable reason". No supervision event was lost - the durable queue held it - but every harness protocol reads that line as "supervision is down" and directs a manual re-arm. The arm now resolves an unobservable close against the durable wake queue, which records every wake before the watcher prints it and whose sequence counter never rewinds, not even across a drain. A cycle the queue proves delivered a wake reports that wake and exits 0; a cycle whose records a handling turn already drained reports the delivery without inventing a reason line; only a cycle that delivered nothing is still the typed nonzero failure. Fixing it in the arm covers codex, opencode, pi, grok and kimi, not just the Claude Stop path. Regressions: tests/fm-session-lock-ancestry.test.sh pins both platforms' ps semantics behind a deterministic process table and runs the real Stop auto-arm in version-named, daemon-parented, and combined real process trees, each orphaned so the walk cannot escape the fixture. tests/fm-watch-arm.test.sh drives a real watcher and a real attached arm through a real wake. Every fault case fails on the previous code. * no-mistakes(review): Bind watcher delivery records to process identity * no-mistakes(review): Return validated watcher identity atomically * no-mistakes(review): Track watcher successors by PID and identity * no-mistakes(document): Consolidate watcher arm-cycle documentation ownership * fix(bin): harden Claude supervision auto-arm recovery (#1495) * fix(supervision): harden Claude auto-arm failure handling * no-mistakes(review): Guarantee automatic retry after Claude auto-arm failures * no-mistakes(review): Gate attended fail-open on verified supervision failure * no-mistakes(document): Document Claude auto-arm retry and guard scope * no-mistakes: apply CI fixes * fix(supervision): make Claude fail-open progression monotonic * no-mistakes(review): Preserve auto-arm failure episodes until verified watcher recovery * no-mistakes(review): Linearize auto-arm failure progression across existing locks * no-mistakes(review): Linearize positive recovery across shared failure episode lock * no-mistakes(review): Scope Claude recovery contention to Claude guard mode * no-mistakes(document): Align supervision auto-arm documentation * no-mistakes(review): Preserve actionable wakes despite healthy successors * no-mistakes(document): Refresh supervision auto-arm documentation * feat(bin): require an explicit per-task delivery contract (#1563) * feat(bin): require an explicit ship delivery mode in fm-brief A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions. * feat(bin): require an explicit ship delivery contract at spawn and promotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode. * docs: record the explicit per-task delivery contract AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly. * test: pass ship delivery flags per call site in the Herdr launcher e2e The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper. * test: pass the ship delivery contract in the secondmate suites Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires. * feat(bin): support remote secondmate homes (#1576) * Add generic remote secondmate transport * Add routed remote secondmate replies * Add remote outbox backlog handoff * Integrate remote secondmate lifecycle * no-mistakes(review): Fix remote snapshot and handoff races * no-mistakes(review): Serialize remote home provisioning transactions * no-mistakes(review): Harden remote lifecycle transaction boundaries * no-mistakes(review): Serialize remote lifecycle mutations and fail closed * no-mistakes(review): Close remote lifecycle and file race windows * no-mistakes(review): Serialize remote reply retirement and inheritance * no-mistakes(review): Harden remote transfer integrity and recovery * no-mistakes(review): Serialize remote respawn with registry retirement * no-mistakes(document): Document remote bootstrap convergence accurately * no-mistakes(document): Clarify skipped remote secondmate mutations * no-mistakes(lint): Resolve remote script ShellCheck warnings * no-mistakes: apply CI fixes * feat: add per-task trace context propagation (#995) * feat(spawn): propagate a native W3C traceparent to spawned agents Add a default-off capability that resolves one W3C traceparent for a task, injects it into the agent's pane shell as the TRACEPARENT environment variable immediately before launch, and records the identical value as traceparent= in state/<id>.meta, so an external observer that explicitly reads that env value or meta field can correlate a worker, a Secondmate, and their nested children into one trace with no collector, storage, UI, or vendor coupling. TRACEPARENT as an environment variable is a firstmate convention carrying a W3C-formatted value: W3C Trace Context standardizes the header, not an env var, and OpenTelemetry SDKs do not read it automatically, so a downstream must consume it deliberately; this feature parents no SDK span by itself. Identity is per task, not per spawn: the carrier is minted with random ids on the first spawn, adopted as a child (fresh span, same trace) for a nested spawn whose parent already holds one, and reused verbatim from the meta on relaunch, so a task keeps one stable logical identity across restarts. A malformed or all-zero inherited value is treated as absent and roots a fresh trace. A new root is sampled (01) - a sampling decision a downstream parent-based sampler honors, not a guarantee that any collector stores a span, and firstmate emits no spans; a child preserves the inherited flag. Trust boundary: a firstmate-minted root is random and reads no prompt, path, task prose, credential, or arbitrary environment key. An inherited TRACEPARENT is opaque caller-controlled data - up to 24 bytes of id passed through after syntax validation - so whoever set it controls those bytes, a bounded fixed-width channel rather than a general content or secret channel. The feature adds no OTEL_* variable, no tracestate, and no arbitrary environment injection; it runs no configurable or arbitrary command, only the fixed local od and tr (resolved from PATH) to read a few bytes of entropy - a small local pipeline with no network or watchdog and no hard latency guarantee. Any entropy or validation failure that returns omits the carrier without aborting the spawn. A default-off spawn leaves the generated meta and launch environment unchanged. Enablement is default-off (config/trace-context, or FM_TRACE_CONTEXT where a non-empty value overrides and unset or empty defers to the file) and is propagated into secondmate homes, taking effect at each agent's next launch: a Secondmate launched or relaunched after enablement carries the primary trace into its nested workers, while an already-running Secondmate roots new traces for its own workers until relaunched. Injection reuses the existing GOTMPDIR channel, so all spawn backends and harnesses and the ship, scout, and secondmate paths are covered. Covered by a pure-library suite and a spawn-path integration test (fake tmux plus a real worktree, hermetic against ambient FM_TRACE_CONTEXT) proving the recorded and injected carriers are identical and sent before launch, that default-off writes and injects neither, that a relaunch reuses the recorded carrier, and that an explicit FM_TRACE_CONTEXT overrides the file both ways; plus a source-owner inheritance test proving trace-context propagates and absence-mirrors through propagate_inheritable_config. Documentation follows the repository documentation-audiences contract: docs/trace-context.md is maintainer-architecture rationale, the configuration schema lives in docs/configuration.md, and the repeatable test evidence is separated into docs/verification/trace-context.md (maintainer-verification), registered in docs/documentation-audiences.json. * fix(spawn): propagate the effective trace-context decision to secondmates FM_TRACE_CONTEXT overrode trace context only in the process that read it. A newly launched secondmate decided enablement from the inherited config/trace-context file alone, so the override did not cross the primary-to-secondmate boundary: FM_TRACE_CONTEXT=off with the file present left the secondmate's nested workers traced (a broken kill switch), and FM_TRACE_CONTEXT=on with the file absent left them untraced despite the inherited carrier. Deliver the primary's effective decision to a newly launched secondmate as a normalized on/off FM_TRACE_CONTEXT in the launch prefix, so a FM_TRACE_CONTEXT override governs the nested primary -> secondmate -> worker chain both ways, not just the copied file. The value is bounded to the literal on/off and does not broaden environment injection; the already-running secondmate boundary is unchanged. Add a genuine two-level spawn regression that drives fm-spawn twice with the exact environment the primary injects into the secondmate and proves both divergent directions end to end. Correct the documentation that implied secondmate coverage on every backend, since orca and cmux reject secondmate spawns, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Clarify Secondmate trace-context launch snapshots * no-mistakes(document): Correct trace-context documentation ownership and relaunch semantics * fix(spawn): resolve the trace-context decision once for carrier and snapshot The effective trace-context decision was read twice per spawn: once inside fm_trace_context_resolve for the recorded carrier, and again for the secondmate FM_TRACE_CONTEXT launch snapshot. A config-file change between the two reads could pair a carrier with the opposite enable state - an injected carrier with an off snapshot, or no carrier with an on snapshot. Freeze the effective on/off decision once, drive the carrier resolution under that frozen FM_TRACE_CONTEXT so it cannot independently re-read the file, and reuse the same frozen decision for the secondmate launch snapshot. Add a spawn-path regression that drives the file-decided path and proves the recorded carrier and the delivered snapshot always agree, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Preserve legacy Secondmate trace boundary * no-mistakes(document): Correct trace-context verification comparison base * no-mistakes(review): Captain, prevent failed trace delivery metadata claims * no-mistakes(review): Captain, align trace-context tests and verification evidence * no-mistakes(document): Correct trace-context verification evidence * no-mistakes(lint): Suppress intentional ShellCheck literal-dollar warnings * no-mistakes(review): Captain: freeze trace context at session start * no-mistakes(test): Captain: stabilize scheduler test and document Kimi trace coverage * no-mistakes(document): Document trace-context safety boundaries * fix(trace): fail off on stale session snapshots Publish each home session decision atomically through a same-directory temporary file and bind it to the current session lock. A replacement failure can no longer leave an earlier on decision active in a later session; missing, stale, malformed, or unpublishable state defaults safely to off. Add regressions for read-only replacement and failed publication, update spawn and session-start fixtures for the lock-bound format, and refresh the architecture and verification records. * no-mistakes(review): Fix trace spawn failure independence and duplicate safety * no-mistakes(document): Refresh trace-context documentation and verification * no-mistakes(review): Clear partial backend input after failed trace submission * no-mistakes(review): Stop unsafe trace delivery before launch append * no-mistakes(document): Document unsafe trace delivery handling * fix(trace): bound each trace to one routed task, never the routing agent A persistent Secondmate holds its launch-time TRACEPARENT in the process environment for its whole life, and routed requests never replace it, so resolving new-task carriers from the ambient environment chained every routed task into one ever-growing trace per Secondmate with distinct parent ids. Resolve now reuses the task's recorded carrier or mints a fresh sampled root, never reading ambient TRACEPARENT, so each routed task is its own trace boundary while relaunch, recovery, and scout-to-ship promotion keep one stable per-task identity. The spawn regression models the reviewed scenario exactly: two unrelated tasks spawned sequentially through one persistent Secondmate environment record and inject distinct trace ids, adopt nothing from the Secondmate's carrier, and a relaunch of the first task reuses its original carrier verbatim. * docs(trace): define the per-task trace boundary The design contract is one task per trace: a persistent Secondmate is routing infrastructure with its own agent identity, never a shared trace root for the unrelated tasks routed through it. Root/recovery semantics replace the removed child-inheritance path, the sampling and safety sections drop inherited-carrier language because ambient TRACEPARENT is never read, and the verification page records the refreshed suite inventories including the two-task Secondmate boundary regression. * test(trace): adopt the explicit per-task delivery contract in spawn fixtures Rebasing onto current main brings the explicit per-task delivery contract: ship spawns now require --mode and --yolo instead of resolving them from the project registry. The trace spawn fixtures pass the same explicit contract canonical spawn tests use, preserving the per-task trace boundary coverage unchanged, and the verification page records the refreshed comparison base. * fix(bin): harden tmux agent liveness across harnesses (#1577) * fix(bin): classify tmux agent liveness independent of process titles `fm_backend_tmux_agent_state` attributed a pane solely from `#{pane_current_command}`, which is a process TITLE a harness can rewrite, not a structural fact. Claude Code 2.1.220 reports its version string there, so a live Claude endpoint classified `ambiguous`: the session-start secondmate liveness sweep could no longer see it, and any consumer that gates on a positive classification refuses outright. Read a second, independent name source: the kernel `comm` of every process in the pane tty's foreground process group. Either source naming a verified harness yields `alive`, because a false `dead` is the one verdict that can start a duplicate agent on a live worktree. Scoping to the foreground process group rather than the pane's descendants keeps a harness-named background process from faking an agent, and covers multi-process launchers (the Pi Launcher path) without a special case. Verified on 2026-08-03 against all seven adapters running for real on tmux 3.6a / macOS 26.5.2 arm64: claude 2.1.220, codex-cli 0.146.0, opencode 1.18.11, pi 0.82.0, pi-signed 0.82.0, grok 0.2.118, kimi 0.31.1 all classify `alive`, each attributed by a source independent of its title. Two tests, because they fail for different reasons: - tests/fm-tmux-agent-liveness.test.sh pins the logic with real processes and no harness, so it runs everywhere CI runs tmux. It drives the two name sources apart on purpose and asserts the divergence, so no case can go quietly vacuous. - tests/fm-harness-liveness-drift-live-e2e.test.sh relaunches every installed harness and fails naming the harness and version when one stops being attributed by a title-independent source. AGENTS.md section 4 carries the resulting standing rule, and firstmate-coding-guidelines owns how to satisfy it. * no-mistakes: apply CI fixes * docs: move the harness-dependent-check policy out of AGENTS.md The standing rule was stated in AGENTS.md section 4 with the mechanics in firstmate-coding-guidelines, which split one contract across two owners and charged every session for a rule that only fires when firstmate's own harness-dependent code is being changed. firstmate-coding-guidelines is now the single owner of both the rule and how to satisfy it: real-harness proof required, that proof authorized to spend tokens, structural signals preferred over vendor-rendered surfaces, and a guard that fails loudly naming the harness and version where a surface signal is unavoidable. No inline stub is left behind, because AGENTS.md already carries the load trigger for that skill in sections 7 and 13, so it is read before any change to firstmate's shared tracked material. Also records the cross-platform lesson the pipeline caught in the portable regression, and corrects that file's header: the divergence assertion lives on the version-string case, which diverges on both supported platforms, rather than on every case. * no-mistakes(review): Harden tmux liveness identity and drift validation * no-mistakes(document): Clarify cross-platform tmux liveness documentation * feat(bin): propagate trace context to remote secondmates (#1609) * feat(bin): trace remote secondmate routes and unify the inherit allowlist Per-task W3C trace context (#995) resolved and injected its carrier only at the local spawn path. A remote secondmate is routed through spawn_remote_secondmate, which returns long before that site and wrote its own metadata block, so a remote secondmate stayed silently untraced even with the capability enabled. The parent home still owns that task's identity, because it holds the metadata an observer reads. It now resolves the carrier against the task's own meta under its own frozen decision - reused verbatim on relaunch, freshly rooted otherwise, never adopting the parent process's ambient TRACEPARENT - and hands it to the configured host through a new fm-spawn --traceparent argument, accepted only for a secondmate launch and only as a strict W3C value. The remote host exports it at the same unconditional pre-launch site and reports back the carrier its endpoint actually holds, which the parent records, so an already-alive endpoint reports the identity its agent really received rather than one the parent merely intended. Disabled remains byte-identical and off. The remote inherit path also carried its own hardcoded copy of the inheritable config set, already drifted from FM_INHERITABLE_CONFIG by trace-context. Both remote ends now derive from that one declaration, so a future item cannot be sent by one side and refused by the other, and session-scoped enablement items are skipped on live convergence exactly as the local path skips them. Also fixes a latent stderr leak: an absent session lock printed a raw redirect failure, which the new remote resolve site made visible. Adds tests/fm-remote-secondmate-trace-context.test.sh, driving the real parent -> fm-on -> remote entrypoint -> control -> remote fm-spawn chain over the deterministic SSH boundary and reading the carrier back from the remote pane's own log. * no-mistakes(document): Clarify remote trace and allowlist contracts * feat(bin): preflight remote runtime tool paths (#1623) * feat(bin): widen the remote runtime PATH and add a remote doctor preflight The fixed remote entrypoint hard-coded a four-directory PATH, so a remote account whose tools live under nix or a per-user profile could not run basic Firstmate work without a login shell. The entrypoint now composes its child PATH from the code root's bin, the account's ~/.local/bin, the common package-manager directories that actually exist on the host, and the portable system tail, deduplicated and in a fixed order, still under env -i with the same variable allowlist and no shell command string. fm-remote-doctor.sh reports that exact PATH by inheriting it from its own entrypoint launch rather than recomposing it, so the ordering keeps one owner. It is read-only, reports where each required and optional tool resolved, and exits non-zero naming every required tool that did not. Remote seeding runs it as a preflight before anything is created on the host and restores the registry when it fails. * no-mistakes(review): Harden remote git authorization and missing-tool diagnostics * no-mistakes(document): Document remote PATH doctor and safe shims * no-mistakes(lint): Fix ShellCheck findings in remote path tests * no-mistakes(lint): Suppress exported fixture's false-positive ShellCheck warning * feat: gate remote second mates on Herdr readiness (#1639) * feat(bin): gate remote second mates on herdr readiness A remote second mate now always runs on the Herdr backend, whose server belongs to the host's GUI login session and therefore outlives the SSH connections that supervise it. fm-spawn's remote route forces that backend and the host-local control script refuses any other, so the requirement cannot be dropped from either side. fm-remote-doctor.sh becomes the single owner of what "ready" means. It keeps its PATH and tool reporting from #1623 and adds the Herdr, Aqua LaunchAgent, GUI-session, server-reachability, and entrypoint-symlink checks, tagging each gap fixable: or human: with the exact operator step. --fix closes only the automatable gaps - writing and loading the Aqua-scoped dev.firstmate.herdr launch agent, starting the server where no launch agent applies, and recreating the entrypoint symlink - then re-derives every check from the host, so a human gap is never presented as fixed. It never creates a login session, writes an auto-login password, or touches FileVault. Remote seed, remote spawn, and the startup liveness relaunch all run the same check, repair, re-check sequence through one shared library and fail closed with the doctor's own gap text. Recovery inherits the gate because it respawns through the same route. Tests drive the real doctor against a controlled account fixture with a private HOME, a state-backed launchctl, and a fake herdr, and prove the dangerous actions are never attempted. The remote lifecycle suites gain a stateful Herdr CLI fixture and answer the readiness gate at the SSH boundary, so they never inspect or repair the runner's own account. * no-mistakes(review): Validate launch-agent contract and confirm Herdr startup * no-mistakes(review): Validate loaded launch-agent contract before readiness * no-mistakes(review): Refuse legacy remote backends without altering routes * no-mistakes(review): Clarify conditional remote readiness repair sequence * no-mistakes(review): Repair remote readiness before liveness probing * no-mistakes(review): Preserve unknown seeds and reject legacy liveness * no-mistakes(document): docs: clarify remote Herdr backend ownership * fix: isolate remote secondmates in shared Herdr session (#1659) * Pin remote secondmates to fm-remote * no-mistakes(review): Fail closed on legacy remote Herdr endpoints * no-mistakes(review): Isolate fm-remote launch agent from interactive default * no-mistakes(document): Document shared remote Herdr retirement safety * feat: route remote commands through an Aqua job worker (#1660) * feat: run remote commands through Aqua job worker * no-mistakes(review): Enforce remote job deadlines and safe worker shutdown * no-mistakes(review): Refresh stale workers and harden dependency-free supervision * no-mistakes(review): Harden worker ownership recovery and shutdown quarantine * no-mistakes(review): Fix doctor bootstrap, harness repair, and output draining * no-mistakes(review): Probe doctor tools through authenticated worker bootstrap * no-mistakes(review): Refresh stale workers before doctor tool probes * no-mistakes(review): Recover stopped quarantines and extend job deadlines * no-mistakes(review): Separate queue and execution timeout windows * no-mistakes(review): Supervise Linux worker crashes and bind root identity * no-mistakes(review): Resolve authorized Nix profile bin links * no-mistakes(review): Clarify Nix path resolution documentation * no-mistakes(review): Harden PATH safety and nvm selection * no-mistakes(review): Honor nvm system defaults and refresh doctor digest * no-mistakes(review): Keep workers ready during active jobs * no-mistakes(review): Bound pre-execution validation by job timeout * no-mistakes(document): Clarify remote worker documentation * no-mistakes(lint): Fix remote worker ShellCheck diagnostics * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * fix: clarify remote doctor bootstrap path (#1691) * fix(bin): bound remote SSH dead-peer detection (#1699) * fix(remote): arm SSH dead-peer detection in fm-on.sh A vanished remote host mid-poll (a reboot, a dropped link) left ssh blocked indefinitely on a half-open TCP connection, because fm-on.sh's ssh invocation had no ServerAliveInterval/ServerAliveCountMax. This wedged the remote-reply ferry: fm-procevent.sh's runner blocked inside the ssh child and never reached its own no-result -> claim-release -> reconcile re-arm self-healing path, which otherwise already handles a nonzero exit with empty output correctly. Recovery required a manual retire and re-arm. Arm ServerAliveInterval=15 and ServerAliveCountMax=3 by default (bounded ~45s detection window), both overridable via FM_SSH_ALIVE_INTERVAL and FM_SSH_ALIVE_COUNT_MAX. This is a transport- level fix in fm-on.sh, so it covers every remote command routed through it, not just the reply ferry. The remote sshd answers keepalive probes independently of whatever the remote command is doing, so a legitimately long-but-alive command (a 55s poll, a clone, the doctor) is never falsely killed - only a truly vanished peer trips it, turning that case into a bounded, detectable ssh failure (exit 255) instead of an indefinite hang. Extends tests/fm-on.test.sh with a behavioral regression asserting a bounded, positive ServerAliveInterval/ServerAliveCountMax on the real ssh argv captured through the FM_SSH_BIN process seam, plus coverage that both are env-overridable. * no-mistakes(document): Document SSH dead-peer detection ownership * fix: report stale AXI tools during bootstrap (#1701) * feat(bootstrap): gate stale axi CLIs at the floors firstmate actually uses Add gh-axi 0.1.29 floor so bare --squash PR merges stop failing quietly on older builds. Raise tasks-axi to FM_TASKS_AXI_MIN=0.2.2 (multi-id mv) while keeping feature probes. Keep quota-axi at 0.1.16 after verifying schema 3 and per-model availability already ship there; runway remains optional. * no-mistakes(document): Clarify AXI compatibility documentation ownership * fix: prevent false watcher-down alarms in Claude sessions (#1661) * fix(guard): stop false watcher-down alarm mid-turn under Claude auto-arm bin/fm-guard.sh derived its watcher-health verdict from fm_watcher_healthy, which requires a live watcher process holding the home lock. Under the Claude Stop-hook auto-arm supervision model the watcher is armed at each turn end and exits on its wake, so it runs only between turns. Every guarded command run mid-turn therefore found no live watcher and printed the "WATCHER DOWN - SUPERVISION IS OFF" banner even though supervision was healthy. Because the episode key was derived from the beacon mtime (which the between-turns watcher advances every poll), the full banner re-printed on essentially every command, and the message always blamed a "fresh beacon" that was in fact fresh. Make the pull guard's health check model-aware via a new fm_watcher_supervision_verdict in bin/fm-wake-lib.sh: - Under the auto-arm model a beacon fresh within FM_GUARD_GRACE is healthy even with no live watcher process; only a beacon stale beyond grace (or absent) is a genuine lapse and alarms. - Under every persistent-watcher harness (codex foreground checkpoint, opencode/pi/grok background arm, tmux, unknown) a live identity-matched watcher with a fresh beacon is still required, unchanged. The banner now names the true failing condition, a missing live watcher process versus a genuinely stale beacon, instead of always blaming the beacon, and the once-per-episode dedup keys on that condition rather than the beacon mtime so a genuine lapse announces once and does not re-print each turn. The turn-end guard keeps the strict fm_watcher_healthy check because it fires at the turn boundary, where the auto-arm brings a fresh watcher up and it cooperates with that arm. fm_watcher_healthy itself is unchanged, so the arm layer's start/attach/replace decisions are unaffected. Tests in tests/fm-guard-stale-banner.test.sh cover the auto-arm healthy fresh-beacon-without-a-watcher case, the auto-arm stale-beacon alarm and its stable episode, the true-reason banner wording, and the reason-keyed episode surviving a beacon mtime change; existing persistent-model cases are pinned to that model. * no-mistakes(review): Pin secondmate supervision model to launched harness * no-mistakes(document): Align watcher documentation with model-aware supervision health * test: prevent fixture temporary directory leaks (#1704) * fix(tests): stop fixture-tempdir helper from self-deleting under command substitution fm_test_tmproot is almost always called as `TMP_ROOT=$(fm_test_tmproot prefix)`, which forks a subshell to capture its stdout. The old implementation set its EXIT cleanup trap inside that call, so the trap fired - and deleted the fixture root - the instant the subshell exited, before the real caller's own EXIT trap was ever installed. Every test using the documented call pattern leaked its fixture root on every run; two suites had already independently discovered and worked around this with ad-hoc mktemp calls. Registration now goes through a $$-keyed registry file instead of in-process state, since $$ resolves to the invoking shell's PID even inside the subshell. The real cleanup trap is armed once at source time (always the real caller, never a subshell) for EXIT, INT, and TERM. A best-effort orphan sweep on next source reaps marked fixture roots old enough to be from a killed prior run. Simplifies the two existing ad-hoc workarounds (fm-procevent.test.sh, wake-helpers.sh) back onto the shared helper now that it works correctly. * no-mistakes(review): Preserve live fixtures during orphan reaping * no-mistakes(review): Harden fixture ownership against PID reuse * no-mistakes(review): Secure cleanup registry against path precreation * no-mistakes(review): Make fixture registration transactional * no-mistakes(document): Documentation already matches fixture cleanup behavior * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * feat(herdr): enable presentation spaces by default (#1708) * feat(herdr): default presentation spaces on with an explicit opt-out Herdr's disposable one-task presentation workspace was opt-in through the presence of local config/herdr-presentation-spaces. It is now on by default, and a home opts out by writing "off" into that same file. Values are read with the whole-file whitespace-stripped convention the other scalar config items already use, plus case folding. An absent file, an empty file, and "on" all resolve on; only "off" opts out; an unrecognized value warns and keeps the default rather than failing a spawn over a purely visual setting. The empty file is exactly the historical opt-in form, so every home that had already enabled the projection stays enabled with no migration step, and no previously enabled home can be turned off by the flip. Because absence now means on at both ends, secondmate inheritance needs no item-specific convergence: mirroring an absent primary file converges a secondmate to the same default-on rather than turning its projection off, and only an explicit primary opt-out propagates the opt-out. The gate itself moves into fm_backend_herdr_presentation_enabled in the Herdr adapter so the semantics have one owner that regressions can exercise directly. * no-mistakes(document): Document Herdr default-on presentation safety --------- Co-authored-by: kunchenguid <kun-1@kunchenguid.com> * fix(bin): surface fleet-wide open decisions on every wake drain (#1711) * fix: surface consolidated open decisions on every wake-drain A needs-decision or blocked event buried under later, unrelated status appends was only ever shown via the last-line wake annotation, so a still-open captain decision could go silently missed even though status_open_decisions (fm-classify-lib.sh) already folds the whole status stream correctly and fleet-snapshot/bearings already reuse it. Wire that same fold into bin/fm-wake-drain.sh: a new fleet-wide scan_open_decisions wrapper scans every state/<id>.status, and fm-wake-drain.sh prints a separate, bounded OPEN DECISIONS section on every drain (including the empty-queue fast path), so session-start and every wake-handling turn surface it for free without duplicating the open/resolved fold itself. Heartbeat wakes drain through the same script, so this covers that surface too. Also tighten status_open_decisions' file guard to skip an unreadable status file instead of leaking a bash redirection error, now that a fleet-wide directory scan can reach files a single targeted read would not. * no-mistakes(review): Prevent status symlinks leaking open decisions * fix: drop unbounded perl subprocess from status symlink guard The review step's own symlink-safety auto-fix (O_NOFOLLOW read via a perl subprocess) forked one perl process per status file scanned by the new fleet-wide open-decisions scan, with no cap - inflating fm-wake-drain.sh's total external-read cost from 8 (the existing annotation read_cap) to 18 in the enrichment-caps regression test. The plain [ -L "$f" ] check already rejects any status file that is itself a symlink before any read happens, which is exactly what the new regression test exercises and is the same defense level the sibling scan_captain_relevant_statuses/last_status_line already rely on elsewhere in this file (no O_NOFOLLOW). Drop the subprocess-based nofollow read and keep the cheap builtin guard. * no-mistakes(document): Document actionable fleet-wide open decision drains * fix(bin): abort parked runs and reap leaked processes before teardown (#1710) * fix(bin): abort orphaned no-mistakes runs and reap leaked processes at teardown Teardown could remove a task's worker while its no-mistakes pipeline run was still parked at a gate, leaving an orphaned run holding a fleet slot indefinitely (observed 2026-08-03: runs parked 7h39m and parked at a post-CI approval gate). It could also leave backgrounded/disowned descendant processes rooted under the worktree or tasktmp surviving reparented to init (observed: two `go test` binaries pinning CPU for hours with no live task meta to attribute them to). Add two coupled pre-teardown steps, both scoped to this task's exact branch/head or worktree/tasktmp so they can never touch another task's run or processes: - conclude_task_no_mistakes_run aborts a run parked at a gate via `no-mistakes axi abort`, cd'd into the exact worktree so the daemon resolves the run itself rather than teardown naming a --run id. - reap_task_worktree_processes sweeps for processes whose cwd is under the worktree or tasktmp (via `lsof -a -d cwd`) and TERM/KILLs them. Both run before any worktree return, branch delete, or backend kill, and are idempotent on a retried teardown. The branch+head attribution logic is factored out of bin/fm-crew-state.sh into the new shared bin/fm-nm-run-lib.sh so both scripts use the same ownership contract. * no-mistakes(review): Fail closed on incomplete teardown cleanup * no-mistakes(review): Bind teardown cleanup to verified run and process identities * no-mistakes(review): Require confirmed aborts and convergent identity-safe process reaping * no-mistakes(review): Handle process exits during teardown identity checks * no-mistakes(review): Restore teardown library in hermetic gotmp fixtures * no-mistakes(document): Document teardown run attribution and timeout * no-mistakes(lint): Rename shell variable conflicting with done keyword * no-mistakes: apply CI fixes * fix: resolve fm-remote-entrypoint.sh SCRIPT_DIR through a PATH symlink (#1709) The script installs as a symlink under ~/.local/bin. Taking dirname of the symlink itself (instead of its real target) pointed SCRIPT_DIR at ~/.local/bin, breaking sourcing of the sibling fm-remote-job-lib.sh. Resolve the real path first, preferring python3's os.path.realpath, then realpath, falling back to the raw BASH_SOURCE on hosts with neither. * fix(pi): gate Calm built-in overrides by activation state (#1724) * fix(pi): stop Calm claiming a built-in tool name another extension owns fm-calm.ts claimed bash/read/edit/write/grep/find/ls unconditionally at extension load, regardless of whether Calm was on. Pi resolves two extensions registering the same built-in name by first-registered-wins with no merge and no unregister call, and Calm's project-local .pi/extensions/ position beats any global or CLI-configured extension, so a user who never even enabled Calm could have their own bash/read/etc override silently replaced. Captain-approved plan implemented: - Registration is now gated on config/calm already being "on" at load time. A Calm-off session or reload registers nothing, so a non-Calm user never contests a name. This stays synchronous during the factory's own load, not deferred to session_start: /reload (and ctx.newSession/fork/switchSession) render the restored transcript from a pre-session_start snapshot of the tool registry, so a deferred claim would miss that render - confirmed by tests/fm-calm-pi-extension .test.sh's hidden-block-geometry E2E when trialed. - The first time Calm turns on in a session that started off (activateBuiltInsIfNeeded, from the /calm command handler), Calm calls pi.getAllTools() - safe only once every extension has finished loading, unlike the load-time path above - to see whether a different extension already owns a name, and skips claiming only that one, leaving it and its owning extension fully intact and callable. - A contested name found this way prints a prominent ctx.ui.notify() warning naming the tool, plus a console diagnostic. - reportBuiltInLosses() remains the backstop for the one case neither of the above can reach: a session that starts or reloads with Calm already on, where the registry snapshot is taken before Calm gets any chance to check ownership. A symlink-safe realpath comparison avoids misreporting Calm's own registration as foreign when its path crosses a symlink (macOS /tmp, /var). Confirmed, bounded trade-off: the very first time a session that started Calm-off turns Calm on, tool-call rows already on screen from before that toggle do not retroactively collapse, because Pi never lets an extension re-point an already-rendered row at a definition registered later. Every session after that first toggle starts with the preference already on and takes the synchronous load-time path, so the guarantee is intact from then on. docs/calm.md and the file's own header document this in full. tests/fm-calm-pi-extension.test.sh gains test_builtin_gate_load_time (config/calm off registers nothing, on registers all 7 synchronously at load) and test_calm_activation_collision_and_regression_bound (first activation claims every uncontested built-in, leaves a foreign bash tool fully intact and callable, warns and logs the contested name, and locks in the documented pre-activation bound against real ToolExecutionComponent rendering). test_rendering_and_session_lifecycle and the live interactive E2E are updated for the new gate-at-load and first-activation-bound contract. * no-mistakes(document): Document Calm tool collision boundaries * no-mistakes: apply CI fixes * fix(bin): persist secondmate parent bindings for cleanup (#1727) * fix(bin): give secondmate homes a durable parent binding record Finished-worker cleanup on a remote second mate refused forever with "cannot resolve the primary home ... durable parent binding". The remote launch hands the child the remote code checkout as its parent home (fm-spawn.sh's sole writer of FM_PUBLIC_FOLLOWUP_PRIMARY_HOME receives FM_HOME=$FM_ROOT from fm-remote-secondmate-control.sh's host-local launch), and that path can never carry the parent's real records, so the guard refused unconditionally once relay looked active anywhere on that host. fm-home-seed.sh and fm-remote-home-provision.sh now write a durable .fm-secondmate-parent record next to the .fm-secondmate-home identity marker, naming the home's route to its parent as local (with the real parent path) or remote (with the parent's SSH alias for diagnostics only). fm-teardown.sh's cleanup gate reads it: a remote parent is out of scope for the delegated-promise check (the whole promised-public- reply subsystem is same-filesystem by construction, so a remote parent can never hold one), while a token committed directly to the child's own .env file - never the process environment - still refuses, so an unrelated export in the remote host's login shell can no longer mask in. For a local secondmate, the durable parent_home now also backs up the launch-time env var, closing a silent fail-open where a restart that dropped the launch prefix made the guard treat a genuinely active parent relay as off. Regression coverage drives the real remote route (SSH boundary + Herdr fixture) and real fm-home-seed.sh seeding rather than hand-crafted markers. * no-mistakes(review): Captain: fail closed on unsafe durable parent records * no-mistakes(review): Captain: enforce durable parent binding commit protocol * no-mistakes(review): Captain: publish local parent binding before identity * no-mistakes(review): Captain: refuse conflicting local parent bindings * no-mistakes(review): Captain: reject non-regular secondmate seed leaves * no-mistakes(review): Captain: enforce unique durable parent bindings * no-mistakes(review): Captain: reject route-incompatible durable parent fields * no-mistakes(document): Document durable secondmate parent bindings * no-mistakes(lint): Fix secondmate parent parser ShellCheck warnings * no-mistakes: apply CI fixes * feat(bin): enforce latest AXI-family tool floors (#1733) * feat(bin): gate lavish-axi at its session_ended floor in bootstrap bin/fm-procevent-lavish.sh decides that a human "Send & End" review is terminal by reading session_ended from the poll response's leading session block. That field first shipped in lavish-axi 0.1.35, so an older installed build silently leaves every ended review source armed forever and captures an empty ended result on each later cycle. The same release is what makes a plain reopen refuse a session the human deliberately ended. Add LAVISH_AXI_MIN=0.1.35 to the existing axi-family floor structure in bin/fm-bootstrap.sh, reusing tool_version_at_least and the same MISSING diagnostic gh-axi already emits, so an incompatible build is reported as an upgrade request before any review surface is armed. Later lavish-axi releases only add artifact-authoring surface the adapter never reads, so the floor is the feature-introduction point rather than latest. Fixtures that stubbed lavish-axi as a bare exit-0 tool would now be read as unparseable builds, so tests/lib.sh gains fm_fake_version_tool and every bootstrap-running suite uses it for lavish-axi. * no-mistakes(review): Clarify lavish-axi version floor rationale * no-mistakes: apply CI fixes * feat(bin): set axi-family floors to current latest under the bump policy The axi-family bootstrap floors are the CURRENT LATEST published version of each tool, captain-bumped periodically to move the whole fleet onto the newest axi tools. They are not the minimum feature-introduced version. The earlier lavish-axi work set a feature-minimum floor, which is the opposite of this policy, so replace it along with the older feature-minimum rationale carried by tasks-axi and quota-axi. State the policy explicitly in bin/fm-bootstrap.sh's header, which owns it, and in each per-tool floor owner, so no future change argues a floor back down to the earliest release that happens to satisfy some behavior. Remove the lavish-axi session_ended and upstream-PR citation, the tasks-axi multi-ID-mv minimum argument, and the quota-axi credential-source argument as floor rationale; the tasks-axi feature probes remain as a separate defense-in-depth concern. Floors: lavish-axi 0.1.45 (was 0.1.35), tasks-axi 0.2.4 (was 0.2.2), quota-axi 0.1.17 (was 0.1.16), gh-axi 0.1.29 unchanged and already latest. Each was verified against the tool's current published version. The mechanism is unchanged: the same shared version helper and the same MISSING diagnostic path. The below-fires and at-or-above-silent regression rows move to the new floors, keeping each boundary genuine by pinning the patch immediately below each floor rather than a version that was only below the old one. Fleet fixtures move to the new floors so a bootstrap- running suite is not reported as an out-of-date build. Three operator-facing backlog handoff and receipt errors named "0.2.2+" while the enforced floor moved, so they now point at the floor's owner instead of duplicating a version number that drifts. * no-mistakes(review): Centralize AXI floor policy beside constants * no-mistakes(review): Clarify bootstrap boundary test comment * no-mistakes(document): Centralize AXI floor policy rationale * fix(bin): bound open decision scans with incremental cursors (#1737) * fix(bin): bound OPEN DECISIONS scan cost with a per-status-file cursor The fleet-wide OPEN DECISIONS scan added in #1711 re-reads and refolds every task's entire lifetime status log on every drain, so its cost grows unbounded with total log size. Add status_open_decisions_incremental and scan_open_decisions_incremental to fm-classify-lib.sh: they persist a per-status-file byte cursor plus the folded open-decision set, and fold only newly appended bytes on each call, reusing status_open_decisions' exact fold-line rule (extracted into _fm_decision_fold_line) so the two strategies can never disagree on what is open. A missing or invalidated cursor (new task, truncated/rewritten/shrunk log) falls back to a full re-fold. bin/fm-wake-drain.sh now calls the incremental wrapper instead of the whole-file scan. * fix(bin): add O(1) rotation detection and read-failure guarding to the cursor fold Add the two pieces the incremental open-decisions cursor was missing, scoped to this repo's actual status-file usage (create-once, append-only, never replaced or rewritten in place): - An O(1) device+inode identity check (one stat call) alongside the existing size-shrink check, so a status file replaced/rotated/recreated at the same path is detected and falls back to a full re-fold, even when the replacement is the same size. A same-inode, same-size, in-place byte edit is a deliberately accepted gap: no code path in this repo ever does that to a status file. - Checked reads: a stat/wc/tail failure is a genuine I/O error, not "the file is empty" - it now reports the already-trusted persisted open set unchanged instead of risking a silent invalidation. Both stay O(1) plus new bytes per call, matching the cursor's bounded- cost design; no content hashing or pending-fragment machinery. * no-mistakes(review): Preserve cursor state across failed incremental reads * no-mistakes(review): Refold status when cursor cache reads fail * no-mistakes(document): Document cursor-backed open-decision scanning * no-mistakes: apply CI fixes * fix(bin): prevent remote polls from blocking session startup (#1754) * fix(bin): preempt remote reply long-polls for queued short jobs Session start on a home with live remote second mates could stall silently for many minutes: the single serial remote job worker ran each armed fm-remote-delta-read.sh reply poll to its full 55s window while bootstrap's short sync, inherit, state, and route commands sat queued behind it, and non-FIFO queue pickup let re-armed polls keep winning the lane. Measured end to end, a trivial short job took 31s behind one 30s poll window. The worker now preempts a running preemptible job (the read-only, cursor- anchored delta read is the only member of that class) as soon as a non-preemptible job is queued, publishing exit 75 with emptied output - byte-identical to the poll's own elapsed-window-with-no-data result - so the parent runner takes its existing no-result path and the watcher re-arms from the same cursor with nothing lost. The delta read translates SIGTERM into that same exit after removing its staging directory. Sibling polls never preempt each other, so two armed monitors cannot churn. The same measured scenario now completes in 1s. * no-mistakes(document): Clarify remote poll preemption documentation * docs: present X mode as the X and Discord public surface (#1778) Discord mentions already ride the same pairing-token opt-in, relay poll, and platform-aware reply path as X mentions, but the docs still read as X-only, so a stranger could not self-serve the Discord path. Add the numbered turn-on steps to the X mode configuration reference, pointing at the myfirstmate dashboard for account creation, bot install, and token issuance rather than duplicating operator setup here, and drop the X-only framing from the README bullet, the documentation index, and the architecture overview. * fix(bin): run session start deterministically from hooks (#1781) * feat(bin): run session start deterministically on hook-capable harnesses Session start relied on a native nudge that only asked the agent to run bin/fm-session-start.sh, and an agent can defer that. Observed 2026-08-01: an /ahoy-first session followed the recap path and did not take the helm until a later request forced it. Claude, Codex, and Pi now RUN the digest in their session-open hook through the new bin/fm-sessionstart-run.sh, so the full ordered digest is in model context before the first turn. That wrapper is the single owner of what a session-open source means: startup and Pi's "new" take the helm, clear and compact re-emit, resume/reload/fork delegate to the nudge, and an unreadable source takes the helm because doing that redundantly is idempotent while skipping it is the bug. Grok and OpenCode keep the nudge as the floor, since neither can carry hook stdout into a model turn. Because the hook now blocks session initialization, fm-session-start.sh bounds itself first. Its steps are not all individually bounded - bootstrap's gh auth probe, tool version probes, the backlog listing and per-task endpoint reads are unbounded - so the whole digest runs as one bounded child (default 120s). Whatever it emitted before the bound survives, and the parent adds a loud STARTUP TRUNCATED banner naming the stage that stalled and every stage that never ran, still exiting 0. --reemit skips only the sweeps startup already reconciled. It still re-verifies lock ownership and still drains queued wakes, which arrived after startup and are the turn's work. fm-bootstrap.sh gains FM_BOOTSTRAP_LOCKED so a re-emit keeps repair ownership instead of deferring to a lock holder that is itself. Also adds bin/fm-timeout-lib.sh as the single owner of bounded execution, replacing three near-identical copies, and gives the ahoy skill a helm check so a nudge-tier harness cannot recap before taking the helm. Verified live on 2026-08-05 against Claude 2.1.222, Codex 0.146.0, and Pi 0.82.0; docs/verification/supervision.md records the per-harness source vocabulary, the two named gaps, and the refresh command. * no-mistakes(review): Harden session-start completion, timeout, and Pi delivery * no-mistakes(review): Harden completion ownership and portable timeout escalation * no-mistakes(review): Normalize watchdog KILL exits without masking command status * no-mistakes(review): Guarantee startup bounds and align harness delivery tiers * no-mistakes(test): Fix Pi session-start live verification fixture * no-mistakes(document): Align session-start documentation with determinis…
verbagem
added a commit
to verbagem/firstmate
that referenced
this pull request
Aug 21, 2026
* feat(linear): Modern AI Productivity Pack dispatch tooling
Component 2's Firstmate-side half: fm-linear-poll.mjs claims pending rows
from the Supabase bridge table (populated by ops/linear-dispatch-webhook/,
PR #197) and dispatches real crewmates, routed by Linear team id per the
2026-07-20 team-per-business restructure. fm-linear-team-scaffold.mjs and
fm-linear-team-reshape.mjs create/reshape Linear teams into the standard
5-state/3-label dispatch shape, used to stand up the 13 current teams.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZTwc4SabVJiWp2ZhR2UV3
* fix(linear): full Notion Project coverage + real Notion task creation
- TEAM_ROUTING now has a real notionProject for all 13 teams, not just 2
(the other 11 got null placeholders in the first pass, called out as the
bare-minimum failure mode to stop doing)
- createNotionTask now actually POSTs to the Notion REST API instead of
writing a JSONL request file for some hypothetical future session to
pick up later
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZTwc4SabVJiWp2ZhR2UV3
* Voice protocol doc update, portable schema check
- AGENTS.md: voice-carries-the-interaction preference (captain 2026-07-19/20)
- herdr.sh: replace grep -F with case/glob for POSIX portability
- .claude/settings.json: autoCompactEnabled, drop the focus-gate hook wiring
the captain tried earlier and asked to remove (SessionStart/UserPromptSubmit/
PreToolUse hooks calling command-center/ops/focus-gate.py) - the actual
script lives in a different repo and was never part of this one
* fix(pi-calm): extend compat boundary to Pi 0.82.1
0.82.1's changelog only touches auth and model-catalog code, none of the
ToolDefinition/renderer surface fm-calm depends on, and the presentation-only
test still passes unmodified against it.
* gnhf 1: Ported the Modern AI Productivity Pack's 03-evals/ into a new evals/ directory in firstmate, rewrote tov-eval.md for Brandon Quijano's actual voice with cited sources, and wired bin/fm-brief.sh's ship-mode scaffold to instruct crewmates to run the relevant eval(s) before reporting done.
* gnhf 2: Added dedicated test coverage in tests/fm-brief.test.sh verifying the eval-step wiring (run_eval.sh + FAIL-means-loop-and-fix) renders correctly across all three ship delivery modes, closing a test-coverage gap left by iteration 1's otherwise-working implementation.
* fix(tests): replace source-content .gitignore assertion with behavioral coverage (#1304)
The second assertion in fm-gitignore-config.test.sh (added by #1261) greps
.gitignore for a specific spelling of the config/ ignore pattern. It fails
on a semantically equivalent pattern like config/** and does not prove Git
actually ignores anything, per the completed source-content-test audit.
Replace it with a real git check-ignore control test on a generated
unrelated path, and strengthen the existing directory-coverage test with
generated unpredictable direct and nested config/ paths.
* feat: bound and consolidate startup memory during stow (#1303)
* Add bounded startup memory curation
* no-mistakes(review): Record reproducible stow verification evidence
* no-mistakes(review): Validate inherited secondmate stow evidence
* no-mistakes(document): Document editable startup-memory budget propagation
* fix(herdr): place workers in the launching workspace (#1328)
* fix(herdr): place workers in the launching agent's exact workspace
Herdr enforces no workspace-label uniqueness, and spawn resolved its
container by taking the FIRST workspace whose label matched the home
label. With two workspaces both labeled "firstmate", a worker launched
from the second one was created in the first, so it appeared in a
different space than the Firstmate the captain was watching.
Reproduced end to end on Herdr 0.7.5 protocol 17 by running the real
bin/fm-spawn.sh inside a launcher pane in the second "firstmate"
workspace: the worker landed in w1 while its launcher was in w2, with an
unrelated third workspace focused throughout, which also rules out any
dependence on the focused workspace.
Placement now binds to the launching process's own Herdr identity. Herdr
injects HERDR_PANE_ID, HERDR_SESSION, and HERDR_SOCKET_PATH into every
process it manages a pane for, and fm_backend_herdr_launcher_identity
resolves that pane's current owning tab and workspace live from Herdr,
cross-checking the pane against its tab and confirming the workspace
exists exactly once in the session. The injected HERDR_TAB_ID and
HERDR_WORKSPACE_ID are creation-time snapshots and are deliberately not
read as current identity. Labels are no longer placement authority.
A claimed parent identity that is unreadable, contradictory, stale, or
from another named session or Herdr server stops the spawn before any
worker endpoint exists, rather than degrading to a label search. A
launcher with no Herdr ancestry has no workspace to inherit and keeps
the per-home labeled container, which must now resolve to exactly one
workspace; two same-labeled candidates refuse instead of adopting
either. A --secondmate launch keeps standing up that home's own
workspace by design.
With presentation spaces enabled, the projected child is created and
bound under that same exact parent and anchors its ordering on it, so a
duplicated home label no longer makes the layout ambiguous. Projection,
focus restoration, restart binding, and quarantine rules are unchanged,
and children are never collapsed into the parent. tmux, Zellij, cmux,
Orca, and the away-mode daemon terminal were each inspected and are not
affected: none resolves a container by searching mutable labels.
tests/fm-backend-herdr-launcher-workspace-e2e.test.sh drives the real
spawn and teardown against an isolated Herdr lab, with its headline case
running fm-spawn.sh inside a real Herdr pane so the identity comes from
Herdr's own injection. The refusal matrix and the ordering anchor are
covered deterministically in tests/fm-backend-herdr.test.sh.
Eight existing real-Herdr suites inherited the developer terminal's own
Herdr pane into their isolated lab sessions, which the new cross-session
check correctly refuses. tests/herdr-test-safety.sh now owns
herdr_forget_inherited_pane and those suites call it, so what they assert
no longer depends on where they were launched from.
Two unrelated fixes found along the way. tests/fm-secondmate-harness.test.sh
had the same class of environment leak through CLAUDECODE, which outranks
PI_CODING_AGENT in bin/fm-harness.sh and made its pi-signed ancestry case
resolve "claude" whenever the suite ran inside Claude Code. And
fm-spawn.sh's usage() printed a fixed line range that had already been
truncating its own help mid-sentence.
* no-mistakes(review): Enforce exact Herdr launcher and projection identity
* no-mistakes(document): Document exact Herdr launcher workspace placement
* fix(calm): refine Calm working boat animation (#1339)
* feat(calm): replace Pi's working row with an animated ship while Calm is on
While Calm is active and one logical agent run is under way, Calm now hides
Pi's built-in working row and renders a small two-row SSHHIP-derived boat in
its place. When Calm is off, Pi's stock working row is left untouched.
The presentation uses only public Pi extension API: setWorkingVisible(false)
plus a temporary setWidget() component whose render(width) owns the responsive
geometry and whose timer requests a TUI render. Visibility follows agent_start
through agent_settled, so the boat does not flicker between tool calls,
automatic continuations, retries, or compaction inside the same run, and
settle, abort, and failure all reach the same cleanup.
fm-calm.ts stays the sole owner of the presentation choice and the only caller
of setWorkingVisible(); the new lib owns the sprite geometry and widget.
* no-mistakes(review): Guarded Calm-off lifecycle visibility writes; focused tests pass
* no-mistakes(test): Fixed Calm E2E wait to include tmux scrollback
* no-mistakes(document): Document Calm working boat behavior
* no-mistakes: apply CI fixes
* feat(calm): slow the Calm boat, animate blue water, and make the sail directional
The boat now moves one column every 880ms while a bounded fixed-cell water phase
advances every 220ms, so the water ripples several times between boat steps and
the presentation reads as calm. One scheduler drives both clocks and disposing
the widget stops them together; ticks rather than wall-clock timestamps drive
every state change, so tests seek animation time exactly.
Colors are standard ANSI foreground codes instead of theme lookups: blue for
every water cell and yellow for the complete boat, each run closed with a
default-foreground reset so nothing bleeds into padding or later frames. ANSI
bytes never enter geometry, so visible width stays exact.
The mainsail is directional and trails aft of the mast: <| travelling right and
|> travelling left. Direction reverses the moment the boat lands on an endpoint,
so the endpoint frame already shows the new heading and no frame at or after a
bounce shows the previous sail.
* test(calm): wait for the Ctrl+O expansion redraw this block asserts
* docs(calm): record the revised working-presentation verification evidence
* no-mistakes(document): Fix Calm feasibility document EOF whitespace
* fix(dispatch): preflight candidate auth before quota escalation (#1349)
* fix(dispatch): scope candidate authentication to its own surface
A locally expired timestamp in one credential store was reported to the
captain as a sign-out, including for dispatch candidates that never read
that store. A `harness=pi, model=xai/grok-*` candidate authenticates
through Pi's own xAI credential, but the only Grok quota reading
available was gated on the standalone Grok CLI's separate token, whose
expiry clock drifts independently. The always-loaded intake rule then
turned that unreadable quota into a mandatory captain escalation.
Add `bin/fm-auth-preflight.sh` as the deterministic owner of the parts
that must not depend on agent memory: it resolves a tuple's
authentication surface from quota-axi's own emitted auth sources rather
than from a harness or model name, so another harness's CLI can never
gate a candidate that does not use it. A vendor CLI is launched only
when the tuple's own harness owns the credential store under test and a
non-destructive discovery command is registered for it, which today is
`grok models` alone. That probe runs at most once with stdin closed and
a hard timeout, reads its verdict from the first stdout line because the
command exits 0 either way, treats unrecognized output as indeterminate,
and never invokes login, logout, or the interactive TUI. Quota is read
at most twice, and unknown headroom never makes a candidate ineligible
on its own.
Update the dispatch procedure to match: usable authentication with
unmeasurable headroom stays eligible at lower preference with the
unknown disclosed, and stop-and-report is reserved for unresolved
authentication, an unresolved relationship, or malformed configuration.
Record that Grok's `credits.remaining` is a prepaid balance rather than
window headroom.
Gate quota-axi at 0.1.16 in bootstrap, the first build reporting
per-credential auth sources. A stale install previously passed the
presence check silently, which is why a fix published two days earlier
was still not in effect.
Replace the orphaned quota-array-dispatch fixtures, which encoded a
`provider: "xai"` shape the tool never emits and had no consumer, with
fixtures shaped like real 0.1.16 output that the new suite drives the
script against. The suite asserts the verdict and, separately, which
vendor CLIs were launched, so a Pi/xAI candidate reaching the Grok CLI
fails. Map `tests/fixtures/<dir>` to its consuming suite so a fixture
change selects the right tests instead of refusing.
* refactor(bootstrap): give the quota-axi floor one owner
The floor was stated twice - once in bootstrap's gate and once inline in
the auth preflight - so bumping it needed two edits that could drift.
Move it to bin/fm-quota-axi-lib.sh alongside its rationale, matching the
existing tasks-axi library, and derive the comparison from the constant
so the number appears exactly once. Bootstrap turns a failing check into
the operator diagnostic; the preflight refuses to emit an unscoped
verdict. Map the new library to both consuming suites so a bump re-runs
them, and record that any usable source means the surface authenticates.
* no-mistakes(review): Captain: bound quota checks and removed Python dependency
* no-mistakes(review): Captain: enforce conservative headroom and exact preflight retry
* no-mistakes(review): Captain: preserve OpenCode eligibility without auth-surface guessing
* no-mistakes(review): Captain: reject malformed OpenCode model relationships
* no-mistakes(review): Captain: exempt verified unmodeled tuples from intake escalation
* no-mistakes(document): Updated dispatch authentication documentation
* no-mistakes: apply CI fixes
* feat(x-mode): reconcile promised public replies deterministically (#1350)
* feat(x-mode): reconcile promised public replies deterministically
A promised final reply in an X or Discord thread was only kept while the
primary remembered it. Compaction or restart erased that memory, so a typed
public-followup obligation could sit at pending-work after its PR merged and
the original thread never got its reply.
Make the promise durable state instead:
- bin/fm-public-followup-emit.sh reports a typed terminal work result (source
home, work id, generation, outcome, safe deliverables, bounded public-safe
text) into the owning home's private inbox. The event id is derived from
that identity tuple, so duplicate reports and restart replay converge with
no coordination, and nothing ever parses a free-form done: sentence.
- bin/fm-public-followup.sh registers a commitment, reconciles events through
tasks-axi public-followup, and runs the idempotent delivery sequence
(begin-delivery with the payload hash, post, record the posted receipt or a
typed error) against the stored platform and opaque thread binding. A
delivery interrupted between post and receipt refuses rather than risk a
second public reply.
- Session start surfaces unresolved commitments from disk, the existing relay
poll surfaces a new terminal-result set once, and teardown refuses while
this home still owes a public reply for that exact work.
tasks-axi public-followup remains the only owner of the obligation state
machine, state/x-context/ the only owner of the private request context, and
fm-x-reply.sh the only thing that posts. Its new optional --receipt-file is
the one addition there, so a caller can record how many messages were sent.
A home that never opted into the myfirstmate relay gates out on a single
[ -f "$FM_HOME/.env" ] test: no tasks-axi call, no backlog or context scan,
no output, and no artifact. Evidence in docs/verification/public-followup.md.
* no-mistakes(review): Hardened public-followup reconciliation and ownership guards
* no-mistakes(review): Hardened typed terminal cleanup and receipt reconciliation
* no-mistakes(review): Automated typed-delivery cleanup and strict backlog validation
* no-mistakes(review): Fail-closed parent resolution and registration-safe delivery
* no-mistakes(review): Harden relay gating and validate secondmate bindings
* no-mistakes(review): Use owner-aware single-gate teardown protection
* no-mistakes(document): Correct public-followup documentation drift
* no-mistakes(lint): Quote done literals to fix ShellCheck warnings
* no-mistakes: apply CI fixes
* feat(bin): replace busy heuristics with semantic lifecycle state (#1327)
* feat: add semantic busy-state contract owner and event writer
One owner (bin/fm-busy-lib.sh) for the captain-approved semantic
busy-state redesign: a per-task gen-bound record written only by
bin/fm-busy-event.sh, per-harness trusted-source classification with
explicit source attribution, busy/idle/unknown/dead semantics where
missing, malformed, stale, or untrusted semantic data is unknown -
never idle - and endpoint death is the only process-level override.
The Grok-only rendered-tail fallback and the standalone-Kimi
verification gate live behind the same classifier.
* feat: arm busy-state at spawn and convert Pi to the semantic extension path
fm-spawn arms the busy-state contract for converted adapters and seeds
busy/fm-spawn (the launch brief is a submitted turn). The Pi/pi-signed
per-task extension now reports agent_start -> busy and agent_settled ->
idle confirmed by ctx.isIdle(), covering auto-retries, compaction
retries, tool loops, and queued continuations, while turn_end stays a
wake notification touch. Teardown removes the new record, gen sidecar,
and lock. Live-verified on Pi 0.82.0: seed -> agent-start busy ->
agent-settled idle with the marker still touched.
* feat: convert OpenCode to the semantic session.status plugin path
The per-task plugin (renamed .opencode/plugins/fm-busy-state.js) now
classifies from OpenCode's semantic session.status events - busy and
retry are active, idle is inactive - latched to the worker's own
session so a subagent child session can never clear the worker's busy
state. The session.idle marker touch stays a wake notification.
Teardown removes both the new and the legacy plugin filenames.
Live-verified on OpenCode 1.17.18 in a real TUI pane: seed ->
session-busy -> session-status-idle.
* feat: convert Claude to the full lifecycle hooks path
The per-task settings.local.json now wires UserPromptSubmit -> busy
and Stop, StopFailure, and SessionEnd -> idle, so API-error and
shutdown turn ends can never strand a busy record; Stop keeps the
turn-ended notification touch. A refused (stale-gen) event exits 0 and
stays silent so Claude's own lifecycle is never broken. Live-verified
on Claude Code 2.1.220: UserPromptSubmit fires for the argv launch
prompt, Stop closes each turn, a mid-stream Escape interrupt fires no
closing hook, and the firstmate-controlled idle/fm-interrupt clear
resolves it.
* feat: gate Codex busy state behind verified semantic sources
The approved contract prefers Codex's app-server turn lifecycle with
capability negotiation and sanctions its lifecycle hooks as the
intermediate. Live probes on codex-cli 0.145.0 show neither is usable
for a pane worker: the app-server daemon is unreachable for a TUI
thread and refuses to start outside the managed standalone install,
and firstmate-written project hooks never fired (interactive with
directory trust granted, and exec, both with
--dangerously-bypass-hook-trust) while global hooks fired in the same
runs. Codex therefore classifies unknown codex-unverified behind an
explicit probe rather than falling back to idle or footer text, and
fm-spawn installs no unverified Codex wiring.
* feat: gate standalone Kimi busy state on live verification
Standalone Kimi has no installed binary here, so per the approved
contract its semantic path stays guarded and it classifies unknown
kimi-unverified rather than idle - and never from its locale-sensitive
moon-phase spinner, which the redesign forbids inventing as a state
source. The gate records the preferred source order (Wire prompt
request lifetime, which brackets a turn and reports cancellation, then
the documented hooks including Interrupt because Stop does not fire on
interrupts) and the exact evidence required to open it. Arming without
wiring would seed a busy record nothing could clear, so both land
together behind the same gate.
* feat: route busy consumers through the contract and drop the global OR
The watcher, crew-state reader, and away-mode daemon now decide busy
state through bin/fm-busy-lib.sh: only an exact busy verdict counts as
working, and unknown never becomes working or a silent idle, so a crew
whose semantic state is missing, malformed, stale, or unverified
surfaces instead of being absorbed. Crew-state reports the producing
source in its detail. The watcher's global OR regex default is gone;
Grok keeps its isolated fallback inside the contract. The daemon's
supervisor-pane reader stays rendered-text - that pane is not a
recorded task - but is now scoped to firstmate's own detected harness
instead of every vendor signature. Secondmate pending-reply
observation is deliberately unchanged and documented as a
delivery-confirmation signal, not task state.
* docs: point busy-state documentation at the single contract owner
Adds a maintainer-architecture section naming bin/fm-busy-lib.sh as
the owner of what busy means, with per-adapter sources, the
unknown-never-idle rule, the endpoint-death override, and the two
rendered-text readers that deliberately stay outside the contract.
Replaces the stale regex-first prose in architecture, tmux-backend,
herdr-backend, and configuration; converts the harness-adapters
per-harness rows from UI signatures to the semantic source each
harness uses; and records the live verification evidence, including
why Codex and standalone Kimi stay unknown.
* fix: arm away-launch signal handlers before acquiring the lifecycle lock
fm_afk_launch_main acquired its lock and only then installed the EXIT,
INT, and TERM traps. A signal arriving in that window terminated the
process by default action and left the lock directory behind, which
blocks the next away-mode launch until the stale-owner reclaim path
clears it. The release helper only removes a lock this process owns,
so the handlers are now armed first. The accompanying test also killed
the child whether or not the lock had appeared and sampled cleanup the
instant wait returned; it now requires the lock, then allows a bounded
settle, so it proves the guarantee instead of racing it.
* test: align fleet, Kimi, lifecycle, and detection suites with the contract
The fleet snapshot and wake-daemon lifecycle fixtures now prove a
working crew through its own semantic busy-state record instead of
rendered pane text, which is what those consumers read. The Kimi
watcher test asserts the approved contract directly: a standalone Kimi
task classifies unknown rather than matching its moon-phase spinner,
while Grok's isolated fallback still classifies only Grok. The
pi-signed detection cases clear ambient harness markers, fixing a
pre-existing failure where the running session's own CLAUDECODE
outranked the fixture's marker.
* fix: stop teardown from deleting a project's own Codex hooks file
An intermediate revision wired Codex through a firstmate-written
<worktree>/.codex/hooks.json, and teardown removed it alongside the
other generated wiring. The Codex wiring was dropped when its probes
came back unverified, so that removal now targets a file firstmate
never creates - and a project may legitimately track its own
.codex/hooks.json, which teardown would then delete from a pooled
worktree.
* fix: keep busy-record parsing from disturbing its sourcing caller
The record parser split fields with set -- under a temporary noglob,
which clobbers a sourcing caller's positional parameters and restores
glob expansion even when the caller had disabled it. The watcher, the
daemon, and the crew-state reader all source this library, so it now
reads fields with read -a, which never globs and never touches caller
state.
* docs: state exactly which Claude hook paths were reproduced live
The busy-state record listed all four wired Claude hooks in the source
column, which could read as a claim that every one fired during the
pass. UserPromptSubmit and Stop did; StopFailure and SessionEnd are
wired from hook names confirmed present in the installed binary, but
the abnormal turn ends they cover were not reproduced.
* test: let reset_fakes own the crew-state busy-text fixture lifecycle
The Grok fallback case set FM_FAKE_BUSY_TEXT and cleared it inline, so
the variable's lifetime was owned by one test rather than by the
shared reset that every other fake already uses.
* no-mistakes(review): Fix semantic busy-state lifecycle races
* no-mistakes(review): Make busy-state retirement idempotent
* no-mistakes(review): Enforce semantic state boundaries for status and injection
* no-mistakes(review): Restore harness-scoped away-mode busy guard
* no-mistakes(document): Refresh semantic busy-state documentation
* no-mistakes: apply CI fixes
* fix: preserve Calm boat continuity across working periods (#1356)
* fix(calm): resume working boat from frozen column across runs
Keep one extension-owned boat animation for the Pi session so settling
freezes column and direction, the next working period resumes there
without hidden-time jumps, and only a fresh session resets to the left edge.
* no-mistakes(review): Freeze Calm boat from last rendered state
* no-mistakes(document): Document Calm boat continuity contract
* fix: restore evidence-based dispatch eligibility (#1358)
* fix(dispatch): judge candidate provider relations instead of rejecting them
Firstmate deterministically dropped supported Pi candidates in the
openai-codex family. bin/fm-auth-preflight.sh resolved a harness=pi tuple's
credential surface by constructing the source id `pi:<model-prefix>`, so
`pi + openai-codex/gpt-5.6-terra` looked for a `pi:openai-codex` source. That
source does not exist, because Pi's Codex family authenticates through the
Codex store quota-axi already lists as `auth-json`/`cli-rpc`. The tuple
returned `eligible=no reason=surface-unresolved` while the Pi catalog listed
the model and the Codex provider reported fresh, usable credentials with 64
effective percent remaining on its all-model scope.
The prefix construction was only ever valid where Pi holds its own credential
(`pi:xai`, `pi:kimi-coding`), which is why every previously configured Pi tuple
resolved and the defect stayed hidden until a Codex-family Pi model was
configured.
Retire dispatch eligibility from deterministic shell. The dispatching first
mate now establishes model support and provider family from each harness's
authoritative catalog, applies quota at the granularity the vendor supplies,
and shows that reasoning. Provider-level and all-model evidence bounds every
model established in that family; a named-model window bounds only its own
model. Missing model-level quota, a missing auth source, unmeasurable headroom,
and unmodeled authentication are disclosed uncertainty. Only concrete
contradictory evidence blocks a candidate.
Replace the preflight with bin/fm-vendor-auth-probe.sh, which keeps the
captain's approved bounded probe envelope without any routing knowledge: it
takes no harness, model, or provider, reads no quota, renders no verdict, and
holds only a fixed-argv safety allowlist. Its behavior suite proves the absent
identity surface, the untouched quota, the uniform exit status, the fixed argv
with stdin closed, and a real bound even when the configured bound is zero.
Also fixed along the way: a zero FM_*_TIMEOUT silently removed the hard bound,
the pinned Grok version had drifted to 0.2.117, and --changed selection refused
outright on any deleted bin/ script.
AGENTS.md section 4 and quota-array-dispatch own the corrected policy,
harness-adapters gets the catalog-responsibility correction, and
docs/verification/dispatch-auth.md records the 2026-07-30 evidence on
Pi 0.82.0, quota-axi 0.1.16, and grok 0.2.117.
* no-mistakes(review): Reject all-zero vendor probe timeouts
* docs: define captain instruction precedence (#1362)
* docs: add captain-authorized inherent red-check merge exception
Keep the default red-PR ban and own one always-loaded exception in the
merge-authority section: captain-explicit PR or bounded batch plus exact
check, only when the failure is inherent to the selected delivery path.
Yolo cannot activate it; final head and the full current check suite must
be verified; other substantive failures remain non-waivable.
* docs: replace narrow red-check exception with captain precedence
Supersede the inherent failing-check merge exception with one always-loaded
Firstmate-local rule: a current explicit concrete captain instruction
overrides a conflicting Firstmate-written standing rule only within exact
scope, never above platform/system/developer instructions. Keep the ordinary
red-PR default and yolo boundary; point section 7 at the section 1 owner.
* docs: define validation supersession sequence (#1407)
* fix: give validation-time captain overrides a supersession sequence
The Validate section let a captain instruction that completely
invalidates the work being validated keep the same task and worker, but
never said how: the adjacent rule flatly bans hand-editing, committing,
aborting, or restarting during an active run with no carve-out, so a
worker facing full invalidation had no sanctioned path forward.
Add the missing sequence: cancel through no-mistakes axi's abort
command, confirm the run has stopped through axi status, recover branch
ownership through axi sync's guarded recovery, only then replace the
obsolete work, and validate once against the final head. The existing
ban on hand-editing an active run now cross-references this sequence
instead of contradicting it.
* no-mistakes(review): Make validation custody recovery conditional
* no-mistakes(document): Clarify validation supersession abort exception
* fix: keep obsolete pipeline commits out of the superseded deliverable
The review-applied fix made custody recovery conditional on
branch_sync.next_action.code, but left an open gap: recovering custody
settles who owns the branch, not what content ships. As written, a
worker could recover an obsolete run's branch and build the
replacement on top of its now-irrelevant commits instead of from the
correct pre-invalidation base, carrying obsolete content into the
final deliverable.
Make that explicit: custody recovery settles ownership, not content,
so the worker replaces obsolete work from the correct base and keeps
the obsolete run's commits out of what gets validated and shipped.
* no-mistakes(test): Restore minimal pre-invalidation replacement instruction
* fix: dedupe redundant "replace the obsolete work" restatement
Line 309 already says the worker replaces the obsolete work from the
correct pre-invalidation base, excluding the obsolete commits. The
closing sentence restated "replace the obsolete work" again before
gating the final validation run, layering the same fact twice instead
of stating it once.
Trim the closing sentence to just the ownership gate and the
single-run-against-final-head requirement it uniquely adds.
* fix: bind backend overrides to exact-task authority (#1413)
* fix: bind explicit --backend to exact-task authority
A Herdr-backed second mate carried a prior one-task --backend tmux
exception forward by analogy, so its child landed in tmux and never
appeared under the second mate in Herdr. Runtime detection was correct;
the authority surface was not.
docs/configuration.md now owns that an explicit --backend is authorized
only for that exact task. AGENTS.md and fm-spawn help point there.
* no-mistakes(document): Consolidate backend selection authorization documentation
* fix(herdr): prevent focus flashes during projected workspace cleanup (#1229)
* fix: remove projected workspaces through Herdr's focus-preserving pane-death path
Herdr 0.7.5's explicit close of a workspace-emptying last pane moves the
attached client's focus to a neighbor workspace, flashing the captain's
whole window and routing in-flight keystrokes to the wrong pane until
Firstmate's exact-tab restore masks it 56-197 ms later.
Teardown and cleanup now plan a workspace-emptying close as a focus-safe
removal: verify the close empties the workspace, reposition the doomed
workspace behind the focused one through the verified workspace.move
transport when it sits before a non-last focused workspace, prove the pane
holds one lone idle shell, and end that shell so Herdr removes the emptied
workspace through its focus-preserving pane-death path. Any ambiguity or
failure falls back to the plain close behind the existing restore backstop,
and fm_backend_herdr_kill applies the same plan for non-projected removals.
Two conditions proven on real hardware are encoded in the adapter: BSD ps
reports a login shell's comm as "-zsh", and an idle shell transiently
hosts a prompt helper right after a workspace.move relayout, absorbed by a
bounded strict-sample settle window in the idle-shell proof, now the single
owner shared with session-start cleanup.
An isolated-lab regression reproduces the raw steal on 0.7.5 and proves the
plan removes a doomed workspace with zero wrong-focus samples and no
corrective focus; unit fixtures cover the position, edge, ambiguity, move
and kill failure, escalation, and transient-helper cases. Upstream fixes
(#1877 explicit close, #1912 pane death) are merged but unreleased; once
released the plan degrades to a harmless reorder-then-remove.
* no-mistakes(review): Confirm pane death from structured not-found responses
* no-mistakes(review): Serialize Herdr kills and sample focus continuously
* no-mistakes(review): Synchronize Herdr focus evidence output
* no-mistakes(review): Refuse unlocked Herdr pane closes
* no-mistakes(document): Correct Herdr focus-safety documentation
* no-mistakes: apply CI fixes
* fix: never erase a Herdr task's records while its pane survives a refused close
A transient presentation-lock contention could produce a completed teardown
while the exact Herdr pane stayed alive as an unowned restored shell: the
kill refused the unlocked close (correctly), returned success, the warning
was suppressed, and cleanup erased the task's status, turn-end, and
metadata records after the isolated copy had already been returned.
Teardown now acquires the named-session presentation lock before anything
destructive: a contended lock refuses up front while the isolated copy, the
task branch, every durable record, and the endpoint are all intact for a
plain rerun, and the projected and flat close paths both run under that one
held lock instead of acquiring their own. Durable records are erased only
once the exact pane is confirmed gone through its structured presence; a
refused, skipped, or failed close retains every record with a visible,
retryable error, and after a skipped close (unresolvable lock path) only a
structured pane_not_found counts as gone - unknown never does.
The teardown regression drives a live contending lock holder end to end:
the refusal touches nothing (no worktree return, no branch drop, no close
attempt), and the retry after release returns the copy, closes the pane
under the lock, and removes the records. The unconfirmed projected close
now refuses with records retained, and the structured-presence gate has a
strict/default unit matrix.
* no-mistakes(review): Require structured pane-not-found before Herdr record removal
* no-mistakes(document): Correct Herdr record-retention verification date
* fix: refuse ambiguity, revalidate SIGKILL ownership, and roll back failed removals
Three accepted-contract corrections from the post-CI personal review of the
Herdr keep-spaces focus-flash mitigation.
Ambiguous endpoint identity no longer counts as a confirmed-gone pane: a
missing or malformed target refuses record removal in the structured
presence gate, and teardown treats missing confirmation machinery as a
refusal instead of skipping the gate, so only an exact structured
pane_not_found ever erases durable task records.
The pane-death SIGKILL escalation re-reads the exact pane's process
information and refuses to signal unless the same shell pid still passes
the strict bare-idle ownership proof, so a pid that exited and was reused
by an unrelated process is never signaled; the refused escalation falls
back to the plain close with the unrelated process untouched.
A reposition whose removal is not confirmed no longer outlives the attempt:
the emptying-close plan records the verified pre-move order and original
index whenever it invokes the mover, and both close owners restore the
exact original workspace order through a second verified move, under the
same held session lock, before reporting the close as failed.
Each defect was reproduced first: the unit matrix documented malformed
identity as gone, the PID-reuse regression showed SIGKILL reaching a
disowned pid, and the rollback regression showed a single unrestored move.
Teardown-level regressions cover unparseable presence retention alongside
the strict identity matrix.
* no-mistakes(review): Require confirmed Herdr removal and resolvable teardown locks
* no-mistakes(review): Enforce structured Herdr closes and teardown preflight
* no-mistakes(review): Preflight explicit Herdr close confirmation helper
* no-mistakes(document): Document Herdr rollback failure semantics
* no-mistakes(review): Captain, harden recursive Herdr teardown safety
* no-mistakes(document): Document recursive Herdr teardown evidence
* fix: retain nested secondmate home when a recursive child cleanup fails
Captain-decided Option A correction for nm-askuser-flash-r6, found during
complete-diff rereview of the merged head.
cleanup_firstmate_home_children's recursive secondmate branch called
itself for a nested child's home without checking the result, then
unconditionally removed that home right after. remove_firstmate_home
ends in an unconditional recursive delete with no check for leftover
records, so a nested secondmate whose own Herdr grandchild failed its
confirmed-gone check would have its entire home - retained grandchild
records included - erased by the very next line.
Guard the recursive call the same way every other fallible call in this
function already is: || return 1, skipping remove_firstmate_home and
leaving the nested home and its records for a safe rerun.
Empirically, fm-teardown.sh's set -eu already halted the script on the
prior unguarded call before reaching removal (verified by hand with the
guard reverted, under both this session's bash and stock macOS bash
3.2) - the reachable behavior was already correct. The explicit guard
is still applied exactly as decided: it matches every sibling call site
in the function, and it stops the correctness of this path depending on
errexit's well-known fragility under refactors (a wrapping if/&&, or a
future subshell) rather than on an explicit check.
Adds a teardown-level regression building on the existing direct-child
Herdr fixtures: a top-level secondmate contains a nested secondmate,
whose own Herdr child's close goes unconfirmed. Proves through the
public fm-teardown.sh interface that the nested home, the nested
secondmate's own record, and the grandchild's metadata and status all
survive, and that the top-level secondmate's record survives too.
* no-mistakes(document): Document nested Herdr teardown retention
* fix: prioritize completion runway in quota-aware dispatch (#1431)
* fix(dispatch): prioritize quota completion runway
* no-mistakes(document): Document completion-aware quota runway selection
* fix(bin): preserve full task contract in no-mistakes intent (#1447)
* Preserve task contract in no-mistakes intent
* no-mistakes(review): Preserve complete current task contract in no-mistakes intent
* fix(bin): parse punctuated secondmate registry entries safely (#1452)
* fix: centralize secondmate registry parsing
* no-mistakes(review): Centralize secondmate registry binding validation
* no-mistakes(review): Harden registry EOF and symlink validation
* no-mistakes(review): Reject unreadable registries before parsing
* no-mistakes(document): Document punctuation-safe secondmate registry validation
* no-mistakes: apply CI fixes
* feat(bin): add durable process-event supervision (#1483)
* feat(procevent): supervise long-polling sources into durable events
Firstmate had no way to wait on a blocking external process without holding
a conversational turn. Add a domain-neutral process-to-event runner plus a
thin adapter around the currently published `lavish-axi poll` interface:
canonical physical source identity, one machine-wide owner per source, direct
argv execution, and durable 0600 result capture before any event referencing
it is published on the existing wake queue. No second notifier, no polling
control plane, and no retry machinery.
A captured result with no durable handled acknowledgement stays eligible for
bounded re-announcement across any number of drains and restarts. Draining a
wake before acting on it and then starting a replacement session resurfaces
the same exact source and sequence, and never puts result payload text in an
event line. `fm-procevent.sh handled <source-id> <sequence>` is the only thing
that stops re-announcement: generation-keyed, private, path-safe, durable, and
atomically idempotent, so a paired external effect gated on its first-time
versus repeat report is never authorized twice.
An acknowledgement is refused unless matching captured result and adapter
records already exist, so a premature or mistyped call cannot suppress a
future result.
The source side is unchanged and still lossy: the published poll clears
feedback destructively before returning it, so a result lost in that window
is unrecoverable. This is never at-least-once, no-loss, or lossless, and the
handled acknowledgement is not a generic exactly-once effect either - a crash
between an external effect and its acknowledgement can still repeat that
effect on replay.
Integrate registered sources with watcher supervision, the guards, and
recoverable secondmate teardown across nested homes, and cover source
identity, lifecycle races, supervision, restart handling, and cleanup safety
with regressions.
* no-mistakes(review): Prevent Lavish prompt text from spoofing missing sessions
* no-mistakes(review): Serialize publication and secure handled acknowledgements
* no-mistakes(document): Document hardened process-event acknowledgement guarantees
* fix(procevent): never reclaim a source whose owned group still runs
A runner is its own process group leader and starts the blocking source in
that group, but the claim records only the leader PID and its identity. If the
leader died while the source child kept running, the missing PID was
classified stale: reconciliation released the claim and started a second
runner while the old blocking source was still consuming the same canonical
source. For the Lavish adapter that means two destructive long polls racing on
one review session, so it is not harmless process litter. It also contradicted
the documented promise that ownership is never released until the whole group
is gone.
Ownership state now distinguishes a generation that is really gone from one
whose leader crashed with its group still alive. Reconcile stops that
surviving group and releases its exact generation before starting any
replacement, and keeps the claim for a later cycle when it cannot prove the
group stopped or another home owns it. Acquisition and `start` treat the same
state as held rather than reclaimable.
Signalling that group is safe precisely because only an absent leader reaches
this state. A reused PID leaves the leader alive, so the identity comparison
still classifies it stale or uncertain and no group signal follows, which
keeps the existing PID-reuse refusal intact.
Add a public-interface regression for the exact crash cut - SIGKILL only the
leader, prove the child group survives, reconcile, and prove the old group is
gone with no second source running - plus its counterexample that a generation
with no leader and no surviving group is still reclaimed. Update the runner
help, operating documentation, skill, and verification record where they
described reclaim in terms of the leader alone.
* no-mistakes(review): Enforce runner group ownership and detect poller overlap
* no-mistakes(review): Isolate runner groups from unrelated caller processes
* no-mistakes(document): Document isolated process-event runner launch
* no-mistakes(lint): Suppress Perl literal ShellCheck false positive
* fix(bin): retire terminal process events and surface queued wakes (#1500)
* fix(bin): deliver process-event results and retire ended sources
Two defects reproduced during a real Lavish adapter session.
One human `Send & End` produced four captured results: the real feedback,
then recurring empty ended sessions. The generic runner had no way to learn
a source was finished, so every reconcile restarted a poll that returned
immediately. The runner now asks the source's own adapter -
`fm-procevent-<adapter>.sh terminal <result-file>` - and on exit 0 alone
re-proves ownership, drops the registration, and releases its own claim
under one source boundary. Terminal knowledge stays adapter-owned: for
Lavish that is an ended session, a missing session, and the final feedback
delivery the published poll marks with `session_ended`. An adapter with no
terminal command keeps its source armed exactly as before. Capture before
publication, captured-result durability, queued wake durability, bounded
re-announcement, handled deduplication, one-owner ownership, and explicit
idempotent retirement are all unchanged.
A captured result queued its `check` wake durably, but a healthy watcher
with a fresh beacon never delivered it; the result surfaced only after a
manual drain. Publication happens outside the watcher (in the runner) or
unconditionally (in reconcile), so the watcher had no newly actionable
signal to report and never reached its rewake path. It now reports a
queued-but-unsurfaced process-event record through the same actionable exit
every other wake uses, deduplicated by the same `.seen-*` marker discipline
the signal scan uses, so the record is always durable before it is
suppressed. The durable queue remains the authority and no second notifier,
poller, timer, queue, or adapter-specific wake path is added.
Regressions cover both, driven end to end: an armed Lavish source against a
stand-in for the published poll polls once, captures once, publishes one
distinct event, and retires itself; two fixture adapters prove the terminal
decision follows the adapter alone; and a real capture plus a real watcher
prove one proactive wake before any drain, with no duplicate wake while the
record stays queued or after it is acknowledged.
* no-mistakes(review): Harden process-event retirement and proactive delivery
* no-mistakes(review): Route process-event delivery through shared wake owner
* no-mistakes(document): Clarify process-event delivery and retirement documentation
* no-mistakes(lint): Fix ShellCheck control-flow warnings
* no-mistakes(lint): Fix wake output status lint warning
* perf: shard portable serial tests across CI runners (#1544)
* perf(ci): shard the portable serial behavior lane across runners
The Behavior portable serial job ran all 69 scripts of the serial
remainder on one runner. The measured serial sum on run 30725985757 was
1143762 ms (19m04s) against a 20-minute timeout, so the job intermittently
reached the cap and was cancelled with every step passing. Setup is only
about 7s, so the cost is entirely test wall time.
Split the lane into four separate-runner shards. Each shard is still
strictly serial, and separate runners mean no two of these stateful
scripts ever share a machine, so the split needs no concurrency isolation
proof. Assignment is longest-processing-time bin packing over measured
per-script duration hints, balancing every shard to 285941 ms (~4m46s) of
expected work, and the timeout tightens from 20 to 15 minutes.
bin/fm-test-run.sh owns the shard count and refuses a lane whose "ofN"
disagrees with it, while ci.yml derives the same count from
strategy.job-total rather than a literal, so changing it in either file
alone fails the lane loudly instead of leaving part of the required suite
unrun. --check-coverage additionally proves the shards are non-empty,
disjoint, and exactly equal to the serial lane. No test is weakened,
skipped, or removed.
Also replace the wall-clock sleeps in the --jobs scheduler test fixture
with an explicit signal handshake between the fixtures. The old
0.5s-versus-0.05s race failed on a loaded machine; the handshake passes
under sustained CPU saturation.
* no-mistakes(review): Correct portable serial shard balance evidence
* no-mistakes(document): Document portable serial shard evidence accurately
* fix(bin): correct session lock and attached watcher supervision (#1545)
* fix(bin): identify harness sessions by path and report delivered wakes
Two supervision faults, both reported by a contributor and both open on the
default branch.
Fault 1: the Stop auto-arm never claims the home. fm_harness_ancestry_pid()
matched only the basename of `ps -o comm=`, and Claude Code's native installer
names the per-session executable by its version (.../share/claude/versions/
2.1.220), so that basename identifies nothing. Three real failure shapes follow:
a version-named session is missed entirely and the hook exits 0 with the epoch
never written (unconditional on Linux, where procps reports the kernel exec name
and ignores argv[0]); a claude-named daemon that directly parents sessions wins
the outermost-contiguous-claude rule ahead of the session itself; and a session
that is both version-named and daemon-parented has its live lock reclaimed as
stale and rewritten to the shared daemon pid, corrupting the home's ownership
record.
Harness identity now also reads whole components of the executable path and of
argv[0], which is what both platforms still carry. Matching whole components
only keeps that widening safe: bin/fm-claude-stop-autoarm.sh and ~/.claude/hooks
scripts have no "claude" component. Ownership is then decided against the
session's whole contiguous harness ancestry rather than one chosen pid, which is
the honest form of the question the library already documents ("does the current
process descend from that same harness?"). That subsumes the outermost-pid rule
for Claude's nested bg-spare worker chain instead of reverting it, and lets a
daemon-parented session recognize its own lock. Lock acquisition still writes the
outermost pid of the run, the only pid that lives as long as the session.
Fault 2: an attached arm reports a delivered cycle as FAILED. The watcher prints
its one reason line to its own stdout, so only the arm that forked it can read
that line; an arm that attached observes nothing but a released lock and called a
completely successful cycle "cycle ended without an actionable reason". No
supervision event was lost - the durable queue held it - but every harness
protocol reads that line as "supervision is down" and directs a manual re-arm.
The arm now resolves an unobservable close against the durable wake queue, which
records every wake before the watcher prints it and whose sequence counter never
rewinds, not even across a drain. A cycle the queue proves delivered a wake
reports that wake and exits 0; a cycle whose records a handling turn already
drained reports the delivery without inventing a reason line; only a cycle that
delivered nothing is still the typed nonzero failure. Fixing it in the arm covers
codex, opencode, pi, grok and kimi, not just the Claude Stop path.
Regressions: tests/fm-session-lock-ancestry.test.sh pins both platforms' ps
semantics behind a deterministic process table and runs the real Stop auto-arm in
version-named, daemon-parented, and combined real process trees, each orphaned so
the walk cannot escape the fixture. tests/fm-watch-arm.test.sh drives a real
watcher and a real attached arm through a real wake. Every fault case fails on
the previous code.
* no-mistakes(review): Bind watcher delivery records to process identity
* no-mistakes(review): Return validated watcher identity atomically
* no-mistakes(review): Track watcher successors by PID and identity
* no-mistakes(document): Consolidate watcher arm-cycle documentation ownership
* fix(bin): harden Claude supervision auto-arm recovery (#1495)
* fix(supervision): harden Claude auto-arm failure handling
* no-mistakes(review): Guarantee automatic retry after Claude auto-arm failures
* no-mistakes(review): Gate attended fail-open on verified supervision failure
* no-mistakes(document): Document Claude auto-arm retry and guard scope
* no-mistakes: apply CI fixes
* fix(supervision): make Claude fail-open progression monotonic
* no-mistakes(review): Preserve auto-arm failure episodes until verified watcher recovery
* no-mistakes(review): Linearize auto-arm failure progression across existing locks
* no-mistakes(review): Linearize positive recovery across shared failure episode lock
* no-mistakes(review): Scope Claude recovery contention to Claude guard mode
* no-mistakes(document): Align supervision auto-arm documentation
* no-mistakes(review): Preserve actionable wakes despite healthy successors
* no-mistakes(document): Refresh supervision auto-arm documentation
* feat(bin): require an explicit per-task delivery contract (#1563)
* feat(bin): require an explicit ship delivery mode in fm-brief
A ship brief's definition of done was shaped by a silent per-project registry
lookup, so an adjusted brief and the task's recorded delivery could disagree and
no one had to decide anything per task.
fm-brief now requires --mode on ship scaffolds, validates it against the closed
set, refuses the conditional no-mistakes-prod-only registry policy as a task
mode, and records the choice as a fixed machine-readable "Delivery contract:
mode=<mode>" line that fm-spawn can check. --mode is refused on scout and
secondmate scaffolds, and --yolo is refused outright because the worker never
owns approval decisions.
* feat(bin): require an explicit ship delivery contract at spawn and promotion
fm-spawn resolved every ship and scout task's mode and yolo from the project
registry, so the delivery posture was never a per-task decision and could
contradict the brief the worker was about to follow.
fm-spawn now requires --mode and --yolo on ship spawns, validates both against
their closed sets, and reads the brief's recorded delivery contract line and
refuses a mismatch before any endpoint exists; a brief scaffolded before that
line existed warns once and launches on the flag. A batch carries one shared
contract that each pair still checks against its own brief. Scout and secondmate
spawns refuse the flags, and a scout now records no mode or yolo at all, which
teardown and the snapshot already tolerate. When the explicit mode carries less
rigor than the project's standing posture, a deviation notice is printed and the
spawn continues, so the registry stays advisory rather than an enforced default.
fm-promote requires the same two flags, because a scout carries no posture to
inherit, and writes them into the task record with the kind flip.
fm-project-mode keeps its one registry parser for the mechanical consumers that
have no task in hand, accepts the conditional no-mistakes-prod-only annotation
and maps it to its most rigorous leg for them, and grows --raw so the deviation
notice can tell a conditional policy apart from a flat mode.
* docs: record the explicit per-task delivery contract
AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at
intake, including the surface classification for a no-mistakes-prod-only project
and the unregistered-project fallback, and the project-management skill defines
that conditional policy as a registration-time posture with its defaults and
initialization consequences. The registry blurb, script table, and architecture
section follow: the registry records the captain's standing posture, and task
delivery is decided per task and passed explicitly.
* test: pass ship delivery flags per call site in the Herdr launcher e2e
The shared spawn helper also launches a secondmate, which refuses the flags, so
the contract belongs at each ship call site rather than inside the helper.
* test: pass the ship delivery contract in the secondmate suites
Both suites scaffold or spawn an ordinary ship task as the control case for a
secondmate assertion, so each needs the explicit contract the ship path now
requires.
* feat(bin): support remote secondmate homes (#1576)
* Add generic remote secondmate transport
* Add routed remote secondmate replies
* Add remote outbox backlog handoff
* Integrate remote secondmate lifecycle
* no-mistakes(review): Fix remote snapshot and handoff races
* no-mistakes(review): Serialize remote home provisioning transactions
* no-mistakes(review): Harden remote lifecycle transaction boundaries
* no-mistakes(review): Serialize remote lifecycle mutations and fail closed
* no-mistakes(review): Close remote lifecycle and file race windows
* no-mistakes(review): Serialize remote reply retirement and inheritance
* no-mistakes(review): Harden remote transfer integrity and recovery
* no-mistakes(review): Serialize remote respawn with registry retirement
* no-mistakes(document): Document remote bootstrap convergence accurately
* no-mistakes(document): Clarify skipped remote secondmate mutations
* no-mistakes(lint): Resolve remote script ShellCheck warnings
* no-mistakes: apply CI fixes
* feat: add per-task trace context propagation (#995)
* feat(spawn): propagate a native W3C traceparent to spawned agents
Add a default-off capability that resolves one W3C traceparent for a task,
injects it into the agent's pane shell as the TRACEPARENT environment
variable immediately before launch, and records the identical value as
traceparent= in state/<id>.meta, so an external observer that explicitly
reads that env value or meta field can correlate a worker, a Secondmate, and
their nested children into one trace with no collector, storage, UI, or
vendor coupling.
TRACEPARENT as an environment variable is a firstmate convention carrying a
W3C-formatted value: W3C Trace Context standardizes the header, not an env
var, and OpenTelemetry SDKs do not read it automatically, so a downstream
must consume it deliberately; this feature parents no SDK span by itself.
Identity is per task, not per spawn: the carrier is minted with random ids on
the first spawn, adopted as a child (fresh span, same trace) for a nested
spawn whose parent already holds one, and reused verbatim from the meta on
relaunch, so a task keeps one stable logical identity across restarts. A
malformed or all-zero inherited value is treated as absent and roots a fresh
trace. A new root is sampled (01) - a sampling decision a downstream
parent-based sampler honors, not a guarantee that any collector stores a
span, and firstmate emits no spans; a child preserves the inherited flag.
Trust boundary: a firstmate-minted root is random and reads no prompt, path,
task prose, credential, or arbitrary environment key. An inherited
TRACEPARENT is opaque caller-controlled data - up to 24 bytes of id passed
through after syntax validation - so whoever set it controls those bytes, a
bounded fixed-width channel rather than a general content or secret channel.
The feature adds no OTEL_* variable, no tracestate, and no arbitrary
environment injection; it runs no configurable or arbitrary command, only the
fixed local od and tr (resolved from PATH) to read a few bytes of entropy - a
small local pipeline with no network or watchdog and no hard latency
guarantee. Any entropy or validation failure that returns omits the carrier
without aborting the spawn. A default-off spawn leaves the generated meta and
launch environment unchanged.
Enablement is default-off (config/trace-context, or FM_TRACE_CONTEXT where a
non-empty value overrides and unset or empty defers to the file) and is
propagated into secondmate homes, taking effect at each agent's next launch:
a Secondmate launched or relaunched after enablement carries the primary
trace into its nested workers, while an already-running Secondmate roots new
traces for its own workers until relaunched. Injection reuses the existing
GOTMPDIR channel, so all spawn backends and harnesses and the ship, scout,
and secondmate paths are covered.
Covered by a pure-library suite and a spawn-path integration test (fake tmux
plus a real worktree, hermetic against ambient FM_TRACE_CONTEXT) proving the
recorded and injected carriers are identical and sent before launch, that
default-off writes and injects neither, that a relaunch reuses the recorded
carrier, and that an explicit FM_TRACE_CONTEXT overrides the file both ways;
plus a source-owner inheritance test proving trace-context propagates and
absence-mirrors through propagate_inheritable_config.
Documentation follows the repository documentation-audiences contract:
docs/trace-context.md is maintainer-architecture rationale, the configuration
schema lives in docs/configuration.md, and the repeatable test evidence is
separated into docs/verification/trace-context.md (maintainer-verification),
registered in docs/documentation-audiences.json.
* fix(spawn): propagate the effective trace-context decision to secondmates
FM_TRACE_CONTEXT overrode trace context only in the process that read it. A
newly launched secondmate decided enablement from the inherited
config/trace-context file alone, so the override did not cross the
primary-to-secondmate boundary: FM_TRACE_CONTEXT=off with the file present left
the secondmate's nested workers traced (a broken kill switch), and
FM_TRACE_CONTEXT=on with the file absent left them untraced despite the
inherited carrier.
Deliver the primary's effective decision to a newly launched secondmate as a
normalized on/off FM_TRACE_CONTEXT in the launch prefix, so a FM_TRACE_CONTEXT
override governs the nested primary -> secondmate -> worker chain both ways, not
just the copied file. The value is bounded to the literal on/off and does not
broaden environment injection; the already-running secondmate boundary is
unchanged.
Add a genuine two-level spawn regression that drives fm-spawn twice with the
exact environment the primary injects into the secondmate and proves both
divergent directions end to end. Correct the documentation that implied
secondmate coverage on every backend, since orca and cmux reject secondmate
spawns, and refresh the verification evidence for the new assertion count.
* no-mistakes(review): Clarify Secondmate trace-context launch snapshots
* no-mistakes(document): Correct trace-context documentation ownership and relaunch semantics
* fix(spawn): resolve the trace-context decision once for carrier and snapshot
The effective trace-context decision was read twice per spawn: once inside
fm_trace_context_resolve for the recorded carrier, and again for the secondmate
FM_TRACE_CONTEXT launch snapshot. A config-file change between the two reads
could pair a carrier with the opposite enable state - an injected carrier with
an off snapshot, or no carrier with an on snapshot.
Freeze the effective on/off decision once, drive the carrier resolution under
that frozen FM_TRACE_CONTEXT so it cannot independently re-read the file, and
reuse the same frozen decision for the secondmate launch snapshot. Add a
spawn-path regression that drives the file-decided path and proves the recorded
carrier and the delivered snapshot always agree, and refresh the verification
evidence for the new assertion count.
* no-mistakes(review): Preserve legacy Secondmate trace boundary
* no-mistakes(document): Correct trace-context verification comparison base
* no-mistakes(review): Captain, prevent failed trace delivery metadata claims
* no-mistakes(review): Captain, align trace-context tests and verification evidence
* no-mistakes(document): Correct trace-context verification evidence
* no-mistakes(lint): Suppress intentional ShellCheck literal-dollar warnings
* no-mistakes(review): Captain: freeze trace context at session start
* no-mistakes(test): Captain: stabilize scheduler test and document Kimi trace coverage
* no-mistakes(document): Document trace-context safety boundaries
* fix(trace): fail off on stale session snapshots
Publish each home session decision atomically through a same-directory temporary file and bind it to the current session lock. A replacement failure can no longer leave an earlier on decision active in a later session; missing, stale, malformed, or unpublishable state defaults safely to off.
Add regressions for read-only replacement and failed publication, update spawn and session-start fixtures for the lock-bound format, and refresh the architecture and verification records.
* no-mistakes(review): Fix trace spawn failure independence and duplicate safety
* no-mistakes(document): Refresh trace-context documentation and verification
* no-mistakes(review): Clear partial backend input after failed trace submission
* no-mistakes(review): Stop unsafe trace delivery before launch append
* no-mistakes(document): Document unsafe trace delivery handling
* fix(trace): bound each trace to one routed task, never the routing agent
A persistent Secondmate holds its launch-time TRACEPARENT in the process
environment for its whole life, and routed requests never replace it, so
resolving new-task carriers from the ambient environment chained every
routed task into one ever-growing trace per Secondmate with distinct
parent ids. Resolve now reuses the task's recorded carrier or mints a
fresh sampled root, ne…
verbagem
added a commit
to verbagem/firstmate
that referenced
this pull request
Sep 3, 2026
… review work (#15) * fix(tests): replace source-content .gitignore assertion with behavioral coverage (#1304) The second assertion in fm-gitignore-config.test.sh (added by #1261) greps .gitignore for a specific spelling of the config/ ignore pattern. It fails on a semantically equivalent pattern like config/** and does not prove Git actually ignores anything, per the completed source-content-test audit. Replace it with a real git check-ignore control test on a generated unrelated path, and strengthen the existing directory-coverage test with generated unpredictable direct and nested config/ paths. * feat: bound and consolidate startup memory during stow (#1303) * Add bounded startup memory curation * no-mistakes(review): Record reproducible stow verification evidence * no-mistakes(review): Validate inherited secondmate stow evidence * no-mistakes(document): Document editable startup-memory budget propagation * fix(herdr): place workers in the launching workspace (#1328) * fix(herdr): place workers in the launching agent's exact workspace Herdr enforces no workspace-label uniqueness, and spawn resolved its container by taking the FIRST workspace whose label matched the home label. With two workspaces both labeled "firstmate", a worker launched from the second one was created in the first, so it appeared in a different space than the Firstmate the captain was watching. Reproduced end to end on Herdr 0.7.5 protocol 17 by running the real bin/fm-spawn.sh inside a launcher pane in the second "firstmate" workspace: the worker landed in w1 while its launcher was in w2, with an unrelated third workspace focused throughout, which also rules out any dependence on the focused workspace. Placement now binds to the launching process's own Herdr identity. Herdr injects HERDR_PANE_ID, HERDR_SESSION, and HERDR_SOCKET_PATH into every process it manages a pane for, and fm_backend_herdr_launcher_identity resolves that pane's current owning tab and workspace live from Herdr, cross-checking the pane against its tab and confirming the workspace exists exactly once in the session. The injected HERDR_TAB_ID and HERDR_WORKSPACE_ID are creation-time snapshots and are deliberately not read as current identity. Labels are no longer placement authority. A claimed parent identity that is unreadable, contradictory, stale, or from another named session or Herdr server stops the spawn before any worker endpoint exists, rather than degrading to a label search. A launcher with no Herdr ancestry has no workspace to inherit and keeps the per-home labeled container, which must now resolve to exactly one workspace; two same-labeled candidates refuse instead of adopting either. A --secondmate launch keeps standing up that home's own workspace by design. With presentation spaces enabled, the projected child is created and bound under that same exact parent and anchors its ordering on it, so a duplicated home label no longer makes the layout ambiguous. Projection, focus restoration, restart binding, and quarantine rules are unchanged, and children are never collapsed into the parent. tmux, Zellij, cmux, Orca, and the away-mode daemon terminal were each inspected and are not affected: none resolves a container by searching mutable labels. tests/fm-backend-herdr-launcher-workspace-e2e.test.sh drives the real spawn and teardown against an isolated Herdr lab, with its headline case running fm-spawn.sh inside a real Herdr pane so the identity comes from Herdr's own injection. The refusal matrix and the ordering anchor are covered deterministically in tests/fm-backend-herdr.test.sh. Eight existing real-Herdr suites inherited the developer terminal's own Herdr pane into their isolated lab sessions, which the new cross-session check correctly refuses. tests/herdr-test-safety.sh now owns herdr_forget_inherited_pane and those suites call it, so what they assert no longer depends on where they were launched from. Two unrelated fixes found along the way. tests/fm-secondmate-harness.test.sh had the same class of environment leak through CLAUDECODE, which outranks PI_CODING_AGENT in bin/fm-harness.sh and made its pi-signed ancestry case resolve "claude" whenever the suite ran inside Claude Code. And fm-spawn.sh's usage() printed a fixed line range that had already been truncating its own help mid-sentence. * no-mistakes(review): Enforce exact Herdr launcher and projection identity * no-mistakes(document): Document exact Herdr launcher workspace placement * fix(calm): refine Calm working boat animation (#1339) * feat(calm): replace Pi's working row with an animated ship while Calm is on While Calm is active and one logical agent run is under way, Calm now hides Pi's built-in working row and renders a small two-row SSHHIP-derived boat in its place. When Calm is off, Pi's stock working row is left untouched. The presentation uses only public Pi extension API: setWorkingVisible(false) plus a temporary setWidget() component whose render(width) owns the responsive geometry and whose timer requests a TUI render. Visibility follows agent_start through agent_settled, so the boat does not flicker between tool calls, automatic continuations, retries, or compaction inside the same run, and settle, abort, and failure all reach the same cleanup. fm-calm.ts stays the sole owner of the presentation choice and the only caller of setWorkingVisible(); the new lib owns the sprite geometry and widget. * no-mistakes(review): Guarded Calm-off lifecycle visibility writes; focused tests pass * no-mistakes(test): Fixed Calm E2E wait to include tmux scrollback * no-mistakes(document): Document Calm working boat behavior * no-mistakes: apply CI fixes * feat(calm): slow the Calm boat, animate blue water, and make the sail directional The boat now moves one column every 880ms while a bounded fixed-cell water phase advances every 220ms, so the water ripples several times between boat steps and the presentation reads as calm. One scheduler drives both clocks and disposing the widget stops them together; ticks rather than wall-clock timestamps drive every state change, so tests seek animation time exactly. Colors are standard ANSI foreground codes instead of theme lookups: blue for every water cell and yellow for the complete boat, each run closed with a default-foreground reset so nothing bleeds into padding or later frames. ANSI bytes never enter geometry, so visible width stays exact. The mainsail is directional and trails aft of the mast: <| travelling right and |> travelling left. Direction reverses the moment the boat lands on an endpoint, so the endpoint frame already shows the new heading and no frame at or after a bounce shows the previous sail. * test(calm): wait for the Ctrl+O expansion redraw this block asserts * docs(calm): record the revised working-presentation verification evidence * no-mistakes(document): Fix Calm feasibility document EOF whitespace * fix(dispatch): preflight candidate auth before quota escalation (#1349) * fix(dispatch): scope candidate authentication to its own surface A locally expired timestamp in one credential store was reported to the captain as a sign-out, including for dispatch candidates that never read that store. A `harness=pi, model=xai/grok-*` candidate authenticates through Pi's own xAI credential, but the only Grok quota reading available was gated on the standalone Grok CLI's separate token, whose expiry clock drifts independently. The always-loaded intake rule then turned that unreadable quota into a mandatory captain escalation. Add `bin/fm-auth-preflight.sh` as the deterministic owner of the parts that must not depend on agent memory: it resolves a tuple's authentication surface from quota-axi's own emitted auth sources rather than from a harness or model name, so another harness's CLI can never gate a candidate that does not use it. A vendor CLI is launched only when the tuple's own harness owns the credential store under test and a non-destructive discovery command is registered for it, which today is `grok models` alone. That probe runs at most once with stdin closed and a hard timeout, reads its verdict from the first stdout line because the command exits 0 either way, treats unrecognized output as indeterminate, and never invokes login, logout, or the interactive TUI. Quota is read at most twice, and unknown headroom never makes a candidate ineligible on its own. Update the dispatch procedure to match: usable authentication with unmeasurable headroom stays eligible at lower preference with the unknown disclosed, and stop-and-report is reserved for unresolved authentication, an unresolved relationship, or malformed configuration. Record that Grok's `credits.remaining` is a prepaid balance rather than window headroom. Gate quota-axi at 0.1.16 in bootstrap, the first build reporting per-credential auth sources. A stale install previously passed the presence check silently, which is why a fix published two days earlier was still not in effect. Replace the orphaned quota-array-dispatch fixtures, which encoded a `provider: "xai"` shape the tool never emits and had no consumer, with fixtures shaped like real 0.1.16 output that the new suite drives the script against. The suite asserts the verdict and, separately, which vendor CLIs were launched, so a Pi/xAI candidate reaching the Grok CLI fails. Map `tests/fixtures/<dir>` to its consuming suite so a fixture change selects the right tests instead of refusing. * refactor(bootstrap): give the quota-axi floor one owner The floor was stated twice - once in bootstrap's gate and once inline in the auth preflight - so bumping it needed two edits that could drift. Move it to bin/fm-quota-axi-lib.sh alongside its rationale, matching the existing tasks-axi library, and derive the comparison from the constant so the number appears exactly once. Bootstrap turns a failing check into the operator diagnostic; the preflight refuses to emit an unscoped verdict. Map the new library to both consuming suites so a bump re-runs them, and record that any usable source means the surface authenticates. * no-mistakes(review): Captain: bound quota checks and removed Python dependency * no-mistakes(review): Captain: enforce conservative headroom and exact preflight retry * no-mistakes(review): Captain: preserve OpenCode eligibility without auth-surface guessing * no-mistakes(review): Captain: reject malformed OpenCode model relationships * no-mistakes(review): Captain: exempt verified unmodeled tuples from intake escalation * no-mistakes(document): Updated dispatch authentication documentation * no-mistakes: apply CI fixes * feat(x-mode): reconcile promised public replies deterministically (#1350) * feat(x-mode): reconcile promised public replies deterministically A promised final reply in an X or Discord thread was only kept while the primary remembered it. Compaction or restart erased that memory, so a typed public-followup obligation could sit at pending-work after its PR merged and the original thread never got its reply. Make the promise durable state instead: - bin/fm-public-followup-emit.sh reports a typed terminal work result (source home, work id, generation, outcome, safe deliverables, bounded public-safe text) into the owning home's private inbox. The event id is derived from that identity tuple, so duplicate reports and restart replay converge with no coordination, and nothing ever parses a free-form done: sentence. - bin/fm-public-followup.sh registers a commitment, reconciles events through tasks-axi public-followup, and runs the idempotent delivery sequence (begin-delivery with the payload hash, post, record the posted receipt or a typed error) against the stored platform and opaque thread binding. A delivery interrupted between post and receipt refuses rather than risk a second public reply. - Session start surfaces unresolved commitments from disk, the existing relay poll surfaces a new terminal-result set once, and teardown refuses while this home still owes a public reply for that exact work. tasks-axi public-followup remains the only owner of the obligation state machine, state/x-context/ the only owner of the private request context, and fm-x-reply.sh the only thing that posts. Its new optional --receipt-file is the one addition there, so a caller can record how many messages were sent. A home that never opted into the myfirstmate relay gates out on a single [ -f "$FM_HOME/.env" ] test: no tasks-axi call, no backlog or context scan, no output, and no artifact. Evidence in docs/verification/public-followup.md. * no-mistakes(review): Hardened public-followup reconciliation and ownership guards * no-mistakes(review): Hardened typed terminal cleanup and receipt reconciliation * no-mistakes(review): Automated typed-delivery cleanup and strict backlog validation * no-mistakes(review): Fail-closed parent resolution and registration-safe delivery * no-mistakes(review): Harden relay gating and validate secondmate bindings * no-mistakes(review): Use owner-aware single-gate teardown protection * no-mistakes(document): Correct public-followup documentation drift * no-mistakes(lint): Quote done literals to fix ShellCheck warnings * no-mistakes: apply CI fixes * feat(bin): replace busy heuristics with semantic lifecycle state (#1327) * feat: add semantic busy-state contract owner and event writer One owner (bin/fm-busy-lib.sh) for the captain-approved semantic busy-state redesign: a per-task gen-bound record written only by bin/fm-busy-event.sh, per-harness trusted-source classification with explicit source attribution, busy/idle/unknown/dead semantics where missing, malformed, stale, or untrusted semantic data is unknown - never idle - and endpoint death is the only process-level override. The Grok-only rendered-tail fallback and the standalone-Kimi verification gate live behind the same classifier. * feat: arm busy-state at spawn and convert Pi to the semantic extension path fm-spawn arms the busy-state contract for converted adapters and seeds busy/fm-spawn (the launch brief is a submitted turn). The Pi/pi-signed per-task extension now reports agent_start -> busy and agent_settled -> idle confirmed by ctx.isIdle(), covering auto-retries, compaction retries, tool loops, and queued continuations, while turn_end stays a wake notification touch. Teardown removes the new record, gen sidecar, and lock. Live-verified on Pi 0.82.0: seed -> agent-start busy -> agent-settled idle with the marker still touched. * feat: convert OpenCode to the semantic session.status plugin path The per-task plugin (renamed .opencode/plugins/fm-busy-state.js) now classifies from OpenCode's semantic session.status events - busy and retry are active, idle is inactive - latched to the worker's own session so a subagent child session can never clear the worker's busy state. The session.idle marker touch stays a wake notification. Teardown removes both the new and the legacy plugin filenames. Live-verified on OpenCode 1.17.18 in a real TUI pane: seed -> session-busy -> session-status-idle. * feat: convert Claude to the full lifecycle hooks path The per-task settings.local.json now wires UserPromptSubmit -> busy and Stop, StopFailure, and SessionEnd -> idle, so API-error and shutdown turn ends can never strand a busy record; Stop keeps the turn-ended notification touch. A refused (stale-gen) event exits 0 and stays silent so Claude's own lifecycle is never broken. Live-verified on Claude Code 2.1.220: UserPromptSubmit fires for the argv launch prompt, Stop closes each turn, a mid-stream Escape interrupt fires no closing hook, and the firstmate-controlled idle/fm-interrupt clear resolves it. * feat: gate Codex busy state behind verified semantic sources The approved contract prefers Codex's app-server turn lifecycle with capability negotiation and sanctions its lifecycle hooks as the intermediate. Live probes on codex-cli 0.145.0 show neither is usable for a pane worker: the app-server daemon is unreachable for a TUI thread and refuses to start outside the managed standalone install, and firstmate-written project hooks never fired (interactive with directory trust granted, and exec, both with --dangerously-bypass-hook-trust) while global hooks fired in the same runs. Codex therefore classifies unknown codex-unverified behind an explicit probe rather than falling back to idle or footer text, and fm-spawn installs no unverified Codex wiring. * feat: gate standalone Kimi busy state on live verification Standalone Kimi has no installed binary here, so per the approved contract its semantic path stays guarded and it classifies unknown kimi-unverified rather than idle - and never from its locale-sensitive moon-phase spinner, which the redesign forbids inventing as a state source. The gate records the preferred source order (Wire prompt request lifetime, which brackets a turn and reports cancellation, then the documented hooks including Interrupt because Stop does not fire on interrupts) and the exact evidence required to open it. Arming without wiring would seed a busy record nothing could clear, so both land together behind the same gate. * feat: route busy consumers through the contract and drop the global OR The watcher, crew-state reader, and away-mode daemon now decide busy state through bin/fm-busy-lib.sh: only an exact busy verdict counts as working, and unknown never becomes working or a silent idle, so a crew whose semantic state is missing, malformed, stale, or unverified surfaces instead of being absorbed. Crew-state reports the producing source in its detail. The watcher's global OR regex default is gone; Grok keeps its isolated fallback inside the contract. The daemon's supervisor-pane reader stays rendered-text - that pane is not a recorded task - but is now scoped to firstmate's own detected harness instead of every vendor signature. Secondmate pending-reply observation is deliberately unchanged and documented as a delivery-confirmation signal, not task state. * docs: point busy-state documentation at the single contract owner Adds a maintainer-architecture section naming bin/fm-busy-lib.sh as the owner of what busy means, with per-adapter sources, the unknown-never-idle rule, the endpoint-death override, and the two rendered-text readers that deliberately stay outside the contract. Replaces the stale regex-first prose in architecture, tmux-backend, herdr-backend, and configuration; converts the harness-adapters per-harness rows from UI signatures to the semantic source each harness uses; and records the live verification evidence, including why Codex and standalone Kimi stay unknown. * fix: arm away-launch signal handlers before acquiring the lifecycle lock fm_afk_launch_main acquired its lock and only then installed the EXIT, INT, and TERM traps. A signal arriving in that window terminated the process by default action and left the lock directory behind, which blocks the next away-mode launch until the stale-owner reclaim path clears it. The release helper only removes a lock this process owns, so the handlers are now armed first. The accompanying test also killed the child whether or not the lock had appeared and sampled cleanup the instant wait returned; it now requires the lock, then allows a bounded settle, so it proves the guarantee instead of racing it. * test: align fleet, Kimi, lifecycle, and detection suites with the contract The fleet snapshot and wake-daemon lifecycle fixtures now prove a working crew through its own semantic busy-state record instead of rendered pane text, which is what those consumers read. The Kimi watcher test asserts the approved contract directly: a standalone Kimi task classifies unknown rather than matching its moon-phase spinner, while Grok's isolated fallback still classifies only Grok. The pi-signed detection cases clear ambient harness markers, fixing a pre-existing failure where the running session's own CLAUDECODE outranked the fixture's marker. * fix: stop teardown from deleting a project's own Codex hooks file An intermediate revision wired Codex through a firstmate-written <worktree>/.codex/hooks.json, and teardown removed it alongside the other generated wiring. The Codex wiring was dropped when its probes came back unverified, so that removal now targets a file firstmate never creates - and a project may legitimately track its own .codex/hooks.json, which teardown would then delete from a pooled worktree. * fix: keep busy-record parsing from disturbing its sourcing caller The record parser split fields with set -- under a temporary noglob, which clobbers a sourcing caller's positional parameters and restores glob expansion even when the caller had disabled it. The watcher, the daemon, and the crew-state reader all source this library, so it now reads fields with read -a, which never globs and never touches caller state. * docs: state exactly which Claude hook paths were reproduced live The busy-state record listed all four wired Claude hooks in the source column, which could read as a claim that every one fired during the pass. UserPromptSubmit and Stop did; StopFailure and SessionEnd are wired from hook names confirmed present in the installed binary, but the abnormal turn ends they cover were not reproduced. * test: let reset_fakes own the crew-state busy-text fixture lifecycle The Grok fallback case set FM_FAKE_BUSY_TEXT and cleared it inline, so the variable's lifetime was owned by one test rather than by the shared reset that every other fake already uses. * no-mistakes(review): Fix semantic busy-state lifecycle races * no-mistakes(review): Make busy-state retirement idempotent * no-mistakes(review): Enforce semantic state boundaries for status and injection * no-mistakes(review): Restore harness-scoped away-mode busy guard * no-mistakes(document): Refresh semantic busy-state documentation * no-mistakes: apply CI fixes * fix: preserve Calm boat continuity across working periods (#1356) * fix(calm): resume working boat from frozen column across runs Keep one extension-owned boat animation for the Pi session so settling freezes column and direction, the next working period resumes there without hidden-time jumps, and only a fresh session resets to the left edge. * no-mistakes(review): Freeze Calm boat from last rendered state * no-mistakes(document): Document Calm boat continuity contract * fix: restore evidence-based dispatch eligibility (#1358) * fix(dispatch): judge candidate provider relations instead of rejecting them Firstmate deterministically dropped supported Pi candidates in the openai-codex family. bin/fm-auth-preflight.sh resolved a harness=pi tuple's credential surface by constructing the source id `pi:<model-prefix>`, so `pi + openai-codex/gpt-5.6-terra` looked for a `pi:openai-codex` source. That source does not exist, because Pi's Codex family authenticates through the Codex store quota-axi already lists as `auth-json`/`cli-rpc`. The tuple returned `eligible=no reason=surface-unresolved` while the Pi catalog listed the model and the Codex provider reported fresh, usable credentials with 64 effective percent remaining on its all-model scope. The prefix construction was only ever valid where Pi holds its own credential (`pi:xai`, `pi:kimi-coding`), which is why every previously configured Pi tuple resolved and the defect stayed hidden until a Codex-family Pi model was configured. Retire dispatch eligibility from deterministic shell. The dispatching first mate now establishes model support and provider family from each harness's authoritative catalog, applies quota at the granularity the vendor supplies, and shows that reasoning. Provider-level and all-model evidence bounds every model established in that family; a named-model window bounds only its own model. Missing model-level quota, a missing auth source, unmeasurable headroom, and unmodeled authentication are disclosed uncertainty. Only concrete contradictory evidence blocks a candidate. Replace the preflight with bin/fm-vendor-auth-probe.sh, which keeps the captain's approved bounded probe envelope without any routing knowledge: it takes no harness, model, or provider, reads no quota, renders no verdict, and holds only a fixed-argv safety allowlist. Its behavior suite proves the absent identity surface, the untouched quota, the uniform exit status, the fixed argv with stdin closed, and a real bound even when the configured bound is zero. Also fixed along the way: a zero FM_*_TIMEOUT silently removed the hard bound, the pinned Grok version had drifted to 0.2.117, and --changed selection refused outright on any deleted bin/ script. AGENTS.md section 4 and quota-array-dispatch own the corrected policy, harness-adapters gets the catalog-responsibility correction, and docs/verification/dispatch-auth.md records the 2026-07-30 evidence on Pi 0.82.0, quota-axi 0.1.16, and grok 0.2.117. * no-mistakes(review): Reject all-zero vendor probe timeouts * docs: define captain instruction precedence (#1362) * docs: add captain-authorized inherent red-check merge exception Keep the default red-PR ban and own one always-loaded exception in the merge-authority section: captain-explicit PR or bounded batch plus exact check, only when the failure is inherent to the selected delivery path. Yolo cannot activate it; final head and the full current check suite must be verified; other substantive failures remain non-waivable. * docs: replace narrow red-check exception with captain precedence Supersede the inherent failing-check merge exception with one always-loaded Firstmate-local rule: a current explicit concrete captain instruction overrides a conflicting Firstmate-written standing rule only within exact scope, never above platform/system/developer instructions. Keep the ordinary red-PR default and yolo boundary; point section 7 at the section 1 owner. * docs: define validation supersession sequence (#1407) * fix: give validation-time captain overrides a supersession sequence The Validate section let a captain instruction that completely invalidates the work being validated keep the same task and worker, but never said how: the adjacent rule flatly bans hand-editing, committing, aborting, or restarting during an active run with no carve-out, so a worker facing full invalidation had no sanctioned path forward. Add the missing sequence: cancel through no-mistakes axi's abort command, confirm the run has stopped through axi status, recover branch ownership through axi sync's guarded recovery, only then replace the obsolete work, and validate once against the final head. The existing ban on hand-editing an active run now cross-references this sequence instead of contradicting it. * no-mistakes(review): Make validation custody recovery conditional * no-mistakes(document): Clarify validation supersession abort exception * fix: keep obsolete pipeline commits out of the superseded deliverable The review-applied fix made custody recovery conditional on branch_sync.next_action.code, but left an open gap: recovering custody settles who owns the branch, not what content ships. As written, a worker could recover an obsolete run's branch and build the replacement on top of its now-irrelevant commits instead of from the correct pre-invalidation base, carrying obsolete content into the final deliverable. Make that explicit: custody recovery settles ownership, not content, so the worker replaces obsolete work from the correct base and keeps the obsolete run's commits out of what gets validated and shipped. * no-mistakes(test): Restore minimal pre-invalidation replacement instruction * fix: dedupe redundant "replace the obsolete work" restatement Line 309 already says the worker replaces the obsolete work from the correct pre-invalidation base, excluding the obsolete commits. The closing sentence restated "replace the obsolete work" again before gating the final validation run, layering the same fact twice instead of stating it once. Trim the closing sentence to just the ownership gate and the single-run-against-final-head requirement it uniquely adds. * fix: bind backend overrides to exact-task authority (#1413) * fix: bind explicit --backend to exact-task authority A Herdr-backed second mate carried a prior one-task --backend tmux exception forward by analogy, so its child landed in tmux and never appeared under the second mate in Herdr. Runtime detection was correct; the authority surface was not. docs/configuration.md now owns that an explicit --backend is authorized only for that exact task. AGENTS.md and fm-spawn help point there. * no-mistakes(document): Consolidate backend selection authorization documentation * fix(herdr): prevent focus flashes during projected workspace cleanup (#1229) * fix: remove projected workspaces through Herdr's focus-preserving pane-death path Herdr 0.7.5's explicit close of a workspace-emptying last pane moves the attached client's focus to a neighbor workspace, flashing the captain's whole window and routing in-flight keystrokes to the wrong pane until Firstmate's exact-tab restore masks it 56-197 ms later. Teardown and cleanup now plan a workspace-emptying close as a focus-safe removal: verify the close empties the workspace, reposition the doomed workspace behind the focused one through the verified workspace.move transport when it sits before a non-last focused workspace, prove the pane holds one lone idle shell, and end that shell so Herdr removes the emptied workspace through its focus-preserving pane-death path. Any ambiguity or failure falls back to the plain close behind the existing restore backstop, and fm_backend_herdr_kill applies the same plan for non-projected removals. Two conditions proven on real hardware are encoded in the adapter: BSD ps reports a login shell's comm as "-zsh", and an idle shell transiently hosts a prompt helper right after a workspace.move relayout, absorbed by a bounded strict-sample settle window in the idle-shell proof, now the single owner shared with session-start cleanup. An isolated-lab regression reproduces the raw steal on 0.7.5 and proves the plan removes a doomed workspace with zero wrong-focus samples and no corrective focus; unit fixtures cover the position, edge, ambiguity, move and kill failure, escalation, and transient-helper cases. Upstream fixes (#1877 explicit close, #1912 pane death) are merged but unreleased; once released the plan degrades to a harmless reorder-then-remove. * no-mistakes(review): Confirm pane death from structured not-found responses * no-mistakes(review): Serialize Herdr kills and sample focus continuously * no-mistakes(review): Synchronize Herdr focus evidence output * no-mistakes(review): Refuse unlocked Herdr pane closes * no-mistakes(document): Correct Herdr focus-safety documentation * no-mistakes: apply CI fixes * fix: never erase a Herdr task's records while its pane survives a refused close A transient presentation-lock contention could produce a completed teardown while the exact Herdr pane stayed alive as an unowned restored shell: the kill refused the unlocked close (correctly), returned success, the warning was suppressed, and cleanup erased the task's status, turn-end, and metadata records after the isolated copy had already been returned. Teardown now acquires the named-session presentation lock before anything destructive: a contended lock refuses up front while the isolated copy, the task branch, every durable record, and the endpoint are all intact for a plain rerun, and the projected and flat close paths both run under that one held lock instead of acquiring their own. Durable records are erased only once the exact pane is confirmed gone through its structured presence; a refused, skipped, or failed close retains every record with a visible, retryable error, and after a skipped close (unresolvable lock path) only a structured pane_not_found counts as gone - unknown never does. The teardown regression drives a live contending lock holder end to end: the refusal touches nothing (no worktree return, no branch drop, no close attempt), and the retry after release returns the copy, closes the pane under the lock, and removes the records. The unconfirmed projected close now refuses with records retained, and the structured-presence gate has a strict/default unit matrix. * no-mistakes(review): Require structured pane-not-found before Herdr record removal * no-mistakes(document): Correct Herdr record-retention verification date * fix: refuse ambiguity, revalidate SIGKILL ownership, and roll back failed removals Three accepted-contract corrections from the post-CI personal review of the Herdr keep-spaces focus-flash mitigation. Ambiguous endpoint identity no longer counts as a confirmed-gone pane: a missing or malformed target refuses record removal in the structured presence gate, and teardown treats missing confirmation machinery as a refusal instead of skipping the gate, so only an exact structured pane_not_found ever erases durable task records. The pane-death SIGKILL escalation re-reads the exact pane's process information and refuses to signal unless the same shell pid still passes the strict bare-idle ownership proof, so a pid that exited and was reused by an unrelated process is never signaled; the refused escalation falls back to the plain close with the unrelated process untouched. A reposition whose removal is not confirmed no longer outlives the attempt: the emptying-close plan records the verified pre-move order and original index whenever it invokes the mover, and both close owners restore the exact original workspace order through a second verified move, under the same held session lock, before reporting the close as failed. Each defect was reproduced first: the unit matrix documented malformed identity as gone, the PID-reuse regression showed SIGKILL reaching a disowned pid, and the rollback regression showed a single unrestored move. Teardown-level regressions cover unparseable presence retention alongside the strict identity matrix. * no-mistakes(review): Require confirmed Herdr removal and resolvable teardown locks * no-mistakes(review): Enforce structured Herdr closes and teardown preflight * no-mistakes(review): Preflight explicit Herdr close confirmation helper * no-mistakes(document): Document Herdr rollback failure semantics * no-mistakes(review): Captain, harden recursive Herdr teardown safety * no-mistakes(document): Document recursive Herdr teardown evidence * fix: retain nested secondmate home when a recursive child cleanup fails Captain-decided Option A correction for nm-askuser-flash-r6, found during complete-diff rereview of the merged head. cleanup_firstmate_home_children's recursive secondmate branch called itself for a nested child's home without checking the result, then unconditionally removed that home right after. remove_firstmate_home ends in an unconditional recursive delete with no check for leftover records, so a nested secondmate whose own Herdr grandchild failed its confirmed-gone check would have its entire home - retained grandchild records included - erased by the very next line. Guard the recursive call the same way every other fallible call in this function already is: || return 1, skipping remove_firstmate_home and leaving the nested home and its records for a safe rerun. Empirically, fm-teardown.sh's set -eu already halted the script on the prior unguarded call before reaching removal (verified by hand with the guard reverted, under both this session's bash and stock macOS bash 3.2) - the reachable behavior was already correct. The explicit guard is still applied exactly as decided: it matches every sibling call site in the function, and it stops the correctness of this path depending on errexit's well-known fragility under refactors (a wrapping if/&&, or a future subshell) rather than on an explicit check. Adds a teardown-level regression building on the existing direct-child Herdr fixtures: a top-level secondmate contains a nested secondmate, whose own Herdr child's close goes unconfirmed. Proves through the public fm-teardown.sh interface that the nested home, the nested secondmate's own record, and the grandchild's metadata and status all survive, and that the top-level secondmate's record survives too. * no-mistakes(document): Document nested Herdr teardown retention * fix: prioritize completion runway in quota-aware dispatch (#1431) * fix(dispatch): prioritize quota completion runway * no-mistakes(document): Document completion-aware quota runway selection * fix(bin): preserve full task contract in no-mistakes intent (#1447) * Preserve task contract in no-mistakes intent * no-mistakes(review): Preserve complete current task contract in no-mistakes intent * fix(bin): parse punctuated secondmate registry entries safely (#1452) * fix: centralize secondmate registry parsing * no-mistakes(review): Centralize secondmate registry binding validation * no-mistakes(review): Harden registry EOF and symlink validation * no-mistakes(review): Reject unreadable registries before parsing * no-mistakes(document): Document punctuation-safe secondmate registry validation * no-mistakes: apply CI fixes * feat(bin): add durable process-event supervision (#1483) * feat(procevent): supervise long-polling sources into durable events Firstmate had no way to wait on a blocking external process without holding a conversational turn. Add a domain-neutral process-to-event runner plus a thin adapter around the currently published `lavish-axi poll` interface: canonical physical source identity, one machine-wide owner per source, direct argv execution, and durable 0600 result capture before any event referencing it is published on the existing wake queue. No second notifier, no polling control plane, and no retry machinery. A captured result with no durable handled acknowledgement stays eligible for bounded re-announcement across any number of drains and restarts. Draining a wake before acting on it and then starting a replacement session resurfaces the same exact source and sequence, and never puts result payload text in an event line. `fm-procevent.sh handled <source-id> <sequence>` is the only thing that stops re-announcement: generation-keyed, private, path-safe, durable, and atomically idempotent, so a paired external effect gated on its first-time versus repeat report is never authorized twice. An acknowledgement is refused unless matching captured result and adapter records already exist, so a premature or mistyped call cannot suppress a future result. The source side is unchanged and still lossy: the published poll clears feedback destructively before returning it, so a result lost in that window is unrecoverable. This is never at-least-once, no-loss, or lossless, and the handled acknowledgement is not a generic exactly-once effect either - a crash between an external effect and its acknowledgement can still repeat that effect on replay. Integrate registered sources with watcher supervision, the guards, and recoverable secondmate teardown across nested homes, and cover source identity, lifecycle races, supervision, restart handling, and cleanup safety with regressions. * no-mistakes(review): Prevent Lavish prompt text from spoofing missing sessions * no-mistakes(review): Serialize publication and secure handled acknowledgements * no-mistakes(document): Document hardened process-event acknowledgement guarantees * fix(procevent): never reclaim a source whose owned group still runs A runner is its own process group leader and starts the blocking source in that group, but the claim records only the leader PID and its identity. If the leader died while the source child kept running, the missing PID was classified stale: reconciliation released the claim and started a second runner while the old blocking source was still consuming the same canonical source. For the Lavish adapter that means two destructive long polls racing on one review session, so it is not harmless process litter. It also contradicted the documented promise that ownership is never released until the whole group is gone. Ownership state now distinguishes a generation that is really gone from one whose leader crashed with its group still alive. Reconcile stops that surviving group and releases its exact generation before starting any replacement, and keeps the claim for a later cycle when it cannot prove the group stopped or another home owns it. Acquisition and `start` treat the same state as held rather than reclaimable. Signalling that group is safe precisely because only an absent leader reaches this state. A reused PID leaves the leader alive, so the identity comparison still classifies it stale or uncertain and no group signal follows, which keeps the existing PID-reuse refusal intact. Add a public-interface regression for the exact crash cut - SIGKILL only the leader, prove the child group survives, reconcile, and prove the old group is gone with no second source running - plus its counterexample that a generation with no leader and no surviving group is still reclaimed. Update the runner help, operating documentation, skill, and verification record where they described reclaim in terms of the leader alone. * no-mistakes(review): Enforce runner group ownership and detect poller overlap * no-mistakes(review): Isolate runner groups from unrelated caller processes * no-mistakes(document): Document isolated process-event runner launch * no-mistakes(lint): Suppress Perl literal ShellCheck false positive * fix(bin): retire terminal process events and surface queued wakes (#1500) * fix(bin): deliver process-event results and retire ended sources Two defects reproduced during a real Lavish adapter session. One human `Send & End` produced four captured results: the real feedback, then recurring empty ended sessions. The generic runner had no way to learn a source was finished, so every reconcile restarted a poll that returned immediately. The runner now asks the source's own adapter - `fm-procevent-<adapter>.sh terminal <result-file>` - and on exit 0 alone re-proves ownership, drops the registration, and releases its own claim under one source boundary. Terminal knowledge stays adapter-owned: for Lavish that is an ended session, a missing session, and the final feedback delivery the published poll marks with `session_ended`. An adapter with no terminal command keeps its source armed exactly as before. Capture before publication, captured-result durability, queued wake durability, bounded re-announcement, handled deduplication, one-owner ownership, and explicit idempotent retirement are all unchanged. A captured result queued its `check` wake durably, but a healthy watcher with a fresh beacon never delivered it; the result surfaced only after a manual drain. Publication happens outside the watcher (in the runner) or unconditionally (in reconcile), so the watcher had no newly actionable signal to report and never reached its rewake path. It now reports a queued-but-unsurfaced process-event record through the same actionable exit every other wake uses, deduplicated by the same `.seen-*` marker discipline the signal scan uses, so the record is always durable before it is suppressed. The durable queue remains the authority and no second notifier, poller, timer, queue, or adapter-specific wake path is added. Regressions cover both, driven end to end: an armed Lavish source against a stand-in for the published poll polls once, captures once, publishes one distinct event, and retires itself; two fixture adapters prove the terminal decision follows the adapter alone; and a real capture plus a real watcher prove one proactive wake before any drain, with no duplicate wake while the record stays queued or after it is acknowledged. * no-mistakes(review): Harden process-event retirement and proactive delivery * no-mistakes(review): Route process-event delivery through shared wake owner * no-mistakes(document): Clarify process-event delivery and retirement documentation * no-mistakes(lint): Fix ShellCheck control-flow warnings * no-mistakes(lint): Fix wake output status lint warning * perf: shard portable serial tests across CI runners (#1544) * perf(ci): shard the portable serial behavior lane across runners The Behavior portable serial job ran all 69 scripts of the serial remainder on one runner. The measured serial sum on run 30725985757 was 1143762 ms (19m04s) against a 20-minute timeout, so the job intermittently reached the cap and was cancelled with every step passing. Setup is only about 7s, so the cost is entirely test wall time. Split the lane into four separate-runner shards. Each shard is still strictly serial, and separate runners mean no two of these stateful scripts ever share a machine, so the split needs no concurrency isolation proof. Assignment is longest-processing-time bin packing over measured per-script duration hints, balancing every shard to 285941 ms (~4m46s) of expected work, and the timeout tightens from 20 to 15 minutes. bin/fm-test-run.sh owns the shard count and refuses a lane whose "ofN" disagrees with it, while ci.yml derives the same count from strategy.job-total rather than a literal, so changing it in either file alone fails the lane loudly instead of leaving part of the required suite unrun. --check-coverage additionally proves the shards are non-empty, disjoint, and exactly equal to the serial lane. No test is weakened, skipped, or removed. Also replace the wall-clock sleeps in the --jobs scheduler test fixture with an explicit signal handshake between the fixtures. The old 0.5s-versus-0.05s race failed on a loaded machine; the handshake passes under sustained CPU saturation. * no-mistakes(review): Correct portable serial shard balance evidence * no-mistakes(document): Document portable serial shard evidence accurately * fix(bin): correct session lock and attached watcher supervision (#1545) * fix(bin): identify harness sessions by path and report delivered wakes Two supervision faults, both reported by a contributor and both open on the default branch. Fault 1: the Stop auto-arm never claims the home. fm_harness_ancestry_pid() matched only the basename of `ps -o comm=`, and Claude Code's native installer names the per-session executable by its version (.../share/claude/versions/ 2.1.220), so that basename identifies nothing. Three real failure shapes follow: a version-named session is missed entirely and the hook exits 0 with the epoch never written (unconditional on Linux, where procps reports the kernel exec name and ignores argv[0]); a claude-named daemon that directly parents sessions wins the outermost-contiguous-claude rule ahead of the session itself; and a session that is both version-named and daemon-parented has its live lock reclaimed as stale and rewritten to the shared daemon pid, corrupting the home's ownership record. Harness identity now also reads whole components of the executable path and of argv[0], which is what both platforms still carry. Matching whole components only keeps that widening safe: bin/fm-claude-stop-autoarm.sh and ~/.claude/hooks scripts have no "claude" component. Ownership is then decided against the session's whole contiguous harness ancestry rather than one chosen pid, which is the honest form of the question the library already documents ("does the current process descend from that same harness?"). That subsumes the outermost-pid rule for Claude's nested bg-spare worker chain instead of reverting it, and lets a daemon-parented session recognize its own lock. Lock acquisition still writes the outermost pid of the run, the only pid that lives as long as the session. Fault 2: an attached arm reports a delivered cycle as FAILED. The watcher prints its one reason line to its own stdout, so only the arm that forked it can read that line; an arm that attached observes nothing but a released lock and called a completely successful cycle "cycle ended without an actionable reason". No supervision event was lost - the durable queue held it - but every harness protocol reads that line as "supervision is down" and directs a manual re-arm. The arm now resolves an unobservable close against the durable wake queue, which records every wake before the watcher prints it and whose sequence counter never rewinds, not even across a drain. A cycle the queue proves delivered a wake reports that wake and exits 0; a cycle whose records a handling turn already drained reports the delivery without inventing a reason line; only a cycle that delivered nothing is still the typed nonzero failure. Fixing it in the arm covers codex, opencode, pi, grok and kimi, not just the Claude Stop path. Regressions: tests/fm-session-lock-ancestry.test.sh pins both platforms' ps semantics behind a deterministic process table and runs the real Stop auto-arm in version-named, daemon-parented, and combined real process trees, each orphaned so the walk cannot escape the fixture. tests/fm-watch-arm.test.sh drives a real watcher and a real attached arm through a real wake. Every fault case fails on the previous code. * no-mistakes(review): Bind watcher delivery records to process identity * no-mistakes(review): Return validated watcher identity atomically * no-mistakes(review): Track watcher successors by PID and identity * no-mistakes(document): Consolidate watcher arm-cycle documentation ownership * fix(bin): harden Claude supervision auto-arm recovery (#1495) * fix(supervision): harden Claude auto-arm failure handling * no-mistakes(review): Guarantee automatic retry after Claude auto-arm failures * no-mistakes(review): Gate attended fail-open on verified supervision failure * no-mistakes(document): Document Claude auto-arm retry and guard scope * no-mistakes: apply CI fixes * fix(supervision): make Claude fail-open progression monotonic * no-mistakes(review): Preserve auto-arm failure episodes until verified watcher recovery * no-mistakes(review): Linearize auto-arm failure progression across existing locks * no-mistakes(review): Linearize positive recovery across shared failure episode lock * no-mistakes(review): Scope Claude recovery contention to Claude guard mode * no-mistakes(document): Align supervision auto-arm documentation * no-mistakes(review): Preserve actionable wakes despite healthy successors * no-mistakes(document): Refresh supervision auto-arm documentation * feat(bin): require an explicit per-task delivery contract (#1563) * feat(bin): require an explicit ship delivery mode in fm-brief A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions. * feat(bin): require an explicit ship delivery contract at spawn and promotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode. * docs: record the explicit per-task delivery contract AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly. * test: pass ship delivery flags per call site in the Herdr launcher e2e The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper. * test: pass the ship delivery contract in the secondmate suites Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires. * feat(bin): support remote secondmate homes (#1576) * Add generic remote secondmate transport * Add routed remote secondmate replies * Add remote outbox backlog handoff * Integrate remote secondmate lifecycle * no-mistakes(review): Fix remote snapshot and handoff races * no-mistakes(review): Serialize remote home provisioning transactions * no-mistakes(review): Harden remote lifecycle transaction boundaries * no-mistakes(review): Serialize remote lifecycle mutations and fail closed * no-mistakes(review): Close remote lifecycle and file race windows * no-mistakes(review): Serialize remote reply retirement and inheritance * no-mistakes(review): Harden remote transfer integrity and recovery * no-mistakes(review): Serialize remote respawn with registry retirement * no-mistakes(document): Document remote bootstrap convergence accurately * no-mistakes(document): Clarify skipped remote secondmate mutations * no-mistakes(lint): Resolve remote script ShellCheck warnings * no-mistakes: apply CI fixes * feat: add per-task trace context propagation (#995) * feat(spawn): propagate a native W3C traceparent to spawned agents Add a default-off capability that resolves one W3C traceparent for a task, injects it into the agent's pane shell as the TRACEPARENT environment variable immediately before launch, and records the identical value as traceparent= in state/<id>.meta, so an external observer that explicitly reads that env value or meta field can correlate a worker, a Secondmate, and their nested children into one trace with no collector, storage, UI, or vendor coupling. TRACEPARENT as an environment variable is a firstmate convention carrying a W3C-formatted value: W3C Trace Context standardizes the header, not an env var, and OpenTelemetry SDKs do not read it automatically, so a downstream must consume it deliberately; this feature parents no SDK span by itself. Identity is per task, not per spawn: the carrier is minted with random ids on the first spawn, adopted as a child (fresh span, same trace) for a nested spawn whose parent already holds one, and reused verbatim from the meta on relaunch, so a task keeps one stable logical identity across restarts. A malformed or all-zero inherited value is treated as absent and roots a fresh trace. A new root is sampled (01) - a sampling decision a downstream parent-based sampler honors, not a guarantee that any collector stores a span, and firstmate emits no spans; a child preserves the inherited flag. Trust boundary: a firstmate-minted root is random and reads no prompt, path, task prose, credential, or arbitrary environment key. An inherited TRACEPARENT is opaque caller-controlled data - up to 24 bytes of id passed through after syntax validation - so whoever set it controls those bytes, a bounded fixed-width channel rather than a general content or secret channel. The feature adds no OTEL_* variable, no tracestate, and no arbitrary environment injection; it runs no configurable or arbitrary command, only the fixed local od and tr (resolved from PATH) to read a few bytes of entropy - a small local pipeline with no network or watchdog and no hard latency guarantee. Any entropy or validation failure that returns omits the carrier without aborting the spawn. A default-off spawn leaves the generated meta and launch environment unchanged. Enablement is default-off (config/trace-context, or FM_TRACE_CONTEXT where a non-empty value overrides and unset or empty defers to the file) and is propagated into secondmate homes, taking effect at each agent's next launch: a Secondmate launched or relaunched after enablement carries the primary trace into its nested workers, while an already-running Secondmate roots new traces for its own workers until relaunched. Injection reuses the existing GOTMPDIR channel, so all spawn backends and harnesses and the ship, scout, and secondmate paths are covered. Covered by a pure-library suite and a spawn-path integration test (fake tmux plus a real worktree, hermetic against ambient FM_TRACE_CONTEXT) proving the recorded and injected carriers are identical and sent before launch, that default-off writes and injects neither, that a relaunch reuses the recorded carrier, and that an explicit FM_TRACE_CONTEXT overrides the file both ways; plus a source-owner inheritance test proving trace-context propagates and absence-mirrors through propagate_inheritable_config. Documentation follows the repository documentation-audiences contract: docs/trace-context.md is maintainer-architecture rationale, the configuration schema lives in docs/configuration.md, and the repeatable test evidence is separated into docs/verification/trace-context.md (maintainer-verification), registered in docs/documentation-audiences.json. * fix(spawn): propagate the effective trace-context decision to secondmates FM_TRACE_CONTEXT overrode trace context only in the process that read it. A newly launched secondmate decided enablement from the inherited config/trace-context file alone, so the override did not cross the primary-to-secondmate boundary: FM_TRACE_CONTEXT=off with the file present left the secondmate's nested workers traced (a broken kill switch), and FM_TRACE_CONTEXT=on with the file absent left them untraced despite the inherited carrier. Deliver the primary's effective decision to a newly launched secondmate as a normalized on/off FM_TRACE_CONTEXT in the launch prefix, so a FM_TRACE_CONTEXT override governs the nested primary -> secondmate -> worker chain both ways, not just the copied file. The value is bounded to the literal on/off and does not broaden environment injection; the already-running secondmate boundary is unchanged. Add a genuine two-level spawn regression that drives fm-spawn twice with the exact environment the primary injects into the secondmate and proves both divergent directions end to end. Correct the documentation that implied secondmate coverage on every backend, since orca and cmux reject secondmate spawns, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Clarify Secondmate trace-context launch snapshots * no-mistakes(document): Correct trace-context documentation ownership and relaunch semantics * fix(spawn): resolve the trace-context decision once for carrier and snapshot The effective trace-context decision was read twice per spawn: once inside fm_trace_context_resolve for the recorded carrier, and again for the secondmate FM_TRACE_CONTEXT launch snapshot. A config-file change between the two reads could pair a carrier with the opposite enable state - an injected carrier with an off snapshot, or no carrier with an on snapshot. Freeze the effective on/off decision once, drive the carrier resolution under that frozen FM_TRACE_CONTEXT so it cannot independently re-read the file, and reuse the same frozen decision for the secondmate launch snapshot. Add a spawn-path regression that drives the file-decided path and proves the recorded carrier and the delivered snapshot always agree, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Preserve legacy Secondmate trace boundary * no-mistakes(document): Correct trace-context verification comparison base * no-mistakes(review): Captain, prevent failed trace delivery metadata claims * no-mistakes(review): Captain, align trace-context tests and verification evidence * no-mistakes(document): Correct trace-context verification evidence * no-mistakes(lint): Suppress intentional ShellCheck literal-dollar warnings * no-mistakes(review): Captain: freeze trace context at session start * no-mistakes(test): Captain: stabilize scheduler test and document Kimi trace coverage * no-mistakes(document): Document trace-context safety boundaries * fix(trace): fail off on stale session snapshots Publish each home session decision atomically through a same-directory temporary file and bind it to the current session lock. A replacement failure can no longer leave an earlier on decision active in a later session; missing, stale, malformed, or unpublishable state defaults safely to off. Add regressions for read-only replacement and failed publication, update spawn and session-start fixtures for the lock-bound format, and refresh the architecture and verification records. * no-mistakes(review): Fix trace spawn failure independence and duplicate safety * no-mistakes(document): Refresh trace-context documentation and verification * no-mistakes(review): Clear partial backend input after failed trace submission * no-mistakes(review): Stop unsafe trace delivery before launch append * no-mistakes(document): Document unsafe trace delivery handling * fix(trace): bound each trace to one routed task, never the routing agent A persistent Secondmate holds its launch-time TRACEPARENT in the process environment for its whole life, and routed requests never replace it, so resolving new-task carriers from the ambient environment chained every routed task into one ever-growing trace per Secondmate with distinct parent ids. Resolve now reuses the task's recorded carrier or mints a fresh sampled root, never reading ambient TRACEPARENT, so each routed task is its own trace boundary while relaunch, recovery, and scout-to-ship promotion keep one stable per-task identity. The spawn regression models the reviewed scenario exactly: two unrelated tasks spawned sequentially through one persistent Secondmate environment record and inject distinct trace ids, adopt nothing from the Secondmate's carrier, and a relaunch of the first task reuses its original carrier verbatim. * docs(trace): define the per-task trace boundary The design contract is one task per trace: a persistent Secondmate is routing infrastructure with its own agent identity, never a shared trace root for the unrelated tasks routed through it. Root/recovery semantics replace the removed child-inheritance path, the sampling and safety sections drop inherited-carrier language because ambient TRACEPARENT is never read, and the verification page records the refreshed suite inventories including the two-task Secondmate boundary regression. * test(trace): adopt the explicit per-task delivery contract in spawn fixtures Rebasing onto current main brings the explicit per-task delivery contract: ship spawns now require --mode and --yolo instead of resolving them from the project registry. The trace spawn fixtures pass the same explicit contract canonical spawn tests use, preserving the per-task trace boundary coverage unchanged, and the verification page records the refreshed comparison base. * fix(bin): harden tmux agent liveness across harnesses (#1577) * fix(bin): classify tmux agent liveness independent of process titles `fm_backend_tmux_agent_state` attributed a pane solely from `#{pane_current_command}`, which is a process TITLE a harness can rewrite, not a structural fact. Claude Code 2.1.220 reports its version string there, so a live Claude endpoint classified `ambiguous`: the session-start secondmate liveness sweep could no longer see it, and any consumer that gates on a positive classification refuses outright. Read a second, independent name source: the kernel `comm` of every process in the pane tty's foreground process group. Either source naming a verified harness yields `alive`, because a false `dead` is the one verdict that can start a duplicate agent on a live worktree. Scoping to the foreground process group rather than the pane's descendants keeps a harness-named background process from faking an agent, and covers multi-process launchers (the Pi …
Kallas95
added a commit
to Kallas95/firstmate
that referenced
this pull request
Sep 5, 2026
* fix: bind backend overrides to exact-task authority (#1413)
* fix: bind explicit --backend to exact-task authority
A Herdr-backed second mate carried a prior one-task --backend tmux
exception forward by analogy, so its child landed in tmux and never
appeared under the second mate in Herdr. Runtime detection was correct;
the authority surface was not.
docs/configuration.md now owns that an explicit --backend is authorized
only for that exact task. AGENTS.md and fm-spawn help point there.
* no-mistakes(document): Consolidate backend selection authorization documentation
* fix(herdr): prevent focus flashes during projected workspace cleanup (#1229)
* fix: remove projected workspaces through Herdr's focus-preserving pane-death path
Herdr 0.7.5's explicit close of a workspace-emptying last pane moves the
attached client's focus to a neighbor workspace, flashing the captain's
whole window and routing in-flight keystrokes to the wrong pane until
Firstmate's exact-tab restore masks it 56-197 ms later.
Teardown and cleanup now plan a workspace-emptying close as a focus-safe
removal: verify the close empties the workspace, reposition the doomed
workspace behind the focused one through the verified workspace.move
transport when it sits before a non-last focused workspace, prove the pane
holds one lone idle shell, and end that shell so Herdr removes the emptied
workspace through its focus-preserving pane-death path. Any ambiguity or
failure falls back to the plain close behind the existing restore backstop,
and fm_backend_herdr_kill applies the same plan for non-projected removals.
Two conditions proven on real hardware are encoded in the adapter: BSD ps
reports a login shell's comm as "-zsh", and an idle shell transiently
hosts a prompt helper right after a workspace.move relayout, absorbed by a
bounded strict-sample settle window in the idle-shell proof, now the single
owner shared with session-start cleanup.
An isolated-lab regression reproduces the raw steal on 0.7.5 and proves the
plan removes a doomed workspace with zero wrong-focus samples and no
corrective focus; unit fixtures cover the position, edge, ambiguity, move
and kill failure, escalation, and transient-helper cases. Upstream fixes
(#1877 explicit close, #1912 pane death) are merged but unreleased; once
released the plan degrades to a harmless reorder-then-remove.
* no-mistakes(review): Confirm pane death from structured not-found responses
* no-mistakes(review): Serialize Herdr kills and sample focus continuously
* no-mistakes(review): Synchronize Herdr focus evidence output
* no-mistakes(review): Refuse unlocked Herdr pane closes
* no-mistakes(document): Correct Herdr focus-safety documentation
* no-mistakes: apply CI fixes
* fix: never erase a Herdr task's records while its pane survives a refused close
A transient presentation-lock contention could produce a completed teardown
while the exact Herdr pane stayed alive as an unowned restored shell: the
kill refused the unlocked close (correctly), returned success, the warning
was suppressed, and cleanup erased the task's status, turn-end, and
metadata records after the isolated copy had already been returned.
Teardown now acquires the named-session presentation lock before anything
destructive: a contended lock refuses up front while the isolated copy, the
task branch, every durable record, and the endpoint are all intact for a
plain rerun, and the projected and flat close paths both run under that one
held lock instead of acquiring their own. Durable records are erased only
once the exact pane is confirmed gone through its structured presence; a
refused, skipped, or failed close retains every record with a visible,
retryable error, and after a skipped close (unresolvable lock path) only a
structured pane_not_found counts as gone - unknown never does.
The teardown regression drives a live contending lock holder end to end:
the refusal touches nothing (no worktree return, no branch drop, no close
attempt), and the retry after release returns the copy, closes the pane
under the lock, and removes the records. The unconfirmed projected close
now refuses with records retained, and the structured-presence gate has a
strict/default unit matrix.
* no-mistakes(review): Require structured pane-not-found before Herdr record removal
* no-mistakes(document): Correct Herdr record-retention verification date
* fix: refuse ambiguity, revalidate SIGKILL ownership, and roll back failed removals
Three accepted-contract corrections from the post-CI personal review of the
Herdr keep-spaces focus-flash mitigation.
Ambiguous endpoint identity no longer counts as a confirmed-gone pane: a
missing or malformed target refuses record removal in the structured
presence gate, and teardown treats missing confirmation machinery as a
refusal instead of skipping the gate, so only an exact structured
pane_not_found ever erases durable task records.
The pane-death SIGKILL escalation re-reads the exact pane's process
information and refuses to signal unless the same shell pid still passes
the strict bare-idle ownership proof, so a pid that exited and was reused
by an unrelated process is never signaled; the refused escalation falls
back to the plain close with the unrelated process untouched.
A reposition whose removal is not confirmed no longer outlives the attempt:
the emptying-close plan records the verified pre-move order and original
index whenever it invokes the mover, and both close owners restore the
exact original workspace order through a second verified move, under the
same held session lock, before reporting the close as failed.
Each defect was reproduced first: the unit matrix documented malformed
identity as gone, the PID-reuse regression showed SIGKILL reaching a
disowned pid, and the rollback regression showed a single unrestored move.
Teardown-level regressions cover unparseable presence retention alongside
the strict identity matrix.
* no-mistakes(review): Require confirmed Herdr removal and resolvable teardown locks
* no-mistakes(review): Enforce structured Herdr closes and teardown preflight
* no-mistakes(review): Preflight explicit Herdr close confirmation helper
* no-mistakes(document): Document Herdr rollback failure semantics
* no-mistakes(review): Captain, harden recursive Herdr teardown safety
* no-mistakes(document): Document recursive Herdr teardown evidence
* fix: retain nested secondmate home when a recursive child cleanup fails
Captain-decided Option A correction for nm-askuser-flash-r6, found during
complete-diff rereview of the merged head.
cleanup_firstmate_home_children's recursive secondmate branch called
itself for a nested child's home without checking the result, then
unconditionally removed that home right after. remove_firstmate_home
ends in an unconditional recursive delete with no check for leftover
records, so a nested secondmate whose own Herdr grandchild failed its
confirmed-gone check would have its entire home - retained grandchild
records included - erased by the very next line.
Guard the recursive call the same way every other fallible call in this
function already is: || return 1, skipping remove_firstmate_home and
leaving the nested home and its records for a safe rerun.
Empirically, fm-teardown.sh's set -eu already halted the script on the
prior unguarded call before reaching removal (verified by hand with the
guard reverted, under both this session's bash and stock macOS bash
3.2) - the reachable behavior was already correct. The explicit guard
is still applied exactly as decided: it matches every sibling call site
in the function, and it stops the correctness of this path depending on
errexit's well-known fragility under refactors (a wrapping if/&&, or a
future subshell) rather than on an explicit check.
Adds a teardown-level regression building on the existing direct-child
Herdr fixtures: a top-level secondmate contains a nested secondmate,
whose own Herdr child's close goes unconfirmed. Proves through the
public fm-teardown.sh interface that the nested home, the nested
secondmate's own record, and the grandchild's metadata and status all
survive, and that the top-level secondmate's record survives too.
* no-mistakes(document): Document nested Herdr teardown retention
* fix: prioritize completion runway in quota-aware dispatch (#1431)
* fix(dispatch): prioritize quota completion runway
* no-mistakes(document): Document completion-aware quota runway selection
* fix(bin): preserve full task contract in no-mistakes intent (#1447)
* Preserve task contract in no-mistakes intent
* no-mistakes(review): Preserve complete current task contract in no-mistakes intent
* fix(bin): parse punctuated secondmate registry entries safely (#1452)
* fix: centralize secondmate registry parsing
* no-mistakes(review): Centralize secondmate registry binding validation
* no-mistakes(review): Harden registry EOF and symlink validation
* no-mistakes(review): Reject unreadable registries before parsing
* no-mistakes(document): Document punctuation-safe secondmate registry validation
* no-mistakes: apply CI fixes
* feat(bin): add durable process-event supervision (#1483)
* feat(procevent): supervise long-polling sources into durable events
Firstmate had no way to wait on a blocking external process without holding
a conversational turn. Add a domain-neutral process-to-event runner plus a
thin adapter around the currently published `lavish-axi poll` interface:
canonical physical source identity, one machine-wide owner per source, direct
argv execution, and durable 0600 result capture before any event referencing
it is published on the existing wake queue. No second notifier, no polling
control plane, and no retry machinery.
A captured result with no durable handled acknowledgement stays eligible for
bounded re-announcement across any number of drains and restarts. Draining a
wake before acting on it and then starting a replacement session resurfaces
the same exact source and sequence, and never puts result payload text in an
event line. `fm-procevent.sh handled <source-id> <sequence>` is the only thing
that stops re-announcement: generation-keyed, private, path-safe, durable, and
atomically idempotent, so a paired external effect gated on its first-time
versus repeat report is never authorized twice.
An acknowledgement is refused unless matching captured result and adapter
records already exist, so a premature or mistyped call cannot suppress a
future result.
The source side is unchanged and still lossy: the published poll clears
feedback destructively before returning it, so a result lost in that window
is unrecoverable. This is never at-least-once, no-loss, or lossless, and the
handled acknowledgement is not a generic exactly-once effect either - a crash
between an external effect and its acknowledgement can still repeat that
effect on replay.
Integrate registered sources with watcher supervision, the guards, and
recoverable secondmate teardown across nested homes, and cover source
identity, lifecycle races, supervision, restart handling, and cleanup safety
with regressions.
* no-mistakes(review): Prevent Lavish prompt text from spoofing missing sessions
* no-mistakes(review): Serialize publication and secure handled acknowledgements
* no-mistakes(document): Document hardened process-event acknowledgement guarantees
* fix(procevent): never reclaim a source whose owned group still runs
A runner is its own process group leader and starts the blocking source in
that group, but the claim records only the leader PID and its identity. If the
leader died while the source child kept running, the missing PID was
classified stale: reconciliation released the claim and started a second
runner while the old blocking source was still consuming the same canonical
source. For the Lavish adapter that means two destructive long polls racing on
one review session, so it is not harmless process litter. It also contradicted
the documented promise that ownership is never released until the whole group
is gone.
Ownership state now distinguishes a generation that is really gone from one
whose leader crashed with its group still alive. Reconcile stops that
surviving group and releases its exact generation before starting any
replacement, and keeps the claim for a later cycle when it cannot prove the
group stopped or another home owns it. Acquisition and `start` treat the same
state as held rather than reclaimable.
Signalling that group is safe precisely because only an absent leader reaches
this state. A reused PID leaves the leader alive, so the identity comparison
still classifies it stale or uncertain and no group signal follows, which
keeps the existing PID-reuse refusal intact.
Add a public-interface regression for the exact crash cut - SIGKILL only the
leader, prove the child group survives, reconcile, and prove the old group is
gone with no second source running - plus its counterexample that a generation
with no leader and no surviving group is still reclaimed. Update the runner
help, operating documentation, skill, and verification record where they
described reclaim in terms of the leader alone.
* no-mistakes(review): Enforce runner group ownership and detect poller overlap
* no-mistakes(review): Isolate runner groups from unrelated caller processes
* no-mistakes(document): Document isolated process-event runner launch
* no-mistakes(lint): Suppress Perl literal ShellCheck false positive
* fix(bin): retire terminal process events and surface queued wakes (#1500)
* fix(bin): deliver process-event results and retire ended sources
Two defects reproduced during a real Lavish adapter session.
One human `Send & End` produced four captured results: the real feedback,
then recurring empty ended sessions. The generic runner had no way to learn
a source was finished, so every reconcile restarted a poll that returned
immediately. The runner now asks the source's own adapter -
`fm-procevent-<adapter>.sh terminal <result-file>` - and on exit 0 alone
re-proves ownership, drops the registration, and releases its own claim
under one source boundary. Terminal knowledge stays adapter-owned: for
Lavish that is an ended session, a missing session, and the final feedback
delivery the published poll marks with `session_ended`. An adapter with no
terminal command keeps its source armed exactly as before. Capture before
publication, captured-result durability, queued wake durability, bounded
re-announcement, handled deduplication, one-owner ownership, and explicit
idempotent retirement are all unchanged.
A captured result queued its `check` wake durably, but a healthy watcher
with a fresh beacon never delivered it; the result surfaced only after a
manual drain. Publication happens outside the watcher (in the runner) or
unconditionally (in reconcile), so the watcher had no newly actionable
signal to report and never reached its rewake path. It now reports a
queued-but-unsurfaced process-event record through the same actionable exit
every other wake uses, deduplicated by the same `.seen-*` marker discipline
the signal scan uses, so the record is always durable before it is
suppressed. The durable queue remains the authority and no second notifier,
poller, timer, queue, or adapter-specific wake path is added.
Regressions cover both, driven end to end: an armed Lavish source against a
stand-in for the published poll polls once, captures once, publishes one
distinct event, and retires itself; two fixture adapters prove the terminal
decision follows the adapter alone; and a real capture plus a real watcher
prove one proactive wake before any drain, with no duplicate wake while the
record stays queued or after it is acknowledged.
* no-mistakes(review): Harden process-event retirement and proactive delivery
* no-mistakes(review): Route process-event delivery through shared wake owner
* no-mistakes(document): Clarify process-event delivery and retirement documentation
* no-mistakes(lint): Fix ShellCheck control-flow warnings
* no-mistakes(lint): Fix wake output status lint warning
* perf: shard portable serial tests across CI runners (#1544)
* perf(ci): shard the portable serial behavior lane across runners
The Behavior portable serial job ran all 69 scripts of the serial
remainder on one runner. The measured serial sum on run 30725985757 was
1143762 ms (19m04s) against a 20-minute timeout, so the job intermittently
reached the cap and was cancelled with every step passing. Setup is only
about 7s, so the cost is entirely test wall time.
Split the lane into four separate-runner shards. Each shard is still
strictly serial, and separate runners mean no two of these stateful
scripts ever share a machine, so the split needs no concurrency isolation
proof. Assignment is longest-processing-time bin packing over measured
per-script duration hints, balancing every shard to 285941 ms (~4m46s) of
expected work, and the timeout tightens from 20 to 15 minutes.
bin/fm-test-run.sh owns the shard count and refuses a lane whose "ofN"
disagrees with it, while ci.yml derives the same count from
strategy.job-total rather than a literal, so changing it in either file
alone fails the lane loudly instead of leaving part of the required suite
unrun. --check-coverage additionally proves the shards are non-empty,
disjoint, and exactly equal to the serial lane. No test is weakened,
skipped, or removed.
Also replace the wall-clock sleeps in the --jobs scheduler test fixture
with an explicit signal handshake between the fixtures. The old
0.5s-versus-0.05s race failed on a loaded machine; the handshake passes
under sustained CPU saturation.
* no-mistakes(review): Correct portable serial shard balance evidence
* no-mistakes(document): Document portable serial shard evidence accurately
* fix(bin): correct session lock and attached watcher supervision (#1545)
* fix(bin): identify harness sessions by path and report delivered wakes
Two supervision faults, both reported by a contributor and both open on the
default branch.
Fault 1: the Stop auto-arm never claims the home. fm_harness_ancestry_pid()
matched only the basename of `ps -o comm=`, and Claude Code's native installer
names the per-session executable by its version (.../share/claude/versions/
2.1.220), so that basename identifies nothing. Three real failure shapes follow:
a version-named session is missed entirely and the hook exits 0 with the epoch
never written (unconditional on Linux, where procps reports the kernel exec name
and ignores argv[0]); a claude-named daemon that directly parents sessions wins
the outermost-contiguous-claude rule ahead of the session itself; and a session
that is both version-named and daemon-parented has its live lock reclaimed as
stale and rewritten to the shared daemon pid, corrupting the home's ownership
record.
Harness identity now also reads whole components of the executable path and of
argv[0], which is what both platforms still carry. Matching whole components
only keeps that widening safe: bin/fm-claude-stop-autoarm.sh and ~/.claude/hooks
scripts have no "claude" component. Ownership is then decided against the
session's whole contiguous harness ancestry rather than one chosen pid, which is
the honest form of the question the library already documents ("does the current
process descend from that same harness?"). That subsumes the outermost-pid rule
for Claude's nested bg-spare worker chain instead of reverting it, and lets a
daemon-parented session recognize its own lock. Lock acquisition still writes the
outermost pid of the run, the only pid that lives as long as the session.
Fault 2: an attached arm reports a delivered cycle as FAILED. The watcher prints
its one reason line to its own stdout, so only the arm that forked it can read
that line; an arm that attached observes nothing but a released lock and called a
completely successful cycle "cycle ended without an actionable reason". No
supervision event was lost - the durable queue held it - but every harness
protocol reads that line as "supervision is down" and directs a manual re-arm.
The arm now resolves an unobservable close against the durable wake queue, which
records every wake before the watcher prints it and whose sequence counter never
rewinds, not even across a drain. A cycle the queue proves delivered a wake
reports that wake and exits 0; a cycle whose records a handling turn already
drained reports the delivery without inventing a reason line; only a cycle that
delivered nothing is still the typed nonzero failure. Fixing it in the arm covers
codex, opencode, pi, grok and kimi, not just the Claude Stop path.
Regressions: tests/fm-session-lock-ancestry.test.sh pins both platforms' ps
semantics behind a deterministic process table and runs the real Stop auto-arm in
version-named, daemon-parented, and combined real process trees, each orphaned so
the walk cannot escape the fixture. tests/fm-watch-arm.test.sh drives a real
watcher and a real attached arm through a real wake. Every fault case fails on
the previous code.
* no-mistakes(review): Bind watcher delivery records to process identity
* no-mistakes(review): Return validated watcher identity atomically
* no-mistakes(review): Track watcher successors by PID and identity
* no-mistakes(document): Consolidate watcher arm-cycle documentation ownership
* fix(bin): harden Claude supervision auto-arm recovery (#1495)
* fix(supervision): harden Claude auto-arm failure handling
* no-mistakes(review): Guarantee automatic retry after Claude auto-arm failures
* no-mistakes(review): Gate attended fail-open on verified supervision failure
* no-mistakes(document): Document Claude auto-arm retry and guard scope
* no-mistakes: apply CI fixes
* fix(supervision): make Claude fail-open progression monotonic
* no-mistakes(review): Preserve auto-arm failure episodes until verified watcher recovery
* no-mistakes(review): Linearize auto-arm failure progression across existing locks
* no-mistakes(review): Linearize positive recovery across shared failure episode lock
* no-mistakes(review): Scope Claude recovery contention to Claude guard mode
* no-mistakes(document): Align supervision auto-arm documentation
* no-mistakes(review): Preserve actionable wakes despite healthy successors
* no-mistakes(document): Refresh supervision auto-arm documentation
* feat(bin): require an explicit per-task delivery contract (#1563)
* feat(bin): require an explicit ship delivery mode in fm-brief
A ship brief's definition of done was shaped by a silent per-project registry
lookup, so an adjusted brief and the task's recorded delivery could disagree and
no one had to decide anything per task.
fm-brief now requires --mode on ship scaffolds, validates it against the closed
set, refuses the conditional no-mistakes-prod-only registry policy as a task
mode, and records the choice as a fixed machine-readable "Delivery contract:
mode=<mode>" line that fm-spawn can check. --mode is refused on scout and
secondmate scaffolds, and --yolo is refused outright because the worker never
owns approval decisions.
* feat(bin): require an explicit ship delivery contract at spawn and promotion
fm-spawn resolved every ship and scout task's mode and yolo from the project
registry, so the delivery posture was never a per-task decision and could
contradict the brief the worker was about to follow.
fm-spawn now requires --mode and --yolo on ship spawns, validates both against
their closed sets, and reads the brief's recorded delivery contract line and
refuses a mismatch before any endpoint exists; a brief scaffolded before that
line existed warns once and launches on the flag. A batch carries one shared
contract that each pair still checks against its own brief. Scout and secondmate
spawns refuse the flags, and a scout now records no mode or yolo at all, which
teardown and the snapshot already tolerate. When the explicit mode carries less
rigor than the project's standing posture, a deviation notice is printed and the
spawn continues, so the registry stays advisory rather than an enforced default.
fm-promote requires the same two flags, because a scout carries no posture to
inherit, and writes them into the task record with the kind flip.
fm-project-mode keeps its one registry parser for the mechanical consumers that
have no task in hand, accepts the conditional no-mistakes-prod-only annotation
and maps it to its most rigorous leg for them, and grows --raw so the deviation
notice can tell a conditional policy apart from a flat mode.
* docs: record the explicit per-task delivery contract
AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at
intake, including the surface classification for a no-mistakes-prod-only project
and the unregistered-project fallback, and the project-management skill defines
that conditional policy as a registration-time posture with its defaults and
initialization consequences. The registry blurb, script table, and architecture
section follow: the registry records the captain's standing posture, and task
delivery is decided per task and passed explicitly.
* test: pass ship delivery flags per call site in the Herdr launcher e2e
The shared spawn helper also launches a secondmate, which refuses the flags, so
the contract belongs at each ship call site rather than inside the helper.
* test: pass the ship delivery contract in the secondmate suites
Both suites scaffold or spawn an ordinary ship task as the control case for a
secondmate assertion, so each needs the explicit contract the ship path now
requires.
* feat(bin): support remote secondmate homes (#1576)
* Add generic remote secondmate transport
* Add routed remote secondmate replies
* Add remote outbox backlog handoff
* Integrate remote secondmate lifecycle
* no-mistakes(review): Fix remote snapshot and handoff races
* no-mistakes(review): Serialize remote home provisioning transactions
* no-mistakes(review): Harden remote lifecycle transaction boundaries
* no-mistakes(review): Serialize remote lifecycle mutations and fail closed
* no-mistakes(review): Close remote lifecycle and file race windows
* no-mistakes(review): Serialize remote reply retirement and inheritance
* no-mistakes(review): Harden remote transfer integrity and recovery
* no-mistakes(review): Serialize remote respawn with registry retirement
* no-mistakes(document): Document remote bootstrap convergence accurately
* no-mistakes(document): Clarify skipped remote secondmate mutations
* no-mistakes(lint): Resolve remote script ShellCheck warnings
* no-mistakes: apply CI fixes
* feat: add per-task trace context propagation (#995)
* feat(spawn): propagate a native W3C traceparent to spawned agents
Add a default-off capability that resolves one W3C traceparent for a task,
injects it into the agent's pane shell as the TRACEPARENT environment
variable immediately before launch, and records the identical value as
traceparent= in state/<id>.meta, so an external observer that explicitly
reads that env value or meta field can correlate a worker, a Secondmate, and
their nested children into one trace with no collector, storage, UI, or
vendor coupling.
TRACEPARENT as an environment variable is a firstmate convention carrying a
W3C-formatted value: W3C Trace Context standardizes the header, not an env
var, and OpenTelemetry SDKs do not read it automatically, so a downstream
must consume it deliberately; this feature parents no SDK span by itself.
Identity is per task, not per spawn: the carrier is minted with random ids on
the first spawn, adopted as a child (fresh span, same trace) for a nested
spawn whose parent already holds one, and reused verbatim from the meta on
relaunch, so a task keeps one stable logical identity across restarts. A
malformed or all-zero inherited value is treated as absent and roots a fresh
trace. A new root is sampled (01) - a sampling decision a downstream
parent-based sampler honors, not a guarantee that any collector stores a
span, and firstmate emits no spans; a child preserves the inherited flag.
Trust boundary: a firstmate-minted root is random and reads no prompt, path,
task prose, credential, or arbitrary environment key. An inherited
TRACEPARENT is opaque caller-controlled data - up to 24 bytes of id passed
through after syntax validation - so whoever set it controls those bytes, a
bounded fixed-width channel rather than a general content or secret channel.
The feature adds no OTEL_* variable, no tracestate, and no arbitrary
environment injection; it runs no configurable or arbitrary command, only the
fixed local od and tr (resolved from PATH) to read a few bytes of entropy - a
small local pipeline with no network or watchdog and no hard latency
guarantee. Any entropy or validation failure that returns omits the carrier
without aborting the spawn. A default-off spawn leaves the generated meta and
launch environment unchanged.
Enablement is default-off (config/trace-context, or FM_TRACE_CONTEXT where a
non-empty value overrides and unset or empty defers to the file) and is
propagated into secondmate homes, taking effect at each agent's next launch:
a Secondmate launched or relaunched after enablement carries the primary
trace into its nested workers, while an already-running Secondmate roots new
traces for its own workers until relaunched. Injection reuses the existing
GOTMPDIR channel, so all spawn backends and harnesses and the ship, scout,
and secondmate paths are covered.
Covered by a pure-library suite and a spawn-path integration test (fake tmux
plus a real worktree, hermetic against ambient FM_TRACE_CONTEXT) proving the
recorded and injected carriers are identical and sent before launch, that
default-off writes and injects neither, that a relaunch reuses the recorded
carrier, and that an explicit FM_TRACE_CONTEXT overrides the file both ways;
plus a source-owner inheritance test proving trace-context propagates and
absence-mirrors through propagate_inheritable_config.
Documentation follows the repository documentation-audiences contract:
docs/trace-context.md is maintainer-architecture rationale, the configuration
schema lives in docs/configuration.md, and the repeatable test evidence is
separated into docs/verification/trace-context.md (maintainer-verification),
registered in docs/documentation-audiences.json.
* fix(spawn): propagate the effective trace-context decision to secondmates
FM_TRACE_CONTEXT overrode trace context only in the process that read it. A
newly launched secondmate decided enablement from the inherited
config/trace-context file alone, so the override did not cross the
primary-to-secondmate boundary: FM_TRACE_CONTEXT=off with the file present left
the secondmate's nested workers traced (a broken kill switch), and
FM_TRACE_CONTEXT=on with the file absent left them untraced despite the
inherited carrier.
Deliver the primary's effective decision to a newly launched secondmate as a
normalized on/off FM_TRACE_CONTEXT in the launch prefix, so a FM_TRACE_CONTEXT
override governs the nested primary -> secondmate -> worker chain both ways, not
just the copied file. The value is bounded to the literal on/off and does not
broaden environment injection; the already-running secondmate boundary is
unchanged.
Add a genuine two-level spawn regression that drives fm-spawn twice with the
exact environment the primary injects into the secondmate and proves both
divergent directions end to end. Correct the documentation that implied
secondmate coverage on every backend, since orca and cmux reject secondmate
spawns, and refresh the verification evidence for the new assertion count.
* no-mistakes(review): Clarify Secondmate trace-context launch snapshots
* no-mistakes(document): Correct trace-context documentation ownership and relaunch semantics
* fix(spawn): resolve the trace-context decision once for carrier and snapshot
The effective trace-context decision was read twice per spawn: once inside
fm_trace_context_resolve for the recorded carrier, and again for the secondmate
FM_TRACE_CONTEXT launch snapshot. A config-file change between the two reads
could pair a carrier with the opposite enable state - an injected carrier with
an off snapshot, or no carrier with an on snapshot.
Freeze the effective on/off decision once, drive the carrier resolution under
that frozen FM_TRACE_CONTEXT so it cannot independently re-read the file, and
reuse the same frozen decision for the secondmate launch snapshot. Add a
spawn-path regression that drives the file-decided path and proves the recorded
carrier and the delivered snapshot always agree, and refresh the verification
evidence for the new assertion count.
* no-mistakes(review): Preserve legacy Secondmate trace boundary
* no-mistakes(document): Correct trace-context verification comparison base
* no-mistakes(review): Captain, prevent failed trace delivery metadata claims
* no-mistakes(review): Captain, align trace-context tests and verification evidence
* no-mistakes(document): Correct trace-context verification evidence
* no-mistakes(lint): Suppress intentional ShellCheck literal-dollar warnings
* no-mistakes(review): Captain: freeze trace context at session start
* no-mistakes(test): Captain: stabilize scheduler test and document Kimi trace coverage
* no-mistakes(document): Document trace-context safety boundaries
* fix(trace): fail off on stale session snapshots
Publish each home session decision atomically through a same-directory temporary file and bind it to the current session lock. A replacement failure can no longer leave an earlier on decision active in a later session; missing, stale, malformed, or unpublishable state defaults safely to off.
Add regressions for read-only replacement and failed publication, update spawn and session-start fixtures for the lock-bound format, and refresh the architecture and verification records.
* no-mistakes(review): Fix trace spawn failure independence and duplicate safety
* no-mistakes(document): Refresh trace-context documentation and verification
* no-mistakes(review): Clear partial backend input after failed trace submission
* no-mistakes(review): Stop unsafe trace delivery before launch append
* no-mistakes(document): Document unsafe trace delivery handling
* fix(trace): bound each trace to one routed task, never the routing agent
A persistent Secondmate holds its launch-time TRACEPARENT in the process
environment for its whole life, and routed requests never replace it, so
resolving new-task carriers from the ambient environment chained every
routed task into one ever-growing trace per Secondmate with distinct
parent ids. Resolve now reuses the task's recorded carrier or mints a
fresh sampled root, never reading ambient TRACEPARENT, so each routed
task is its own trace boundary while relaunch, recovery, and
scout-to-ship promotion keep one stable per-task identity.
The spawn regression models the reviewed scenario exactly: two unrelated
tasks spawned sequentially through one persistent Secondmate environment
record and inject distinct trace ids, adopt nothing from the Secondmate's
carrier, and a relaunch of the first task reuses its original carrier
verbatim.
* docs(trace): define the per-task trace boundary
The design contract is one task per trace: a persistent Secondmate is
routing infrastructure with its own agent identity, never a shared trace
root for the unrelated tasks routed through it. Root/recovery semantics
replace the removed child-inheritance path, the sampling and safety
sections drop inherited-carrier language because ambient TRACEPARENT is
never read, and the verification page records the refreshed suite
inventories including the two-task Secondmate boundary regression.
* test(trace): adopt the explicit per-task delivery contract in spawn fixtures
Rebasing onto current main brings the explicit per-task delivery contract:
ship spawns now require --mode and --yolo instead of resolving them from the
project registry. The trace spawn fixtures pass the same explicit contract
canonical spawn tests use, preserving the per-task trace boundary coverage
unchanged, and the verification page records the refreshed comparison base.
* fix(bin): harden tmux agent liveness across harnesses (#1577)
* fix(bin): classify tmux agent liveness independent of process titles
`fm_backend_tmux_agent_state` attributed a pane solely from
`#{pane_current_command}`, which is a process TITLE a harness can rewrite,
not a structural fact. Claude Code 2.1.220 reports its version string there,
so a live Claude endpoint classified `ambiguous`: the session-start secondmate
liveness sweep could no longer see it, and any consumer that gates on a
positive classification refuses outright.
Read a second, independent name source: the kernel `comm` of every process in
the pane tty's foreground process group. Either source naming a verified
harness yields `alive`, because a false `dead` is the one verdict that can
start a duplicate agent on a live worktree. Scoping to the foreground process
group rather than the pane's descendants keeps a harness-named background
process from faking an agent, and covers multi-process launchers (the Pi
Launcher path) without a special case.
Verified on 2026-08-03 against all seven adapters running for real on tmux
3.6a / macOS 26.5.2 arm64: claude 2.1.220, codex-cli 0.146.0, opencode
1.18.11, pi 0.82.0, pi-signed 0.82.0, grok 0.2.118, kimi 0.31.1 all classify
`alive`, each attributed by a source independent of its title.
Two tests, because they fail for different reasons:
- tests/fm-tmux-agent-liveness.test.sh pins the logic with real processes and
no harness, so it runs everywhere CI runs tmux. It drives the two name
sources apart on purpose and asserts the divergence, so no case can go
quietly vacuous.
- tests/fm-harness-liveness-drift-live-e2e.test.sh relaunches every installed
harness and fails naming the harness and version when one stops being
attributed by a title-independent source.
AGENTS.md section 4 carries the resulting standing rule, and
firstmate-coding-guidelines owns how to satisfy it.
* no-mistakes: apply CI fixes
* docs: move the harness-dependent-check policy out of AGENTS.md
The standing rule was stated in AGENTS.md section 4 with the mechanics in
firstmate-coding-guidelines, which split one contract across two owners and
charged every session for a rule that only fires when firstmate's own
harness-dependent code is being changed.
firstmate-coding-guidelines is now the single owner of both the rule and how
to satisfy it: real-harness proof required, that proof authorized to spend
tokens, structural signals preferred over vendor-rendered surfaces, and a
guard that fails loudly naming the harness and version where a surface signal
is unavoidable. No inline stub is left behind, because AGENTS.md already
carries the load trigger for that skill in sections 7 and 13, so it is read
before any change to firstmate's shared tracked material.
Also records the cross-platform lesson the pipeline caught in the portable
regression, and corrects that file's header: the divergence assertion lives
on the version-string case, which diverges on both supported platforms,
rather than on every case.
* no-mistakes(review): Harden tmux liveness identity and drift validation
* no-mistakes(document): Clarify cross-platform tmux liveness documentation
* feat(bin): propagate trace context to remote secondmates (#1609)
* feat(bin): trace remote secondmate routes and unify the inherit allowlist
Per-task W3C trace context (#995) resolved and injected its carrier only at
the local spawn path. A remote secondmate is routed through
spawn_remote_secondmate, which returns long before that site and wrote its own
metadata block, so a remote secondmate stayed silently untraced even with the
capability enabled.
The parent home still owns that task's identity, because it holds the metadata
an observer reads. It now resolves the carrier against the task's own meta
under its own frozen decision - reused verbatim on relaunch, freshly rooted
otherwise, never adopting the parent process's ambient TRACEPARENT - and hands
it to the configured host through a new fm-spawn --traceparent argument,
accepted only for a secondmate launch and only as a strict W3C value. The
remote host exports it at the same unconditional pre-launch site and reports
back the carrier its endpoint actually holds, which the parent records, so an
already-alive endpoint reports the identity its agent really received rather
than one the parent merely intended. Disabled remains byte-identical and off.
The remote inherit path also carried its own hardcoded copy of the inheritable
config set, already drifted from FM_INHERITABLE_CONFIG by trace-context. Both
remote ends now derive from that one declaration, so a future item cannot be
sent by one side and refused by the other, and session-scoped enablement items
are skipped on live convergence exactly as the local path skips them.
Also fixes a latent stderr leak: an absent session lock printed a raw redirect
failure, which the new remote resolve site made visible.
Adds tests/fm-remote-secondmate-trace-context.test.sh, driving the real
parent -> fm-on -> remote entrypoint -> control -> remote fm-spawn chain over
the deterministic SSH boundary and reading the carrier back from the remote
pane's own log.
* no-mistakes(document): Clarify remote trace and allowlist contracts
* feat(bin): preflight remote runtime tool paths (#1623)
* feat(bin): widen the remote runtime PATH and add a remote doctor preflight
The fixed remote entrypoint hard-coded a four-directory PATH, so a remote
account whose tools live under nix or a per-user profile could not run basic
Firstmate work without a login shell. The entrypoint now composes its child
PATH from the code root's bin, the account's ~/.local/bin, the common
package-manager directories that actually exist on the host, and the portable
system tail, deduplicated and in a fixed order, still under env -i with the
same variable allowlist and no shell command string.
fm-remote-doctor.sh reports that exact PATH by inheriting it from its own
entrypoint launch rather than recomposing it, so the ordering keeps one owner.
It is read-only, reports where each required and optional tool resolved, and
exits non-zero naming every required tool that did not. Remote seeding runs it
as a preflight before anything is created on the host and restores the registry
when it fails.
* no-mistakes(review): Harden remote git authorization and missing-tool diagnostics
* no-mistakes(document): Document remote PATH doctor and safe shims
* no-mistakes(lint): Fix ShellCheck findings in remote path tests
* no-mistakes(lint): Suppress exported fixture's false-positive ShellCheck warning
* feat: gate remote second mates on Herdr readiness (#1639)
* feat(bin): gate remote second mates on herdr readiness
A remote second mate now always runs on the Herdr backend, whose server
belongs to the host's GUI login session and therefore outlives the SSH
connections that supervise it. fm-spawn's remote route forces that backend
and the host-local control script refuses any other, so the requirement
cannot be dropped from either side.
fm-remote-doctor.sh becomes the single owner of what "ready" means. It keeps
its PATH and tool reporting from #1623 and adds the Herdr, Aqua LaunchAgent,
GUI-session, server-reachability, and entrypoint-symlink checks, tagging each
gap fixable: or human: with the exact operator step. --fix closes only the
automatable gaps - writing and loading the Aqua-scoped dev.firstmate.herdr
launch agent, starting the server where no launch agent applies, and
recreating the entrypoint symlink - then re-derives every check from the host,
so a human gap is never presented as fixed. It never creates a login session,
writes an auto-login password, or touches FileVault.
Remote seed, remote spawn, and the startup liveness relaunch all run the same
check, repair, re-check sequence through one shared library and fail closed
with the doctor's own gap text. Recovery inherits the gate because it respawns
through the same route.
Tests drive the real doctor against a controlled account fixture with a
private HOME, a state-backed launchctl, and a fake herdr, and prove the
dangerous actions are never attempted. The remote lifecycle suites gain a
stateful Herdr CLI fixture and answer the readiness gate at the SSH boundary,
so they never inspect or repair the runner's own account.
* no-mistakes(review): Validate launch-agent contract and confirm Herdr startup
* no-mistakes(review): Validate loaded launch-agent contract before readiness
* no-mistakes(review): Refuse legacy remote backends without altering routes
* no-mistakes(review): Clarify conditional remote readiness repair sequence
* no-mistakes(review): Repair remote readiness before liveness probing
* no-mistakes(review): Preserve unknown seeds and reject legacy liveness
* no-mistakes(document): docs: clarify remote Herdr backend ownership
* fix: isolate remote secondmates in shared Herdr session (#1659)
* Pin remote secondmates to fm-remote
* no-mistakes(review): Fail closed on legacy remote Herdr endpoints
* no-mistakes(review): Isolate fm-remote launch agent from interactive default
* no-mistakes(document): Document shared remote Herdr retirement safety
* feat: route remote commands through an Aqua job worker (#1660)
* feat: run remote commands through Aqua job worker
* no-mistakes(review): Enforce remote job deadlines and safe worker shutdown
* no-mistakes(review): Refresh stale workers and harden dependency-free supervision
* no-mistakes(review): Harden worker ownership recovery and shutdown quarantine
* no-mistakes(review): Fix doctor bootstrap, harness repair, and output draining
* no-mistakes(review): Probe doctor tools through authenticated worker bootstrap
* no-mistakes(review): Refresh stale workers before doctor tool probes
* no-mistakes(review): Recover stopped quarantines and extend job deadlines
* no-mistakes(review): Separate queue and execution timeout windows
* no-mistakes(review): Supervise Linux worker crashes and bind root identity
* no-mistakes(review): Resolve authorized Nix profile bin links
* no-mistakes(review): Clarify Nix path resolution documentation
* no-mistakes(review): Harden PATH safety and nvm selection
* no-mistakes(review): Honor nvm system defaults and refresh doctor digest
* no-mistakes(review): Keep workers ready during active jobs
* no-mistakes(review): Bound pre-execution validation by job timeout
* no-mistakes(document): Clarify remote worker documentation
* no-mistakes(lint): Fix remote worker ShellCheck diagnostics
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
* fix: clarify remote doctor bootstrap path (#1691)
* fix(bin): bound remote SSH dead-peer detection (#1699)
* fix(remote): arm SSH dead-peer detection in fm-on.sh
A vanished remote host mid-poll (a reboot, a dropped link) left ssh
blocked indefinitely on a half-open TCP connection, because fm-on.sh's
ssh invocation had no ServerAliveInterval/ServerAliveCountMax. This
wedged the remote-reply ferry: fm-procevent.sh's runner blocked inside
the ssh child and never reached its own no-result -> claim-release ->
reconcile re-arm self-healing path, which otherwise already handles a
nonzero exit with empty output correctly. Recovery required a manual
retire and re-arm.
Arm ServerAliveInterval=15 and ServerAliveCountMax=3 by default
(bounded ~45s detection window), both overridable via
FM_SSH_ALIVE_INTERVAL and FM_SSH_ALIVE_COUNT_MAX. This is a transport-
level fix in fm-on.sh, so it covers every remote command routed
through it, not just the reply ferry. The remote sshd answers
keepalive probes independently of whatever the remote command is
doing, so a legitimately long-but-alive command (a 55s poll, a clone,
the doctor) is never falsely killed - only a truly vanished peer trips
it, turning that case into a bounded, detectable ssh failure (exit
255) instead of an indefinite hang.
Extends tests/fm-on.test.sh with a behavioral regression asserting a
bounded, positive ServerAliveInterval/ServerAliveCountMax on the real
ssh argv captured through the FM_SSH_BIN process seam, plus coverage
that both are env-overridable.
* no-mistakes(document): Document SSH dead-peer detection ownership
* fix: report stale AXI tools during bootstrap (#1701)
* feat(bootstrap): gate stale axi CLIs at the floors firstmate actually uses
Add gh-axi 0.1.29 floor so bare --squash PR merges stop failing quietly on
older builds. Raise tasks-axi to FM_TASKS_AXI_MIN=0.2.2 (multi-id mv) while
keeping feature probes. Keep quota-axi at 0.1.16 after verifying schema 3
and per-model availability already ship there; runway remains optional.
* no-mistakes(document): Clarify AXI compatibility documentation ownership
* fix: prevent false watcher-down alarms in Claude sessions (#1661)
* fix(guard): stop false watcher-down alarm mid-turn under Claude auto-arm
bin/fm-guard.sh derived its watcher-health verdict from fm_watcher_healthy,
which requires a live watcher process holding the home lock. Under the Claude
Stop-hook auto-arm supervision model the watcher is armed at each turn end and
exits on its wake, so it runs only between turns. Every guarded command run
mid-turn therefore found no live watcher and printed the "WATCHER DOWN -
SUPERVISION IS OFF" banner even though supervision was healthy. Because the
episode key was derived from the beacon mtime (which the between-turns watcher
advances every poll), the full banner re-printed on essentially every command,
and the message always blamed a "fresh beacon" that was in fact fresh.
Make the pull guard's health check model-aware via a new
fm_watcher_supervision_verdict in bin/fm-wake-lib.sh:
- Under the auto-arm model a beacon fresh within FM_GUARD_GRACE is healthy even
with no live watcher process; only a beacon stale beyond grace (or absent) is
a genuine lapse and alarms.
- Under every persistent-watcher harness (codex foreground checkpoint,
opencode/pi/grok background arm, tmux, unknown) a live identity-matched
watcher with a fresh beacon is still required, unchanged.
The banner now names the true failing condition, a missing live watcher process
versus a genuinely stale beacon, instead of always blaming the beacon, and the
once-per-episode dedup keys on that condition rather than the beacon mtime so a
genuine lapse announces once and does not re-print each turn.
The turn-end guard keeps the strict fm_watcher_healthy check because it fires at
the turn boundary, where the auto-arm brings a fresh watcher up and it
cooperates with that arm. fm_watcher_healthy itself is unchanged, so the arm
layer's start/attach/replace decisions are unaffected.
Tests in tests/fm-guard-stale-banner.test.sh cover the auto-arm healthy
fresh-beacon-without-a-watcher case, the auto-arm stale-beacon alarm and its
stable episode, the true-reason banner wording, and the reason-keyed episode
surviving a beacon mtime change; existing persistent-model cases are pinned to
that model.
* no-mistakes(review): Pin secondmate supervision model to launched harness
* no-mistakes(document): Align watcher documentation with model-aware supervision health
* test: prevent fixture temporary directory leaks (#1704)
* fix(tests): stop fixture-tempdir helper from self-deleting under command substitution
fm_test_tmproot is almost always called as `TMP_ROOT=$(fm_test_tmproot prefix)`,
which forks a subshell to capture its stdout. The old implementation set its
EXIT cleanup trap inside that call, so the trap fired - and deleted the fixture
root - the instant the subshell exited, before the real caller's own EXIT trap
was ever installed. Every test using the documented call pattern leaked its
fixture root on every run; two suites had already independently discovered and
worked around this with ad-hoc mktemp calls.
Registration now goes through a $$-keyed registry file instead of in-process
state, since $$ resolves to the invoking shell's PID even inside the
subshell. The real cleanup trap is armed once at source time (always the real
caller, never a subshell) for EXIT, INT, and TERM. A best-effort orphan sweep
on next source reaps marked fixture roots old enough to be from a killed prior
run.
Simplifies the two existing ad-hoc workarounds (fm-procevent.test.sh,
wake-helpers.sh) back onto the shared helper now that it works correctly.
* no-mistakes(review): Preserve live fixtures during orphan reaping
* no-mistakes(review): Harden fixture ownership against PID reuse
* no-mistakes(review): Secure cleanup registry against path precreation
* no-mistakes(review): Make fixture registration transactional
* no-mistakes(document): Documentation already matches fixture cleanup behavior
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
* feat(herdr): enable presentation spaces by default (#1708)
* feat(herdr): default presentation spaces on with an explicit opt-out
Herdr's disposable one-task presentation workspace was opt-in through the
presence of local config/herdr-presentation-spaces. It is now on by default,
and a home opts out by writing "off" into that same file.
Values are read with the whole-file whitespace-stripped convention the other
scalar config items already use, plus case folding. An absent file, an empty
file, and "on" all resolve on; only "off" opts out; an unrecognized value warns
and keeps the default rather than failing a spawn over a purely visual setting.
The empty file is exactly the historical opt-in form, so every home that had
already enabled the projection stays enabled with no migration step, and no
previously enabled home can be turned off by the flip.
Because absence now means on at both ends, secondmate inheritance needs no
item-specific convergence: mirroring an absent primary file converges a
secondmate to the same default-on rather than turning its projection off, and
only an explicit primary opt-out propagates the opt-out.
The gate itself moves into fm_backend_herdr_presentation_enabled in the Herdr
adapter so the semantics have one owner that regressions can exercise directly.
* no-mistakes(document): Document Herdr default-on presentation safety
---------
Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
* fix(bin): surface fleet-wide open decisions on every wake drain (#1711)
* fix: surface consolidated open decisions on every wake-drain
A needs-decision or blocked event buried under later, unrelated status
appends was only ever shown via the last-line wake annotation, so a
still-open captain decision could go silently missed even though
status_open_decisions (fm-classify-lib.sh) already folds the whole
status stream correctly and fleet-snapshot/bearings already reuse it.
Wire that same fold into bin/fm-wake-drain.sh: a new fleet-wide
scan_open_decisions wrapper scans every state/<id>.status, and
fm-wake-drain.sh prints a separate, bounded OPEN DECISIONS section on
every drain (including the empty-queue fast path), so session-start
and every wake-handling turn surface it for free without duplicating
the open/resolved fold itself. Heartbeat wakes drain through the same
script, so this covers that surface too.
Also tighten status_open_decisions' file guard to skip an unreadable
status file instead of leaking a bash redirection error, now that a
fleet-wide directory scan can reach files a single targeted read
would not.
* no-mistakes(review): Prevent status symlinks leaking open decisions
* fix: drop unbounded perl subprocess from status symlink guard
The review step's own symlink-safety auto-fix (O_NOFOLLOW read via a
perl subprocess) forked one perl process per status file scanned by
the new fleet-wide open-decisions scan, with no cap - inflating
fm-wake-drain.sh's total external-read cost from 8 (the existing
annotation read_cap) to 18 in the enrichment-caps regression test.
The plain [ -L "$f" ] check already rejects any status file that is
itself a symlink before any read happens, which is exactly what the
new regression test exercises and is the same defense level the
sibling scan_captain_relevant_statuses/last_status_line already rely
on elsewhere in this file (no O_NOFOLLOW). Drop the subprocess-based
nofollow read and keep the cheap builtin guard.
* no-mistakes(document): Document actionable fleet-wide open decision drains
* fix(bin): abort parked runs and reap leaked processes before teardown (#1710)
* fix(bin): abort orphaned no-mistakes runs and reap leaked processes at teardown
Teardown could remove a task's worker while its no-mistakes pipeline run was
still parked at a gate, leaving an orphaned run holding a fleet slot
indefinitely (observed 2026-08-03: runs parked 7h39m and parked at a
post-CI approval gate). It could also leave backgrounded/disowned
descendant processes rooted under the worktree or tasktmp surviving
reparented to init (observed: two `go test` binaries pinning CPU for
hours with no live task meta to attribute them to).
Add two coupled pre-teardown steps, both scoped to this task's exact
branch/head or worktree/tasktmp so they can never touch another task's
run or processes:
- conclude_task_no_mistakes_run aborts a run parked at a gate via
`no-mistakes axi abort`, cd'd into the exact worktree so the daemon
resolves the run itself rather than teardown naming a --run id.
- reap_task_worktree_processes sweeps for processes whose cwd is under
the worktree or tasktmp (via `lsof -a -d cwd`) and TERM/KILLs them.
Both run before any worktree return, branch delete, or backend kill,
and are idempotent on a retried teardown. The branch+head attribution
logic is factored out of bin/fm-crew-state.sh into the new shared
bin/fm-nm-run-lib.sh so both scripts use the same ownership contract.
* no-mistakes(review): Fail closed on incomplete teardown cleanup
* no-mistakes(review): Bind teardown cleanup to verified run and process identities
* no-mistakes(review): Require confirmed aborts and convergent identity-safe process reaping
* no-mistakes(review): Handle process exits during teardown identity checks
* no-mistakes(review): Restore teardown library in hermetic gotmp fixtures
* no-mistakes(document): Document teardown run attribution and timeout
* no-mistakes(lint): Rename shell variable conflicting with done keyword
* no-mistakes: apply CI fixes
* fix: resolve fm-remote-entrypoint.sh SCRIPT_DIR through a PATH symlink (#1709)
The script installs as a symlink under ~/.local/bin. Taking dirname of
the symlink itself (instead of its real target) pointed SCRIPT_DIR at
~/.local/bin, breaking sourcing of the sibling fm-remote-job-lib.sh.
Resolve the real path first, preferring python3's os.path.realpath,
then realpath, falling back to the raw BASH_SOURCE on hosts with
neither.
* fix(pi): gate Calm built-in overrides by activation state (#1724)
* fix(pi): stop Calm claiming a built-in tool name another extension owns
fm-calm.ts claimed bash/read/edit/write/grep/find/ls unconditionally at
extension load, regardless of whether Calm was on. Pi resolves two
extensions registering the same built-in name by first-registered-wins
with no merge and no unregister call, and Calm's project-local
.pi/extensions/ position beats any global or CLI-configured extension,
so a user who never even enabled Calm could have their own bash/read/etc
override silently replaced.
Captain-approved plan implemented:
- Registration is now gated on config/calm already being "on" at load
time. A Calm-off session or reload registers nothing, so a non-Calm
user never contests a name. This stays synchronous during the
factory's own load, not deferred to session_start: /reload (and
ctx.newSession/fork/switchSession) render the restored transcript from
a pre-session_start snapshot of the tool registry, so a deferred claim
would miss that render - confirmed by tests/fm-calm-pi-extension
.test.sh's hidden-block-geometry E2E when trialed.
- The first time Calm turns on in a session that started off
(activateBuiltInsIfNeeded, from the /calm command handler), Calm calls
pi.getAllTools() - safe only once every extension has finished loading,
unlike the load-time path above - to see whether a different extension
already owns a name, and skips claiming only that one, leaving it and
its owning extension fully intact and callable.
- A contested name found this way prints a prominent ctx.ui.notify()
warning naming the tool, plus a console diagnostic.
- reportBuiltInLosses() remains the backstop for the one case neither of
the above can reach: a session that starts or reloads with Calm already
on, where the registry snapshot is taken before Calm gets any chance to
check ownership. A symlink-safe realpath comparison avoids misreporting
Calm's own registration as foreign when its path crosses a symlink
(macOS /tmp, /var).
Confirmed, bounded trade-off: the very first time a session that started
Calm-off turns Calm on, tool-call rows already on screen from before that
toggle do not retroactively collapse, because Pi never lets an extension
re-point an already-rendered row at a definition registered later. Every
session after that first toggle starts with the preference already on and
takes the synchronous load-time path, so the guarantee is intact from
then on. docs/calm.md and the file's own header document this in full.
tests/fm-calm-pi-extension.test.sh gains test_builtin_gate_load_time
(config/calm off registers nothing, on registers all 7 synchronously at
load) and test_calm_activation_collision_and_regression_bound (first
activation claims every uncontested built-in, leaves a foreign bash tool
fully intact and callable, warns and logs the contested name, and locks
in the documented pre-activation bound against real ToolExecutionComponent
rendering). test_rendering_and_session_lifecycle and the live interactive
E2E are updated for the new gate-at-load and first-activation-bound
contract.
* no-mistakes(document): Document Calm tool collision boundaries
* no-mistakes: apply CI fixes
* fix(bin): persist secondmate parent bindings for cleanup (#1727)
* fix(bin): give secondmate homes a durable parent binding record
Finished-worker cleanup on a remote second mate refused forever with
"cannot resolve the primary home ... durable parent binding". The
remote launch hands the child the remote code checkout as its parent
home (fm-spawn.sh's sole writer of FM_PUBLIC_FOLLOWUP_PRIMARY_HOME
receives FM_HOME=$FM_ROOT from fm-remote-secondmate-control.sh's
host-local launch), and that path can never carry the parent's real
records, so the guard refused unconditionally once relay looked active
anywhere on that host.
fm-home-seed.sh and fm-remote-home-provision.sh now write a durable
.fm-secondmate-parent record next to the .fm-secondmate-home identity
marker, naming the home's route to its parent as local (with the real
parent path) or remote (with the parent's SSH alias for diagnostics
only). fm-teardown.sh's cleanup gate reads it: a remote parent is out
of scope for the delegated-promise check (the whole promised-public-
reply subsystem is same-filesystem by construction, so a remote parent
can never hold one), while a token committed directly to the child's
own .env file - never the process environment - still refuses, so an
unrelated export in the remote host's login shell can no longer mask
in. For a local secondmate, the durable parent_home now also backs up
the launch-time env var, closing a silent fail-open where a restart
that dropped the launch prefix made the guard treat a genuinely active
parent relay as off.
Regression coverage drives the real remote route (SSH boundary + Herdr
fixture) and real fm-home-seed.sh seeding rather than hand-crafted
markers.
* no-mistakes(review): Captain: fail closed on unsafe durable parent records
* no-mistakes(review): Captain: enforce durable parent binding commit protocol
* no-mistakes(review): Captain: publish local parent binding before identity
* no-mistakes(review): Captain: refuse conflicting local parent bindings
* no-mistakes(review): Captain: reject non-regular secondmate seed leaves
* no-mistakes(review): Captain: enforce unique durable parent bindings
* no-mistakes(review): Captain: reject route-incompatible durable parent fields
* no-mistakes(document): Document durable secondmate parent bindings
* no-mistakes(lint): Fix secondmate parent parser ShellCheck warnings
* no-mistakes: apply CI fixes
* feat(bin): enforce latest AXI-family tool floors (#1733)
* feat(bin): gate lavish-axi at its session_ended floor in bootstrap
bin/fm-procevent-lavish.sh decides that a human "Send & End" review is
terminal by reading session_ended from the poll response's leading session
block. That field first shipped in lavish-axi 0.1.35, so an older installed
build silently leaves every ended review source armed forever and captures
an empty ended result on each later cycle. The same release is what makes a
plain reopen refuse a session the human deliberately ended.
Add LAVISH_AXI_MIN=0.1.35 to the existing axi-family floor structure in
bin/fm-bootstrap.sh, reusing tool_version_at_least and the same MISSING
diagnostic gh-axi already emits, so an incompatible build is reported as an
upgrade request b…
lytv
pushed a commit
to lytv/mymate
that referenced
this pull request
Sep 8, 2026
…d#1563) * feat(bin): require an explicit ship delivery mode in fm-brief A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions. * feat(bin): require an explicit ship delivery contract at spawn and promotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode. * docs: record the explicit per-task delivery contract AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly. * test: pass ship delivery flags per call site in the Herdr launcher e2e The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper. * test: pass the ship delivery contract in the secondmate suites Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires.
Lcxiv
added a commit
to Lcxiv/firstmate
that referenced
this pull request
Sep 15, 2026
…ck guards (#18) * fix: preserve Calm boat continuity across working periods (#1356) * fix(calm): resume working boat from frozen column across runs Keep one extension-owned boat animation for the Pi session so settling freezes column and direction, the next working period resumes there without hidden-time jumps, and only a fresh session resets to the left edge. * no-mistakes(review): Freeze Calm boat from last rendered state * no-mistakes(document): Document Calm boat continuity contract * fix: restore evidence-based dispatch eligibility (#1358) * fix(dispatch): judge candidate provider relations instead of rejecting them Firstmate deterministically dropped supported Pi candidates in the openai-codex family. bin/fm-auth-preflight.sh resolved a harness=pi tuple's credential surface by constructing the source id `pi:<model-prefix>`, so `pi + openai-codex/gpt-5.6-terra` looked for a `pi:openai-codex` source. That source does not exist, because Pi's Codex family authenticates through the Codex store quota-axi already lists as `auth-json`/`cli-rpc`. The tuple returned `eligible=no reason=surface-unresolved` while the Pi catalog listed the model and the Codex provider reported fresh, usable credentials with 64 effective percent remaining on its all-model scope. The prefix construction was only ever valid where Pi holds its own credential (`pi:xai`, `pi:kimi-coding`), which is why every previously configured Pi tuple resolved and the defect stayed hidden until a Codex-family Pi model was configured. Retire dispatch eligibility from deterministic shell. The dispatching first mate now establishes model support and provider family from each harness's authoritative catalog, applies quota at the granularity the vendor supplies, and shows that reasoning. Provider-level and all-model evidence bounds every model established in that family; a named-model window bounds only its own model. Missing model-level quota, a missing auth source, unmeasurable headroom, and unmodeled authentication are disclosed uncertainty. Only concrete contradictory evidence blocks a candidate. Replace the preflight with bin/fm-vendor-auth-probe.sh, which keeps the captain's approved bounded probe envelope without any routing knowledge: it takes no harness, model, or provider, reads no quota, renders no verdict, and holds only a fixed-argv safety allowlist. Its behavior suite proves the absent identity surface, the untouched quota, the uniform exit status, the fixed argv with stdin closed, and a real bound even when the configured bound is zero. Also fixed along the way: a zero FM_*_TIMEOUT silently removed the hard bound, the pinned Grok version had drifted to 0.2.117, and --changed selection refused outright on any deleted bin/ script. AGENTS.md section 4 and quota-array-dispatch own the corrected policy, harness-adapters gets the catalog-responsibility correction, and docs/verification/dispatch-auth.md records the 2026-07-30 evidence on Pi 0.82.0, quota-axi 0.1.16, and grok 0.2.117. * no-mistakes(review): Reject all-zero vendor probe timeouts * docs: define captain instruction precedence (#1362) * docs: add captain-authorized inherent red-check merge exception Keep the default red-PR ban and own one always-loaded exception in the merge-authority section: captain-explicit PR or bounded batch plus exact check, only when the failure is inherent to the selected delivery path. Yolo cannot activate it; final head and the full current check suite must be verified; other substantive failures remain non-waivable. * docs: replace narrow red-check exception with captain precedence Supersede the inherent failing-check merge exception with one always-loaded Firstmate-local rule: a current explicit concrete captain instruction overrides a conflicting Firstmate-written standing rule only within exact scope, never above platform/system/developer instructions. Keep the ordinary red-PR default and yolo boundary; point section 7 at the section 1 owner. * docs: define validation supersession sequence (#1407) * fix: give validation-time captain overrides a supersession sequence The Validate section let a captain instruction that completely invalidates the work being validated keep the same task and worker, but never said how: the adjacent rule flatly bans hand-editing, committing, aborting, or restarting during an active run with no carve-out, so a worker facing full invalidation had no sanctioned path forward. Add the missing sequence: cancel through no-mistakes axi's abort command, confirm the run has stopped through axi status, recover branch ownership through axi sync's guarded recovery, only then replace the obsolete work, and validate once against the final head. The existing ban on hand-editing an active run now cross-references this sequence instead of contradicting it. * no-mistakes(review): Make validation custody recovery conditional * no-mistakes(document): Clarify validation supersession abort exception * fix: keep obsolete pipeline commits out of the superseded deliverable The review-applied fix made custody recovery conditional on branch_sync.next_action.code, but left an open gap: recovering custody settles who owns the branch, not what content ships. As written, a worker could recover an obsolete run's branch and build the replacement on top of its now-irrelevant commits instead of from the correct pre-invalidation base, carrying obsolete content into the final deliverable. Make that explicit: custody recovery settles ownership, not content, so the worker replaces obsolete work from the correct base and keeps the obsolete run's commits out of what gets validated and shipped. * no-mistakes(test): Restore minimal pre-invalidation replacement instruction * fix: dedupe redundant "replace the obsolete work" restatement Line 309 already says the worker replaces the obsolete work from the correct pre-invalidation base, excluding the obsolete commits. The closing sentence restated "replace the obsolete work" again before gating the final validation run, layering the same fact twice instead of stating it once. Trim the closing sentence to just the ownership gate and the single-run-against-final-head requirement it uniquely adds. * fix: bind backend overrides to exact-task authority (#1413) * fix: bind explicit --backend to exact-task authority A Herdr-backed second mate carried a prior one-task --backend tmux exception forward by analogy, so its child landed in tmux and never appeared under the second mate in Herdr. Runtime detection was correct; the authority surface was not. docs/configuration.md now owns that an explicit --backend is authorized only for that exact task. AGENTS.md and fm-spawn help point there. * no-mistakes(document): Consolidate backend selection authorization documentation * fix(herdr): prevent focus flashes during projected workspace cleanup (#1229) * fix: remove projected workspaces through Herdr's focus-preserving pane-death path Herdr 0.7.5's explicit close of a workspace-emptying last pane moves the attached client's focus to a neighbor workspace, flashing the captain's whole window and routing in-flight keystrokes to the wrong pane until Firstmate's exact-tab restore masks it 56-197 ms later. Teardown and cleanup now plan a workspace-emptying close as a focus-safe removal: verify the close empties the workspace, reposition the doomed workspace behind the focused one through the verified workspace.move transport when it sits before a non-last focused workspace, prove the pane holds one lone idle shell, and end that shell so Herdr removes the emptied workspace through its focus-preserving pane-death path. Any ambiguity or failure falls back to the plain close behind the existing restore backstop, and fm_backend_herdr_kill applies the same plan for non-projected removals. Two conditions proven on real hardware are encoded in the adapter: BSD ps reports a login shell's comm as "-zsh", and an idle shell transiently hosts a prompt helper right after a workspace.move relayout, absorbed by a bounded strict-sample settle window in the idle-shell proof, now the single owner shared with session-start cleanup. An isolated-lab regression reproduces the raw steal on 0.7.5 and proves the plan removes a doomed workspace with zero wrong-focus samples and no corrective focus; unit fixtures cover the position, edge, ambiguity, move and kill failure, escalation, and transient-helper cases. Upstream fixes (#1877 explicit close, #1912 pane death) are merged but unreleased; once released the plan degrades to a harmless reorder-then-remove. * no-mistakes(review): Confirm pane death from structured not-found responses * no-mistakes(review): Serialize Herdr kills and sample focus continuously * no-mistakes(review): Synchronize Herdr focus evidence output * no-mistakes(review): Refuse unlocked Herdr pane closes * no-mistakes(document): Correct Herdr focus-safety documentation * no-mistakes: apply CI fixes * fix: never erase a Herdr task's records while its pane survives a refused close A transient presentation-lock contention could produce a completed teardown while the exact Herdr pane stayed alive as an unowned restored shell: the kill refused the unlocked close (correctly), returned success, the warning was suppressed, and cleanup erased the task's status, turn-end, and metadata records after the isolated copy had already been returned. Teardown now acquires the named-session presentation lock before anything destructive: a contended lock refuses up front while the isolated copy, the task branch, every durable record, and the endpoint are all intact for a plain rerun, and the projected and flat close paths both run under that one held lock instead of acquiring their own. Durable records are erased only once the exact pane is confirmed gone through its structured presence; a refused, skipped, or failed close retains every record with a visible, retryable error, and after a skipped close (unresolvable lock path) only a structured pane_not_found counts as gone - unknown never does. The teardown regression drives a live contending lock holder end to end: the refusal touches nothing (no worktree return, no branch drop, no close attempt), and the retry after release returns the copy, closes the pane under the lock, and removes the records. The unconfirmed projected close now refuses with records retained, and the structured-presence gate has a strict/default unit matrix. * no-mistakes(review): Require structured pane-not-found before Herdr record removal * no-mistakes(document): Correct Herdr record-retention verification date * fix: refuse ambiguity, revalidate SIGKILL ownership, and roll back failed removals Three accepted-contract corrections from the post-CI personal review of the Herdr keep-spaces focus-flash mitigation. Ambiguous endpoint identity no longer counts as a confirmed-gone pane: a missing or malformed target refuses record removal in the structured presence gate, and teardown treats missing confirmation machinery as a refusal instead of skipping the gate, so only an exact structured pane_not_found ever erases durable task records. The pane-death SIGKILL escalation re-reads the exact pane's process information and refuses to signal unless the same shell pid still passes the strict bare-idle ownership proof, so a pid that exited and was reused by an unrelated process is never signaled; the refused escalation falls back to the plain close with the unrelated process untouched. A reposition whose removal is not confirmed no longer outlives the attempt: the emptying-close plan records the verified pre-move order and original index whenever it invokes the mover, and both close owners restore the exact original workspace order through a second verified move, under the same held session lock, before reporting the close as failed. Each defect was reproduced first: the unit matrix documented malformed identity as gone, the PID-reuse regression showed SIGKILL reaching a disowned pid, and the rollback regression showed a single unrestored move. Teardown-level regressions cover unparseable presence retention alongside the strict identity matrix. * no-mistakes(review): Require confirmed Herdr removal and resolvable teardown locks * no-mistakes(review): Enforce structured Herdr closes and teardown preflight * no-mistakes(review): Preflight explicit Herdr close confirmation helper * no-mistakes(document): Document Herdr rollback failure semantics * no-mistakes(review): Captain, harden recursive Herdr teardown safety * no-mistakes(document): Document recursive Herdr teardown evidence * fix: retain nested secondmate home when a recursive child cleanup fails Captain-decided Option A correction for nm-askuser-flash-r6, found during complete-diff rereview of the merged head. cleanup_firstmate_home_children's recursive secondmate branch called itself for a nested child's home without checking the result, then unconditionally removed that home right after. remove_firstmate_home ends in an unconditional recursive delete with no check for leftover records, so a nested secondmate whose own Herdr grandchild failed its confirmed-gone check would have its entire home - retained grandchild records included - erased by the very next line. Guard the recursive call the same way every other fallible call in this function already is: || return 1, skipping remove_firstmate_home and leaving the nested home and its records for a safe rerun. Empirically, fm-teardown.sh's set -eu already halted the script on the prior unguarded call before reaching removal (verified by hand with the guard reverted, under both this session's bash and stock macOS bash 3.2) - the reachable behavior was already correct. The explicit guard is still applied exactly as decided: it matches every sibling call site in the function, and it stops the correctness of this path depending on errexit's well-known fragility under refactors (a wrapping if/&&, or a future subshell) rather than on an explicit check. Adds a teardown-level regression building on the existing direct-child Herdr fixtures: a top-level secondmate contains a nested secondmate, whose own Herdr child's close goes unconfirmed. Proves through the public fm-teardown.sh interface that the nested home, the nested secondmate's own record, and the grandchild's metadata and status all survive, and that the top-level secondmate's record survives too. * no-mistakes(document): Document nested Herdr teardown retention * fix: prioritize completion runway in quota-aware dispatch (#1431) * fix(dispatch): prioritize quota completion runway * no-mistakes(document): Document completion-aware quota runway selection * fix(bin): preserve full task contract in no-mistakes intent (#1447) * Preserve task contract in no-mistakes intent * no-mistakes(review): Preserve complete current task contract in no-mistakes intent * fix(bin): parse punctuated secondmate registry entries safely (#1452) * fix: centralize secondmate registry parsing * no-mistakes(review): Centralize secondmate registry binding validation * no-mistakes(review): Harden registry EOF and symlink validation * no-mistakes(review): Reject unreadable registries before parsing * no-mistakes(document): Document punctuation-safe secondmate registry validation * no-mistakes: apply CI fixes * feat(bin): add durable process-event supervision (#1483) * feat(procevent): supervise long-polling sources into durable events Firstmate had no way to wait on a blocking external process without holding a conversational turn. Add a domain-neutral process-to-event runner plus a thin adapter around the currently published `lavish-axi poll` interface: canonical physical source identity, one machine-wide owner per source, direct argv execution, and durable 0600 result capture before any event referencing it is published on the existing wake queue. No second notifier, no polling control plane, and no retry machinery. A captured result with no durable handled acknowledgement stays eligible for bounded re-announcement across any number of drains and restarts. Draining a wake before acting on it and then starting a replacement session resurfaces the same exact source and sequence, and never puts result payload text in an event line. `fm-procevent.sh handled <source-id> <sequence>` is the only thing that stops re-announcement: generation-keyed, private, path-safe, durable, and atomically idempotent, so a paired external effect gated on its first-time versus repeat report is never authorized twice. An acknowledgement is refused unless matching captured result and adapter records already exist, so a premature or mistyped call cannot suppress a future result. The source side is unchanged and still lossy: the published poll clears feedback destructively before returning it, so a result lost in that window is unrecoverable. This is never at-least-once, no-loss, or lossless, and the handled acknowledgement is not a generic exactly-once effect either - a crash between an external effect and its acknowledgement can still repeat that effect on replay. Integrate registered sources with watcher supervision, the guards, and recoverable secondmate teardown across nested homes, and cover source identity, lifecycle races, supervision, restart handling, and cleanup safety with regressions. * no-mistakes(review): Prevent Lavish prompt text from spoofing missing sessions * no-mistakes(review): Serialize publication and secure handled acknowledgements * no-mistakes(document): Document hardened process-event acknowledgement guarantees * fix(procevent): never reclaim a source whose owned group still runs A runner is its own process group leader and starts the blocking source in that group, but the claim records only the leader PID and its identity. If the leader died while the source child kept running, the missing PID was classified stale: reconciliation released the claim and started a second runner while the old blocking source was still consuming the same canonical source. For the Lavish adapter that means two destructive long polls racing on one review session, so it is not harmless process litter. It also contradicted the documented promise that ownership is never released until the whole group is gone. Ownership state now distinguishes a generation that is really gone from one whose leader crashed with its group still alive. Reconcile stops that surviving group and releases its exact generation before starting any replacement, and keeps the claim for a later cycle when it cannot prove the group stopped or another home owns it. Acquisition and `start` treat the same state as held rather than reclaimable. Signalling that group is safe precisely because only an absent leader reaches this state. A reused PID leaves the leader alive, so the identity comparison still classifies it stale or uncertain and no group signal follows, which keeps the existing PID-reuse refusal intact. Add a public-interface regression for the exact crash cut - SIGKILL only the leader, prove the child group survives, reconcile, and prove the old group is gone with no second source running - plus its counterexample that a generation with no leader and no surviving group is still reclaimed. Update the runner help, operating documentation, skill, and verification record where they described reclaim in terms of the leader alone. * no-mistakes(review): Enforce runner group ownership and detect poller overlap * no-mistakes(review): Isolate runner groups from unrelated caller processes * no-mistakes(document): Document isolated process-event runner launch * no-mistakes(lint): Suppress Perl literal ShellCheck false positive * fix(bin): retire terminal process events and surface queued wakes (#1500) * fix(bin): deliver process-event results and retire ended sources Two defects reproduced during a real Lavish adapter session. One human `Send & End` produced four captured results: the real feedback, then recurring empty ended sessions. The generic runner had no way to learn a source was finished, so every reconcile restarted a poll that returned immediately. The runner now asks the source's own adapter - `fm-procevent-<adapter>.sh terminal <result-file>` - and on exit 0 alone re-proves ownership, drops the registration, and releases its own claim under one source boundary. Terminal knowledge stays adapter-owned: for Lavish that is an ended session, a missing session, and the final feedback delivery the published poll marks with `session_ended`. An adapter with no terminal command keeps its source armed exactly as before. Capture before publication, captured-result durability, queued wake durability, bounded re-announcement, handled deduplication, one-owner ownership, and explicit idempotent retirement are all unchanged. A captured result queued its `check` wake durably, but a healthy watcher with a fresh beacon never delivered it; the result surfaced only after a manual drain. Publication happens outside the watcher (in the runner) or unconditionally (in reconcile), so the watcher had no newly actionable signal to report and never reached its rewake path. It now reports a queued-but-unsurfaced process-event record through the same actionable exit every other wake uses, deduplicated by the same `.seen-*` marker discipline the signal scan uses, so the record is always durable before it is suppressed. The durable queue remains the authority and no second notifier, poller, timer, queue, or adapter-specific wake path is added. Regressions cover both, driven end to end: an armed Lavish source against a stand-in for the published poll polls once, captures once, publishes one distinct event, and retires itself; two fixture adapters prove the terminal decision follows the adapter alone; and a real capture plus a real watcher prove one proactive wake before any drain, with no duplicate wake while the record stays queued or after it is acknowledged. * no-mistakes(review): Harden process-event retirement and proactive delivery * no-mistakes(review): Route process-event delivery through shared wake owner * no-mistakes(document): Clarify process-event delivery and retirement documentation * no-mistakes(lint): Fix ShellCheck control-flow warnings * no-mistakes(lint): Fix wake output status lint warning * perf: shard portable serial tests across CI runners (#1544) * perf(ci): shard the portable serial behavior lane across runners The Behavior portable serial job ran all 69 scripts of the serial remainder on one runner. The measured serial sum on run 30725985757 was 1143762 ms (19m04s) against a 20-minute timeout, so the job intermittently reached the cap and was cancelled with every step passing. Setup is only about 7s, so the cost is entirely test wall time. Split the lane into four separate-runner shards. Each shard is still strictly serial, and separate runners mean no two of these stateful scripts ever share a machine, so the split needs no concurrency isolation proof. Assignment is longest-processing-time bin packing over measured per-script duration hints, balancing every shard to 285941 ms (~4m46s) of expected work, and the timeout tightens from 20 to 15 minutes. bin/fm-test-run.sh owns the shard count and refuses a lane whose "ofN" disagrees with it, while ci.yml derives the same count from strategy.job-total rather than a literal, so changing it in either file alone fails the lane loudly instead of leaving part of the required suite unrun. --check-coverage additionally proves the shards are non-empty, disjoint, and exactly equal to the serial lane. No test is weakened, skipped, or removed. Also replace the wall-clock sleeps in the --jobs scheduler test fixture with an explicit signal handshake between the fixtures. The old 0.5s-versus-0.05s race failed on a loaded machine; the handshake passes under sustained CPU saturation. * no-mistakes(review): Correct portable serial shard balance evidence * no-mistakes(document): Document portable serial shard evidence accurately * fix(bin): correct session lock and attached watcher supervision (#1545) * fix(bin): identify harness sessions by path and report delivered wakes Two supervision faults, both reported by a contributor and both open on the default branch. Fault 1: the Stop auto-arm never claims the home. fm_harness_ancestry_pid() matched only the basename of `ps -o comm=`, and Claude Code's native installer names the per-session executable by its version (.../share/claude/versions/ 2.1.220), so that basename identifies nothing. Three real failure shapes follow: a version-named session is missed entirely and the hook exits 0 with the epoch never written (unconditional on Linux, where procps reports the kernel exec name and ignores argv[0]); a claude-named daemon that directly parents sessions wins the outermost-contiguous-claude rule ahead of the session itself; and a session that is both version-named and daemon-parented has its live lock reclaimed as stale and rewritten to the shared daemon pid, corrupting the home's ownership record. Harness identity now also reads whole components of the executable path and of argv[0], which is what both platforms still carry. Matching whole components only keeps that widening safe: bin/fm-claude-stop-autoarm.sh and ~/.claude/hooks scripts have no "claude" component. Ownership is then decided against the session's whole contiguous harness ancestry rather than one chosen pid, which is the honest form of the question the library already documents ("does the current process descend from that same harness?"). That subsumes the outermost-pid rule for Claude's nested bg-spare worker chain instead of reverting it, and lets a daemon-parented session recognize its own lock. Lock acquisition still writes the outermost pid of the run, the only pid that lives as long as the session. Fault 2: an attached arm reports a delivered cycle as FAILED. The watcher prints its one reason line to its own stdout, so only the arm that forked it can read that line; an arm that attached observes nothing but a released lock and called a completely successful cycle "cycle ended without an actionable reason". No supervision event was lost - the durable queue held it - but every harness protocol reads that line as "supervision is down" and directs a manual re-arm. The arm now resolves an unobservable close against the durable wake queue, which records every wake before the watcher prints it and whose sequence counter never rewinds, not even across a drain. A cycle the queue proves delivered a wake reports that wake and exits 0; a cycle whose records a handling turn already drained reports the delivery without inventing a reason line; only a cycle that delivered nothing is still the typed nonzero failure. Fixing it in the arm covers codex, opencode, pi, grok and kimi, not just the Claude Stop path. Regressions: tests/fm-session-lock-ancestry.test.sh pins both platforms' ps semantics behind a deterministic process table and runs the real Stop auto-arm in version-named, daemon-parented, and combined real process trees, each orphaned so the walk cannot escape the fixture. tests/fm-watch-arm.test.sh drives a real watcher and a real attached arm through a real wake. Every fault case fails on the previous code. * no-mistakes(review): Bind watcher delivery records to process identity * no-mistakes(review): Return validated watcher identity atomically * no-mistakes(review): Track watcher successors by PID and identity * no-mistakes(document): Consolidate watcher arm-cycle documentation ownership * fix(bin): harden Claude supervision auto-arm recovery (#1495) * fix(supervision): harden Claude auto-arm failure handling * no-mistakes(review): Guarantee automatic retry after Claude auto-arm failures * no-mistakes(review): Gate attended fail-open on verified supervision failure * no-mistakes(document): Document Claude auto-arm retry and guard scope * no-mistakes: apply CI fixes * fix(supervision): make Claude fail-open progression monotonic * no-mistakes(review): Preserve auto-arm failure episodes until verified watcher recovery * no-mistakes(review): Linearize auto-arm failure progression across existing locks * no-mistakes(review): Linearize positive recovery across shared failure episode lock * no-mistakes(review): Scope Claude recovery contention to Claude guard mode * no-mistakes(document): Align supervision auto-arm documentation * no-mistakes(review): Preserve actionable wakes despite healthy successors * no-mistakes(document): Refresh supervision auto-arm documentation * feat(bin): require an explicit per-task delivery contract (#1563) * feat(bin): require an explicit ship delivery mode in fm-brief A ship brief's definition of done was shaped by a silent per-project registry lookup, so an adjusted brief and the task's recorded delivery could disagree and no one had to decide anything per task. fm-brief now requires --mode on ship scaffolds, validates it against the closed set, refuses the conditional no-mistakes-prod-only registry policy as a task mode, and records the choice as a fixed machine-readable "Delivery contract: mode=<mode>" line that fm-spawn can check. --mode is refused on scout and secondmate scaffolds, and --yolo is refused outright because the worker never owns approval decisions. * feat(bin): require an explicit ship delivery contract at spawn and promotion fm-spawn resolved every ship and scout task's mode and yolo from the project registry, so the delivery posture was never a per-task decision and could contradict the brief the worker was about to follow. fm-spawn now requires --mode and --yolo on ship spawns, validates both against their closed sets, and reads the brief's recorded delivery contract line and refuses a mismatch before any endpoint exists; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that each pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records no mode or yolo at all, which teardown and the snapshot already tolerate. When the explicit mode carries less rigor than the project's standing posture, a deviation notice is printed and the spawn continues, so the registry stays advisory rather than an enforced default. fm-promote requires the same two flags, because a scout carries no posture to inherit, and writes them into the task record with the kind flip. fm-project-mode keeps its one registry parser for the mechanical consumers that have no task in hand, accepts the conditional no-mistakes-prod-only annotation and maps it to its most rigorous leg for them, and grows --raw so the deviation notice can tell a conditional policy apart from a flat mode. * docs: record the explicit per-task delivery contract AGENTS.md section 7 now owns how each ship task's mode and yolo are resolved at intake, including the surface classification for a no-mistakes-prod-only project and the unregistered-project fallback, and the project-management skill defines that conditional policy as a registration-time posture with its defaults and initialization consequences. The registry blurb, script table, and architecture section follow: the registry records the captain's standing posture, and task delivery is decided per task and passed explicitly. * test: pass ship delivery flags per call site in the Herdr launcher e2e The shared spawn helper also launches a secondmate, which refuses the flags, so the contract belongs at each ship call site rather than inside the helper. * test: pass the ship delivery contract in the secondmate suites Both suites scaffold or spawn an ordinary ship task as the control case for a secondmate assertion, so each needs the explicit contract the ship path now requires. * feat(bin): support remote secondmate homes (#1576) * Add generic remote secondmate transport * Add routed remote secondmate replies * Add remote outbox backlog handoff * Integrate remote secondmate lifecycle * no-mistakes(review): Fix remote snapshot and handoff races * no-mistakes(review): Serialize remote home provisioning transactions * no-mistakes(review): Harden remote lifecycle transaction boundaries * no-mistakes(review): Serialize remote lifecycle mutations and fail closed * no-mistakes(review): Close remote lifecycle and file race windows * no-mistakes(review): Serialize remote reply retirement and inheritance * no-mistakes(review): Harden remote transfer integrity and recovery * no-mistakes(review): Serialize remote respawn with registry retirement * no-mistakes(document): Document remote bootstrap convergence accurately * no-mistakes(document): Clarify skipped remote secondmate mutations * no-mistakes(lint): Resolve remote script ShellCheck warnings * no-mistakes: apply CI fixes * feat: add per-task trace context propagation (#995) * feat(spawn): propagate a native W3C traceparent to spawned agents Add a default-off capability that resolves one W3C traceparent for a task, injects it into the agent's pane shell as the TRACEPARENT environment variable immediately before launch, and records the identical value as traceparent= in state/<id>.meta, so an external observer that explicitly reads that env value or meta field can correlate a worker, a Secondmate, and their nested children into one trace with no collector, storage, UI, or vendor coupling. TRACEPARENT as an environment variable is a firstmate convention carrying a W3C-formatted value: W3C Trace Context standardizes the header, not an env var, and OpenTelemetry SDKs do not read it automatically, so a downstream must consume it deliberately; this feature parents no SDK span by itself. Identity is per task, not per spawn: the carrier is minted with random ids on the first spawn, adopted as a child (fresh span, same trace) for a nested spawn whose parent already holds one, and reused verbatim from the meta on relaunch, so a task keeps one stable logical identity across restarts. A malformed or all-zero inherited value is treated as absent and roots a fresh trace. A new root is sampled (01) - a sampling decision a downstream parent-based sampler honors, not a guarantee that any collector stores a span, and firstmate emits no spans; a child preserves the inherited flag. Trust boundary: a firstmate-minted root is random and reads no prompt, path, task prose, credential, or arbitrary environment key. An inherited TRACEPARENT is opaque caller-controlled data - up to 24 bytes of id passed through after syntax validation - so whoever set it controls those bytes, a bounded fixed-width channel rather than a general content or secret channel. The feature adds no OTEL_* variable, no tracestate, and no arbitrary environment injection; it runs no configurable or arbitrary command, only the fixed local od and tr (resolved from PATH) to read a few bytes of entropy - a small local pipeline with no network or watchdog and no hard latency guarantee. Any entropy or validation failure that returns omits the carrier without aborting the spawn. A default-off spawn leaves the generated meta and launch environment unchanged. Enablement is default-off (config/trace-context, or FM_TRACE_CONTEXT where a non-empty value overrides and unset or empty defers to the file) and is propagated into secondmate homes, taking effect at each agent's next launch: a Secondmate launched or relaunched after enablement carries the primary trace into its nested workers, while an already-running Secondmate roots new traces for its own workers until relaunched. Injection reuses the existing GOTMPDIR channel, so all spawn backends and harnesses and the ship, scout, and secondmate paths are covered. Covered by a pure-library suite and a spawn-path integration test (fake tmux plus a real worktree, hermetic against ambient FM_TRACE_CONTEXT) proving the recorded and injected carriers are identical and sent before launch, that default-off writes and injects neither, that a relaunch reuses the recorded carrier, and that an explicit FM_TRACE_CONTEXT overrides the file both ways; plus a source-owner inheritance test proving trace-context propagates and absence-mirrors through propagate_inheritable_config. Documentation follows the repository documentation-audiences contract: docs/trace-context.md is maintainer-architecture rationale, the configuration schema lives in docs/configuration.md, and the repeatable test evidence is separated into docs/verification/trace-context.md (maintainer-verification), registered in docs/documentation-audiences.json. * fix(spawn): propagate the effective trace-context decision to secondmates FM_TRACE_CONTEXT overrode trace context only in the process that read it. A newly launched secondmate decided enablement from the inherited config/trace-context file alone, so the override did not cross the primary-to-secondmate boundary: FM_TRACE_CONTEXT=off with the file present left the secondmate's nested workers traced (a broken kill switch), and FM_TRACE_CONTEXT=on with the file absent left them untraced despite the inherited carrier. Deliver the primary's effective decision to a newly launched secondmate as a normalized on/off FM_TRACE_CONTEXT in the launch prefix, so a FM_TRACE_CONTEXT override governs the nested primary -> secondmate -> worker chain both ways, not just the copied file. The value is bounded to the literal on/off and does not broaden environment injection; the already-running secondmate boundary is unchanged. Add a genuine two-level spawn regression that drives fm-spawn twice with the exact environment the primary injects into the secondmate and proves both divergent directions end to end. Correct the documentation that implied secondmate coverage on every backend, since orca and cmux reject secondmate spawns, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Clarify Secondmate trace-context launch snapshots * no-mistakes(document): Correct trace-context documentation ownership and relaunch semantics * fix(spawn): resolve the trace-context decision once for carrier and snapshot The effective trace-context decision was read twice per spawn: once inside fm_trace_context_resolve for the recorded carrier, and again for the secondmate FM_TRACE_CONTEXT launch snapshot. A config-file change between the two reads could pair a carrier with the opposite enable state - an injected carrier with an off snapshot, or no carrier with an on snapshot. Freeze the effective on/off decision once, drive the carrier resolution under that frozen FM_TRACE_CONTEXT so it cannot independently re-read the file, and reuse the same frozen decision for the secondmate launch snapshot. Add a spawn-path regression that drives the file-decided path and proves the recorded carrier and the delivered snapshot always agree, and refresh the verification evidence for the new assertion count. * no-mistakes(review): Preserve legacy Secondmate trace boundary * no-mistakes(document): Correct trace-context verification comparison base * no-mistakes(review): Captain, prevent failed trace delivery metadata claims * no-mistakes(review): Captain, align trace-context tests and verification evidence * no-mistakes(document): Correct trace-context verification evidence * no-mistakes(lint): Suppress intentional ShellCheck literal-dollar warnings * no-mistakes(review): Captain: freeze trace context at session start * no-mistakes(test): Captain: stabilize scheduler test and document Kimi trace coverage * no-mistakes(document): Document trace-context safety boundaries * fix(trace): fail off on stale session snapshots Publish each home session decision atomically through a same-directory temporary file and bind it to the current session lock. A replacement failure can no longer leave an earlier on decision active in a later session; missing, stale, malformed, or unpublishable state defaults safely to off. Add regressions for read-only replacement and failed publication, update spawn and session-start fixtures for the lock-bound format, and refresh the architecture and verification records. * no-mistakes(review): Fix trace spawn failure independence and duplicate safety * no-mistakes(document): Refresh trace-context documentation and verification * no-mistakes(review): Clear partial backend input after failed trace submission * no-mistakes(review): Stop unsafe trace delivery before launch append * no-mistakes(document): Document unsafe trace delivery handling * fix(trace): bound each trace to one routed task, never the routing agent A persistent Secondmate holds its launch-time TRACEPARENT in the process environment for its whole life, and routed requests never replace it, so resolving new-task carriers from the ambient environment chained every routed task into one ever-growing trace per Secondmate with distinct parent ids. Resolve now reuses the task's recorded carrier or mints a fresh sampled root, never reading ambient TRACEPARENT, so each routed task is its own trace boundary while relaunch, recovery, and scout-to-ship promotion keep one stable per-task identity. The spawn regression models the reviewed scenario exactly: two unrelated tasks spawned sequentially through one persistent Secondmate environment record and inject distinct trace ids, adopt nothing from the Secondmate's carrier, and a relaunch of the first task reuses its original carrier verbatim. * docs(trace): define the per-task trace boundary The design contract is one task per trace: a persistent Secondmate is routing infrastructure with its own agent identity, never a shared trace root for the unrelated tasks routed through it. Root/recovery semantics replace the removed child-inheritance path, the sampling and safety sections drop inherited-carrier language because ambient TRACEPARENT is never read, and the verification page records the refreshed suite inventories including the two-task Secondmate boundary regression. * test(trace): adopt the explicit per-task delivery contract in spawn fixtures Rebasing onto current main brings the explicit per-task delivery contract: ship spawns now require --mode and --yolo instead of resolving them from the project registry. The trace spawn fixtures pass the same explicit contract canonical spawn tests use, preserving the per-task trace boundary coverage unchanged, and the verification page records the refreshed comparison base. * fix(bin): harden tmux agent liveness across harnesses (#1577) * fix(bin): classify tmux agent liveness independent of process titles `fm_backend_tmux_agent_state` attributed a pane solely from `#{pane_current_command}`, which is a process TITLE a harness can rewrite, not a structural fact. Claude Code 2.1.220 reports its version string there, so a live Claude endpoint classified `ambiguous`: the session-start secondmate liveness sweep could no longer see it, and any consumer that gates on a positive classification refuses outright. Read a second, independent name source: the kernel `comm` of every process in the pane tty's foreground process group. Either source naming a verified harness yields `alive`, because a false `dead` is the one verdict that can start a duplicate agent on a live worktree. Scoping to the foreground process group rather than the pane's descendants keeps a harness-named background process from faking an agent, and covers multi-process launchers (the Pi Launcher path) without a special case. Verified on 2026-08-03 against all seven adapters running for real on tmux 3.6a / macOS 26.5.2 arm64: claude 2.1.220, codex-cli 0.146.0, opencode 1.18.11, pi 0.82.0, pi-signed 0.82.0, grok 0.2.118, kimi 0.31.1 all classify `alive`, each attributed by a source independent of its title. Two tests, because they fail for different reasons: - tests/fm-tmux-agent-liveness.test.sh pins the logic with real processes and no harness, so it runs everywhere CI runs tmux. It drives the two name sources apart on purpose and asserts the divergence, so no case can go quietly vacuous. - tests/fm-harness-liveness-drift-live-e2e.test.sh relaunches every installed harness and fails naming the harness and version when one stops being attributed by a title-independent source. AGENTS.md section 4 carries the resulting standing rule, and firstmate-coding-guidelines owns how to satisfy it. * no-mistakes: apply CI fixes * docs: move the harness-dependent-check policy out of AGENTS.md The standing rule was stated in AGENTS.md section 4 with the mechanics in firstmate-coding-guidelines, which split one contract across two owners and charged every session for a rule that only fires when firstmate's own harness-dependent code is being changed. firstmate-coding-guidelines is now the single owner of both the rule and how to satisfy it: real-harness proof required, that proof authorized to spend tokens, structural signals preferred over vendor-rendered surfaces, and a guard that fails loudly naming the harness and version where a surface signal is unavoidable. No inline stub is left behind, because AGENTS.md already carries the load trigger for that skill in sections 7 and 13, so it is read before any change to firstmate's shared tracked material. Also records the cross-platform lesson the pipeline caught in the portable regression, and corrects that file's header: the divergence assertion lives on the version-string case, which diverges on both supported platforms, rather than on every case. * no-mistakes(review): Harden tmux liveness identity and drift validation * no-mistakes(document): Clarify cross-platform tmux liveness documentation * feat(bin): propagate trace context to remote secondmates (#1609) * feat(bin): trace remote secondmate routes and unify the inherit allowlist Per-task W3C trace context (#995) resolved and injected its carrier only at the local spawn path. A remote secondmate is routed through spawn_remote_secondmate, which returns long before that site and wrote its own metadata block, so a remote secondmate stayed silently untraced even with the capability enabled. The parent home still owns that task's identity, because it holds the metadata an observer reads. It now resolves the carrier against the task's own meta under its own frozen decision - reused verbatim on relaunch, freshly rooted otherwise, never adopting the parent process's ambient TRACEPARENT - and hands it to the configured host through a new fm-spawn --traceparent argument, accepted only for a secondmate launch and only as a strict W3C value. The remote host exports it at the same unconditional pre-launch site and reports back the carrier its endpoint actually holds, which the parent records, so an already-alive endpoint reports the identity its agent really received rather than one the parent merely intended. Disabled remains byte-identical and off. The remote inherit path also carried its own hardcoded copy of the inheritable config set, already drifted from FM_INHERITABLE_CONFIG by trace-context. Both remote ends now derive from that one declaration, so a future item cannot be sent by one side and refused by the other, and session-scoped enablement items are skipped on live convergence exactly as the local path skips them. Also fixes a latent stderr leak: an absent session lock printed a raw redirect failure, which the new remote resolve site made visible. Adds tests/fm-remote-secondmate-trace-context.test.sh, driving the real parent -> fm-on -> remote entrypoint -> control -> remote fm-spawn chain over the deterministic SSH boundary and reading the carrier back from the remote pane's own log. * no-mistakes(document): Clarify remote trace and allowlist contracts * feat(bin): preflight remote runtime tool paths (#1623) * feat(bin): widen the remote runtime PATH and add a remote doctor preflight The fixed remote entrypoint hard-coded a four-directory PATH, so a remote account whose tools live under nix or a per-user profile could not run basic Firstmate work without a login shell. The entrypoint now composes its child PATH from the code root's bin, the account's ~/.local/bin, the common package-manager directories that actually exist on the host, and the portable system tail, deduplicated and in a fixed order, still under env -i with the same variable allowlist and no shell command string. fm-remote-doctor.sh reports that exact PATH by inheriting it from its own entrypoint launch rather than recomposing it, so the ordering keeps one owner. It is read-only, reports where each required and optional tool resolved, and exits non-zero naming every required tool that did not. Remote seeding runs it as a preflight before anything is created on the host and restores the registry when it fails. * no-mistakes(review): Harden remote git authorization and missing-tool diagnostics * no-mistakes(document): Document remote PATH doctor and safe shims * no-mistakes(lint): Fix ShellCheck findings in remote path tests * no-mistakes(lint): Suppress exported fixture's false-positive ShellCheck warning * feat: gate remote second mates on Herdr readiness (#1639) * feat(bin): gate remote second mates on herdr readiness A remote second mate now always runs on the Herdr backend, whose server belongs to the host's GUI login session and therefore outlives the SSH connections that supervise it. fm-spawn's remote route forces that backend and the host-local control script refuses any other, so the requirement cannot be dropped from either side. fm-remote-doctor.sh becomes the single owner of what "ready" means. It keeps its PATH and tool reporting from #1623 and adds the Herdr, Aqua LaunchAgent, GUI-session, server-reachability, and entrypoint-symlink checks, tagging each gap fixable: or human: with the exact operator step. --fix closes only the automatable gaps - writing and loading the Aqua-scoped dev.firstmate.herdr launch agent, starting the server where no launch agent applies, and recreating the entrypoint symlink - then re-derives every check from the host, so a human gap is never presented as fixed. It never creates a login session, writes an auto-login password, or touches FileVault. Remote seed, remote spawn, and the startup liveness relaunch all run the same check, repair, re-check sequence through one shared library and fail closed with the doctor's own gap text. Recovery inherits the gate because it respawns through the same route. Tests drive the real doctor against a controlled account fixture with a private HOME, a state-backed launchctl, and a fake herdr, and prove the dangerous actions are never attempted. The remote lifecycle suites gain a stateful Herdr CLI fixture and answer the readiness gate at the SSH boundary, so they never inspect or repair the runner's own account. * no-mistakes(review): Validate launch-agent contract and confirm Herdr startup * no-mistakes(review): Validate loaded launch-agent contract before readiness * no-mistakes(review): Refuse legacy remote backends without altering routes * no-mistakes(review): Clarify conditional remote readiness repair sequence * no-mistakes(review): Repair remote readiness before liveness probing * no-mistakes(review): Preserve unknown seeds and reject legacy liveness * no-mistakes(document): docs: clarify remote Herdr backend ownership * fix: isolate remote secondmates in shared Herdr session (#1659) * Pin remote secondmates to fm-remote * no-mistakes(review): Fail closed on legacy remote Herdr endpoints * no-mistakes(review): Isolate fm-remote launch agent from interactive default * no-mistakes(document): Document shared remote Herdr retirement safety * feat: route remote commands through an Aqua job worker (#1660) * feat: run remote commands through Aqua job worker * no-mistakes(review): Enforce remote job deadlines and safe worker shutdown * no-mistakes(review): Refresh stale workers and harden dependency-free supervision * no-mistakes(review): Harden worker ownership recovery and shutdown quarantine * no-mistakes(review): Fix doctor bootstrap, harness repair, and output draining * no-mistakes(review): Probe doctor tools through authenticated worker bootstrap * no-mistakes(review): Refresh stale workers before doctor tool probes * no-mistakes(review): Recover stopped quarantines and extend job deadlines * no-mistakes(review): Separate queue and execution timeout windows * no-mistakes(review): Supervise Linux worker crashes and bind root identity * no-mistakes(review): Resolve authorized Nix profile bin links * no-mistakes(review): Clarify Nix path resolution documentation * no-mistakes(review): Harden PATH safety and nvm selection * no-mistakes(review): Honor nvm system defaults and refresh doctor digest * no-mistakes(review): Keep workers ready during active jobs * no-mistakes(review): Bound pre-execution validation by job timeout * no-mistakes(document): Clarify remote worker documentation * no-mistakes(lint): Fix remote worker ShellCheck diagnostics * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * fix: clarify remote doctor bootstrap path (#1691) * fix(bin): bound remote SSH dead-peer detection (#1699) * fix(remote): arm SSH dead-peer detection in fm-on.sh A vanished remote host mid-poll (a reboot, a dropped link) left ssh blocked indefinitely on a half-open TCP connection, because fm-on.sh's ssh invocation had no ServerAliveInterval/ServerAliveCountMax. This wedged the remote-reply ferry: fm-procevent.sh's runner blocked inside the ssh child and never reached its own no-result -> claim-release -> reconcile re-arm self-healing path, which otherwise already handles a nonzero exit with empty output correctly. Recovery required a manual retire and re-arm. Arm ServerAliveInterval=15 and ServerAliveCountMax=3 by default (bounded ~45s detection window), both overridable via FM_SSH_ALIVE_INTERVAL and FM_SSH_ALIVE_COUNT_MAX. This is a transport- level fix in fm-on.sh, so it covers every remote command routed through it, not just the reply ferry. The remote sshd answers keepalive probes independently of whatever the remote command is doing, so a legitimately long-but-alive command (a 55s poll, a clone, the doctor) is never falsely killed - only a truly vanished peer trips it, turning that case into a bounded, detectable ssh failure (exit 255) instead of an indefinite hang. Extends tests/fm-on.test.sh with a behavioral regression asserting a bounded, positive ServerAliveInterval/ServerAliveCountMax on the real ssh argv captured through the FM_SSH_BIN process seam, plus coverage that both are env-overridable. * no-mistakes(document): Document SSH dead-peer detection ownership * fix: report stale AXI tools during bootstrap (#1701) * feat(bootstrap): gate stale axi CLIs at the floors firstmate actually uses Add gh-axi 0.1.29 floor so bare --squash PR merges stop failing quietly on older builds. Raise tasks-axi to FM_TASKS_AXI_MIN=0.2.2 (multi-id mv) while keeping feature probes. Keep quota-axi at 0.1.16 after verifying schema 3 and per-model availability already ship there; runway remains optional. * no-mistakes(document): Clarify AXI compatibility documentation ownership * fix: prevent false watcher-down alarms in Claude sessions (#1661) * fix(guard): stop false watcher-down alarm mid-turn under Claude auto-arm bin/fm-guard.sh derived its watcher-health verdict from fm_watcher_healthy, which requires a live watcher process holding the home lock. Under the Claude Stop-hook auto-arm supervision model the watcher is armed at each turn end and exits on its wake, so it runs only between turns. Every guarded command run mid-turn therefore found no live watcher and printed the "WATCHER DOWN - SUPERVISION IS OFF" banner even though supervision was healthy. Because the episode key was derived from the beacon mtime (which the between-turns watcher advances every poll), the full banner re-printed on essentially every command, and the message always blamed a "fresh beacon" that was in fact fresh. Make the pull guard's health check model-aware via a new fm_watcher_supervision_verdict in bin/fm-wake-lib.sh: - Under the auto-arm model a beacon fresh within FM_GUARD_GRACE is healthy even with no live watcher process; only a beacon stale beyond grace (or absent) is a genuine lapse and alarms. - Under every persistent-watcher harness (codex foreground checkpoint, opencode/pi/grok background arm, tmux, unknown) a live identity-matched watcher with a fresh beacon is still required, unchanged. The banner now names the true failing condition, a missing live watcher process versus a genuinely stale beacon, instead of always blaming the beacon, and the once-per-episode dedup keys on that condition rather than the beacon mtime so a genuine lapse announces once and does not re-print each turn. The turn-end guard keeps the strict fm_watcher_healthy check because it fires at the turn boundary, where the auto-arm brings a fresh watcher up and it cooperates with that arm. fm_watcher_healthy itself is unchanged, so the arm layer's start/attach/replace decisions are unaffected. Tests in tests/fm-guard-stale-banner.test.sh cover the auto-arm healthy fresh-beacon-without-a-watcher case, the auto-arm stale-beacon alarm and its stable episode, the true-reason banner wording, and the reason-keyed episode surviving a beacon mtime change; existing persistent-model cases are pinned to that model. * no-mistakes(review): Pin secondmate supervision model to launched harness * no-mistakes(document): Align watcher documentation with model-aware supervision health * test: prevent fixture temporary directory leaks (#1704) * fix(tests): stop fixture-tempdir helper from self-deleting under command substitution fm_test_tmproot is almost always called as `TMP_ROOT=$(fm_test_tmproot prefix)`, which forks a subshell to capture its stdout. The old implementation set its EXIT cleanup trap inside that call, so the trap fired - and deleted the fixture root - the instant the subshell exited, before the real caller's own EXIT trap was ever installed. Every test using the documented call pattern leaked its fixture root on every run; two suites had already independently discovered and worked around this with ad-hoc mktemp calls. Registration now goes through a $$-keyed registry file instead of in-process state, since $$ resolves to the invoking shell's PID even inside the subshell. The real cleanup trap is armed once at source time (always the real caller, never a subshell) for EXIT, INT, and TERM. A best-effort orphan sweep on next source reaps marked fixture roots old enough to be from a killed prior run. Simplifies the two existing ad-hoc workarounds (fm-procevent.test.sh, wake-helpers.sh) back onto the shared helper now that it works correctly. * no-mistakes(review): Preserve live fixtures during orphan reaping * no-mistakes(review): Harden fixture ownership against PID reuse * no-mistakes(review): Secure cleanup registry against path precreation * no-mistakes(review): Make fixture registration transactional * no-mistakes(document): Documentation already matches fixture cleanup behavior * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * feat(herdr): enable presentation spaces by default (#1708) * feat(herdr): default presentation spaces on with an explicit opt-out Herdr's disposable one-task presentation workspace was opt-in through the presence of local config/herdr-presentation-spaces. It is now on by default, and a home opts out by writing "off" into that same file. Values are read with the whole-file whitespace-stripped convention the other scalar config items already use, plus case folding. An absent file, an empty file, and "on" all resolve on; only "off" opts out; an unrecognized value warns and keeps the default rather than failing a spawn over a purely visual setting. The empty file is exactly the historical opt-in form, so every home that had already enabled the projection stays enabled with no migration step, and no previously enabled home can be turned off by the flip. Because absence now means on at both ends, secondmate inheritance needs no item-specific convergence: mirroring an absent primary file converges a secondmate to the same default-on rather than turning its projection off, and only an explicit primary opt-out propagates the opt-out. The gate itself moves into fm_backend_herdr_presentation_enabled in the Herdr adapter so the semantics have one owner that regressions can exercise directly. * no-mistakes(document): Document Herdr default-on presentation safety --------- Co-authored-by: kunchenguid <kun-1@kunchenguid.com> * fix(bin): surface fleet-wide open decisions on every wake drain (#1711) * fix: surface consolidated open decisions on every wake-drain A needs-decision or blocked event buried under later, unrelated status appends was only ever shown via the last-line wake annotation, so a still-open captain decision could go silently missed even though status_open_decisions (fm-classify-lib.sh) already folds the whole status stream correctly and fleet-snapshot/bearings already reuse it. Wire that same fold into bin/fm-wake-drain.sh: a new fleet-wide scan_open_decisions wrapper scans every state/<id>.status, and fm-wake-drain.sh prints a separate, bounded OPEN DECISIONS section on every drain (including the empty-queue fast path), so session-start and every wake-handling turn surface it for free without duplicating the open/resolved fold itself. Heartbeat wakes drain through the same script, so this covers that surface too. Also tighten status_open_decisions' file guard to skip an unreadable status file instead of leaking a bash redirection error, now that a fleet-wide directory scan can reach files a single targeted read would not. * no-mistakes(review): Prevent status symlinks leaking open decisions * fix: drop unbounded perl subprocess from status symlink guard The review step's own symlink-safety auto-fix (O_NOFOLLOW read via a perl subprocess) forked one perl process per status file scanned by the new fleet-wide open-decisions scan, with no cap - inflating fm-wake-drain.sh's total external-read cost from 8 (the existing annotation read_cap) to 18 in the enrichment-caps regression test. The plain [ -L "$f" ] check already rejects any status file that is itself a symlink before any read happens, which is exactly what the new regression test exercises and is the same defense level the sibling scan_captain_relevant_statuses/last_status_line already rely on elsewhere in this file (no O_NOFOLLOW). Drop the subprocess-based nofollow read and keep the cheap builtin guard. * no-mistakes(document): Document actionable fleet-wide open decision drains * fix(bin): abort parked runs and reap leaked processes before teardown (#1710) * fix(bin): abort orphaned no-mistakes runs and reap leaked processes at teardown Teardown could remove a task's worker while its no-mistakes pipeline run was still parked at a gate, leaving an orphaned run holding a fleet slot indefinitely (observed 2026-08-03: runs parked 7h39m and parked at a post-CI approval gate). It could also leave backgrounded/disowned descendant processes rooted under the worktree or tasktmp surviving reparented to init (observed: two `go test` binaries pinning CPU for hours with no live task meta to attribute them to). Add two coupled pre-teardown steps, both scoped to this task's exact branch/head or worktree/tasktmp so they can never touch another task's run or processes: - conclude_task_no_mistakes_run aborts a run parked at a gate via `no-mistakes axi abort`, cd'd into the exact worktree so the daemon resolves the run itself rather than teardown naming a --run id. - reap_task_worktree_processes sweeps for processes whose cwd is under the worktree or tasktmp (via `lsof -a -d cwd`) and TERM/KILLs them. Both run before any worktree return, branch delete, or backend kill, and are idempotent on a retried teardown. The branch+head attribution logic is factored out of bin/fm-crew-state.sh into the new shared bin/fm-nm-run-lib.sh so both scripts use the same ownership contract. * no-mistakes(review): Fail closed on incomplete teardown cleanup * no-mistakes(review): Bind teardown cleanup to verified run and process identities * no-mistakes(review): Require confirmed aborts and convergent identity-safe process reaping * no-mistakes(review): Handle process exits during teardown identity checks * no-mistakes(review): Restore teardown library in hermetic gotmp fixtures * no-mistakes(document): Document teardown run attribution and timeout * no-mistakes(lint): Rename shell variable conflicting with done keyword * no-mistakes: apply CI fixes * fix: resolve fm-remote-entrypoint.sh SCRIPT_DIR through a PATH symlink (#1709) The script installs as a symlink under ~/.local/bin. Taking dirname of the symlink itself (instead of its real target) pointed SCRIPT_DIR at ~/.local/bin, breaking sourcing of the sibling fm-remote-job-lib.sh. Resolve the real path first, preferring python3's os.path.realpath, then realpath, falling back to the raw BASH_SOURCE on hosts with neither. * fix(pi): gate Calm built-in overrides by activation state (#1724) * fix(pi): stop Calm claiming a built-in tool name another extension owns fm-calm.ts claimed bash/read/edit/write/grep/find/ls unconditionally at extension load, regardless of w…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the revised explicit delivery-mode contract adopted from scout
fm-delivery-explicit-mode-design-r1(report Q7 contract, Q8 slices). Supersedes #1480, which is closed; only its project-management prose and test scenarios were salvaged, not its.deliveryrecord architecture.Problem
A task's delivery mode and
yoloposture were resolved by a silent per-project registry lookup, independently, in two places:fm-brief.shwhen shaping the definition of done, andfm-spawn.shwhen writing task metadata. Nothing forced a per-task decision, and an adjusted brief could say one thing while the recorded task delivery said another.What changes
fm-brief.shrequires--modeon ship scaffolds, validates the closed set, and records the choice as a fixed machine-readableDelivery contract: mode=<mode>line in the generated definition of done.--modeis refused on scout and secondmate scaffolds;--yolois refused outright, since the worker never owns approval decisions.fm-spawn.shrequires--modeand--yoloon ship spawns and validates both closed sets. Before any endpoint exists it reads the brief's recorded contract line and refuses a mismatch; a brief scaffolded before that line existed warns once and launches on the flag. A batch carries one shared contract that every pair still checks against its own brief. Scout and secondmate spawns refuse the flags, and a scout now records nomode=/yolo=at all - teardown already defaults an absent mode tono-mistakesand the snapshot renders it empty. When the explicit mode carries less rigor than the project's standing posture, a one-line deviation notice prints and the spawn continues, so the registry stays advisory rather than an enforced default.fm-promote.shrequires the same two flags, because a scout carries no posture to inherit, and writes them into the task record alongside thekind=flip. The mode is now decided by the firstmate that just read the scout report rather than frozen from scout-spawn time.fm-project-mode.shremains the single registry parser for the mechanical consumers that have no task in hand (fleet sync'slocal-onlyskip, home seeding's refusal and no-mistakes init). It accepts the conditionalno-mistakes-prod-onlyannotation and maps it to its most rigorous leg for them, and grows--rawso the deviation notice can tell a conditional policy apart from a flat mode.Instruction surface: AGENTS.md section 7 owns the intake resolution, including the
no-mistakes-prod-onlysurface classification and the unregistered-project fallback; the project-management skill defines the conditional policy as a registration-time posture with its defaults and initialization consequences; the registry blurb, script table, and architecture section follow.Deliberate boundaries
no-mistakes>direct-PR>local-only). Registeredlocal-onlyshipped asdirect-PRis not announced, matching the adopted contract; that case exposes a remote and may be worth revisiting.no-mistakesstanding default, so a downgrade there is announced too. The notice names the standing posture rather than a registry line, because for an unregistered project there is none.Verification
bin/fm-lint.shclean;bin/fm-doc-audience-check.shok (61 surfaces, 175 local links);bin/fm-test-run.sh --check-coverageok.bin/fm-test-run.sh --changed --base main: 35 scripts, 0 failed.tests/fm-task-delivery.test.shcovers the spawn contract (required and closed-set flags with no metadata written, scout/secondmate refusal, brief agreement plus the legacy-brief warning, the deviation-notice matrix, scouts recording no posture), promotion, and the registry mapping. Extendedtests/fm-brief.test.shcovers the brief contract, including that the explicit mode wins over the registered posture.