Skip to content

fix: parse decision verbs before status metadata tags - #2280

Merged
kunchenguid merged 3 commits into
mainfrom
fm/fm-remote-reply-decision-closer-fix-r1
Aug 12, 2026
Merged

kunchenguid merged 3 commits into
mainfrom
fm/fm-remote-reply-decision-closer-fix-r1

Conversation

@kunchenguid

Copy link
Copy Markdown
Owner

Intent

Bug fix in firstmate shared tracked material: the open-decisions fold mis-parses a remote-secondmate reply status line, so fm-send --resolve-key cannot close it. Root cause: status_line_verb() in bin/fm-classify-lib.sh only strips a leading [key=...] tag before the colon when isolating the verb word. A remote secondmate reply routinely prepends a [corr=...] correlation tag ahead of (or instead of) [key=...] - e.g. 'needs-decision [corr=d448ea86afa4bf67] [key=loan-installment-cadence-amount]: ...' - which the old parser did not strip, so the returned verb was contaminated ('needs-decision [corr=...]') and the fold's verb match silently failed to recognize the line as a decision at all, causing fm-send --resolve-key to wrongly refuse with 'no open decision with that key'. Fix: generalize status_line_verb to strip EVERY [name=value] tag before the colon (not just [key=...]) so the leading word returned is the bare verb (needs-decision/blocked/resolved/etc), regardless of how many bracketed tags precede the colon or their order. This is a fix to the ONE owner of this parser; no other place in the codebase duplicates single-tag-only stripping (_fm_key_before_colon, _fm_key_at_note_head, and _fm_decision_key already use substring/wildcard matching for [key=...] and are unaffected). Deliberately excluded: no remote-only send retry, no special remote closer path, no remote-vs-local branching - the shared parser is fixed so local and remote fold identically (transport-only difference principle). Required regression tests (added, all passing): tests/fm-classify-decision-key.test.sh gained cases driving the real status_line_verb/status_open_decisions functions for (1) a [corr=...] tag before [key=...] opening and closing under the stated key, (2) a corr-only tag opening under the 'default' key exactly like a bare needs-decision line, (3) a key-only tag (no corr) still opening correctly - no regression, and (4) blocked/resolved verbs parsing correctly regardless of bracket-tag order. tests/fm-send-resolve-key.test.sh gained an end-to-end test reproducing the exact reported remote-reply line and asserting fm-send --resolve-key now succeeds and closes it (previously it refused). All new tests were verified to fail against the pre-fix parser and pass after the fix. This is pure shell parser logic, not harness-dependent, so the portable tests/ regression suite is sufficient and no live-harness guard is needed. Delivery: mode=no-mistakes, yolo OFF - the captain (not this agent) must approve any ask-user finding and make the final merge decision. Note for the PR body: this touches the running open-decisions/supervision parsing surface, so fleet homes need a self-update after merge to pick up the fix.

What Changed

  • Strip all [name=value] metadata tags from status verbs, allowing correlation-tagged decisions to open and close correctly.
  • Bump the open-decisions fold version so existing cursors rebuild under the corrected parsing semantics.
  • Add regression coverage for tagged decision folding and fm-send --resolve-key; fleet homes should self-update after merge.

Risk Assessment

✅ Low: The shared parser fix is narrowly scoped, required behavioral regressions are covered, and the fold-version bump correctly invalidates stale persisted cursors.

Testing

Inspected the target diff, ran the three focused parser/send/cursor regression scripts, and reproduced the reported remote reply flow end-to-end: the corr-tagged decision surfaced, fm-send --resolve-key crossed the remote transport and exited successfully, appended the resolution locally, and disappeared from the open-decisions output.

Evidence: End-to-end remote corr-tag resolve-key CLI transcript
$ fm-wake-drain.sh  # before answering
OPEN DECISIONS (still open, folded from the durable status logs - not just the latest line):
rsm [key=loan-installment-cadence-amount] needs-decision: pick the cadence
OPEN DECISIONS: close one by answering it: bin/fm-send.sh <task> --resolve-key <key> '<answer>'

$ fm-send.sh rsm --resolve-key loan-installment-cadence-amount monthly
exit=0

$ remote transport invocation
-o ForwardAgent=no -o ClearAllForwardings=yes -o SendEnv=-* -o ServerAliveInterval=15 -o ServerAliveCountMax=3 -- remote-mac fm-remote-entrypoint.sh 1 L3JlbW90ZS9yb290 L3JlbW90ZS9ob21l Zm0tcmVtb3RlLXNlY29uZG1hdGUtY29udHJvbC5zaABzZW5kAHJzbQBbZm0tZnJvbS1maXJzdG1hdGVd4oGjY29ycj1lNWY2MjQ2YzBkMzZhMGYzIG1vbnRobHkA

$ persisted status ledger
needs-decision [corr=d448ea86afa4bf67] [key=loan-installment-cadence-amount]: pick the cadence
resolved [key=loan-installment-cadence-amount]: answered: monthly

$ fm-wake-drain.sh  # after answering
(no open decisions)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • 🚨 bin/fm-classify-lib.sh:189 - Changing status_line_verb changes fold semantics, but FM_OPEN_DECISIONS_FOLD_VERSION remains 3 at line 444. After upgrading, an existing v3 cursor already positioned past a corr-tagged decision is trusted with an empty open set, so incremental OPEN DECISIONS scans never reprocess or surface that decision. Bump the fold version and add a regression covering migration from a pre-fix cursor.

🔧 Fix: Invalidate stale decision cursors after parser fix
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-classify-decision-key.test.sh
  • bash tests/fm-send-resolve-key.test.sh
  • bash tests/fm-wake-drain-open-decisions-cursor.test.sh
  • Manual remote-secondmate fixture using real bin/fm-wake-drain.sh and bin/fm-send.sh rsm --resolve-key loan-installment-cadence-amount monthly with a stubbed SSH transport
  • Validated the evidence transcript contains the initially open keyed decision, successful exit, remote transport invocation, persisted resolution, and no remaining open decision
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…verb

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

Generalize the parser to strip every "[name=value]" tag before the
colon, in any order and count, so local and remote replies fold
identically.
@kunchenguid
kunchenguid merged commit b91016f into main Aug 12, 2026
13 checks passed
@kunchenguid
kunchenguid deleted the fm/fm-remote-reply-decision-closer-fix-r1 branch August 12, 2026 20:50
spirispark added a commit to spirispark/firstmate that referenced this pull request Aug 13, 2026
* fix: preserve fleet state in truncated session-start digests (#1798)

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

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

Three changes, one file's worth of composition:

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

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

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

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

* no-mistakes(document): Clarify digest source recovery comments

* feat(send): close answered decisions at answer time via --resolve-key (#1842)

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

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

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

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

* fix(bin): seed remote secondmates from supplied origins (#1836)

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

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

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

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

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* fix(tests): restore reliable fm-send backend parity coverage (#1851)

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

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

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

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

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

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

* no-mistakes(document): Clarify fm-send key regression scope

* fix(bin): mirror remote secondmate status streams (#1846)

* fix(bin): mirror the whole remote secondmate status stream

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

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

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

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

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

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

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

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

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

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

* no-mistakes(document): Document remote status-stream mirroring

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

* fix(bin): fail closed on NUL bytes in the durable parent binding (#1847)

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

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

* fix(skills): reconcile inherited secondmate plans with shipped state (#1853)

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

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

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

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

* no-mistakes(document): Clarify secondmate record-intake scope

* fix: move network checks off the session-start blocking path (#1860)

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

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

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

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

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

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

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

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

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

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

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

* no-mistakes(document): Document deferred startup network behavior accurately

* fix(procevent): apply remote replies during capture (#1831)

* fix(procevent): apply a captured adapter result in code, not by instruction

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* no-mistakes(document): Clarify remote reply relay documentation

* feat(skills): port internal stow curation disciplines to the public skill (#1841)

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

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

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

* fix(bin): keep tracked Claude hook entries inert under grok 1.0.0 (#1917)

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

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

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

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

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

Two deliberate limits:

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

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

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

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

* no-mistakes(review): docs: state grok guard criterion by event coverage

* feat(startup-network): record per-step elapsed times for the deferred stage (#1918)

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

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

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

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

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

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

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

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

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

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

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

* fix(remote-job): stop workers abandoned by a pruned code root (#1927)

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

Three things combined to make that possible:

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

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

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

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

* feat(bin): lint only the changed shard locally, full lint in CI (#1925)

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

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

* no-mistakes: apply CI fixes

* feat(bin): add deterministic agent lifecycle control (#1568)

* feat(bin): add deterministic agent lifecycle control

Separate firstmate's data plane from its control plane.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

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

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

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

* feat(stow): add tiered decaying memory management (#1984)

* feat(stow): tiered decaying memory with captain-gated offload to local excluded skills

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* no-mistakes(document): Clarify stow receipt marker actions

* docs: add project vision (#1997)

* docs: add firstmate vision

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

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

* no-mistakes: apply CI fixes

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

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

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

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

* fix(cmux): classify borderless Claude composer

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

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

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

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

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

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

* fix(watcher): resurface durable work after downtime

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

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

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

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

* ci: add Windows Herdr automation spike

* ci: run Windows spike on its pull request

* fix: wait for Windows Herdr command output

* fix: run ANSI probe in pane shell

* ci: keep Windows Herdr spike manually triggered

* docs: clarify Windows Herdr spike verdict

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

* Add guided ahoy decision flow

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

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

* Harden stow memory budget policy

* Refine internal stow offload policy

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

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

* fix(spawn): refresh pooled worktree base

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

* no-mistakes: apply CI fixes

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

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

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

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

* no-mistakes: apply CI fixes

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

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

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

* no-mistakes: apply CI fixes

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

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

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

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

* docs(vision): state the experience goal positively

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

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

* fix: reconcile inactive terminal outcomes

* fix: stream secondmate summary inputs

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

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

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

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

* refactor: simplify inactive outcome reconciliation

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* ci: raise Herdr test timeout (#2191)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

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

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

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

Fixes #2109

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: collapse duplicate supervision wakes without losing legitimate updates

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

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

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

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

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

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

* feat(harness): add Cursor Agent CLI adapter

# Conflicts:
#	bin/fm-spawn.sh

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

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

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

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

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

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

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

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

Two consequences wired up:

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

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

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

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

Verified live on cursor-agent 2026.08.11-e8db854.

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

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

Cursor shipped as "unknown cursor-unverified" on the premise that it exposes no
semantic turn lifecycle, only a rendered "Working" footer. That premise is
wrong: cursor-agent persists an append-only JSONL transcript per conversation
and brackets every submitted turn with a ro…
mfernrdx added a commit to mfernrdx/firstmate that referenced this pull request Aug 14, 2026
…CLAUDE.md syncing (#1)

* fix(bin): keep tracked Claude hook entries inert under grok 1.0.0 (#1917)

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

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

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

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

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

Two deliberate limits:

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

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

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

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

* no-mistakes(review): docs: state grok guard criterion by event coverage

* feat(startup-network): record per-step elapsed times for the deferred stage (#1918)

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

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

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

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

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

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

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

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

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

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

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

* fix(remote-job): stop workers abandoned by a pruned code root (#1927)

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

Three things combined to make that possible:

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

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

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

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

* feat(bin): lint only the changed shard locally, full lint in CI (#1925)

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

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

* no-mistakes: apply CI fixes

* feat(bin): add deterministic agent lifecycle control (#1568)

* feat(bin): add deterministic agent lifecycle control

Separate firstmate's data plane from its control plane.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

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

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

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

* feat(stow): add tiered decaying memory management (#1984)

* feat(stow): tiered decaying memory with captain-gated offload to local excluded skills

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* no-mistakes(document): Clarify stow receipt marker actions

* docs: add project vision (#1997)

* docs: add firstmate vision

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

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

* no-mistakes: apply CI fixes

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

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

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

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

* fix(cmux): classify borderless Claude composer

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

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

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

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

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

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

* fix(watcher): resurface durable work after downtime

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

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

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

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

* ci: add Windows Herdr automation spike

* ci: run Windows spike on its pull request

* fix: wait for Windows Herdr command output

* fix: run ANSI probe in pane shell

* ci: keep Windows Herdr spike manually triggered

* docs: clarify Windows Herdr spike verdict

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

* Add guided ahoy decision flow

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

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

* Harden stow memory budget policy

* Refine internal stow offload policy

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

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

* fix(spawn): refresh pooled worktree base

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

* no-mistakes: apply CI fixes

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

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

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

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

* no-mistakes: apply CI fixes

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

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

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

* no-mistakes: apply CI fixes

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

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

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

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

* docs(vision): state the experience goal positively

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

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

* fix: reconcile inactive terminal outcomes

* fix: stream secondmate summary inputs

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

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

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

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

* refactor: simplify inactive outcome reconciliation

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* ci: raise Herdr test timeout (#2191)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

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

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

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

Fixes #2109

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: collapse duplicate supervision wakes without losing legitimate updates

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

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

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

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

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

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

* feat(harness): add Cursor Agent CLI adapter

# Conflicts:
#	bin/fm-spawn.sh

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

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

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

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

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

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

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

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

Two consequences wired up:

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

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

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

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

Verified live on cursor-agent 2026.08.11-e8db854.

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

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

Cursor shipped as "unknown cursor-unverified" on the premise that it exposes no
semantic turn lifecycle, only a rendered "Working" footer. That premise is
wrong: cursor-agent persists an append-only JSONL transcript per conversation
and brackets every submitted turn with a role:user open and a typed turn_ended
close. Verified live on 2026.08.11-e8db854, including the interrupt path, where
Escape closes the turn with status "aborted" - so this source covers manual
interruption, which Claude's Stop hook does not.

That makes it a genuine pull source in the muse mould rather than the rendered
text the redesign forbids: no writer, no arm, no gen, nothing seeded that could
never be cleared. Cursor's `ctrl+c to stop` footer stays out of the verdict, and
herdr's narrower native streaming state cannot stand in for it either.

Binding deliberately does not reconstruct cursor's workspace-slug directory
name. That slug collapses path separators, so rebuilding it would be a guess
that could bind the wrong pane; cursor records the exact absolute workspace path
in each project's .workspace-trusted, and the binding matches on that. A
conversation recorded as prior at spawn is excluded, so a relaunch in a reused
worktree folds its own turn rather than its predecessor's. Requiring a unique
remaining conversation keeps zero and several both unknown, because neither
proves anything about the current turn.

The regression pins the fold with real transcript files and asserts the
dangerous direction stays closed: an unresolvable binding, a record-free file,
an unclaimed workspace, and a workspace-path PREFIX all read unknown, never
idle. The prefix case uses an opaque fixture slug so a slug-rebuilding
implementation cannot pass it.

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

* feat(cursor): make the cursor launch runnable and give it lifecycle control

Five gaps that together kept a cursor crewmate from being drivable end to end.

Launch. The template invoked `cursor agent`, but `cursor` is not the CLI - the
installed names are `cursor-agent` and the legacy alias `agent` - so the command
could not run at all on a machine with a normal cursor install. It now resolves
through the verified owner, which also refuses a spawn loudly instead of leaving
a pane that dies with command-not-found and reads as a wedged worker.

Session binding. fm-spawn writes state/<id>.cursor-session so the busy fold can
find this pane's transcript, and teardown removes it.

Lifecycle control. No cursor PR touched fm-control-lib.sh, so
`fm-control <id> interrupt|exit|relaunch` could not drive a cursor worker at
all. Verified live: interrupt is a single Escape, exit is /exit, and cursor does
NOT repollute its composer with the cancelled prompt, so unlike muse it needs no
clear key. Secondmate is refused, matching the spawn refusal.

Submit acknowledgement. cursor parks its terminal cursor outside its composer,
so the composer verdict on tmux is always `unknown` and a submit could never be
acknowledged from the composer alone. The submit core's existing idle-to-busy
transition covers that case, but only if the pane's busy footer is recognised,
so cursor's `ctrl+c to stop` joins the harness-less default union the submit
cores read. The TOKEN is matched rather than the spinner verb: the same version
rendered both `Working` and `Running` in consecutive turns.

Bootstrap. A configured cursor crew harness with no cursor executable is now a
loud MISSING diagnostic rather than a first-spawn failure, and it accepts either
installed name.

Interrupt cancellation is deliberately left unconfirmed. The transcript does
type an aborted close, but its post-interrupt write latency measured as
variable - sometimes seconds, sometimes not within twenty - so a claim built on
it would be unreliable. Normal turn completion is prompt, which is what the busy
fold actually depends on.

Two inherited tests are corrected rather than deleted: the busy test asserted
cursor could have no semantic source, and the launch test pinned the literal
`cursor agent` string. Both now pin the verified behaviour, including that the
launch never allocates a second worktree.

Co-authored-by: ABHISHAKE KUMAR BOJJA <abojja@uvic.ca>
Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* docs(cursor): record the verified crewmate facts and extend the drift guard

The inherited cursor entry was written against 2026.08.04-aaa8809 and several of
its claims no longer hold: it named `cursor agent` as the binary (not the CLI
name), listed six Grok model ids of which the live catalog now returns two, and
recorded busy state, exit, interrupt, and skill invocation as unverified.

Replaced with what was measured against 2026.08.11-e8db854, including the two
facts most likely to be rediscovered painfully: cursor runs as a bundled node
script so its pane title is a bare `node`, and it parks its terminal cursor
outside its composer, which makes the tmux composer verdict permanently
`unknown` by design rather than a defect to chase.

Model ids now route to `--list-models` for the account instead of a fixed list,
since that list is exactly what drifted.

The live drift guard covers cursor, resolving it through the same verified owner
fm-spawn uses and passing --trust so the probe cannot hang on the workspace
prompt. Run against every installed harness: 8 checked, all alive, with cursor
reporting title='node' foreground=[.../cursor-agent] - the drift shape this
guard exists to catch.

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

* docs(agents): record the cursor session-binding state file

The state/ layout section is the inventory every session reads; a busy-source
binding that fm-spawn writes and teardown removes belongs in it alongside muse's.

* no-mistakes(review): Sanitize ambient Cursor marker in harness tests

* no-mistakes(review): Validate Cursor models against live catalog

* no-mistakes(review): Reject unsupported secondmates before binary preflight

* no-mistakes(review): Narrow Cursor ancestry detection to structured process identity

* no-mistakes(review): Parse Cursor transcripts and sanitize inherited markers

* no-mistakes(review): Handle malformed Cursor transcript records safely

* no-mistakes(review): Validate malformed Cursor closes in fallback parser

* no-mistakes(review): Retire stale Cursor bindings during relaunch

* no-mistakes(review): Fix Cursor drift guard command variable

* no-mistakes(review): Narrow Cursor identity to versioned install trees

* no-mistakes(document): Document Cursor harness boundaries

* refactor(composer): move the delivery busy footers to the shared owner

The per-harness rendered busy footers lived in bin/fm-tmux-lib.sh under
FM_TMUX_* names, so cursor's `ctrl+c to stop` signature - and every other
harness's - was reachable only from tmux. That placement was wrong on its own
terms: herdr, zellij, cmux, and orca run the same harnesses and face the same
question these footers answer, which is whether a submitted Enter actually
landed. Nothing about the signature is tmux-specific.

Moved verbatim into bin/fm-composer-lib.sh, the shared composer/delivery owner
every backend already sources, and renamed to FM_DELIVERY_* so the names stop
claiming a scope they never had. All five adapters now reach cursor's signature;
verified per adapter rather than assumed.

The boundary the move must not blur is stated where it now lives: this is a
DELIVERY guard, never a worker-state source. Confirming a keystroke landed is a
different question from asking what a worker is doing, and bin/fm-busy-lib.sh
remains the semantic owner that forbids classifying a harness from rendered
text. Cursor still classifies only from its transcript fold, which is already
backend-agnostic because it folds a file rather than reading a pane - the same
verdict on all six backends.

The old FM_TMUX_* aliases are dropped rather than kept as dead shims: nothing
outside the moved block referenced them except fm-busy-lib.sh's grok fallback,
which now reads the new name. The documented operator override, FM_BUSY_REGEX,
is untouched.

Also removes a dead duplicate CURSOR_INVOKED_AS check in bin/fm-harness.sh,
unreachable behind the marker check above it.

* no-mistakes(review): Correct shared delivery guard ownership references

* no-mistakes(document): Document shared delivery guards and Cursor backend limits

* no-mistakes: apply CI fixes

* fix(composer): bound a bare composer's wrap region at a half-block rule

A live cursor crewmate on herdr classified its IDLE composer as `pending`, and
fm-send consequently exited 1 with "delivery unconfirmed" on a message that had
actually landed. The cause is not cursor-specific.

Herdr draws a composer's top and bottom rules with the half-block glyphs U+2584
and U+2580 rather than the box-drawing family. fm_composer_row_has_edge knew
only the box-drawing set, so no box was detected; the composer was found as a
BARE row, and its wrap region - which extends while rows are non-blank and carry
no structural edge - walked straight through the composer's own closing rule and
swallowed the model and path footer below it. That footer is real text, so the
region classified pending on a genuinely idle pane.

Teaching the shared edge detector the half-block glyphs bounds the region at the
closing rule. Measured on the captured bytes of a real herdr cursor pane: the
same capture that read `pending` now reads `empty`.

This is a shared shape-path change, so it is deliberately narrow - it adds
glyphs to the edge vocabulary and changes no verdict logic - and the whole
composer and backend suite is green, including the other harnesses' herdr
fixtures.

The regression pins the real captured shape and asserts the footer content is
genuinely present, so the case cannot pass vacuously if the region were ever
bounded for some unrelated reason.

* fix(herdr): confirm a cursor submit from the rendered-footer transition

Herdr's composer-shape fix made an idle cursor pane classify `empty`, but
`fm-send` still exited 1 with "delivery unconfirmed" on messages that had
actually landed. Live measurement found the second, independent cause.

Herdr reports a cursor pane `agent_status=blocked` in EVERY state - idle,
mid-turn, and after - so the submit path's idle-baseline native confirmation is
structurally unreachable for cursor and every send falls into the composer
branch. That branch reads cursor's mid-turn composer row, which renders its own
`Add a follow-up` placeholder beside a right-aligned `ctrl+c to stop`. That
token is composer content, so the verdict is `pending` on a composer holding no
user text at all, and the Enter-retry budget then reports pending.

The escape is the same semantic signal the native path uses, read from the
pane's verified busy footer instead of native agent-state, and it is the
rendered-footer twin of the tmux submit core's turn-started confirmation: an
idle-to-busy transition ACROSS our Enter proves the harness accepted the
submission. The baseline is taken before the first Enter and only when the
native baseline was not legibly idle, so the idle-baseline path still never
reads pane content and a pane already mid-turn before we typed keeps reporting
`pending` rather than borrowing another turn as proof of this delivery.

The composer verdict is deliberately NOT relaxed. A right-aligned status token
on the composer row stays content for every other caller, including the
away-mode pre-injection guard, and the shared cursorless submit core is left
untouched so zellij, cmux, and Orca keep the behavior their own follow-up owns.

Verified live on herdr 0.8.0 and cursor-agent 2026.08.11-e8db854 in an isolated
lab session: `fm-send` now exits 0 and the steer executes, interrupt cancels a
running turn, `/exit` stops the agent, and teardown clears the record. All seven
panes of the running default session classify identically before and after the
shape fix, so no other harness regressed.

* no-mistakes(review): Prevent working Herdr baselines from falsely confirming delivery

* no-mistakes(document): Correct Cursor harness and backend documentation

---------

Co-authored-by: ABHISHAKE KUMAR BOJJA <abojja@uvic.ca>
Co-authored-by: Amplify Logic AI <lars@sockinator.co>
Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* fix(bin): require quota-axi 0.1.25 (#2300)

* fix: raise quota-axi floor to 0.1.25 for Cursor CLI quota awareness

Homes on latest main need quota-axi #87 so Desktop-absent CLI machines report a fresh Cursor quota instead of a false sign-in-required.

* no-mistakes(document): Update quota floor documentation pointer

* fix(bin): never turn a real CLAUDE.md into a symlink, avoid it when unsafe

fm-ensure-agents-md.sh symlinked CLAUDE.md -> AGENTS.md unconditionally.
On a repo whose git checkout will not materialize symlinks (Windows
checkouts commonly run with core.symlinks=false), a tracked symlink
checks out as a plain text stub holding its link target, silently
breaking any agent that reads CLAUDE.md. site-feasibility hit and
reverted exactly this; running the helper again reintroduced it.

New decision rule:
- A repo that already has a real, regular CLAUDE.md is never promoted
  into a symlink arrangement. Promotion now copies CLAUDE.md's content
  into AGENTS.md and keeps CLAUDE.md as a real file synced to it,
  regardless of whether this worktree's own filesystem happens to
  support symlinks.
- When CLAUDE.md doesn't exist yet, a new claude_symlink_unsafe() check
  decides whether to create it as a symlink or a real synced duplicate.
  It combines two real signals instead of guessing from the path: git's
  own effective core.symlinks for that repo (git's own record of
  whether it will materialize a tracked symlink there), and a live
  filesystem probe as a fallback when that config is unset.
- Two real files with identical content are now treated as the
  synced-duplicate steady state (idempotent "unchanged"); two real
  files with different content still refuse as a genuine conflict.

The generated brief text needs no change: the helper stays a no-arg-
required, safe-by-default call.

Colocated tests cover: a real existing CLAUDE.md never becoming a
symlink (including a core.symlinks=false, site-feasibility-shaped
repro); the symlinks-unreliable fallback producing a synced real file,
both when creating from scratch and when only AGENTS.md exists; the
existing correct-symlink and case-variant conflict paths untouched;
and the different-content conflict refusal preserved.

* no-mistakes(review): trap-clean symlink probe, fix architecture doc claim

* no-mistakes(document): document unconditional real-CLAUDE.md rule in architecture.md

* fix(bin): close the fresh-repo DrvFs gap in claude_symlink_unsafe()

The round-1 no-mistakes reviewer flagged, and the captain verified live on
his own /mnt/c checkout, that a fresh repo on WSL's DrvFs (the bind that
exposes a native Windows drive into Linux) still passed both existing
signals: git config core.symlinks is unset on a brand-new repo, and ln -s
succeeds on DrvFs itself, so claude_symlink_unsafe() would still emit a
real CLAUDE.md symlink there. A later native-Windows git clone of that same
repo defaults to core.symlinks=false and materializes it as the dead
9-byte stub - the exact regression this change exists to prevent - and
this is the captain's own highest-risk environment, since all his
production checkouts live under /mnt/c.

Add a third, honest signal: on_drvfs() reads the real mount table
(FM_PROC_ROOT_OVERRIDE-overridable, /proc/mounts by default, the same
override convention bin/fm-cursor-lib.sh already uses for testability)
rather than matching the path string. DrvFs surfaces as fstype "drvfs"
directly on older WSL, or as fstype "9p" carrying "aname=drvfs" in its
mount options on current WSL2 - confirmed against a live WSL2 host, where
/mnt/c is aname=drvfs and /usr/lib/wsl/drivers is a same-fstype "9p" mount
with aname=drivers, correctly left alone. The two existing signals
(core.symlinks=false, the live ln -s probe) are kept unchanged as
additional checks; any one signal reporting unsafe is enough.

Colocated tests: a fresh git-init repo under a simulated DrvFs mount (via
FM_PROC_ROOT_OVERRIDE) gets a real, synced CLAUDE.md rather than a
symlink, with a same-directory ln -s sanity check proving the block came
from the DrvFs signal and not an incidental local filesystem limit; and a
plain non-DrvFs /mnt mount (ext4) still gets a real symlink, proving the
check isn't guessing from the /mnt/ path prefix alone.

* fix(bin): self-heal a drifted real CLAUDE.md duplicate instead of hard-conflicting

no-mistakes review flagged, and firstmate confirmed as a required fix under
standing authority, that a real-duplicate CLAUDE.md (the symlink-unsafe
fallback) had no way to stay in sync past the first divergence. The normal
workflow - a task edits only AGENTS.md, per bin/fm-brief.sh's Project
memory section, then re-runs this helper - leaves the two real files
differing, and the next run then hard-exited 1 asking for manual
reconciliation instead of re-syncing, on exactly the DrvFs/Windows-checkout
projects this whole change targets.

Firstmate's decision (option c of three considered): mark helper-created
real-duplicate CLAUDE.md files and auto-resync only marked files on a
mismatch, keeping today's hard-conflict refusal for any CLAUDE.md this
helper did not create - closing the drift papercut without weakening the
conflict safety net for a genuinely independent file. Rejected: always
auto-resyncing on mismatch (would silently discard independently-authored
CLAUDE.md content, violating "never clobber a distinct real file"), and
leaving it as a hard conflict (makes every future AGENTS.md edit on the
captain's own Windows checkouts - the highest-risk set this task exists to
protect - require manual reconciliation, failing "safe by default").

Implementation: write_claude_duplicate() writes AGENTS.md's content plus a
single trailing HTML-comment marker line whenever CLAUDE.md is written as a
real duplicate (both on first creation and on resync). claude_has_marker()
checks a CLAUDE.md's actual last line for that marker to tell a
helper-owned duplicate apart from a hand-authored file; its absence always
falls through to today's hard-conflict-on-mismatch behavior (fail safe, no
new abstraction, no config, no state file outside the repo). A legacy
byte-identical duplicate from before this marker existed is still
recognized as in sync and gets upgraded with the marker so its project
self-heals going forward too.

Also fixed the round-2 review's auto-fix finding: the non-DrvFs-mount test
fixture used a repo path under $TMP_ROOT rather than an actual /mnt-shaped
path, so it would have still passed even if on_drvfs() regressed into
path-prefix guessing instead of reading the real mount table. The repo now
lives under a genuine $TMP_ROOT/mnt/z/... subtree, with a fixture assertion
that the path actually resolved to that shape.

Colocated tests added: a marked duplicate resyncs (and stays idempotent)
after an AGENTS.md-only edit; an unmarked real CLAUDE.md still refuses on
a content mismatch; a legacy byte-identical unmarked duplicate is upgraded
with the marker on its first post-upgrade run. Existing tests that
asserted byte-for-byte equality between AGENTS.md and a real-duplicate
CLAUDE.md were updated to a shared assert_synced_duplicate helper that
checks the new expected shape (AGENTS.md content plus exactly one marker
line) instead.

* no-mistakes(review): make CLAUDE.md sync marker CRLF-aware

* no-mistakes(review): keep CLAUDE.md sync marker on its own line

* no-mistakes(document): document CLAUDE.md sync-marker resync contract in architecture.md

* fix(bin): report the sync-marker upgrade and promotion mirror status accurately

Two message-accuracy corrections to bin/fm-ensure-agents-md.sh's CLAUDE.md
sync-marker mechanism, decided by firstmate as message-only fixes with no
new refusal path, guarantee, or subsystem:

- The legacy-duplicate upgrade path (a byte-identical real CLAUDE.md from
  before the sync marker existed) reported "unchanged" even though
  sync_claude() had just rewritten CLAUDE.md's bytes to add the marker.
  Crewmates read this status line, via bin/fm-brief.sh's Project memory
  instruction, to decide whether anything needs committing, so the report
  now distinguishes a real marker-added write ("updated: added the sync
  marker...") from a genuine no-op ("unchanged: ..."), based on whether
  CLAUDE.md already carried the marker before this run touched it.

- The promotion path (an already-real CLAUDE.md gets copied into AGENTS.md
  and marked) permanently opts that file into the auto-resync mechanism
  without saying so: a captain who kept hand-editing CLAUDE.md directly
  after promotion would have those edits silently discarded on the next
  run. The "promoted:" line now states plainly that CLAUDE.md is a managed
  mirror of AGENTS.md from that point on and that future edits should
  target AGENTS.md instead - the promotion behavior itself is unchanged.

Colocated tests updated: the legacy-upgrade test now asserts the accurate
"updated:" report (and that a further re-run correctly reports "unchanged"
once nothing is left to change), and the promotion test asserts the new
managed-mirror wording.

* no-mistakes(review): strip sync marker when promoting CLAUDE.md into AGENTS.md

* no-mistakes(document): document marker adoption and promotion mirror contract

---------

Co-authored-by: Jay Park <jay.jongcheol.park@gmail.com>
Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
Co-authored-by: Roelof Blom <roelof@rb2.nl>
Co-authored-by: ABHISHAKE KUMAR BOJJA <abojja@uvic.ca>
Co-authored-by: Amplify Logic AI <lars@sockinator.co>
Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>
Co-authored-by: Michael Fernandez <michael@…
tiago-peixoto added a commit to tiago-peixoto/firstmate that referenced this pull request Aug 14, 2026
* Add permanent fork main integration workflow

* no-mistakes(review): gate startup upstream probe on validated fork topology

* no-mistakes(test): retire upstream-accepted divergences on re-provable Git evidence

* no-mistakes(document): add fork manifest to shared tracked material list

* fix(fork): make integration workflow actor-safe

* no-mistakes(review): guard empty resolved revert and tighten fork path handling

* no-mistakes(document): correct fork-upstream network-check and fork script doc facts

* no-mistakes(document): name fork-upstream probe in label; deduplicate fork skill

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

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

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

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

* fix(watcher): resurface durable work after downtime

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

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

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

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

* ci: add Windows Herdr automation spike

* ci: run Windows spike on its pull request

* fix: wait for Windows Herdr command output

* fix: run ANSI probe in pane shell

* ci: keep Windows Herdr spike manually triggered

* docs: clarify Windows Herdr spike verdict

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

* Add guided ahoy decision flow

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

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

* Harden stow memory budget policy

* Refine internal stow offload policy

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

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

* fix(spawn): refresh pooled worktree base

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

* no-mistakes: apply CI fixes

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

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

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

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

* no-mistakes: apply CI fixes

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

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

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

* no-mistakes: apply CI fixes

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

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

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

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

* docs(vision): state the experience goal positively

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

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

* fix: reconcile inactive terminal outcomes

* fix: stream secondmate summary inputs

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

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

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

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

* refactor: simplify inactive outcome reconciliation

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* ci: raise Herdr test timeout (#2191)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

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

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

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

Fixes #2109

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: collapse duplicate supervision wakes without losing legitimate updates

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

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

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

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

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

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

* feat(harness): add Cursor Agent CLI adapter

# Conflicts:
#	bin/fm-spawn.sh

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

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

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

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

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

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

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

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

Two consequences wired up:

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

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

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

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

Verified live on cursor-agent 2026.08.11-e8db854.

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

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

Cursor shipped as "unknown cursor-unverified" on the premise that it exposes no
semantic turn lifecycle, only a rendered "Working" footer. That premise is
wrong: cursor-agent persists an append-only JSONL transcript per conversation
and brackets every submitted turn with a role:user open and a typed turn_ended
close. Verified live on 2026.08.11-e8db854, including the interrupt path, where
Escape closes the turn with status "aborted" - so this source covers manual
interruption, which Claude's Stop hook does not.

That makes it a genuine pull source in the muse mould rather than the rendered
text the redesign forbids: no writer, no arm, no gen, nothing seeded that could
never be cleared. Cursor's `ctrl+c to stop` footer stays out of the verdict, and
herdr's narrower native streaming state cannot stand in for it either.

Binding deliberately does not reconstruct cursor's workspace-slug directory
name. That slug collapses path separators, so rebuilding it would be a guess
that could bind the wrong pane; cursor records the exact absolute workspace path
in each project's .workspace-trusted, and the binding matches on that. A
conversation recorded as prior at spawn is excluded, so a relaunch in a reused
worktree folds its own turn rather than its predecessor's. Requiring a unique
remaining conversation keeps zero and several both unknown, because neither
proves anything about the current turn.

The regression pins the fold with real transcript files and asserts the
dangerous direction stays closed: an unresolvable binding, a record-free file,
an unclaimed workspace, and a workspace-path PREFIX all read unknown, never
idle. The prefix case uses an opaque fixture slug so a slug-rebuilding
implementation cannot pass it.

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

* feat(cursor): make the cursor launch runnable and give it lifecycle control

Five gaps that together kept a cursor crewmate from being drivable end to end.

Launch. The template invoked `cursor agent`, but `cursor` is not the CLI - the
installed names are `cursor-agent` and the legacy alias `agent` - so the command
could not run at all on a machine with a normal cursor install. It now resolves
through the verified owner, which also refuses a spawn loudly instead of leaving
a pane that dies with command-not-found and reads as a wedged worker.

Session binding. fm-spawn writes state/<id>.cursor-session so the busy fold can
find this pane's transcript, and teardown removes it.

Lifecycle control. No cursor PR touched fm-control-lib.sh, so
`fm-control <id> interrupt|exit|relaunch` could not drive a cursor worker at
all. Verified live: interrupt is a single Escape, exit is /exit, and cursor does
NOT repollute its composer with the cancelled prompt, so unlike muse it needs no
clear key. Secondmate is refused, matching the spawn refusal.

Submit acknowledgement. cursor parks its terminal cursor outside its composer,
so the composer verdict on tmux is always `unknown` and a submit could never be
acknowledged from the composer alone. The submit core's existing idle-to-busy
transition covers that case, but only if the pane's busy footer is recognised,
so cursor's `ctrl+c to stop` joins the harness-less default union the submit
cores read. The TOKEN is matched rather than the spinner verb: the same version
rendered both `Working` and `Running` in consecutive turns.

Bootstrap. A configured cursor crew harness with no cursor executable is now a
loud MISSING diagnostic rather than a first-spawn failure, and it accepts either
installed name.

Interrupt cancellation is deliberately left unconfirmed. The transcript does
type an aborted close, but its post-interrupt write latency measured as
variable - sometimes seconds, sometimes not within twenty - so a claim built on
it would be unreliable. Normal turn completion is prompt, which is what the busy
fold actually depends on.

Two inherited tests are corrected rather than deleted: the busy test asserted
cursor could have no semantic source, and the launch test pinned the literal
`cursor agent` string. Both now pin the verified behaviour, including that the
launch never allocates a second worktree.

Co-authored-by: ABHISHAKE KUMAR BOJJA <abojja@uvic.ca>
Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* docs(cursor): record the verified crewmate facts and extend the drift guard

The inherited cursor entry was written against 2026.08.04-aaa8809 and several of
its claims no longer hold: it named `cursor agent` as the binary (not the CLI
name), listed six Grok model ids of which the live catalog now returns two, and
recorded busy state, exit, interrupt, and skill invocation as unverified.

Replaced with what was measured against 2026.08.11-e8db854, including the two
facts most likely to be rediscovered painfully: cursor runs as a bundled node
script so its pane title is a bare `node`, and it parks its terminal cursor
outside its composer, which makes the tmux composer verdict permanently
`unknown` by design rather than a defect to chase.

Model ids now route to `--list-models` for the account instead of a fixed list,
since that list is exactly what drifted.

The live drift guard covers cursor, resolving it through the same verified owner
fm-spawn uses and passing --trust so the probe cannot hang on the workspace
prompt. Run against every installed harness: 8 checked, all alive, with cursor
reporting title='node' foreground=[.../cursor-agent] - the drift shape this
guard exists to catch.

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

* docs(agents): record the cursor session-binding state file

The state/ layout section is the inventory every session reads; a busy-source
binding that fm-spawn writes and teardown removes belongs in it alongside muse's.

* no-mistakes(review): Sanitize ambient Cursor marker in harness tests

* no-mistakes(review): Validate Cursor models against live catalog

* no-mistakes(review): Reject unsupported secondmates before binary preflight

* no-mistakes(review): Narrow Cursor ancestry detection to structured process identity

* no-mistakes(review): Parse Cursor transcripts and sanitize inherited markers

* no-mistakes(review): Handle malformed Cursor transcript records safely

* no-mistakes(review): Validate malformed Cursor closes in fallback parser

* no-mistakes(review): Retire stale Cursor bindings during relaunch

* no-mistakes(review): Fix Cursor drift guard command variable

* no-mistakes(review): Narrow Cursor identity to versioned install trees

* no-mistakes(document): Document Cursor harness boundaries

* refactor(composer): move the delivery busy footers to the shared owner

The per-harness rendered busy footers lived in bin/fm-tmux-lib.sh under
FM_TMUX_* names, so cursor's `ctrl+c to stop` signature - and every other
harness's - was reachable only from tmux. That placement was wrong on its own
terms: herdr, zellij, cmux, and orca run the same harnesses and face the same
question these footers answer, which is whether a submitted Enter actually
landed. Nothing about the signature is tmux-specific.

Moved verbatim into bin/fm-composer-lib.sh, the shared composer/delivery owner
every backend already sources, and renamed to FM_DELIVERY_* so the names stop
claiming a scope they never had. All five adapters now reach cursor's signature;
verified per adapter rather than assumed.

The boundary the move must not blur is stated where it now lives: this is a
DELIVERY guard, never a worker-state source. Confirming a keystroke landed is a
different question from asking what a worker is doing, and bin/fm-busy-lib.sh
remains the semantic owner that forbids classifying a harness from rendered
text. Cursor still classifies only from its transcript fold, which is already
backend-agnostic because it folds a file rather than reading a pane - the same
verdict on all six backends.

The old FM_TMUX_* aliases are dropped rather than kept as dead shims: nothing
outside the moved block referenced them except fm-busy-lib.sh's grok fallback,
which now reads the new name. The documented operator override, FM_BUSY_REGEX,
is untouched.

Also removes a dead duplicate CURSOR_INVOKED_AS check in bin/fm-harness.sh,
unreachable behind the marker check above it.

* no-mistakes(review): Correct shared delivery guard ownership references

* no-mistakes(document): Document shared delivery guards and Cursor backend limits

* no-mistakes: apply CI fixes

* fix(composer): bound a bare composer's wrap region at a half-block rule

A live cursor crewmate on herdr classified its IDLE composer as `pending`, and
fm-send consequently exited 1 with "delivery unconfirmed" on a message that had
actually landed. The cause is not cursor-specific.

Herdr draws a composer's top and bottom rules with the half-block glyphs U+2584
and U+2580 rather than the box-drawing family. fm_composer_row_has_edge knew
only the box-drawing set, so no box was detected; the composer was found as a
BARE row, and its wrap region - which extends while rows are non-blank and carry
no structural edge - walked straight through the composer's own closing rule and
swallowed the model and path footer below it. That footer is real text, so the
region classified pending on a genuinely idle pane.

Teaching the shared edge detector the half-block glyphs bounds the region at the
closing rule. Measured on the captured bytes of a real herdr cursor pane: the
same capture that read `pending` now reads `empty`.

This is a shared shape-path change, so it is deliberately narrow - it adds
glyphs to the edge vocabulary and changes no verdict logic - and the whole
composer and backend suite is green, including the other harnesses' herdr
fixtures.

The regression pins the real captured shape and asserts the footer content is
genuinely present, so the case cannot pass vacuously if the region were ever
bounded for some unrelated reason.

* fix(herdr): confirm a cursor submit from the rendered-footer transition

Herdr's composer-shape fix made an idle cursor pane classify `empty`, but
`fm-send` still exited 1 with "delivery unconfirmed" on messages that had
actually landed. Live measurement found the second, independent cause.

Herdr reports a cursor pane `agent_status=blocked` in EVERY state - idle,
mid-turn, and after - so the submit path's idle-baseline native confirmation is
structurally unreachable for cursor and every send falls into the composer
branch. That branch reads cursor's mid-turn composer row, which renders its own
`Add a follow-up` placeholder beside a right-aligned `ctrl+c to stop`. That
token is composer content, so the verdict is `pending` on a composer holding no
user text at all, and the Enter-retry budget then reports pending.

The escape is the same semantic signal the native path uses, read from the
pane's verified busy footer instead of native agent-state, and it is the
rendered-footer twin of the tmux submit core's turn-started confirmation: an
idle-to-busy transition ACROSS our Enter proves the harness accepted the
submission. The baseline is taken before the first Enter and only when the
native baseline was not legibly idle, so the idle-baseline path still never
reads pane content and a pane already mid-turn before we typed keeps reporting
`pending` rather than borrowing another turn as proof of this delivery.

The composer verdict is deliberately NOT relaxed. A right-aligned status token
on the composer row stays content for every other caller, including the
away-mode pre-injection guard, and the shared cursorless submit core is left
untouched so zellij, cmux, and Orca keep the behavior their own follow-up owns.

Verified live on herdr 0.8.0 and cursor-agent 2026.08.11-e8db854 in an isolated
lab session: `fm-send` now exits 0 and the steer executes, interrupt cancels a
running turn, `/exit` stops the agent, and teardown clears the record. All seven
panes of the running default session classify identically before and after the
shape fix, so no other harness regressed.

* no-mistakes(review): Prevent working Herdr baselines from falsely confirming delivery

* no-mistakes(document): Correct Cursor harness and backend documentation

---------

Co-authored-by: ABHISHAKE KUMAR BOJJA <abojja@uvic.ca>
Co-authored-by: Amplify Logic AI <lars@sockinator.co>
Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* fix(bin): require quota-axi 0.1.25 (#2300)

* fix: raise quota-axi floor to 0.1.25 for Cursor CLI quota awareness

Homes on latest main need quota-axi #87 so Desktop-absent CLI machines report a fresh Cursor quota instead of a false sign-in-required.

* no-mistakes(document): Update quota floor documentation pointer

* fix(bin): prevent false Pi watcher alarms during hand-offs (#2304)

* fix(guard): stop the false send-time watcher-down alarm on Pi primaries

On a Pi primary the watcher process is not the liveness signal. The Pi
extension tears the watcher down on every actionable wake and spawns the
replacement itself, so the singleton lock is legitimately unheld between
cycles: every one of the 799 cycles in a live primary's ledger ends with
lock_after=pid:none, and a live capture caught the guard verdict flipping to
no-watcher during one hand-off with the beacon 63s old.

bin/fm-guard.sh classified Pi as a persistent-watcher harness, which demands a
live identity-matched lock holder at all times, so any guarded command landing
in a hand-off painted the full WATCHER DOWN - SUPERVISION IS OFF banner and
told firstmate to repair a cycle the extension already owns and is restoring.

Add an extension supervision model for pi and pi-signed. A live
identity-matched watcher stays the ordinary healthy state; an unheld lock is
healthy only while the beacon is fresh within grace AND a live Pi session
provably owns continuity - both primary extensions recorded in their state
markers at their current on-disk builds by the process named in state/.lock,
with that process still alive. Without that proof the banner fires exactly as
before, so an unloaded, version-drifted, or exited Pi session is loud
immediately and a cycle the extension never restores is loud once the beacon
passes grace. The queued-wake warning, the PID-strict turn-end guard, and
every other primary's detection are untouched.

Fold session-start's duplicate Pi marker predicate into the shared library so
the ownership contract has one owner.

* no-mistakes(review): Restrict Pi hand-off tolerance to unheld watcher locks

* no-mistakes(document): Document Pi watcher hand-off supervision

* feat: support Cursor Agent CLI as a primary harness (#2305)

* feat(cursor): add Cursor Agent CLI primary hooks, park supervision, and session start

Register a tracked project-scope .cursor/hooks.json for Cursor's stop,
sessionStart, preCompact, and preToolUse steps.

bin/fm-turnend-guard-cursor.sh owns Cursor's turn boundary as a park: it
foregrounds the watcher arm, holds the boundary open until an actionable close,
and returns that wake as one follow-up. Exit 2 is a silent no-op on Cursor's
stop step, so the adapter never uses it. The follow-up loop is bounded twice,
by Cursor's own loop_limit and by the payload's loop_count.

bin/fm-sessionstart-cursor.sh delivers the digest as additional_context at
sessionStart, and stages it for the next turn boundary at preCompact, which
cannot inject context.

Cursor also loads the tracked Claude settings, so bin/fm-hook-host-lib.sh lets
each tracked Claude-shaped entrypoint stand down on a Cursor-delivered payload
rather than running every covered event twice.

bin/fm-tmux-lib.sh reclassifies a Cursor pane's composer cursorlessly, because
Cursor parks its terminal cursor outside the composer, which restores a genuine
composer-empty proof and unblocks away-mode escalation delivery.

* feat(cursor): make Cursor Agent CLI a verified primary harness

Resolve Cursor in the session-lock ancestry through bin/fm-cursor-lib.sh, which
a Cursor primary needs before it can hold its own home lock, and classify its
stop-hook park under the autoarm supervision model so the mid-turn pull guard
stops reporting a healthy between-turns watcher as down.

Read a Cursor pane's composer cursorlessly on tmux, gated on Cursor's own
structural process identity, which restores a genuine composer-empty proof and
lets away-mode escalations reach a Cursor primary with no daemon change.

Lift the secondmate refusals in bin/fm-spawn.sh and bin/fm-control-lib.sh now
that the supervision protocol exists and is recorded.

Cover the whole surface with a portable regression over real processes, an
opt-in live guard against the installed cursor-agent, and dated per-harness
evidence.

* docs(cursor): record Cursor as a verified primary across the owning surfaces

Update the turn-end guard, session-start, arm-seatbelt, cd-guard, watcher
continuity, architecture, configuration, README, and harness-adapters owners,
and add dated live evidence to the supervision and runtime-backend verification
records. Correct the recorded Cursor tmux composer verdict: the cursor-anchored
read is still blind, but the composite reader is no longer unknown.

Lift the remaining remote-secondmate refusal missed in the previous commit, and
add the new libs to the existing fixtures that copy a fixed dependency list.

* refactor(cursor): name the park's stand-down condition for both its causes

Also record that Cursor's preCompact firing itself is not yet live-verified,
while the static evidence that it cannot inject context, and the staging path
that follows from it, both are.

* test: give the pretool fixtures their new dependency and one lint owner

The cd-guard fixture copies a fixed dependency list and now needs the shared
hook-host predicate. Both pretool suites also asserted cleanliness with a bare
shellcheck call, a second and weaker copy of the lint definition that
bin/fm-lint.sh owns: it omits --external-sources, so it failed the moment these
checkers sourced a shared library. They now delegate to that owner.

* test: assert the cursor secondmate contract instead of its removed refusal

A cursor secondmate now launches, so the suite asserts what its park actually
needs: --trust so the home's project hooks load at all, its own home pinned as
the workspace, and the autoarm supervision model inherited across the launch.

* no-mistakes(review): Serialize Cursor wakes and bind staged context

* no-mistakes(review): Serialize Cursor context and nag state commits

* no-mistakes(review): Enforce Cursor ceiling before staged context delivery

* no-mistakes(review): Serialize Cursor claims and staged context

* no-mistakes(review): Serialize Cursor ownership and state commits

* no-mistakes(review): Protect Cursor context across session takeover

* no-mistakes(review): Preserve Cursor context across session takeover

* no-mistakes(review): Enforce owner-keyed Cursor staged context

* no-mistakes(review): Atomically claim Cursor follow-ups and staged context

* no-mistakes(review): Defer Cursor preCompact staging and simplify supersession

* no-mistakes(review): Serialize Cursor park commits and defer preCompact

* no-mistakes(review): Stop Cursor parks after session takeover

* no-mistakes(test): Route Cursor preCompact context through stop follow-up

* no-mistakes(document): Update Cursor primary documentation

* revert(cursor): cut preCompact staging from this change

Carrying a compaction digest across two concurrently running stop hooks kept
producing races that could deliver it twice or strand it indefinitely, and
closing them kept enlarging a critical section inside a hook Cursor awaits at
the turn boundary. Native preCompact firing was never observed either, so the
surface has no empirical basis yet.

Remove the adapter, its registration, its staged path in the park, and its
tests, and record the surface as deferred and uncovered alongside the Codex
interactive TUI. A regression now asserts preCompact stays unregistered so it
cannot return without its own design and evidence.

This change ships the proven core only: the turn-end follow-up park, the
run-tier session start, and away-mode delivery.

* no-mistakes(review): Correct Cursor park supersession documentation

* no-mistakes(document): Clarify Cursor run-tier verification ownership

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

---------

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

* feat(bin): add decline and repair paths for decision holds (#2330)

* feat(bin): add unrouted close paths to the captain decision gate

A captain who declines a held decision leaves no follow-up work to route,
so `resolve` could not express that answer: it requires at least one
`--routed-to` task. The only way to close such a hold was a direct
`tasks-axi done`, which never writes the durable resolution record the
completion gate reads, so the originating investigation could no longer
pass `verify` and its cleanup stayed blocked.

Add two close paths that route no work:

- `decline` closes an actively held hold with a recorded captain decision
  and no routed task. It refuses while any task is still blocked by the
  hold, because releasing routed work without recording it is `resolve`'s
  job.
- `repair` records the missing resolution block on a hold that was already
  closed outside this script. It never reopens a hold and never clears a
  dependency edge, and it refuses a hold that is still actively held.

Both require a non-empty captain decision file and share `resolve`'s
digest-based retry identity, so an exact retry is idempotent while a
changed decision is rejected. The recorded body now also names which path
closed the hold, and each routed entry regains its own line.

The gate itself is unchanged: an unanswered decision still fails
completion and blocks teardown, and neither new path can close a hold
without the captain's recorded word.

* fix(bin): require captain-hold provenance before repairing a decision

`repair` checked only that the backlog item was kind captain and Done, so
an ordinary captain-kind task that was never held for the captain could be
closed, repaired, and then pass the completion gate.

tasks-axi keeps `hold_kind` through a close, so it is the surviving proof
that an identity really was a captain hold. Require it before writing the
resolution record, and cover the case in the gate regression.

* no-mistakes(document): Correct decision-hold lifecycle documentation

* fix(bin): surface buried wake status lines once (#2331)

* fix(bin): surface buried status notes on wake drain

A note: answer immediately followed by a routine note was dropped because
annotations kept only the newest line and note: never enters OPEN DECISIONS.
Present every unread note and pending-reply resolution since the last drain
cursor, and annotate every unread line on a queued signal.

* no-mistakes(review): Fix unread status cursor races and overflow

* no-mistakes(review): Preserve cursors when status span reads fail

* no-mistakes(review): Make status presentation transactional under I/O failures

* no-mistakes(review): Simplify unread status cursor and presentation locking

* no-mistakes(review): Align cursor failure regressions with transactional presentation

* no-mistakes(review): Retire stale presentation cursors during task teardown

* no-mistakes(review): Preserve routine status until signal annotation

* no-mistakes(review): Correct unread status cap documentation

* no-mistakes(document): Document unread wake status presentation

* no-mistakes(lint): Fix wake surfacing ShellCheck warnings

* no-mistakes: apply CI fixes

* feat: add max Calm presentation level (#2334)

* feat(calm): add a max presentation level that hides mid-turn working notes

Calm's home-local preference becomes a three-state level instead of a
boolean: "off" is stock Pi, "on" is today's Calm, and "max" is Calm plus
hiding the assistant text of messages the model did not end its response
with. `/calm max` selects it from any state, a plain `/calm` steps max
back to ordinary Calm and otherwise keeps the existing on/off cycle, and
any other argument keeps that cycle too.

`config/calm` now persists "max" as its own literal value, so a session
start, resume, fork, or reload restores the stored level rather than
treating it as unrecognized and dropping to off.

The hide rule keys on Pi's intrinsic per-message stopReason: "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, because suppressing it would also stop a genuine reply from
streaming. The existing assistant layout adapter filters the blocks out
of the same shallow presentation copy it already uses for collapsed
thinking, so the message, model context, session storage, /export, and
delivery are untouched and a hidden mid-turn row collapses to zero
height. The new "assistant-working-note" class keeps that choice in the
visibility policy owner, where ordinary Calm keeps it visible.

* no-mistakes(document): Clarify Calm max persistence and taxonomy

* feat(calm): hide mid-turn working notes by default (#2339)

* feat(calm): make hiding mid-turn working notes the ordinary Calm state

Calm collapses back to the two-state on/off toggle it was before the max
presentation level, with max's hide rule promoted into ordinary Calm.
Calm on now hides mid-turn assistant working notes in addition to what it
already hid, and the /calm command parses no argument again.

The hide rule itself is unchanged: assistant text is removed from the
shallow presentation copy when the message's own stopReason is "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, so a genuine reply still streams. The message, model context,
session storage, /export, and delivery remain untouched.

config/calm persists only "on" and "off" again, but the reader still maps
a persisted "max" to on so a home upgraded from the removed level keeps
Calm on instead of dropping to off.

The mid-turn hide is now default behavior rather than an opt-in level, so
docs/calm.md documents it for users, docs/configuration.md records the
two written values plus the legacy max mapping, and the feasibility
taxonomy drops its level-scoped wording.

* no-mistakes(document): Document ordinary Calm working-note hiding

* chore: store no-mistakes test evidence in the repo (#2355)

* chore: ignore scratchpad/ at the repo root (#2359)

* no-mistakes(review): Fix fork update and divergence lifecycle safeguards

* no-mistakes(review): Guard fork propagation and document daily cadence

* no-mistakes(review): Support nested fork delivery and safe upstream retirement

* no-mistakes(document): Clarify fork-main documentation contracts

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
Co-authored-by: ABHISHAKE KUMAR BOJJA <abojja@uvic.ca>
Co-authored-by: Amplify Logic AI <lars@sockinator.co>
Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>
Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
tiago-peixoto added a commit to tiago-peixoto/firstmate that referenced this pull request Aug 15, 2026
* docs(stow): generalize read-before-write in the public stow skill (#2091)

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

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

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

* fix(watcher): resurface durable work after downtime

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

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

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

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

* ci: add Windows Herdr automation spike

* ci: run Windows spike on its pull request

* fix: wait for Windows Herdr command output

* fix: run ANSI probe in pane shell

* ci: keep Windows Herdr spike manually triggered

* docs: clarify Windows Herdr spike verdict

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

* Add guided ahoy decision flow

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

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

* Harden stow memory budget policy

* Refine internal stow offload policy

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

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

* fix(spawn): refresh pooled worktree base

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

* no-mistakes: apply CI fixes

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

* docs(verification): record the trusted-checkout live matrix rerun

The pipeline's isolated gate worktree is untrusted, so claude, grok, and
muse stopped at first-launch trust dialogs there (the guard refuses to
confirm them by design). This rerun from the trusted checkout at the final
validated head verified all six installed harnesses, the strict blank-row
deferral, and the hardened zellij false-positive probe live.

* no-mistakes(document): Align composer verification evidence

* no-mistakes: apply CI fixes

* no-mistakes(review): Restore proven box bottom-cursor classification

* no-mistakes(review): Preserve styled placeholder-like drafts as pending

* no-mistakes(document): Align composer safety and Zellij delivery documentation

* no-mistakes: apply CI fixes

* docs(verification): refresh the live matrix with the final-head trusted rerun

The post-validation rerun from the trusted checkout verified all six
installed harnesses at the branch's final head, including Claude 2.1.227
(auto-updated since the audit's captures) and Grok, which the untrusted
gate worktree could not verify past their first-launch trust dialogs.

* fix(spawn): gate Pi TUI mode by CLI capability (#2117)

* fix(spawn): gate Pi regular TUI flag by capability

* no-mistakes(review): Document conditional Pi TUI capability detection

* no-mistakes(review): Pin Pi probing and launch to one executable

* no-mistakes(review): Preserve literal pinned Pi paths and update documentation

* no-mistakes(review): Defer pinned Pi path insertion until final substitution

* no-mistakes(document): Document version-safe Pi launch probing

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* docs(vision): elevate experience, pain narrative, and distro virtues (#2147)

* docs(vision): elevate experience, pain narrative, and distro virtues

Fold the captain's public vision framing into VISION.md: peace of mind as a
primary goal, multi-session context-switch pain as the problem one interface
solves, clone-and-run setup ease, self-evolution including community, and
explicit harness/backend orthogonality. Reconcile experience-as-garnish into
experience-as-purpose and update aligns/resists accordingly.

* docs(vision): state the experience goal positively

Drop the negative "not a smart workflow / useful tool / impressive technology"
pretext. Lead straight into the positive experience north star.

* feat(bin): reconcile inactive terminal crew outcomes (#2167)

* fix: reconcile inactive terminal outcomes

* fix: stream secondmate summary inputs

* no-mistakes(review): Fix reconciliation locking and request delivery retries

* no-mistakes(review): Prevent retries after unknown request delivery

* no-mistakes(document): Clarify inactive reconciliation cadence and receipts

* no-mistakes(lint): Quote terminal status arguments in reconciliation tests

* refactor: simplify inactive outcome reconciliation

* no-mistakes(review): Bound inactive reconciliation scans with durable progress

* no-mistakes(review): Bound reconciliation and deduplicate recovery notices

* no-mistakes(document): Document inactive outcome reconciliation contracts

* no-mistakes(review): Reject relative local secondmate parent routes

* no-mistakes(review): Key terminal receipts by spawn incarnation

* no-mistakes(review): Stabilize legacy receipts and lock reconciliation snapshots

* no-mistakes(review): Fail closed on invalid secondmate identity markers

* no-mistakes(document): Document durable inactive-outcome reconciliation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* ci: raise Herdr test timeout (#2191)

* fix: refresh stale Pi instructions after compaction (#2163)

* fix(session-start): refresh drifted instructions on stale rebuilds

* test(session-start): prove Pi instruction refresh end to end

* no-mistakes(review): Fix stale instruction refresh and baseline integrity

* no-mistakes(review): Preserve true-start baselines across Pi continuations

* no-mistakes(review): Correct Pi continuation classification and live expectation

* no-mistakes(review): Correct Pi continuation coverage documentation

* no-mistakes(review): Fix read-only refresh and exact Pi session restores

* no-mistakes(review): Classify Pi create-if-missing sessions correctly

* no-mistakes(review): Classify named Pi sessions using immutable headers

* no-mistakes(review): Correct Codex interactive coverage diagnostic

* no-mistakes(document): Document immutable Pi compaction instruction refresh

* no-mistakes(document): Correct Pi refresh documentation and validation claims

* feat: add deterministic condition-to-action watcher (#2200)

* feat(bin): add deterministic condition->action watch adapter on the process-event channel

Register a (condition, action) pair once with bin/fm-procevent-when.sh and the
existing process-to-event runner polls the condition tokenlessly, fires the
action at most once on a stable true, and wakes firstmate exactly once with the
captured outcome - instead of burning an agent turn per re-check.

The pair is stored privately under state/when/ and hash-bound by a trust record
the same way fm-check-register.sh binds a custom check, so a mutated spec is
refused without executing anything. A durable exclusive fired marker claimed
before the action makes restarts and re-polls unable to double-fire; every
failure path (mutated spec, condition error past budget, expired deadline,
failed action, uncaptured earlier fire) ends in a terminal captured outcome
that wakes firstmate rather than a silent retry. Eligibility stays a firstmate
judgment: only exact, safe, reversible actions may be bound, and judgment-
needing or destructive actions keep the wake-and-decide flow.

* no-mistakes(review): Harden when watcher concurrency, deadlines, timeouts, and output

* no-mistakes(test): Bind watcher actions to registered executable bytes

* no-mistakes(document): Correct condition-action watcher documentation

* no-mistakes(document): Clarify outcome wake re-announcement

* no-mistakes: apply CI fixes

* fix(bin): honor a decision key stated after the verb colon (#2202)

The open-decisions fold only recognized a [key=<slug>] token between the
verb and the colon (needs-decision [key=x]: note). The common worker
shape with the colon first (needs-decision: [key=x] note) silently
folded its stated key into the shared "default" bucket, so two open
decisions could collapse into one record and fm-send --resolve-key <x>
refused to close the decision it plainly named.

A complete token at the head of the note is now an equivalent stated-key
position for every keyed verb, shared by the whole-file and incremental
folds through the one _fm_decision_key owner. The documented
before-colon position wins when both are present, a token deeper in the
note stays prose, a bare keyless line still folds to "default", and a
stated-but-malformed slug is rejected rather than rewritten to
"default". A consumed note-head token is stripped from the note so both
positions yield identical records, and the incremental fold version is
bumped so persisted cursors folded under the old interpretation are
rebuilt from the authoritative log.

Fixes #2109

* fix(bin): prevent watcher recovery acknowledgement livelock (#2212)

* fix(bin): keep a recovery acknowledgement valid across republication

A watcher cycle that opened and closed while the model handled its drained
wakes minted a fresh recovery generation, which invalidated the exact
acknowledgement the drain had just printed. That acknowledgement then consumed
nothing, so the marker stayed pending and every later arm spent its whole cycle
re-announcing the same recovery instead of supervising - a livelock the home
could not leave on its own.

A downtime publication now reuses the generation of an outstanding handling
episode, so a close during the handling window cannot orphan the printed
acknowledgement. The acknowledgement itself separates its two facts: queue-row
consumption is bound to the monotonic --ack-through sequence and always
happens, while only retiring the episode is bound to --recovery-generation. A
generation that moved on is a non-fatal result that names its own remedy
instead of a refusal that consumes nothing.

* no-mistakes(review): Preserve recovery generations and consume stale acknowledgements safely

* no-mistakes(document): Document sequence-bound recovery acknowledgements

* feat(fmx-respond): consume Relay conversation chains (#2206)

* feat(fmx-respond): consume in_reply_to_chain conversation context

The relay's poll payload can carry in_reply_to_chain, an oldest-first
transcript of the surrounding conversation, but the mention-handling
procedure only ever read the immediate in_reply_to parent, so referents
like "this" in a standalone mention stayed unresolvable even when
context was delivered.

Teach fmx-respond to read the chain when present (optional and
backward-compatible: often absent today, kind label not required),
resolve referents against the whole transcript, and extend the
untrusted-content framing to every chain entry including the upcoming
kind=history entries. Document the field's wire shape in
docs/configuration.md as the firstmate-side owner.

* no-mistakes(document): Document Relay chain context ownership

* fix: parse decision verbs before status metadata tags (#2280)

* fix(bin): strip every bracket tag, not just [key=...], from a status verb

status_line_verb only stripped a leading "[key=...]" token before the
colon, so a remote secondmate reply's leading "[corr=...]" correlation
tag stayed glued onto the returned verb word ("needs-decision
[corr=...]" instead of "needs-decision"). The open-decisions fold's
verb match then silently failed to recognize the line at all, so
fm-send --resolve-key refused to close a decision that was plainly
open on the status line.

Generalize the parser to strip every "[name=value]" tag before the
colon, in any order and count, so local and remote replies fold
identically.

* no-mistakes(review): Invalidate stale decision cursors after parser fix

* no-mistakes(document): Clarify status metadata verb parsing

* fix(bin): collapse duplicate supervision wakes (#2287)

* fix: collapse duplicate supervision wakes without losing legitimate updates

One remote-secondmate note produced two handling turns (a procevent check
wake published before autohandle, then a signal wake for the same mirrored
bytes), already-ingested replays such as a cursor-loss whole-log recapture
still woke with nothing to do, this home's own bookkeeping closes (fm-send
--resolve-key, the pending-reply escalation close, the captain-held
transfer) re-woke the session that wrote them, and turn-ended-only wakes
were annotated with already-announced status lines that looked like fresh
progress.

Dedup rules, each at its layer's one owner:
- fm-procevent.sh: an adapter may declare 'self-announcing'; the runner
  then applies first and publishes a check wake only for what remains
  unhandled. fm-procevent-remote-reply.sh declares it: the mirrored status
  append is the single announcement, so a fully applied capture publishes
  nothing and a byte-identical replay stays completely quiet. All other
  adapters keep strict publish-before-apply.
- fm-wake-lib.sh: fm_wake_signal_sig/seen_path/seen_current now own the
  watcher's signal signature and .seen-* marker format, plus
  fm_wake_status_append_self_announced, the guarded bookkeeping append
  that advances the marker only over exactly its own bytes and fails
  toward waking on any pending or interleaved foreign write.
- fm-send.sh, fm-pending-reply-lib.sh, fm-decision-hold.sh: bookkeeping
  closes go through that guarded append; escalation opens stay plain
  appends because a new blocker must wake.
- fm-wake-lib.sh annotations: a historical (turn-ended-only) row skips its
  status annotation only when the file's signature provably matches the
  seen marker; anything unannounced keeps annotating.
- fm-classify-lib.sh: a kind=secondmate task's status signal is never
  absorbed as provably-working, because that stream is the routed-reply
  channel the parent must read.

Also fixes a pre-existing exit-path deadlock the regression run reproduced:
a TERM inside a recovery-marker critical section left fm_lock_try_acquire
spinning against this same process's abandoned hold; a self-held lock is
now reclaimed (a subshell still waits on its parent's live hold).

Regression tests drive the real wake functions and executables in both
directions: each duplicate case collapses, while a new remote reply, new
decision, new blocker, merge result, failure, first status change, and a
later different note on the same task all still wake.

* no-mistakes(document): Document wake deduplication contracts

* feat: add Cursor CLI crew harness (#2238)

* feat(harness): add Cursor Agent CLI adapter

# Conflicts:
#	bin/fm-spawn.sh

* fix(composer): read cursor-agent's reverse-video placeholder as idle

cursor-agent renders its idle composer placeholder dim (SGR 2) but paints the
cell under the terminal cursor in reverse video (SGR 0;7). Reverse video is
neither dim nor a dark truecolor foreground, so the shared ghost stripper keeps
that one character and an idle composer reduces to a lone `P`. Judged on its
own, that remnant reads `pending` on a genuinely idle pane, which defers
away-mode escalation indefinitely on the styled cursorless backends.

Teach the ONE fleet-wide classifier the shape instead of adding an adapter-local
copy: register `→` as an agent prompt glyph so the composer row is structurally
findable at all (without it the bottom-most shape is a stale shell prompt echo
in the scrollback), add both verified placeholders to the idle set, and consult
the styling-independent plain row when the styled row is only a remnant.

The plain-row branch demands the remnant be a proper, strictly shorter substring
of a plain row matching a fully anchored placeholder. Real typed text is
uniformly bright, so stripping leaves it equal to the plain row and it stays
`pending` - verified live against a pane where the typed text was exactly the
placeholder string.

Verified live on cursor-agent 2026.08.11-e8db854; the regression pins the real
captured bytes and asserts the remnant survives stripping, so the case cannot go
vacuous if the stripper later learns SGR 7.

Co-authored-by: Amplify Logic AI <lars@sockinator.co>

* feat(cursor): narrow cursor identity and order its marker before CLAUDECODE

Cursor ships two executable names - `cursor-agent` and the legacy alias `agent`
- and runs as a bundled node script, so tmux reports the pane command as a bare
`node`. Neither `agent` nor `node` can be trusted by name, so identity gets one
owner in bin/fm-cursor-lib.sh that demands cursor's own name or install tree in
the path or argv[0], from the structural signal only. Probing an arbitrary pid's
executable during a liveness poll would execute a stranger's binary, which is
the hazard that rule exists to close.

Two consequences wired up:

Detection. cursor-agent does NOT clear an inherited CLAUDECODE, so a cursor
worker launched under a claude primary carries both markers and whichever is
tested first wins. The cursor markers are ordered ahead of the CLAUDECODE check;
fm-spawn additionally clears foreign markers at the launch boundary. Both are
kept deliberately - launch sanitization only covers sessions fm-spawn started,
while the ordering also covers a cursor session started by hand. Verified live
that CURSOR_INVOKED_AS is set on the agent process and CURSOR_AGENT=1 on the
child/tool processes fm-harness.sh actually runs as.

Pane liveness. A cursor pane now classifies `agent`. An unrelated node or agent
stays `other`, which the liveness callers already fold into `ambiguous` rather
than `dead`, so a stranger's node pane is never reported agent-free.

Resolution prints the STABLE launcher rather than the canonical target: identity
is proven through canonicalization, but cursor's canonical path carries a
version its own auto-update replaces, and pinning that would strand a task on a
version that can vanish.

The regression drives the two identity signals apart - a cursor-named executable
outside any cursor tree, and a non-cursor-named alias inside one - and asserts
each carries a verdict alone, so no single vendor string is load-bearing. Its
negative controls are real spawned processes, not fixtures.

Verified live on cursor-agent 2026.08.11-e8db854.

Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* feat(cursor): classify cursor busy state from its own turn transcript

Cursor shipped as "unknown cursor-unverified" on the premise that it exposes no
semantic turn lifecycle, only a rendered "Working" footer. That premise is
wrong: cursor-agent persists an append-only JSONL transcript per conversation
and brackets every submitted turn with a role:user open and a typed turn_ended
close. Verified live on 2026.08.11-e8db854, including the interrupt path, where
Escape closes the turn with status "aborted" - so this source covers manual
interruption, which Claude's Stop hook does not.

That makes it a genuine pull source in the muse mould rather than the rendered
text the redesign forbids: no writer, no arm, no gen, nothing seeded that could
never be cleared. Cursor's `ctrl+c to stop` footer stays out of the verdict, and
herdr's narrower native streaming state cannot stand in for it either.

Binding deliberately does not reconstruct cursor's workspace-slug directory
name. That slug collapses path separators, so rebuilding it would be a guess
that could bind the wrong pane; cursor records the exact absolute workspace path
in each project's .workspace-trusted, and the binding matches on that. A
conversation recorded as prior at spawn is excluded, so a relaunch in a reused
worktree folds its own turn rather than its predecessor's. Requiring a unique
remaining conversation keeps zero and several both unknown, because neither
proves anything about the current turn.

The regression pins the fold with real transcript files and asserts the
dangerous direction stays closed: an unresolvable binding, a record-free file,
an unclaimed workspace, and a workspace-path PREFIX all read unknown, never
idle. The prefix case uses an opaque fixture slug so a slug-rebuilding
implementation cannot pass it.

Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* feat(cursor): make the cursor launch runnable and give it lifecycle control

Five gaps that together kept a cursor crewmate from being drivable end to end.

Launch. The template invoked `cursor agent`, but `cursor` is not the CLI - the
installed names are `cursor-agent` and the legacy alias `agent` - so the command
could not run at all on a machine with a normal cursor install. It now resolves
through the verified owner, which also refuses a spawn loudly instead of leaving
a pane that dies with command-not-found and reads as a wedged worker.

Session binding. fm-spawn writes state/<id>.cursor-session so the busy fold can
find this pane's transcript, and teardown removes it.

Lifecycle control. No cursor PR touched fm-control-lib.sh, so
`fm-control <id> interrupt|exit|relaunch` could not drive a cursor worker at
all. Verified live: interrupt is a single Escape, exit is /exit, and cursor does
NOT repollute its composer with the cancelled prompt, so unlike muse it needs no
clear key. Secondmate is refused, matching the spawn refusal.

Submit acknowledgement. cursor parks its terminal cursor outside its composer,
so the composer verdict on tmux is always `unknown` and a submit could never be
acknowledged from the composer alone. The submit core's existing idle-to-busy
transition covers that case, but only if the pane's busy footer is recognised,
so cursor's `ctrl+c to stop` joins the harness-less default union the submit
cores read. The TOKEN is matched rather than the spinner verb: the same version
rendered both `Working` and `Running` in consecutive turns.

Bootstrap. A configured cursor crew harness with no cursor executable is now a
loud MISSING diagnostic rather than a first-spawn failure, and it accepts either
installed name.

Interrupt cancellation is deliberately left unconfirmed. The transcript does
type an aborted close, but its post-interrupt write latency measured as
variable - sometimes seconds, sometimes not within twenty - so a claim built on
it would be unreliable. Normal turn completion is prompt, which is what the busy
fold actually depends on.

Two inherited tests are corrected rather than deleted: the busy test asserted
cursor could have no semantic source, and the launch test pinned the literal
`cursor agent` string. Both now pin the verified behaviour, including that the
launch never allocates a second worktree.

Co-authored-by: ABHISHAKE KUMAR BOJJA <abojja@uvic.ca>
Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* docs(cursor): record the verified crewmate facts and extend the drift guard

The inherited cursor entry was written against 2026.08.04-aaa8809 and several of
its claims no longer hold: it named `cursor agent` as the binary (not the CLI
name), listed six Grok model ids of which the live catalog now returns two, and
recorded busy state, exit, interrupt, and skill invocation as unverified.

Replaced with what was measured against 2026.08.11-e8db854, including the two
facts most likely to be rediscovered painfully: cursor runs as a bundled node
script so its pane title is a bare `node`, and it parks its terminal cursor
outside its composer, which makes the tmux composer verdict permanently
`unknown` by design rather than a defect to chase.

Model ids now route to `--list-models` for the account instead of a fixed list,
since that list is exactly what drifted.

The live drift guard covers cursor, resolving it through the same verified owner
fm-spawn uses and passing --trust so the probe cannot hang on the workspace
prompt. Run against every installed harness: 8 checked, all alive, with cursor
reporting title='node' foreground=[.../cursor-agent] - the drift shape this
guard exists to catch.

Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* docs(agents): record the cursor session-binding state file

The state/ layout section is the inventory every session reads; a busy-source
binding that fm-spawn writes and teardown removes belongs in it alongside muse's.

* no-mistakes(review): Sanitize ambient Cursor marker in harness tests

* no-mistakes(review): Validate Cursor models against live catalog

* no-mistakes(review): Reject unsupported secondmates before binary preflight

* no-mistakes(review): Narrow Cursor ancestry detection to structured process identity

* no-mistakes(review): Parse Cursor transcripts and sanitize inherited markers

* no-mistakes(review): Handle malformed Cursor transcript records safely

* no-mistakes(review): Validate malformed Cursor closes in fallback parser

* no-mistakes(review): Retire stale Cursor bindings during relaunch

* no-mistakes(review): Fix Cursor drift guard command variable

* no-mistakes(review): Narrow Cursor identity to versioned install trees

* no-mistakes(document): Document Cursor harness boundaries

* refactor(composer): move the delivery busy footers to the shared owner

The per-harness rendered busy footers lived in bin/fm-tmux-lib.sh under
FM_TMUX_* names, so cursor's `ctrl+c to stop` signature - and every other
harness's - was reachable only from tmux. That placement was wrong on its own
terms: herdr, zellij, cmux, and orca run the same harnesses and face the same
question these footers answer, which is whether a submitted Enter actually
landed. Nothing about the signature is tmux-specific.

Moved verbatim into bin/fm-composer-lib.sh, the shared composer/delivery owner
every backend already sources, and renamed to FM_DELIVERY_* so the names stop
claiming a scope they never had. All five adapters now reach cursor's signature;
verified per adapter rather than assumed.

The boundary the move must not blur is stated where it now lives: this is a
DELIVERY guard, never a worker-state source. Confirming a keystroke landed is a
different question from asking what a worker is doing, and bin/fm-busy-lib.sh
remains the semantic owner that forbids classifying a harness from rendered
text. Cursor still classifies only from its transcript fold, which is already
backend-agnostic because it folds a file rather than reading a pane - the same
verdict on all six backends.

The old FM_TMUX_* aliases are dropped rather than kept as dead shims: nothing
outside the moved block referenced them except fm-busy-lib.sh's grok fallback,
which now reads the new name. The documented operator override, FM_BUSY_REGEX,
is untouched.

Also removes a dead duplicate CURSOR_INVOKED_AS check in bin/fm-harness.sh,
unreachable behind the marker check above it.

* no-mistakes(review): Correct shared delivery guard ownership references

* no-mistakes(document): Document shared delivery guards and Cursor backend limits

* no-mistakes: apply CI fixes

* fix(composer): bound a bare composer's wrap region at a half-block rule

A live cursor crewmate on herdr classified its IDLE composer as `pending`, and
fm-send consequently exited 1 with "delivery unconfirmed" on a message that had
actually landed. The cause is not cursor-specific.

Herdr draws a composer's top and bottom rules with the half-block glyphs U+2584
and U+2580 rather than the box-drawing family. fm_composer_row_has_edge knew
only the box-drawing set, so no box was detected; the composer was found as a
BARE row, and its wrap region - which extends while rows are non-blank and carry
no structural edge - walked straight through the composer's own closing rule and
swallowed the model and path footer below it. That footer is real text, so the
region classified pending on a genuinely idle pane.

Teaching the shared edge detector the half-block glyphs bounds the region at the
closing rule. Measured on the captured bytes of a real herdr cursor pane: the
same capture that read `pending` now reads `empty`.

This is a shared shape-path change, so it is deliberately narrow - it adds
glyphs to the edge vocabulary and changes no verdict logic - and the whole
composer and backend suite is green, including the other harnesses' herdr
fixtures.

The regression pins the real captured shape and asserts the footer content is
genuinely present, so the case cannot pass vacuously if the region were ever
bounded for some unrelated reason.

* fix(herdr): confirm a cursor submit from the rendered-footer transition

Herdr's composer-shape fix made an idle cursor pane classify `empty`, but
`fm-send` still exited 1 with "delivery unconfirmed" on messages that had
actually landed. Live measurement found the second, independent cause.

Herdr reports a cursor pane `agent_status=blocked` in EVERY state - idle,
mid-turn, and after - so the submit path's idle-baseline native confirmation is
structurally unreachable for cursor and every send falls into the composer
branch. That branch reads cursor's mid-turn composer row, which renders its own
`Add a follow-up` placeholder beside a right-aligned `ctrl+c to stop`. That
token is composer content, so the verdict is `pending` on a composer holding no
user text at all, and the Enter-retry budget then reports pending.

The escape is the same semantic signal the native path uses, read from the
pane's verified busy footer instead of native agent-state, and it is the
rendered-footer twin of the tmux submit core's turn-started confirmation: an
idle-to-busy transition ACROSS our Enter proves the harness accepted the
submission. The baseline is taken before the first Enter and only when the
native baseline was not legibly idle, so the idle-baseline path still never
reads pane content and a pane already mid-turn before we typed keeps reporting
`pending` rather than borrowing another turn as proof of this delivery.

The composer verdict is deliberately NOT relaxed. A right-aligned status token
on the composer row stays content for every other caller, including the
away-mode pre-injection guard, and the shared cursorless submit core is left
untouched so zellij, cmux, and Orca keep the behavior their own follow-up owns.

Verified live on herdr 0.8.0 and cursor-agent 2026.08.11-e8db854 in an isolated
lab session: `fm-send` now exits 0 and the steer executes, interrupt cancels a
running turn, `/exit` stops the agent, and teardown clears the record. All seven
panes of the running default session classify identically before and after the
shape fix, so no other harness regressed.

* no-mistakes(review): Prevent working Herdr baselines from falsely confirming delivery

* no-mistakes(document): Correct Cursor harness and backend documentation

---------

Co-authored-by: ABHISHAKE KUMAR BOJJA <abojja@uvic.ca>
Co-authored-by: Amplify Logic AI <lars@sockinator.co>
Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* fix(bin): require quota-axi 0.1.25 (#2300)

* fix: raise quota-axi floor to 0.1.25 for Cursor CLI quota awareness

Homes on latest main need quota-axi #87 so Desktop-absent CLI machines report a fresh Cursor quota instead of a false sign-in-required.

* no-mistakes(document): Update quota floor documentation pointer

* fix(bin): prevent false Pi watcher alarms during hand-offs (#2304)

* fix(guard): stop the false send-time watcher-down alarm on Pi primaries

On a Pi primary the watcher process is not the liveness signal. The Pi
extension tears the watcher down on every actionable wake and spawns the
replacement itself, so the singleton lock is legitimately unheld between
cycles: every one of the 799 cycles in a live primary's ledger ends with
lock_after=pid:none, and a live capture caught the guard verdict flipping to
no-watcher during one hand-off with the beacon 63s old.

bin/fm-guard.sh classified Pi as a persistent-watcher harness, which demands a
live identity-matched lock holder at all times, so any guarded command landing
in a hand-off painted the full WATCHER DOWN - SUPERVISION IS OFF banner and
told firstmate to repair a cycle the extension already owns and is restoring.

Add an extension supervision model for pi and pi-signed. A live
identity-matched watcher stays the ordinary healthy state; an unheld lock is
healthy only while the beacon is fresh within grace AND a live Pi session
provably owns continuity - both primary extensions recorded in their state
markers at their current on-disk builds by the process named in state/.lock,
with that process still alive. Without that proof the banner fires exactly as
before, so an unloaded, version-drifted, or exited Pi session is loud
immediately and a cycle the extension never restores is loud once the beacon
passes grace. The queued-wake warning, the PID-strict turn-end guard, and
every other primary's detection are untouched.

Fold session-start's duplicate Pi marker predicate into the shared library so
the ownership contract has one owner.

* no-mistakes(review): Restrict Pi hand-off tolerance to unheld watcher locks

* no-mistakes(document): Document Pi watcher hand-off supervision

* feat: support Cursor Agent CLI as a primary harness (#2305)

* feat(cursor): add Cursor Agent CLI primary hooks, park supervision, and session start

Register a tracked project-scope .cursor/hooks.json for Cursor's stop,
sessionStart, preCompact, and preToolUse steps.

bin/fm-turnend-guard-cursor.sh owns Cursor's turn boundary as a park: it
foregrounds the watcher arm, holds the boundary open until an actionable close,
and returns that wake as one follow-up. Exit 2 is a silent no-op on Cursor's
stop step, so the adapter never uses it. The follow-up loop is bounded twice,
by Cursor's own loop_limit and by the payload's loop_count.

bin/fm-sessionstart-cursor.sh delivers the digest as additional_context at
sessionStart, and stages it for the next turn boundary at preCompact, which
cannot inject context.

Cursor also loads the tracked Claude settings, so bin/fm-hook-host-lib.sh lets
each tracked Claude-shaped entrypoint stand down on a Cursor-delivered payload
rather than running every covered event twice.

bin/fm-tmux-lib.sh reclassifies a Cursor pane's composer cursorlessly, because
Cursor parks its terminal cursor outside the composer, which restores a genuine
composer-empty proof and unblocks away-mode escalation delivery.

* feat(cursor): make Cursor Agent CLI a verified primary harness

Resolve Cursor in the session-lock ancestry through bin/fm-cursor-lib.sh, which
a Cursor primary needs before it can hold its own home lock, and classify its
stop-hook park under the autoarm supervision model so the mid-turn pull guard
stops reporting a healthy between-turns watcher as down.

Read a Cursor pane's composer cursorlessly on tmux, gated on Cursor's own
structural process identity, which restores a genuine composer-empty proof and
lets away-mode escalations reach a Cursor primary with no daemon change.

Lift the secondmate refusals in bin/fm-spawn.sh and bin/fm-control-lib.sh now
that the supervision protocol exists and is recorded.

Cover the whole surface with a portable regression over real processes, an
opt-in live guard against the installed cursor-agent, and dated per-harness
evidence.

* docs(cursor): record Cursor as a verified primary across the owning surfaces

Update the turn-end guard, session-start, arm-seatbelt, cd-guard, watcher
continuity, architecture, configuration, README, and harness-adapters owners,
and add dated live evidence to the supervision and runtime-backend verification
records. Correct the recorded Cursor tmux composer verdict: the cursor-anchored
read is still blind, but the composite reader is no longer unknown.

Lift the remaining remote-secondmate refusal missed in the previous commit, and
add the new libs to the existing fixtures that copy a fixed dependency list.

* refactor(cursor): name the park's stand-down condition for both its causes

Also record that Cursor's preCompact firing itself is not yet live-verified,
while the static evidence that it cannot inject context, and the staging path
that follows from it, both are.

* test: give the pretool fixtures their new dependency and one lint owner

The cd-guard fixture copies a fixed dependency list and now needs the shared
hook-host predicate. Both pretool suites also asserted cleanliness with a bare
shellcheck call, a second and weaker copy of the lint definition that
bin/fm-lint.sh owns: it omits --external-sources, so it failed the moment these
checkers sourced a shared library. They now delegate to that owner.

* test: assert the cursor secondmate contract instead of its removed refusal

A cursor secondmate now launches, so the suite asserts what its park actually
needs: --trust so the home's project hooks load at all, its own home pinned as
the workspace, and the autoarm supervision model inherited across the launch.

* no-mistakes(review): Serialize Cursor wakes and bind staged context

* no-mistakes(review): Serialize Cursor context and nag state commits

* no-mistakes(review): Enforce Cursor ceiling before staged context delivery

* no-mistakes(review): Serialize Cursor claims and staged context

* no-mistakes(review): Serialize Cursor ownership and state commits

* no-mistakes(review): Protect Cursor context across session takeover

* no-mistakes(review): Preserve Cursor context across session takeover

* no-mistakes(review): Enforce owner-keyed Cursor staged context

* no-mistakes(review): Atomically claim Cursor follow-ups and staged context

* no-mistakes(review): Defer Cursor preCompact staging and simplify supersession

* no-mistakes(review): Serialize Cursor park commits and defer preCompact

* no-mistakes(review): Stop Cursor parks after session takeover

* no-mistakes(test): Route Cursor preCompact context through stop follow-up

* no-mistakes(document): Update Cursor primary documentation

* revert(cursor): cut preCompact staging from this change

Carrying a compaction digest across two concurrently running stop hooks kept
producing races that could deliver it twice or strand it indefinitely, and
closing them kept enlarging a critical section inside a hook Cursor awaits at
the turn boundary. Native preCompact firing was never observed either, so the
surface has no empirical basis yet.

Remove the adapter, its registration, its staged path in the park, and its
tests, and record the surface as deferred and uncovered alongside the Codex
interactive TUI. A regression now asserts preCompact stays unregistered so it
cannot return without its own design and evidence.

This change ships the proven core only: the turn-end follow-up park, the
run-tier session start, and away-mode delivery.

* no-mistakes(review): Correct Cursor park supersession documentation

* no-mistakes(document): Clarify Cursor run-tier verification ownership

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

---------

Co-authored-by: kunchenguid <kun-1@kunchenguid.com>

* feat(bin): add decline and repair paths for decision holds (#2330)

* feat(bin): add unrouted close paths to the captain decision gate

A captain who declines a held decision leaves no follow-up work to route,
so `resolve` could not express that answer: it requires at least one
`--routed-to` task. The only way to close such a hold was a direct
`tasks-axi done`, which never writes the durable resolution record the
completion gate reads, so the originating investigation could no longer
pass `verify` and its cleanup stayed blocked.

Add two close paths that route no work:

- `decline` closes an actively held hold with a recorded captain decision
  and no routed task. It refuses while any task is still blocked by the
  hold, because releasing routed work without recording it is `resolve`'s
  job.
- `repair` records the missing resolution block on a hold that was already
  closed outside this script. It never reopens a hold and never clears a
  dependency edge, and it refuses a hold that is still actively held.

Both require a non-empty captain decision file and share `resolve`'s
digest-based retry identity, so an exact retry is idempotent while a
changed decision is rejected. The recorded body now also names which path
closed the hold, and each routed entry regains its own line.

The gate itself is unchanged: an unanswered decision still fails
completion and blocks teardown, and neither new path can close a hold
without the captain's recorded word.

* fix(bin): require captain-hold provenance before repairing a decision

`repair` checked only that the backlog item was kind captain and Done, so
an ordinary captain-kind task that was never held for the captain could be
closed, repaired, and then pass the completion gate.

tasks-axi keeps `hold_kind` through a close, so it is the surviving proof
that an identity really was a captain hold. Require it before writing the
resolution record, and cover the case in the gate regression.

* no-mistakes(document): Correct decision-hold lifecycle documentation

* fix(bin): surface buried wake status lines once (#2331)

* fix(bin): surface buried status notes on wake drain

A note: answer immediately followed by a routine note was dropped because
annotations kept only the newest line and note: never enters OPEN DECISIONS.
Present every unread note and pending-reply resolution since the last drain
cursor, and annotate every unread line on a queued signal.

* no-mistakes(review): Fix unread status cursor races and overflow

* no-mistakes(review): Preserve cursors when status span reads fail

* no-mistakes(review): Make status presentation transactional under I/O failures

* no-mistakes(review): Simplify unread status cursor and presentation locking

* no-mistakes(review): Align cursor failure regressions with transactional presentation

* no-mistakes(review): Retire stale presentation cursors during task teardown

* no-mistakes(review): Preserve routine status until signal annotation

* no-mistakes(review): Correct unread status cap documentation

* no-mistakes(document): Document unread wake status presentation

* no-mistakes(lint): Fix wake surfacing ShellCheck warnings

* no-mistakes: apply CI fixes

* feat: add max Calm presentation level (#2334)

* feat(calm): add a max presentation level that hides mid-turn working notes

Calm's home-local preference becomes a three-state level instead of a
boolean: "off" is stock Pi, "on" is today's Calm, and "max" is Calm plus
hiding the assistant text of messages the model did not end its response
with. `/calm max` selects it from any state, a plain `/calm` steps max
back to ordinary Calm and otherwise keeps the existing on/off cycle, and
any other argument keeps that cycle too.

`config/calm` now persists "max" as its own literal value, so a session
start, resume, fork, or reload restores the stored level rather than
treating it as unrecognized and dropping to off.

The hide rule keys on Pi's intrinsic per-message stopReason: "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, because suppressing it would also stop a genuine reply from
streaming. The existing assistant layout adapter filters the blocks out
of the same shallow presentation copy it already uses for collapsed
thinking, so the message, model context, session storage, /export, and
delivery are untouched and a hidden mid-turn row collapses to zero
height. The new "assistant-working-note" class keeps that choice in the
visibility policy owner, where ordinary Calm keeps it visible.

* no-mistakes(document): Clarify Calm max persistence and taxonomy

* feat(calm): hide mid-turn working notes by default (#2339)

* feat(calm): make hiding mid-turn working notes the ordinary Calm state

Calm collapses back to the two-state on/off toggle it was before the max
presentation level, with max's hide rule promoted into ordinary Calm.
Calm on now hides mid-turn assistant working notes in addition to what it
already hid, and the /calm command parses no argument again.

The hide rule itself is unchanged: assistant text is removed from the
shallow presentation copy when the message's own stopReason is "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, so a genuine reply still streams. The message, model context,
session storage, /export, and delivery remain untouched.

config/calm persists only "on" and "off" again, but the reader still maps
a persisted "max" to on so a home upgraded from the removed level keeps
Calm on instead of dropping to off.

The mid-turn hide is now default behavior rather than an opt-in level, so
docs/calm.md documents it for users, docs/configuration.md records the
two written values plus the legacy max mapping, and the feasibility
taxonomy drops its level-scoped wording.

* no-mistakes(document): Document ordinary Calm working-note hiding

* chore: store no-mistakes test evidence in the repo (#2355)

* chore: ignore scratchpad/ at the repo root (#2359)

* no-mistakes(review): Fix wake annotation parsing for unbounded status reads

* no-mistakes(document): Correct stale wake-read documentation comments

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
Co-authored-by: ABHISHAKE KUMAR BOJJA <abojja@uvic.ca>
Co-authored-by: Amplify Logic AI <lars@sockinator.co>
Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>
Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
huynhtandat223 added a commit to huynhtandat223/firstmate that referenced this pull request Aug 18, 2026
* feat(bin): add deterministic agent lifecycle control (#1568)

* feat(bin): add deterministic agent lifecycle control

Separate firstmate's data plane from its control plane.

bin/fm-send.sh is the data plane: conversational text, always
routing-marked for a kind=secondmate target. That marking is right for a
message and wrong for a lifecycle command - a marked "/quit" arrives as
ordinary chat the agent reasons about instead of executing.

bin/fm-control.sh is the control plane: allowlisted interrupt, exit, and
transactional relaunch verbs addressed to an exact task id, with
per-harness mechanics owned by the executable bin/fm-control-lib.sh
rather than improvised in agent prose, and a verified postcondition for
every action. There is no arbitrary-text and no raw-key entry point.

relaunch runs as a transaction with a durable journal: it resolves the
profile, proves the work it must preserve is recoverable, records the
required progress note, stops the old agent, then delegates the launch
to its single owner, bin/fm-spawn.sh --relaunch, which adopts the
recorded endpoint and worktree instead of creating either. A refusal
before the stop leaves the record and instructions byte-identical; a
failure after it reports the concrete state rather than claiming an
agent that is not running. Teardown and discard stay separate and
explicit.

exit and relaunch require a backend with a recovery-grade agent-state
classifier, so zellij, orca, and cmux are refused rather than reported
as successful blind. A remotely placed secondmate is refused by name,
because its agent runs on a host where none of these postconditions can
be read.

* fix(control): resolve a recorded harness to its adapter before retiring wiring

fm-spawn arms per-task harness wiring on prefixes, because a task
launched from a raw command records that command's basename rather than
the exact adapter name. The control plane's retirement tables are keyed
by the exact adapter, so a task recorded as `grok-2` had its turn-end
token, private registry entry, and worktree hook pointer armed and never
retired - leaving a registry entry that outlived the agent that owned
it.

State the prefix rule once, in the capability owner, and resolve the
recorded value through it before every table lookup. bin/fm-send.sh's
composer-clear lookup reads the same owner instead of keeping its own
copy of which adapters need one.

* test(control): pin muse session-binding retirement across a harness switch

* no-mistakes(review): Resolve prefixed harnesses across lifecycle control verbs

* no-mistakes(review): Report interrupt delivery without fabricating cancellation state

* no-mistakes(review): Clear disabled relaunch trace context atomically

* no-mistakes(review): Clarify control interrupts and restore legacy send state

* no-mistakes(review): Refuse ambiguous relaunches and report exit delivery

* no-mistakes(review): Revalidate interrupts and accept interrupt-stopped exits

* no-mistakes(review): Lock descendant tasks before forced recursive teardown

* no-mistakes(document): Align lifecycle adapter documentation with control plane

* no-mistakes: apply CI fixes

* fix(bin): serialize fresh task publication with forced teardown

Forced secondmate teardown enumerated a home's task set, locked what it
found, then re-enumerated while removing. A fresh spawn takes only its
own per-task lock, so a record published inside that window was
invisible to the preflight and visible to the cleanup: it was
destructively processed while never lifecycle-locked.

Reproduced with real agents. A record published 0.249s after teardown
began was removed, its window closed, and its worktree returned to the
pool - while both commands reported success. A per-task lock cannot
protect a task that does not exist yet.

Add a per-home task-set lock guarding WHICH tasks a home has, as opposed
to the metadata lock guarding one task's record. Teardown takes it per
home, parent before child, before enumerating and holds it through
cleanup. A fresh spawn takes it before its own per-task locks and holds
it through publication; a relaunch is exempt, because it republishes an
existing task already covered by that task's control lock.

Either the spawn publishes first and the teardown's preflight covers it,
or the teardown owns the set and the spawn refuses. Both directions fail
closed, and both are pinned by tests that hold the lock rather than
racing on timing.

* no-mistakes(review): Serialize remote secondmate publication with forced teardown

* no-mistakes(review): Preserve remote spawn routing and state initialization

* no-mistakes(review): Serialize teardown when descendant state is absent

* no-mistakes(review): Cover symlinked descendant state refusal

* no-mistakes(document): Document task-set serialization safeguards

* no-mistakes(lint): Isolate task-set lock path resolution

* no-mistakes: apply CI fixes

* feat(stow): add tiered decaying memory management (#1984)

* feat(stow): tiered decaying memory with captain-gated offload to local excluded skills

Implement the captain-adopted /stow redesign from the v2 tiering report as
amended by the adoption decision:

- Per-entry trailing HTML-comment markers with three tiers named for their
  handling: pinned (no clock, no eviction), aging (stale after 30 days),
  perishable (stale after 7 days, mandatory checkable expiry condition).
- File-scoped defaults (captain.md and captain-shared.md pinned,
  learnings.md aging) with a self-describing legend line per file header.
- Reinforcement requires session evidence; re-reading memory never counts.
- Archive-not-delete: stale and budget-evicted entries move with provenance
  to the never-injected data/memory-archive.md; prune always means the cold
  tier, and a stale unique fact is never deleted.
- Captain-gated over-budget offload: staleness evaluated before scope, the
  sweep runs only when still over budget after decay and consolidation,
  proposals go through the receipt plus one durable captain-held backlog
  item, migration runs through the destination's normal path, and the
  memory entry leaves only once the destination is live.
- Offload destination per the adoption decision: a user-owned skill under
  .agents/skills/<freeform-name>/ excluded via the local .git/info/exclude,
  with the hard rule that stow never creates or writes a tracked skill.
- Five graduation moves, receipt verbs archived and proposed-offload, and
  the one-time non-destructive migration of unmarked legacy entries.

The public skills/stow/SKILL.md mirrors the generic parts (markers, decay,
archive exit, user-approved on-demand offload exit, migration) with no
firstmate-specific paths.

The load-bearing assumption that a git-excluded skill is still discovered
was verified empirically against Claude Code 2.1.226 (direct
.git/info/exclude scratch-repo test plus an in-repo ignored-probe test);
the dated evidence is recorded in docs/verification/stow-memory.md.

The graduation list's deletion move is deliberately narrowed to duplicates
already preserved by a stronger owner, reconciling the v2 report's retained
'deletion of a stale entry' wording with its own prune-always-archives
rule.

* no-mistakes(review): Persist legacy migration grace across stow passes

* no-mistakes(review): Enforce archival invariants and exempt default-pinned legacy entries

* no-mistakes(review): Clarify offload scope, archive placement, and marker boundaries

* no-mistakes(review): Enforce aging fallback and verify excluded skill loading

* no-mistakes(review): Fix stow decay, pinned offload, and archival safeguards

* no-mistakes(review): Preserve pinned entries, approvals, and archive provenance

* no-mistakes(review): Restrict stow mutations to editable memory files

* no-mistakes(review): Clarify skill destinations, collision checks, and migration legends

* no-mistakes(review): Resolve exclude paths for linked worktrees

* no-mistakes(review): Secure per-home excluded skill migration

* no-mistakes(test): Require explicit tier markers on new stow entries

* no-mistakes(test): Route missing shared legends to primary owner

* no-mistakes(document): Align stow documentation with tiered memory

* fix(stow): converge the pass on an over-budget home (dogfood D1-D3)

The dogfood run against a copy of the real over-budget home showed the
pass increasing the deficit from 624 to 1,107 estimated tokens and the
relief ladder provably unable to reach budget. Three skill-text fixes:

- D1: markers become single-token spellings (<!--a:DATE-->, <!--p:DATE-->,
  <!--P-->, <!--g-->), entries matching a pinned file default carry no
  marker, the per-file policy legend collapses to a one-line pointer
  naming the stow skill as the scheme owner, and marker/pointer bytes are
  explicitly counted content - roughly 76% less metadata cost on the
  dogfooded home's first installment.
- D2: the eviction rung gains a convergence precondition - total the
  eligible pool first, and when archiving all of it cannot reach budget,
  skip eviction entirely, archive nothing for budget reasons, and report
  the exempt pinned floor as the concrete inability in the final step.
- D3: budget eviction considers only dated aging entries; <!--g-->
  legacy-grace entries are ineligible until their grace cycle resolves,
  so eviction cannot cancel promised grace or invert against validation.

Public skill mirrors the D1 marker/pointer changes; D2/D3 are internal
because the public skill has no budget ladder.

* no-mistakes(test): Enforce evidence-only reinforcement during stow migration

* no-mistakes(document): Clarify stow receipt marker actions

* docs: add project vision (#1997)

* docs: add firstmate vision

* no-mistakes(test): Classify VISION.md as public product documentation

* no-mistakes(document): Restore approved one-file vision diff

* no-mistakes: apply CI fixes

* fix(spawn): force regular Pi TUI for crews (#2005)

* fix(spawn): force regular Pi TUI for crews

* no-mistakes(document): Documented Pi regular TUI launch mode

* fix(cmux): classify borderless Claude composers (#2029)

* fix(cmux): classify borderless Claude composer

* no-mistakes(review): Normalize cmux NBSP prompts across locales

* no-mistakes(document): Document cmux borderless Claude composer classification

* docs(stow): generalize read-before-write in the public stow skill (#2091)

The public installer-facing stow skill scoped its classify-then-replace
discipline to TODO/BACKLOG items only, so findings routed to a memory file
had no stated rule against a blind append or a wholesale overwrite.

Step 6 now classifies every finding against the destination's current
contents as new, duplicate, superseding, or obsolete, and states the
considered replacement each classification implies. The outcomes follow the
tiered-memory contract already in the file: an obsolete entry is refreshed,
archived, or replaced in a way that preserves its fact, a duplicate folds
into the entry that already carries it, and a superseded body worth keeping
leaves through step 7's existing exits rather than a second recovery
mechanism.

* fix: resurface durable supervision work after re-arm (#2065)

* fix(watcher): resurface durable work after downtime

* no-mistakes(review): Make watcher rearm recovery durable and cursor-safe

* no-mistakes(review): Persist safe recovery markers across migration lock recovery

* no-mistakes(review): Retain stale lock when recovery marker publication fails

* no-mistakes(review): Preserve delivery-gap recovery and quarantine malformed markers

* no-mistakes(review): Serialize recovery consumption and report acknowledgment failures

* no-mistakes(review): Centralize recovery publication before clearing watcher evidence

* no-mistakes(review): Guarantee recovery evidence across queue and lock handoffs

* no-mistakes(review): Publish recovery evidence before durable wake commits

* no-mistakes(review): Replace recovery marker Perl dependency with Node

* no-mistakes(review): Keep interrupted wakes durable until handling acknowledgment

* no-mistakes(review): Add post-handling durable wake acknowledgements

* no-mistakes(review): Enforce post-handling acknowledgement across recovery and AFK return

* no-mistakes(review): Bind wake acknowledgements to recovery generations

* no-mistakes(review): Align wake regressions with generation-bound acknowledgements

* no-mistakes(document): Document durable re-arm recovery semantics

* no-mistakes(lint): Resolve ShellCheck warnings in recovery and watcher tests

* no-mistakes: apply CI fixes

* test(watcher): assert post-handling wake replay

* no-mistakes(review): Prevent successor loops and adopt legacy wake generations

* no-mistakes(review): Rearm durable wakes without recursive successor recovery

* no-mistakes(review): Align recovery tests with handling marker state

* no-mistakes(review): Delay handling transition until successor launch is established

* no-mistakes(review): Confirm wake handling only after successful prompt delivery

* no-mistakes(review): Acknowledge AFK wakes only after evidence publication

* no-mistakes(review): Prevent AFK wake loss before post-handling acknowledgement

* no-mistakes(document): Document durable wake acknowledgement semantics

* no-mistakes(lint): Suppress false positive for recovery action output

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* ci: measure Herdr automation on Windows runners (#2100)

* ci: add Windows Herdr automation spike

* ci: run Windows spike on its pull request

* fix: wait for Windows Herdr command output

* fix: run ANSI probe in pane shell

* ci: keep Windows Herdr spike manually triggered

* docs: clarify Windows Herdr spike verdict

* feat(ahoy): guide captains through open decisions (#2099)

* Add guided ahoy decision flow

* no-mistakes(document): Document guided Ahoy decision flow

* fix(stow): enforce startup-memory budget decisions (#2110)

* Harden stow memory budget policy

* Refine internal stow offload policy

* no-mistakes(review): Enforce shared-budget decisions and autonomous offload

* fix(spawn): refresh pooled worktrees from origin before launch (#2116)

* fix(spawn): refresh pooled worktree base

* no-mistakes(document): Document spawn base-freshness invariant

* no-mistakes: apply CI fixes

* fix(composer): unify safe classification across backends (#2102)

* refactor(composer): one shape owner behind thin capture adapters, whole matrix fixed

Consolidate every composer shape - bordered boxes (all families, geometry,
titled bottom borders), bare agent-glyph rows and their wrap regions,
opencode's left bar, and pi's identity-gated separator pair - into
fm_composer_classify_screen in bin/fm-composer-lib.sh. Adapters now
contribute only a capture and a declarative capability descriptor
(styled/cursor/identity/rows); capability differences change how confidently
a shape is judged, never what the shapes are, so a new harness shape is
teachable in exactly one place.

Correctness fixes landed as part of the consolidation (audit
data/fm-composer-consolidation-audit-s1):
- locale-safe Unicode-space normalization in the shared owner (closes the
  fleet-wide half of #1988; cmux's local byte-exact NBSP case deleted;
  naming converges with PR #1995's normalization primitive)
- muse's bare glyph joins the shared set, unbreaking muse on herdr/cmux/orca
- orca learns the borderless bare shape, drops its backward-paged composer
  window, and can no longer classify a stale startup banner as the composer
- tmux tolerates a titled bottom border, unbreaking grok steering
- the left-bar shape makes opencode readable on every backend
- zellij gets a real classifier through dump-screen --ansi, replacing the
  content-diff submit heuristic that could confirm an undelivered message
  and close a --resolve-key decision (the fleet's only false positive)
- fm-spawn's kimi launch-readiness regex (the fourth shape copy) now routes
  through the shared classifier

The strict blank-row posture applies fleet-wide (captain decision
blank-row-injection-posture): no positive container proof = unknown = defer,
replacing tmux's permissive blank-cursor-row rule. Away-mode injection was
re-validated end to end on real tmux (defer on partial input and unproven
rows, clean delivery with swallowed-Enter retry into proven-empty
composers). The tmux submit core gains a baseline-idle turn-started
conversion so pi steering stays confirmed while its working screen hides
the composer; busy conversion without that baseline remains forbidden.

Plain-capture backends now degrade a glyph row carrying trailing text to
unknown instead of a false pending, per the approved capability rule.

Portable regressions pin the full byte-capture matrix from the audit under
a UTF-8 locale and LC_ALL=C, the strict-vs-permissive divergence, and
deliberate signal separation; the opt-in live guard
(tests/fm-composer-matrix-live-e2e.test.sh) verified every installed
harness against the real classifier, recorded in
docs/verification/runtime-backends.md.

* no-mistakes(review): Fix Pi glyph ambiguity and complete profile matrix

* no-mistakes(review): Preserve bare verdict when Pi identity probe is absent

* no-mistakes(review): Harden composer structure and titled-border geometry

* no-mistakes(review): Require proven idle baseline and strict Zellij guard

* no-mistakes(review): Reject box bottom borders as composer input rows

* no-mistakes(review): Prove Zellij probe typing before classifier retries

* no-mistakes(review): Preserve Pi identity uncertainty and scan full left-bar drafts

* no-mistakes(review): Verify Zellij text lands before submitting

* no-mistakes(review): Scope Zellij typing verification to selected composer content

* no-mistakes(review): Verify Zellij pastes through composer-scoped content deltas

* no-mistakes(review): Prove wrapped bare Zellij pastes through composer extraction

* no-mistakes(review): Invalidate stale cursorless composers below dead shell prompts

* no-mistakes(review): Handle shell prompt placeholders in composer extraction

* no-mistakes(review): Classify cursorless bare continuation regions safely

* no-mistakes(review): Reject stale cursorless containers below live activity

* no-mistakes(review): Preserve prompt glyphs in wrapped Zellij pastes

* no-mistakes(review): Reject live shell rows during composer extraction

* no-mistakes(review): Preserve wrapped glyph continuations through submit retries

* no-mistakes(review): Scope idle placeholders to proven positions

* no-mistakes(review): Restore boxed placeholders and live prompt reanchoring

* no-mistakes(review): Fix Zellij placeholder and wrapped glyph paste proof

* no-mistakes(document): Align composer architecture documentation

* no-mistakes(lint): Fix ShellCheck warnings in composer refactor

* no-mistakes: apply CI fixes

* docs(verification): record the trusted-checkout live matrix rerun

The pipeline's isolated gate worktree is untrusted, so claude, grok, and
muse stopped at first-launch trust dialogs there (the guard refuses to
confirm them by design). This rerun from the trusted checkout at the final
validated head verified all six installed harnesses, the strict blank-row
deferral, and the hardened zellij false-positive probe live.

* no-mistakes(document): Align composer verification evidence

* no-mistakes: apply CI fixes

* no-mistakes(review): Restore proven box bottom-cursor classification

* no-mistakes(review): Preserve styled placeholder-like drafts as pending

* no-mistakes(document): Align composer safety and Zellij delivery documentation

* no-mistakes: apply CI fixes

* docs(verification): refresh the live matrix with the final-head trusted rerun

The post-validation rerun from the trusted checkout verified all six
installed harnesses at the branch's final head, including Claude 2.1.227
(auto-updated since the audit's captures) and Grok, which the untrusted
gate worktree could not verify past their first-launch trust dialogs.

* fix(spawn): gate Pi TUI mode by CLI capability (#2117)

* fix(spawn): gate Pi regular TUI flag by capability

* no-mistakes(review): Document conditional Pi TUI capability detection

* no-mistakes(review): Pin Pi probing and launch to one executable

* no-mistakes(review): Preserve literal pinned Pi paths and update documentation

* no-mistakes(review): Defer pinned Pi path insertion until final substitution

* no-mistakes(document): Document version-safe Pi launch probing

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* docs(vision): elevate experience, pain narrative, and distro virtues (#2147)

* docs(vision): elevate experience, pain narrative, and distro virtues

Fold the captain's public vision framing into VISION.md: peace of mind as a
primary goal, multi-session context-switch pain as the problem one interface
solves, clone-and-run setup ease, self-evolution including community, and
explicit harness/backend orthogonality. Reconcile experience-as-garnish into
experience-as-purpose and update aligns/resists accordingly.

* docs(vision): state the experience goal positively

Drop the negative "not a smart workflow / useful tool / impressive technology"
pretext. Lead straight into the positive experience north star.

* feat(bin): reconcile inactive terminal crew outcomes (#2167)

* fix: reconcile inactive terminal outcomes

* fix: stream secondmate summary inputs

* no-mistakes(review): Fix reconciliation locking and request delivery retries

* no-mistakes(review): Prevent retries after unknown request delivery

* no-mistakes(document): Clarify inactive reconciliation cadence and receipts

* no-mistakes(lint): Quote terminal status arguments in reconciliation tests

* refactor: simplify inactive outcome reconciliation

* no-mistakes(review): Bound inactive reconciliation scans with durable progress

* no-mistakes(review): Bound reconciliation and deduplicate recovery notices

* no-mistakes(document): Document inactive outcome reconciliation contracts

* no-mistakes(review): Reject relative local secondmate parent routes

* no-mistakes(review): Key terminal receipts by spawn incarnation

* no-mistakes(review): Stabilize legacy receipts and lock reconciliation snapshots

* no-mistakes(review): Fail closed on invalid secondmate identity markers

* no-mistakes(document): Document durable inactive-outcome reconciliation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* ci: raise Herdr test timeout (#2191)

* fix: refresh stale Pi instructions after compaction (#2163)

* fix(session-start): refresh drifted instructions on stale rebuilds

* test(session-start): prove Pi instruction refresh end to end

* no-mistakes(review): Fix stale instruction refresh and baseline integrity

* no-mistakes(review): Preserve true-start baselines across Pi continuations

* no-mistakes(review): Correct Pi continuation classification and live expectation

* no-mistakes(review): Correct Pi continuation coverage documentation

* no-mistakes(review): Fix read-only refresh and exact Pi session restores

* no-mistakes(review): Classify Pi create-if-missing sessions correctly

* no-mistakes(review): Classify named Pi sessions using immutable headers

* no-mistakes(review): Correct Codex interactive coverage diagnostic

* no-mistakes(document): Document immutable Pi compaction instruction refresh

* no-mistakes(document): Correct Pi refresh documentation and validation claims

* feat: add deterministic condition-to-action watcher (#2200)

* feat(bin): add deterministic condition->action watch adapter on the process-event channel

Register a (condition, action) pair once with bin/fm-procevent-when.sh and the
existing process-to-event runner polls the condition tokenlessly, fires the
action at most once on a stable true, and wakes firstmate exactly once with the
captured outcome - instead of burning an agent turn per re-check.

The pair is stored privately under state/when/ and hash-bound by a trust record
the same way fm-check-register.sh binds a custom check, so a mutated spec is
refused without executing anything. A durable exclusive fired marker claimed
before the action makes restarts and re-polls unable to double-fire; every
failure path (mutated spec, condition error past budget, expired deadline,
failed action, uncaptured earlier fire) ends in a terminal captured outcome
that wakes firstmate rather than a silent retry. Eligibility stays a firstmate
judgment: only exact, safe, reversible actions may be bound, and judgment-
needing or destructive actions keep the wake-and-decide flow.

* no-mistakes(review): Harden when watcher concurrency, deadlines, timeouts, and output

* no-mistakes(test): Bind watcher actions to registered executable bytes

* no-mistakes(document): Correct condition-action watcher documentation

* no-mistakes(document): Clarify outcome wake re-announcement

* no-mistakes: apply CI fixes

* fix(bin): honor a decision key stated after the verb colon (#2202)

The open-decisions fold only recognized a [key=<slug>] token between the
verb and the colon (needs-decision [key=x]: note). The common worker
shape with the colon first (needs-decision: [key=x] note) silently
folded its stated key into the shared "default" bucket, so two open
decisions could collapse into one record and fm-send --resolve-key <x>
refused to close the decision it plainly named.

A complete token at the head of the note is now an equivalent stated-key
position for every keyed verb, shared by the whole-file and incremental
folds through the one _fm_decision_key owner. The documented
before-colon position wins when both are present, a token deeper in the
note stays prose, a bare keyless line still folds to "default", and a
stated-but-malformed slug is rejected rather than rewritten to
"default". A consumed note-head token is stripped from the note so both
positions yield identical records, and the incremental fold version is
bumped so persisted cursors folded under the old interpretation are
rebuilt from the authoritative log.

Fixes #2109

* fix(bin): prevent watcher recovery acknowledgement livelock (#2212)

* fix(bin): keep a recovery acknowledgement valid across republication

A watcher cycle that opened and closed while the model handled its drained
wakes minted a fresh recovery generation, which invalidated the exact
acknowledgement the drain had just printed. That acknowledgement then consumed
nothing, so the marker stayed pending and every later arm spent its whole cycle
re-announcing the same recovery instead of supervising - a livelock the home
could not leave on its own.

A downtime publication now reuses the generation of an outstanding handling
episode, so a close during the handling window cannot orphan the printed
acknowledgement. The acknowledgement itself separates its two facts: queue-row
consumption is bound to the monotonic --ack-through sequence and always
happens, while only retiring the episode is bound to --recovery-generation. A
generation that moved on is a non-fatal result that names its own remedy
instead of a refusal that consumes nothing.

* no-mistakes(review): Preserve recovery generations and consume stale acknowledgements safely

* no-mistakes(document): Document sequence-bound recovery acknowledgements

* feat(fmx-respond): consume Relay conversation chains (#2206)

* feat(fmx-respond): consume in_reply_to_chain conversation context

The relay's poll payload can carry in_reply_to_chain, an oldest-first
transcript of the surrounding conversation, but the mention-handling
procedure only ever read the immediate in_reply_to parent, so referents
like "this" in a standalone mention stayed unresolvable even when
context was delivered.

Teach fmx-respond to read the chain when present (optional and
backward-compatible: often absent today, kind label not required),
resolve referents against the whole transcript, and extend the
untrusted-content framing to every chain entry including the upcoming
kind=history entries. Document the field's wire shape in
docs/configuration.md as the firstmate-side owner.

* no-mistakes(document): Document Relay chain context ownership

* fix: parse decision verbs before status metadata tags (#2280)

* fix(bin): strip every bracket tag, not just [key=...], from a status verb

status_line_verb only stripped a leading "[key=...]" token before the
colon, so a remote secondmate reply's leading "[corr=...]" correlation
tag stayed glued onto the returned verb word ("needs-decision
[corr=...]" instead of "needs-decision"). The open-decisions fold's
verb match then silently failed to recognize the line at all, so
fm-send --resolve-key refused to close a decision that was plainly
open on the status line.

Generalize the parser to strip every "[name=value]" tag before the
colon, in any order and count, so local and remote replies fold
identically.

* no-mistakes(review): Invalidate stale decision cursors after parser fix

* no-mistakes(document): Clarify status metadata verb parsing

* fix(bin): collapse duplicate supervision wakes (#2287)

* fix: collapse duplicate supervision wakes without losing legitimate updates

One remote-secondmate note produced two handling turns (a procevent check
wake published before autohandle, then a signal wake for the same mirrored
bytes), already-ingested replays such as a cursor-loss whole-log recapture
still woke with nothing to do, this home's own bookkeeping closes (fm-send
--resolve-key, the pending-reply escalation close, the captain-held
transfer) re-woke the session that wrote them, and turn-ended-only wakes
were annotated with already-announced status lines that looked like fresh
progress.

Dedup rules, each at its layer's one owner:
- fm-procevent.sh: an adapter may declare 'self-announcing'; the runner
  then applies first and publishes a check wake only for what remains
  unhandled. fm-procevent-remote-reply.sh declares it: the mirrored status
  append is the single announcement, so a fully applied capture publishes
  nothing and a byte-identical replay stays completely quiet. All other
  adapters keep strict publish-before-apply.
- fm-wake-lib.sh: fm_wake_signal_sig/seen_path/seen_current now own the
  watcher's signal signature and .seen-* marker format, plus
  fm_wake_status_append_self_announced, the guarded bookkeeping append
  that advances the marker only over exactly its own bytes and fails
  toward waking on any pending or interleaved foreign write.
- fm-send.sh, fm-pending-reply-lib.sh, fm-decision-hold.sh: bookkeeping
  closes go through that guarded append; escalation opens stay plain
  appends because a new blocker must wake.
- fm-wake-lib.sh annotations: a historical (turn-ended-only) row skips its
  status annotation only when the file's signature provably matches the
  seen marker; anything unannounced keeps annotating.
- fm-classify-lib.sh: a kind=secondmate task's status signal is never
  absorbed as provably-working, because that stream is the routed-reply
  channel the parent must read.

Also fixes a pre-existing exit-path deadlock the regression run reproduced:
a TERM inside a recovery-marker critical section left fm_lock_try_acquire
spinning against this same process's abandoned hold; a self-held lock is
now reclaimed (a subshell still waits on its parent's live hold).

Regression tests drive the real wake functions and executables in both
directions: each duplicate case collapses, while a new remote reply, new
decision, new blocker, merge result, failure, first status change, and a
later different note on the same task all still wake.

* no-mistakes(document): Document wake deduplication contracts

* feat: add Cursor CLI crew harness (#2238)

* feat(harness): add Cursor Agent CLI adapter

# Conflicts:
#	bin/fm-spawn.sh

* fix(composer): read cursor-agent's reverse-video placeholder as idle

cursor-agent renders its idle composer placeholder dim (SGR 2) but paints the
cell under the terminal cursor in reverse video (SGR 0;7). Reverse video is
neither dim nor a dark truecolor foreground, so the shared ghost stripper keeps
that one character and an idle composer reduces to a lone `P`. Judged on its
own, that remnant reads `pending` on a genuinely idle pane, which defers
away-mode escalation indefinitely on the styled cursorless backends.

Teach the ONE fleet-wide classifier the shape instead of adding an adapter-local
copy: register `→` as an agent prompt glyph so the composer row is structurally
findable at all (without it the bottom-most shape is a stale shell prompt echo
in the scrollback), add both verified placeholders to the idle set, and consult
the styling-independent plain row when the styled row is only a remnant.

The plain-row branch demands the remnant be a proper, strictly shorter substring
of a plain row matching a fully anchored placeholder. Real typed text is
uniformly bright, so stripping leaves it equal to the plain row and it stays
`pending` - verified live against a pane where the typed text was exactly the
placeholder string.

Verified live on cursor-agent 2026.08.11-e8db854; the regression pins the real
captured bytes and asserts the remnant survives stripping, so the case cannot go
vacuous if the stripper later learns SGR 7.

Co-authored-by: Amplify Logic AI <lars@sockinator.co>

* feat(cursor): narrow cursor identity and order its marker before CLAUDECODE

Cursor ships two executable names - `cursor-agent` and the legacy alias `agent`
- and runs as a bundled node script, so tmux reports the pane command as a bare
`node`. Neither `agent` nor `node` can be trusted by name, so identity gets one
owner in bin/fm-cursor-lib.sh that demands cursor's own name or install tree in
the path or argv[0], from the structural signal only. Probing an arbitrary pid's
executable during a liveness poll would execute a stranger's binary, which is
the hazard that rule exists to close.

Two consequences wired up:

Detection. cursor-agent does NOT clear an inherited CLAUDECODE, so a cursor
worker launched under a claude primary carries both markers and whichever is
tested first wins. The cursor markers are ordered ahead of the CLAUDECODE check;
fm-spawn additionally clears foreign markers at the launch boundary. Both are
kept deliberately - launch sanitization only covers sessions fm-spawn started,
while the ordering also covers a cursor session started by hand. Verified live
that CURSOR_INVOKED_AS is set on the agent process and CURSOR_AGENT=1 on the
child/tool processes fm-harness.sh actually runs as.

Pane liveness. A cursor pane now classifies `agent`. An unrelated node or agent
stays `other`, which the liveness callers already fold into `ambiguous` rather
than `dead`, so a stranger's node pane is never reported agent-free.

Resolution prints the STABLE launcher rather than the canonical target: identity
is proven through canonicalization, but cursor's canonical path carries a
version its own auto-update replaces, and pinning that would strand a task on a
version that can vanish.

The regression drives the two identity signals apart - a cursor-named executable
outside any cursor tree, and a non-cursor-named alias inside one - and asserts
each carries a verdict alone, so no single vendor string is load-bearing. Its
negative controls are real spawned processes, not fixtures.

Verified live on cursor-agent 2026.08.11-e8db854.

Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* feat(cursor): classify cursor busy state from its own turn transcript

Cursor shipped as "unknown cursor-unverified" on the premise that it exposes no
semantic turn lifecycle, only a rendered "Working" footer. That premise is
wrong: cursor-agent persists an append-only JSONL transcript per conversation
and brackets every submitted turn with a role:user open and a typed turn_ended
close. Verified live on 2026.08.11-e8db854, including the interrupt path, where
Escape closes the turn with status "aborted" - so this source covers manual
interruption, which Claude's Stop hook does not.

That makes it a genuine pull source in the muse mould rather than the rendered
text the redesign forbids: no writer, no arm, no gen, nothing seeded that could
never be cleared. Cursor's `ctrl+c to stop` footer stays out of the verdict, and
herdr's narrower native streaming state cannot stand in for it either.

Binding deliberately does not reconstruct cursor's workspace-slug directory
name. That slug collapses path separators, so rebuilding it would be a guess
that could bind the wrong pane; cursor records the exact absolute workspace path
in each project's .workspace-trusted, and the binding matches on that. A
conversation recorded as prior at spawn is excluded, so a relaunch in a reused
worktree folds its own turn rather than its predecessor's. Requiring a unique
remaining conversation keeps zero and several both unknown, because neither
proves anything about the current turn.

The regression pins the fold with real transcript files and asserts the
dangerous direction stays closed: an unresolvable binding, a record-free file,
an unclaimed workspace, and a workspace-path PREFIX all read unknown, never
idle. The prefix case uses an opaque fixture slug so a slug-rebuilding
implementation cannot pass it.

Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* feat(cursor): make the cursor launch runnable and give it lifecycle control

Five gaps that together kept a cursor crewmate from being drivable end to end.

Launch. The template invoked `cursor agent`, but `cursor` is not the CLI - the
installed names are `cursor-agent` and the legacy alias `agent` - so the command
could not run at all on a machine with a normal cursor install. It now resolves
through the verified owner, which also refuses a spawn loudly instead of leaving
a pane that dies with command-not-found and reads as a wedged worker.

Session binding. fm-spawn writes state/<id>.cursor-session so the busy fold can
find this pane's transcript, and teardown removes it.

Lifecycle control. No cursor PR touched fm-control-lib.sh, so
`fm-control <id> interrupt|exit|relaunch` could not drive a cursor worker at
all. Verified live: interrupt is a single Escape, exit is /exit, and cursor does
NOT repollute its composer with the cancelled prompt, so unlike muse it needs no
clear key. Secondmate is refused, matching the spawn refusal.

Submit acknowledgement. cursor parks its terminal cursor outside its composer,
so the composer verdict on tmux is always `unknown` and a submit could never be
acknowledged from the composer alone. The submit core's existing idle-to-busy
transition covers that case, but only if the pane's busy footer is recognised,
so cursor's `ctrl+c to stop` joins the harness-less default union the submit
cores read. The TOKEN is matched rather than the spinner verb: the same version
rendered both `Working` and `Running` in consecutive turns.

Bootstrap. A configured cursor crew harness with no cursor executable is now a
loud MISSING diagnostic rather than a first-spawn failure, and it accepts either
installed name.

Interrupt cancellation is deliberately left unconfirmed. The transcript does
type an aborted close, but its post-interrupt write latency measured as
variable - sometimes seconds, sometimes not within twenty - so a claim built on
it would be unreliable. Normal turn completion is prompt, which is what the busy
fold actually depends on.

Two inherited tests are corrected rather than deleted: the busy test asserted
cursor could have no semantic source, and the launch test pinned the literal
`cursor agent` string. Both now pin the verified behaviour, including that the
launch never allocates a second worktree.

Co-authored-by: ABHISHAKE KUMAR BOJJA <abojja@uvic.ca>
Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* docs(cursor): record the verified crewmate facts and extend the drift guard

The inherited cursor entry was written against 2026.08.04-aaa8809 and several of
its claims no longer hold: it named `cursor agent` as the binary (not the CLI
name), listed six Grok model ids of which the live catalog now returns two, and
recorded busy state, exit, interrupt, and skill invocation as unverified.

Replaced with what was measured against 2026.08.11-e8db854, including the two
facts most likely to be rediscovered painfully: cursor runs as a bundled node
script so its pane title is a bare `node`, and it parks its terminal cursor
outside its composer, which makes the tmux composer verdict permanently
`unknown` by design rather than a defect to chase.

Model ids now route to `--list-models` for the account instead of a fixed list,
since that list is exactly what drifted.

The live drift guard covers cursor, resolving it through the same verified owner
fm-spawn uses and passing --trust so the probe cannot hang on the workspace
prompt. Run against every installed harness: 8 checked, all alive, with cursor
reporting title='node' foreground=[.../cursor-agent] - the drift shape this
guard exists to catch.

Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* docs(agents): record the cursor session-binding state file

The state/ layout section is the inventory every session reads; a busy-source
binding that fm-spawn writes and teardown removes belongs in it alongside muse's.

* no-mistakes(review): Sanitize ambient Cursor marker in harness tests

* no-mistakes(review): Validate Cursor models against live catalog

* no-mistakes(review): Reject unsupported secondmates before binary preflight

* no-mistakes(review): Narrow Cursor ancestry detection to structured process identity

* no-mistakes(review): Parse Cursor transcripts and sanitize inherited markers

* no-mistakes(review): Handle malformed Cursor transcript records safely

* no-mistakes(review): Validate malformed Cursor closes in fallback parser

* no-mistakes(review): Retire stale Cursor bindings during relaunch

* no-mistakes(review): Fix Cursor drift guard command variable

* no-mistakes(review): Narrow Cursor identity to versioned install trees

* no-mistakes(document): Document Cursor harness boundaries

* refactor(composer): move the delivery busy footers to the shared owner

The per-harness rendered busy footers lived in bin/fm-tmux-lib.sh under
FM_TMUX_* names, so cursor's `ctrl+c to stop` signature - and every other
harness's - was reachable only from tmux. That placement was wrong on its own
terms: herdr, zellij, cmux, and orca run the same harnesses and face the same
question these footers answer, which is whether a submitted Enter actually
landed. Nothing about the signature is tmux-specific.

Moved verbatim into bin/fm-composer-lib.sh, the shared composer/delivery owner
every backend already sources, and renamed to FM_DELIVERY_* so the names stop
claiming a scope they never had. All five adapters now reach cursor's signature;
verified per adapter rather than assumed.

The boundary the move must not blur is stated where it now lives: this is a
DELIVERY guard, never a worker-state source. Confirming a keystroke landed is a
different question from asking what a worker is doing, and bin/fm-busy-lib.sh
remains the semantic owner that forbids classifying a harness from rendered
text. Cursor still classifies only from its transcript fold, which is already
backend-agnostic because it folds a file rather than reading a pane - the same
verdict on all six backends.

The old FM_TMUX_* aliases are dropped rather than kept as dead shims: nothing
outside the moved block referenced them except fm-busy-lib.sh's grok fallback,
which now reads the new name. The documented operator override, FM_BUSY_REGEX,
is untouched.

Also removes a dead duplicate CURSOR_INVOKED_AS check in bin/fm-harness.sh,
unreachable behind the marker check above it.

* no-mistakes(review): Correct shared delivery guard ownership references

* no-mistakes(document): Document shared delivery guards and Cursor backend limits

* no-mistakes: apply CI fixes

* fix(composer): bound a bare composer's wrap region at a half-block rule

A live cursor crewmate on herdr classified its IDLE composer as `pending`, and
fm-send consequently exited 1 with "delivery unconfirmed" on a message that had
actually landed. The cause is not cursor-specific.

Herdr draws a composer's top and bottom rules with the half-block glyphs U+2584
and U+2580 rather than the box-drawing family. fm_composer_row_has_edge knew
only the box-drawing set, so no box was detected; the composer was found as a
BARE row, and its wrap region - which extends while rows are non-blank and carry
no structural edge - walked straight through the composer's own closing rule and
swallowed the model and path footer below it. That footer is real text, so the
region classified pending on a genuinely idle pane.

Teaching the shared edge detector the half-block glyphs bounds the region at the
closing rule. Measured on the captured bytes of a real herdr cursor pane: the
same capture that read `pending` now reads `empty`.

This is a shared shape-path change, so it is deliberately narrow - it adds
glyphs to the edge vocabulary and changes no verdict logic - and the whole
composer and backend suite is green, including the other harnesses' herdr
fixtures.

The regression pins the real captured shape and asserts the footer content is
genuinely present, so the case cannot pass vacuously if the region were ever
bounded for some unrelated reason.

* fix(herdr): confirm a cursor submit from the rendered-footer transition

Herdr's composer-shape fix made an idle cursor pane classify `empty`, but
`fm-send` still exited 1 with "delivery unconfirmed" on messages that had
actually landed. Live measurement found the second, independent cause.

Herdr reports a cursor pane `agent_status=blocked` in EVERY state - idle,
mid-turn, and after - so the submit path's idle-baseline native confirmation is
structurally unreachable for cursor and every send falls into the composer
branch. That branch reads cursor's mid-turn composer row, which renders its own
`Add a follow-up` placeholder beside a right-aligned `ctrl+c to stop`. That
token is composer content, so the verdict is `pending` on a composer holding no
user text at all, and the Enter-retry budget then reports pending.

The escape is the same semantic signal the native path uses, read from the
pane's verified busy footer instead of native agent-state, and it is the
rendered-footer twin of the tmux submit core's turn-started confirmation: an
idle-to-busy transition ACROSS our Enter proves the harness accepted the
submission. The baseline is taken before the first Enter and only when the
native baseline was not legibly idle, so the idle-baseline path still never
reads pane content and a pane already mid-turn before we typed keeps reporting
`pending` rather than borrowing another turn as proof of this delivery.

The composer verdict is deliberately NOT relaxed. A right-aligned status token
on the composer row stays content for every other caller, including the
away-mode pre-injection guard, and the shared cursorless submit core is left
untouched so zellij, cmux, and Orca keep the behavior their own follow-up owns.

Verified live on herdr 0.8.0 and cursor-agent 2026.08.11-e8db854 in an isolated
lab session: `fm-send` now exits 0 and the steer executes, interrupt cancels a
running turn, `/exit` stops the agent, and teardown clears the record. All seven
panes of the running default session classify identically before and after the
shape fix, so no other harness regressed.

* no-mistakes(review): Prevent working Herdr baselines from falsely confirming delivery

* no-mistakes(document): Correct Cursor harness and backend documentation

---------

Co-authored-by: ABHISHAKE KUMAR BOJJA <abojja@uvic.ca>
Co-authored-by: Amplify Logic AI <lars@sockinator.co>
Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* fix(bin): require quota-axi 0.1.25 (#2300)

* fix: raise quota-axi floor to 0.1.25 for Cursor CLI quota awareness

Homes on latest main need quota-axi #87 so Desktop-absent CLI machines report a fresh Cursor quota instead of a false sign-in-required.

* no-mistakes(document): Update quota floor documentation pointer

* fix(bin): prevent false Pi watcher alarms during hand-offs (#2304)

* fix(guard): stop the false send-time watcher-down alarm on Pi primaries

On a Pi primary the watcher process is not the liveness signal. The Pi
extension tears the watcher down on every actionable wake and spawns the
replacement itself, so the singleton lock is legitimately unheld between
cycles: every one of the 799 cycles in a live primary's ledger ends with
lock_after=pid:none, and a live capture caught the guard verdict flipping to
no-watcher during one hand-off with the beacon 63s old.

bin/fm-guard.sh classified Pi as a persistent-watcher harness, which demands a
live identity-matched lock holder at all times, so any guarded command landing
in a hand-off painted the full WATCHER DOWN - SUPERVISION IS OFF banner and
told firstmate to repair a cycle the extension already owns and is restoring.

Add an extension supervision model for pi and pi-signed. A live
identity-matched watcher stays the ordinary healthy state; an unheld lock is
healthy only while the beacon is fresh within grace AND a live Pi session
provably owns continuity - both primary extensions recorded in their state
markers at their current on-disk builds by the process named in state/.lock,
with that process still alive. Without that proof the banner fires exactly as
before, so an unloaded, version-drifted, or exited Pi session is loud
immediately and a cycle the extension never restores is loud once the beacon
passes grace. The queued-wake warning, the PID-strict turn-end guard, and
every other primary's detection are untouched.

Fold session-start's duplicate Pi marker predicate into the shared library so
the ownership contract has one owner.

* no-mistakes(review): Restrict Pi hand-off tolerance to unheld watcher locks

* no-mistakes(document): Document Pi watcher hand-off supervision

* feat: support Cursor Agent CLI as a primary harness (#2305)

* feat(cursor): add Cursor Agent CLI primary hooks, park supervision, and session start

Register a tracked project-scope .cursor/hooks.json for Cursor's stop,
sessionStart, preCompact, and preToolUse steps.

bin/fm-turnend-guard-cursor.sh owns Cursor's turn boundary as a park: it
foregrounds the watcher arm, holds the boundary open until an actionable close,
and returns that wake as one follow-up. Exit 2 is a silent no-op on Cursor's
stop step, so the adapter never uses it. The follow-up loop is bounded twice,
by Cursor's own loop_limit and by the payload's loop_count.

bin/fm-sessionstart-cursor.sh delivers the digest as additional_context at
sessionStart, and stages it for the next turn boundary at preCompact, which
cannot inject context.

Cursor also loads the tracked Claude settings, so bin/fm-hook-host-lib.sh lets
each tracked Claude-shaped entrypoint stand down on a Cursor-delivered payload
rather than running every covered event twice.

bin/fm-tmux-lib.sh reclassifies a Cursor pane's composer cursorlessly, because
Cursor parks its terminal cursor outside the composer, which restores a genuine
composer-empty proof and unblocks away-mode escalation delivery.

* feat(cursor): make Cursor Agent CLI a verified primary harness

Resolve Cursor in the session-lock ancestry through bin/fm-cursor-lib.sh, which
a Cursor primary needs before it can hold its own home lock, and classify its
stop-hook park under the autoarm supervision model so the mid-turn pull guard
stops reporting a healthy between-turns watcher as down.

Read a Cursor pane's composer cursorlessly on tmux, gated on Cursor's own
structural process identity, which restores a genuine composer-empty proof and
lets away-mode escalations reach a Cursor primary with no daemon change.

Lift the secondmate refusals in bin/fm-spawn.sh and bin/fm-control-lib.sh now
that the supervision protocol exists and is recorded.

Cover the whole surface with a portable regression over real processes, an
opt-in live guard against the installed cursor-agent, and dated per-harness
evidence.

* docs(cursor): record Cursor as a verified primary across the owning surfaces

Update the turn-end guard, session-start, arm-seatbelt, cd-guard, watcher
continuity, architecture, configuration, README, and harness-adapters owners,
and add dated live evidence to the supervision and runtime-backend verification
records. Correct the recorded Cursor tmux composer verdict: the cursor-anchored
read is still blind, but the composite reader is no longer unknown.

Lift the remaining remote-secondmate refusal missed in the previous commit, and
add the new libs to the existing fixtures that copy a fixed dependency list.

* refactor(cursor): name the park's stand-down condition for both its causes

Also record that Cursor's preCompact firing itself is not yet live-verified,
while the static evidence that it cannot inject context, and the staging path
that follows from it, both are.

* test: give the pretool fixtures their new dependency and one lint owner

The cd-guard fixture copies a fixed dependency list and now needs the shared
hook-host predicate. Both pretool suites also asserted cleanliness with a bare
shellcheck call, a second and weaker copy of the lint definition that
bin/fm-lint.sh owns: it omits --external-sources, so it failed the moment these
checkers sourced a shared library. They now delegate to that owner.

* test: assert the cursor secondmate contract instead of its removed refusal

A cursor secondmate now launches, so the suite asserts what its park actually
needs: --trust so the home's project hooks load at all, its own home pinned as
the workspace, and the autoarm supervision model inherited across the launch.

* no-mistakes(review): Serialize Cursor wakes and bind staged context

* no-mistakes(review): Serialize Cursor context and nag state commits

* no-mistakes(review): Enforce Cursor ceiling before staged context delivery

* no-mistakes(review): Serialize Cursor claims and staged context

* no-mistakes(review): Serialize Cursor ownership and state commits

* no-mistakes(review): Protect Cursor context across session takeover

* no-mistakes(review): Preserve Cursor context across session takeover

* no-mistakes(review): Enforce owner-keyed Cursor staged context

* no-mistakes(review): Atomically claim Cursor follow-ups and staged context

* no-mistakes(review): Defer Cursor preCompact staging and simplify supersession

* no-mistakes(review): Serialize Cursor park commits and defer preCompact

* no-mistakes(review): Stop Cursor parks after session takeover

* no-mistakes(test): Route Cursor preCompact context through stop follow-up

* no-mistakes(document): Update Cursor primary documentation

* revert(cursor): cut preCompact staging from this change

Carrying a compaction digest across two concurrently running stop hooks kept
producing races that could deliver it twice or strand it indefinitely, and
closing them kept enlarging a critical section inside a hook Cursor awaits at
the turn boundary. Native preCompact firing was never observed either, so the
surface has no empirical basis yet.

Remove the adapter, its registration, its staged path in the park, and its
tests, and record the surface as deferred and uncovered alongside the Codex
interactive TUI. A regression now asserts preCompact stays unregistered so it
cannot return without its own design and evidence.

This change ships the proven core only: the turn-end follow-up park, the
run-tier session start, and away-mode delivery.

* no-mistakes(review): Correct Cursor park supersession documentation

* no-mistakes(document): Clarify Cursor run-tier verification ownership

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

---------

Co-authored-by: kunchenguid <kun-1@kunchenguid.com>

* feat(bin): add decline and repair paths for decision holds (#2330)

* feat(bin): add unrouted close paths to the captain decision gate

A captain who declines a held decision leaves no follow-up work to route,
so `resolve` could not express that answer: it requires at least one
`--routed-to` task. The only way to close such a hold was a direct
`tasks-axi done`, which never writes the durable resolution record the
completion gate reads, so the originating investigation could no longer
pass `verify` and its cleanup stayed blocked.

Add two close paths that route no work:

- `decline` closes an actively held hold with a recorded captain decision
  and no routed task. It refuses while any task is still blocked by the
  hold, because releasing routed work without recording it is `resolve`'s
  job.
- `repair` records the missing resolution block on a hold that was already
  closed outside this script. It never reopens a hold and never clears a
  dependency edge, and it refuses a hold that is still actively held.

Both require a non-empty captain decision file and share `resolve`'s
digest-based retry identity, so an exact retry is idempotent while a
changed decision is rejected. The recorded body now also names which path
closed the hold, and each routed entry regains its own line.

The gate itself is unchanged: an unanswered decision still fails
completion and blocks teardown, and neither new path can close a hold
without the captain's recorded word.

* fix(bin): require captain-hold provenance before repairing a decision

`repair` checked only that the backlog item was kind captain and Done, so
an ordinary captain-kind task that was never held for the captain could be
closed, repaired, and then pass the completion gate.

tasks-axi keeps `hold_kind` through a close, so it is the surviving proof
that an identity really was a captain hold. Require it before writing the
resolution record, and cover the case in the gate regression.

* no-mistakes(document): Correct decision-hold lifecycle documentation

* fix(bin): surface buried wake status lines once (#2331)

* fix(bin): surface buried status notes on wake drain

A note: answer immediately followed by a routine note was dropped because
annotations kept only the newest line and note: never enters OPEN DECISIONS.
Present every unread note and pending-reply resolution since the last drain
cursor, and annotate every unread line on a queued signal.

* no-mistakes(review): Fix unread status cursor races and overflow

* no-mistakes(review): Preserve cursors when status span reads fail

* no-mistakes(review): Make status presentation transactional under I/O failures

* no-mistakes(review): Simplify unread status cursor and presentation locking

* no-mistakes(review): Align cursor failure regressions with transactional presentation

* no-mistakes(review): Retire stale presentation cursors during task teardown

* no-mistakes(review): Preserve routine status until signal annotation

* no-mistakes(review): Correct unread status cap documentation

* no-mistakes(document): Document unread wake status presentation

* no-mistakes(lint): Fix wake surfacing ShellCheck warnings

* no-mistakes: apply CI fixes

* feat: add max Calm presentation level (#2334)

* feat(calm): add a max presentation level that hides mid-turn working notes

Calm's home-local preference becomes a three-state level instead of a
boolean: "off" is stock Pi, "on" is today's Calm, and "max" is Calm plus
hiding the assistant text of messages the model did not end its response
with. `/calm max` selects it from any state, a plain `/calm` steps max
back to ordinary Calm and otherwise keeps the existing on/off cycle, and
any other argument keeps that cycle too.

`config/calm` now persists "max" as its own literal value, so a session
start, resume, fork, or reload restores the stored level rather than
treating it as unrecognized and dropping to off.

The hide rule keys on Pi's intrinsic per-message stopReason: "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, because suppressing it would also stop a genuine reply from
streaming. The existing assistant layout adapter filters the blocks out
of the same shallow presentation copy it already uses for collapsed
thinking, so the message, model context, session storage, /export, and
delivery are untouched and a hidden mid-turn row collapses to zero
height. The new "assistant-working-note" class keeps that choice in the
visibility policy owner, where ordinary Calm keeps it visible.

* no-mistakes(document): Clarify Calm max persistence and taxonomy

* feat(calm): hide mid-turn working notes by default (#2339)

* feat(calm): make hiding mid-turn working notes the ordinary Calm state

Calm collapses back to the two-state on/off toggle it was before the max
presentation level, with max's hide rule promoted into ordinary Calm.
Calm on now hides mid-turn assistant working notes in addition to what it
already hid, and the /calm command parses no argument again.

The hide rule itself is unchanged: assistant text is removed from the
shallow presentation copy when the message's own stopReason is "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, so a genuine reply still streams. The message, model context,
session storage, /export, and delivery remain untouched.

config/calm persists only "on" and "off" again, but the reader still maps
a persisted "max" to on so a home upgraded from the removed level keeps
Calm on instead of dropping to off.

The mid-turn hide is now default behavior rather than an opt-in level, so
docs/calm.md documents it for users, docs/configuration.md records the
two written values plus the legacy max mapping, and the feasibility
taxonomy drops its level-scoped wording.

* no-mistakes(document): Document ordinary Calm working-note hiding

* chore: store no-mistakes test evidence in the repo (#2355)

* chore: ignore scratchpad/ at the repo root (#2359)

* fix(ci): fail hung Herdr behavior runs in 20 minutes (#2413)

A wedged family-run step was occupying the runner until the 75-minute
job cap; bound that step so cleanup and timing artifacts still upload.

* fix: keep the public promise reachable when work is routed to a second mate (#2457)

The lightweight Relay follow-up link lives in the answering home's own
state/<task-id>.meta, so it can only bind work that home owns. When a
Relay-linked request is routed to a second mate, the task record lives in the
second mate's home, fm-x-link.sh failed with a bare "no such task ...meta", and
nothing else picked the promise up: only the soft acknowledgement was ever
posted. The typed promised-final path already supports --work-home
secondmate:<id>; the playbook simply never chose it.

- fmx-respond now states the routing rule crisply: a task in this home takes the
  lightweight link, and second-mate-routed work takes a promised-final
  commitment bound to that home, registered up front with the brief command
  carried into the routed worker's instructions.
- fm-x-link.sh refuses a task with no local record by naming the registered
  second mate whose home actually holds it and printing the promised-final
  registration command, with the exact --work-home when the match is
  unambiguous. A home with no registered second mates keeps the plain error.
- fm-backlog-handoff.sh reports, after a successful move, any moved key that
  still owes a public reply bound to main/<key>, since that binding no longer
  names the home owning the work. The move itself is never blocked.

Docs and the secondmate handoff prose follow the same rule. Tests cover the
refusal, its scoping, the unchanged local-link path, and both handoff outcomes
at the script boundary.

* docs(skills): add remote-secondmate recovery hint for false-negative verdicts (#2456)

* fix(skills): hint that remote secondmate liveness verdicts false-negative

fm-crew-state and fm-send routinely misreport a live remote secondmate
as dead; confirm against the pane before relaunching, and relaunch only
through fm-spawn.sh, never raw herdr pane surgery.

* no-mistakes: apply CI fixes

* fix(calm): keep Pi's export confirmation visible (#2461)

Pi 0.83.0 added a status line to every tool-expansion change, and Pi
updates the previous status line in place when two status messages
arrive back to back. Calm's post-export redraw cycled tool expansion on
the macrotask right after Pi printed "Session exported to: <path>", so
both expansion status lines coalesced over that confirmation and the
captain was left with no record of where their export landed.

Calm now repaints only the tool rows it presents, by invalidating each
row through the render context Pi hands its render slots, and requests
the surrounding redraw through setStatus. Neither appends to the
transcript. The repaint is still needed because Pi can re-render a row
asynchronously - the built-in edit row invalidates itself once its diff
is ready - and that re-render can land inside the window where /export
forces stock rendering.

The real-terminal /export case now asserts the confirmation is still on
screen after the redraw has settled, and that the redraw restored every
Calm-hidden row, instead of only racing the moment the confirmation
first appeared.

* feat(stow): add open-record persistence to /stow before reset (#2488)

* feat(stow): persist the open records a session is holding

/stow curated memory and captured session knowledge, but never touched
record state, while AGENTS.md called it an "unfinished-work sweep" and the
receipt declared the session "safe to re…
kaan-sirin pushed a commit to kaan-sirin/firstmate that referenced this pull request Aug 18, 2026
* fix(bin): strip every bracket tag, not just [key=...], from a status verb

status_line_verb only stripped a leading "[key=...]" token before the
colon, so a remote secondmate reply's leading "[corr=...]" correlation
tag stayed glued onto the returned verb word ("needs-decision
[corr=...]" instead of "needs-decision"). The open-decisions fold's
verb match then silently failed to recognize the line at all, so
fm-send --resolve-key refused to close a decision that was plainly
open on the status line.

Generalize the parser to strip every "[name=value]" tag before the
colon, in any order and count, so local and remote replies fold
identically.

* no-mistakes(review): Invalidate stale decision cursors after parser fix

* no-mistakes(document): Clarify status metadata verb parsing
guanchengh-lgtm added a commit to guanchengh-lgtm/firstmate that referenced this pull request Aug 25, 2026
* feat(bin): add deterministic agent lifecycle control (#1568)

* feat(bin): add deterministic agent lifecycle control

Separate firstmate's data plane from its control plane.

bin/fm-send.sh is the data plane: conversational text, always
routing-marked for a kind=secondmate target. That marking is right for a
message and wrong for a lifecycle command - a marked "/quit" arrives as
ordinary chat the agent reasons about instead of executing.

bin/fm-control.sh is the control plane: allowlisted interrupt, exit, and
transactional relaunch verbs addressed to an exact task id, with
per-harness mechanics owned by the executable bin/fm-control-lib.sh
rather than improvised in agent prose, and a verified postcondition for
every action. There is no arbitrary-text and no raw-key entry point.

relaunch runs as a transaction with a durable journal: it resolves the
profile, proves the work it must preserve is recoverable, records the
required progress note, stops the old agent, then delegates the launch
to its single owner, bin/fm-spawn.sh --relaunch, which adopts the
recorded endpoint and worktree instead of creating either. A refusal
before the stop leaves the record and instructions byte-identical; a
failure after it reports the concrete state rather than claiming an
agent that is not running. Teardown and discard stay separate and
explicit.

exit and relaunch require a backend with a recovery-grade agent-state
classifier, so zellij, orca, and cmux are refused rather than reported
as successful blind. A remotely placed secondmate is refused by name,
because its agent runs on a host where none of these postconditions can
be read.

* fix(control): resolve a recorded harness to its adapter before retiring wiring

fm-spawn arms per-task harness wiring on prefixes, because a task
launched from a raw command records that command's basename rather than
the exact adapter name. The control plane's retirement tables are keyed
by the exact adapter, so a task recorded as `grok-2` had its turn-end
token, private registry entry, and worktree hook pointer armed and never
retired - leaving a registry entry that outlived the agent that owned
it.

State the prefix rule once, in the capability owner, and resolve the
recorded value through it before every table lookup. bin/fm-send.sh's
composer-clear lookup reads the same owner instead of keeping its own
copy of which adapters need one.

* test(control): pin muse session-binding retirement across a harness switch

* no-mistakes(review): Resolve prefixed harnesses across lifecycle control verbs

* no-mistakes(review): Report interrupt delivery without fabricating cancellation state

* no-mistakes(review): Clear disabled relaunch trace context atomically

* no-mistakes(review): Clarify control interrupts and restore legacy send state

* no-mistakes(review): Refuse ambiguous relaunches and report exit delivery

* no-mistakes(review): Revalidate interrupts and accept interrupt-stopped exits

* no-mistakes(review): Lock descendant tasks before forced recursive teardown

* no-mistakes(document): Align lifecycle adapter documentation with control plane

* no-mistakes: apply CI fixes

* fix(bin): serialize fresh task publication with forced teardown

Forced secondmate teardown enumerated a home's task set, locked what it
found, then re-enumerated while removing. A fresh spawn takes only its
own per-task lock, so a record published inside that window was
invisible to the preflight and visible to the cleanup: it was
destructively processed while never lifecycle-locked.

Reproduced with real agents. A record published 0.249s after teardown
began was removed, its window closed, and its worktree returned to the
pool - while both commands reported success. A per-task lock cannot
protect a task that does not exist yet.

Add a per-home task-set lock guarding WHICH tasks a home has, as opposed
to the metadata lock guarding one task's record. Teardown takes it per
home, parent before child, before enumerating and holds it through
cleanup. A fresh spawn takes it before its own per-task locks and holds
it through publication; a relaunch is exempt, because it republishes an
existing task already covered by that task's control lock.

Either the spawn publishes first and the teardown's preflight covers it,
or the teardown owns the set and the spawn refuses. Both directions fail
closed, and both are pinned by tests that hold the lock rather than
racing on timing.

* no-mistakes(review): Serialize remote secondmate publication with forced teardown

* no-mistakes(review): Preserve remote spawn routing and state initialization

* no-mistakes(review): Serialize teardown when descendant state is absent

* no-mistakes(review): Cover symlinked descendant state refusal

* no-mistakes(document): Document task-set serialization safeguards

* no-mistakes(lint): Isolate task-set lock path resolution

* no-mistakes: apply CI fixes

* feat(stow): add tiered decaying memory management (#1984)

* feat(stow): tiered decaying memory with captain-gated offload to local excluded skills

Implement the captain-adopted /stow redesign from the v2 tiering report as
amended by the adoption decision:

- Per-entry trailing HTML-comment markers with three tiers named for their
  handling: pinned (no clock, no eviction), aging (stale after 30 days),
  perishable (stale after 7 days, mandatory checkable expiry condition).
- File-scoped defaults (captain.md and captain-shared.md pinned,
  learnings.md aging) with a self-describing legend line per file header.
- Reinforcement requires session evidence; re-reading memory never counts.
- Archive-not-delete: stale and budget-evicted entries move with provenance
  to the never-injected data/memory-archive.md; prune always means the cold
  tier, and a stale unique fact is never deleted.
- Captain-gated over-budget offload: staleness evaluated before scope, the
  sweep runs only when still over budget after decay and consolidation,
  proposals go through the receipt plus one durable captain-held backlog
  item, migration runs through the destination's normal path, and the
  memory entry leaves only once the destination is live.
- Offload destination per the adoption decision: a user-owned skill under
  .agents/skills/<freeform-name>/ excluded via the local .git/info/exclude,
  with the hard rule that stow never creates or writes a tracked skill.
- Five graduation moves, receipt verbs archived and proposed-offload, and
  the one-time non-destructive migration of unmarked legacy entries.

The public skills/stow/SKILL.md mirrors the generic parts (markers, decay,
archive exit, user-approved on-demand offload exit, migration) with no
firstmate-specific paths.

The load-bearing assumption that a git-excluded skill is still discovered
was verified empirically against Claude Code 2.1.226 (direct
.git/info/exclude scratch-repo test plus an in-repo ignored-probe test);
the dated evidence is recorded in docs/verification/stow-memory.md.

The graduation list's deletion move is deliberately narrowed to duplicates
already preserved by a stronger owner, reconciling the v2 report's retained
'deletion of a stale entry' wording with its own prune-always-archives
rule.

* no-mistakes(review): Persist legacy migration grace across stow passes

* no-mistakes(review): Enforce archival invariants and exempt default-pinned legacy entries

* no-mistakes(review): Clarify offload scope, archive placement, and marker boundaries

* no-mistakes(review): Enforce aging fallback and verify excluded skill loading

* no-mistakes(review): Fix stow decay, pinned offload, and archival safeguards

* no-mistakes(review): Preserve pinned entries, approvals, and archive provenance

* no-mistakes(review): Restrict stow mutations to editable memory files

* no-mistakes(review): Clarify skill destinations, collision checks, and migration legends

* no-mistakes(review): Resolve exclude paths for linked worktrees

* no-mistakes(review): Secure per-home excluded skill migration

* no-mistakes(test): Require explicit tier markers on new stow entries

* no-mistakes(test): Route missing shared legends to primary owner

* no-mistakes(document): Align stow documentation with tiered memory

* fix(stow): converge the pass on an over-budget home (dogfood D1-D3)

The dogfood run against a copy of the real over-budget home showed the
pass increasing the deficit from 624 to 1,107 estimated tokens and the
relief ladder provably unable to reach budget. Three skill-text fixes:

- D1: markers become single-token spellings (<!--a:DATE-->, <!--p:DATE-->,
  <!--P-->, <!--g-->), entries matching a pinned file default carry no
  marker, the per-file policy legend collapses to a one-line pointer
  naming the stow skill as the scheme owner, and marker/pointer bytes are
  explicitly counted content - roughly 76% less metadata cost on the
  dogfooded home's first installment.
- D2: the eviction rung gains a convergence precondition - total the
  eligible pool first, and when archiving all of it cannot reach budget,
  skip eviction entirely, archive nothing for budget reasons, and report
  the exempt pinned floor as the concrete inability in the final step.
- D3: budget eviction considers only dated aging entries; <!--g-->
  legacy-grace entries are ineligible until their grace cycle resolves,
  so eviction cannot cancel promised grace or invert against validation.

Public skill mirrors the D1 marker/pointer changes; D2/D3 are internal
because the public skill has no budget ladder.

* no-mistakes(test): Enforce evidence-only reinforcement during stow migration

* no-mistakes(document): Clarify stow receipt marker actions

* docs: add project vision (#1997)

* docs: add firstmate vision

* no-mistakes(test): Classify VISION.md as public product documentation

* no-mistakes(document): Restore approved one-file vision diff

* no-mistakes: apply CI fixes

* fix(spawn): force regular Pi TUI for crews (#2005)

* fix(spawn): force regular Pi TUI for crews

* no-mistakes(document): Documented Pi regular TUI launch mode

* fix(cmux): classify borderless Claude composers (#2029)

* fix(cmux): classify borderless Claude composer

* no-mistakes(review): Normalize cmux NBSP prompts across locales

* no-mistakes(document): Document cmux borderless Claude composer classification

* docs(stow): generalize read-before-write in the public stow skill (#2091)

The public installer-facing stow skill scoped its classify-then-replace
discipline to TODO/BACKLOG items only, so findings routed to a memory file
had no stated rule against a blind append or a wholesale overwrite.

Step 6 now classifies every finding against the destination's current
contents as new, duplicate, superseding, or obsolete, and states the
considered replacement each classification implies. The outcomes follow the
tiered-memory contract already in the file: an obsolete entry is refreshed,
archived, or replaced in a way that preserves its fact, a duplicate folds
into the entry that already carries it, and a superseded body worth keeping
leaves through step 7's existing exits rather than a second recovery
mechanism.

* fix: resurface durable supervision work after re-arm (#2065)

* fix(watcher): resurface durable work after downtime

* no-mistakes(review): Make watcher rearm recovery durable and cursor-safe

* no-mistakes(review): Persist safe recovery markers across migration lock recovery

* no-mistakes(review): Retain stale lock when recovery marker publication fails

* no-mistakes(review): Preserve delivery-gap recovery and quarantine malformed markers

* no-mistakes(review): Serialize recovery consumption and report acknowledgment failures

* no-mistakes(review): Centralize recovery publication before clearing watcher evidence

* no-mistakes(review): Guarantee recovery evidence across queue and lock handoffs

* no-mistakes(review): Publish recovery evidence before durable wake commits

* no-mistakes(review): Replace recovery marker Perl dependency with Node

* no-mistakes(review): Keep interrupted wakes durable until handling acknowledgment

* no-mistakes(review): Add post-handling durable wake acknowledgements

* no-mistakes(review): Enforce post-handling acknowledgement across recovery and AFK return

* no-mistakes(review): Bind wake acknowledgements to recovery generations

* no-mistakes(review): Align wake regressions with generation-bound acknowledgements

* no-mistakes(document): Document durable re-arm recovery semantics

* no-mistakes(lint): Resolve ShellCheck warnings in recovery and watcher tests

* no-mistakes: apply CI fixes

* test(watcher): assert post-handling wake replay

* no-mistakes(review): Prevent successor loops and adopt legacy wake generations

* no-mistakes(review): Rearm durable wakes without recursive successor recovery

* no-mistakes(review): Align recovery tests with handling marker state

* no-mistakes(review): Delay handling transition until successor launch is established

* no-mistakes(review): Confirm wake handling only after successful prompt delivery

* no-mistakes(review): Acknowledge AFK wakes only after evidence publication

* no-mistakes(review): Prevent AFK wake loss before post-handling acknowledgement

* no-mistakes(document): Document durable wake acknowledgement semantics

* no-mistakes(lint): Suppress false positive for recovery action output

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* ci: measure Herdr automation on Windows runners (#2100)

* ci: add Windows Herdr automation spike

* ci: run Windows spike on its pull request

* fix: wait for Windows Herdr command output

* fix: run ANSI probe in pane shell

* ci: keep Windows Herdr spike manually triggered

* docs: clarify Windows Herdr spike verdict

* feat(ahoy): guide captains through open decisions (#2099)

* Add guided ahoy decision flow

* no-mistakes(document): Document guided Ahoy decision flow

* fix(stow): enforce startup-memory budget decisions (#2110)

* Harden stow memory budget policy

* Refine internal stow offload policy

* no-mistakes(review): Enforce shared-budget decisions and autonomous offload

* fix(spawn): refresh pooled worktrees from origin before launch (#2116)

* fix(spawn): refresh pooled worktree base

* no-mistakes(document): Document spawn base-freshness invariant

* no-mistakes: apply CI fixes

* fix(composer): unify safe classification across backends (#2102)

* refactor(composer): one shape owner behind thin capture adapters, whole matrix fixed

Consolidate every composer shape - bordered boxes (all families, geometry,
titled bottom borders), bare agent-glyph rows and their wrap regions,
opencode's left bar, and pi's identity-gated separator pair - into
fm_composer_classify_screen in bin/fm-composer-lib.sh. Adapters now
contribute only a capture and a declarative capability descriptor
(styled/cursor/identity/rows); capability differences change how confidently
a shape is judged, never what the shapes are, so a new harness shape is
teachable in exactly one place.

Correctness fixes landed as part of the consolidation (audit
data/fm-composer-consolidation-audit-s1):
- locale-safe Unicode-space normalization in the shared owner (closes the
  fleet-wide half of #1988; cmux's local byte-exact NBSP case deleted;
  naming converges with PR #1995's normalization primitive)
- muse's bare glyph joins the shared set, unbreaking muse on herdr/cmux/orca
- orca learns the borderless bare shape, drops its backward-paged composer
  window, and can no longer classify a stale startup banner as the composer
- tmux tolerates a titled bottom border, unbreaking grok steering
- the left-bar shape makes opencode readable on every backend
- zellij gets a real classifier through dump-screen --ansi, replacing the
  content-diff submit heuristic that could confirm an undelivered message
  and close a --resolve-key decision (the fleet's only false positive)
- fm-spawn's kimi launch-readiness regex (the fourth shape copy) now routes
  through the shared classifier

The strict blank-row posture applies fleet-wide (captain decision
blank-row-injection-posture): no positive container proof = unknown = defer,
replacing tmux's permissive blank-cursor-row rule. Away-mode injection was
re-validated end to end on real tmux (defer on partial input and unproven
rows, clean delivery with swallowed-Enter retry into proven-empty
composers). The tmux submit core gains a baseline-idle turn-started
conversion so pi steering stays confirmed while its working screen hides
the composer; busy conversion without that baseline remains forbidden.

Plain-capture backends now degrade a glyph row carrying trailing text to
unknown instead of a false pending, per the approved capability rule.

Portable regressions pin the full byte-capture matrix from the audit under
a UTF-8 locale and LC_ALL=C, the strict-vs-permissive divergence, and
deliberate signal separation; the opt-in live guard
(tests/fm-composer-matrix-live-e2e.test.sh) verified every installed
harness against the real classifier, recorded in
docs/verification/runtime-backends.md.

* no-mistakes(review): Fix Pi glyph ambiguity and complete profile matrix

* no-mistakes(review): Preserve bare verdict when Pi identity probe is absent

* no-mistakes(review): Harden composer structure and titled-border geometry

* no-mistakes(review): Require proven idle baseline and strict Zellij guard

* no-mistakes(review): Reject box bottom borders as composer input rows

* no-mistakes(review): Prove Zellij probe typing before classifier retries

* no-mistakes(review): Preserve Pi identity uncertainty and scan full left-bar drafts

* no-mistakes(review): Verify Zellij text lands before submitting

* no-mistakes(review): Scope Zellij typing verification to selected composer content

* no-mistakes(review): Verify Zellij pastes through composer-scoped content deltas

* no-mistakes(review): Prove wrapped bare Zellij pastes through composer extraction

* no-mistakes(review): Invalidate stale cursorless composers below dead shell prompts

* no-mistakes(review): Handle shell prompt placeholders in composer extraction

* no-mistakes(review): Classify cursorless bare continuation regions safely

* no-mistakes(review): Reject stale cursorless containers below live activity

* no-mistakes(review): Preserve prompt glyphs in wrapped Zellij pastes

* no-mistakes(review): Reject live shell rows during composer extraction

* no-mistakes(review): Preserve wrapped glyph continuations through submit retries

* no-mistakes(review): Scope idle placeholders to proven positions

* no-mistakes(review): Restore boxed placeholders and live prompt reanchoring

* no-mistakes(review): Fix Zellij placeholder and wrapped glyph paste proof

* no-mistakes(document): Align composer architecture documentation

* no-mistakes(lint): Fix ShellCheck warnings in composer refactor

* no-mistakes: apply CI fixes

* docs(verification): record the trusted-checkout live matrix rerun

The pipeline's isolated gate worktree is untrusted, so claude, grok, and
muse stopped at first-launch trust dialogs there (the guard refuses to
confirm them by design). This rerun from the trusted checkout at the final
validated head verified all six installed harnesses, the strict blank-row
deferral, and the hardened zellij false-positive probe live.

* no-mistakes(document): Align composer verification evidence

* no-mistakes: apply CI fixes

* no-mistakes(review): Restore proven box bottom-cursor classification

* no-mistakes(review): Preserve styled placeholder-like drafts as pending

* no-mistakes(document): Align composer safety and Zellij delivery documentation

* no-mistakes: apply CI fixes

* docs(verification): refresh the live matrix with the final-head trusted rerun

The post-validation rerun from the trusted checkout verified all six
installed harnesses at the branch's final head, including Claude 2.1.227
(auto-updated since the audit's captures) and Grok, which the untrusted
gate worktree could not verify past their first-launch trust dialogs.

* fix(spawn): gate Pi TUI mode by CLI capability (#2117)

* fix(spawn): gate Pi regular TUI flag by capability

* no-mistakes(review): Document conditional Pi TUI capability detection

* no-mistakes(review): Pin Pi probing and launch to one executable

* no-mistakes(review): Preserve literal pinned Pi paths and update documentation

* no-mistakes(review): Defer pinned Pi path insertion until final substitution

* no-mistakes(document): Document version-safe Pi launch probing

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* docs(vision): elevate experience, pain narrative, and distro virtues (#2147)

* docs(vision): elevate experience, pain narrative, and distro virtues

Fold the captain's public vision framing into VISION.md: peace of mind as a
primary goal, multi-session context-switch pain as the problem one interface
solves, clone-and-run setup ease, self-evolution including community, and
explicit harness/backend orthogonality. Reconcile experience-as-garnish into
experience-as-purpose and update aligns/resists accordingly.

* docs(vision): state the experience goal positively

Drop the negative "not a smart workflow / useful tool / impressive technology"
pretext. Lead straight into the positive experience north star.

* feat(bin): reconcile inactive terminal crew outcomes (#2167)

* fix: reconcile inactive terminal outcomes

* fix: stream secondmate summary inputs

* no-mistakes(review): Fix reconciliation locking and request delivery retries

* no-mistakes(review): Prevent retries after unknown request delivery

* no-mistakes(document): Clarify inactive reconciliation cadence and receipts

* no-mistakes(lint): Quote terminal status arguments in reconciliation tests

* refactor: simplify inactive outcome reconciliation

* no-mistakes(review): Bound inactive reconciliation scans with durable progress

* no-mistakes(review): Bound reconciliation and deduplicate recovery notices

* no-mistakes(document): Document inactive outcome reconciliation contracts

* no-mistakes(review): Reject relative local secondmate parent routes

* no-mistakes(review): Key terminal receipts by spawn incarnation

* no-mistakes(review): Stabilize legacy receipts and lock reconciliation snapshots

* no-mistakes(review): Fail closed on invalid secondmate identity markers

* no-mistakes(document): Document durable inactive-outcome reconciliation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* ci: raise Herdr test timeout (#2191)

* fix: refresh stale Pi instructions after compaction (#2163)

* fix(session-start): refresh drifted instructions on stale rebuilds

* test(session-start): prove Pi instruction refresh end to end

* no-mistakes(review): Fix stale instruction refresh and baseline integrity

* no-mistakes(review): Preserve true-start baselines across Pi continuations

* no-mistakes(review): Correct Pi continuation classification and live expectation

* no-mistakes(review): Correct Pi continuation coverage documentation

* no-mistakes(review): Fix read-only refresh and exact Pi session restores

* no-mistakes(review): Classify Pi create-if-missing sessions correctly

* no-mistakes(review): Classify named Pi sessions using immutable headers

* no-mistakes(review): Correct Codex interactive coverage diagnostic

* no-mistakes(document): Document immutable Pi compaction instruction refresh

* no-mistakes(document): Correct Pi refresh documentation and validation claims

* feat: add deterministic condition-to-action watcher (#2200)

* feat(bin): add deterministic condition->action watch adapter on the process-event channel

Register a (condition, action) pair once with bin/fm-procevent-when.sh and the
existing process-to-event runner polls the condition tokenlessly, fires the
action at most once on a stable true, and wakes firstmate exactly once with the
captured outcome - instead of burning an agent turn per re-check.

The pair is stored privately under state/when/ and hash-bound by a trust record
the same way fm-check-register.sh binds a custom check, so a mutated spec is
refused without executing anything. A durable exclusive fired marker claimed
before the action makes restarts and re-polls unable to double-fire; every
failure path (mutated spec, condition error past budget, expired deadline,
failed action, uncaptured earlier fire) ends in a terminal captured outcome
that wakes firstmate rather than a silent retry. Eligibility stays a firstmate
judgment: only exact, safe, reversible actions may be bound, and judgment-
needing or destructive actions keep the wake-and-decide flow.

* no-mistakes(review): Harden when watcher concurrency, deadlines, timeouts, and output

* no-mistakes(test): Bind watcher actions to registered executable bytes

* no-mistakes(document): Correct condition-action watcher documentation

* no-mistakes(document): Clarify outcome wake re-announcement

* no-mistakes: apply CI fixes

* fix(bin): honor a decision key stated after the verb colon (#2202)

The open-decisions fold only recognized a [key=<slug>] token between the
verb and the colon (needs-decision [key=x]: note). The common worker
shape with the colon first (needs-decision: [key=x] note) silently
folded its stated key into the shared "default" bucket, so two open
decisions could collapse into one record and fm-send --resolve-key <x>
refused to close the decision it plainly named.

A complete token at the head of the note is now an equivalent stated-key
position for every keyed verb, shared by the whole-file and incremental
folds through the one _fm_decision_key owner. The documented
before-colon position wins when both are present, a token deeper in the
note stays prose, a bare keyless line still folds to "default", and a
stated-but-malformed slug is rejected rather than rewritten to
"default". A consumed note-head token is stripped from the note so both
positions yield identical records, and the incremental fold version is
bumped so persisted cursors folded under the old interpretation are
rebuilt from the authoritative log.

Fixes #2109

* fix(bin): prevent watcher recovery acknowledgement livelock (#2212)

* fix(bin): keep a recovery acknowledgement valid across republication

A watcher cycle that opened and closed while the model handled its drained
wakes minted a fresh recovery generation, which invalidated the exact
acknowledgement the drain had just printed. That acknowledgement then consumed
nothing, so the marker stayed pending and every later arm spent its whole cycle
re-announcing the same recovery instead of supervising - a livelock the home
could not leave on its own.

A downtime publication now reuses the generation of an outstanding handling
episode, so a close during the handling window cannot orphan the printed
acknowledgement. The acknowledgement itself separates its two facts: queue-row
consumption is bound to the monotonic --ack-through sequence and always
happens, while only retiring the episode is bound to --recovery-generation. A
generation that moved on is a non-fatal result that names its own remedy
instead of a refusal that consumes nothing.

* no-mistakes(review): Preserve recovery generations and consume stale acknowledgements safely

* no-mistakes(document): Document sequence-bound recovery acknowledgements

* feat(fmx-respond): consume Relay conversation chains (#2206)

* feat(fmx-respond): consume in_reply_to_chain conversation context

The relay's poll payload can carry in_reply_to_chain, an oldest-first
transcript of the surrounding conversation, but the mention-handling
procedure only ever read the immediate in_reply_to parent, so referents
like "this" in a standalone mention stayed unresolvable even when
context was delivered.

Teach fmx-respond to read the chain when present (optional and
backward-compatible: often absent today, kind label not required),
resolve referents against the whole transcript, and extend the
untrusted-content framing to every chain entry including the upcoming
kind=history entries. Document the field's wire shape in
docs/configuration.md as the firstmate-side owner.

* no-mistakes(document): Document Relay chain context ownership

* fix: parse decision verbs before status metadata tags (#2280)

* fix(bin): strip every bracket tag, not just [key=...], from a status verb

status_line_verb only stripped a leading "[key=...]" token before the
colon, so a remote secondmate reply's leading "[corr=...]" correlation
tag stayed glued onto the returned verb word ("needs-decision
[corr=...]" instead of "needs-decision"). The open-decisions fold's
verb match then silently failed to recognize the line at all, so
fm-send --resolve-key refused to close a decision that was plainly
open on the status line.

Generalize the parser to strip every "[name=value]" tag before the
colon, in any order and count, so local and remote replies fold
identically.

* no-mistakes(review): Invalidate stale decision cursors after parser fix

* no-mistakes(document): Clarify status metadata verb parsing

* fix(bin): collapse duplicate supervision wakes (#2287)

* fix: collapse duplicate supervision wakes without losing legitimate updates

One remote-secondmate note produced two handling turns (a procevent check
wake published before autohandle, then a signal wake for the same mirrored
bytes), already-ingested replays such as a cursor-loss whole-log recapture
still woke with nothing to do, this home's own bookkeeping closes (fm-send
--resolve-key, the pending-reply escalation close, the captain-held
transfer) re-woke the session that wrote them, and turn-ended-only wakes
were annotated with already-announced status lines that looked like fresh
progress.

Dedup rules, each at its layer's one owner:
- fm-procevent.sh: an adapter may declare 'self-announcing'; the runner
  then applies first and publishes a check wake only for what remains
  unhandled. fm-procevent-remote-reply.sh declares it: the mirrored status
  append is the single announcement, so a fully applied capture publishes
  nothing and a byte-identical replay stays completely quiet. All other
  adapters keep strict publish-before-apply.
- fm-wake-lib.sh: fm_wake_signal_sig/seen_path/seen_current now own the
  watcher's signal signature and .seen-* marker format, plus
  fm_wake_status_append_self_announced, the guarded bookkeeping append
  that advances the marker only over exactly its own bytes and fails
  toward waking on any pending or interleaved foreign write.
- fm-send.sh, fm-pending-reply-lib.sh, fm-decision-hold.sh: bookkeeping
  closes go through that guarded append; escalation opens stay plain
  appends because a new blocker must wake.
- fm-wake-lib.sh annotations: a historical (turn-ended-only) row skips its
  status annotation only when the file's signature provably matches the
  seen marker; anything unannounced keeps annotating.
- fm-classify-lib.sh: a kind=secondmate task's status signal is never
  absorbed as provably-working, because that stream is the routed-reply
  channel the parent must read.

Also fixes a pre-existing exit-path deadlock the regression run reproduced:
a TERM inside a recovery-marker critical section left fm_lock_try_acquire
spinning against this same process's abandoned hold; a self-held lock is
now reclaimed (a subshell still waits on its parent's live hold).

Regression tests drive the real wake functions and executables in both
directions: each duplicate case collapses, while a new remote reply, new
decision, new blocker, merge result, failure, first status change, and a
later different note on the same task all still wake.

* no-mistakes(document): Document wake deduplication contracts

* feat: add Cursor CLI crew harness (#2238)

* feat(harness): add Cursor Agent CLI adapter

# Conflicts:
#	bin/fm-spawn.sh

* fix(composer): read cursor-agent's reverse-video placeholder as idle

cursor-agent renders its idle composer placeholder dim (SGR 2) but paints the
cell under the terminal cursor in reverse video (SGR 0;7). Reverse video is
neither dim nor a dark truecolor foreground, so the shared ghost stripper keeps
that one character and an idle composer reduces to a lone `P`. Judged on its
own, that remnant reads `pending` on a genuinely idle pane, which defers
away-mode escalation indefinitely on the styled cursorless backends.

Teach the ONE fleet-wide classifier the shape instead of adding an adapter-local
copy: register `→` as an agent prompt glyph so the composer row is structurally
findable at all (without it the bottom-most shape is a stale shell prompt echo
in the scrollback), add both verified placeholders to the idle set, and consult
the styling-independent plain row when the styled row is only a remnant.

The plain-row branch demands the remnant be a proper, strictly shorter substring
of a plain row matching a fully anchored placeholder. Real typed text is
uniformly bright, so stripping leaves it equal to the plain row and it stays
`pending` - verified live against a pane where the typed text was exactly the
placeholder string.

Verified live on cursor-agent 2026.08.11-e8db854; the regression pins the real
captured bytes and asserts the remnant survives stripping, so the case cannot go
vacuous if the stripper later learns SGR 7.

Co-authored-by: Amplify Logic AI <lars@sockinator.co>

* feat(cursor): narrow cursor identity and order its marker before CLAUDECODE

Cursor ships two executable names - `cursor-agent` and the legacy alias `agent`
- and runs as a bundled node script, so tmux reports the pane command as a bare
`node`. Neither `agent` nor `node` can be trusted by name, so identity gets one
owner in bin/fm-cursor-lib.sh that demands cursor's own name or install tree in
the path or argv[0], from the structural signal only. Probing an arbitrary pid's
executable during a liveness poll would execute a stranger's binary, which is
the hazard that rule exists to close.

Two consequences wired up:

Detection. cursor-agent does NOT clear an inherited CLAUDECODE, so a cursor
worker launched under a claude primary carries both markers and whichever is
tested first wins. The cursor markers are ordered ahead of the CLAUDECODE check;
fm-spawn additionally clears foreign markers at the launch boundary. Both are
kept deliberately - launch sanitization only covers sessions fm-spawn started,
while the ordering also covers a cursor session started by hand. Verified live
that CURSOR_INVOKED_AS is set on the agent process and CURSOR_AGENT=1 on the
child/tool processes fm-harness.sh actually runs as.

Pane liveness. A cursor pane now classifies `agent`. An unrelated node or agent
stays `other`, which the liveness callers already fold into `ambiguous` rather
than `dead`, so a stranger's node pane is never reported agent-free.

Resolution prints the STABLE launcher rather than the canonical target: identity
is proven through canonicalization, but cursor's canonical path carries a
version its own auto-update replaces, and pinning that would strand a task on a
version that can vanish.

The regression drives the two identity signals apart - a cursor-named executable
outside any cursor tree, and a non-cursor-named alias inside one - and asserts
each carries a verdict alone, so no single vendor string is load-bearing. Its
negative controls are real spawned processes, not fixtures.

Verified live on cursor-agent 2026.08.11-e8db854.

Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* feat(cursor): classify cursor busy state from its own turn transcript

Cursor shipped as "unknown cursor-unverified" on the premise that it exposes no
semantic turn lifecycle, only a rendered "Working" footer. That premise is
wrong: cursor-agent persists an append-only JSONL transcript per conversation
and brackets every submitted turn with a role:user open and a typed turn_ended
close. Verified live on 2026.08.11-e8db854, including the interrupt path, where
Escape closes the turn with status "aborted" - so this source covers manual
interruption, which Claude's Stop hook does not.

That makes it a genuine pull source in the muse mould rather than the rendered
text the redesign forbids: no writer, no arm, no gen, nothing seeded that could
never be cleared. Cursor's `ctrl+c to stop` footer stays out of the verdict, and
herdr's narrower native streaming state cannot stand in for it either.

Binding deliberately does not reconstruct cursor's workspace-slug directory
name. That slug collapses path separators, so rebuilding it would be a guess
that could bind the wrong pane; cursor records the exact absolute workspace path
in each project's .workspace-trusted, and the binding matches on that. A
conversation recorded as prior at spawn is excluded, so a relaunch in a reused
worktree folds its own turn rather than its predecessor's. Requiring a unique
remaining conversation keeps zero and several both unknown, because neither
proves anything about the current turn.

The regression pins the fold with real transcript files and asserts the
dangerous direction stays closed: an unresolvable binding, a record-free file,
an unclaimed workspace, and a workspace-path PREFIX all read unknown, never
idle. The prefix case uses an opaque fixture slug so a slug-rebuilding
implementation cannot pass it.

Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* feat(cursor): make the cursor launch runnable and give it lifecycle control

Five gaps that together kept a cursor crewmate from being drivable end to end.

Launch. The template invoked `cursor agent`, but `cursor` is not the CLI - the
installed names are `cursor-agent` and the legacy alias `agent` - so the command
could not run at all on a machine with a normal cursor install. It now resolves
through the verified owner, which also refuses a spawn loudly instead of leaving
a pane that dies with command-not-found and reads as a wedged worker.

Session binding. fm-spawn writes state/<id>.cursor-session so the busy fold can
find this pane's transcript, and teardown removes it.

Lifecycle control. No cursor PR touched fm-control-lib.sh, so
`fm-control <id> interrupt|exit|relaunch` could not drive a cursor worker at
all. Verified live: interrupt is a single Escape, exit is /exit, and cursor does
NOT repollute its composer with the cancelled prompt, so unlike muse it needs no
clear key. Secondmate is refused, matching the spawn refusal.

Submit acknowledgement. cursor parks its terminal cursor outside its composer,
so the composer verdict on tmux is always `unknown` and a submit could never be
acknowledged from the composer alone. The submit core's existing idle-to-busy
transition covers that case, but only if the pane's busy footer is recognised,
so cursor's `ctrl+c to stop` joins the harness-less default union the submit
cores read. The TOKEN is matched rather than the spinner verb: the same version
rendered both `Working` and `Running` in consecutive turns.

Bootstrap. A configured cursor crew harness with no cursor executable is now a
loud MISSING diagnostic rather than a first-spawn failure, and it accepts either
installed name.

Interrupt cancellation is deliberately left unconfirmed. The transcript does
type an aborted close, but its post-interrupt write latency measured as
variable - sometimes seconds, sometimes not within twenty - so a claim built on
it would be unreliable. Normal turn completion is prompt, which is what the busy
fold actually depends on.

Two inherited tests are corrected rather than deleted: the busy test asserted
cursor could have no semantic source, and the launch test pinned the literal
`cursor agent` string. Both now pin the verified behaviour, including that the
launch never allocates a second worktree.

Co-authored-by: ABHISHAKE KUMAR BOJJA <abojja@uvic.ca>
Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* docs(cursor): record the verified crewmate facts and extend the drift guard

The inherited cursor entry was written against 2026.08.04-aaa8809 and several of
its claims no longer hold: it named `cursor agent` as the binary (not the CLI
name), listed six Grok model ids of which the live catalog now returns two, and
recorded busy state, exit, interrupt, and skill invocation as unverified.

Replaced with what was measured against 2026.08.11-e8db854, including the two
facts most likely to be rediscovered painfully: cursor runs as a bundled node
script so its pane title is a bare `node`, and it parks its terminal cursor
outside its composer, which makes the tmux composer verdict permanently
`unknown` by design rather than a defect to chase.

Model ids now route to `--list-models` for the account instead of a fixed list,
since that list is exactly what drifted.

The live drift guard covers cursor, resolving it through the same verified owner
fm-spawn uses and passing --trust so the probe cannot hang on the workspace
prompt. Run against every installed harness: 8 checked, all alive, with cursor
reporting title='node' foreground=[.../cursor-agent] - the drift shape this
guard exists to catch.

Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* docs(agents): record the cursor session-binding state file

The state/ layout section is the inventory every session reads; a busy-source
binding that fm-spawn writes and teardown removes belongs in it alongside muse's.

* no-mistakes(review): Sanitize ambient Cursor marker in harness tests

* no-mistakes(review): Validate Cursor models against live catalog

* no-mistakes(review): Reject unsupported secondmates before binary preflight

* no-mistakes(review): Narrow Cursor ancestry detection to structured process identity

* no-mistakes(review): Parse Cursor transcripts and sanitize inherited markers

* no-mistakes(review): Handle malformed Cursor transcript records safely

* no-mistakes(review): Validate malformed Cursor closes in fallback parser

* no-mistakes(review): Retire stale Cursor bindings during relaunch

* no-mistakes(review): Fix Cursor drift guard command variable

* no-mistakes(review): Narrow Cursor identity to versioned install trees

* no-mistakes(document): Document Cursor harness boundaries

* refactor(composer): move the delivery busy footers to the shared owner

The per-harness rendered busy footers lived in bin/fm-tmux-lib.sh under
FM_TMUX_* names, so cursor's `ctrl+c to stop` signature - and every other
harness's - was reachable only from tmux. That placement was wrong on its own
terms: herdr, zellij, cmux, and orca run the same harnesses and face the same
question these footers answer, which is whether a submitted Enter actually
landed. Nothing about the signature is tmux-specific.

Moved verbatim into bin/fm-composer-lib.sh, the shared composer/delivery owner
every backend already sources, and renamed to FM_DELIVERY_* so the names stop
claiming a scope they never had. All five adapters now reach cursor's signature;
verified per adapter rather than assumed.

The boundary the move must not blur is stated where it now lives: this is a
DELIVERY guard, never a worker-state source. Confirming a keystroke landed is a
different question from asking what a worker is doing, and bin/fm-busy-lib.sh
remains the semantic owner that forbids classifying a harness from rendered
text. Cursor still classifies only from its transcript fold, which is already
backend-agnostic because it folds a file rather than reading a pane - the same
verdict on all six backends.

The old FM_TMUX_* aliases are dropped rather than kept as dead shims: nothing
outside the moved block referenced them except fm-busy-lib.sh's grok fallback,
which now reads the new name. The documented operator override, FM_BUSY_REGEX,
is untouched.

Also removes a dead duplicate CURSOR_INVOKED_AS check in bin/fm-harness.sh,
unreachable behind the marker check above it.

* no-mistakes(review): Correct shared delivery guard ownership references

* no-mistakes(document): Document shared delivery guards and Cursor backend limits

* no-mistakes: apply CI fixes

* fix(composer): bound a bare composer's wrap region at a half-block rule

A live cursor crewmate on herdr classified its IDLE composer as `pending`, and
fm-send consequently exited 1 with "delivery unconfirmed" on a message that had
actually landed. The cause is not cursor-specific.

Herdr draws a composer's top and bottom rules with the half-block glyphs U+2584
and U+2580 rather than the box-drawing family. fm_composer_row_has_edge knew
only the box-drawing set, so no box was detected; the composer was found as a
BARE row, and its wrap region - which extends while rows are non-blank and carry
no structural edge - walked straight through the composer's own closing rule and
swallowed the model and path footer below it. That footer is real text, so the
region classified pending on a genuinely idle pane.

Teaching the shared edge detector the half-block glyphs bounds the region at the
closing rule. Measured on the captured bytes of a real herdr cursor pane: the
same capture that read `pending` now reads `empty`.

This is a shared shape-path change, so it is deliberately narrow - it adds
glyphs to the edge vocabulary and changes no verdict logic - and the whole
composer and backend suite is green, including the other harnesses' herdr
fixtures.

The regression pins the real captured shape and asserts the footer content is
genuinely present, so the case cannot pass vacuously if the region were ever
bounded for some unrelated reason.

* fix(herdr): confirm a cursor submit from the rendered-footer transition

Herdr's composer-shape fix made an idle cursor pane classify `empty`, but
`fm-send` still exited 1 with "delivery unconfirmed" on messages that had
actually landed. Live measurement found the second, independent cause.

Herdr reports a cursor pane `agent_status=blocked` in EVERY state - idle,
mid-turn, and after - so the submit path's idle-baseline native confirmation is
structurally unreachable for cursor and every send falls into the composer
branch. That branch reads cursor's mid-turn composer row, which renders its own
`Add a follow-up` placeholder beside a right-aligned `ctrl+c to stop`. That
token is composer content, so the verdict is `pending` on a composer holding no
user text at all, and the Enter-retry budget then reports pending.

The escape is the same semantic signal the native path uses, read from the
pane's verified busy footer instead of native agent-state, and it is the
rendered-footer twin of the tmux submit core's turn-started confirmation: an
idle-to-busy transition ACROSS our Enter proves the harness accepted the
submission. The baseline is taken before the first Enter and only when the
native baseline was not legibly idle, so the idle-baseline path still never
reads pane content and a pane already mid-turn before we typed keeps reporting
`pending` rather than borrowing another turn as proof of this delivery.

The composer verdict is deliberately NOT relaxed. A right-aligned status token
on the composer row stays content for every other caller, including the
away-mode pre-injection guard, and the shared cursorless submit core is left
untouched so zellij, cmux, and Orca keep the behavior their own follow-up owns.

Verified live on herdr 0.8.0 and cursor-agent 2026.08.11-e8db854 in an isolated
lab session: `fm-send` now exits 0 and the steer executes, interrupt cancels a
running turn, `/exit` stops the agent, and teardown clears the record. All seven
panes of the running default session classify identically before and after the
shape fix, so no other harness regressed.

* no-mistakes(review): Prevent working Herdr baselines from falsely confirming delivery

* no-mistakes(document): Correct Cursor harness and backend documentation

---------

Co-authored-by: ABHISHAKE KUMAR BOJJA <abojja@uvic.ca>
Co-authored-by: Amplify Logic AI <lars@sockinator.co>
Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* fix(bin): require quota-axi 0.1.25 (#2300)

* fix: raise quota-axi floor to 0.1.25 for Cursor CLI quota awareness

Homes on latest main need quota-axi #87 so Desktop-absent CLI machines report a fresh Cursor quota instead of a false sign-in-required.

* no-mistakes(document): Update quota floor documentation pointer

* fix(bin): prevent false Pi watcher alarms during hand-offs (#2304)

* fix(guard): stop the false send-time watcher-down alarm on Pi primaries

On a Pi primary the watcher process is not the liveness signal. The Pi
extension tears the watcher down on every actionable wake and spawns the
replacement itself, so the singleton lock is legitimately unheld between
cycles: every one of the 799 cycles in a live primary's ledger ends with
lock_after=pid:none, and a live capture caught the guard verdict flipping to
no-watcher during one hand-off with the beacon 63s old.

bin/fm-guard.sh classified Pi as a persistent-watcher harness, which demands a
live identity-matched lock holder at all times, so any guarded command landing
in a hand-off painted the full WATCHER DOWN - SUPERVISION IS OFF banner and
told firstmate to repair a cycle the extension already owns and is restoring.

Add an extension supervision model for pi and pi-signed. A live
identity-matched watcher stays the ordinary healthy state; an unheld lock is
healthy only while the beacon is fresh within grace AND a live Pi session
provably owns continuity - both primary extensions recorded in their state
markers at their current on-disk builds by the process named in state/.lock,
with that process still alive. Without that proof the banner fires exactly as
before, so an unloaded, version-drifted, or exited Pi session is loud
immediately and a cycle the extension never restores is loud once the beacon
passes grace. The queued-wake warning, the PID-strict turn-end guard, and
every other primary's detection are untouched.

Fold session-start's duplicate Pi marker predicate into the shared library so
the ownership contract has one owner.

* no-mistakes(review): Restrict Pi hand-off tolerance to unheld watcher locks

* no-mistakes(document): Document Pi watcher hand-off supervision

* feat: support Cursor Agent CLI as a primary harness (#2305)

* feat(cursor): add Cursor Agent CLI primary hooks, park supervision, and session start

Register a tracked project-scope .cursor/hooks.json for Cursor's stop,
sessionStart, preCompact, and preToolUse steps.

bin/fm-turnend-guard-cursor.sh owns Cursor's turn boundary as a park: it
foregrounds the watcher arm, holds the boundary open until an actionable close,
and returns that wake as one follow-up. Exit 2 is a silent no-op on Cursor's
stop step, so the adapter never uses it. The follow-up loop is bounded twice,
by Cursor's own loop_limit and by the payload's loop_count.

bin/fm-sessionstart-cursor.sh delivers the digest as additional_context at
sessionStart, and stages it for the next turn boundary at preCompact, which
cannot inject context.

Cursor also loads the tracked Claude settings, so bin/fm-hook-host-lib.sh lets
each tracked Claude-shaped entrypoint stand down on a Cursor-delivered payload
rather than running every covered event twice.

bin/fm-tmux-lib.sh reclassifies a Cursor pane's composer cursorlessly, because
Cursor parks its terminal cursor outside the composer, which restores a genuine
composer-empty proof and unblocks away-mode escalation delivery.

* feat(cursor): make Cursor Agent CLI a verified primary harness

Resolve Cursor in the session-lock ancestry through bin/fm-cursor-lib.sh, which
a Cursor primary needs before it can hold its own home lock, and classify its
stop-hook park under the autoarm supervision model so the mid-turn pull guard
stops reporting a healthy between-turns watcher as down.

Read a Cursor pane's composer cursorlessly on tmux, gated on Cursor's own
structural process identity, which restores a genuine composer-empty proof and
lets away-mode escalations reach a Cursor primary with no daemon change.

Lift the secondmate refusals in bin/fm-spawn.sh and bin/fm-control-lib.sh now
that the supervision protocol exists and is recorded.

Cover the whole surface with a portable regression over real processes, an
opt-in live guard against the installed cursor-agent, and dated per-harness
evidence.

* docs(cursor): record Cursor as a verified primary across the owning surfaces

Update the turn-end guard, session-start, arm-seatbelt, cd-guard, watcher
continuity, architecture, configuration, README, and harness-adapters owners,
and add dated live evidence to the supervision and runtime-backend verification
records. Correct the recorded Cursor tmux composer verdict: the cursor-anchored
read is still blind, but the composite reader is no longer unknown.

Lift the remaining remote-secondmate refusal missed in the previous commit, and
add the new libs to the existing fixtures that copy a fixed dependency list.

* refactor(cursor): name the park's stand-down condition for both its causes

Also record that Cursor's preCompact firing itself is not yet live-verified,
while the static evidence that it cannot inject context, and the staging path
that follows from it, both are.

* test: give the pretool fixtures their new dependency and one lint owner

The cd-guard fixture copies a fixed dependency list and now needs the shared
hook-host predicate. Both pretool suites also asserted cleanliness with a bare
shellcheck call, a second and weaker copy of the lint definition that
bin/fm-lint.sh owns: it omits --external-sources, so it failed the moment these
checkers sourced a shared library. They now delegate to that owner.

* test: assert the cursor secondmate contract instead of its removed refusal

A cursor secondmate now launches, so the suite asserts what its park actually
needs: --trust so the home's project hooks load at all, its own home pinned as
the workspace, and the autoarm supervision model inherited across the launch.

* no-mistakes(review): Serialize Cursor wakes and bind staged context

* no-mistakes(review): Serialize Cursor context and nag state commits

* no-mistakes(review): Enforce Cursor ceiling before staged context delivery

* no-mistakes(review): Serialize Cursor claims and staged context

* no-mistakes(review): Serialize Cursor ownership and state commits

* no-mistakes(review): Protect Cursor context across session takeover

* no-mistakes(review): Preserve Cursor context across session takeover

* no-mistakes(review): Enforce owner-keyed Cursor staged context

* no-mistakes(review): Atomically claim Cursor follow-ups and staged context

* no-mistakes(review): Defer Cursor preCompact staging and simplify supersession

* no-mistakes(review): Serialize Cursor park commits and defer preCompact

* no-mistakes(review): Stop Cursor parks after session takeover

* no-mistakes(test): Route Cursor preCompact context through stop follow-up

* no-mistakes(document): Update Cursor primary documentation

* revert(cursor): cut preCompact staging from this change

Carrying a compaction digest across two concurrently running stop hooks kept
producing races that could deliver it twice or strand it indefinitely, and
closing them kept enlarging a critical section inside a hook Cursor awaits at
the turn boundary. Native preCompact firing was never observed either, so the
surface has no empirical basis yet.

Remove the adapter, its registration, its staged path in the park, and its
tests, and record the surface as deferred and uncovered alongside the Codex
interactive TUI. A regression now asserts preCompact stays unregistered so it
cannot return without its own design and evidence.

This change ships the proven core only: the turn-end follow-up park, the
run-tier session start, and away-mode delivery.

* no-mistakes(review): Correct Cursor park supersession documentation

* no-mistakes(document): Clarify Cursor run-tier verification ownership

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

---------

Co-authored-by: kunchenguid <kun-1@kunchenguid.com>

* feat(bin): add decline and repair paths for decision holds (#2330)

* feat(bin): add unrouted close paths to the captain decision gate

A captain who declines a held decision leaves no follow-up work to route,
so `resolve` could not express that answer: it requires at least one
`--routed-to` task. The only way to close such a hold was a direct
`tasks-axi done`, which never writes the durable resolution record the
completion gate reads, so the originating investigation could no longer
pass `verify` and its cleanup stayed blocked.

Add two close paths that route no work:

- `decline` closes an actively held hold with a recorded captain decision
  and no routed task. It refuses while any task is still blocked by the
  hold, because releasing routed work without recording it is `resolve`'s
  job.
- `repair` records the missing resolution block on a hold that was already
  closed outside this script. It never reopens a hold and never clears a
  dependency edge, and it refuses a hold that is still actively held.

Both require a non-empty captain decision file and share `resolve`'s
digest-based retry identity, so an exact retry is idempotent while a
changed decision is rejected. The recorded body now also names which path
closed the hold, and each routed entry regains its own line.

The gate itself is unchanged: an unanswered decision still fails
completion and blocks teardown, and neither new path can close a hold
without the captain's recorded word.

* fix(bin): require captain-hold provenance before repairing a decision

`repair` checked only that the backlog item was kind captain and Done, so
an ordinary captain-kind task that was never held for the captain could be
closed, repaired, and then pass the completion gate.

tasks-axi keeps `hold_kind` through a close, so it is the surviving proof
that an identity really was a captain hold. Require it before writing the
resolution record, and cover the case in the gate regression.

* no-mistakes(document): Correct decision-hold lifecycle documentation

* fix(bin): surface buried wake status lines once (#2331)

* fix(bin): surface buried status notes on wake drain

A note: answer immediately followed by a routine note was dropped because
annotations kept only the newest line and note: never enters OPEN DECISIONS.
Present every unread note and pending-reply resolution since the last drain
cursor, and annotate every unread line on a queued signal.

* no-mistakes(review): Fix unread status cursor races and overflow

* no-mistakes(review): Preserve cursors when status span reads fail

* no-mistakes(review): Make status presentation transactional under I/O failures

* no-mistakes(review): Simplify unread status cursor and presentation locking

* no-mistakes(review): Align cursor failure regressions with transactional presentation

* no-mistakes(review): Retire stale presentation cursors during task teardown

* no-mistakes(review): Preserve routine status until signal annotation

* no-mistakes(review): Correct unread status cap documentation

* no-mistakes(document): Document unread wake status presentation

* no-mistakes(lint): Fix wake surfacing ShellCheck warnings

* no-mistakes: apply CI fixes

* feat: add max Calm presentation level (#2334)

* feat(calm): add a max presentation level that hides mid-turn working notes

Calm's home-local preference becomes a three-state level instead of a
boolean: "off" is stock Pi, "on" is today's Calm, and "max" is Calm plus
hiding the assistant text of messages the model did not end its response
with. `/calm max` selects it from any state, a plain `/calm` steps max
back to ordinary Calm and otherwise keeps the existing on/off cycle, and
any other argument keeps that cycle too.

`config/calm` now persists "max" as its own literal value, so a session
start, resume, fork, or reload restores the stored level rather than
treating it as unrecognized and dropping to off.

The hide rule keys on Pi's intrinsic per-message stopReason: "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, because suppressing it would also stop a genuine reply from
streaming. The existing assistant layout adapter filters the blocks out
of the same shallow presentation copy it already uses for collapsed
thinking, so the message, model context, session storage, /export, and
delivery are untouched and a hidden mid-turn row collapses to zero
height. The new "assistant-working-note" class keeps that choice in the
visibility policy owner, where ordinary Calm keeps it visible.

* no-mistakes(document): Clarify Calm max persistence and taxonomy

* feat(calm): hide mid-turn working notes by default (#2339)

* feat(calm): make hiding mid-turn working notes the ordinary Calm state

Calm collapses back to the two-state on/off toggle it was before the max
presentation level, with max's hide rule promoted into ordinary Calm.
Calm on now hides mid-turn assistant working notes in addition to what it
already hid, and the /calm command parses no argument again.

The hide rule itself is unchanged: assistant text is removed from the
shallow presentation copy when the message's own stopReason is "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, so a genuine reply still streams. The message, model context,
session storage, /export, and delivery remain untouched.

config/calm persists only "on" and "off" again, but the reader still maps
a persisted "max" to on so a home upgraded from the removed level keeps
Calm on instead of dropping to off.

The mid-turn hide is now default behavior rather than an opt-in level, so
docs/calm.md documents it for users, docs/configuration.md records the
two written values plus the legacy max mapping, and the feasibility
taxonomy drops its level-scoped wording.

* no-mistakes(document): Document ordinary Calm working-note hiding

* chore: store no-mistakes test evidence in the repo (#2355)

* chore: ignore scratchpad/ at the repo root (#2359)

* fix(ci): fail hung Herdr behavior runs in 20 minutes (#2413)

A wedged family-run step was occupying the runner until the 75-minute
job cap; bound that step so cleanup and timing artifacts still upload.

* fix: keep the public promise reachable when work is routed to a second mate (#2457)

The lightweight Relay follow-up link lives in the answering home's own
state/<task-id>.meta, so it can only bind work that home owns. When a
Relay-linked request is routed to a second mate, the task record lives in the
second mate's home, fm-x-link.sh failed with a bare "no such task ...meta", and
nothing else picked the promise up: only the soft acknowledgement was ever
posted. The typed promised-final path already supports --work-home
secondmate:<id>; the playbook simply never chose it.

- fmx-respond now states the routing rule crisply: a task in this home takes the
  lightweight link, and second-mate-routed work takes a promised-final
  commitment bound to that home, registered up front with the brief command
  carried into the routed worker's instructions.
- fm-x-link.sh refuses a task with no local record by naming the registered
  second mate whose home actually holds it and printing the promised-final
  registration command, with the exact --work-home when the match is
  unambiguous. A home with no registered second mates keeps the plain error.
- fm-backlog-handoff.sh reports, after a successful move, any moved key that
  still owes a public reply bound to main/<key>, since that binding no longer
  names the home owning the work. The move itself is never blocked.

Docs and the secondmate handoff prose follow the same rule. Tests cover the
refusal, its scoping, the unchanged local-link path, and both handoff outcomes
at the script boundary.

* docs(skills): add remote-secondmate recovery hint for false-negative verdicts (#2456)

* fix(skills): hint that remote secondmate liveness verdicts false-negative

fm-crew-state and fm-send routinely misreport a live remote secondmate
as dead; confirm against the pane before relaunching, and relaunch only
through fm-spawn.sh, never raw herdr pane surgery.

* no-mistakes: apply CI fixes

* fix(calm): keep Pi's export confirmation visible (#2461)

Pi 0.83.0 added a status line to every tool-expansion change, and Pi
updates the previous status line in place when two status messages
arrive back to back. Calm's post-export redraw cycled tool expansion on
the macrotask right after Pi printed "Session exported to: <path>", so
both expansion status lines coalesced over that confirmation and the
captain was left with no record of where their export landed.

Calm now repaints only the tool rows it presents, by invalidating each
row through the render context Pi hands its render slots, and requests
the surrounding redraw through setStatus. Neither appends to the
transcript. The repaint is still needed because Pi can re-render a row
asynchronously - the built-in edit row invalidates itself once its diff
is ready - and that re-render can land inside the window where /export
forces stock rendering.

The real-terminal /export case now asserts the confirmation is still on
screen after the redraw has settled, and that the redraw restored every
Calm-hidden row, instead of only racing the moment the confirmation
first appeared.

* feat(stow): add open-record persistence to /stow before reset (#2488)

* feat(stow): persist the open records a session is holding

/stow curated memory and captured session knowledge, but never touched
record state, while AGENTS.md called it an "unfinished-work sweep" and the
receipt declare…
mremond added a commit to mremond/firstmate that referenced this pull request Aug 26, 2026
… parser

Rebasing onto main put this change beside kunchenguid#2280, which made verb parsing
stop at the first "[name=value]" tag. Both edit status_line_verb with
different intents, so the resolution keeps both rules rather than letting
one overwrite the other:

- kunchenguid#2280's tag stop is kept verbatim and now owns every BRACKETED tag,
  including the "[corr=...]" form fm-secondmate-report.sh writes. The
  bracket-unwrapping arm this branch had added to the token test is
  therefore removed as unreachable.
- This branch's token walk is kept and narrowed to the UNBRACKETED token
  fm-pending-reply-lib.sh writes, which the tag stop does not reach.

Two consequences of standing beside kunchenguid#2280 rather than before it:

The fold version had collided at 4: kunchenguid#2280 spent it on the tag-stop
parser and this branch had spent it on the token read. A cursor
persisted under kunchenguid#2280's reading predates this one and must still be
rebuilt, so the version moves to 5.

A bracketed impostor is dropped from the malformed-token list. On main
today "resolved [corr=deadbeef] [key=victim]:" already reads as the bare
verb, as does "resolved [anything at all] [key=victim]:", because the tag
stop ends the parse at the opening bracket regardless of content. That is
kunchenguid#2280's reviewed contract; asserting otherwise here would narrow it. The
unbracketed impostors it owns stay strict and still fold as prose.

Adds a consumer test for the two verb-string case arms that postdate this
branch: fm-supervise-daemon.sh's transient-stale arm and
fm-crew-state.sh's map_log_state.
kunchenguid pushed a commit that referenced this pull request Aug 26, 2026
#1967)

* fix(classify): read the decision fold through a correlation token

status_line_verb stripped a trailing [key=...] from a status line's prefix
but left everything else glued to the verb, so a line carrying the
correlation token bin/fm-pending-reply-lib.sh embeds and a secondmate
echoes back matched no arm of _fm_decision_fold_line. Such a line folded as
ordinary status in both directions: a needs-decision or blocked opener never
opened its key, and a resolved or captain-held closer never closed one. The
same glued verb also hid correlated done and blocked lines from
status_is_captain_relevant and status_is_terminal_verb, and let correlated
working and resolved lines leak through the free-text fallback the
nonterminal guard was meant to stop.

The verb parse now walks whole words and drops only a token of the exact
shape a firstmate library writes - corr=<16 hex>, plus the bracketed form
bin/fm-secondmate-report.sh emits - before or after the key token, unkeyed,
or doubled. An arbitrary name=value word is deliberately NOT skipped:
skipping unknown tokens would let free text carrying an equals sign reduce
to a bare verb and impersonate a transition, which is the takeover the
strict parse and _fm_decision_key_transition_allowed exist to prevent. A
prefix with no corr= substring is returned byte-for-byte as before, so every
line without a token keeps its exact historical verb.

FM_OPEN_DECISIONS_FOLD_VERSION goes to 3, because every cursor persisted
under the previous reading carries an open set computed while correlated
lines were invisible and must be rebuilt from byte 0.

Measured over a real 383-line status log: 254 lines keep byte-identical
captain-relevance, pause, terminal-verb and captain-held verdicts, and all
129 changed lines carry a valid token - 14 correlated done/blocked/
needs-decision lines become captain-visible, and 20 correlated
working/resolved lines stop being escalated on prose alone.

* fix(review): Captain, block token-first decision impersonation

* fix(document): Clarify normalized status verb ownership

* fix(classify): reconcile the correlation-token read with the tag-stop parser

Rebasing onto main put this change beside #2280, which made verb parsing
stop at the first "[name=value]" tag. Both edit status_line_verb with
different intents, so the resolution keeps both rules rather than letting
one overwrite the other:

- #2280's tag stop is kept verbatim and now owns every BRACKETED tag,
  including the "[corr=...]" form fm-secondmate-report.sh writes. The
  bracket-unwrapping arm this branch had added to the token test is
  therefore removed as unreachable.
- This branch's token walk is kept and narrowed to the UNBRACKETED token
  fm-pending-reply-lib.sh writes, which the tag stop does not reach.

Two consequences of standing beside #2280 rather than before it:

The fold version had collided at 4: #2280 spent it on the tag-stop
parser and this branch had spent it on the token read. A cursor
persisted under #2280's reading predates this one and must still be
rebuilt, so the version moves to 5.

A bracketed impostor is dropped from the malformed-token list. On main
today "resolved [corr=deadbeef] [key=victim]:" already reads as the bare
verb, as does "resolved [anything at all] [key=victim]:", because the tag
stop ends the parse at the opening bracket regardless of content. That is
#2280's reviewed contract; asserting otherwise here would narrow it. The
unbracketed impostors it owns stay strict and still fold as prose.

Adds a consumer test for the two verb-string case arms that postdate this
branch: fm-supervise-daemon.sh's transient-stale arm and
fm-crew-state.sh's map_log_state.

* fix(review): Captain: Seed cursor migration fixture with version four

* fix(document): Clarify voice status normalization ownership
akrcdev added a commit to akrcdev/firstmate that referenced this pull request Aug 29, 2026
* fix(bin): require project clone roots during fleet sync (#2849)

* fix(bin): require a clone root before fleet-sync touches a project

Git repository discovery walks upward, so `git -C projects/<dir>` on a plain
directory nested under projects/ resolves to the enclosing repository - in a
firstmate home, the firstmate checkout itself. fm-fleet-sync.sh guarded its
candidates with `rev-parse --is-inside-work-tree`, which such a directory
passes, so every later git call read, pruned and fast-forwarded firstmate's own
default branch and reported it under the project directory's label. A running
session's AGENTS.md changed underneath it, and the report named a project that
had nothing to do with the change.

Require each candidate to be the root of its own work tree before any other git
command: compare `rev-parse --show-toplevel` against the directory's own
physical path. Both sides are physical, so a symlinked clone still compares
equal. Anything else is skipped by name, naming the repository that would have
been touched, and bootstrap relays that as a FLEET_SYNC line.

Regression coverage reproduces the wrong-repo fast-forward against a home nested
inside another repository, in both the whole-fleet and single-project forms, and
pins that a symlinked clone dir still syncs.

* no-mistakes(review): Keep enclosing fixture clean during clone-root regression

* fix(bin): retry transient Lavish poll interruptions (#2846)

* fix(procevent): retry a transient Lavish poll interruption quietly

A live Lavish listener can be cut short by the server with exactly

    error: Lavish Editor poll response was interrupted
    code: SERVER_ERROR

while the session's marks remain available. Firstmate registered raw
`lavish-axi poll` output, so the generic process-event runner captured
that transient response as a result and woke the whole fleet over what is
really an internal retry.

The Lavish adapter now registers its own listener command, which reruns
the published blocking poll up to 12 times at 5 second intervals for that
one exact two-line response. The match is deliberately narrow: real
feedback, ended and missing sessions, any other SERVER_ERROR, and the same
interruption still standing once the bound is spent all pass straight
through and are captured and announced as before. The retry is a Lavish
fact, so the generic runner stays adapter-agnostic.

`FM_LAVISH_POLL_RETRY_DELAY` is a bounded 0 to 60 second override for the
interval only, refused rather than rounded when malformed, so a test can
exercise the real bound without waiting it out.

* no-mistakes(review): Harden Lavish retry matching, validation, and cleanup

* no-mistakes(review): Bound Lavish retry staging and stabilize regression

* no-mistakes(document): docs: explain Lavish retry adoption

* no-mistakes(lint): Restore Lavish trap ShellCheck suppression

* fix(brief): stop the documented {TASK} fill from corrupting the Herdr gate (#2838)

The unguarded Herdr declaration quoted `{TASK}` in its own prose while the
scaffold instructs firstmate to replace every `{TASK}` placeholder. The
documented global replace therefore spliced the whole task body into the
middle of the safety gate's sentence, silently destroying the one contract
that exists precisely because the scaffold cannot inspect the task text.

Reword the gate to refer to the task text filled in above, leaving the
placeholder only at its genuine fill site. Rewording rather than renaming the
token keeps the unfilled-charter guards in fm-home-seed.sh and
fm-remote-home-seed.sh working unchanged.

Add a regression test that performs the documented global fill on ship and
scout scaffolds and asserts the body lands once and the gate survives.

* fix(bin): resolve the busy-state lock mtime with the platform's own stat form (#2837)

The writer lock's stale-lock branch read the lock's mtime with
`stat -f %m ... || stat -c %Y ...`. On GNU coreutils `-f` is filesystem
stat, so it consumed the format string as a path, complained on stderr,
printed a partial filesystem dump ("  File: ...") on stdout, and still
exited 0. The GNU form in the fallback therefore never ran, and the
following arithmetic evaluated the word `File`, aborting the writer under
`set -u` with "File: unbound variable".

fm-teardown.sh died there after returning the worktree, leaving
state/<id>.meta, .status, .busy-gen, .busy-state, .busy-state.lock/ and
.turn-ended behind. The surviving metadata kept the watcher monitoring an
endpoint whose agent was gone, so a finished task produced stale wakes
forever, and every re-run died identically because the abandoned lock was
never broken.

Detect the platform once and pick the right stat form, the pattern
bin/fm-watch.sh already documents, and treat any non-numeric result as
"just created" so a future portability surprise degrades to a lock-timeout
refusal rather than killing teardown mid-way.

* fix(stow): add opt-in pass horizon for memory decay (#2850)

* fix(stow): give memory decay a per-pass horizon so the clock fires

The tiered decay clocks were wall-clock only, while admission is per-pass:
each /stow admits the findings that pass produced. In a home that stows
daily those two rates diverge by the stow cadence, an entry the fleet keeps
exercising never reaches 30 days unreinforced, and memory only grows while
the pass reports decay evaluated.

Give each dated marker an optional unreinforced-pass counter and make both
tiers stale at whichever horizon comes first: 10 passes or 30 days for
aging, 3 passes or 7 days for perishable. Reinforcement clears the counter
and nothing else does, so the existing evidence-based restamp rule stays
the only way an entry renews its lease. An absent /N means zero, so entries
that stay exercised carry no extra marker bytes, and a rarely stowed home
keeps its current behaviour through the unchanged date horizon.

* no-mistakes(document): Align stow workflow with dual decay clocks

* fix(stow): make the per-pass decay horizon opt-in

The unreinforced-pass horizon shipped as a new default archival cadence,
which is a product default rather than a restoration of the existing
wall-clock contract. Keep the 30-day and 7-day horizons as the only
default clock, and put the 10-pass and 3-pass horizons behind an explicit
opt-in: config/stow-pass-horizon for the firstmate home, and the file's
own header pointer for the public skill.

With the opt-in absent no counter is written and no counter is read, so a
home that does not ask for it decays exactly as it does today.

* no-mistakes(review): Preserve frozen counters and correct archive provenance

* test(watcher): stop fixture confirmation budgets racing real child startup (#2876)

tests/fm-watcher-lock.test.sh passed in isolation but failed intermittently
under full-suite and ambient concurrent load. bin/fm-watch-arm.sh computes its
confirmation deadline immediately after forking the real child watcher, so the
child's entire fork, exec, lock acquisition and beacon publication has to land
inside that wall clock. Two cases shrank that budget to one second, leaving a
two-second window for work measured at 3.1-4.9s under CPU oversubscription, so
the arm honestly reported "FAILED - no live watcher with a fresh beacon" and
their premises collapsed. A third case ran on the production budget, but its
child must also execute a registered check before exiting: measured at 1.9-2.3s
idle and 9.1-13.1s under load, against an 11s budget.

The two cases that must confirm a real child now hold the arm to production's
own budget instead of a shrunken fixture one, the immediate-wake case gets an
explicit budget with headroom over its measured loaded cost, and the two waits
for the arm's typed failure are sized off the largest production default rather
than a fixed eight seconds.

No bin/ change and no default behavior change: the lock's fail-closed semantics,
SIGSTOP handling, stale-heartbeat detection and the arm's typed failures are
untouched. Verified 4/4 green at 3x CPU oversubscription (loadavg 75-80) after
3/3 red before the change, and CONTRIBUTING.md records the convention.

* fix(bin): deterministically order remote tool paths (#2870)

* fix(bin): order discovered tool installs by the shell's own expansion

fm_remote_job_compose_operator_path built the asdf and mise install
directories with `compgen -G`, which does not sort. Bash sorts glob
matches in pathexp.c, on the shell's own pathname-expansion path only;
`compgen -G` reaches the same glob_filename through pcomplete.c, which
sorts nothing. On bash 3.2 (macOS /bin/bash) and every bash before 5.3
that handed the composition raw readdir order, so which install of a
multi-version tool a remote job resolved was decided by directory order
on disk rather than by this composition.

Expand the globs at the call sites and let the function take the matches,
so the composition and the documented portable-PATH contract are the same
operation. Quoting the account home at the call site also stops a home
whose name contains glob metacharacters from being reinterpreted.

The colocated regression pins both the order and the mechanism: bash 5.3
moved sorting into the glob library, so an order-only assertion cannot
see the defect there.

* no-mistakes(review): Remove source-reading PATH regression guard

* fix(bin): prevent routed secondmate work from stranding (#2848)

* fix: surface stalled secondmate queues and wake handoffs

* no-mistakes(review): Make handoff wakes retryable and stall alerts crash-safe

* no-mistakes(review): Prevent duplicate handoff wakes and cover remote delivery

* no-mistakes(review): Serialize local handoffs and preserve pre-move wake intent

* no-mistakes(review): Serialize teardown with handoffs and retain remote wake confirmation

* no-mistakes(review): Reconcile correlated handoff wake delivery after crashes

* no-mistakes(review): Keep failed wakes retryable and isolate stall receipts

* no-mistakes(review): Reset known-undelivered wake attempts for durable retries

* no-mistakes(review): Refuse duplicate sends for unresolved delivery attempts

* no-mistakes(review): Atomically restore retryability after reconciled send failures

* no-mistakes(review): Serialize delivery confirmation with reconciliation

* no-mistakes(document): Document routed wake and stall supervision

* no-mistakes(lint): Fix ShellCheck expansion and subshell warnings

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes(review): Retire stale wake state and defer pre-move wakes

* no-mistakes(review): Secure markers, bind batches, and preserve teardown routes

* no-mistakes(review): Preserve unresolved prepared wakes across unrelated handoffs

* no-mistakes(review): Preserve prepared wakes before unrelated moving handoffs

* no-mistakes(document): Document prepared wake batch ownership

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes(review): Make local wake retirement recoverable

* no-mistakes(document): Clarify handoff recovery and teardown documentation

* fix: make macOS inbox test path portable (#2857)

* feat(bin): deliver local steers through durable task inboxes (#2856)

* feat(bin): steer local tasks by durable inbox record plus constant doorbell

Stage 1 (local steers) of the captain-adopted reframe in
data/fm-send-reliability-reframe-s1/report.md: an ordinary fm-send text
steer to a task recorded in this home is appended as a sequenced durable
record under state/<id>.inbox/ and the terminal receives only one constant
self-describing doorbell line, best-effort. The worker acknowledges by
moving the record into handled/; the watcher re-rings an unacknowledged
message on an idle pane and escalates once as an ordinary stale wake.
--resolve-key closes decisions at enqueue time, because the durable
enqueue IS delivery to the task's record. bin/fm-task-inbox-lib.sh owns
the record format, doorbell line, and re-ring ladder.

The typed plane remains for what must reach the terminal itself:
lifecycle keys, harness-native slash and codex $-skill invocations,
explicit backend targets, and the remote secondmate leg (unchanged until
the remote inbox leg ships separately). The composer classifier is
demoted from delivery proof to an advisory ring guard that skips only on
a proven pending verdict.

Verified live against claude, codex, opencode, pi, grok, and muse: each
real worker read its record, acted, and acked with the mv
(docs/verification/runtime-backends.md "Steering-inbox doorbell").

* docs(verification): flag the grok 1.0.5 composer-matrix staleness observed by the doorbell run

* test(captain-hold): read the chat-channel answer from the durable inbox record

* test: migrate fm-control's marker contrast to the inbox record and fix macOS wc padding in the tool-update suite

* no-mistakes(review): Harden inbox locking, teardown races, and acknowledgements

* no-mistakes(review): Serialize watcher actions with inbox acknowledgements

* no-mistakes(review): Bound metadata locking and tighten acknowledgement rechecks

* no-mistakes(review): Preserve exact inbox bytes and harden delivery recovery

* no-mistakes(review): Harden watcher bookkeeping against concurrent inbox teardown

* no-mistakes(document): Update inbox and typed-plane documentation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* revert(pipeline): keep parser-native secondmate marking and the both-failed exit out of stage 1

The CI monitor's fix changed the secondmate marking contract for
parser-native invocations (appending the marker after the text) and
softened the both-commit-and-marker-failed branch to exit 0. The merge
authority ruled the marking question out of scope for this stage-1
transport PR (follow-up: fm-send-secondmate-harness-invocation-r1) and
ruled the both-failed case a loud nonzero local failure. Restore both,
keeping the monitor's legitimate migrations and hardening.

* no-mistakes(document): Document inbox and typed-plane boundaries

* no-mistakes(document): Scope backend transport docs to typed plane

* no-mistakes(document): Clarify inbox attempt-budget documentation

* no-mistakes: apply CI fixes

* fix(send): the durable record alone governs the inbox exit status

Captain-refined ruling on the F2/Greptile finding: the durable inbox
record is what delivers the steer, so pending-reply bookkeeping trouble
after a successful enqueue never exits nonzero - a resend-inviting status
would make automated callers enqueue the delivered instruction again
under a new sequence. With the recovery marker stored the watcher
reconciles silently; with the commit and marker both lost the send
surfaces a distinct reply-tracking-degraded do-not-resend warning and
still exits 0. Nonzero remains only where nothing was delivered (or a
decision close needs its manual command). Regression: record durable +
both bookkeeping writes lost -> exit 0, one record, no duplicate.

* no-mistakes(review): Preserve inbox ordering with drain-all doorbells

* no-mistakes(review): Surface unwritable inbox ladder bookkeeping

* no-mistakes(review): Silence ladder failures after inbox acknowledgement

* no-mistakes(document): Update steering inbox documentation

* no-mistakes: apply CI fixes

* feat(bin): add fast local lint mode (#2891)

* feat: add fast local lint mode

* fix: preserve complete fm-lint help

* fix: isolate fast lint mode

* no-mistakes(document): Clarify lint mode documentation ownership

* no-mistakes: apply CI fixes

* feat(bin): deliver remote steers through durable inboxes (#2901)

* feat(bin): deliver remote secondmate steers through durable task inboxes

Stage 2 of the inbox+doorbell steer channel (stage 1: #2856). A remote
secondmate steer now crosses fm-on.sh as a durable record written
idempotently into the remote home's steering inbox plus a best-effort
remote doorbell, and the last typed-payload steer transport is deleted:

- fm-remote-secondmate-control.sh cmd_send writes the record via the new
  fm_task_inbox_write_idempotent and rings the doorbell; it no longer
  types the payload through an inner fm-send at an explicit pane target.
- fm-send.sh routes every remote text steer (harness-native included,
  which marking already reduced to chat) onto the remote inbox leg,
  retries the identical leg once on ssh 255, closes --resolve-key
  decisions at enqueue for remote too, and preserves a marked request's
  reply expectation when completion stays unknown. The exit-3-as-
  delivered remap, the 255 do-not-resend trap, and the remote typed
  submit block are removed.
- fm-task-inbox-lib.sh owns the idempotent enqueue: an exact-body re-run
  lands on the existing record, handled or not, so an ambiguous
  transport can always be safely re-run.
- Tests pin the new contract end to end (record + doorbell + no typed
  payload across ssh, one-record idempotence under an ambiguous
  transport, enqueue-time decision close, loud real failures, and the
  deleted typed-payload behaviors gone), and AGENTS.md plus
  docs/remote-secondmates.md describe the remote leg's new semantics.

* no-mistakes(review): Harden remote inbox delivery against lifecycle races

* no-mistakes(review): Enable correlation-preserving remote steer resends

* no-mistakes(review): Fail closed on stale correlation resends

* no-mistakes(review): Include home context in remote resend commands

* no-mistakes(review): Lock and revalidate remote parent routes

* no-mistakes(document): Clarify remote steer retry documentation

* no-mistakes: apply CI fixes

* feat: add persistent Pi supervision branch (#2858)

* wip: forked supervision on Pi (checkpoint before docs)

* fix(pi-branch): harden mirror delivery, fallback encoding, and session replacement

Peek-then-shift mirror flush so a failed append retries instead of dropping;
durable mirror cursor commits only after delivery into the branch;
the main fallback wake is operational-encoded like every watcher injection;
session_shutdown quiesces the generation and session_start re-arms, so /new
and /resume no longer kill the branch permanently. Registers the extension in
the strict typecheck, adds the dispatch handshake test, the branch extension
suite, the bash-level regression suite, the session-start replay test, and
the opt-in real-SDK live guard.

* test(fixtures): carry the branch-dispatch lib and lease lib into isolated fixtures

The watcher extension now imports lib/fm-branch-dispatch.ts and fm-teardown
sources fm-lease-lib.sh, so every fixture that copies or symlinks those
files in isolation gains the new sibling.

* no-mistakes(review): Prevent shutdown wake loss and serialize lease claims

* no-mistakes(review): Durably hand off wakes and retain portable leases

* no-mistakes(review): Require durable reports and clear disposed branch leases

* no-mistakes(review): Enforce per-wake outcomes and quiescent lease cleanup

* no-mistakes(review): Require wake acknowledgements and tighten branch lifecycle boundaries

* no-mistakes(review): Require complete acknowledgements and replay cleanup failures

* no-mistakes(review): Bind supervision to lock ownership and durable delivery

* no-mistakes(review): Activate branch lazily after session lock acquisition

* no-mistakes(review): Preserve undelivered mirror context across extension rebinds

* no-mistakes(review): Acknowledge startup replay only after main delivery

* no-mistakes(review): Isolate replay metadata from untrusted digest content

* no-mistakes(review): Reject duplicate reports for active wake sequences

* no-mistakes(review): Retain failed fallbacks and deduplicate outcome replay

* no-mistakes(review): Deduplicate durable outcomes and cache delivery receipts

* no-mistakes(review): Anchor wake sequence matching to outcome fields

* no-mistakes(document): Clarify Pi supervision durability contracts

* no-mistakes(lint): Fix ShellCheck issues in branch supervision scripts

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* refactor(pi-branch): collapse to confused-agent-grade guards per captain decision

Captain decision A: the lease/actor guards target the CONFUSED-AGENT threat
model bin/fm-gate-refuse-lib.sh already documents; adversarial-grade
separation is impossible in the shared-process design and is filed as
separate follow-up work. Rip out the machinery that chased it: the
generation fence and shell-provenance markers, the wrapper-tagged ancestry
walks, guard auto-claim with per-script release traps, the pending-wake
files and ack-receipt correlation (the durable wake queue already
re-presents anything unacknowledged), the delivery-receipt store with
contiguous cursor advancement, the session-start replay-metadata channel,
and the branch tool quiescence counters.

Keep the behaviors the board requires, each on its simplest implementation:
lazy per-action session-lock ownership (cold start activates after the lock
lands; a secondary session stays inert), mirror durability across extension
rebinds via the durable cursor, replay-exactly-once from the one read
cursor, the awaited operational-encoded fallback, per-generation stray-lease
cleanup, session-lock-bound lease liveness (a recycled pid or a non-Pi home
never honors a leftover lease), the loud accidental-override guards
(readonly actor prelude, cross-actor claim refusal), and the role-partition
refinements (no forced teardown, no direct relaunch for the branch).
Default-on-for-Pi is unchanged.

* no-mistakes(review): Enforce lock ownership and serialize lease mutations

* no-mistakes(review): Synchronize guard cleanup and bind leases to lock owner

* no-mistakes(review): Report outcomes before acknowledging durable wakes

* no-mistakes(review): Restrict leases to Pi and instruct main claims

* no-mistakes(review): Reject malformed lease locks and torn outcome tails

* no-mistakes(review): Validate complete outcome tails before appending

* no-mistakes(review): Guard branch side effects across session replacements

* no-mistakes(document): Update Pi supervision durability and lease documentation

* no-mistakes(lint): Suppress intentional nested-shell expansion warning

* no-mistakes: apply CI fixes

* fix(pi-branch): authorize lease releases by caller

* fix(lint): break redundant source-analysis path in fm-lease-lib.sh

fm-lease-lib.sh's lazy fallback source of fm-wake-lib.sh gave ShellCheck's
--external-sources traversal a second path into an already 1540-line file
that fm-send.sh and fm-teardown.sh also source directly, blowing up the
recursive analysis past CI's lint timeout. Mark it a source=/dev/null
analysis boundary, matching the existing fm-task-inbox-lib.sh convention.

Also restores bin/fm-lint.sh and tests/fm-lint.test.sh to the shared
serial-lint definition (dropping an unrelated parallel-sharding change
that was itself hanging and masked this root cause).

* no-mistakes(document): Correct lease caller-authorization documentation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* fix(bin): parallelize startup network sweeps (#2927)

* feat(bin): parallelize session-start remote secondmate network sweeps

Run per-secondmate liveness and convergence probes concurrently and overlap clone refresh, while replaying each mate's fail-closed diagnostic in original order. Ignore scratchpad* so untracked scratch no longer blocks remote sync.

Co-authored-by: Cursor <cursoragent@cursor.com>

* no-mistakes(document): Document parallel startup network sweeps

* no-mistakes(lint): Fix empty environment assignment lint warning

* no-mistakes: apply CI fixes

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* test: handle absent watcher wake queues (#2845)

* fix(tests): count declared-pause wakes without crashing on an absent queue

The exited-declared-pause case counts queued stale wakes by handing
state/.wake-queue straight to awk. A watcher that queues nothing never
creates that file, and awk aborts on a missing path before its END rule
runs, so the count collapses to the empty string. The next comparison
then fails as an integer-expression error and surfaces as a wake flood
with no number, hiding the real contract breach the following grep names.

Read the queue the way the drain-count assertion at the end of this file
already does: silence awk's open error and default an absent queue to
zero. Applied to all four counts in this case, including the live
external-decision gate pair whose queue an acknowledged drain can also
leave behind. An absent queue now reports "did not use the bounded
paused recheck", while a genuine flood still fails with its real count.

Fixes #2628

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* style(pi): distinguish routine and captain supervision merge notes by icon (#2934)

* style(pi): restyle supervision merge notes with a sailboat and matching pad

Secondary-session notes were flush against the TUI edge and fully tinted.
Use the sailboat prefix, Pi's default outputPad, boat-only color, and dim remainder so they sit like real messages.

* style(pi): distinguish routine and captain merge notes by icon only

Visible notes now lead with a sailboat or anchor, then only the dim outcome.
Drop the branch-merged wording and verdict brackets so the icon is the only kind signal.

* docs(pi): add the approved multi-brain architecture poster (#2938)

The markdown contract stays the owner; the still is only the visual of the idea.

* feat(pi): default branch supervision and route heartbeats (#2939)

* fix(bin): bound remote job worker supervisor restarts (#2942)

* fix(bin): bound remote worker supervisors

* no-mistakes(review): release incumbent supervisor before starting its replacement

* no-mistakes(review): wait out a healthy same-root supervisor instead of replacing it

* no-mistakes(review): narrow remote worker change to restart accounting only

* no-mistakes(document): clarify supervisor restart guard is a lifetime total

* fix: safely split supervision wake handling by actor (#2953)

* feat(bin,pi): per-actor wake consume, silent success gating, merge-poll dedup

Three related fixes to the shared wake-drain and Pi supervision-branch
dispatch machinery so a routine success is never main-blocking and a
mixed queue can safely split between actors.

1. Successful routine results no longer create main-blocking wake rows.
   fm-startup-network.sh only enqueues a check: startup-network wake when
   the deferred result is actionable (state is not "done", or the report
   carries a bootstrap-diagnostics actionable prefix); a clean success
   stays durable in the report file without ever waking the agent.

2. Per-actor wake-drain consume contract. bin/fm-wake-drain.sh now scopes
   presentation and --ack-through to the current actor
   (bin/fm-lease-lib.sh's fm_lease_actor): main keeps the original
   whole-queue cutoff behavior, unaffected. A branch actor
   (FM_SUPERVISION_ACTOR=branch, set only inside the Pi supervision
   branch's own bash tool calls) is scoped to an explicit eligible-row
   snapshot instead of a cutoff comparison, so it can never remove a row
   it was not granted - the fix for the swallow risk that used to force
   an all-or-nothing whole-queue fallback to main.
   .pi/extensions/lib/fm-branch-dispatch.ts's scopeForUnreadWake is the
   single owner of eligibility: a check-kind row (merge-confirmation
   polls, Relay mentions, credential/auth failures) is now excluded
   rather than vetoing the whole scan for a non-heartbeat wake, while a
   heartbeat review keeps its original all-or-nothing rule unchanged.
   writeEligibleRowsSnapshot publishes the exact eligible sequence
   numbers before every branch prompt; fm-primary-pi-watch.ts's offer
   still refuses a check-kind trigger outright so a main-only close is
   never itself routed to the branch.

3. A repeat identical merged-PR-poll result for an already-notified task
   is absorbed instead of enqueued again. A poll's own retirement state
   is scoped to one registration and cannot see a prior registration's
   outcome, so a task re-registered after its merge was already surfaced
   would otherwise wake main a second time for the same event.
   bin/fm-pr-lib.sh's new per-task pr-poll-merge-notified marker survives
   across re-registrations to catch that case; the first notification for
   a task still reaches main unchanged.

Regression tests colocated in tests/fm-startup-network.test.sh,
tests/fm-wake-queue.test.sh (including the mixed-queue no-swallow
property), tests/fm-pi-branch-extension.test.sh, and
tests/fm-pr-check-security.test.sh. docs/watcher-continuity.md and
docs/pi-supervision-branch.md updated for the new contracts.

* no-mistakes(review): Bind merge deduplication to canonical PR identity

* no-mistakes(review): Serialize wake row ownership across main and branch

* no-mistakes(review): Bind branch grants and deduplicate within actor claims

* no-mistakes(review): Fallback main-owned wake claims to main delivery

* no-mistakes(review): Clarify silent startup success guidance

* no-mistakes(review): Release residual branch grants after settled prompts

* no-mistakes(review): Reject truncated wake rows as corrupted

* no-mistakes(document): Document per-actor routing and silent startup success

* no-mistakes(lint): Fix ShellCheck findings in wake grant and startup test

* no-mistakes: apply CI fixes

* fix(pi): hide branch outcomes tool rows in Calm (#3024)

* Hide branch outcome tool in Pi Calm

* no-mistakes(review): Preserve stock outcomes rendering and document tool audit

* no-mistakes(review): Document branch read tool audit disposition

* no-mistakes(review): Match stock outcomes output sanitization

* no-mistakes(document): Document Calm custom-tool visibility

* fix: bind no-mistakes attestations to PR head (#3027)

* fix: delegate no-mistakes PR gate to pinned action

* no-mistakes(document): Document commit-bound no-mistakes attestations

* feat(pi): add persistent supervision branch model selection (#3028)

* feat(pi): let operators pin a cheaper supervision-branch model

Supervision is an easier job than the captain's own conversation, so the
Pi supervision branch does not need main's model. A new /supervision-model
command opens Pi's own selector over Pi's own catalog of credentialed
models, plus a "Follow main" entry, and persists the pick as one
<provider>/<model-id> line in this home's gitignored
config/supervision-branch-model. Firstmate keeps no model catalog of its
own.

The branch resolves the pin at every branch build - the first wake of a
cold start and the reopen after /new, /resume, /fork, or reload - so the
choice survives all of them, and picking also releases the live branch so
the next wake reopens the same persistent branch conversation under the
new model. An absent, unreadable, or unparseable file means no pin and
keeps today's behavior byte for byte: no model option is passed and Pi
picks the branch's model exactly as before.

A pin naming a model Pi cannot hand back is never silently downgraded
onto main's model: the branch refuses to build and the wake falls back to
the captain-facing main path naming the unusable pin, which is the
extension's existing failure direction.

The choice is home-local and not part of secondmate inherited
configuration, matching the Pi Calm preference precedent.

docs/configuration.md owns the operator-facing schema. Portable
regressions cover pin-present on create and reopen, pin-absent default,
the command's persistence, cancellation, and live rebind, and both
unusable and unparseable pins. The opt-in real-SDK guard proves the
vendor surface the pin reads and that an explicit model wins over the
model a reopened session recorded.

* no-mistakes(review): Fix supervision model runtime and rebind races

* no-mistakes(review): Restrict supervision picker to isolated runtime models

* no-mistakes(document): Document supervision branch model selection

* fix(pi): make the supervision model pin authoritative on every reopen

Clearing the pin with "Follow main" removed the file but the next branch
build reopened the persistent branch session with no explicit model
override, so Pi restored the model that session had recorded - the old
pinned model - while the command reported that the branch now follows
main. The same gap meant an absent pin did not reliably mean
same-model-as-main once a home had pinned once.

The pin file's current state now decides the model on every branch build,
create and reopen alike, overriding Pi's session-state restore. With a
pin, that model. With no pin, main's own current model is applied
explicitly, tracked from the contexts Pi already hands the extension plus
its model_select event, since the branch is built at wake time with no
context of its own. Only when main's model is unknown, or this home's
stored credentials cannot run it in the isolated branch runtime, does a
build fall back to passing no override at all, which is the behavior from
before the pin existed; the branch is never refused over model choice.

The command's notification now reports the model actually applied, and
says plainly when clearing the pin could not apply main's model instead
of claiming a change that did not take effect.

No credential handling changes: the branch still relies entirely on the
stored credentials its own runtime already holds, and the picker stays
restricted to models that runtime can resolve.

Colocated regressions cover pin present on create and reopen, clearing
the pin returning a reopened branch to main's model and specifically not
the old pinned one, an unparseable pin behaving as no pin, and the
unknown-main-model fallback to no override.

* no-mistakes(review): Make unpinned supervision follow main model changes

* no-mistakes(document): Correct supervision model documentation

* feat(pi): let /supervision-model pick branch reasoning effort (#3079)

* feat(pi): let /supervision-model pick the branch's reasoning effort

Supervision is an easier job than the captain's own conversation, so the
Pi supervision branch does not need main's reasoning effort any more than
it needs main's model. /supervision-model now settles both in one flow:
the existing model picker, then a follow-up effort picker built from Pi's
own supported thinking levels for the model just chosen. Firstmate keeps
no effort catalog of its own; the menu, the clamp, and the vocabulary all
come from Pi.

The pick persists as one line in this home's gitignored
config/supervision-branch-effort, independent of the model pin: a captain
may pin a model, an effort, both, or neither. The effort pin's current
state decides the branch effort on every branch build - the first wake of
a cold start and the reopen after /new, /resume, /fork, or reload - and
overrides Pi's restore of whatever level a reopened branch session
recorded, which is what keeps "Follow main" honest. With no pin, main's
own current effort is applied explicitly and followed live through Pi's
thinking_level_select event, the same way an unpinned branch already
follows main's model, and the two selections now share one build revision
so either change invalidates an in-flight build.

The branch is never refused over effort. Pi owns the clamp, so a pinned
level the branch's model cannot run becomes that model's nearest supported
level while the captain's raw pick is kept for a model that supports it,
and the command reports the level the branch will really run at rather
than the raw pin. A token Pi would not recognize at all is treated as no
pin rather than passed to that clamp, which would otherwise collapse a
typo into the model's lowest level. Only when main's effort cannot be read
either does a build pass no effort override at all, which is the behavior
from before this file existed.

Pi's own effort vocabulary is pinned by a bidirectional type assertion
against Pi's getThinkingLevel return type, so the tracked strict typecheck
against the installed package fails the moment Pi adds or removes a level.

docs/configuration.md owns the operator-facing schema for both pins.
Portable regressions cover the pin on create and reopen, model-only and
effort-only pins working independently, clearing a pin returning the
branch to main's effort, live-follow of a mid-session change, the clamp,
an unrecognized token, the unknown-main-effort fallback, and the command's
two-step flow, persistence, cancellation, and honest reporting. The opt-in
real-SDK guard proves the vendor surface all of that rests on, and also
repairs a pre-existing gap that left it unable to load the extension at
all.

* no-mistakes(review): Resolve effective branch effort honestly

* no-mistakes(document): Clarify Pi-owned effort picker behavior

* fix: keep routine supervision noise out of captain chat (#3093)

* fix(supervision): silence empty board closes and decouple the heartbeat

Two unrelated sources of noise put routine supervision events in the
captain's chat.

An empty Lavish board close - the captain reads a review surface, says
nothing, and closes it - became a check wake whose entire content was
that nothing happened. Suppress it at its source instead of routing it
anywhere: the generic runner gains a `silent` adapter seam mirroring the
existing `terminal` one, and the Lavish adapter answers it for exactly
one positively-determined shape, an `ended` session carrying no queued
content block. A silenced result is recorded durably handled so it does
not return on a later reconcile. Everything else announces unchanged - a
`Send & End` close carrying the captain's real answer, an `ended` result
still carrying content, a waiting or missing session, an unreadable
result, and every adapter that implements no `silent` command at all.
The keyed-answer feed is untouched, so suppressing an announcement never
suppresses the captain's own answer.

A fleet heartbeat was deferred to main merely because some unrelated
check row happened to be sitting unread, which put a routine fleet
review in the chat for a reason that had nothing to do with the fleet. A
check row is permanently main-owned, so it is now excluded from a
heartbeat claim rather than vetoing the scan, exactly as in every other
mode. What all-or-nothing guarantees is unchanged: the branch takes
every branch-ownable unread row or none of them, and an unresolvable
task-local row, an unknown row kind, or an unreadable queue still defers
the whole review to main. Main is still woken for the check on that
check's own triggering close, so nothing starves.

Main-only classes are unchanged and now each covered by a test: Relay
mentions, credential failures, merge confirmations, real board answers,
and watcher-failure repair. The per-actor acknowledgement and
no-cross-swallow properties are untouched.

* no-mistakes(review): Fail closed on all Lavish content headers

* no-mistakes(review): Suppress false unacknowledged status for silenced results

* fix(bin): stop a correlation token from hiding and stranding decisions (#1967)

* fix(classify): read the decision fold through a correlation token

status_line_verb stripped a trailing [key=...] from a status line's prefix
but left everything else glued to the verb, so a line carrying the
correlation token bin/fm-pending-reply-lib.sh embeds and a secondmate
echoes back matched no arm of _fm_decision_fold_line. Such a line folded as
ordinary status in both directions: a needs-decision or blocked opener never
opened its key, and a resolved or captain-held closer never closed one. The
same glued verb also hid correlated done and blocked lines from
status_is_captain_relevant and status_is_terminal_verb, and let correlated
working and resolved lines leak through the free-text fallback the
nonterminal guard was meant to stop.

The verb parse now walks whole words and drops only a token of the exact
shape a firstmate library writes - corr=<16 hex>, plus the bracketed form
bin/fm-secondmate-report.sh emits - before or after the key token, unkeyed,
or doubled. An arbitrary name=value word is deliberately NOT skipped:
skipping unknown tokens would let free text carrying an equals sign reduce
to a bare verb and impersonate a transition, which is the takeover the
strict parse and _fm_decision_key_transition_allowed exist to prevent. A
prefix with no corr= substring is returned byte-for-byte as before, so every
line without a token keeps its exact historical verb.

FM_OPEN_DECISIONS_FOLD_VERSION goes to 3, because every cursor persisted
under the previous reading carries an open set computed while correlated
lines were invisible and must be rebuilt from byte 0.

Measured over a real 383-line status log: 254 lines keep byte-identical
captain-relevance, pause, terminal-verb and captain-held verdicts, and all
129 changed lines carry a valid token - 14 correlated done/blocked/
needs-decision lines become captain-visible, and 20 correlated
working/resolved lines stop being escalated on prose alone.

* fix(review): Captain, block token-first decision impersonation

* fix(document): Clarify normalized status verb ownership

* fix(classify): reconcile the correlation-token read with the tag-stop parser

Rebasing onto main put this change beside #2280, which made verb parsing
stop at the first "[name=value]" tag. Both edit status_line_verb with
different intents, so the resolution keeps both rules rather than letting
one overwrite the other:

- #2280's tag stop is kept verbatim and now owns every BRACKETED tag,
  including the "[corr=...]" form fm-secondmate-report.sh writes. The
  bracket-unwrapping arm this branch had added to the token test is
  therefore removed as unreachable.
- This branch's token walk is kept and narrowed to the UNBRACKETED token
  fm-pending-reply-lib.sh writes, which the tag stop does not reach.

Two consequences of standing beside #2280 rather than before it:

The fold version had collided at 4: #2280 spent it on the tag-stop
parser and this branch had spent it on the token read. A cursor
persisted under #2280's reading predates this one and must still be
rebuilt, so the version moves to 5.

A bracketed impostor is dropped from the malformed-token list. On main
today "resolved [corr=deadbeef] [key=victim]:" already reads as the bare
verb, as does "resolved [anything at all] [key=victim]:", because the tag
stop ends the parse at the opening bracket regardless of content. That is
#2280's reviewed contract; asserting otherwise here would narrow it. The
unbracketed impostors it owns stay strict and still fold as prose.

Adds a consumer test for the two verb-string case arms that postdate this
branch: fm-supervise-daemon.sh's transient-stale arm and
fm-crew-state.sh's map_log_state.

* fix(review): Captain: Seed cursor migration fixture with version four

* fix(document): Clarify voice status normalization ownership

* fix(bin): Cursor-Park unter Pi-Host ohne Cursor-Identität stilllegen (#3115)

* fix(bin): Cursor-Park unter Pi-Host stilllegen.

pi-cursor-sdk lädt .cursor/hooks.json in die Pi-Sitzung und parkte einen zweiten Watcher; das erzeugte rearm-resurface und brach laufende Rückfragen ab. Bei PI_CODING_AGENT=true beendet der Park sofort, native cursor-agent Primaries bleiben unverändert.

* fix(bin): Cursor-Park trotz PI-Leak nur ohne Cursor-Identität stilllegen.

Stand-down gilt nur bei PI_CODING_AGENT=true ohne CURSOR_AGENT und ohne CURSOR_INVOKED_AS. Handgestartete cursor-agent Primaries mit geerbtem PI-Marker parken weiter.

* no-mistakes(document): Document Cursor park Pi-host stand-down

* fix(bin): no-mistakes-Mindestversion auf 1.46.0 anheben.

Die PR-Attestierung verlangt ab 1.46.0 strukturierte Pipeline-Schritte; der Bootstrap-Floor blieb bei 1.31.2 und ließ zu alte Builds zu.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(pi): make supervision model picker searchable and scrollable (#3099)

* fix(pi): make /supervision-model's model list bounded and searchable

Pi's generic extension selector renders every option at once with no
search box, so a real eligible catalog ran off the top of the terminal.
The model step now draws the same rows through Pi's own SelectList - the
bounded scrolling primitive behind Pi's /model picker - with Pi's own
Input and fuzzy filter above it for search, keeping 'Follow main' first,
the branch-runtime eligibility filter intact, and the pick branch-only.

Pi's ModelSelectorComponent is deliberately not reused: its selection
handler writes the captain's default model through Pi's settings manager,
which would move main's conversation as a side effect of pinning the
branch. The effort step's menu is a handful of levels and stays on Pi's
plain selector dialog.

* no-mistakes(document): Clarify supervision picker documentation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes(document): Document searchable supervision model picker

* no-mistakes: apply CI fixes

* fix(bin): durably report merged pull requests (#3104)

* fix: make a landed merge leave a durable outcome

A merge was the one lifecycle event that left no record outside the
merging agent's memory. bin/fm-pr-merge.sh ended at the forge call, and a
home merging under standing authority never waits for the merge poll that
would otherwise confirm it, so three real merges reached the captain as
silence.

bin/fm-merge-outcome-lib.sh is the single owner of that record. A
secondmate home reports the landed PR upward on the same parent reply
channel its terminal-outcome backstop already uses; a main home records
it on the durable wake queue. The record is at most once per task and
canonical PR identity, and only a merge that actually landed produces one.

The merge poll feeds that same channel when it detects a merge this home
did not perform, so the captain's own forge merge and a merge firstmate
performed itself produce one consistent outcome instead of two reporting
paths. No new state file and no second watch path.

Two smaller gaps from the same failure:

- A mate charter listed its report triggers without naming a landed
  merge. Under standing merge authority nothing is ever "ready for
  review", so the enumerated list silently omitted the case that matters.
- A secondmate home seeded without its parent binding failed every
  terminal-outcome report for the same reason, and the diagnostic never
  named the binding. It does now.

* no-mistakes(review): Harden durable merge outcome reporting

* no-mistakes(review): Make merge race regression deterministic

* no-mistakes(review): Make merge outcomes retry-idempotent and forge-confirmed

* no-mistakes(review): Unify merge publication under canonical outcome marker

* no-mistakes(review): Publish merge outcomes before committing dedup markers

* no-mistakes(review): Document at-least-once merge outcome recovery

* no-mistakes(review): Use supported GitHub confirmation and update recovery docs

* no-mistakes(review): Preserve distinct merge wakes by PR identity

* no-mistakes(document): Document durable merge outcome semantics

* no-mistakes(test): Make merge outcome interleaving test deterministic

* no-mistakes(document): Clarify merge outcome documentation ownership

* fix(lint): keep the merge-outcome library an analysis boundary

bin/fm-watch.sh followed the new merge-outcome library's source graph,
which reaches the wake queue, PR identity, and secondmate parent
libraries. Expanding that inside an already-large lint root pushed
ShellCheck's external-source analysis past the bounded CI lint worker:
the Lint job was killed with SIGTERM after five silent minutes, twice,
having emitted no diagnostics at all.

Make it an analysis boundary, exactly as the transition and inbox owners
directly above and below it already are and for the same stated reason.
Coverage is unchanged because the library is a canonical lint root in its
own right and is still linted as one.

Measured locally: the watcher goes from not terminating within 120s to
9s clean, and the library alone lints in 1s clean.

* fix(bearings): preserve projections through inventory mismatches (#3129)

* fix(bearings): keep an inventory-mismatch home readable, and mark warnings as repairs

A backlog-vs-metadata inventory mismatch inside a secondmate home was being
reported as "we cannot read that home", which discarded that home's open
captain calls, queued work, landed work, and live workers from the whole
Bearings digest. The main home already treats the identical mismatch as a
harmless disclosure; this makes the secondmate path agree.

- fm-fleet-snapshot.sh: the invalidity gate now passes orphan_in_flight,
  unowned_current, and terminal_in_flight through the partial-structured
  carve-out alongside child_current_unavailable, so those homes keep their
  decisions, holds, queued, landed, and live work and leave unreadable[].
  missing_backlog and unstructured_current stay on the discard path, because
  there the backlog itself is untrustworthy.
- fm-fleet-snapshot.sh: the same three kinds no longer collapse the home's own
  classification to "unknown"; the real captain_decision / active_child_work /
  externally_held classification survives and invalidity carries the warning.
  An unavailable child state still collapses it, including when a mismatch
  masks it under strict-invalidity precedence.
- secondmate_landed.partial now keys on partial-structured trust rather than an
  unknown state, so an inventory-mismatch home is still disclosed as partial.

Ask the home that owns the wrong books to fix them:

- bin/fm-secondmate-reconcile.sh sends exactly one reconcile instruction per
  mismatch episode through the ordinary steering transport. A persistent
  mismatch keeps its episode identity and never re-nags; a changed mismatch
  earns one more ask; a repaired one is forgotten so a recurrence is asked
  about again. The parent never touches the mate's own files, and a failed send
  records nothing so the next run retries it.

Give integrity warnings their own look on the board:

- charted rows take an optional kind of "queued" (the default) or "warning".
  A warning badges "needs repair" instead of "waiting" and is excluded from the
  Charted Next count, so alarms stop reading as dispatchable queued work. No
  fifth board section, and every existing payload stays valid.

Tests pin the new policy behaviorally: the retained surfaces and classification
for all three mismatch kinds, the still-discarding unstructured_current and
missing_backlog cases, the once-per-episode reconcile ask through real durable
steering records, and the board rendering exercised through the shipped
template under a minimal DOM shim.

* no-mistakes(review): Make reconcile dedupe atomic and warnings non-dispatchable

* no-mistakes(review): Preserve reconcile identity and reject stale snapshots

* no-mistakes(review): Order snapshots uniquely and canonicalize episode identities

* no-mistakes(review): Add fire-and-forget reconcile and separate warning overflow

* no-mistakes(review): Exclude fire-and-forget from escalation and track reconcile background

* no-mistakes(review): Run reconcile enqueue inline across all adapters

* no-mistakes(review): Track reconcile clears across strict-invalidity homes

* no-mistakes(review): Persist reconcile transitions atomically

* no-mistakes(document): Document reconcile and fire-and-forget contracts

* refactor(bearings): replace the reconcile episode dedupe with a 4-hour cooldown

The reconcile ask needed to fire once per problem without nagging on every
recap. The episode-precise record that tried to do that had to be correct in
every direction at once - order two concurrent snapshots, tell a repair from a
new problem, and never lose a clear - and each direction it got wrong either
swallowed a nudge or sent a duplicate.

A per-home cooldown removes the whole class. One durable timestamp per home,
one nudge per four hours, and nothing to get stale, mis-order, or
mis-classify: a home in mismatch is asked once, later recaps stay silent, and
a mismatch still sitting there after the window earns one gentle re-nudge.

- bin/fm-secondmate-reconcile.sh: state/<id>.reconcile-nudged holds the epoch
  second of the last ask; FM_RECONCILE_COOLDOWN_SECONDS names the window. The
  episode identity, ordering generation, pending/clear transitions, and
  delivery-identity reuse are all gone. A known-undelivered send starts no
  cooldown so the next run retries it; an unconfirmed one does, because a
  duplicate ask is worse than one the mate may already hold.
- bin/fm-fleet-snapshot.sh, bin/fm-bearings-snapshot.sh: drop the snapshot
  `observation` monotonic identity, which existed only to order those records.
- bin/fm-teardown.sh: retire the cooldown record with the endpoint's other
  runtime artifacts, so reseeding a retired id is not silenced by its
  predecessor's window.

The inline durable fire-and-forget send is unchanged, and the projection fix
and the warning surface are untouched.

Tests follow the behavior: the cooldown suite now pins one ask per window, the
re-nudge after it, the four-hour boundary, per-home independence, and that the
ask stays out of a re-ring ladder that still rings an ordinary steer beside it.
The obsolete observation-ordering test is deleted with the machinery it covered.

* no-mistakes(review): Serialize reconcile cooldown commits with mate lifecycle

* no-mistakes(review): Reject stale reconcile snapshots across mate reincarnations

* no-mistakes(review): Start reconcile cooldown after delivery completes

* no-mistakes(review): Keep reconcile sends nonblocking and remove pending residue

* no-mistakes(document): Document reconcile skip and stale-endpoint behavior

* no-mistakes(lint): Fix reconcile test subshell lint warning

* no-mistakes: apply CI fixes

* fix(bin): reconcile markerless remote secondmates safely (#3140)

* fix(bin): stop dropping reconcile nudges for markerless remote secondmates

A persistent remote secondmate's parent-side state/<id>.meta never carries
spawn_gen: bin/fm-spawn.sh's spawn_remote_secondmate() is its sole writer and
never writes one, because that incarnation identity does not apply to a
remote route. fm-secondmate-reconcile.sh's row filter required a non-empty
spawn_gen matching an identifier regex, so every such row was silently
dropped before the per-row loop ever saw it: no sent/stale/failed line, no
cooldown record, nothing sent, and no trace of why.

Give a legitimately markerless persistent remote secondmate a safe substitute
identity - its recorded remote_host - instead of weakening the spawn_gen
check for rows that do have a generation:

- bin/fm-secondmate-reconcile.sh: carry host through the row projection for
  both fm-fleet-snapshot.v1 and fm-bearings.v1 documents, and admit an empty
  spawn_gen instead of filtering the row out. A new revalidate_identity()
  compares the sampled spawn_gen against current metadata when one was
  sampled (unchanged), or the sampled host against the metadata's
  remote_host when none was sampled and the metadata still carries no
  spawn_gen of its own. A row with neither a spawn_gen nor a host has no
  safe identity at all and fails loudly instead of vanishing, exactly the
  visibility the original bug lacked.
- Rows now join on the ASCII unit separator rather than @tsv: bash's
  IFS-whitespace read collapses consecutive tabs, which would have silently
  dropped a legitimately empty field again.
- bin/fm-bearings-snapshot.sh: thread host through the secondmate_reconcile
  projection so the fm-bearings.v1 path (the one bearings itself feeds to
  the reconcile hook) carries the same substitute identity.
- tests/fm-secondmate-reconcile.test.sh: end-to-end coverage through the real
  remote transport (fm-on.sh + fm-remote-secondmate-control.sh against a
  genuinely seeded remote home) for a markerless mate nudged once per
  cooldown window, a stale/replaced remote route refused exactly like the
  existing local spawn_gen case, and a row with no identity at all failing
  loudly rather than being swallowed.

* no-mistakes(review): Enforce markerless remote host identity during final delivery

* no-mistakes(document): Document markerless remote reconciliation safety

* fix(bin): hand a busy declared pause to the away-mode daemon once, undecorated (#3147)

* fix(watch): hand a busy declared pause to the away-mode daemon undecorated

While away mode is active the daemon owns triage and the watcher reverts to
one-shot, handing over plain wake identities the daemon classifies itself. The
busy-turn bound was the one stale path that did not: with afk active it ran the
wedge timer, so the daemon received a wake already decorated as a possible wedge.

That decoration outranks the daemon's own verdict. handle_wake escalates an
enriched wedge reason before its pause classification can apply, so a crew that
declared the wait itself - a `paused:` external wait or a verified captain-held
transfer holding a live foreground call - was wedge-escalated once per
FM_STALE_ESCALATE_SECS for as long as the wait lasted, the escalation count
climbing into demand-deep-inspection on a pane nobody needed to inspect.
Measured on the pre-fix tree, five consecutive re-arms produced five escalations.

busy_turn_bound_check now reads the declaration before the afk branch: away mode
hands off the plain window identity, one-shot per distinct stale hash, leaving
normal-mode pause bookkeeping unwritten because the daemon owns it there. The
daemon then classifies the wait itself and self-handles it on the long cadence.
Normal-mode behavior is unchanged, and lifting the declaration still restores the
busy-pane wedge escalation on the same pane.

The regression covers all three: the undecorated handoff with no wedge timer or
escalation counter, the one-shot on re-arm that the escalation ladder used to
climb, and the restored wedge escalation once the declaration is lifted.

* no-mistakes(review): key afk busy-pause handoff on declaration, clear wedge state

* no-mistakes(document): docs: scope away-mode busy-bound handoff to declared waits

* no-mistakes(document): docs: note afk busy-bound handoff in watcher header

---------

Co-authored-by: Talon Stark <talonstark@gmail.com>

* fix(bin): name the stale submodule pin behind a pooled slot refusal (#3121)

* fix(bin): explain a pooled slot's stale submodule refusal

A pool slot whose submodule pin moved is refused with "is not clean; refusing
to discard uncommitted work", while the operator's own `git status` in that slot
reads clean. The message names no submodule, no pin, and no remedy, so the
refusal is unreadable and the slot looks wedged for no reason. That is the
failure that jammed three slots in a row when a submodule pin moved.

The refusal itself was never the bug and is unchanged: the gate still refuses,
and still touches nothing. It now distinguishes the one case it can prove and
says what it found - the submodule, the pin the slot has, the pin the base
records, and the command that clears it.

The diagnosis is deliberately conservative, because ` M <path>` alone cannot
tell a stale pin from real work. An entry is reported as stale only when every
reported entry is a gitlink whose submodule is internally clean and whose
recorded pin actually differs. A submodule holding uncommitted work, untracked
files, or an unpushed commit therefore keeps the original uncommitted-work
refusal, even when its pin is also stale - the remedy command would be wrong
there, and the conservative refusal is the safe answer.

Nothing is converged, synced, initialized, or deleted. There is no new failure
path: a slot that launched before still launches, a slot that refused before
still refuses, and projects that configure a submodule `ignore` are read exactly
as before. Paths are read with core.quotePath=false so a non-ASCII submodule is
named rather than falling back to the unreadable message.

Tests keep the reproductions that prove the message is accurate: the stale-pin
diagnosis (which fails against the previous refusal), work inside a submodule
still refused as uncommitted work, and a stale pin carrying real work refused
conservatively rather than called stale. Each asserts the slot is left untouched.

* no-mistakes(review): require remote containment before calling a submodule pin stale

* fix(bin): stop printing a remedy the containment check cannot stand behind

The stale-pin diagnosis printed `git submodule update --checkout` as the command
that clears the slot. The containment check behind it reads local refs only and
never fetches, because this gate has to stay usable offline. A remote-tracking
ref that has gone stale - its upstream branch deleted or force-pushed, and never
pruned - still reads as containment, so a commit that is really unpushed can look
contained and that command would move the submodule off it.

Naming the submodule and both pins is the whole point of the diagnosis: it turns
"is not clean", on a slot whose own `git status` reads clean, into a statement of
which submodule drifted and where it drifted from. The operator can choose the
remedy from that, seeing the whole picture. Printing an instruction that rests on
a judgement which can be fooled is worse than printing none, so it is dropped.

The limitation is now stated where it applies, in the script header and beside
the check itself, rather than left for a reader to discover.

No fetch is added: the gate stays offline-safe by design. Nothing else changes -
the same conditions are refused, the slot is still never touched, and a submodule
carrying real work or an unpushed commit still keeps the conservative
uncommitted-work refusal.

* no-mistakes(review): bound submodule containment probe to first commit

* fix(pi): prevent stale captain outcome re-emissions (#3154)

* fix(pi): type captain supervision outcomes so main relays them

A captain-relevant branch outcome reached main as a bare user message with
no marker of origin or required action, written in main's own captain-facing
voice, landing in a tail that often already held several such notes. Pi keeps
only a custom message's content when it builds the provider request, so
customType and display never reach the model and content was the only place
that identity could live.

Main could not tell an incoming outcome from its own earlier answer and
sometimes re-emitted that answer instead of relaying the outcome, losing it.
Measured against real Pi 0.84.1 on openai-codex/gpt-5.6-sol: 6 failures in 24
turns, rising to 3 in 6 once one stale answer was already in the tail, which
is how one captain conversation saw six identical messages in a row. The same
scenario with the outcome typed failed 0 times in 14 turns.

Wrap only the captain-verdict note in the branch-outcome operational kind
owned by bin/fm-operational-input.sh. Delivery is otherwise unchanged: still
display: false, still one triggerTurn follow-up, so the turn remains the
single captain-visible outcome and no hidden note is ever shown twice.
Routine notes stay plain because their renderer reads the glyph off the front
of that same string. An outcome that cannot be encoded degrades to the same
instruction as plain text rather than being lost, matching this file's stated
failure direction.

The existing assertions could not catch this: they pin the sendMessage
options and never look at what main receives. Add a portable regression that
classifies the delivered payload with the real protocol executable, and a
live guard that runs the real Pi SDK's own convertToLlm to prove content is
the entire model-visible payload.

* no-mistakes(document): Document typed Pi captain outcomes

* fix(bin): keep a declared wait on the pause cadence under a busy pane or enriched wedge (#3155)

* fix(bin): keep a busy pane from retiring a still-declared wait's window

The away-mode daemon's pause re-surface recheck (housekeeping step 2b) read a
busy pane as "the crew resumed" and dropped the declared-wait marker, without
re-reading that the crew's own latest status line still declared the wait.

That inference is not safe, because a declared wait can legitimately hold a pane
busy: a worker sitting on a long foreground call keeps that call live for as long
as the wait lasts. The marker is then cleared while the declaration still stands,
and migrate_watcher_pause_markers recreates it with a fresh timestamp on the very
next tick, so the window restarts forever and the wait never matures into its one
bounded recheck.

Away mode makes that terminal. Since the watcher half landed, a busy pane under a
declared wait is handed to the daemon exactly once per declaration and never woken
again while the declaration stands (bin/fm-watch.sh, busy_turn_bound_check), so
this recheck is the only thing left that can re-surface the pane at all. Measured
end to end on a throwaway state root, away mode active, a pi pane busy past
FM_BUSY_TURN_MAX_SECS, status still `paused:`, over six PAUSE_RESURFACE_SECS
windows: 0 captain-facing rechecks before this change, 6 after - one per window,
with the marker reset each time.

The fix drops only the busy arm of the 2b probe, leaving it an endpoint-readability
check: exit code 2 still means the capture failed, so the endpoint is gone and the
marker goes. The loop head above already drops the marker the moment the status
line stops declaring the wait, so nothing else is needed to end …
DereKk8 added a commit to DereKk8/firstmate that referenced this pull request Aug 31, 2026
* fix: bind no-mistakes attestations to PR head (#3027)

* fix: delegate no-mistakes PR gate to pinned action

* no-mistakes(document): Document commit-bound no-mistakes attestations

* feat(pi): add persistent supervision branch model selection (#3028)

* feat(pi): let operators pin a cheaper supervision-branch model

Supervision is an easier job than the captain's own conversation, so the
Pi supervision branch does not need main's model. A new /supervision-model
command opens Pi's own selector over Pi's own catalog of credentialed
models, plus a "Follow main" entry, and persists the pick as one
<provider>/<model-id> line in this home's gitignored
config/supervision-branch-model. Firstmate keeps no model catalog of its
own.

The branch resolves the pin at every branch build - the first wake of a
cold start and the reopen after /new, /resume, /fork, or reload - so the
choice survives all of them, and picking also releases the live branch so
the next wake reopens the same persistent branch conversation under the
new model. An absent, unreadable, or unparseable file means no pin and
keeps today's behavior byte for byte: no model option is passed and Pi
picks the branch's model exactly as before.

A pin naming a model Pi cannot hand back is never silently downgraded
onto main's model: the branch refuses to build and the wake falls back to
the captain-facing main path naming the unusable pin, which is the
extension's existing failure direction.

The choice is home-local and not part of secondmate inherited
configuration, matching the Pi Calm preference precedent.

docs/configuration.md owns the operator-facing schema. Portable
regressions cover pin-present on create and reopen, pin-absent default,
the command's persistence, cancellation, and live rebind, and both
unusable and unparseable pins. The opt-in real-SDK guard proves the
vendor surface the pin reads and that an explicit model wins over the
model a reopened session recorded.

* no-mistakes(review): Fix supervision model runtime and rebind races

* no-mistakes(review): Restrict supervision picker to isolated runtime models

* no-mistakes(document): Document supervision branch model selection

* fix(pi): make the supervision model pin authoritative on every reopen

Clearing the pin with "Follow main" removed the file but the next branch
build reopened the persistent branch session with no explicit model
override, so Pi restored the model that session had recorded - the old
pinned model - while the command reported that the branch now follows
main. The same gap meant an absent pin did not reliably mean
same-model-as-main once a home had pinned once.

The pin file's current state now decides the model on every branch build,
create and reopen alike, overriding Pi's session-state restore. With a
pin, that model. With no pin, main's own current model is applied
explicitly, tracked from the contexts Pi already hands the extension plus
its model_select event, since the branch is built at wake time with no
context of its own. Only when main's model is unknown, or this home's
stored credentials cannot run it in the isolated branch runtime, does a
build fall back to passing no override at all, which is the behavior from
before the pin existed; the branch is never refused over model choice.

The command's notification now reports the model actually applied, and
says plainly when clearing the pin could not apply main's model instead
of claiming a change that did not take effect.

No credential handling changes: the branch still relies entirely on the
stored credentials its own runtime already holds, and the picker stays
restricted to models that runtime can resolve.

Colocated regressions cover pin present on create and reopen, clearing
the pin returning a reopened branch to main's model and specifically not
the old pinned one, an unparseable pin behaving as no pin, and the
unknown-main-model fallback to no override.

* no-mistakes(review): Make unpinned supervision follow main model changes

* no-mistakes(document): Correct supervision model documentation

* feat(pi): let /supervision-model pick branch reasoning effort (#3079)

* feat(pi): let /supervision-model pick the branch's reasoning effort

Supervision is an easier job than the captain's own conversation, so the
Pi supervision branch does not need main's reasoning effort any more than
it needs main's model. /supervision-model now settles both in one flow:
the existing model picker, then a follow-up effort picker built from Pi's
own supported thinking levels for the model just chosen. Firstmate keeps
no effort catalog of its own; the menu, the clamp, and the vocabulary all
come from Pi.

The pick persists as one line in this home's gitignored
config/supervision-branch-effort, independent of the model pin: a captain
may pin a model, an effort, both, or neither. The effort pin's current
state decides the branch effort on every branch build - the first wake of
a cold start and the reopen after /new, /resume, /fork, or reload - and
overrides Pi's restore of whatever level a reopened branch session
recorded, which is what keeps "Follow main" honest. With no pin, main's
own current effort is applied explicitly and followed live through Pi's
thinking_level_select event, the same way an unpinned branch already
follows main's model, and the two selections now share one build revision
so either change invalidates an in-flight build.

The branch is never refused over effort. Pi owns the clamp, so a pinned
level the branch's model cannot run becomes that model's nearest supported
level while the captain's raw pick is kept for a model that supports it,
and the command reports the level the branch will really run at rather
than the raw pin. A token Pi would not recognize at all is treated as no
pin rather than passed to that clamp, which would otherwise collapse a
typo into the model's lowest level. Only when main's effort cannot be read
either does a build pass no effort override at all, which is the behavior
from before this file existed.

Pi's own effort vocabulary is pinned by a bidirectional type assertion
against Pi's getThinkingLevel return type, so the tracked strict typecheck
against the installed package fails the moment Pi adds or removes a level.

docs/configuration.md owns the operator-facing schema for both pins.
Portable regressions cover the pin on create and reopen, model-only and
effort-only pins working independently, clearing a pin returning the
branch to main's effort, live-follow of a mid-session change, the clamp,
an unrecognized token, the unknown-main-effort fallback, and the command's
two-step flow, persistence, cancellation, and honest reporting. The opt-in
real-SDK guard proves the vendor surface all of that rests on, and also
repairs a pre-existing gap that left it unable to load the extension at
all.

* no-mistakes(review): Resolve effective branch effort honestly

* no-mistakes(document): Clarify Pi-owned effort picker behavior

* fix: keep routine supervision noise out of captain chat (#3093)

* fix(supervision): silence empty board closes and decouple the heartbeat

Two unrelated sources of noise put routine supervision events in the
captain's chat.

An empty Lavish board close - the captain reads a review surface, says
nothing, and closes it - became a check wake whose entire content was
that nothing happened. Suppress it at its source instead of routing it
anywhere: the generic runner gains a `silent` adapter seam mirroring the
existing `terminal` one, and the Lavish adapter answers it for exactly
one positively-determined shape, an `ended` session carrying no queued
content block. A silenced result is recorded durably handled so it does
not return on a later reconcile. Everything else announces unchanged - a
`Send & End` close carrying the captain's real answer, an `ended` result
still carrying content, a waiting or missing session, an unreadable
result, and every adapter that implements no `silent` command at all.
The keyed-answer feed is untouched, so suppressing an announcement never
suppresses the captain's own answer.

A fleet heartbeat was deferred to main merely because some unrelated
check row happened to be sitting unread, which put a routine fleet
review in the chat for a reason that had nothing to do with the fleet. A
check row is permanently main-owned, so it is now excluded from a
heartbeat claim rather than vetoing the scan, exactly as in every other
mode. What all-or-nothing guarantees is unchanged: the branch takes
every branch-ownable unread row or none of them, and an unresolvable
task-local row, an unknown row kind, or an unreadable queue still defers
the whole review to main. Main is still woken for the check on that
check's own triggering close, so nothing starves.

Main-only classes are unchanged and now each covered by a test: Relay
mentions, credential failures, merge confirmations, real board answers,
and watcher-failure repair. The per-actor acknowledgement and
no-cross-swallow properties are untouched.

* no-mistakes(review): Fail closed on all Lavish content headers

* no-mistakes(review): Suppress false unacknowledged status for silenced results

* fix(bin): stop a correlation token from hiding and stranding decisions (#1967)

* fix(classify): read the decision fold through a correlation token

status_line_verb stripped a trailing [key=...] from a status line's prefix
but left everything else glued to the verb, so a line carrying the
correlation token bin/fm-pending-reply-lib.sh embeds and a secondmate
echoes back matched no arm of _fm_decision_fold_line. Such a line folded as
ordinary status in both directions: a needs-decision or blocked opener never
opened its key, and a resolved or captain-held closer never closed one. The
same glued verb also hid correlated done and blocked lines from
status_is_captain_relevant and status_is_terminal_verb, and let correlated
working and resolved lines leak through the free-text fallback the
nonterminal guard was meant to stop.

The verb parse now walks whole words and drops only a token of the exact
shape a firstmate library writes - corr=<16 hex>, plus the bracketed form
bin/fm-secondmate-report.sh emits - before or after the key token, unkeyed,
or doubled. An arbitrary name=value word is deliberately NOT skipped:
skipping unknown tokens would let free text carrying an equals sign reduce
to a bare verb and impersonate a transition, which is the takeover the
strict parse and _fm_decision_key_transition_allowed exist to prevent. A
prefix with no corr= substring is returned byte-for-byte as before, so every
line without a token keeps its exact historical verb.

FM_OPEN_DECISIONS_FOLD_VERSION goes to 3, because every cursor persisted
under the previous reading carries an open set computed while correlated
lines were invisible and must be rebuilt from byte 0.

Measured over a real 383-line status log: 254 lines keep byte-identical
captain-relevance, pause, terminal-verb and captain-held verdicts, and all
129 changed lines carry a valid token - 14 correlated done/blocked/
needs-decision lines become captain-visible, and 20 correlated
working/resolved lines stop being escalated on prose alone.

* fix(review): Captain, block token-first decision impersonation

* fix(document): Clarify normalized status verb ownership

* fix(classify): reconcile the correlation-token read with the tag-stop parser

Rebasing onto main put this change beside #2280, which made verb parsing
stop at the first "[name=value]" tag. Both edit status_line_verb with
different intents, so the resolution keeps both rules rather than letting
one overwrite the other:

- #2280's tag stop is kept verbatim and now owns every BRACKETED tag,
  including the "[corr=...]" form fm-secondmate-report.sh writes. The
  bracket-unwrapping arm this branch had added to the token test is
  therefore removed as unreachable.
- This branch's token walk is kept and narrowed to the UNBRACKETED token
  fm-pending-reply-lib.sh writes, which the tag stop does not reach.

Two consequences of standing beside #2280 rather than before it:

The fold version had collided at 4: #2280 spent it on the tag-stop
parser and this branch had spent it on the token read. A cursor
persisted under #2280's reading predates this one and must still be
rebuilt, so the version moves to 5.

A bracketed impostor is dropped from the malformed-token list. On main
today "resolved [corr=deadbeef] [key=victim]:" already reads as the bare
verb, as does "resolved [anything at all] [key=victim]:", because the tag
stop ends the parse at the opening bracket regardless of content. That is
#2280's reviewed contract; asserting otherwise here would narrow it. The
unbracketed impostors it owns stay strict and still fold as prose.

Adds a consumer test for the two verb-string case arms that postdate this
branch: fm-supervise-daemon.sh's transient-stale arm and
fm-crew-state.sh's map_log_state.

* fix(review): Captain: Seed cursor migration fixture with version four

* fix(document): Clarify voice status normalization ownership

* fix(bin): Cursor-Park unter Pi-Host ohne Cursor-Identität stilllegen (#3115)

* fix(bin): Cursor-Park unter Pi-Host stilllegen.

pi-cursor-sdk lädt .cursor/hooks.json in die Pi-Sitzung und parkte einen zweiten Watcher; das erzeugte rearm-resurface und brach laufende Rückfragen ab. Bei PI_CODING_AGENT=true beendet der Park sofort, native cursor-agent Primaries bleiben unverändert.

* fix(bin): Cursor-Park trotz PI-Leak nur ohne Cursor-Identität stilllegen.

Stand-down gilt nur bei PI_CODING_AGENT=true ohne CURSOR_AGENT und ohne CURSOR_INVOKED_AS. Handgestartete cursor-agent Primaries mit geerbtem PI-Marker parken weiter.

* no-mistakes(document): Document Cursor park Pi-host stand-down

* fix(bin): no-mistakes-Mindestversion auf 1.46.0 anheben.

Die PR-Attestierung verlangt ab 1.46.0 strukturierte Pipeline-Schritte; der Bootstrap-Floor blieb bei 1.31.2 und ließ zu alte Builds zu.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(pi): make supervision model picker searchable and scrollable (#3099)

* fix(pi): make /supervision-model's model list bounded and searchable

Pi's generic extension selector renders every option at once with no
search box, so a real eligible catalog ran off the top of the terminal.
The model step now draws the same rows through Pi's own SelectList - the
bounded scrolling primitive behind Pi's /model picker - with Pi's own
Input and fuzzy filter above it for search, keeping 'Follow main' first,
the branch-runtime eligibility filter intact, and the pick branch-only.

Pi's ModelSelectorComponent is deliberately not reused: its selection
handler writes the captain's default model through Pi's settings manager,
which would move main's conversation as a side effect of pinning the
branch. The effort step's menu is a handful of levels and stays on Pi's
plain selector dialog.

* no-mistakes(document): Clarify supervision picker documentation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes(document): Document searchable supervision model picker

* no-mistakes: apply CI fixes

* fix(bin): durably report merged pull requests (#3104)

* fix: make a landed merge leave a durable outcome

A merge was the one lifecycle event that left no record outside the
merging agent's memory. bin/fm-pr-merge.sh ended at the forge call, and a
home merging under standing authority never waits for the merge poll that
would otherwise confirm it, so three real merges reached the captain as
silence.

bin/fm-merge-outcome-lib.sh is the single owner of that record. A
secondmate home reports the landed PR upward on the same parent reply
channel its terminal-outcome backstop already uses; a main home records
it on the durable wake queue. The record is at most once per task and
canonical PR identity, and only a merge that actually landed produces one.

The merge poll feeds that same channel when it detects a merge this home
did not perform, so the captain's own forge merge and a merge firstmate
performed itself produce one consistent outcome instead of two reporting
paths. No new state file and no second watch path.

Two smaller gaps from the same failure:

- A mate charter listed its report triggers without naming a landed
  merge. Under standing merge authority nothing is ever "ready for
  review", so the enumerated list silently omitted the case that matters.
- A secondmate home seeded without its parent binding failed every
  terminal-outcome report for the same reason, and the diagnostic never
  named the binding. It does now.

* no-mistakes(review): Harden durable merge outcome reporting

* no-mistakes(review): Make merge race regression deterministic

* no-mistakes(review): Make merge outcomes retry-idempotent and forge-confirmed

* no-mistakes(review): Unify merge publication under canonical outcome marker

* no-mistakes(review): Publish merge outcomes before committing dedup markers

* no-mistakes(review): Document at-least-once merge outcome recovery

* no-mistakes(review): Use supported GitHub confirmation and update recovery docs

* no-mistakes(review): Preserve distinct merge wakes by PR identity

* no-mistakes(document): Document durable merge outcome semantics

* no-mistakes(test): Make merge outcome interleaving test deterministic

* no-mistakes(document): Clarify merge outcome documentation ownership

* fix(lint): keep the merge-outcome library an analysis boundary

bin/fm-watch.sh followed the new merge-outcome library's source graph,
which reaches the wake queue, PR identity, and secondmate parent
libraries. Expanding that inside an already-large lint root pushed
ShellCheck's external-source analysis past the bounded CI lint worker:
the Lint job was killed with SIGTERM after five silent minutes, twice,
having emitted no diagnostics at all.

Make it an analysis boundary, exactly as the transition and inbox owners
directly above and below it already are and for the same stated reason.
Coverage is unchanged because the library is a canonical lint root in its
own right and is still linted as one.

Measured locally: the watcher goes from not terminating within 120s to
9s clean, and the library alone lints in 1s clean.

* fix(bearings): preserve projections through inventory mismatches (#3129)

* fix(bearings): keep an inventory-mismatch home readable, and mark warnings as repairs

A backlog-vs-metadata inventory mismatch inside a secondmate home was being
reported as "we cannot read that home", which discarded that home's open
captain calls, queued work, landed work, and live workers from the whole
Bearings digest. The main home already treats the identical mismatch as a
harmless disclosure; this makes the secondmate path agree.

- fm-fleet-snapshot.sh: the invalidity gate now passes orphan_in_flight,
  unowned_current, and terminal_in_flight through the partial-structured
  carve-out alongside child_current_unavailable, so those homes keep their
  decisions, holds, queued, landed, and live work and leave unreadable[].
  missing_backlog and unstructured_current stay on the discard path, because
  there the backlog itself is untrustworthy.
- fm-fleet-snapshot.sh: the same three kinds no longer collapse the home's own
  classification to "unknown"; the real captain_decision / active_child_work /
  externally_held classification survives and invalidity carries the warning.
  An unavailable child state still collapses it, including when a mismatch
  masks it under strict-invalidity precedence.
- secondmate_landed.partial now keys on partial-structured trust rather than an
  unknown state, so an inventory-mismatch home is still disclosed as partial.

Ask the home that owns the wrong books to fix them:

- bin/fm-secondmate-reconcile.sh sends exactly one reconcile instruction per
  mismatch episode through the ordinary steering transport. A persistent
  mismatch keeps its episode identity and never re-nags; a changed mismatch
  earns one more ask; a repaired one is forgotten so a recurrence is asked
  about again. The parent never touches the mate's own files, and a failed send
  records nothing so the next run retries it.

Give integrity warnings their own look on the board:

- charted rows take an optional kind of "queued" (the default) or "warning".
  A warning badges "needs repair" instead of "waiting" and is excluded from the
  Charted Next count, so alarms stop reading as dispatchable queued work. No
  fifth board section, and every existing payload stays valid.

Tests pin the new policy behaviorally: the retained surfaces and classification
for all three mismatch kinds, the still-discarding unstructured_current and
missing_backlog cases, the once-per-episode reconcile ask through real durable
steering records, and the board rendering exercised through the shipped
template under a minimal DOM shim.

* no-mistakes(review): Make reconcile dedupe atomic and warnings non-dispatchable

* no-mistakes(review): Preserve reconcile identity and reject stale snapshots

* no-mistakes(review): Order snapshots uniquely and canonicalize episode identities

* no-mistakes(review): Add fire-and-forget reconcile and separate warning overflow

* no-mistakes(review): Exclude fire-and-forget from escalation and track reconcile background

* no-mistakes(review): Run reconcile enqueue inline across all adapters

* no-mistakes(review): Track reconcile clears across strict-invalidity homes

* no-mistakes(review): Persist reconcile transitions atomically

* no-mistakes(document): Document reconcile and fire-and-forget contracts

* refactor(bearings): replace the reconcile episode dedupe with a 4-hour cooldown

The reconcile ask needed to fire once per problem without nagging on every
recap. The episode-precise record that tried to do that had to be correct in
every direction at once - order two concurrent snapshots, tell a repair from a
new problem, and never lose a clear - and each direction it got wrong either
swallowed a nudge or sent a duplicate.

A per-home cooldown removes the whole class. One durable timestamp per home,
one nudge per four hours, and nothing to get stale, mis-order, or
mis-classify: a home in mismatch is asked once, later recaps stay silent, and
a mismatch still sitting there after the window earns one gentle re-nudge.

- bin/fm-secondmate-reconcile.sh: state/<id>.reconcile-nudged holds the epoch
  second of the last ask; FM_RECONCILE_COOLDOWN_SECONDS names the window. The
  episode identity, ordering generation, pending/clear transitions, and
  delivery-identity reuse are all gone. A known-undelivered send starts no
  cooldown so the next run retries it; an unconfirmed one does, because a
  duplicate ask is worse than one the mate may already hold.
- bin/fm-fleet-snapshot.sh, bin/fm-bearings-snapshot.sh: drop the snapshot
  `observation` monotonic identity, which existed only to order those records.
- bin/fm-teardown.sh: retire the cooldown record with the endpoint's other
  runtime artifacts, so reseeding a retired id is not silenced by its
  predecessor's window.

The inline durable fire-and-forget send is unchanged, and the projection fix
and the warning surface are untouched.

Tests follow the behavior: the cooldown suite now pins one ask per window, the
re-nudge after it, the four-hour boundary, per-home independence, and that the
ask stays out of a re-ring ladder that still rings an ordinary steer beside it.
The obsolete observation-ordering test is deleted with the machinery it covered.

* no-mistakes(review): Serialize reconcile cooldown commits with mate lifecycle

* no-mistakes(review): Reject stale reconcile snapshots across mate reincarnations

* no-mistakes(review): Start reconcile cooldown after delivery completes

* no-mistakes(review): Keep reconcile sends nonblocking and remove pending residue

* no-mistakes(document): Document reconcile skip and stale-endpoint behavior

* no-mistakes(lint): Fix reconcile test subshell lint warning

* no-mistakes: apply CI fixes

* fix(bin): reconcile markerless remote secondmates safely (#3140)

* fix(bin): stop dropping reconcile nudges for markerless remote secondmates

A persistent remote secondmate's parent-side state/<id>.meta never carries
spawn_gen: bin/fm-spawn.sh's spawn_remote_secondmate() is its sole writer and
never writes one, because that incarnation identity does not apply to a
remote route. fm-secondmate-reconcile.sh's row filter required a non-empty
spawn_gen matching an identifier regex, so every such row was silently
dropped before the per-row loop ever saw it: no sent/stale/failed line, no
cooldown record, nothing sent, and no trace of why.

Give a legitimately markerless persistent remote secondmate a safe substitute
identity - its recorded remote_host - instead of weakening the spawn_gen
check for rows that do have a generation:

- bin/fm-secondmate-reconcile.sh: carry host through the row projection for
  both fm-fleet-snapshot.v1 and fm-bearings.v1 documents, and admit an empty
  spawn_gen instead of filtering the row out. A new revalidate_identity()
  compares the sampled spawn_gen against current metadata when one was
  sampled (unchanged), or the sampled host against the metadata's
  remote_host when none was sampled and the metadata still carries no
  spawn_gen of its own. A row with neither a spawn_gen nor a host has no
  safe identity at all and fails loudly instead of vanishing, exactly the
  visibility the original bug lacked.
- Rows now join on the ASCII unit separator rather than @tsv: bash's
  IFS-whitespace read collapses consecutive tabs, which would have silently
  dropped a legitimately empty field again.
- bin/fm-bearings-snapshot.sh: thread host through the secondmate_reconcile
  projection so the fm-bearings.v1 path (the one bearings itself feeds to
  the reconcile hook) carries the same substitute identity.
- tests/fm-secondmate-reconcile.test.sh: end-to-end coverage through the real
  remote transport (fm-on.sh + fm-remote-secondmate-control.sh against a
  genuinely seeded remote home) for a markerless mate nudged once per
  cooldown window, a stale/replaced remote route refused exactly like the
  existing local spawn_gen case, and a row with no identity at all failing
  loudly rather than being swallowed.

* no-mistakes(review): Enforce markerless remote host identity during final delivery

* no-mistakes(document): Document markerless remote reconciliation safety

* fix(bin): hand a busy declared pause to the away-mode daemon once, undecorated (#3147)

* fix(watch): hand a busy declared pause to the away-mode daemon undecorated

While away mode is active the daemon owns triage and the watcher reverts to
one-shot, handing over plain wake identities the daemon classifies itself. The
busy-turn bound was the one stale path that did not: with afk active it ran the
wedge timer, so the daemon received a wake already decorated as a possible wedge.

That decoration outranks the daemon's own verdict. handle_wake escalates an
enriched wedge reason before its pause classification can apply, so a crew that
declared the wait itself - a `paused:` external wait or a verified captain-held
transfer holding a live foreground call - was wedge-escalated once per
FM_STALE_ESCALATE_SECS for as long as the wait lasted, the escalation count
climbing into demand-deep-inspection on a pane nobody needed to inspect.
Measured on the pre-fix tree, five consecutive re-arms produced five escalations.

busy_turn_bound_check now reads the declaration before the afk branch: away mode
hands off the plain window identity, one-shot per distinct stale hash, leaving
normal-mode pause bookkeeping unwritten because the daemon owns it there. The
daemon then classifies the wait itself and self-handles it on the long cadence.
Normal-mode behavior is unchanged, and lifting the declaration still restores the
busy-pane wedge escalation on the same pane.

The regression covers all three: the undecorated handoff with no wedge timer or
escalation counter, the one-shot on re-arm that the escalation ladder used to
climb, and the restored wedge escalation once the declaration is lifted.

* no-mistakes(review): key afk busy-pause handoff on declaration, clear wedge state

* no-mistakes(document): docs: scope away-mode busy-bound handoff to declared waits

* no-mistakes(document): docs: note afk busy-bound handoff in watcher header

---------

Co-authored-by: Talon Stark <talonstark@gmail.com>

* fix(bin): name the stale submodule pin behind a pooled slot refusal (#3121)

* fix(bin): explain a pooled slot's stale submodule refusal

A pool slot whose submodule pin moved is refused with "is not clean; refusing
to discard uncommitted work", while the operator's own `git status` in that slot
reads clean. The message names no submodule, no pin, and no remedy, so the
refusal is unreadable and the slot looks wedged for no reason. That is the
failure that jammed three slots in a row when a submodule pin moved.

The refusal itself was never the bug and is unchanged: the gate still refuses,
and still touches nothing. It now distinguishes the one case it can prove and
says what it found - the submodule, the pin the slot has, the pin the base
records, and the command that clears it.

The diagnosis is deliberately conservative, because ` M <path>` alone cannot
tell a stale pin from real work. An entry is reported as stale only when every
reported entry is a gitlink whose submodule is internally clean and whose
recorded pin actually differs. A submodule holding uncommitted work, untracked
files, or an unpushed commit therefore keeps the original uncommitted-work
refusal, even when its pin is also stale - the remedy command would be wrong
there, and the conservative refusal is the safe answer.

Nothing is converged, synced, initialized, or deleted. There is no new failure
path: a slot that launched before still launches, a slot that refused before
still refuses, and projects that configure a submodule `ignore` are read exactly
as before. Paths are read with core.quotePath=false so a non-ASCII submodule is
named rather than falling back to the unreadable message.

Tests keep the reproductions that prove the message is accurate: the stale-pin
diagnosis (which fails against the previous refusal), work inside a submodule
still refused as uncommitted work, and a stale pin carrying real work refused
conservatively rather than called stale. Each asserts the slot is left untouched.

* no-mistakes(review): require remote containment before calling a submodule pin stale

* fix(bin): stop printing a remedy the containment check cannot stand behind

The stale-pin diagnosis printed `git submodule update --checkout` as the command
that clears the slot. The containment check behind it reads local refs only and
never fetches, because this gate has to stay usable offline. A remote-tracking
ref that has gone stale - its upstream branch deleted or force-pushed, and never
pruned - still reads as containment, so a commit that is really unpushed can look
contained and that command would move the submodule off it.

Naming the submodule and both pins is the whole point of the diagnosis: it turns
"is not clean", on a slot whose own `git status` reads clean, into a statement of
which submodule drifted and where it drifted from. The operator can choose the
remedy from that, seeing the whole picture. Printing an instruction that rests on
a judgement which can be fooled is worse than printing none, so it is dropped.

The limitation is now stated where it applies, in the script header and beside
the check itself, rather than left for a reader to discover.

No fetch is added: the gate stays offline-safe by design. Nothing else changes -
the same conditions are refused, the slot is still never touched, and a submodule
carrying real work or an unpushed commit still keeps the conservative
uncommitted-work refusal.

* no-mistakes(review): bound submodule containment probe to first commit

* fix(pi): prevent stale captain outcome re-emissions (#3154)

* fix(pi): type captain supervision outcomes so main relays them

A captain-relevant branch outcome reached main as a bare user message with
no marker of origin or required action, written in main's own captain-facing
voice, landing in a tail that often already held several such notes. Pi keeps
only a custom message's content when it builds the provider request, so
customType and display never reach the model and content was the only place
that identity could live.

Main could not tell an incoming outcome from its own earlier answer and
sometimes re-emitted that answer instead of relaying the outcome, losing it.
Measured against real Pi 0.84.1 on openai-codex/gpt-5.6-sol: 6 failures in 24
turns, rising to 3 in 6 once one stale answer was already in the tail, which
is how one captain conversation saw six identical messages in a row. The same
scenario with the outcome typed failed 0 times in 14 turns.

Wrap only the captain-verdict note in the branch-outcome operational kind
owned by bin/fm-operational-input.sh. Delivery is otherwise unchanged: still
display: false, still one triggerTurn follow-up, so the turn remains the
single captain-visible outcome and no hidden note is ever shown twice.
Routine notes stay plain because their renderer reads the glyph off the front
of that same string. An outcome that cannot be encoded degrades to the same
instruction as plain text rather than being lost, matching this file's stated
failure direction.

The existing assertions could not catch this: they pin the sendMessage
options and never look at what main receives. Add a portable regression that
classifies the delivered payload with the real protocol executable, and a
live guard that runs the real Pi SDK's own convertToLlm to prove content is
the entire model-visible payload.

* no-mistakes(document): Document typed Pi captain outcomes

* fix(bin): keep a declared wait on the pause cadence under a busy pane or enriched wedge (#3155)

* fix(bin): keep a busy pane from retiring a still-declared wait's window

The away-mode daemon's pause re-surface recheck (housekeeping step 2b) read a
busy pane as "the crew resumed" and dropped the declared-wait marker, without
re-reading that the crew's own latest status line still declared the wait.

That inference is not safe, because a declared wait can legitimately hold a pane
busy: a worker sitting on a long foreground call keeps that call live for as long
as the wait lasts. The marker is then cleared while the declaration still stands,
and migrate_watcher_pause_markers recreates it with a fresh timestamp on the very
next tick, so the window restarts forever and the wait never matures into its one
bounded recheck.

Away mode makes that terminal. Since the watcher half landed, a busy pane under a
declared wait is handed to the daemon exactly once per declaration and never woken
again while the declaration stands (bin/fm-watch.sh, busy_turn_bound_check), so
this recheck is the only thing left that can re-surface the pane at all. Measured
end to end on a throwaway state root, away mode active, a pi pane busy past
FM_BUSY_TURN_MAX_SECS, status still `paused:`, over six PAUSE_RESURFACE_SECS
windows: 0 captain-facing rechecks before this change, 6 after - one per window,
with the marker reset each time.

The fix drops only the busy arm of the 2b probe, leaving it an endpoint-readability
check: exit code 2 still means the capture failed, so the endpoint is gone and the
marker goes. The loop head above already drops the marker the moment the status
line stops declaring the wait, so nothing else is needed to end the routing, and
the reconcile path runs before the probe ever reads a pane.

tests/fm-daemon.test.sh: test_housekeeping_paused_resumed_cleared pinned the old
inference on purpose - its fixture's status line still read `paused:` while the
pane was busy, and its comment read "A pause whose pane became busy again (the
crew resumed)". Its fixture now resumes the way a crew actually resumes, by
appending a non-declaring status line, and it asserts its own busy verdict first
so it cannot silently decay into the idle-pane case that
test_housekeeping_paused_unpaused_cleared already covers. What it pins is now the
inverse guard: a busy pane must not GATE the clear either, so an over-correction
that kept the marker alive whenever the pane is busy would fail it.

test_housekeeping_busy_declared_wait_matures_its_window is the new regression, over
both declaration forms. It asserts the busy verdict, then that ticks inside the
window neither escalate nor let the marker be recreated with a fresh timestamp,
then exactly one recheck past the window named for the right human and never a
wedge, then silence on the next tick inside the reset window. It fails on
unmodified main with "produced 0 escalations past its window, expected exactly
one".

Refs #3149

* fix(bin): let a declared wait outrank an enriched wedge escalation

handle_wake classifies a stale wake through classify_stale, which returns a
`pause` verdict for a crew whose latest status line declares an external wait or
a verified captain-held transfer. It then threw that verdict away whenever the
wake reason matched `idle *s, possible wedge, escalation *`, so the watcher's
enriched wedge decoration outranked the crew's own declaration and a healthy
declared wait was escalated once per FM_STALE_ESCALATE_SECS for as long as the
wait lasted.

The enriched reason earns its precedence over the daemon's cheaper status-log
absorption honestly - it carries the watcher's escalation count and its explicit
"do not re-absorb on the run-step/pane state alone" demand. A `pause` verdict is
not run-step or pane state. It is the crew's own declaration that this pane waits
by design, which is precisely the question the wedge timer cannot answer for
itself, so it is the one verdict that decoration must not override.

The two classifications genuinely disagree in steady state rather than only in a
race: a crew that declares `paused:` while its no-mistakes run is still attributed
to its code reads `working` to the watcher's pause_state_class, so the watcher
takes the wedge timer while the daemon's classify_stale reads the status log and
correctly returns `pause`.

The wait stays bounded, not silenced. Absorbing to the pause action records the
declared-wait marker and drops wedge aging, and housekeeping (2b) then owns the
re-surface, so the pane still reaches the captain - once per PAUSE_RESURFACE_SECS
as an explicit "recheck whether the wait still holds", instead of once per
FM_STALE_ESCALATE_SECS as a possible wedge. Measured on a throwaway state root
over five wedge cadences for one declared wait: 5 escalations climbing to
demand-deep-inspection before this change, 0 after, with the one bounded recheck
still delivered.

tests/fm-daemon.test.sh: test_stale_diagnostic_wedge_survives_busy_housekeeping's
`paused` case pinned the old precedence on purpose, asserting exactly one
escalation carrying the demand-deep-inspection payload. That case now asserts the
pause cadence instead - no escalation inside the window, pause tracking recorded -
while the `working` and `prior-terminal` cases keep asserting the enriched wedge
verbatim, so the override itself is still pinned everywhere it is correct.

test_enriched_wedge_under_declared_wait_uses_pause_cadence is the new regression.
It asserts the fixture's own classifier verdict is a pause first, so the case
cannot go vacuous, then drives four consecutive wedge-cadence deliveries in both
the plain and demand-deep-inspection forms through the real handle_wake and
housekeeping pair, then matures the window for exactly one awaiting-external
recheck, then lifts the declaration and requires the same enriched wedge to
escalate again unchanged. It fails on unmodified main at the first delivery.

Refs #3149

* no-mistakes(review): align afk skill recheck wording with still-declared contract

* no-mistakes(document): daemon doc comments: pause window ages on declaration

---------

Co-authored-by: Talon Stark <talonstark@gmail.com>

* fix(bin): recover Claude auto-arm from hung claims (#3156)

* fix(bin): make Claude auto-arm continuity self-heal past a hung claim

On a Claude primary, a Stop-hook auto-arm process that hung mid-arm held
the single-flight owner lock with its epoch ledger frozen at
outcome=arming, and the abandonment proof read any live lock holder in
arming as legitimately deciding forever. Every later Stop firing exited
0 at the lock, the turn-end guard kept deferring to the hung owner as
recovery under way, and the watcher was never auto-re-armed again for
the rest of the session - supervision survived only on manual arms and
lapsed between them (the 2026-08-26 watcher flap).

Corrections layered onto the lock-held-across-arm shape each reopened
the same concurrency class one level down, so this replaces the claim
machinery wholesale with a generation-based optimistic design:

- The epoch ledger's monotonic sequence IS the claim generation; the
  two-line entry (classic epoch record plus the claimant's MANDATORY
  pid-identity) is the claim. Every firing defers to a live OPEN claim:
  outcome arming, owner alive, identity recomputes and matches, and not
  stuck (entry and watcher beacon both older than the guard grace).
- A finished, dead, identity-mismatched, identityless, or stuck claim
  is superseded by simply taking the next generation - no signalling or
  revocation of a steady-state predecessor.
- No mutex is held across arming or output; the owner lock survives
  only as a micro-mutex around individual ledger writes. A superseded
  owner goes completely silent: ownership is re-verified before every
  arm invocation, episode-state mutation, ledger write, and
  continuation.
- The irrevocable commit point of a translation is the exit status
  (the harness delivers the collected stderr only on exit 2), so the
  owned terminal ledger write is the atomic commit: the winning
  generation exits 2 unconditionally after it, a refused one exits 0
  silently even after printing, and the once-per-episode failure notice
  commits in the same owned critical section as the winning failed
  write. Two bounded residuals are documented accepted intent: an owner
  dying between its owned write and its own exit, and a hung old-build
  owner resuming during the one legacy upgrade window.
- The pre-generation lock-holding claim shape keeps defer-or-reclaim
  behavior through a legacy shim: a live identity-verified stuck owner
  is retired via TERM (with a queued TERM sufficient when the owner is
  stopped) before its lock is removed, an unverified or identityless
  pid is never signalled but never blocks a proven-abandoned reclaim,
  and the lock's identity evidence is grafted into the ledger
  (mtime-preserving) so pid-reuse protection survives the lock.
- The guard reads the same predicates for recovery ownership and its
  terminal fail-open (which re-checks for a live open claim under the
  held locks before committing the attended alarm), with ledger reads
  anchored to line 1 so the identity line can never confuse them.

Behavioral regression coverage exercises all three edge classes through
the real hook and guard - a live open claim defers with no lock held, a
stuck claim is superseded and the home re-arms, and an end-to-end run
with a genuinely hung owner shows a concurrent firing deferring
promptly mid-arm, a later firing superseding the stuck owner, and the
superseded owner exiting silently without a second translation - plus
the identityless/reused-pid loopholes, the superseded-owner arm
boundary, and the legacy TERM, SIGSTOP, and signal-free reclaim paths.

* no-mistakes(review): Refuse auto-arm commits when notice marker creation fails

* no-mistakes(review): Make episode reset atomic with generation ownership

* no-mistakes(document): Update auto-arm generation and commit documentation

* fix(bin): verify the real GitHub merge outcome instead of reporting an unproved merge (#3064)

* fix(pr): verify GitHub merge outcome

* no-mistakes(review): Captain, fixed forge-only merge verification, queue guidance, metadata propagation

* no-mistakes(document): Correct forge-specific merge documentation

* no-mistakes(review): Captain: forge-only queue fix, focused tests pass

* no-mistakes(review): Captain: suppress closed-state guidance and prove parent regression

* no-mistakes(review): Captain: remove history proof; retain executable regressions

* no-mistakes(document): Clarify GitHub recording timing in architecture docs

* no-mistakes(document): Clarify outcome-aware PR merge recording documentation

* no-mistakes: apply CI fixes

* Revert "no-mistakes: apply CI fixes"

This reverts commit c326cfa9430c6173eedc8ff7f27d19d0552daf01.

The automatic CI repair round removed the up-front `gh` prerequisite check
while keeping the `gh` dependency: `bin/fm-pr-merge.sh` still calls
`gh api graphql` for the outcome read and `gh api` for the branch-rules read.
That left the same hard requirement without the clear named error, and review
immediately raised a new finding for exactly the failure the check prevents -
`gh-axi pr merge` landing the merge while the follow-up read fails, so the PR
metadata is never recorded.

The check is also symmetric with the GitLab arm directly above it, which
already refuses up front when `glab` or `jq` is missing, on the stated
principle that a missing tool should be a named prerequisite rather than a
merge that is armed and then refused for an unexplained reason.

The workflows this round was chasing sit at `action_required` because this is
a fork pull request; no code change can turn them green.

* fix(pr): keep PR bookkeeping when a merge outcome read fails

On the GitHub path a merge call that returned success was followed by
`github_read_outcome || exit 1`, so a transient API failure, rate limit,
or network blip during the read dropped out of the script before
`record_pr_metadata` ever ran. The merge could have landed while `pr=`
went unrecorded and the merge poll was never armed - bookkeeping lost on
a real merge. The failure path just above already recorded metadata
before exiting, so the error path was more careful than the success one.

Record the PR before that refusal. Recording arms the later merge poll
and is not a success claim, which is the same reasoning that keeps
`record_pr_metadata` on the gh-axi failure path. The refusal itself is
unchanged: exit stays non-zero and the message still names the concrete
observed state. Metadata is withheld only when the read succeeds and
proves the pull request neither merged nor queued.

Pin it with a case that stubs `gh api graphql` into failure after a
successful `gh-axi pr merge`, asserting both the non-zero exit and the
recorded metadata.

* no-mistakes(review): Aggregate queue rules and report conflicts explicitly

* fix(pr): keep the merge abstraction reachable and its bookkeeping intact

Two holes remained in the outcome-verified GitHub merge path, both on
installations where gh-axi is present but gh is not.

The verification preflight refused before bin/fm-pr-merge.sh ever reached
the configured gh-axi merge abstraction, so an installation without gh
could no longer merge at all. gh-axi now performs the merge unconditionally
and the queue-aware gh read became an optional enrichment: with gh on PATH
its GraphQL view still separates merged from queued, and without gh the
gh-axi view still proves a landed merge while every outcome it cannot prove
refuses.

The PR metadata recording sat behind the outcome read, so a merge that
landed before that read failed lost pr= and its merge poll. Recording now
happens once, before either forge call, which arms the poll without
claiming a landed outcome and leaves teardown a PR identity to verify
against no matter how the read ends.

Rebasing onto main also restored the durable merge-outcome reporting and
the GitLab landed-state confirmation that the conflict resolution dropped.

Tests pin each fix through the executable interface: the merge abstraction
is reached and verified with gh absent, a failed fallback read keeps its
bookkeeping, and a mock that snapshots the task meta during the forge call
proves pr= is recorded before the merge can land.

* no-mistakes(review): fix(pr): de-dup queue methods, fall back on failed gh read, refresh contracts

* no-mistakes(review): fix(pr): quote forge output and explain armed auto-merge on refusal

* no-mistakes(review): fix(pr): claim auto-merge armed only when the forge accepted it

* no-mistakes(review): fix(pr): tell the operator what each GitHub refusal could not observe

* no-mistakes(review): fix(pr): gate every forge-acceptance claim on a successful merge

* no-mistakes(document): align merge docs with verified GitHub outcome contract

* fix(pi): prevent duplicate captain outcome reports (#3184)

* fix(pi): stop reporting one merge to the captain twice

The supervision branch's captain-outcome note told main, unconditionally,
that the note "is not your own earlier output" and to relay it now. When
main had already reported the same event, that assertion was false and the
order turned the correct response - saying nothing new - into a mechanical
re-report, so the captain saw one merge reported twice in 16 seconds.

Two independent changes, both needed:

- The relay instruction is now conditional. It still names itself as a
  supervision outcome so main cannot mistake it for its own earlier answer
  (the silent loss that instruction exists to prevent), and it now lets
  main stay quiet about an outcome it has already given the captain.

- The merge case is closed at its source rather than left to that judgment.
  One merge reaches a home on two independent paths by design - main's own
  permanently main-owned merge poll, and the branch's task-local status
  wake - and main's captain-facing text only reaches the branch's mirror at
  main's turn end, so the branch can escalate before it could possibly see
  the captain was already told. bin/fm-pr-merge-notified.sh answers that
  question from bin/fm-pr-lib.sh's canonical merge-notification marker, so
  the answer holds regardless of mirror timing. A captain outcome naming an
  already-published merge is delivered as the ordinary rendered note
  instead of opening a follow-up turn: still appended, still visible, still
  recorded with the verdict the branch decided, minus the wasted turn.

Any error, timeout, or unreadable state relays the outcome. A duplicate
announces itself; a lost outcome does not.

Regression coverage drives the real delivery path in both directions: a new
outcome must still reach the captain in exactly one follow-up turn even
beside an unrelated published merge, and an already-published merge must
open no second turn while a different PR in the same task still does. The
merge path's real producer and this new consumer are exercised end to end
in tests/fm-pr-merge.test.sh.

Pi-only by construction: the delivery path lives in .pi/extensions, so no
other harness loads it, and the new script only reads existing markers.

* no-mistakes(review): Document accepted latest-marker suppression residual

* no-mistakes(review): Recheck ownership before merge outcome delivery

* no-mistakes(document): Document merge-outcome suppression exception

* refactor(pi): drop the source-level merge suppression, keep the envelope fix

The captain reviewed this branch and judged the source-level duplicate
suppression overly complicated for the problem it solved, and asked for
the change to be reduced to the envelope wording alone.

Remove the mergeIntoMain downgrade path, bin/fm-pr-merge-notified.sh, and
every test and document that existed only for it. What remains is the
conditional captain-outcome instruction: main is told to stay quiet about
an outcome it has already reported and to relay anything else, which
covers the duplicate without a second mechanism.

The silent-loss protection is untouched - the note is still typed,
self-describing, and delivered as one invisible follow-up turn - and the
behavioral tests still assert that, now requiring both halves of the
conditional instruction.

* no-mistakes(ci): Clarified in code comments and owned documentation that this is intentionally an M1-only, model-facing conditional relay fix—not source-level suppression—addressing Greptile’s mistaken scope expectation without changing runtime behavior. Net diff remains 3 files and 27 insertions. Verified with fm-pi-branch-extension tests, fm-lint, doc audience check, and git diff --check; all passed

* no-mistakes(ci): Strengthened the runtime delivery test to verify the captain outcome retains its required self-description and outcome text. Verified with `bash tests/fm-pi-branch-extension.test.sh`, `bin/fm-lint.sh`, `bin/fm-doc-audience-check.sh`, and `git diff --check`; all passed. The outer pipeline can now commit and attest the new head

* fix(bin): prioritize active pipeline-owned crew runs (#3194)

* fix(bin): bind the live pipeline-owned run instead of a superseded failed row

fm-crew-state.sh bound a superseded FAILED no-mistakes run to a task instead
of the LIVE replacement run: the live run's pipeline-owned lane head is not a
git object in the task worktree, so head-equality attribution rejected it and
the coarse runs-list fallback silently continued past the RUNNING row onto an
older failed row whose head equalled the stale worktree HEAD. The home summary
then flipped invalid and Bearings hid the home's live work (F10).

Attribution precedence now follows the daemon's own identity:
- An ACTIVE run for the task's branch binds without head equality while
  branch_sync.state is pipeline_owned (fm_nm_run_is_pipeline_owned_active);
  the pipeline owning the branch is itself the attribution.
- A genuinely failed run with no later run on the branch still reports failed
  through the unchanged head-equality path - real failures are not hidden.
- In the coarse runs scan, an unresolvable head is unknown attribution and
  stops the scan (fm_nm_head_resolvable) instead of falling through to an
  older row; a resolvable-but-mismatched head keeps the historical
  reused-branch skip.

The exemption never applies to a terminal run and requires pipeline_owned
specifically, both pinned by negative-control tests. Fixture shape verified
against the live incident run's real axi status output.

* no-mistakes(document): Updated run-attribution documentation ownership

* fix(pi): surface requested outcomes without replaying fleet events (#3211)

* fix(pi): surface requested supervision outcomes

* no-mistakes(review): Mirror in-flight captain requests before branch dispatch

* no-mistakes(review): Exercise real branch ownership and main outcome access

* no-mistakes(review): Preserve request tails and align verdict guidance

* no-mistakes(review): Preserve complete current captain requests

* no-mistakes(review): Require visible requested outcomes and realistic classification

* no-mistakes(document): Align supervision outcome documentation

* no-mistakes(ci): Fixed Greptile’s runtime-ordering finding. The extension now stages Pi’s authoritative `before_agent_start` prompt before SessionManager persistence and suppresses the later duplicate entry. Updated docs and behavioral regression to reproduce real Pi ordering and verify each prompt is mirrored exactly once. Passed branch-extension tests, supervision tests, strict Pi typecheck, full lint, and diff checks

* no-mistakes(review): Use canonical operational input classification

* no-mistakes(review): Filter legacy operational inputs canonically

* no-mistakes(document): Clarify captain request mirroring boundary

* no-mistakes(ci): Fixed the CI time-boundary failure in tests/fm-public-followup.test.sh by pinning its clock, including context-registry setup. This prevents follow-up fixtures from expiring based on wall time. Verified the full regression suite passes, project-owned lint passes, and git diff checks are clean

* no-mistakes(document): Clarify captain-visible supervision outcome documentation

* feat(bin): add concurrent bounded remote transport lanes (#3210)

* feat(bin): per-home remote transport lanes with cancellation, bounded send, and closed stdin

All remote commands for every home on one host used to serialize through one
single-job-at-a-time worker on one shared queue: a timed-out caller abandoned a
staged job that kept running, retries convoyed behind it, fm-send's remote leg
had no time bound, and staging captured the caller's stdin to EOF so any
fm-on.sh caller with an open stdin wedged staging indefinitely.

- The worker now serves one lane per staged home: same-home jobs run strictly
  FIFO in a new staging-sequence order while different homes run concurrently,
  each lane as its own top-level worker process (a backgrounded subshell does
  not reliably reap dead children, so a zombie group leader kept a finished
  command's process group signalable). Long-poll preemption is lane-scoped.
- A caller that disconnects or times out cancels its job: the entrypoint marks
  the record on any post-staging exit and probes its parent so a dead ssh
  channel cancels without a signal; the worker skips cancelled queued jobs,
  terminates a running cancelled job's process group, and reaps the record.
- fm-send's remote leg is bounded by FM_SEND_REMOTE_BUDGET (default 30s) and a
  bound hit exits through the existing unconfirmed-delivery contract, which
  stays idempotent because the remote enqueue deduplicates.
- fm-on.sh defaults the remote command's stdin to /dev/null; the three payload
  callers pass the new --stdin flag. Abandoned .stage.* litter is age-reaped.
- The job execution deadline no longer loses up to a second to clock
  truncation.

* no-mistakes(review): Protect live stages and validate send budgets early

* no-mistakes(review): Preserve sequence lock ownership during stale recovery

* no-mistakes(review): Allocate job sequences at publication boundary

* no-mistakes(review): Bound remote keys and extend stale lock recovery

* no-mistakes(document): Document bounded remote transport behavior

* no-mistakes(lint): Suppress intentional deferred-expansion lint warning

* no-mistakes(ci): Fixed stale sequence-lock recovery by reconciling the counter against published job records before allocating the next sequence, preventing duplicate sequences and same-home FIFO violations. Added a behavioral regression test reproducing displacement after publication and verifying execution order. Passed fm-remote-transport-lanes.test.sh, fm-remote-job.test.sh, fm-lint.sh, and git diff --check

* no-mistakes(review): Use atomic sequence claims and lossless lane keys

* no-mistakes(review): Recover regressed sequence hints and rate-limit claim reaping

* no-mistakes(review): Restrict worker heartbeats to serving loop

* no-mistakes(review): Verify supervisor identity before lane recovery signals

* no-mistakes(review): Verify tracked lane and claim owner identities

* no-mistakes(document): Clarify remote lane and transport contracts

* no-mistakes(ci): Fixed the CI time-boundary failure by pinning fm-public-followup tests to a deterministic clock, including context-registry setup. Verified tests/fm-public-followup.test.sh, tests/fm-remote-transport-lanes.test.sh, shellcheck, and git diff --check

* no-mistakes(review): Preserve assigned lane ownership of queued jobs

* no-mistakes(review): Reserve homes owned by foreign queued lanes

* no-mistakes(review): Preserve completed results during crash recovery

* no-mistakes(review): Harden claim cleanup, expiry, and cancellation races

* no-mistakes(review): Verify process groups and reap abandoned results

* no-mistakes(review): Stop leaderless groups and reap cancelled publications

* no-mistakes(document): Correct remote transport lifecycle documentation

* no-mistakes(lint): Quote done state comparisons for ShellCheck

* fix(bin): accelerate and bound changed test runs (#3250)

* fix(tests): make the changed-file map select per script and stabilize a budget flake

The changed-file map's bin/ fallback resolved a direct test reference to that
test's whole FAMILY. bin/fm-push-transition-lib.sh is named by exactly one
real-Herdr E2E, so a one-line change to it selected all 12 real-herdr-gated
scripts, including a 341s presentation E2E with no dependency on it.

Resolve direct test references per script, and keep resolving consumer bin/
scripts through the curated map so recorded family-level coupling survives.

Also fix a load-sensitive flake: the tool-update budget deadline is whole-second
granular, so a test budget of 1 left headroom anywhere in (0, 1] seconds and the
first budget check could already read as exhausted.

* feat(bin): make suite wall clock a result and let a family's concurrency be proven

--max-wall-ms fails a run whose wall clock exceeds the caller's budget, after
reporting the per-script results. A suite that stays green while outgrowing its
caller's invocation budget is the regression that got an agent killed mid-run
and retried invisibly, so duration has to be a result rather than a log note.

--pool on the isolation-proof harness runs the same concurrent proof over a
whole family, so 'is this family safe to parallelize?' is answered by a command
instead of a guess. Measured watcher-wake-lock and refused it: 3 of 18 scripts
fail under concurrency on wall-clock assertions about reaching the next poll.

* perf(bin): schedule the changed suite concurrently, longest first

The watcher-wake-lock family is proven concurrent-safe (two clean runs, 18
candidates, 0 failures at 4 workers; docs/fm-test-isolation-proof.md), so
--changed now schedules its proven-concurrent scripts with bounded parallelism
and runs any unproven remainder serially afterwards, never beside them.

Concurrent runs are ordered longest-hint-first. Workers are handed scripts in
order, so alphabetical order started the 193s fm-watch-triage last and stranded
it running alone: 395s wall against a 205s balanced four-worker sum.

An explicit --jobs keeps its strict refusal, so every CI lane is unchanged.

* fix(bin): bound a hung test instead of letting it hang the suite

tests/fm-calm-pi-extension.test.sh was observed running 17+ minutes against a
464ms recorded hint, and the suite had no per-script bound to stop it. An
unbounded suite is precisely what silently outruns a caller's invocation budget,
and --max-wall-ms is evaluated after the run so it cannot end one that never
finishes.

--per-script-timeout-secs terminates a script that outruns it and records exit
124, so the run still completes, accounts for the script, and fails. The
auto-concurrent --changed path applies 900s, far above the slowest real script
(the 341s Herdr presentation E2E), so it only ever converts a hang.

* no-mistakes(review): Enforce safe concurrency and descendant timeouts

* no-mistakes(review): Validate empty runs and isolation proof pools

* no-mistakes(review): Measure selection time in wall budget

* no-mistakes(review): Reap interrupted workers and bound finalization

* no-mistakes(review): Contain shutdown descendants and watchdog finalization

* no-mistakes(review): Honor remaining budget and close launch races

* no-mistakes(review): Restore timeout helper and simplify runner cleanup

* no-mistakes(review): Record isolation pool admission metadata

* no-mistakes(review): Bound Chrome reap and scope proof admission

* no-mistakes(review): Align proof scheduling and preserve budget summaries

* no-mistakes(review): Remove unreliable finalization watchdog

* no-mistakes(review): Freeze budget duration and enforce admission caps

* no-mistakes(document): Refresh test runner concurrency documentation

* no-mistakes(lint): Fix ShellCheck findings in test runner scripts

* no-mistakes(ci): Fixed Greptile’s concurrency-consent finding. `--changed` now remains serial by default; `--changed --jobs auto` explicitly opts into bounded concurrency and the automatic hang timeout. Updated documentation and added behavioral coverage proving serial default behavior, explicit concurrent scheduling, and refusal of `--jobs auto` outside `--changed`. Verified with `bash tests/fm-test-run.test.sh`, `bin/fm-lint.sh`, and `git diff --check`

* no-mistakes(review): Restore automatic changed-suite concurrency and timeout

* no-mistakes(review): Correct changed-suite contributor guidance

* no-mistakes(review): Reject gate-skipped isolation proofs

* no-mistakes(review): Correct automatic concurrency evidence

* no-mistakes(review): Isolate nested runner process groups

* no-mistakes(review): Remove unreliable signal cleanup machinery

* no-mistakes(test): Narrow changed-suite selection to executable contract owners

* no-mistakes(document): Document isolation proof skip and artifact semantics

* no-mistakes(ci): Fixed Greptile’s concurrency-consent finding. `--changed` now remains serial by default; bounded concurrency requires explicit `--jobs auto`. Updated behavioral coverage, contributor guidance, and isolation-proof commands accordingly. Verified with `tests/fm-test-run.test.sh`, `bin/fm-doc-audience-check.sh`, `bin/fm-lint.sh`, Bash syntax checks, and `git diff --check`; all passed

* no-mistakes(review): Restore plain changed-suite automatic concurrency

* no-mistakes(review): Record resolved changed-suite worker co…
peterOC26 added a commit to peterOC26/firstmate that referenced this pull request Sep 1, 2026
* fix: safely split supervision wake handling by actor (#2953)

* feat(bin,pi): per-actor wake consume, silent success gating, merge-poll dedup

Three related fixes to the shared wake-drain and Pi supervision-branch
dispatch machinery so a routine success is never main-blocking and a
mixed queue can safely split between actors.

1. Successful routine results no longer create main-blocking wake rows.
   fm-startup-network.sh only enqueues a check: startup-network wake when
   the deferred result is actionable (state is not "done", or the report
   carries a bootstrap-diagnostics actionable prefix); a clean success
   stays durable in the report file without ever waking the agent.

2. Per-actor wake-drain consume contract. bin/fm-wake-drain.sh now scopes
   presentation and --ack-through to the current actor
   (bin/fm-lease-lib.sh's fm_lease_actor): main keeps the original
   whole-queue cutoff behavior, unaffected. A branch actor
   (FM_SUPERVISION_ACTOR=branch, set only inside the Pi supervision
   branch's own bash tool calls) is scoped to an explicit eligible-row
   snapshot instead of a cutoff comparison, so it can never remove a row
   it was not granted - the fix for the swallow risk that used to force
   an all-or-nothing whole-queue fallback to main.
   .pi/extensions/lib/fm-branch-dispatch.ts's scopeForUnreadWake is the
   single owner of eligibility: a check-kind row (merge-confirmation
   polls, Relay mentions, credential/auth failures) is now excluded
   rather than vetoing the whole scan for a non-heartbeat wake, while a
   heartbeat review keeps its original all-or-nothing rule unchanged.
   writeEligibleRowsSnapshot publishes the exact eligible sequence
   numbers before every branch prompt; fm-primary-pi-watch.ts's offer
   still refuses a check-kind trigger outright so a main-only close is
   never itself routed to the branch.

3. A repeat identical merged-PR-poll result for an already-notified task
   is absorbed instead of enqueued again. A poll's own retirement state
   is scoped to one registration and cannot see a prior registration's
   outcome, so a task re-registered after its merge was already surfaced
   would otherwise wake main a second time for the same event.
   bin/fm-pr-lib.sh's new per-task pr-poll-merge-notified marker survives
   across re-registrations to catch that case; the first notification for
   a task still reaches main unchanged.

Regression tests colocated in tests/fm-startup-network.test.sh,
tests/fm-wake-queue.test.sh (including the mixed-queue no-swallow
property), tests/fm-pi-branch-extension.test.sh, and
tests/fm-pr-check-security.test.sh. docs/watcher-continuity.md and
docs/pi-supervision-branch.md updated for the new contracts.

* no-mistakes(review): Bind merge deduplication to canonical PR identity

* no-mistakes(review): Serialize wake row ownership across main and branch

* no-mistakes(review): Bind branch grants and deduplicate within actor claims

* no-mistakes(review): Fallback main-owned wake claims to main delivery

* no-mistakes(review): Clarify silent startup success guidance

* no-mistakes(review): Release residual branch grants after settled prompts

* no-mistakes(review): Reject truncated wake rows as corrupted

* no-mistakes(document): Document per-actor routing and silent startup success

* no-mistakes(lint): Fix ShellCheck findings in wake grant and startup test

* no-mistakes: apply CI fixes

* fix(pi): hide branch outcomes tool rows in Calm (#3024)

* Hide branch outcome tool in Pi Calm

* no-mistakes(review): Preserve stock outcomes rendering and document tool audit

* no-mistakes(review): Document branch read tool audit disposition

* no-mistakes(review): Match stock outcomes output sanitization

* no-mistakes(document): Document Calm custom-tool visibility

* fix: bind no-mistakes attestations to PR head (#3027)

* fix: delegate no-mistakes PR gate to pinned action

* no-mistakes(document): Document commit-bound no-mistakes attestations

* feat(pi): add persistent supervision branch model selection (#3028)

* feat(pi): let operators pin a cheaper supervision-branch model

Supervision is an easier job than the captain's own conversation, so the
Pi supervision branch does not need main's model. A new /supervision-model
command opens Pi's own selector over Pi's own catalog of credentialed
models, plus a "Follow main" entry, and persists the pick as one
<provider>/<model-id> line in this home's gitignored
config/supervision-branch-model. Firstmate keeps no model catalog of its
own.

The branch resolves the pin at every branch build - the first wake of a
cold start and the reopen after /new, /resume, /fork, or reload - so the
choice survives all of them, and picking also releases the live branch so
the next wake reopens the same persistent branch conversation under the
new model. An absent, unreadable, or unparseable file means no pin and
keeps today's behavior byte for byte: no model option is passed and Pi
picks the branch's model exactly as before.

A pin naming a model Pi cannot hand back is never silently downgraded
onto main's model: the branch refuses to build and the wake falls back to
the captain-facing main path naming the unusable pin, which is the
extension's existing failure direction.

The choice is home-local and not part of secondmate inherited
configuration, matching the Pi Calm preference precedent.

docs/configuration.md owns the operator-facing schema. Portable
regressions cover pin-present on create and reopen, pin-absent default,
the command's persistence, cancellation, and live rebind, and both
unusable and unparseable pins. The opt-in real-SDK guard proves the
vendor surface the pin reads and that an explicit model wins over the
model a reopened session recorded.

* no-mistakes(review): Fix supervision model runtime and rebind races

* no-mistakes(review): Restrict supervision picker to isolated runtime models

* no-mistakes(document): Document supervision branch model selection

* fix(pi): make the supervision model pin authoritative on every reopen

Clearing the pin with "Follow main" removed the file but the next branch
build reopened the persistent branch session with no explicit model
override, so Pi restored the model that session had recorded - the old
pinned model - while the command reported that the branch now follows
main. The same gap meant an absent pin did not reliably mean
same-model-as-main once a home had pinned once.

The pin file's current state now decides the model on every branch build,
create and reopen alike, overriding Pi's session-state restore. With a
pin, that model. With no pin, main's own current model is applied
explicitly, tracked from the contexts Pi already hands the extension plus
its model_select event, since the branch is built at wake time with no
context of its own. Only when main's model is unknown, or this home's
stored credentials cannot run it in the isolated branch runtime, does a
build fall back to passing no override at all, which is the behavior from
before the pin existed; the branch is never refused over model choice.

The command's notification now reports the model actually applied, and
says plainly when clearing the pin could not apply main's model instead
of claiming a change that did not take effect.

No credential handling changes: the branch still relies entirely on the
stored credentials its own runtime already holds, and the picker stays
restricted to models that runtime can resolve.

Colocated regressions cover pin present on create and reopen, clearing
the pin returning a reopened branch to main's model and specifically not
the old pinned one, an unparseable pin behaving as no pin, and the
unknown-main-model fallback to no override.

* no-mistakes(review): Make unpinned supervision follow main model changes

* no-mistakes(document): Correct supervision model documentation

* feat(pi): let /supervision-model pick branch reasoning effort (#3079)

* feat(pi): let /supervision-model pick the branch's reasoning effort

Supervision is an easier job than the captain's own conversation, so the
Pi supervision branch does not need main's reasoning effort any more than
it needs main's model. /supervision-model now settles both in one flow:
the existing model picker, then a follow-up effort picker built from Pi's
own supported thinking levels for the model just chosen. Firstmate keeps
no effort catalog of its own; the menu, the clamp, and the vocabulary all
come from Pi.

The pick persists as one line in this home's gitignored
config/supervision-branch-effort, independent of the model pin: a captain
may pin a model, an effort, both, or neither. The effort pin's current
state decides the branch effort on every branch build - the first wake of
a cold start and the reopen after /new, /resume, /fork, or reload - and
overrides Pi's restore of whatever level a reopened branch session
recorded, which is what keeps "Follow main" honest. With no pin, main's
own current effort is applied explicitly and followed live through Pi's
thinking_level_select event, the same way an unpinned branch already
follows main's model, and the two selections now share one build revision
so either change invalidates an in-flight build.

The branch is never refused over effort. Pi owns the clamp, so a pinned
level the branch's model cannot run becomes that model's nearest supported
level while the captain's raw pick is kept for a model that supports it,
and the command reports the level the branch will really run at rather
than the raw pin. A token Pi would not recognize at all is treated as no
pin rather than passed to that clamp, which would otherwise collapse a
typo into the model's lowest level. Only when main's effort cannot be read
either does a build pass no effort override at all, which is the behavior
from before this file existed.

Pi's own effort vocabulary is pinned by a bidirectional type assertion
against Pi's getThinkingLevel return type, so the tracked strict typecheck
against the installed package fails the moment Pi adds or removes a level.

docs/configuration.md owns the operator-facing schema for both pins.
Portable regressions cover the pin on create and reopen, model-only and
effort-only pins working independently, clearing a pin returning the
branch to main's effort, live-follow of a mid-session change, the clamp,
an unrecognized token, the unknown-main-effort fallback, and the command's
two-step flow, persistence, cancellation, and honest reporting. The opt-in
real-SDK guard proves the vendor surface all of that rests on, and also
repairs a pre-existing gap that left it unable to load the extension at
all.

* no-mistakes(review): Resolve effective branch effort honestly

* no-mistakes(document): Clarify Pi-owned effort picker behavior

* fix: keep routine supervision noise out of captain chat (#3093)

* fix(supervision): silence empty board closes and decouple the heartbeat

Two unrelated sources of noise put routine supervision events in the
captain's chat.

An empty Lavish board close - the captain reads a review surface, says
nothing, and closes it - became a check wake whose entire content was
that nothing happened. Suppress it at its source instead of routing it
anywhere: the generic runner gains a `silent` adapter seam mirroring the
existing `terminal` one, and the Lavish adapter answers it for exactly
one positively-determined shape, an `ended` session carrying no queued
content block. A silenced result is recorded durably handled so it does
not return on a later reconcile. Everything else announces unchanged - a
`Send & End` close carrying the captain's real answer, an `ended` result
still carrying content, a waiting or missing session, an unreadable
result, and every adapter that implements no `silent` command at all.
The keyed-answer feed is untouched, so suppressing an announcement never
suppresses the captain's own answer.

A fleet heartbeat was deferred to main merely because some unrelated
check row happened to be sitting unread, which put a routine fleet
review in the chat for a reason that had nothing to do with the fleet. A
check row is permanently main-owned, so it is now excluded from a
heartbeat claim rather than vetoing the scan, exactly as in every other
mode. What all-or-nothing guarantees is unchanged: the branch takes
every branch-ownable unread row or none of them, and an unresolvable
task-local row, an unknown row kind, or an unreadable queue still defers
the whole review to main. Main is still woken for the check on that
check's own triggering close, so nothing starves.

Main-only classes are unchanged and now each covered by a test: Relay
mentions, credential failures, merge confirmations, real board answers,
and watcher-failure repair. The per-actor acknowledgement and
no-cross-swallow properties are untouched.

* no-mistakes(review): Fail closed on all Lavish content headers

* no-mistakes(review): Suppress false unacknowledged status for silenced results

* fix(bin): stop a correlation token from hiding and stranding decisions (#1967)

* fix(classify): read the decision fold through a correlation token

status_line_verb stripped a trailing [key=...] from a status line's prefix
but left everything else glued to the verb, so a line carrying the
correlation token bin/fm-pending-reply-lib.sh embeds and a secondmate
echoes back matched no arm of _fm_decision_fold_line. Such a line folded as
ordinary status in both directions: a needs-decision or blocked opener never
opened its key, and a resolved or captain-held closer never closed one. The
same glued verb also hid correlated done and blocked lines from
status_is_captain_relevant and status_is_terminal_verb, and let correlated
working and resolved lines leak through the free-text fallback the
nonterminal guard was meant to stop.

The verb parse now walks whole words and drops only a token of the exact
shape a firstmate library writes - corr=<16 hex>, plus the bracketed form
bin/fm-secondmate-report.sh emits - before or after the key token, unkeyed,
or doubled. An arbitrary name=value word is deliberately NOT skipped:
skipping unknown tokens would let free text carrying an equals sign reduce
to a bare verb and impersonate a transition, which is the takeover the
strict parse and _fm_decision_key_transition_allowed exist to prevent. A
prefix with no corr= substring is returned byte-for-byte as before, so every
line without a token keeps its exact historical verb.

FM_OPEN_DECISIONS_FOLD_VERSION goes to 3, because every cursor persisted
under the previous reading carries an open set computed while correlated
lines were invisible and must be rebuilt from byte 0.

Measured over a real 383-line status log: 254 lines keep byte-identical
captain-relevance, pause, terminal-verb and captain-held verdicts, and all
129 changed lines carry a valid token - 14 correlated done/blocked/
needs-decision lines become captain-visible, and 20 correlated
working/resolved lines stop being escalated on prose alone.

* fix(review): Captain, block token-first decision impersonation

* fix(document): Clarify normalized status verb ownership

* fix(classify): reconcile the correlation-token read with the tag-stop parser

Rebasing onto main put this change beside #2280, which made verb parsing
stop at the first "[name=value]" tag. Both edit status_line_verb with
different intents, so the resolution keeps both rules rather than letting
one overwrite the other:

- #2280's tag stop is kept verbatim and now owns every BRACKETED tag,
  including the "[corr=...]" form fm-secondmate-report.sh writes. The
  bracket-unwrapping arm this branch had added to the token test is
  therefore removed as unreachable.
- This branch's token walk is kept and narrowed to the UNBRACKETED token
  fm-pending-reply-lib.sh writes, which the tag stop does not reach.

Two consequences of standing beside #2280 rather than before it:

The fold version had collided at 4: #2280 spent it on the tag-stop
parser and this branch had spent it on the token read. A cursor
persisted under #2280's reading predates this one and must still be
rebuilt, so the version moves to 5.

A bracketed impostor is dropped from the malformed-token list. On main
today "resolved [corr=deadbeef] [key=victim]:" already reads as the bare
verb, as does "resolved [anything at all] [key=victim]:", because the tag
stop ends the parse at the opening bracket regardless of content. That is
#2280's reviewed contract; asserting otherwise here would narrow it. The
unbracketed impostors it owns stay strict and still fold as prose.

Adds a consumer test for the two verb-string case arms that postdate this
branch: fm-supervise-daemon.sh's transient-stale arm and
fm-crew-state.sh's map_log_state.

* fix(review): Captain: Seed cursor migration fixture with version four

* fix(document): Clarify voice status normalization ownership

* fix(bin): Cursor-Park unter Pi-Host ohne Cursor-Identität stilllegen (#3115)

* fix(bin): Cursor-Park unter Pi-Host stilllegen.

pi-cursor-sdk lädt .cursor/hooks.json in die Pi-Sitzung und parkte einen zweiten Watcher; das erzeugte rearm-resurface und brach laufende Rückfragen ab. Bei PI_CODING_AGENT=true beendet der Park sofort, native cursor-agent Primaries bleiben unverändert.

* fix(bin): Cursor-Park trotz PI-Leak nur ohne Cursor-Identität stilllegen.

Stand-down gilt nur bei PI_CODING_AGENT=true ohne CURSOR_AGENT und ohne CURSOR_INVOKED_AS. Handgestartete cursor-agent Primaries mit geerbtem PI-Marker parken weiter.

* no-mistakes(document): Document Cursor park Pi-host stand-down

* fix(bin): no-mistakes-Mindestversion auf 1.46.0 anheben.

Die PR-Attestierung verlangt ab 1.46.0 strukturierte Pipeline-Schritte; der Bootstrap-Floor blieb bei 1.31.2 und ließ zu alte Builds zu.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(pi): make supervision model picker searchable and scrollable (#3099)

* fix(pi): make /supervision-model's model list bounded and searchable

Pi's generic extension selector renders every option at once with no
search box, so a real eligible catalog ran off the top of the terminal.
The model step now draws the same rows through Pi's own SelectList - the
bounded scrolling primitive behind Pi's /model picker - with Pi's own
Input and fuzzy filter above it for search, keeping 'Follow main' first,
the branch-runtime eligibility filter intact, and the pick branch-only.

Pi's ModelSelectorComponent is deliberately not reused: its selection
handler writes the captain's default model through Pi's settings manager,
which would move main's conversation as a side effect of pinning the
branch. The effort step's menu is a handful of levels and stays on Pi's
plain selector dialog.

* no-mistakes(document): Clarify supervision picker documentation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes(document): Document searchable supervision model picker

* no-mistakes: apply CI fixes

* fix(bin): durably report merged pull requests (#3104)

* fix: make a landed merge leave a durable outcome

A merge was the one lifecycle event that left no record outside the
merging agent's memory. bin/fm-pr-merge.sh ended at the forge call, and a
home merging under standing authority never waits for the merge poll that
would otherwise confirm it, so three real merges reached the captain as
silence.

bin/fm-merge-outcome-lib.sh is the single owner of that record. A
secondmate home reports the landed PR upward on the same parent reply
channel its terminal-outcome backstop already uses; a main home records
it on the durable wake queue. The record is at most once per task and
canonical PR identity, and only a merge that actually landed produces one.

The merge poll feeds that same channel when it detects a merge this home
did not perform, so the captain's own forge merge and a merge firstmate
performed itself produce one consistent outcome instead of two reporting
paths. No new state file and no second watch path.

Two smaller gaps from the same failure:

- A mate charter listed its report triggers without naming a landed
  merge. Under standing merge authority nothing is ever "ready for
  review", so the enumerated list silently omitted the case that matters.
- A secondmate home seeded without its parent binding failed every
  terminal-outcome report for the same reason, and the diagnostic never
  named the binding. It does now.

* no-mistakes(review): Harden durable merge outcome reporting

* no-mistakes(review): Make merge race regression deterministic

* no-mistakes(review): Make merge outcomes retry-idempotent and forge-confirmed

* no-mistakes(review): Unify merge publication under canonical outcome marker

* no-mistakes(review): Publish merge outcomes before committing dedup markers

* no-mistakes(review): Document at-least-once merge outcome recovery

* no-mistakes(review): Use supported GitHub confirmation and update recovery docs

* no-mistakes(review): Preserve distinct merge wakes by PR identity

* no-mistakes(document): Document durable merge outcome semantics

* no-mistakes(test): Make merge outcome interleaving test deterministic

* no-mistakes(document): Clarify merge outcome documentation ownership

* fix(lint): keep the merge-outcome library an analysis boundary

bin/fm-watch.sh followed the new merge-outcome library's source graph,
which reaches the wake queue, PR identity, and secondmate parent
libraries. Expanding that inside an already-large lint root pushed
ShellCheck's external-source analysis past the bounded CI lint worker:
the Lint job was killed with SIGTERM after five silent minutes, twice,
having emitted no diagnostics at all.

Make it an analysis boundary, exactly as the transition and inbox owners
directly above and below it already are and for the same stated reason.
Coverage is unchanged because the library is a canonical lint root in its
own right and is still linted as one.

Measured locally: the watcher goes from not terminating within 120s to
9s clean, and the library alone lints in 1s clean.

* fix(bearings): preserve projections through inventory mismatches (#3129)

* fix(bearings): keep an inventory-mismatch home readable, and mark warnings as repairs

A backlog-vs-metadata inventory mismatch inside a secondmate home was being
reported as "we cannot read that home", which discarded that home's open
captain calls, queued work, landed work, and live workers from the whole
Bearings digest. The main home already treats the identical mismatch as a
harmless disclosure; this makes the secondmate path agree.

- fm-fleet-snapshot.sh: the invalidity gate now passes orphan_in_flight,
  unowned_current, and terminal_in_flight through the partial-structured
  carve-out alongside child_current_unavailable, so those homes keep their
  decisions, holds, queued, landed, and live work and leave unreadable[].
  missing_backlog and unstructured_current stay on the discard path, because
  there the backlog itself is untrustworthy.
- fm-fleet-snapshot.sh: the same three kinds no longer collapse the home's own
  classification to "unknown"; the real captain_decision / active_child_work /
  externally_held classification survives and invalidity carries the warning.
  An unavailable child state still collapses it, including when a mismatch
  masks it under strict-invalidity precedence.
- secondmate_landed.partial now keys on partial-structured trust rather than an
  unknown state, so an inventory-mismatch home is still disclosed as partial.

Ask the home that owns the wrong books to fix them:

- bin/fm-secondmate-reconcile.sh sends exactly one reconcile instruction per
  mismatch episode through the ordinary steering transport. A persistent
  mismatch keeps its episode identity and never re-nags; a changed mismatch
  earns one more ask; a repaired one is forgotten so a recurrence is asked
  about again. The parent never touches the mate's own files, and a failed send
  records nothing so the next run retries it.

Give integrity warnings their own look on the board:

- charted rows take an optional kind of "queued" (the default) or "warning".
  A warning badges "needs repair" instead of "waiting" and is excluded from the
  Charted Next count, so alarms stop reading as dispatchable queued work. No
  fifth board section, and every existing payload stays valid.

Tests pin the new policy behaviorally: the retained surfaces and classification
for all three mismatch kinds, the still-discarding unstructured_current and
missing_backlog cases, the once-per-episode reconcile ask through real durable
steering records, and the board rendering exercised through the shipped
template under a minimal DOM shim.

* no-mistakes(review): Make reconcile dedupe atomic and warnings non-dispatchable

* no-mistakes(review): Preserve reconcile identity and reject stale snapshots

* no-mistakes(review): Order snapshots uniquely and canonicalize episode identities

* no-mistakes(review): Add fire-and-forget reconcile and separate warning overflow

* no-mistakes(review): Exclude fire-and-forget from escalation and track reconcile background

* no-mistakes(review): Run reconcile enqueue inline across all adapters

* no-mistakes(review): Track reconcile clears across strict-invalidity homes

* no-mistakes(review): Persist reconcile transitions atomically

* no-mistakes(document): Document reconcile and fire-and-forget contracts

* refactor(bearings): replace the reconcile episode dedupe with a 4-hour cooldown

The reconcile ask needed to fire once per problem without nagging on every
recap. The episode-precise record that tried to do that had to be correct in
every direction at once - order two concurrent snapshots, tell a repair from a
new problem, and never lose a clear - and each direction it got wrong either
swallowed a nudge or sent a duplicate.

A per-home cooldown removes the whole class. One durable timestamp per home,
one nudge per four hours, and nothing to get stale, mis-order, or
mis-classify: a home in mismatch is asked once, later recaps stay silent, and
a mismatch still sitting there after the window earns one gentle re-nudge.

- bin/fm-secondmate-reconcile.sh: state/<id>.reconcile-nudged holds the epoch
  second of the last ask; FM_RECONCILE_COOLDOWN_SECONDS names the window. The
  episode identity, ordering generation, pending/clear transitions, and
  delivery-identity reuse are all gone. A known-undelivered send starts no
  cooldown so the next run retries it; an unconfirmed one does, because a
  duplicate ask is worse than one the mate may already hold.
- bin/fm-fleet-snapshot.sh, bin/fm-bearings-snapshot.sh: drop the snapshot
  `observation` monotonic identity, which existed only to order those records.
- bin/fm-teardown.sh: retire the cooldown record with the endpoint's other
  runtime artifacts, so reseeding a retired id is not silenced by its
  predecessor's window.

The inline durable fire-and-forget send is unchanged, and the projection fix
and the warning surface are untouched.

Tests follow the behavior: the cooldown suite now pins one ask per window, the
re-nudge after it, the four-hour boundary, per-home independence, and that the
ask stays out of a re-ring ladder that still rings an ordinary steer beside it.
The obsolete observation-ordering test is deleted with the machinery it covered.

* no-mistakes(review): Serialize reconcile cooldown commits with mate lifecycle

* no-mistakes(review): Reject stale reconcile snapshots across mate reincarnations

* no-mistakes(review): Start reconcile cooldown after delivery completes

* no-mistakes(review): Keep reconcile sends nonblocking and remove pending residue

* no-mistakes(document): Document reconcile skip and stale-endpoint behavior

* no-mistakes(lint): Fix reconcile test subshell lint warning

* no-mistakes: apply CI fixes

* fix(bin): reconcile markerless remote secondmates safely (#3140)

* fix(bin): stop dropping reconcile nudges for markerless remote secondmates

A persistent remote secondmate's parent-side state/<id>.meta never carries
spawn_gen: bin/fm-spawn.sh's spawn_remote_secondmate() is its sole writer and
never writes one, because that incarnation identity does not apply to a
remote route. fm-secondmate-reconcile.sh's row filter required a non-empty
spawn_gen matching an identifier regex, so every such row was silently
dropped before the per-row loop ever saw it: no sent/stale/failed line, no
cooldown record, nothing sent, and no trace of why.

Give a legitimately markerless persistent remote secondmate a safe substitute
identity - its recorded remote_host - instead of weakening the spawn_gen
check for rows that do have a generation:

- bin/fm-secondmate-reconcile.sh: carry host through the row projection for
  both fm-fleet-snapshot.v1 and fm-bearings.v1 documents, and admit an empty
  spawn_gen instead of filtering the row out. A new revalidate_identity()
  compares the sampled spawn_gen against current metadata when one was
  sampled (unchanged), or the sampled host against the metadata's
  remote_host when none was sampled and the metadata still carries no
  spawn_gen of its own. A row with neither a spawn_gen nor a host has no
  safe identity at all and fails loudly instead of vanishing, exactly the
  visibility the original bug lacked.
- Rows now join on the ASCII unit separator rather than @tsv: bash's
  IFS-whitespace read collapses consecutive tabs, which would have silently
  dropped a legitimately empty field again.
- bin/fm-bearings-snapshot.sh: thread host through the secondmate_reconcile
  projection so the fm-bearings.v1 path (the one bearings itself feeds to
  the reconcile hook) carries the same substitute identity.
- tests/fm-secondmate-reconcile.test.sh: end-to-end coverage through the real
  remote transport (fm-on.sh + fm-remote-secondmate-control.sh against a
  genuinely seeded remote home) for a markerless mate nudged once per
  cooldown window, a stale/replaced remote route refused exactly like the
  existing local spawn_gen case, and a row with no identity at all failing
  loudly rather than being swallowed.

* no-mistakes(review): Enforce markerless remote host identity during final delivery

* no-mistakes(document): Document markerless remote reconciliation safety

* fix(bin): hand a busy declared pause to the away-mode daemon once, undecorated (#3147)

* fix(watch): hand a busy declared pause to the away-mode daemon undecorated

While away mode is active the daemon owns triage and the watcher reverts to
one-shot, handing over plain wake identities the daemon classifies itself. The
busy-turn bound was the one stale path that did not: with afk active it ran the
wedge timer, so the daemon received a wake already decorated as a possible wedge.

That decoration outranks the daemon's own verdict. handle_wake escalates an
enriched wedge reason before its pause classification can apply, so a crew that
declared the wait itself - a `paused:` external wait or a verified captain-held
transfer holding a live foreground call - was wedge-escalated once per
FM_STALE_ESCALATE_SECS for as long as the wait lasted, the escalation count
climbing into demand-deep-inspection on a pane nobody needed to inspect.
Measured on the pre-fix tree, five consecutive re-arms produced five escalations.

busy_turn_bound_check now reads the declaration before the afk branch: away mode
hands off the plain window identity, one-shot per distinct stale hash, leaving
normal-mode pause bookkeeping unwritten because the daemon owns it there. The
daemon then classifies the wait itself and self-handles it on the long cadence.
Normal-mode behavior is unchanged, and lifting the declaration still restores the
busy-pane wedge escalation on the same pane.

The regression covers all three: the undecorated handoff with no wedge timer or
escalation counter, the one-shot on re-arm that the escalation ladder used to
climb, and the restored wedge escalation once the declaration is lifted.

* no-mistakes(review): key afk busy-pause handoff on declaration, clear wedge state

* no-mistakes(document): docs: scope away-mode busy-bound handoff to declared waits

* no-mistakes(document): docs: note afk busy-bound handoff in watcher header

---------

Co-authored-by: Talon Stark <talonstark@gmail.com>

* fix(bin): name the stale submodule pin behind a pooled slot refusal (#3121)

* fix(bin): explain a pooled slot's stale submodule refusal

A pool slot whose submodule pin moved is refused with "is not clean; refusing
to discard uncommitted work", while the operator's own `git status` in that slot
reads clean. The message names no submodule, no pin, and no remedy, so the
refusal is unreadable and the slot looks wedged for no reason. That is the
failure that jammed three slots in a row when a submodule pin moved.

The refusal itself was never the bug and is unchanged: the gate still refuses,
and still touches nothing. It now distinguishes the one case it can prove and
says what it found - the submodule, the pin the slot has, the pin the base
records, and the command that clears it.

The diagnosis is deliberately conservative, because ` M <path>` alone cannot
tell a stale pin from real work. An entry is reported as stale only when every
reported entry is a gitlink whose submodule is internally clean and whose
recorded pin actually differs. A submodule holding uncommitted work, untracked
files, or an unpushed commit therefore keeps the original uncommitted-work
refusal, even when its pin is also stale - the remedy command would be wrong
there, and the conservative refusal is the safe answer.

Nothing is converged, synced, initialized, or deleted. There is no new failure
path: a slot that launched before still launches, a slot that refused before
still refuses, and projects that configure a submodule `ignore` are read exactly
as before. Paths are read with core.quotePath=false so a non-ASCII submodule is
named rather than falling back to the unreadable message.

Tests keep the reproductions that prove the message is accurate: the stale-pin
diagnosis (which fails against the previous refusal), work inside a submodule
still refused as uncommitted work, and a stale pin carrying real work refused
conservatively rather than called stale. Each asserts the slot is left untouched.

* no-mistakes(review): require remote containment before calling a submodule pin stale

* fix(bin): stop printing a remedy the containment check cannot stand behind

The stale-pin diagnosis printed `git submodule update --checkout` as the command
that clears the slot. The containment check behind it reads local refs only and
never fetches, because this gate has to stay usable offline. A remote-tracking
ref that has gone stale - its upstream branch deleted or force-pushed, and never
pruned - still reads as containment, so a commit that is really unpushed can look
contained and that command would move the submodule off it.

Naming the submodule and both pins is the whole point of the diagnosis: it turns
"is not clean", on a slot whose own `git status` reads clean, into a statement of
which submodule drifted and where it drifted from. The operator can choose the
remedy from that, seeing the whole picture. Printing an instruction that rests on
a judgement which can be fooled is worse than printing none, so it is dropped.

The limitation is now stated where it applies, in the script header and beside
the check itself, rather than left for a reader to discover.

No fetch is added: the gate stays offline-safe by design. Nothing else changes -
the same conditions are refused, the slot is still never touched, and a submodule
carrying real work or an unpushed commit still keeps the conservative
uncommitted-work refusal.

* no-mistakes(review): bound submodule containment probe to first commit

* fix(pi): prevent stale captain outcome re-emissions (#3154)

* fix(pi): type captain supervision outcomes so main relays them

A captain-relevant branch outcome reached main as a bare user message with
no marker of origin or required action, written in main's own captain-facing
voice, landing in a tail that often already held several such notes. Pi keeps
only a custom message's content when it builds the provider request, so
customType and display never reach the model and content was the only place
that identity could live.

Main could not tell an incoming outcome from its own earlier answer and
sometimes re-emitted that answer instead of relaying the outcome, losing it.
Measured against real Pi 0.84.1 on openai-codex/gpt-5.6-sol: 6 failures in 24
turns, rising to 3 in 6 once one stale answer was already in the tail, which
is how one captain conversation saw six identical messages in a row. The same
scenario with the outcome typed failed 0 times in 14 turns.

Wrap only the captain-verdict note in the branch-outcome operational kind
owned by bin/fm-operational-input.sh. Delivery is otherwise unchanged: still
display: false, still one triggerTurn follow-up, so the turn remains the
single captain-visible outcome and no hidden note is ever shown twice.
Routine notes stay plain because their renderer reads the glyph off the front
of that same string. An outcome that cannot be encoded degrades to the same
instruction as plain text rather than being lost, matching this file's stated
failure direction.

The existing assertions could not catch this: they pin the sendMessage
options and never look at what main receives. Add a portable regression that
classifies the delivered payload with the real protocol executable, and a
live guard that runs the real Pi SDK's own convertToLlm to prove content is
the entire model-visible payload.

* no-mistakes(document): Document typed Pi captain outcomes

* fix(bin): keep a declared wait on the pause cadence under a busy pane or enriched wedge (#3155)

* fix(bin): keep a busy pane from retiring a still-declared wait's window

The away-mode daemon's pause re-surface recheck (housekeeping step 2b) read a
busy pane as "the crew resumed" and dropped the declared-wait marker, without
re-reading that the crew's own latest status line still declared the wait.

That inference is not safe, because a declared wait can legitimately hold a pane
busy: a worker sitting on a long foreground call keeps that call live for as long
as the wait lasts. The marker is then cleared while the declaration still stands,
and migrate_watcher_pause_markers recreates it with a fresh timestamp on the very
next tick, so the window restarts forever and the wait never matures into its one
bounded recheck.

Away mode makes that terminal. Since the watcher half landed, a busy pane under a
declared wait is handed to the daemon exactly once per declaration and never woken
again while the declaration stands (bin/fm-watch.sh, busy_turn_bound_check), so
this recheck is the only thing left that can re-surface the pane at all. Measured
end to end on a throwaway state root, away mode active, a pi pane busy past
FM_BUSY_TURN_MAX_SECS, status still `paused:`, over six PAUSE_RESURFACE_SECS
windows: 0 captain-facing rechecks before this change, 6 after - one per window,
with the marker reset each time.

The fix drops only the busy arm of the 2b probe, leaving it an endpoint-readability
check: exit code 2 still means the capture failed, so the endpoint is gone and the
marker goes. The loop head above already drops the marker the moment the status
line stops declaring the wait, so nothing else is needed to end the routing, and
the reconcile path runs before the probe ever reads a pane.

tests/fm-daemon.test.sh: test_housekeeping_paused_resumed_cleared pinned the old
inference on purpose - its fixture's status line still read `paused:` while the
pane was busy, and its comment read "A pause whose pane became busy again (the
crew resumed)". Its fixture now resumes the way a crew actually resumes, by
appending a non-declaring status line, and it asserts its own busy verdict first
so it cannot silently decay into the idle-pane case that
test_housekeeping_paused_unpaused_cleared already covers. What it pins is now the
inverse guard: a busy pane must not GATE the clear either, so an over-correction
that kept the marker alive whenever the pane is busy would fail it.

test_housekeeping_busy_declared_wait_matures_its_window is the new regression, over
both declaration forms. It asserts the busy verdict, then that ticks inside the
window neither escalate nor let the marker be recreated with a fresh timestamp,
then exactly one recheck past the window named for the right human and never a
wedge, then silence on the next tick inside the reset window. It fails on
unmodified main with "produced 0 escalations past its window, expected exactly
one".

Refs #3149

* fix(bin): let a declared wait outrank an enriched wedge escalation

handle_wake classifies a stale wake through classify_stale, which returns a
`pause` verdict for a crew whose latest status line declares an external wait or
a verified captain-held transfer. It then threw that verdict away whenever the
wake reason matched `idle *s, possible wedge, escalation *`, so the watcher's
enriched wedge decoration outranked the crew's own declaration and a healthy
declared wait was escalated once per FM_STALE_ESCALATE_SECS for as long as the
wait lasted.

The enriched reason earns its precedence over the daemon's cheaper status-log
absorption honestly - it carries the watcher's escalation count and its explicit
"do not re-absorb on the run-step/pane state alone" demand. A `pause` verdict is
not run-step or pane state. It is the crew's own declaration that this pane waits
by design, which is precisely the question the wedge timer cannot answer for
itself, so it is the one verdict that decoration must not override.

The two classifications genuinely disagree in steady state rather than only in a
race: a crew that declares `paused:` while its no-mistakes run is still attributed
to its code reads `working` to the watcher's pause_state_class, so the watcher
takes the wedge timer while the daemon's classify_stale reads the status log and
correctly returns `pause`.

The wait stays bounded, not silenced. Absorbing to the pause action records the
declared-wait marker and drops wedge aging, and housekeeping (2b) then owns the
re-surface, so the pane still reaches the captain - once per PAUSE_RESURFACE_SECS
as an explicit "recheck whether the wait still holds", instead of once per
FM_STALE_ESCALATE_SECS as a possible wedge. Measured on a throwaway state root
over five wedge cadences for one declared wait: 5 escalations climbing to
demand-deep-inspection before this change, 0 after, with the one bounded recheck
still delivered.

tests/fm-daemon.test.sh: test_stale_diagnostic_wedge_survives_busy_housekeeping's
`paused` case pinned the old precedence on purpose, asserting exactly one
escalation carrying the demand-deep-inspection payload. That case now asserts the
pause cadence instead - no escalation inside the window, pause tracking recorded -
while the `working` and `prior-terminal` cases keep asserting the enriched wedge
verbatim, so the override itself is still pinned everywhere it is correct.

test_enriched_wedge_under_declared_wait_uses_pause_cadence is the new regression.
It asserts the fixture's own classifier verdict is a pause first, so the case
cannot go vacuous, then drives four consecutive wedge-cadence deliveries in both
the plain and demand-deep-inspection forms through the real handle_wake and
housekeeping pair, then matures the window for exactly one awaiting-external
recheck, then lifts the declaration and requires the same enriched wedge to
escalate again unchanged. It fails on unmodified main at the first delivery.

Refs #3149

* no-mistakes(review): align afk skill recheck wording with still-declared contract

* no-mistakes(document): daemon doc comments: pause window ages on declaration

---------

Co-authored-by: Talon Stark <talonstark@gmail.com>

* fix(bin): recover Claude auto-arm from hung claims (#3156)

* fix(bin): make Claude auto-arm continuity self-heal past a hung claim

On a Claude primary, a Stop-hook auto-arm process that hung mid-arm held
the single-flight owner lock with its epoch ledger frozen at
outcome=arming, and the abandonment proof read any live lock holder in
arming as legitimately deciding forever. Every later Stop firing exited
0 at the lock, the turn-end guard kept deferring to the hung owner as
recovery under way, and the watcher was never auto-re-armed again for
the rest of the session - supervision survived only on manual arms and
lapsed between them (the 2026-08-26 watcher flap).

Corrections layered onto the lock-held-across-arm shape each reopened
the same concurrency class one level down, so this replaces the claim
machinery wholesale with a generation-based optimistic design:

- The epoch ledger's monotonic sequence IS the claim generation; the
  two-line entry (classic epoch record plus the claimant's MANDATORY
  pid-identity) is the claim. Every firing defers to a live OPEN claim:
  outcome arming, owner alive, identity recomputes and matches, and not
  stuck (entry and watcher beacon both older than the guard grace).
- A finished, dead, identity-mismatched, identityless, or stuck claim
  is superseded by simply taking the next generation - no signalling or
  revocation of a steady-state predecessor.
- No mutex is held across arming or output; the owner lock survives
  only as a micro-mutex around individual ledger writes. A superseded
  owner goes completely silent: ownership is re-verified before every
  arm invocation, episode-state mutation, ledger write, and
  continuation.
- The irrevocable commit point of a translation is the exit status
  (the harness delivers the collected stderr only on exit 2), so the
  owned terminal ledger write is the atomic commit: the winning
  generation exits 2 unconditionally after it, a refused one exits 0
  silently even after printing, and the once-per-episode failure notice
  commits in the same owned critical section as the winning failed
  write. Two bounded residuals are documented accepted intent: an owner
  dying between its owned write and its own exit, and a hung old-build
  owner resuming during the one legacy upgrade window.
- The pre-generation lock-holding claim shape keeps defer-or-reclaim
  behavior through a legacy shim: a live identity-verified stuck owner
  is retired via TERM (with a queued TERM sufficient when the owner is
  stopped) before its lock is removed, an unverified or identityless
  pid is never signalled but never blocks a proven-abandoned reclaim,
  and the lock's identity evidence is grafted into the ledger
  (mtime-preserving) so pid-reuse protection survives the lock.
- The guard reads the same predicates for recovery ownership and its
  terminal fail-open (which re-checks for a live open claim under the
  held locks before committing the attended alarm), with ledger reads
  anchored to line 1 so the identity line can never confuse them.

Behavioral regression coverage exercises all three edge classes through
the real hook and guard - a live open claim defers with no lock held, a
stuck claim is superseded and the home re-arms, and an end-to-end run
with a genuinely hung owner shows a concurrent firing deferring
promptly mid-arm, a later firing superseding the stuck owner, and the
superseded owner exiting silently without a second translation - plus
the identityless/reused-pid loopholes, the superseded-owner arm
boundary, and the legacy TERM, SIGSTOP, and signal-free reclaim paths.

* no-mistakes(review): Refuse auto-arm commits when notice marker creation fails

* no-mistakes(review): Make episode reset atomic with generation ownership

* no-mistakes(document): Update auto-arm generation and commit documentation

* fix(bin): verify the real GitHub merge outcome instead of reporting an unproved merge (#3064)

* fix(pr): verify GitHub merge outcome

* no-mistakes(review): Captain, fixed forge-only merge verification, queue guidance, metadata propagation

* no-mistakes(document): Correct forge-specific merge documentation

* no-mistakes(review): Captain: forge-only queue fix, focused tests pass

* no-mistakes(review): Captain: suppress closed-state guidance and prove parent regression

* no-mistakes(review): Captain: remove history proof; retain executable regressions

* no-mistakes(document): Clarify GitHub recording timing in architecture docs

* no-mistakes(document): Clarify outcome-aware PR merge recording documentation

* no-mistakes: apply CI fixes

* Revert "no-mistakes: apply CI fixes"

This reverts commit c326cfa9430c6173eedc8ff7f27d19d0552daf01.

The automatic CI repair round removed the up-front `gh` prerequisite check
while keeping the `gh` dependency: `bin/fm-pr-merge.sh` still calls
`gh api graphql` for the outcome read and `gh api` for the branch-rules read.
That left the same hard requirement without the clear named error, and review
immediately raised a new finding for exactly the failure the check prevents -
`gh-axi pr merge` landing the merge while the follow-up read fails, so the PR
metadata is never recorded.

The check is also symmetric with the GitLab arm directly above it, which
already refuses up front when `glab` or `jq` is missing, on the stated
principle that a missing tool should be a named prerequisite rather than a
merge that is armed and then refused for an unexplained reason.

The workflows this round was chasing sit at `action_required` because this is
a fork pull request; no code change can turn them green.

* fix(pr): keep PR bookkeeping when a merge outcome read fails

On the GitHub path a merge call that returned success was followed by
`github_read_outcome || exit 1`, so a transient API failure, rate limit,
or network blip during the read dropped out of the script before
`record_pr_metadata` ever ran. The merge could have landed while `pr=`
went unrecorded and the merge poll was never armed - bookkeeping lost on
a real merge. The failure path just above already recorded metadata
before exiting, so the error path was more careful than the success one.

Record the PR before that refusal. Recording arms the later merge poll
and is not a success claim, which is the same reasoning that keeps
`record_pr_metadata` on the gh-axi failure path. The refusal itself is
unchanged: exit stays non-zero and the message still names the concrete
observed state. Metadata is withheld only when the read succeeds and
proves the pull request neither merged nor queued.

Pin it with a case that stubs `gh api graphql` into failure after a
successful `gh-axi pr merge`, asserting both the non-zero exit and the
recorded metadata.

* no-mistakes(review): Aggregate queue rules and report conflicts explicitly

* fix(pr): keep the merge abstraction reachable and its bookkeeping intact

Two holes remained in the outcome-verified GitHub merge path, both on
installations where gh-axi is present but gh is not.

The verification preflight refused before bin/fm-pr-merge.sh ever reached
the configured gh-axi merge abstraction, so an installation without gh
could no longer merge at all. gh-axi now performs the merge unconditionally
and the queue-aware gh read became an optional enrichment: with gh on PATH
its GraphQL view still separates merged from queued, and without gh the
gh-axi view still proves a landed merge while every outcome it cannot prove
refuses.

The PR metadata recording sat behind the outcome read, so a merge that
landed before that read failed lost pr= and its merge poll. Recording now
happens once, before either forge call, which arms the poll without
claiming a landed outcome and leaves teardown a PR identity to verify
against no matter how the read ends.

Rebasing onto main also restored the durable merge-outcome reporting and
the GitLab landed-state confirmation that the conflict resolution dropped.

Tests pin each fix through the executable interface: the merge abstraction
is reached and verified with gh absent, a failed fallback read keeps its
bookkeeping, and a mock that snapshots the task meta during the forge call
proves pr= is recorded before the merge can land.

* no-mistakes(review): fix(pr): de-dup queue methods, fall back on failed gh read, refresh contracts

* no-mistakes(review): fix(pr): quote forge output and explain armed auto-merge on refusal

* no-mistakes(review): fix(pr): claim auto-merge armed only when the forge accepted it

* no-mistakes(review): fix(pr): tell the operator what each GitHub refusal could not observe

* no-mistakes(review): fix(pr): gate every forge-acceptance claim on a successful merge

* no-mistakes(document): align merge docs with verified GitHub outcome contract

* fix(pi): prevent duplicate captain outcome reports (#3184)

* fix(pi): stop reporting one merge to the captain twice

The supervision branch's captain-outcome note told main, unconditionally,
that the note "is not your own earlier output" and to relay it now. When
main had already reported the same event, that assertion was false and the
order turned the correct response - saying nothing new - into a mechanical
re-report, so the captain saw one merge reported twice in 16 seconds.

Two independent changes, both needed:

- The relay instruction is now conditional. It still names itself as a
  supervision outcome so main cannot mistake it for its own earlier answer
  (the silent loss that instruction exists to prevent), and it now lets
  main stay quiet about an outcome it has already given the captain.

- The merge case is closed at its source rather than left to that judgment.
  One merge reaches a home on two independent paths by design - main's own
  permanently main-owned merge poll, and the branch's task-local status
  wake - and main's captain-facing text only reaches the branch's mirror at
  main's turn end, so the branch can escalate before it could possibly see
  the captain was already told. bin/fm-pr-merge-notified.sh answers that
  question from bin/fm-pr-lib.sh's canonical merge-notification marker, so
  the answer holds regardless of mirror timing. A captain outcome naming an
  already-published merge is delivered as the ordinary rendered note
  instead of opening a follow-up turn: still appended, still visible, still
  recorded with the verdict the branch decided, minus the wasted turn.

Any error, timeout, or unreadable state relays the outcome. A duplicate
announces itself; a lost outcome does not.

Regression coverage drives the real delivery path in both directions: a new
outcome must still reach the captain in exactly one follow-up turn even
beside an unrelated published merge, and an already-published merge must
open no second turn while a different PR in the same task still does. The
merge path's real producer and this new consumer are exercised end to end
in tests/fm-pr-merge.test.sh.

Pi-only by construction: the delivery path lives in .pi/extensions, so no
other harness loads it, and the new script only reads existing markers.

* no-mistakes(review): Document accepted latest-marker suppression residual

* no-mistakes(review): Recheck ownership before merge outcome delivery

* no-mistakes(document): Document merge-outcome suppression exception

* refactor(pi): drop the source-level merge suppression, keep the envelope fix

The captain reviewed this branch and judged the source-level duplicate
suppression overly complicated for the problem it solved, and asked for
the change to be reduced to the envelope wording alone.

Remove the mergeIntoMain downgrade path, bin/fm-pr-merge-notified.sh, and
every test and document that existed only for it. What remains is the
conditional captain-outcome instruction: main is told to stay quiet about
an outcome it has already reported and to relay anything else, which
covers the duplicate without a second mechanism.

The silent-loss protection is untouched - the note is still typed,
self-describing, and delivered as one invisible follow-up turn - and the
behavioral tests still assert that, now requiring both halves of the
conditional instruction.

* no-mistakes(ci): Clarified in code comments and owned documentation that this is intentionally an M1-only, model-facing conditional relay fix—not source-level suppression—addressing Greptile’s mistaken scope expectation without changing runtime behavior. Net diff remains 3 files and 27 insertions. Verified with fm-pi-branch-extension tests, fm-lint, doc audience check, and git diff --check; all passed

* no-mistakes(ci): Strengthened the runtime delivery test to verify the captain outcome retains its required self-description and outcome text. Verified with `bash tests/fm-pi-branch-extension.test.sh`, `bin/fm-lint.sh`, `bin/fm-doc-audience-check.sh`, and `git diff --check`; all passed. The outer pipeline can now commit and attest the new head

* fix(bin): prioritize active pipeline-owned crew runs (#3194)

* fix(bin): bind the live pipeline-owned run instead of a superseded failed row

fm-crew-state.sh bound a superseded FAILED no-mistakes run to a task instead
of the LIVE replacement run: the live run's pipeline-owned lane head is not a
git object in the task worktree, so head-equality attribution rejected it and
the coarse runs-list fallback silently continued past the RUNNING row onto an
older failed row whose head equalled the stale worktree HEAD. The home summary
then flipped invalid and Bearings hid the home's live work (F10).

Attribution precedence now follows the daemon's own identity:
- An ACTIVE run for the task's branch binds without head equality while
  branch_sync.state is pipeline_owned (fm_nm_run_is_pipeline_owned_active);
  the pipeline owning the branch is itself the attribution.
- A genuinely failed run with no later run on the branch still reports failed
  through the unchanged head-equality path - real failures are not hidden.
- In the coarse runs scan, an unresolvable head is unknown attribution and
  stops the scan (fm_nm_head_resolvable) instead of falling through to an
  older row; a resolvable-but-mismatched head keeps the historical
  reused-branch skip.

The exemption never applies to a terminal run and requires pipeline_owned
specifically, both pinned by negative-control tests. Fixture shape verified
against the live incident run's real axi status output.

* no-mistakes(document): Updated run-attribution documentation ownership

* fix(pi): surface requested outcomes without replaying fleet events (#3211)

* fix(pi): surface requested supervision outcomes

* no-mistakes(review): Mirror in-flight captain requests before branch dispatch

* no-mistakes(review): Exercise real branch ownership and main outcome access

* no-mistakes(review): Preserve request tails and align verdict guidance

* no-mistakes(review): Preserve complete current captain requests

* no-mistakes(review): Require visible requested outcomes and realistic classification

* no-mistakes(document): Align supervision outcome documentation

* no-mistakes(ci): Fixed Greptile’s runtime-ordering finding. The extension now stages Pi’s authoritative `before_agent_start` prompt before SessionManager persistence and suppresses the later duplicate entry. Updated docs and behavioral regression to reproduce real Pi ordering and verify each prompt is mirrored exactly once. Passed branch-extension tests, supervision tests, strict Pi typecheck, full lint, and diff checks

* no-mistakes(review): Use canonical operational input classification

* no-mistakes(review): Filter legacy operational inputs canonically

* no-mistakes(document): Clarify captain request mirroring boundary

* no-mistakes(ci): Fixed the CI time-boundary failure in tests/fm-public-followup.test.sh by pinning its clock, including context-registry setup. This prevents follow-up fixtures from expiring based on wall time. Verified the full regression suite passes, project-owned lint passes, and git diff checks are clean

* no-mistakes(document): Clarify captain-visible supervision outcome documentation

* feat(bin): add concurrent bounded remote transport lanes (#3210)

* feat(bin): per-home remote transport lanes with cancellation, bounded send, and closed stdin

All remote commands for every home on one host used to serialize through one
single-job-at-a-time worker on one shared queue: a timed-out caller abandoned a
staged job that kept running, retries convoyed behind it, fm-send's remote leg
had no time bound, and staging captured the caller's stdin to EOF so any
fm-on.sh caller with an open stdin wedged staging indefinitely.

- The worker now serves one lane per staged home: same-home jobs run strictly
  FIFO in a new staging-sequence order while different homes run concurrently,
  each lane as its own top-level worker process (a backgrounded subshell does
  not reliably reap dead children, so a zombie group leader kept a finished
  command's process group signalable). Long-poll preemption is lane-scoped.
- A caller that disconnects or times out cancels its job: the entrypoint marks
  the record on any post-staging exit and probes its parent so a dead ssh
  channel cancels without a signal; the worker skips cancelled queued jobs,
  terminates a running cancelled job's process group, and reaps the record.
- fm-send's remote leg is bounded by FM_SEND_REMOTE_BUDGET (default 30s) and a
  bound hit exits through the existing unconfirmed-delivery contract, which
  stays idempotent because the remote enqueue deduplicates.
- fm-on.sh defaults the remote command's stdin to /dev/null; the three payload
  callers pass the new --stdin flag. Abandoned .stage.* litter is age-reaped.
- The job execution deadline no longer loses up to a second to clock
  truncation.

* no-mistakes(review): Protect live stages and validate send budgets early

* no-mistakes(review): Preserve sequence lock ownership during stale recovery

* no-mistakes(review): Allocate job sequences at publication boundary

* no-mistakes(review): Bound remote keys and extend stale lock recovery

* no-mistakes(document): Document bounded remote transport behavior

* no-mistakes(lint): Suppress intentional deferred-expansion lint warning

* no-mistakes(ci): Fixed stale sequence-lock recovery by reconciling the counter against published job records before allocating the next sequence, preventing duplicate sequences and same-home FIFO violations. Added a behavioral regression test reproducing displacement after publication and verifying execution order. Passed fm-remote-transport-lanes.test.sh, fm-remote-job.test.sh, fm-lint.sh, and git diff --check

* no-mistakes(review): Use atomic sequence claims and lossless lane keys

* no-mistakes(review): Recover regressed sequence hints and rate-limit claim reaping

* no-mistakes(review): Restrict worker heartbeats to serving loop

* no-mistakes(review): Verify supervisor identity before lane recovery signals

* no-mistakes(review): Verify tracked lane and claim owner identities

* no-mistakes(document): Clarify remote lane and transport contracts

* no-mistakes(ci): Fixed the CI time-boundary failure by pinning fm-public-followup tests to a deterministic clock, including context-registry setup. Verified tests/fm-public-followup.test.sh, tests/fm-remote-transport-lanes.test.sh, shellcheck, and git diff --check

* no-mistakes(review): Preserve assigned lane ownership of queued jobs

* no-mistakes(review): Reserve homes owned by foreign queued lanes

* no-mistakes(review): Preserve completed results during crash recovery

* no-mistakes(review): Harden claim cleanup, expiry, and cancellation races

* no-mistakes(review): Verify process groups and reap abandoned results

* no-mistakes(review): Stop leaderless groups and reap cancelled publications

* no-mistakes(document): Correct remote transport lifecycle documentation

* no-mistakes(lint): Quote done state comparisons for ShellCheck

* fix(bin): accelerate and bound changed test runs (#3250)

* fix(tests): make the changed-file map select per script and stabilize a budget flake

The changed-file map's bin/ fallback resolved a direct test reference to that
test's whole FAMILY. bin/fm-push-transition-lib.sh is named by exactly one
real-Herdr E2E, so a one-line change to it selected all 12 real-herdr-gated
scripts, including a 341s presentation E2E with no dependency on it.

Resolve direct test references per script, and keep resolving consumer bin/
scripts through the curated map so recorded family-level coupling survives.

Also fix a load-sensitive flake: the tool-update budget deadline is whole-second
granular, so a test budget of 1 left headroom anywhere in (0, 1] seconds and the
first budget check could already read as exhausted.

* feat(bin): make suite wall clock a result and let a family's concurrency be proven

--max-wall-ms fails a run whose wall clock exceeds the caller's budget, after
reporting the per-script results. A suite that stays green while outgrowing its
caller's invocation budget is the regression that got an agent killed mid-run
and retried invisibly, so duration has to be a result rather than a log note.

--pool on the isolation-proof harness runs the same concurrent proof over a
whole family, so 'is this family safe to parallelize?' is answered by a command
instead of a guess. Measured watcher-wake-lock and refused it: 3 of 18 scripts
fail under concurrency on wall-clock assertions about reaching the next poll.

* perf(bin): schedule the changed suite concurrently, longest first

The watcher-wake-lock family is proven concurrent-…
digbycampbell added a commit to digbycampbell/firstmate that referenced this pull request Sep 1, 2026
* feat(bin): add a spoken interface that answers from records and hands work over (#2767)

* feat(voice): spoken round trip on Nova Sonic 2 with a measured relay cost

Step one of the spoken interface: the laptop captures and plays audio, this
desktop holds the model session, and no AWS credential leaves the desktop.

Measured, amazon.nova-2-sonic-v1:0 in eu-north-1, end of speech to first byte
of reply audio, 6 runs each, all answered, on a question that forces a records
read:

  relay path   1.229 1.379 1.428 1.447 1.481 1.516  median 1.438
  direct       1.147 1.179 1.203 1.237 1.244 1.317  median 1.220

The relay costs about 0.22s of the median. The direct figure reproduces the
earlier survey, which is what makes it a usable control. Excluded: the
captain's own ssh round trip, microphone capture, and speaker output. This
desktop has no microphone and no speaker, so every run used audio files.

Three pieces:

  bin/fm-voice-relay.py    holds the conversation on this host
  bin/fm_voice_records.py  what a spoken answer may read, and the handover
  bin/fm-voice-client.py   the laptop end; audio devices UNVERIFIED
  bin/fm_voice_frame.py    the wire format both machines share

Real work is handed to the existing bin/fm-inbox.sh rather than a second
queueing surface, and the agent says it is handing over rather than answering
as firstmate.

Read scope: Done history and free-form note bodies are never assembled at any
scope, so the wide default cannot reach the places commercial detail
accumulates. config/voice-read-scope narrows it to counts only, and
config/voice-read-deny excludes a named item in one line. The boundary is an
executable test that widening the reader fails.

Push to talk is the default because it is cheaper and the choice is still open;
--listen open-mic is the single flip.

Two traps worth knowing: a clip with no trailing silence is never answered, and
the end of a reply is contentEnd with stopReason END_TURN, not completionEnd.
A second user turn in one session is treated as barge-in unconditionally, and
an interrupted turn that calls a tool is lost, so the session reconnects per
turn and gives up conversational memory. That is the concrete thing step three
has to solve.

* no-mistakes(review): fix voice relay credential reuse, frame validation and record parsing

* no-mistakes(review): test uplink header guard, bound unknown expiry, align state dir

* no-mistakes(review): decide deny per item, guard turn failures, bound ambient credentials

* no-mistakes(review): read account config from home, harden deny and turn failures

* no-mistakes(review): close status verb set, fix inbox help, pair data override

* no-mistakes(review): keep profile-free relay alive, unblock loop, fix dead assertion

* no-mistakes(review): hide finished pull requests, refuse open mic, keep suite offline

* no-mistakes(review): survive reader failures, release devices, fix claims

A failure while handling a model event, or while sending a tool result,
left the reader task dead with ended and turn_done clear, and close()
re-raised the stored failure on every await. One dropped stream became a
relay that could never build another session. The reader now reports the
session over in a finally whatever killed it, and close() absorbs the
task the same way it already absorbed its sends.

The laptop client releases what it already started when a later startup
step refuses, SystemExit from the handshake wait included, and names a
device refusal instead of leaking a raw PortAudio error. Whether it
releases correctly against a real device is still unverified here.

The records docstring claimed every reading was filtered to open ids.
Only the pull request count and list are; the worker count and the state
histogram cover every live runtime record, finished ids included,
because a meta file still on disk still needs tearing down.

The finished-work deny half of the suite asserted things that held with
the deny list absent. It is replaced by a deny on an open title, which
removes the row and says so while the count stays honest.

* no-mistakes(review): name reader failures, split file and device refusals

A failure inside the model reader released the waiting turn and told
nobody. The session was not marked spent, no notice reached the client,
and the client waits for a reply end or a notice, so the captain got
their whole timeout of silence and then a record saying the turn went
unanswered with nothing about why. Both ends of the relay now name a
failed turn through one function, once per turn, and --self-test carries
the cause in relay_error the way the client's own record does.

Two things that are not failures stay that way. A stream that simply
ends is the end of a session, which serve still reads on its own terms.
A stream that goes away because close() asked it to is an ordinary
renew, and announcing it would have put a failure notice in front of the
captain on every turn.

On the laptop end, the refusal that became a device error covered the
file-backed playback and capture too, so a mistyped --in-file was
reported as an audio device failure and the advice named the flag that
had just failed. The file ends now report the path and the flag that
chose it and stay an OSError; the device ends keep the device advice and
name the flag for that end. The device paths remain unrun here, so only
the file halves are covered by a test.

* no-mistakes(test): survive model session end, order client turn frames

* no-mistakes(document): sync voice relay docs with reviewed relay behavior

* no-mistakes(document): re-measure relay latency and correct its cause

* no-mistakes(document): correct measurement date and name the unmeasured SSH hop

* no-mistakes(document): describe the unpublished control measurement, fix list formatting

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* fix(bin): preserve Relay follow-up loops until explicit disposition (#2763)

* fix: keep Relay public loops open until retire

Delivering a promised-final reply was deleting the only record that tied a public thread to later work, so a follow-on ship silently owed no closing reply. Retain the registration after delivery, rechain follow-on work onto the same thread, and make retire --reason the only close.

* no-mistakes(review): Propagate public follow-up registration removal failures

* no-mistakes(review): Persist retire receipts and align parent resolution

* no-mistakes(review): Make rechain resumable after partial obligation creation

* no-mistakes(review): Repair follow-up state, briefs, and expiry escalation

* no-mistakes(review): Serialize follow-up delivery stamps with retirement

* no-mistakes(review): Serialize rechain claims and protect registration terminal states

* no-mistakes(review): Avoid reporting retired delivery loops as open

* no-mistakes(document): Refresh public-loop documentation and verification evidence

* no-mistakes: apply CI fixes

* no-mistakes(review): Preserve delivered follow-up bindings during registration replay

* no-mistakes(review): Harden public follow-up retirement and rechain races

* no-mistakes(review): Fail closed on unresolved secondmate retirement

* no-mistakes(review): Bind secondmate cleanup to its recorded canonical home

* no-mistakes(review): Fix rechain command output and expiry validation

* no-mistakes(review): Validate brief keys and warn on remote promotion

* no-mistakes(document): Document retained public follow-up loops

* no-mistakes(lint): Remove unused bounded-wait loop variable

* feat(bin): merge GitLab merge requests through the guarded PR merge path (#2779)

* feat(bin): merge GitLab merge requests through the guarded PR merge path

bin/fm-pr-lib.sh already parses a GitLab merge request URL for the watcher,
but bin/fm-pr-merge.sh refused every non-github provider, so a merge request
had to be merged by hand and got none of the recording, guards, or audit
trail a pull request gets.

The merge path now dispatches on the parsed provider. A GitHub URL keeps its
exact previous behavior. A GitLab URL is addressed through glab by the project
URL rebuilt from the parsed host and path, so a merge request on any instance
resolves and no host is hardcoded, and no merge-method flag is added because
the project's own merge method is what should apply.

A GitLab merge happens only after one live read of the merge request confirms
it is open, detailed_merge_status is mergeable, has_conflicts is false,
blocking_discussions_resolved is true, and the head pipeline succeeded at the
exact current head. Every failing condition is reported, not just the first.
The verified head is bound to the merge with glab's --sha, so a push landing
between the read and the merge fails the merge instead of landing commits
nothing verified. Recorded metadata is never the authority for any of this: a
rebase moves the head and leaves a recorded value stale, so a recorded head
that disagrees with the live one is reported rather than trusted, and the
recorded value is read before the recording step because that step drops a
GitLab head it cannot resolve.

* no-mistakes(review): reject bundled -R clusters and make tool-absence cases host-independent

* no-mistakes(test): state authorised GitHub narrowing of bundled -R guard

This branch NARROWS GitHub behaviour. The narrowing was authorised
deliberately rather than slipping in by accident, and it applies to both
providers, GitHub and GitLab alike, because a script that guards one provider
and not the other is a trap for the next reader.

What bin/fm-pr-merge.sh now refuses is extra merge arguments containing a
bundled short-option cluster that includes R, for example "-dR other/repo".
The forge CLIs expand such a cluster one character at a time, so it carries
"--repo other/repo", and that later value wins over the repository the URL
named. Before this change, "fm-pr-merge.sh <task> <github-url> -- -dR
other/repo" reached "gh-axi pr merge 12 --repo example/repo --squash -dR
other/repo" and exited 0 with pr= recorded and the merge poll armed. It now
exits 1 with "extra merge arguments must not override the repository", records
nothing, and invokes no forge merge command. Every other GitHub invocation is
byte-identical to the base commit.

Closing that hole honours the existing rule rather than departing from it. The
file header already forbids --repo and -R because the repository must come
only from the URL, so a bundled cluster carrying a repository override was
never legitimate behaviour to preserve: it was that guard being evaded.
Redirecting a merge to a repository the URL does not name is exactly what the
guard exists to prevent.

The refusal is already pinned on both paths by the existing case
test_bundled_repo_override_args_refuse_before_recording in
tests/fm-pr-merge.test.sh. On GitHub ("-dR wrong/repo") and on GitLab ("-yR
https://other.example/g/p") it asserts exit 1, the refusal wording, no pr= in
the task meta, no armed merge poll, and no forge merge command invoked, with a
control case proving a cluster that carries no repository override still
reaches the forge. No duplicate assertion was added. Both assertions were
confirmed to have teeth by narrowing the guard back to a bare -R and watching
each path fail.

This commit carries no file change: the guard and its coverage landed in
614853d, and this message exists so the pull request description states the
narrowing.

* no-mistakes(document): fix README pointer for GitLab watch and merge doc

* no-mistakes: apply CI fixes

* fix(bin): record a lost relay connection instead of an unanswered turn (#2788)

* no-mistakes: apply CI fixes

* fix(bin): drop a private record citation and narrow the review rule

Three corrections to the spoken interface that landed in #2767, plus one
fix carried over from that branch after its pull request had already been
merged.

The confidentiality fix. The module docstring of bin/fm-voice-relay.py
cited a private, gitignored fleet record by exact path and section number.
That widens what this public repository points at, and it cannot resolve
for any reader here, because the path has never been in the repository.
Both traps it pointed at are already described in full in the list
immediately below it, and docs/voice-relay.md carries the same two for
operators with no citation at all, so the pointer is removed and no claim
is weakened by losing it. Two comments that referred to "the survey" as
though it were something a reader could open are reworded the same way.
Neither exposed a path, so that half is comprehensibility rather than
confidentiality.

The review rule. .greptile/rules.md is kept, because its conditions are
right and deleting it would leave the next reviewer to re-litigate a
decision already argued out. What was wrong with it is narrower than its
existence: it read as settled repository policy, when whether VISION.md
itself should be reconciled is an open question belonging to the captain.
One sentence now says so, and says that the conditions listed below it are
what the interpretation depends on. That narrows the claim rather than
widening it.

The carried-over fix. The first commit on this branch is 7f98e797 from
fm/voice-relay-build-v4, taken verbatim rather than rewritten. It closes
the window where a transport failure was recorded and then erased, so a
run could be emitted as answered false with relay_error null. That matters
more than it looks: relay_error is the field that keeps an infrastructure
failure from being averaged into a latency figure, so the failure mode is
a dead connection wearing the costume of a slow reply. It landed fifteen
minutes after #2767 merged and so never reached the default branch.

* no-mistakes(review): name a reason on every unanswered-turn close path

* no-mistakes(review): guard the downlink body and pin frames to their turn

* no-mistakes(review): attribute reply audio to its own turn and tell endings apart

* no-mistakes(review): tell a cut-short reply from an unanswered turn

* no-mistakes(review): discard reply audio arriving after the output closes

* no-mistakes(review): count discarded reply audio on the speaker path too

* no-mistakes(review): keep a reason off a turn already answered in full

* no-mistakes(review): say a reset cut a reply short, not that none arrived

* no-mistakes(review): read one turn's audio count once, and hush a tidy exit

* no-mistakes(document): fix stale session-end relay_error claim in voice-relay guide

* fix(composer): stop a blocked pi pane from proving an empty composer (#2811)

A pi worker parked on an interactive prompt - a permission dialog, a
question menu, a trust dialog - reports agent_status=blocked, because it
is waiting on a human keystroke. Pi draws that menu above its separator
pair, so the composer region between the rules is blank and structure
alone looks like a free composer. _fm_composer_pi_verdict admitted
blocked alongside idle and done, so the shared classifier reported an
affirmatively empty composer for exactly the pane where typing is unsafe.

Every "is it safe to type here?" consumer reads that verdict and proceeds
only on an affirmative empty, so both are told yes on a parked prompt:
the away-mode injection guard in bin/fm-supervise-daemon.sh, and fm-send's
pre-type refusal. The keys then answer the menu instead of composing a
message - the highlighted default is selected, the text is discarded, and
the record attributes a decision to a human who never made it.

blocked now defers to unknown, which every consumer already treats as
fail-closed. idle and done still prove an empty composer, so ordinary
steering is unchanged, and Cursor is unaffected because its always-blocked
panes never reach this pi-only branch.

Regression coverage lands first at both levels: the verdict owner
(a blocked pi defers) and the herdr adapter (a parked pi prompt is not an
empty composer).

* fix(bin): require project clone roots during fleet sync (#2849)

* fix(bin): require a clone root before fleet-sync touches a project

Git repository discovery walks upward, so `git -C projects/<dir>` on a plain
directory nested under projects/ resolves to the enclosing repository - in a
firstmate home, the firstmate checkout itself. fm-fleet-sync.sh guarded its
candidates with `rev-parse --is-inside-work-tree`, which such a directory
passes, so every later git call read, pruned and fast-forwarded firstmate's own
default branch and reported it under the project directory's label. A running
session's AGENTS.md changed underneath it, and the report named a project that
had nothing to do with the change.

Require each candidate to be the root of its own work tree before any other git
command: compare `rev-parse --show-toplevel` against the directory's own
physical path. Both sides are physical, so a symlinked clone still compares
equal. Anything else is skipped by name, naming the repository that would have
been touched, and bootstrap relays that as a FLEET_SYNC line.

Regression coverage reproduces the wrong-repo fast-forward against a home nested
inside another repository, in both the whole-fleet and single-project forms, and
pins that a symlinked clone dir still syncs.

* no-mistakes(review): Keep enclosing fixture clean during clone-root regression

* fix(bin): retry transient Lavish poll interruptions (#2846)

* fix(procevent): retry a transient Lavish poll interruption quietly

A live Lavish listener can be cut short by the server with exactly

    error: Lavish Editor poll response was interrupted
    code: SERVER_ERROR

while the session's marks remain available. Firstmate registered raw
`lavish-axi poll` output, so the generic process-event runner captured
that transient response as a result and woke the whole fleet over what is
really an internal retry.

The Lavish adapter now registers its own listener command, which reruns
the published blocking poll up to 12 times at 5 second intervals for that
one exact two-line response. The match is deliberately narrow: real
feedback, ended and missing sessions, any other SERVER_ERROR, and the same
interruption still standing once the bound is spent all pass straight
through and are captured and announced as before. The retry is a Lavish
fact, so the generic runner stays adapter-agnostic.

`FM_LAVISH_POLL_RETRY_DELAY` is a bounded 0 to 60 second override for the
interval only, refused rather than rounded when malformed, so a test can
exercise the real bound without waiting it out.

* no-mistakes(review): Harden Lavish retry matching, validation, and cleanup

* no-mistakes(review): Bound Lavish retry staging and stabilize regression

* no-mistakes(document): docs: explain Lavish retry adoption

* no-mistakes(lint): Restore Lavish trap ShellCheck suppression

* fix(brief): stop the documented {TASK} fill from corrupting the Herdr gate (#2838)

The unguarded Herdr declaration quoted `{TASK}` in its own prose while the
scaffold instructs firstmate to replace every `{TASK}` placeholder. The
documented global replace therefore spliced the whole task body into the
middle of the safety gate's sentence, silently destroying the one contract
that exists precisely because the scaffold cannot inspect the task text.

Reword the gate to refer to the task text filled in above, leaving the
placeholder only at its genuine fill site. Rewording rather than renaming the
token keeps the unfilled-charter guards in fm-home-seed.sh and
fm-remote-home-seed.sh working unchanged.

Add a regression test that performs the documented global fill on ship and
scout scaffolds and asserts the body lands once and the gate survives.

* fix(bin): resolve the busy-state lock mtime with the platform's own stat form (#2837)

The writer lock's stale-lock branch read the lock's mtime with
`stat -f %m ... || stat -c %Y ...`. On GNU coreutils `-f` is filesystem
stat, so it consumed the format string as a path, complained on stderr,
printed a partial filesystem dump ("  File: ...") on stdout, and still
exited 0. The GNU form in the fallback therefore never ran, and the
following arithmetic evaluated the word `File`, aborting the writer under
`set -u` with "File: unbound variable".

fm-teardown.sh died there after returning the worktree, leaving
state/<id>.meta, .status, .busy-gen, .busy-state, .busy-state.lock/ and
.turn-ended behind. The surviving metadata kept the watcher monitoring an
endpoint whose agent was gone, so a finished task produced stale wakes
forever, and every re-run died identically because the abandoned lock was
never broken.

Detect the platform once and pick the right stat form, the pattern
bin/fm-watch.sh already documents, and treat any non-numeric result as
"just created" so a future portability surprise degrades to a lock-timeout
refusal rather than killing teardown mid-way.

* fix(stow): add opt-in pass horizon for memory decay (#2850)

* fix(stow): give memory decay a per-pass horizon so the clock fires

The tiered decay clocks were wall-clock only, while admission is per-pass:
each /stow admits the findings that pass produced. In a home that stows
daily those two rates diverge by the stow cadence, an entry the fleet keeps
exercising never reaches 30 days unreinforced, and memory only grows while
the pass reports decay evaluated.

Give each dated marker an optional unreinforced-pass counter and make both
tiers stale at whichever horizon comes first: 10 passes or 30 days for
aging, 3 passes or 7 days for perishable. Reinforcement clears the counter
and nothing else does, so the existing evidence-based restamp rule stays
the only way an entry renews its lease. An absent /N means zero, so entries
that stay exercised carry no extra marker bytes, and a rarely stowed home
keeps its current behaviour through the unchanged date horizon.

* no-mistakes(document): Align stow workflow with dual decay clocks

* fix(stow): make the per-pass decay horizon opt-in

The unreinforced-pass horizon shipped as a new default archival cadence,
which is a product default rather than a restoration of the existing
wall-clock contract. Keep the 30-day and 7-day horizons as the only
default clock, and put the 10-pass and 3-pass horizons behind an explicit
opt-in: config/stow-pass-horizon for the firstmate home, and the file's
own header pointer for the public skill.

With the opt-in absent no counter is written and no counter is read, so a
home that does not ask for it decays exactly as it does today.

* no-mistakes(review): Preserve frozen counters and correct archive provenance

* test(watcher): stop fixture confirmation budgets racing real child startup (#2876)

tests/fm-watcher-lock.test.sh passed in isolation but failed intermittently
under full-suite and ambient concurrent load. bin/fm-watch-arm.sh computes its
confirmation deadline immediately after forking the real child watcher, so the
child's entire fork, exec, lock acquisition and beacon publication has to land
inside that wall clock. Two cases shrank that budget to one second, leaving a
two-second window for work measured at 3.1-4.9s under CPU oversubscription, so
the arm honestly reported "FAILED - no live watcher with a fresh beacon" and
their premises collapsed. A third case ran on the production budget, but its
child must also execute a registered check before exiting: measured at 1.9-2.3s
idle and 9.1-13.1s under load, against an 11s budget.

The two cases that must confirm a real child now hold the arm to production's
own budget instead of a shrunken fixture one, the immediate-wake case gets an
explicit budget with headroom over its measured loaded cost, and the two waits
for the arm's typed failure are sized off the largest production default rather
than a fixed eight seconds.

No bin/ change and no default behavior change: the lock's fail-closed semantics,
SIGSTOP handling, stale-heartbeat detection and the arm's typed failures are
untouched. Verified 4/4 green at 3x CPU oversubscription (loadavg 75-80) after
3/3 red before the change, and CONTRIBUTING.md records the convention.

* fix(bin): deterministically order remote tool paths (#2870)

* fix(bin): order discovered tool installs by the shell's own expansion

fm_remote_job_compose_operator_path built the asdf and mise install
directories with `compgen -G`, which does not sort. Bash sorts glob
matches in pathexp.c, on the shell's own pathname-expansion path only;
`compgen -G` reaches the same glob_filename through pcomplete.c, which
sorts nothing. On bash 3.2 (macOS /bin/bash) and every bash before 5.3
that handed the composition raw readdir order, so which install of a
multi-version tool a remote job resolved was decided by directory order
on disk rather than by this composition.

Expand the globs at the call sites and let the function take the matches,
so the composition and the documented portable-PATH contract are the same
operation. Quoting the account home at the call site also stops a home
whose name contains glob metacharacters from being reinterpreted.

The colocated regression pins both the order and the mechanism: bash 5.3
moved sorting into the glob library, so an order-only assertion cannot
see the defect there.

* no-mistakes(review): Remove source-reading PATH regression guard

* fix(bin): prevent routed secondmate work from stranding (#2848)

* fix: surface stalled secondmate queues and wake handoffs

* no-mistakes(review): Make handoff wakes retryable and stall alerts crash-safe

* no-mistakes(review): Prevent duplicate handoff wakes and cover remote delivery

* no-mistakes(review): Serialize local handoffs and preserve pre-move wake intent

* no-mistakes(review): Serialize teardown with handoffs and retain remote wake confirmation

* no-mistakes(review): Reconcile correlated handoff wake delivery after crashes

* no-mistakes(review): Keep failed wakes retryable and isolate stall receipts

* no-mistakes(review): Reset known-undelivered wake attempts for durable retries

* no-mistakes(review): Refuse duplicate sends for unresolved delivery attempts

* no-mistakes(review): Atomically restore retryability after reconciled send failures

* no-mistakes(review): Serialize delivery confirmation with reconciliation

* no-mistakes(document): Document routed wake and stall supervision

* no-mistakes(lint): Fix ShellCheck expansion and subshell warnings

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes(review): Retire stale wake state and defer pre-move wakes

* no-mistakes(review): Secure markers, bind batches, and preserve teardown routes

* no-mistakes(review): Preserve unresolved prepared wakes across unrelated handoffs

* no-mistakes(review): Preserve prepared wakes before unrelated moving handoffs

* no-mistakes(document): Document prepared wake batch ownership

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes(review): Make local wake retirement recoverable

* no-mistakes(document): Clarify handoff recovery and teardown documentation

* fix: make macOS inbox test path portable (#2857)

* feat(bin): deliver local steers through durable task inboxes (#2856)

* feat(bin): steer local tasks by durable inbox record plus constant doorbell

Stage 1 (local steers) of the captain-adopted reframe in
data/fm-send-reliability-reframe-s1/report.md: an ordinary fm-send text
steer to a task recorded in this home is appended as a sequenced durable
record under state/<id>.inbox/ and the terminal receives only one constant
self-describing doorbell line, best-effort. The worker acknowledges by
moving the record into handled/; the watcher re-rings an unacknowledged
message on an idle pane and escalates once as an ordinary stale wake.
--resolve-key closes decisions at enqueue time, because the durable
enqueue IS delivery to the task's record. bin/fm-task-inbox-lib.sh owns
the record format, doorbell line, and re-ring ladder.

The typed plane remains for what must reach the terminal itself:
lifecycle keys, harness-native slash and codex $-skill invocations,
explicit backend targets, and the remote secondmate leg (unchanged until
the remote inbox leg ships separately). The composer classifier is
demoted from delivery proof to an advisory ring guard that skips only on
a proven pending verdict.

Verified live against claude, codex, opencode, pi, grok, and muse: each
real worker read its record, acted, and acked with the mv
(docs/verification/runtime-backends.md "Steering-inbox doorbell").

* docs(verification): flag the grok 1.0.5 composer-matrix staleness observed by the doorbell run

* test(captain-hold): read the chat-channel answer from the durable inbox record

* test: migrate fm-control's marker contrast to the inbox record and fix macOS wc padding in the tool-update suite

* no-mistakes(review): Harden inbox locking, teardown races, and acknowledgements

* no-mistakes(review): Serialize watcher actions with inbox acknowledgements

* no-mistakes(review): Bound metadata locking and tighten acknowledgement rechecks

* no-mistakes(review): Preserve exact inbox bytes and harden delivery recovery

* no-mistakes(review): Harden watcher bookkeeping against concurrent inbox teardown

* no-mistakes(document): Update inbox and typed-plane documentation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* revert(pipeline): keep parser-native secondmate marking and the both-failed exit out of stage 1

The CI monitor's fix changed the secondmate marking contract for
parser-native invocations (appending the marker after the text) and
softened the both-commit-and-marker-failed branch to exit 0. The merge
authority ruled the marking question out of scope for this stage-1
transport PR (follow-up: fm-send-secondmate-harness-invocation-r1) and
ruled the both-failed case a loud nonzero local failure. Restore both,
keeping the monitor's legitimate migrations and hardening.

* no-mistakes(document): Document inbox and typed-plane boundaries

* no-mistakes(document): Scope backend transport docs to typed plane

* no-mistakes(document): Clarify inbox attempt-budget documentation

* no-mistakes: apply CI fixes

* fix(send): the durable record alone governs the inbox exit status

Captain-refined ruling on the F2/Greptile finding: the durable inbox
record is what delivers the steer, so pending-reply bookkeeping trouble
after a successful enqueue never exits nonzero - a resend-inviting status
would make automated callers enqueue the delivered instruction again
under a new sequence. With the recovery marker stored the watcher
reconciles silently; with the commit and marker both lost the send
surfaces a distinct reply-tracking-degraded do-not-resend warning and
still exits 0. Nonzero remains only where nothing was delivered (or a
decision close needs its manual command). Regression: record durable +
both bookkeeping writes lost -> exit 0, one record, no duplicate.

* no-mistakes(review): Preserve inbox ordering with drain-all doorbells

* no-mistakes(review): Surface unwritable inbox ladder bookkeeping

* no-mistakes(review): Silence ladder failures after inbox acknowledgement

* no-mistakes(document): Update steering inbox documentation

* no-mistakes: apply CI fixes

* feat(bin): add fast local lint mode (#2891)

* feat: add fast local lint mode

* fix: preserve complete fm-lint help

* fix: isolate fast lint mode

* no-mistakes(document): Clarify lint mode documentation ownership

* no-mistakes: apply CI fixes

* feat(bin): deliver remote steers through durable inboxes (#2901)

* feat(bin): deliver remote secondmate steers through durable task inboxes

Stage 2 of the inbox+doorbell steer channel (stage 1: #2856). A remote
secondmate steer now crosses fm-on.sh as a durable record written
idempotently into the remote home's steering inbox plus a best-effort
remote doorbell, and the last typed-payload steer transport is deleted:

- fm-remote-secondmate-control.sh cmd_send writes the record via the new
  fm_task_inbox_write_idempotent and rings the doorbell; it no longer
  types the payload through an inner fm-send at an explicit pane target.
- fm-send.sh routes every remote text steer (harness-native included,
  which marking already reduced to chat) onto the remote inbox leg,
  retries the identical leg once on ssh 255, closes --resolve-key
  decisions at enqueue for remote too, and preserves a marked request's
  reply expectation when completion stays unknown. The exit-3-as-
  delivered remap, the 255 do-not-resend trap, and the remote typed
  submit block are removed.
- fm-task-inbox-lib.sh owns the idempotent enqueue: an exact-body re-run
  lands on the existing record, handled or not, so an ambiguous
  transport can always be safely re-run.
- Tests pin the new contract end to end (record + doorbell + no typed
  payload across ssh, one-record idempotence under an ambiguous
  transport, enqueue-time decision close, loud real failures, and the
  deleted typed-payload behaviors gone), and AGENTS.md plus
  docs/remote-secondmates.md describe the remote leg's new semantics.

* no-mistakes(review): Harden remote inbox delivery against lifecycle races

* no-mistakes(review): Enable correlation-preserving remote steer resends

* no-mistakes(review): Fail closed on stale correlation resends

* no-mistakes(review): Include home context in remote resend commands

* no-mistakes(review): Lock and revalidate remote parent routes

* no-mistakes(document): Clarify remote steer retry documentation

* no-mistakes: apply CI fixes

* feat: add persistent Pi supervision branch (#2858)

* wip: forked supervision on Pi (checkpoint before docs)

* fix(pi-branch): harden mirror delivery, fallback encoding, and session replacement

Peek-then-shift mirror flush so a failed append retries instead of dropping;
durable mirror cursor commits only after delivery into the branch;
the main fallback wake is operational-encoded like every watcher injection;
session_shutdown quiesces the generation and session_start re-arms, so /new
and /resume no longer kill the branch permanently. Registers the extension in
the strict typecheck, adds the dispatch handshake test, the branch extension
suite, the bash-level regression suite, the session-start replay test, and
the opt-in real-SDK live guard.

* test(fixtures): carry the branch-dispatch lib and lease lib into isolated fixtures

The watcher extension now imports lib/fm-branch-dispatch.ts and fm-teardown
sources fm-lease-lib.sh, so every fixture that copies or symlinks those
files in isolation gains the new sibling.

* no-mistakes(review): Prevent shutdown wake loss and serialize lease claims

* no-mistakes(review): Durably hand off wakes and retain portable leases

* no-mistakes(review): Require durable reports and clear disposed branch leases

* no-mistakes(review): Enforce per-wake outcomes and quiescent lease cleanup

* no-mistakes(review): Require wake acknowledgements and tighten branch lifecycle boundaries

* no-mistakes(review): Require complete acknowledgements and replay cleanup failures

* no-mistakes(review): Bind supervision to lock ownership and durable delivery

* no-mistakes(review): Activate branch lazily after session lock acquisition

* no-mistakes(review): Preserve undelivered mirror context across extension rebinds

* no-mistakes(review): Acknowledge startup replay only after main delivery

* no-mistakes(review): Isolate replay metadata from untrusted digest content

* no-mistakes(review): Reject duplicate reports for active wake sequences

* no-mistakes(review): Retain failed fallbacks and deduplicate outcome replay

* no-mistakes(review): Deduplicate durable outcomes and cache delivery receipts

* no-mistakes(review): Anchor wake sequence matching to outcome fields

* no-mistakes(document): Clarify Pi supervision durability contracts

* no-mistakes(lint): Fix ShellCheck issues in branch supervision scripts

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* refactor(pi-branch): collapse to confused-agent-grade guards per captain decision

Captain decision A: the lease/actor guards target the CONFUSED-AGENT threat
model bin/fm-gate-refuse-lib.sh already documents; adversarial-grade
separation is impossible in the shared-process design and is filed as
separate follow-up work. Rip out the machinery that chased it: the
generation fence and shell-provenance markers, the wrapper-tagged ancestry
walks, guard auto-claim with per-script release traps, the pending-wake
files and ack-receipt correlation (the durable wake queue already
re-presents anything unacknowledged), the delivery-receipt store with
contiguous cursor advancement, the session-start replay-metadata channel,
and the branch tool quiescence counters.

Keep the behaviors the board requires, each on its simplest implementation:
lazy per-action session-lock ownership (cold start activates after the lock
lands; a secondary session stays inert), mirror durability across extension
rebinds via the durable cursor, replay-exactly-once from the one read
cursor, the awaited operational-encoded fallback, per-generation stray-lease
cleanup, session-lock-bound lease liveness (a recycled pid or a non-Pi home
never honors a leftover lease), the loud accidental-override guards
(readonly actor prelude, cross-actor claim refusal), and the role-partition
refinements (no forced teardown, no direct relaunch for the branch).
Default-on-for-Pi is unchanged.

* no-mistakes(review): Enforce lock ownership and serialize lease mutations

* no-mistakes(review): Synchronize guard cleanup and bind leases to lock owner

* no-mistakes(review): Report outcomes before acknowledging durable wakes

* no-mistakes(review): Restrict leases to Pi and instruct main claims

* no-mistakes(review): Reject malformed lease locks and torn outcome tails

* no-mistakes(review): Validate complete outcome tails before appending

* no-mistakes(review): Guard branch side effects across session replacements

* no-mistakes(document): Update Pi supervision durability and lease documentation

* no-mistakes(lint): Suppress intentional nested-shell expansion warning

* no-mistakes: apply CI fixes

* fix(pi-branch): authorize lease releases by caller

* fix(lint): break redundant source-analysis path in fm-lease-lib.sh

fm-lease-lib.sh's lazy fallback source of fm-wake-lib.sh gave ShellCheck's
--external-sources traversal a second path into an already 1540-line file
that fm-send.sh and fm-teardown.sh also source directly, blowing up the
recursive analysis past CI's lint timeout. Mark it a source=/dev/null
analysis boundary, matching the existing fm-task-inbox-lib.sh convention.

Also restores bin/fm-lint.sh and tests/fm-lint.test.sh to the shared
serial-lint definition (dropping an unrelated parallel-sharding change
that was itself hanging and masked this root cause).

* no-mistakes(document): Correct lease caller-authorization documentation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* fix(bin): parallelize startup network sweeps (#2927)

* feat(bin): parallelize session-start remote secondmate network sweeps

Run per-secondmate liveness and convergence probes concurrently and overlap clone refresh, while replaying each mate's fail-closed diagnostic in original order. Ignore scratchpad* so untracked scratch no longer blocks remote sync.

Co-authored-by: Cursor <cursoragent@cursor.com>

* no-mistakes(document): Document parallel startup network sweeps

* no-mistakes(lint): Fix empty environment assignment lint warning

* no-mistakes: apply CI fixes

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* test: handle absent watcher wake queues (#2845)

* fix(tests): count declared-pause wakes without crashing on an absent queue

The exited-declared-pause case counts queued stale wakes by handing
state/.wake-queue straight to awk. A watcher that queues nothing never
creates that file, and awk aborts on a missing path before its END rule
runs, so the count collapses to the empty string. The next comparison
then fails as an integer-expression error and surfaces as a wake flood
with no number, hiding the real contract breach the following grep names.

Read the queue the way the drain-count assertion at the end of this file
already does: silence awk's open error and default an absent queue to
zero. Applied to all four counts in this case, including the live
external-decision gate pair whose queue an acknowledged drain can also
leave behind. An absent queue now reports "did not use the bounded
paused recheck", while a genuine flood still fails with its real count.

Fixes #2628

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* style(pi): distinguish routine and captain supervision merge notes by icon (#2934)

* style(pi): restyle supervision merge notes with a sailboat and matching pad

Secondary-session notes were flush against the TUI edge and fully tinted.
Use the sailboat prefix, Pi's default outputPad, boat-only color, and dim remainder so they sit like real messages.

* style(pi): distinguish routine and captain merge notes by icon only

Visible notes now lead with a sailboat or anchor, then only the dim outcome.
Drop the branch-merged wording and verdict brackets so the icon is the only kind signal.

* docs(pi): add the approved multi-brain architecture poster (#2938)

The markdown contract stays the owner; the still is only the visual of the idea.

* feat(pi): default branch supervision and route heartbeats (#2939)

* fix(bin): bound remote job worker supervisor restarts (#2942)

* fix(bin): bound remote worker supervisors

* no-mistakes(review): release incumbent supervisor before starting its replacement

* no-mistakes(review): wait out a healthy same-root supervisor instead of replacing it

* no-mistakes(review): narrow remote worker change to restart accounting only

* no-mistakes(document): clarify supervisor restart guard is a lifetime total

* fix: safely split supervision wake handling by actor (#2953)

* feat(bin,pi): per-actor wake consume, silent success gating, merge-poll dedup

Three related fixes to the shared wake-drain and Pi supervision-branch
dispatch machinery so a routine success is never main-blocking and a
mixed queue can safely split between actors.

1. Successful routine results no longer create main-blocking wake rows.
   fm-startup-network.sh only enqueues a check: startup-network wake when
   the deferred result is actionable (state is not "done", or the report
   carries a bootstrap-diagnostics actionable prefix); a clean success
   stays durable in the report file without ever waking the agent.

2. Per-actor wake-drain consume contract. bin/fm-wake-drain.sh now scopes
   presentation and --ack-through to the current actor
   (bin/fm-lease-lib.sh's fm_lease_actor): main keeps the original
   whole-queue cutoff behavior, unaffected. A branch actor
   (FM_SUPERVISION_ACTOR=branch, set only inside the Pi supervision
   branch's own bash tool calls) is scoped to an explicit eligible-row
   snapshot instead of a cutoff comparison, so it can never remove a row
   it was not granted - the fix for the swallow risk that used to force
   an all-or-nothing whole-queue fallback to main.
   .pi/extensions/lib/fm-branch-dispatch.ts's scopeForUnreadWake is the
   single owner of eligibility: a check-kind row (merge-confirmation
   polls, Relay mentions, credential/auth failures) is now excluded
   rather than vetoing the whole scan for a non-heartbeat wake, while a
   heartbeat review keeps its original all-or-nothing rule unchanged.
   writeEligibleRowsSnapshot publishes the exact eligible sequence
   numbers before every branch prompt; fm-primary-pi-watch.ts's offer
   still refuses a check-kind trigger outright so a main-only close is
   never itself routed to the branch.

3. A repeat identical merged-PR-poll result for an already-notified task
   is absorbed instead of enqueued again. A poll's own retirement state
   is scoped to one registration and cannot see a prior registration's
   outcome, so a task re-registered after its merge was already surfaced
   would otherwise wake main a second time for the same event.
   bin/fm-pr-lib.sh's new per-task pr-poll-merge-notified marker survives
   across re-registrations to catch that case; the first notification for
   a task still reaches main unchanged.

Regression tests colocated in tests/fm-startup-network.test.sh,
tests/fm-wake-queue.test.sh (including the mixed-queue no-swallow
property), tests/fm-pi-branch-extension.test.sh, and
tests/fm-pr-check-security.test.sh. docs/watcher-continuity.md and
docs/pi-supervision-branch.md updated for the new contracts.

* no-mistakes(review): Bind merge deduplication to canonical PR identity

* no-mistakes(review): Serialize wake row ownership across main and branch

* no-mistakes(review): Bind branch grants and deduplicate within actor claims

* no-mistakes(review): Fallback main-owned wake claims to main delivery

* no-mistakes(review): Clarify silent startup success guidance

* no-mistakes(review): Release residual branch grants after settled prompts

* no-mistakes(review): Reject truncated wake rows as corrupted

* no-mistakes(document): Document per-actor routing and silent startup success

* no-mistakes(lint): Fix ShellCheck findings in wake grant and startup test

* no-mistakes: apply CI fixes

* fix(pi): hide branch outcomes tool rows in Calm (#3024)

* Hide branch outcome tool in Pi Calm

* no-mistakes(review): Preserve stock outcomes rendering and document tool audit

* no-mistakes(review): Document branch read tool audit disposition

* no-mistakes(review): Match stock outcomes output sanitization

* no-mistakes(document): Document Calm custom-tool visibility

* fix: bind no-mistakes attestations to PR head (#3027)

* fix: delegate no-mistakes PR gate to pinned action

* no-mistakes(document): Document commit-bound no-mistakes attestations

* feat(pi): add persistent supervision branch model selection (#3028)

* feat(pi): let operators pin a cheaper supervision-branch model

Supervision is an easier job than the captain's own conversation, so the
Pi supervision branch does not need main's model. A new /supervision-model
command opens Pi's own selector over Pi's own catalog of credentialed
models, plus a "Follow main" entry, and persists the pick as one
<provider>/<model-id> line in this home's gitignored
config/supervision-branch-model. Firstmate keeps no model catalog of its
own.

The branch resolves the pin at every branch build - the first wake of a
cold start and the reopen after /new, /resume, /fork, or reload - so the
choice survives all of them, and picking also releases the live branch so
the next wake reopens the same persistent branch conversation under the
new model. An absent, unreadable, or unparseable file means no pin and
keeps today's behavior byte for byte: no model option is passed and Pi
picks the branch's model exactly as before.

A pin naming a model Pi cannot hand back is never silently downgraded
onto main's model: the branch refuses to build and the wake falls back to
the captain-facing main path naming the unusable pin, which is the
extension's existing failure direction.

The choice is home-local and not part of secondmate inherited
configuration, matching the Pi Calm preference precedent.

docs/configuration.md owns the operator-facing schema. Portable
regressions cover pin-present on create and reopen, pin-absent default,
the command's persistence, cancellation, and live rebind, and both
unusable and unparseable pins. The opt-in real-SDK guard proves the
vendor surface the pin reads and that an explicit model wins over the
model a reopened session recorded.

* no-mistakes(review): Fix supervision model runtime and rebind races

* no-mistakes(review): Restrict supervision picker to isolated runtime models

* no-mistakes(document): Document supervision branch model selection

* fix(pi): make the supervision model pin authoritative on every reopen

Clearing the pin with "Follow main" removed the file but the next branch
build reopened the persistent branch session with no explicit model
override, so Pi restored the model that session had recorded - the old
pinned model - while the command reported that the branch now follows
main. The same gap meant an absent pin did not reliably mean
same-model-as-main once a home had pinned once.

The pin file's current state now decides the model on every branch build,
create and reopen alike, overriding Pi's session-state restore. With a
pin, that model. With no pin, main's own current model is applied
explicitly, tracked from the contexts Pi already hands the extension plus
its model_select event, since the branch is built at wake time with no
context of its own. Only when main's model is unknown, or this home's
stored credentials cannot run it in the isolated branch runtime, does a
build fall back to passing no override at all, which is the behavior from
before the pin existed; the branch is never refused over model choice.

The command's notification now reports the model actually applied, and
says plainly when clearing the pin could not apply main's model instead
of claiming a change that did not take effect.

No credential handling changes: the branch still relies entirely on the
stored credentials its own runtime already holds, and the picker stays
restricted to models that runtime can resolve.

Colocated regressions cover pin present on create and reopen, clearing
the pin returning a reopened branch to main's model and specifically not
the old pinned one, an unparseable pin behaving as no pin, and the
unknown-main-model fallback to no override.

* no-mistakes(review): Make unpinned supervision follow main model changes

* no-mistakes(document): Correct supervision model documentation

* feat(pi): let /supervision-model pick branch reasoning effort (#3079)

* feat(pi): let /supervision-model pick the branch's reasoning effort

Supervision is an easier job than the captain's own conversation, so the
Pi supervision branch does not need main's reasoning effort any more than
it needs main's model. /supervision-model now settles both in one flow:
the existing model picker, then a follow-up effort picker built from Pi's
own supported thinking levels for the model just chosen. Firstmate keeps
no effort catalog of its own; the menu, the clamp, and the vocabulary all
come from Pi.

The pick persists as one line in this home's gitignored
config/supervision-branch-effort, independent of the model pin: a captain
may pin a model, an effort, both, or neither. The effort pin's current
state decides the branch effort on every branch build - the first wake of
a cold start and the reopen after /new, /resume, /fork, or reload - and
overrides Pi's restore of whatever level a reopened branch session
recorded, which is what keeps "Follow main" honest. With no pin, main's
own current effort is applied explicitly and followed live through Pi's
thinking_level_select event, the same way an unpinned branch already
follows main's model, and the two selections now share one build revision
so either change invalidates an in-flight build.

The branch is never refused over effort. Pi owns the clamp, so a pinned
level the branch's model cannot run becomes that model's nearest supported
level while the captain's raw pick is kept for a model that supports it,
and the command reports the level the branch will really run at rather
than the raw pin. A token Pi would not recognize at all is treated as no
pin rather than passed to that clamp, which would otherwise collapse a
typo into the model's lowest level. Only when main's effort cannot be read
either does a build pass no effort override at all, which is the behavior
from before this file existed.

Pi's own effort vocabulary is pinned by a bidirectional type assertion
against Pi's getThinkingLevel return type, so the tracked strict typecheck
against the installed package fails the moment Pi adds or removes a level.

docs/configuration.md owns the operator-facing schema for both pins.
Portable regressions cover the pin on create and reopen, model-only and
effort-only pins working independently, clearing a pin returning the
branch to main's effort, live-follow of a mid-session change, the clamp,
an unrecognized token, the unknown-main-effort fallback, and the command's
two-step flow, persistence, cancellation, and honest reporting. The opt-in
real-SDK guard proves the vendor surface all of that rests on, and also
repairs a pre-existing gap that left it unable to load the extension at
all.

* no-mistakes(review): Resolve effective branch effort honestly

* no-mistakes(document): Clarify Pi-owned effort picker behavior

* fix: keep routine supervision noise out of captain chat (#3093)

* fix(supervision): silence empty board closes and decouple the heartbeat

Two unrelated sources of noise put routine supervision events in the
captain's chat.

An empty Lavish board close - the captain reads a review surface, says
nothing, and closes it - became a check wake whose entire content was
that nothing happened. Suppress it at its source instead of routing it
anywhere: the generic runner gains a `silent` adapter seam mirroring the
existing `terminal` one, and the Lavish adapter answers it for exactly
one positively-determined shape, an `ended` session carrying no queued
content block. A silenced result is recorded durably handled so it does
not return on a later reconcile. Everything else announces unchanged - a
`Send & End` close carrying the captain's real answer, an `ended` result
still carrying content, a waiting or missing session, an unreadable
result, and every adapter that implements no `silent` command at all.
The keyed-answer feed is untouched, so suppressing an announcement never
suppresses the captain's own answer.

A fleet heartbeat was deferred to main merely because some unrelated
check row happened to be sitting unread, which put a routine fleet
review in the chat for a reason that had nothing to do with the fleet. A
check row is permanently main-owned, so it is now excluded from a
heartbeat claim rather than vetoing the scan, exactly as in every other
mode. What all-or-nothing guarantees is unchanged: the branch takes
every branch-ownable unread row or none of them, and an unresolvable
task-local row, an unknown row kind, or an unreadable queue still defers
the whole review to main. Main is still woken for the check on that
check's own triggering close, so nothing starves.

Main-only classes are unchanged and now each covered by a test: Relay
mentions, credential failures, merge confirmations, real board answers,
and watcher-failure repair. The per-actor acknowledgement and
no-cross-swallow properties are untouched.

* no-mistakes(review): Fail closed on all Lavish content headers

* no-mistakes(review): Suppress false unacknowledged status for silenced results

* fix(bin): stop a correlation token from hiding and stranding decisions (#1967)

* fix(classify): read the decision fold through a correlation token

status_line_verb stripped a trailing [key=...] from a status line's prefix
but left everything else glued to the verb, so a line carrying the
correlation token bin/fm-pending-reply-lib.sh embeds and a secondmate
echoes back matched no arm of _fm_decision_fold_line. Such a line folded as
ordinary status in both directions: a needs-decision or blocked opener never
opened its key, and a resolved or captain-held closer never closed one. The
same glued verb also hid correlated done and blocked lines from
status_is_captain_relevant and status_is_terminal_verb, and let correlated
working and resolved lines leak through the free-text fallback the
nonterminal guard was meant to stop.

The verb parse now walks whole words and drops only a token of the exact
shape a firstmate library writes - corr=<16 hex>, plus the bracketed form
bin/fm-secondmate-report.sh emits - before or after the key token, unkeyed,
or doubled. An arbitrary name=value word is deliberately NOT skipped:
skipping unknown tokens would let free text carrying an equals sign reduce
to a bare verb and impersonate a transition, which is the takeover the
strict parse and _fm_decision_key_transition_allowed exist to prevent. A
prefix with no corr= substring is returned byte-for-byte as before, so every
line without a token keeps its exact historical verb.

FM_OPEN_DECISIONS_FOLD_VERSION goes to 3, because every cursor persisted
under the previous reading carries an open set computed while correlated
lines were invisible and must be rebuilt from byte 0.

Measured over a real 383-line status log: 254 lines keep byte-identical
captain-relevance, pause, terminal-verb and captain-held verdicts, and all
129 changed lines carry a valid token - 14 correlated done/blocked/
needs-decision lines become captain-visible, and 20 correlated
working/resolved lines stop being escalated on prose alone.

* fix(review): Captain, block token-first decision impersonation

* fix(document): Clarify normalized status verb ownership

* fix(classify): reconcile the correlation-token read with the tag-stop parser

Rebasing onto main put this change beside #2280, which made verb parsing
stop at the first "[name=value]" tag. Both edit status_line_verb with
different intents, so the resolution keeps both rules rather than letting
one overwrite the other:

- #2280's tag stop is kept verbatim and now owns every BRACKETED tag,
  including the "[corr=...]" form fm-secondmate-report.sh writes. The
  bracket-unwrapping arm this branch had added to the token test is
  therefore removed as unreachable.
- This branch's token walk is kept and narrowed to the UNBRACKETED token
  fm-pending-reply-lib.sh writes, which the tag stop does not reach.

Two consequences of standing beside #2280 rather than before it:

The fold version had collided at 4: #2280 spent it on the tag-stop
parser and this branch had spent it on the token read. A cursor
persisted under #2280's reading predates this one and must still be
rebuilt, so the version moves to 5.

A bracketed impostor is dropped from the malformed-token list. On main
today "resolved [corr=deadbeef] [key=victim]:" already reads as the bare
verb, as does "resolved [anything at all] [key=victim]:", because the tag
stop ends the parse at the opening bracket regardless of content. That is
#2280's reviewed contract; asserting otherwise here would narrow it. The
unbracketed impostors it owns stay strict and still fold as prose.

Adds a consumer test for the two verb-string case arms that postdate this
branch: fm-supervise-daemon.sh's transient-stale arm and
fm-crew-state.sh's map_log_state.

* fix(review): Captain: Seed cursor migration fixture with version four

* fix(document): Clarify voice status normalization ownership

* fix(bin): Cursor-Park unter Pi-Host ohne Cursor-Identität stilllegen (#3115)

* fix(bin): Cursor-Park unter Pi-Host stilllegen.

pi-cursor-sdk lädt .cursor/hooks.json in die Pi-Sitzung und parkte einen zweiten Watcher; das erzeugte rearm-resurface und brach laufende Rückfragen ab. Bei PI_CODING_AGENT=true beendet der Park sofort, native cursor-agent Primaries bleiben unverändert.

* fix(bin): Cursor-Park trotz PI-Leak nur ohne Cursor-Identität stilllegen.

Stand-down gilt nur bei PI_CODING_AGENT=true ohne CURSOR_AGENT und ohne CURSOR_INVOKED_AS. Handgestartete cursor-agent Primaries mit geerbtem PI-Marker parken weiter.

* no-mistakes(document): Document Cursor park Pi-host stand-down

* fix(bin): no-mistakes-Mindestversion auf 1.46.0 anheben.

Die PR-Attestierung verlangt ab 1.46.0 strukturierte Pipeline-Schritte; der Bootstrap-Floor blieb bei 1.31.2 und ließ zu alte Builds zu.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(pi): make supervision model picker searchable and scrollable (#3099)

* fix(pi): make /supervision-model's model list bounded and searchable

Pi's generic extension selector renders every option at once with no
search box, so a real eligible catalog ran off the top of the terminal.
The model step now draws the same rows through Pi's own SelectList - the
bounded scrolling primitive behind Pi's /model picker - with Pi's own
Input and fuzzy filter above it for search, keeping 'Follow main' first,
the branch-runtime eligibility filter intact, and the pick branch-only.

Pi's ModelSelectorComponent is deliberately not reused: its selection
handler writes the captain's default model through Pi's settings manager,
which would move main's conversation as a side effect of pinning the
branch. The effort step's menu is a handful of levels and stays on Pi's
plain selector dialog.

* no-mistakes(document): Clarify supervision picker documentation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes(document): Document searchable supervision model picker

* no-mistakes: apply CI fixes

* fix(bin): durably report merged pull requests (#3104)

* fix: make a landed merge leave a durable outcome

A merge was the one lifecycle event that left no record outside the
merging agent's memory. bin/fm-pr-merge.sh ended at the forge call, and a
home merging under standing authority never waits for the merge poll that
would otherwise confirm it, so three real merges reached the captain as
silence.

bin/fm-merge-outcome-lib.sh is the single owner of that record. A
secondmate home reports the landed PR upward on the same parent reply
channel its terminal-outcome backstop already uses; a main home records
it on the durable wake queue. The record is at most once per task and
canonical PR identity, and only a merge that actually landed produces one.

The merge poll feeds that same channel when it detects a merge this home
did not perform, so the captain's own forge merge and a merge firstmate
performed itself produce one consistent outcome instead of two reporting
paths. No new state file and no second watch path.

Two smaller gaps from the same failure:

- A mate charter listed its report triggers without naming a landed
  merge. Under standing merge authority nothing is ever "ready for
  review", so the enumerated list silently omitted the case that matters.
- A secondmate home seeded without its parent binding failed every
  terminal-outcome report for the same reason, and the diagnostic never
  named the binding. It does now.

* no-mistakes(review): Harden durable merge outcome reporting

* no-mistakes(review): Make merge race regression deterministic

* no-mistakes(review): Make merge outcomes retry-idempotent and forge-confirmed

* no-mistakes(review): Unify merge publication under canonical outcome marker

* no-mistakes(review): Publish merge outcomes before committing dedup markers

* no-mistakes(review): Document at-least-once merge outcome recovery

* no-mistakes(review): Use supported GitHub confirmation and update recovery docs

* no-mistakes(review): Preserve distinct merge wakes by PR identity

* no-mistakes(document): Document durable merge outcome semantics

* no-mistakes(test): Make merge outcome interleaving test deterministic

* no-mistakes(document): Clarify merge outcome documentation ownership

* fix(lint): keep the merge-outcome library an analysis boundary

bin/fm-watch.sh followed the new merge-outcome library's source graph,
which reaches the wake queue, PR identity, and secondmate parent
libraries. Expanding that inside an already-large lint root pushed
ShellCheck's external-source analysis past the bounded CI lint worker:
the Lint job was killed with SIGTERM after five silent minutes, twice,
having emitted no diagnostics at all.

Make it an analysis boundary, exactly as the transition and inbox owners
directly above and below it already are and for the same stated reason.
Coverage is unchanged because the library is a canonical lint root in its
own right and is still linted as one.

Measured locally: the watcher goes from not terminating within 120s to
9s clean, and the library alone lints in 1s clean.

* fix(bearings): preserve projections through inventory mismatches (#3129)

* fix(bearings): keep an inventory-mismatch home readable, and mark warnings as repairs

A backlog-vs-metadata inventory mismatch inside a secondmate home was being
reported as "we cannot read that home", which discarded that home's open
captain calls, queued work, landed work, and live workers from the whole
Bearings digest. The main home already treats the identical mismatch as a
harmless disclosure; this makes the secondmate path agree.

- fm-fleet-snapshot.sh: the invalidity gate now passes orphan_in_flight,
  unowned_current, and terminal_in_flight through the partial-structured
  carve-out alongside child_current_unavailable, so those homes keep their
  decisions, holds, queued, landed, and live work and leave unreadable[].
  missing_backlog and unstructured_current stay on the discard path, because
  there the backlog itself is untrustworthy.
- fm-fleet-snapshot.sh: the same three kinds no longer collapse the home's own
  classification to "unknown"; the real captain_decision / active_child_work /
  externally_held classification survives and invalidity carries the warning.
  An unavailable child state still collapses it, including when a mismatch
  masks it under strict-invalidity precedence.
- secondmate_landed.partial now keys on partial-structured trust rather than an
  unknown state, so an inventory-mismatch home is still…
ameen-saeed added a commit to ameen-saeed/firstmate that referenced this pull request Sep 1, 2026
…t and SSF-style agent-cookbook (#1)

* feat(bin): deliver remote steers through durable inboxes (#2901)

* feat(bin): deliver remote secondmate steers through durable task inboxes

Stage 2 of the inbox+doorbell steer channel (stage 1: #2856). A remote
secondmate steer now crosses fm-on.sh as a durable record written
idempotently into the remote home's steering inbox plus a best-effort
remote doorbell, and the last typed-payload steer transport is deleted:

- fm-remote-secondmate-control.sh cmd_send writes the record via the new
  fm_task_inbox_write_idempotent and rings the doorbell; it no longer
  types the payload through an inner fm-send at an explicit pane target.
- fm-send.sh routes every remote text steer (harness-native included,
  which marking already reduced to chat) onto the remote inbox leg,
  retries the identical leg once on ssh 255, closes --resolve-key
  decisions at enqueue for remote too, and preserves a marked request's
  reply expectation when completion stays unknown. The exit-3-as-
  delivered remap, the 255 do-not-resend trap, and the remote typed
  submit block are removed.
- fm-task-inbox-lib.sh owns the idempotent enqueue: an exact-body re-run
  lands on the existing record, handled or not, so an ambiguous
  transport can always be safely re-run.
- Tests pin the new contract end to end (record + doorbell + no typed
  payload across ssh, one-record idempotence under an ambiguous
  transport, enqueue-time decision close, loud real failures, and the
  deleted typed-payload behaviors gone), and AGENTS.md plus
  docs/remote-secondmates.md describe the remote leg's new semantics.

* no-mistakes(review): Harden remote inbox delivery against lifecycle races

* no-mistakes(review): Enable correlation-preserving remote steer resends

* no-mistakes(review): Fail closed on stale correlation resends

* no-mistakes(review): Include home context in remote resend commands

* no-mistakes(review): Lock and revalidate remote parent routes

* no-mistakes(document): Clarify remote steer retry documentation

* no-mistakes: apply CI fixes

* feat: add persistent Pi supervision branch (#2858)

* wip: forked supervision on Pi (checkpoint before docs)

* fix(pi-branch): harden mirror delivery, fallback encoding, and session replacement

Peek-then-shift mirror flush so a failed append retries instead of dropping;
durable mirror cursor commits only after delivery into the branch;
the main fallback wake is operational-encoded like every watcher injection;
session_shutdown quiesces the generation and session_start re-arms, so /new
and /resume no longer kill the branch permanently. Registers the extension in
the strict typecheck, adds the dispatch handshake test, the branch extension
suite, the bash-level regression suite, the session-start replay test, and
the opt-in real-SDK live guard.

* test(fixtures): carry the branch-dispatch lib and lease lib into isolated fixtures

The watcher extension now imports lib/fm-branch-dispatch.ts and fm-teardown
sources fm-lease-lib.sh, so every fixture that copies or symlinks those
files in isolation gains the new sibling.

* no-mistakes(review): Prevent shutdown wake loss and serialize lease claims

* no-mistakes(review): Durably hand off wakes and retain portable leases

* no-mistakes(review): Require durable reports and clear disposed branch leases

* no-mistakes(review): Enforce per-wake outcomes and quiescent lease cleanup

* no-mistakes(review): Require wake acknowledgements and tighten branch lifecycle boundaries

* no-mistakes(review): Require complete acknowledgements and replay cleanup failures

* no-mistakes(review): Bind supervision to lock ownership and durable delivery

* no-mistakes(review): Activate branch lazily after session lock acquisition

* no-mistakes(review): Preserve undelivered mirror context across extension rebinds

* no-mistakes(review): Acknowledge startup replay only after main delivery

* no-mistakes(review): Isolate replay metadata from untrusted digest content

* no-mistakes(review): Reject duplicate reports for active wake sequences

* no-mistakes(review): Retain failed fallbacks and deduplicate outcome replay

* no-mistakes(review): Deduplicate durable outcomes and cache delivery receipts

* no-mistakes(review): Anchor wake sequence matching to outcome fields

* no-mistakes(document): Clarify Pi supervision durability contracts

* no-mistakes(lint): Fix ShellCheck issues in branch supervision scripts

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* refactor(pi-branch): collapse to confused-agent-grade guards per captain decision

Captain decision A: the lease/actor guards target the CONFUSED-AGENT threat
model bin/fm-gate-refuse-lib.sh already documents; adversarial-grade
separation is impossible in the shared-process design and is filed as
separate follow-up work. Rip out the machinery that chased it: the
generation fence and shell-provenance markers, the wrapper-tagged ancestry
walks, guard auto-claim with per-script release traps, the pending-wake
files and ack-receipt correlation (the durable wake queue already
re-presents anything unacknowledged), the delivery-receipt store with
contiguous cursor advancement, the session-start replay-metadata channel,
and the branch tool quiescence counters.

Keep the behaviors the board requires, each on its simplest implementation:
lazy per-action session-lock ownership (cold start activates after the lock
lands; a secondary session stays inert), mirror durability across extension
rebinds via the durable cursor, replay-exactly-once from the one read
cursor, the awaited operational-encoded fallback, per-generation stray-lease
cleanup, session-lock-bound lease liveness (a recycled pid or a non-Pi home
never honors a leftover lease), the loud accidental-override guards
(readonly actor prelude, cross-actor claim refusal), and the role-partition
refinements (no forced teardown, no direct relaunch for the branch).
Default-on-for-Pi is unchanged.

* no-mistakes(review): Enforce lock ownership and serialize lease mutations

* no-mistakes(review): Synchronize guard cleanup and bind leases to lock owner

* no-mistakes(review): Report outcomes before acknowledging durable wakes

* no-mistakes(review): Restrict leases to Pi and instruct main claims

* no-mistakes(review): Reject malformed lease locks and torn outcome tails

* no-mistakes(review): Validate complete outcome tails before appending

* no-mistakes(review): Guard branch side effects across session replacements

* no-mistakes(document): Update Pi supervision durability and lease documentation

* no-mistakes(lint): Suppress intentional nested-shell expansion warning

* no-mistakes: apply CI fixes

* fix(pi-branch): authorize lease releases by caller

* fix(lint): break redundant source-analysis path in fm-lease-lib.sh

fm-lease-lib.sh's lazy fallback source of fm-wake-lib.sh gave ShellCheck's
--external-sources traversal a second path into an already 1540-line file
that fm-send.sh and fm-teardown.sh also source directly, blowing up the
recursive analysis past CI's lint timeout. Mark it a source=/dev/null
analysis boundary, matching the existing fm-task-inbox-lib.sh convention.

Also restores bin/fm-lint.sh and tests/fm-lint.test.sh to the shared
serial-lint definition (dropping an unrelated parallel-sharding change
that was itself hanging and masked this root cause).

* no-mistakes(document): Correct lease caller-authorization documentation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* fix(bin): parallelize startup network sweeps (#2927)

* feat(bin): parallelize session-start remote secondmate network sweeps

Run per-secondmate liveness and convergence probes concurrently and overlap clone refresh, while replaying each mate's fail-closed diagnostic in original order. Ignore scratchpad* so untracked scratch no longer blocks remote sync.

Co-authored-by: Cursor <cursoragent@cursor.com>

* no-mistakes(document): Document parallel startup network sweeps

* no-mistakes(lint): Fix empty environment assignment lint warning

* no-mistakes: apply CI fixes

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* test: handle absent watcher wake queues (#2845)

* fix(tests): count declared-pause wakes without crashing on an absent queue

The exited-declared-pause case counts queued stale wakes by handing
state/.wake-queue straight to awk. A watcher that queues nothing never
creates that file, and awk aborts on a missing path before its END rule
runs, so the count collapses to the empty string. The next comparison
then fails as an integer-expression error and surfaces as a wake flood
with no number, hiding the real contract breach the following grep names.

Read the queue the way the drain-count assertion at the end of this file
already does: silence awk's open error and default an absent queue to
zero. Applied to all four counts in this case, including the live
external-decision gate pair whose queue an acknowledged drain can also
leave behind. An absent queue now reports "did not use the bounded
paused recheck", while a genuine flood still fails with its real count.

Fixes #2628

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* style(pi): distinguish routine and captain supervision merge notes by icon (#2934)

* style(pi): restyle supervision merge notes with a sailboat and matching pad

Secondary-session notes were flush against the TUI edge and fully tinted.
Use the sailboat prefix, Pi's default outputPad, boat-only color, and dim remainder so they sit like real messages.

* style(pi): distinguish routine and captain merge notes by icon only

Visible notes now lead with a sailboat or anchor, then only the dim outcome.
Drop the branch-merged wording and verdict brackets so the icon is the only kind signal.

* docs(pi): add the approved multi-brain architecture poster (#2938)

The markdown contract stays the owner; the still is only the visual of the idea.

* feat(pi): default branch supervision and route heartbeats (#2939)

* fix(bin): bound remote job worker supervisor restarts (#2942)

* fix(bin): bound remote worker supervisors

* no-mistakes(review): release incumbent supervisor before starting its replacement

* no-mistakes(review): wait out a healthy same-root supervisor instead of replacing it

* no-mistakes(review): narrow remote worker change to restart accounting only

* no-mistakes(document): clarify supervisor restart guard is a lifetime total

* fix: safely split supervision wake handling by actor (#2953)

* feat(bin,pi): per-actor wake consume, silent success gating, merge-poll dedup

Three related fixes to the shared wake-drain and Pi supervision-branch
dispatch machinery so a routine success is never main-blocking and a
mixed queue can safely split between actors.

1. Successful routine results no longer create main-blocking wake rows.
   fm-startup-network.sh only enqueues a check: startup-network wake when
   the deferred result is actionable (state is not "done", or the report
   carries a bootstrap-diagnostics actionable prefix); a clean success
   stays durable in the report file without ever waking the agent.

2. Per-actor wake-drain consume contract. bin/fm-wake-drain.sh now scopes
   presentation and --ack-through to the current actor
   (bin/fm-lease-lib.sh's fm_lease_actor): main keeps the original
   whole-queue cutoff behavior, unaffected. A branch actor
   (FM_SUPERVISION_ACTOR=branch, set only inside the Pi supervision
   branch's own bash tool calls) is scoped to an explicit eligible-row
   snapshot instead of a cutoff comparison, so it can never remove a row
   it was not granted - the fix for the swallow risk that used to force
   an all-or-nothing whole-queue fallback to main.
   .pi/extensions/lib/fm-branch-dispatch.ts's scopeForUnreadWake is the
   single owner of eligibility: a check-kind row (merge-confirmation
   polls, Relay mentions, credential/auth failures) is now excluded
   rather than vetoing the whole scan for a non-heartbeat wake, while a
   heartbeat review keeps its original all-or-nothing rule unchanged.
   writeEligibleRowsSnapshot publishes the exact eligible sequence
   numbers before every branch prompt; fm-primary-pi-watch.ts's offer
   still refuses a check-kind trigger outright so a main-only close is
   never itself routed to the branch.

3. A repeat identical merged-PR-poll result for an already-notified task
   is absorbed instead of enqueued again. A poll's own retirement state
   is scoped to one registration and cannot see a prior registration's
   outcome, so a task re-registered after its merge was already surfaced
   would otherwise wake main a second time for the same event.
   bin/fm-pr-lib.sh's new per-task pr-poll-merge-notified marker survives
   across re-registrations to catch that case; the first notification for
   a task still reaches main unchanged.

Regression tests colocated in tests/fm-startup-network.test.sh,
tests/fm-wake-queue.test.sh (including the mixed-queue no-swallow
property), tests/fm-pi-branch-extension.test.sh, and
tests/fm-pr-check-security.test.sh. docs/watcher-continuity.md and
docs/pi-supervision-branch.md updated for the new contracts.

* no-mistakes(review): Bind merge deduplication to canonical PR identity

* no-mistakes(review): Serialize wake row ownership across main and branch

* no-mistakes(review): Bind branch grants and deduplicate within actor claims

* no-mistakes(review): Fallback main-owned wake claims to main delivery

* no-mistakes(review): Clarify silent startup success guidance

* no-mistakes(review): Release residual branch grants after settled prompts

* no-mistakes(review): Reject truncated wake rows as corrupted

* no-mistakes(document): Document per-actor routing and silent startup success

* no-mistakes(lint): Fix ShellCheck findings in wake grant and startup test

* no-mistakes: apply CI fixes

* fix(pi): hide branch outcomes tool rows in Calm (#3024)

* Hide branch outcome tool in Pi Calm

* no-mistakes(review): Preserve stock outcomes rendering and document tool audit

* no-mistakes(review): Document branch read tool audit disposition

* no-mistakes(review): Match stock outcomes output sanitization

* no-mistakes(document): Document Calm custom-tool visibility

* fix: bind no-mistakes attestations to PR head (#3027)

* fix: delegate no-mistakes PR gate to pinned action

* no-mistakes(document): Document commit-bound no-mistakes attestations

* feat(pi): add persistent supervision branch model selection (#3028)

* feat(pi): let operators pin a cheaper supervision-branch model

Supervision is an easier job than the captain's own conversation, so the
Pi supervision branch does not need main's model. A new /supervision-model
command opens Pi's own selector over Pi's own catalog of credentialed
models, plus a "Follow main" entry, and persists the pick as one
<provider>/<model-id> line in this home's gitignored
config/supervision-branch-model. Firstmate keeps no model catalog of its
own.

The branch resolves the pin at every branch build - the first wake of a
cold start and the reopen after /new, /resume, /fork, or reload - so the
choice survives all of them, and picking also releases the live branch so
the next wake reopens the same persistent branch conversation under the
new model. An absent, unreadable, or unparseable file means no pin and
keeps today's behavior byte for byte: no model option is passed and Pi
picks the branch's model exactly as before.

A pin naming a model Pi cannot hand back is never silently downgraded
onto main's model: the branch refuses to build and the wake falls back to
the captain-facing main path naming the unusable pin, which is the
extension's existing failure direction.

The choice is home-local and not part of secondmate inherited
configuration, matching the Pi Calm preference precedent.

docs/configuration.md owns the operator-facing schema. Portable
regressions cover pin-present on create and reopen, pin-absent default,
the command's persistence, cancellation, and live rebind, and both
unusable and unparseable pins. The opt-in real-SDK guard proves the
vendor surface the pin reads and that an explicit model wins over the
model a reopened session recorded.

* no-mistakes(review): Fix supervision model runtime and rebind races

* no-mistakes(review): Restrict supervision picker to isolated runtime models

* no-mistakes(document): Document supervision branch model selection

* fix(pi): make the supervision model pin authoritative on every reopen

Clearing the pin with "Follow main" removed the file but the next branch
build reopened the persistent branch session with no explicit model
override, so Pi restored the model that session had recorded - the old
pinned model - while the command reported that the branch now follows
main. The same gap meant an absent pin did not reliably mean
same-model-as-main once a home had pinned once.

The pin file's current state now decides the model on every branch build,
create and reopen alike, overriding Pi's session-state restore. With a
pin, that model. With no pin, main's own current model is applied
explicitly, tracked from the contexts Pi already hands the extension plus
its model_select event, since the branch is built at wake time with no
context of its own. Only when main's model is unknown, or this home's
stored credentials cannot run it in the isolated branch runtime, does a
build fall back to passing no override at all, which is the behavior from
before the pin existed; the branch is never refused over model choice.

The command's notification now reports the model actually applied, and
says plainly when clearing the pin could not apply main's model instead
of claiming a change that did not take effect.

No credential handling changes: the branch still relies entirely on the
stored credentials its own runtime already holds, and the picker stays
restricted to models that runtime can resolve.

Colocated regressions cover pin present on create and reopen, clearing
the pin returning a reopened branch to main's model and specifically not
the old pinned one, an unparseable pin behaving as no pin, and the
unknown-main-model fallback to no override.

* no-mistakes(review): Make unpinned supervision follow main model changes

* no-mistakes(document): Correct supervision model documentation

* feat(pi): let /supervision-model pick branch reasoning effort (#3079)

* feat(pi): let /supervision-model pick the branch's reasoning effort

Supervision is an easier job than the captain's own conversation, so the
Pi supervision branch does not need main's reasoning effort any more than
it needs main's model. /supervision-model now settles both in one flow:
the existing model picker, then a follow-up effort picker built from Pi's
own supported thinking levels for the model just chosen. Firstmate keeps
no effort catalog of its own; the menu, the clamp, and the vocabulary all
come from Pi.

The pick persists as one line in this home's gitignored
config/supervision-branch-effort, independent of the model pin: a captain
may pin a model, an effort, both, or neither. The effort pin's current
state decides the branch effort on every branch build - the first wake of
a cold start and the reopen after /new, /resume, /fork, or reload - and
overrides Pi's restore of whatever level a reopened branch session
recorded, which is what keeps "Follow main" honest. With no pin, main's
own current effort is applied explicitly and followed live through Pi's
thinking_level_select event, the same way an unpinned branch already
follows main's model, and the two selections now share one build revision
so either change invalidates an in-flight build.

The branch is never refused over effort. Pi owns the clamp, so a pinned
level the branch's model cannot run becomes that model's nearest supported
level while the captain's raw pick is kept for a model that supports it,
and the command reports the level the branch will really run at rather
than the raw pin. A token Pi would not recognize at all is treated as no
pin rather than passed to that clamp, which would otherwise collapse a
typo into the model's lowest level. Only when main's effort cannot be read
either does a build pass no effort override at all, which is the behavior
from before this file existed.

Pi's own effort vocabulary is pinned by a bidirectional type assertion
against Pi's getThinkingLevel return type, so the tracked strict typecheck
against the installed package fails the moment Pi adds or removes a level.

docs/configuration.md owns the operator-facing schema for both pins.
Portable regressions cover the pin on create and reopen, model-only and
effort-only pins working independently, clearing a pin returning the
branch to main's effort, live-follow of a mid-session change, the clamp,
an unrecognized token, the unknown-main-effort fallback, and the command's
two-step flow, persistence, cancellation, and honest reporting. The opt-in
real-SDK guard proves the vendor surface all of that rests on, and also
repairs a pre-existing gap that left it unable to load the extension at
all.

* no-mistakes(review): Resolve effective branch effort honestly

* no-mistakes(document): Clarify Pi-owned effort picker behavior

* fix: keep routine supervision noise out of captain chat (#3093)

* fix(supervision): silence empty board closes and decouple the heartbeat

Two unrelated sources of noise put routine supervision events in the
captain's chat.

An empty Lavish board close - the captain reads a review surface, says
nothing, and closes it - became a check wake whose entire content was
that nothing happened. Suppress it at its source instead of routing it
anywhere: the generic runner gains a `silent` adapter seam mirroring the
existing `terminal` one, and the Lavish adapter answers it for exactly
one positively-determined shape, an `ended` session carrying no queued
content block. A silenced result is recorded durably handled so it does
not return on a later reconcile. Everything else announces unchanged - a
`Send & End` close carrying the captain's real answer, an `ended` result
still carrying content, a waiting or missing session, an unreadable
result, and every adapter that implements no `silent` command at all.
The keyed-answer feed is untouched, so suppressing an announcement never
suppresses the captain's own answer.

A fleet heartbeat was deferred to main merely because some unrelated
check row happened to be sitting unread, which put a routine fleet
review in the chat for a reason that had nothing to do with the fleet. A
check row is permanently main-owned, so it is now excluded from a
heartbeat claim rather than vetoing the scan, exactly as in every other
mode. What all-or-nothing guarantees is unchanged: the branch takes
every branch-ownable unread row or none of them, and an unresolvable
task-local row, an unknown row kind, or an unreadable queue still defers
the whole review to main. Main is still woken for the check on that
check's own triggering close, so nothing starves.

Main-only classes are unchanged and now each covered by a test: Relay
mentions, credential failures, merge confirmations, real board answers,
and watcher-failure repair. The per-actor acknowledgement and
no-cross-swallow properties are untouched.

* no-mistakes(review): Fail closed on all Lavish content headers

* no-mistakes(review): Suppress false unacknowledged status for silenced results

* fix(bin): stop a correlation token from hiding and stranding decisions (#1967)

* fix(classify): read the decision fold through a correlation token

status_line_verb stripped a trailing [key=...] from a status line's prefix
but left everything else glued to the verb, so a line carrying the
correlation token bin/fm-pending-reply-lib.sh embeds and a secondmate
echoes back matched no arm of _fm_decision_fold_line. Such a line folded as
ordinary status in both directions: a needs-decision or blocked opener never
opened its key, and a resolved or captain-held closer never closed one. The
same glued verb also hid correlated done and blocked lines from
status_is_captain_relevant and status_is_terminal_verb, and let correlated
working and resolved lines leak through the free-text fallback the
nonterminal guard was meant to stop.

The verb parse now walks whole words and drops only a token of the exact
shape a firstmate library writes - corr=<16 hex>, plus the bracketed form
bin/fm-secondmate-report.sh emits - before or after the key token, unkeyed,
or doubled. An arbitrary name=value word is deliberately NOT skipped:
skipping unknown tokens would let free text carrying an equals sign reduce
to a bare verb and impersonate a transition, which is the takeover the
strict parse and _fm_decision_key_transition_allowed exist to prevent. A
prefix with no corr= substring is returned byte-for-byte as before, so every
line without a token keeps its exact historical verb.

FM_OPEN_DECISIONS_FOLD_VERSION goes to 3, because every cursor persisted
under the previous reading carries an open set computed while correlated
lines were invisible and must be rebuilt from byte 0.

Measured over a real 383-line status log: 254 lines keep byte-identical
captain-relevance, pause, terminal-verb and captain-held verdicts, and all
129 changed lines carry a valid token - 14 correlated done/blocked/
needs-decision lines become captain-visible, and 20 correlated
working/resolved lines stop being escalated on prose alone.

* fix(review): Captain, block token-first decision impersonation

* fix(document): Clarify normalized status verb ownership

* fix(classify): reconcile the correlation-token read with the tag-stop parser

Rebasing onto main put this change beside #2280, which made verb parsing
stop at the first "[name=value]" tag. Both edit status_line_verb with
different intents, so the resolution keeps both rules rather than letting
one overwrite the other:

- #2280's tag stop is kept verbatim and now owns every BRACKETED tag,
  including the "[corr=...]" form fm-secondmate-report.sh writes. The
  bracket-unwrapping arm this branch had added to the token test is
  therefore removed as unreachable.
- This branch's token walk is kept and narrowed to the UNBRACKETED token
  fm-pending-reply-lib.sh writes, which the tag stop does not reach.

Two consequences of standing beside #2280 rather than before it:

The fold version had collided at 4: #2280 spent it on the tag-stop
parser and this branch had spent it on the token read. A cursor
persisted under #2280's reading predates this one and must still be
rebuilt, so the version moves to 5.

A bracketed impostor is dropped from the malformed-token list. On main
today "resolved [corr=deadbeef] [key=victim]:" already reads as the bare
verb, as does "resolved [anything at all] [key=victim]:", because the tag
stop ends the parse at the opening bracket regardless of content. That is
#2280's reviewed contract; asserting otherwise here would narrow it. The
unbracketed impostors it owns stay strict and still fold as prose.

Adds a consumer test for the two verb-string case arms that postdate this
branch: fm-supervise-daemon.sh's transient-stale arm and
fm-crew-state.sh's map_log_state.

* fix(review): Captain: Seed cursor migration fixture with version four

* fix(document): Clarify voice status normalization ownership

* fix(bin): Cursor-Park unter Pi-Host ohne Cursor-Identität stilllegen (#3115)

* fix(bin): Cursor-Park unter Pi-Host stilllegen.

pi-cursor-sdk lädt .cursor/hooks.json in die Pi-Sitzung und parkte einen zweiten Watcher; das erzeugte rearm-resurface und brach laufende Rückfragen ab. Bei PI_CODING_AGENT=true beendet der Park sofort, native cursor-agent Primaries bleiben unverändert.

* fix(bin): Cursor-Park trotz PI-Leak nur ohne Cursor-Identität stilllegen.

Stand-down gilt nur bei PI_CODING_AGENT=true ohne CURSOR_AGENT und ohne CURSOR_INVOKED_AS. Handgestartete cursor-agent Primaries mit geerbtem PI-Marker parken weiter.

* no-mistakes(document): Document Cursor park Pi-host stand-down

* fix(bin): no-mistakes-Mindestversion auf 1.46.0 anheben.

Die PR-Attestierung verlangt ab 1.46.0 strukturierte Pipeline-Schritte; der Bootstrap-Floor blieb bei 1.31.2 und ließ zu alte Builds zu.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(pi): make supervision model picker searchable and scrollable (#3099)

* fix(pi): make /supervision-model's model list bounded and searchable

Pi's generic extension selector renders every option at once with no
search box, so a real eligible catalog ran off the top of the terminal.
The model step now draws the same rows through Pi's own SelectList - the
bounded scrolling primitive behind Pi's /model picker - with Pi's own
Input and fuzzy filter above it for search, keeping 'Follow main' first,
the branch-runtime eligibility filter intact, and the pick branch-only.

Pi's ModelSelectorComponent is deliberately not reused: its selection
handler writes the captain's default model through Pi's settings manager,
which would move main's conversation as a side effect of pinning the
branch. The effort step's menu is a handful of levels and stays on Pi's
plain selector dialog.

* no-mistakes(document): Clarify supervision picker documentation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes(document): Document searchable supervision model picker

* no-mistakes: apply CI fixes

* fix(bin): durably report merged pull requests (#3104)

* fix: make a landed merge leave a durable outcome

A merge was the one lifecycle event that left no record outside the
merging agent's memory. bin/fm-pr-merge.sh ended at the forge call, and a
home merging under standing authority never waits for the merge poll that
would otherwise confirm it, so three real merges reached the captain as
silence.

bin/fm-merge-outcome-lib.sh is the single owner of that record. A
secondmate home reports the landed PR upward on the same parent reply
channel its terminal-outcome backstop already uses; a main home records
it on the durable wake queue. The record is at most once per task and
canonical PR identity, and only a merge that actually landed produces one.

The merge poll feeds that same channel when it detects a merge this home
did not perform, so the captain's own forge merge and a merge firstmate
performed itself produce one consistent outcome instead of two reporting
paths. No new state file and no second watch path.

Two smaller gaps from the same failure:

- A mate charter listed its report triggers without naming a landed
  merge. Under standing merge authority nothing is ever "ready for
  review", so the enumerated list silently omitted the case that matters.
- A secondmate home seeded without its parent binding failed every
  terminal-outcome report for the same reason, and the diagnostic never
  named the binding. It does now.

* no-mistakes(review): Harden durable merge outcome reporting

* no-mistakes(review): Make merge race regression deterministic

* no-mistakes(review): Make merge outcomes retry-idempotent and forge-confirmed

* no-mistakes(review): Unify merge publication under canonical outcome marker

* no-mistakes(review): Publish merge outcomes before committing dedup markers

* no-mistakes(review): Document at-least-once merge outcome recovery

* no-mistakes(review): Use supported GitHub confirmation and update recovery docs

* no-mistakes(review): Preserve distinct merge wakes by PR identity

* no-mistakes(document): Document durable merge outcome semantics

* no-mistakes(test): Make merge outcome interleaving test deterministic

* no-mistakes(document): Clarify merge outcome documentation ownership

* fix(lint): keep the merge-outcome library an analysis boundary

bin/fm-watch.sh followed the new merge-outcome library's source graph,
which reaches the wake queue, PR identity, and secondmate parent
libraries. Expanding that inside an already-large lint root pushed
ShellCheck's external-source analysis past the bounded CI lint worker:
the Lint job was killed with SIGTERM after five silent minutes, twice,
having emitted no diagnostics at all.

Make it an analysis boundary, exactly as the transition and inbox owners
directly above and below it already are and for the same stated reason.
Coverage is unchanged because the library is a canonical lint root in its
own right and is still linted as one.

Measured locally: the watcher goes from not terminating within 120s to
9s clean, and the library alone lints in 1s clean.

* fix(bearings): preserve projections through inventory mismatches (#3129)

* fix(bearings): keep an inventory-mismatch home readable, and mark warnings as repairs

A backlog-vs-metadata inventory mismatch inside a secondmate home was being
reported as "we cannot read that home", which discarded that home's open
captain calls, queued work, landed work, and live workers from the whole
Bearings digest. The main home already treats the identical mismatch as a
harmless disclosure; this makes the secondmate path agree.

- fm-fleet-snapshot.sh: the invalidity gate now passes orphan_in_flight,
  unowned_current, and terminal_in_flight through the partial-structured
  carve-out alongside child_current_unavailable, so those homes keep their
  decisions, holds, queued, landed, and live work and leave unreadable[].
  missing_backlog and unstructured_current stay on the discard path, because
  there the backlog itself is untrustworthy.
- fm-fleet-snapshot.sh: the same three kinds no longer collapse the home's own
  classification to "unknown"; the real captain_decision / active_child_work /
  externally_held classification survives and invalidity carries the warning.
  An unavailable child state still collapses it, including when a mismatch
  masks it under strict-invalidity precedence.
- secondmate_landed.partial now keys on partial-structured trust rather than an
  unknown state, so an inventory-mismatch home is still disclosed as partial.

Ask the home that owns the wrong books to fix them:

- bin/fm-secondmate-reconcile.sh sends exactly one reconcile instruction per
  mismatch episode through the ordinary steering transport. A persistent
  mismatch keeps its episode identity and never re-nags; a changed mismatch
  earns one more ask; a repaired one is forgotten so a recurrence is asked
  about again. The parent never touches the mate's own files, and a failed send
  records nothing so the next run retries it.

Give integrity warnings their own look on the board:

- charted rows take an optional kind of "queued" (the default) or "warning".
  A warning badges "needs repair" instead of "waiting" and is excluded from the
  Charted Next count, so alarms stop reading as dispatchable queued work. No
  fifth board section, and every existing payload stays valid.

Tests pin the new policy behaviorally: the retained surfaces and classification
for all three mismatch kinds, the still-discarding unstructured_current and
missing_backlog cases, the once-per-episode reconcile ask through real durable
steering records, and the board rendering exercised through the shipped
template under a minimal DOM shim.

* no-mistakes(review): Make reconcile dedupe atomic and warnings non-dispatchable

* no-mistakes(review): Preserve reconcile identity and reject stale snapshots

* no-mistakes(review): Order snapshots uniquely and canonicalize episode identities

* no-mistakes(review): Add fire-and-forget reconcile and separate warning overflow

* no-mistakes(review): Exclude fire-and-forget from escalation and track reconcile background

* no-mistakes(review): Run reconcile enqueue inline across all adapters

* no-mistakes(review): Track reconcile clears across strict-invalidity homes

* no-mistakes(review): Persist reconcile transitions atomically

* no-mistakes(document): Document reconcile and fire-and-forget contracts

* refactor(bearings): replace the reconcile episode dedupe with a 4-hour cooldown

The reconcile ask needed to fire once per problem without nagging on every
recap. The episode-precise record that tried to do that had to be correct in
every direction at once - order two concurrent snapshots, tell a repair from a
new problem, and never lose a clear - and each direction it got wrong either
swallowed a nudge or sent a duplicate.

A per-home cooldown removes the whole class. One durable timestamp per home,
one nudge per four hours, and nothing to get stale, mis-order, or
mis-classify: a home in mismatch is asked once, later recaps stay silent, and
a mismatch still sitting there after the window earns one gentle re-nudge.

- bin/fm-secondmate-reconcile.sh: state/<id>.reconcile-nudged holds the epoch
  second of the last ask; FM_RECONCILE_COOLDOWN_SECONDS names the window. The
  episode identity, ordering generation, pending/clear transitions, and
  delivery-identity reuse are all gone. A known-undelivered send starts no
  cooldown so the next run retries it; an unconfirmed one does, because a
  duplicate ask is worse than one the mate may already hold.
- bin/fm-fleet-snapshot.sh, bin/fm-bearings-snapshot.sh: drop the snapshot
  `observation` monotonic identity, which existed only to order those records.
- bin/fm-teardown.sh: retire the cooldown record with the endpoint's other
  runtime artifacts, so reseeding a retired id is not silenced by its
  predecessor's window.

The inline durable fire-and-forget send is unchanged, and the projection fix
and the warning surface are untouched.

Tests follow the behavior: the cooldown suite now pins one ask per window, the
re-nudge after it, the four-hour boundary, per-home independence, and that the
ask stays out of a re-ring ladder that still rings an ordinary steer beside it.
The obsolete observation-ordering test is deleted with the machinery it covered.

* no-mistakes(review): Serialize reconcile cooldown commits with mate lifecycle

* no-mistakes(review): Reject stale reconcile snapshots across mate reincarnations

* no-mistakes(review): Start reconcile cooldown after delivery completes

* no-mistakes(review): Keep reconcile sends nonblocking and remove pending residue

* no-mistakes(document): Document reconcile skip and stale-endpoint behavior

* no-mistakes(lint): Fix reconcile test subshell lint warning

* no-mistakes: apply CI fixes

* fix(bin): reconcile markerless remote secondmates safely (#3140)

* fix(bin): stop dropping reconcile nudges for markerless remote secondmates

A persistent remote secondmate's parent-side state/<id>.meta never carries
spawn_gen: bin/fm-spawn.sh's spawn_remote_secondmate() is its sole writer and
never writes one, because that incarnation identity does not apply to a
remote route. fm-secondmate-reconcile.sh's row filter required a non-empty
spawn_gen matching an identifier regex, so every such row was silently
dropped before the per-row loop ever saw it: no sent/stale/failed line, no
cooldown record, nothing sent, and no trace of why.

Give a legitimately markerless persistent remote secondmate a safe substitute
identity - its recorded remote_host - instead of weakening the spawn_gen
check for rows that do have a generation:

- bin/fm-secondmate-reconcile.sh: carry host through the row projection for
  both fm-fleet-snapshot.v1 and fm-bearings.v1 documents, and admit an empty
  spawn_gen instead of filtering the row out. A new revalidate_identity()
  compares the sampled spawn_gen against current metadata when one was
  sampled (unchanged), or the sampled host against the metadata's
  remote_host when none was sampled and the metadata still carries no
  spawn_gen of its own. A row with neither a spawn_gen nor a host has no
  safe identity at all and fails loudly instead of vanishing, exactly the
  visibility the original bug lacked.
- Rows now join on the ASCII unit separator rather than @tsv: bash's
  IFS-whitespace read collapses consecutive tabs, which would have silently
  dropped a legitimately empty field again.
- bin/fm-bearings-snapshot.sh: thread host through the secondmate_reconcile
  projection so the fm-bearings.v1 path (the one bearings itself feeds to
  the reconcile hook) carries the same substitute identity.
- tests/fm-secondmate-reconcile.test.sh: end-to-end coverage through the real
  remote transport (fm-on.sh + fm-remote-secondmate-control.sh against a
  genuinely seeded remote home) for a markerless mate nudged once per
  cooldown window, a stale/replaced remote route refused exactly like the
  existing local spawn_gen case, and a row with no identity at all failing
  loudly rather than being swallowed.

* no-mistakes(review): Enforce markerless remote host identity during final delivery

* no-mistakes(document): Document markerless remote reconciliation safety

* fix(bin): hand a busy declared pause to the away-mode daemon once, undecorated (#3147)

* fix(watch): hand a busy declared pause to the away-mode daemon undecorated

While away mode is active the daemon owns triage and the watcher reverts to
one-shot, handing over plain wake identities the daemon classifies itself. The
busy-turn bound was the one stale path that did not: with afk active it ran the
wedge timer, so the daemon received a wake already decorated as a possible wedge.

That decoration outranks the daemon's own verdict. handle_wake escalates an
enriched wedge reason before its pause classification can apply, so a crew that
declared the wait itself - a `paused:` external wait or a verified captain-held
transfer holding a live foreground call - was wedge-escalated once per
FM_STALE_ESCALATE_SECS for as long as the wait lasted, the escalation count
climbing into demand-deep-inspection on a pane nobody needed to inspect.
Measured on the pre-fix tree, five consecutive re-arms produced five escalations.

busy_turn_bound_check now reads the declaration before the afk branch: away mode
hands off the plain window identity, one-shot per distinct stale hash, leaving
normal-mode pause bookkeeping unwritten because the daemon owns it there. The
daemon then classifies the wait itself and self-handles it on the long cadence.
Normal-mode behavior is unchanged, and lifting the declaration still restores the
busy-pane wedge escalation on the same pane.

The regression covers all three: the undecorated handoff with no wedge timer or
escalation counter, the one-shot on re-arm that the escalation ladder used to
climb, and the restored wedge escalation once the declaration is lifted.

* no-mistakes(review): key afk busy-pause handoff on declaration, clear wedge state

* no-mistakes(document): docs: scope away-mode busy-bound handoff to declared waits

* no-mistakes(document): docs: note afk busy-bound handoff in watcher header

---------

Co-authored-by: Talon Stark <talonstark@gmail.com>

* fix(bin): name the stale submodule pin behind a pooled slot refusal (#3121)

* fix(bin): explain a pooled slot's stale submodule refusal

A pool slot whose submodule pin moved is refused with "is not clean; refusing
to discard uncommitted work", while the operator's own `git status` in that slot
reads clean. The message names no submodule, no pin, and no remedy, so the
refusal is unreadable and the slot looks wedged for no reason. That is the
failure that jammed three slots in a row when a submodule pin moved.

The refusal itself was never the bug and is unchanged: the gate still refuses,
and still touches nothing. It now distinguishes the one case it can prove and
says what it found - the submodule, the pin the slot has, the pin the base
records, and the command that clears it.

The diagnosis is deliberately conservative, because ` M <path>` alone cannot
tell a stale pin from real work. An entry is reported as stale only when every
reported entry is a gitlink whose submodule is internally clean and whose
recorded pin actually differs. A submodule holding uncommitted work, untracked
files, or an unpushed commit therefore keeps the original uncommitted-work
refusal, even when its pin is also stale - the remedy command would be wrong
there, and the conservative refusal is the safe answer.

Nothing is converged, synced, initialized, or deleted. There is no new failure
path: a slot that launched before still launches, a slot that refused before
still refuses, and projects that configure a submodule `ignore` are read exactly
as before. Paths are read with core.quotePath=false so a non-ASCII submodule is
named rather than falling back to the unreadable message.

Tests keep the reproductions that prove the message is accurate: the stale-pin
diagnosis (which fails against the previous refusal), work inside a submodule
still refused as uncommitted work, and a stale pin carrying real work refused
conservatively rather than called stale. Each asserts the slot is left untouched.

* no-mistakes(review): require remote containment before calling a submodule pin stale

* fix(bin): stop printing a remedy the containment check cannot stand behind

The stale-pin diagnosis printed `git submodule update --checkout` as the command
that clears the slot. The containment check behind it reads local refs only and
never fetches, because this gate has to stay usable offline. A remote-tracking
ref that has gone stale - its upstream branch deleted or force-pushed, and never
pruned - still reads as containment, so a commit that is really unpushed can look
contained and that command would move the submodule off it.

Naming the submodule and both pins is the whole point of the diagnosis: it turns
"is not clean", on a slot whose own `git status` reads clean, into a statement of
which submodule drifted and where it drifted from. The operator can choose the
remedy from that, seeing the whole picture. Printing an instruction that rests on
a judgement which can be fooled is worse than printing none, so it is dropped.

The limitation is now stated where it applies, in the script header and beside
the check itself, rather than left for a reader to discover.

No fetch is added: the gate stays offline-safe by design. Nothing else changes -
the same conditions are refused, the slot is still never touched, and a submodule
carrying real work or an unpushed commit still keeps the conservative
uncommitted-work refusal.

* no-mistakes(review): bound submodule containment probe to first commit

* fix(pi): prevent stale captain outcome re-emissions (#3154)

* fix(pi): type captain supervision outcomes so main relays them

A captain-relevant branch outcome reached main as a bare user message with
no marker of origin or required action, written in main's own captain-facing
voice, landing in a tail that often already held several such notes. Pi keeps
only a custom message's content when it builds the provider request, so
customType and display never reach the model and content was the only place
that identity could live.

Main could not tell an incoming outcome from its own earlier answer and
sometimes re-emitted that answer instead of relaying the outcome, losing it.
Measured against real Pi 0.84.1 on openai-codex/gpt-5.6-sol: 6 failures in 24
turns, rising to 3 in 6 once one stale answer was already in the tail, which
is how one captain conversation saw six identical messages in a row. The same
scenario with the outcome typed failed 0 times in 14 turns.

Wrap only the captain-verdict note in the branch-outcome operational kind
owned by bin/fm-operational-input.sh. Delivery is otherwise unchanged: still
display: false, still one triggerTurn follow-up, so the turn remains the
single captain-visible outcome and no hidden note is ever shown twice.
Routine notes stay plain because their renderer reads the glyph off the front
of that same string. An outcome that cannot be encoded degrades to the same
instruction as plain text rather than being lost, matching this file's stated
failure direction.

The existing assertions could not catch this: they pin the sendMessage
options and never look at what main receives. Add a portable regression that
classifies the delivered payload with the real protocol executable, and a
live guard that runs the real Pi SDK's own convertToLlm to prove content is
the entire model-visible payload.

* no-mistakes(document): Document typed Pi captain outcomes

* fix(bin): keep a declared wait on the pause cadence under a busy pane or enriched wedge (#3155)

* fix(bin): keep a busy pane from retiring a still-declared wait's window

The away-mode daemon's pause re-surface recheck (housekeeping step 2b) read a
busy pane as "the crew resumed" and dropped the declared-wait marker, without
re-reading that the crew's own latest status line still declared the wait.

That inference is not safe, because a declared wait can legitimately hold a pane
busy: a worker sitting on a long foreground call keeps that call live for as long
as the wait lasts. The marker is then cleared while the declaration still stands,
and migrate_watcher_pause_markers recreates it with a fresh timestamp on the very
next tick, so the window restarts forever and the wait never matures into its one
bounded recheck.

Away mode makes that terminal. Since the watcher half landed, a busy pane under a
declared wait is handed to the daemon exactly once per declaration and never woken
again while the declaration stands (bin/fm-watch.sh, busy_turn_bound_check), so
this recheck is the only thing left that can re-surface the pane at all. Measured
end to end on a throwaway state root, away mode active, a pi pane busy past
FM_BUSY_TURN_MAX_SECS, status still `paused:`, over six PAUSE_RESURFACE_SECS
windows: 0 captain-facing rechecks before this change, 6 after - one per window,
with the marker reset each time.

The fix drops only the busy arm of the 2b probe, leaving it an endpoint-readability
check: exit code 2 still means the capture failed, so the endpoint is gone and the
marker goes. The loop head above already drops the marker the moment the status
line stops declaring the wait, so nothing else is needed to end the routing, and
the reconcile path runs before the probe ever reads a pane.

tests/fm-daemon.test.sh: test_housekeeping_paused_resumed_cleared pinned the old
inference on purpose - its fixture's status line still read `paused:` while the
pane was busy, and its comment read "A pause whose pane became busy again (the
crew resumed)". Its fixture now resumes the way a crew actually resumes, by
appending a non-declaring status line, and it asserts its own busy verdict first
so it cannot silently decay into the idle-pane case that
test_housekeeping_paused_unpaused_cleared already covers. What it pins is now the
inverse guard: a busy pane must not GATE the clear either, so an over-correction
that kept the marker alive whenever the pane is busy would fail it.

test_housekeeping_busy_declared_wait_matures_its_window is the new regression, over
both declaration forms. It asserts the busy verdict, then that ticks inside the
window neither escalate nor let the marker be recreated with a fresh timestamp,
then exactly one recheck past the window named for the right human and never a
wedge, then silence on the next tick inside the reset window. It fails on
unmodified main with "produced 0 escalations past its window, expected exactly
one".

Refs #3149

* fix(bin): let a declared wait outrank an enriched wedge escalation

handle_wake classifies a stale wake through classify_stale, which returns a
`pause` verdict for a crew whose latest status line declares an external wait or
a verified captain-held transfer. It then threw that verdict away whenever the
wake reason matched `idle *s, possible wedge, escalation *`, so the watcher's
enriched wedge decoration outranked the crew's own declaration and a healthy
declared wait was escalated once per FM_STALE_ESCALATE_SECS for as long as the
wait lasted.

The enriched reason earns its precedence over the daemon's cheaper status-log
absorption honestly - it carries the watcher's escalation count and its explicit
"do not re-absorb on the run-step/pane state alone" demand. A `pause` verdict is
not run-step or pane state. It is the crew's own declaration that this pane waits
by design, which is precisely the question the wedge timer cannot answer for
itself, so it is the one verdict that decoration must not override.

The two classifications genuinely disagree in steady state rather than only in a
race: a crew that declares `paused:` while its no-mistakes run is still attributed
to its code reads `working` to the watcher's pause_state_class, so the watcher
takes the wedge timer while the daemon's classify_stale reads the status log and
correctly returns `pause`.

The wait stays bounded, not silenced. Absorbing to the pause action records the
declared-wait marker and drops wedge aging, and housekeeping (2b) then owns the
re-surface, so the pane still reaches the captain - once per PAUSE_RESURFACE_SECS
as an explicit "recheck whether the wait still holds", instead of once per
FM_STALE_ESCALATE_SECS as a possible wedge. Measured on a throwaway state root
over five wedge cadences for one declared wait: 5 escalations climbing to
demand-deep-inspection before this change, 0 after, with the one bounded recheck
still delivered.

tests/fm-daemon.test.sh: test_stale_diagnostic_wedge_survives_busy_housekeeping's
`paused` case pinned the old precedence on purpose, asserting exactly one
escalation carrying the demand-deep-inspection payload. That case now asserts the
pause cadence instead - no escalation inside the window, pause tracking recorded -
while the `working` and `prior-terminal` cases keep asserting the enriched wedge
verbatim, so the override itself is still pinned everywhere it is correct.

test_enriched_wedge_under_declared_wait_uses_pause_cadence is the new regression.
It asserts the fixture's own classifier verdict is a pause first, so the case
cannot go vacuous, then drives four consecutive wedge-cadence deliveries in both
the plain and demand-deep-inspection forms through the real handle_wake and
housekeeping pair, then matures the window for exactly one awaiting-external
recheck, then lifts the declaration and requires the same enriched wedge to
escalate again unchanged. It fails on unmodified main at the first delivery.

Refs #3149

* no-mistakes(review): align afk skill recheck wording with still-declared contract

* no-mistakes(document): daemon doc comments: pause window ages on declaration

---------

Co-authored-by: Talon Stark <talonstark@gmail.com>

* fix(bin): recover Claude auto-arm from hung claims (#3156)

* fix(bin): make Claude auto-arm continuity self-heal past a hung claim

On a Claude primary, a Stop-hook auto-arm process that hung mid-arm held
the single-flight owner lock with its epoch ledger frozen at
outcome=arming, and the abandonment proof read any live lock holder in
arming as legitimately deciding forever. Every later Stop firing exited
0 at the lock, the turn-end guard kept deferring to the hung owner as
recovery under way, and the watcher was never auto-re-armed again for
the rest of the session - supervision survived only on manual arms and
lapsed between them (the 2026-08-26 watcher flap).

Corrections layered onto the lock-held-across-arm shape each reopened
the same concurrency class one level down, so this replaces the claim
machinery wholesale with a generation-based optimistic design:

- The epoch ledger's monotonic sequence IS the claim generation; the
  two-line entry (classic epoch record plus the claimant's MANDATORY
  pid-identity) is the claim. Every firing defers to a live OPEN claim:
  outcome arming, owner alive, identity recomputes and matches, and not
  stuck (entry and watcher beacon both older than the guard grace).
- A finished, dead, identity-mismatched, identityless, or stuck claim
  is superseded by simply taking the next generation - no signalling or
  revocation of a steady-state predecessor.
- No mutex is held across arming or output; the owner lock survives
  only as a micro-mutex around individual ledger writes. A superseded
  owner goes completely silent: ownership is re-verified before every
  arm invocation, episode-state mutation, ledger write, and
  continuation.
- The irrevocable commit point of a translation is the exit status
  (the harness delivers the collected stderr only on exit 2), so the
  owned terminal ledger write is the atomic commit: the winning
  generation exits 2 unconditionally after it, a refused one exits 0
  silently even after printing, and the once-per-episode failure notice
  commits in the same owned critical section as the winning failed
  write. Two bounded residuals are documented accepted intent: an owner
  dying between its owned write and its own exit, and a hung old-build
  owner resuming during the one legacy upgrade window.
- The pre-generation lock-holding claim shape keeps defer-or-reclaim
  behavior through a legacy shim: a live identity-verified stuck owner
  is retired via TERM (with a queued TERM sufficient when the owner is
  stopped) before its lock is removed, an unverified or identityless
  pid is never signalled but never blocks a proven-abandoned reclaim,
  and the lock's identity evidence is grafted into the ledger
  (mtime-preserving) so pid-reuse protection survives the lock.
- The guard reads the same predicates for recovery ownership and its
  terminal fail-open (which re-checks for a live open claim under the
  held locks before committing the attended alarm), with ledger reads
  anchored to line 1 so the identity line can never confuse them.

Behavioral regression coverage exercises all three edge classes through
the real hook and guard - a live open claim defers with no lock held, a
stuck claim is superseded and the home re-arms, and an end-to-end run
with a genuinely hung owner shows a concurrent firing deferring
promptly mid-arm, a later firing superseding the stuck owner, and the
superseded owner exiting silently without a second translation - plus
the identityless/reused-pid loopholes, the superseded-owner arm
boundary, and the legacy TERM, SIGSTOP, and signal-free reclaim paths.

* no-mistakes(review): Refuse auto-arm commits when notice marker creation fails

* no-mistakes(review): Make episode reset atomic with generation ownership

* no-mistakes(document): Update auto-arm generation and commit documentation

* fix(bin): verify the real GitHub merge outcome instead of reporting an unproved merge (#3064)

* fix(pr): verify GitHub merge outcome

* no-mistakes(review): Captain, fixed forge-only merge verification, queue guidance, metadata propagation

* no-mistakes(document): Correct forge-specific merge documentation

* no-mistakes(review): Captain: forge-only queue fix, focused tests pass

* no-mistakes(review): Captain: suppress closed-state guidance and prove parent regression

* no-mistakes(review): Captain: remove history proof; retain executable regressions

* no-mistakes(document): Clarify GitHub recording timing in architecture docs

* no-mistakes(document): Clarify outcome-aware PR merge recording documentation

* no-mistakes: apply CI fixes

* Revert "no-mistakes: apply CI fixes"

This reverts commit c326cfa9430c6173eedc8ff7f27d19d0552daf01.

The automatic CI repair round removed the up-front `gh` prerequisite check
while keeping the `gh` dependency: `bin/fm-pr-merge.sh` still calls
`gh api graphql` for the outcome read and `gh api` for the branch-rules read.
That left the same hard requirement without the clear named error, and review
immediately raised a new finding for exactly the failure the check prevents -
`gh-axi pr merge` landing the merge while the follow-up read fails, so the PR
metadata is never recorded.

The check is also symmetric with the GitLab arm directly above it, which
already refuses up front when `glab` or `jq` is missing, on the stated
principle that a missing tool should be a named prerequisite rather than a
merge that is armed and then refused for an unexplained reason.

The workflows this round was chasing sit at `action_required` because this is
a fork pull request; no code change can turn them green.

* fix(pr): keep PR bookkeeping when a merge outcome read fails

On the GitHub path a merge call that returned success was followed by
`github_read_outcome || exit 1`, so a transient API failure, rate limit,
or network blip during the read dropped out of the script before
`record_pr_metadata` ever ran. The merge could have landed while `pr=`
went unrecorded and the merge poll was never armed - bookkeeping lost on
a real merge. The failure path just above already recorded metadata
before exiting, so the error path was more careful than the success one.

Record the PR before that refusal. Recording arms the later merge poll
and is not a success claim, which is the same reasoning that keeps
`record_pr_metadata` on the gh-axi failure path. The refusal itself is
unchanged: exit stays non-zero and the message still names the concrete
observed state. Metadata is withheld only when the read succeeds and
proves the pull request neither merged nor queued.

Pin it with a case that stubs `gh api graphql` into failure after a
successful `gh-axi pr merge`, asserting both the non-zero exit and the
recorded metadata.

* no-mistakes(review): Aggregate queue rules and report conflicts explicitly

* fix(pr): keep the merge abstraction reachable and its bookkeeping intact

Two holes remained in the outcome-verified GitHub merge path, both on
installations where gh-axi is present but gh is not.

The verification preflight refused before bin/fm-pr-merge.sh ever reached
the configured gh-axi merge abstraction, so an installation without gh
could no longer merge at all. gh-axi now performs the merge unconditionally
and the queue-aware gh read became an optional enrichment: with gh on PATH
its GraphQL view still separates merged from queued, and without gh the
gh-axi view still proves a landed merge while every outcome it cannot prove
refuses.

The PR metadata recording sat behind the outcome read, so a merge that
landed before that read failed lost pr= and its merge poll. Recording now
happens once, before either forge call, which arms the poll without
claiming a landed outcome and leaves teardown a PR identity to verify
against no matter how the read ends.

Rebasing onto main also restored the durable merge-outcome reporting and
the GitLab landed-state confirmation that the conflict resolution dropped.

Tests pin each fix through the executable interface: the merge abstraction
is reached and verified with gh absent, a failed fallback read keeps its
bookkeeping, and a mock that snapshots the task meta during the forge call
proves pr= is recorded before the merge can land.

* no-mistakes(review): fix(pr): de-dup queue methods, fall back on failed gh read, refresh contracts

* no-mistakes(review): fix(pr): quote forge output and explain armed auto-merge on refusal

* no-mistakes(review): fix(pr): claim auto-merge armed only when the forge accepted it

* no-mistakes(review): fix(pr): tell the operator what each GitHub refusal could not observe

* no-mistakes(review): fix(pr): gate every forge-acceptance claim on a successful merge

* no-mistakes(document): align merge docs with verified GitHub outcome contract

* fix(pi): prevent duplicate captain outcome reports (#3184)

* fix(pi): stop reporting one merge to the captain twice

The supervision branch's captain-outcome note told main, unconditionally,
that the note "is not your own earlier output" and to relay it now. When
main had already reported the same event, that assertion was false and the
order turned the correct response - saying nothing new - into a mechanical
re-report, so the captain saw one merge reported twice in 16 seconds.

Two independent changes, both needed:

- The relay instruction is now conditional. It still names itself as a
  supervision outcome so main cannot mistake it for its own earlier answer
  (the silent loss that instruction exists to prevent), and it now lets
  main stay quiet about an outcome it has already given the captain.

- The merge case is closed at its source rather than left to that judgment.
  One merge reaches a home on two independent paths by design - main's own
  permanently main-owned merge poll, and the branch's task-local status
  wake - and main's captain-facing text only reaches the branch's mirror at
  main's turn end, so the branch can escalate before it could possibly see
  the captain was already told. bin/fm-pr-merge-notified.sh answers that
  question from bin/fm-pr-lib.sh's canonical merge-notification marker, so
  the answer holds regardless of mirror timing. A captain outcome naming an
  already-published merge is delivered as the ordinary rendered note
  instead of opening a follow-up turn: still appended, still visible, still
  recorded with the verdict the branch decided, minus the wasted turn.

Any error, timeout, or unreadable state relays the outcome. A duplicate
announces itself; a lost outcome does not.

Regression coverage drives the real delivery path in both directions: a new
outcome must still reach the captain in exactly one follow-up turn even
beside an unrelated published merge, and an already-published merge must
open no second turn while a different PR in the same task still does. The
merge path's real producer and this new consumer are exercised end to end
in tests/fm-pr-merge.test.sh.

Pi-only by construction: the delivery path lives in .pi/exte…
adonis-garcia-git added a commit to adonis-garcia-git/firstmate that referenced this pull request Sep 3, 2026
…iation (#12)

* feat(bin): merge GitLab merge requests through the guarded PR merge path (#2779)

* feat(bin): merge GitLab merge requests through the guarded PR merge path

bin/fm-pr-lib.sh already parses a GitLab merge request URL for the watcher,
but bin/fm-pr-merge.sh refused every non-github provider, so a merge request
had to be merged by hand and got none of the recording, guards, or audit
trail a pull request gets.

The merge path now dispatches on the parsed provider. A GitHub URL keeps its
exact previous behavior. A GitLab URL is addressed through glab by the project
URL rebuilt from the parsed host and path, so a merge request on any instance
resolves and no host is hardcoded, and no merge-method flag is added because
the project's own merge method is what should apply.

A GitLab merge happens only after one live read of the merge request confirms
it is open, detailed_merge_status is mergeable, has_conflicts is false,
blocking_discussions_resolved is true, and the head pipeline succeeded at the
exact current head. Every failing condition is reported, not just the first.
The verified head is bound to the merge with glab's --sha, so a push landing
between the read and the merge fails the merge instead of landing commits
nothing verified. Recorded metadata is never the authority for any of this: a
rebase moves the head and leaves a recorded value stale, so a recorded head
that disagrees with the live one is reported rather than trusted, and the
recorded value is read before the recording step because that step drops a
GitLab head it cannot resolve.

* no-mistakes(review): reject bundled -R clusters and make tool-absence cases host-independent

* no-mistakes(test): state authorised GitHub narrowing of bundled -R guard

This branch NARROWS GitHub behaviour. The narrowing was authorised
deliberately rather than slipping in by accident, and it applies to both
providers, GitHub and GitLab alike, because a script that guards one provider
and not the other is a trap for the next reader.

What bin/fm-pr-merge.sh now refuses is extra merge arguments containing a
bundled short-option cluster that includes R, for example "-dR other/repo".
The forge CLIs expand such a cluster one character at a time, so it carries
"--repo other/repo", and that later value wins over the repository the URL
named. Before this change, "fm-pr-merge.sh <task> <github-url> -- -dR
other/repo" reached "gh-axi pr merge 12 --repo example/repo --squash -dR
other/repo" and exited 0 with pr= recorded and the merge poll armed. It now
exits 1 with "extra merge arguments must not override the repository", records
nothing, and invokes no forge merge command. Every other GitHub invocation is
byte-identical to the base commit.

Closing that hole honours the existing rule rather than departing from it. The
file header already forbids --repo and -R because the repository must come
only from the URL, so a bundled cluster carrying a repository override was
never legitimate behaviour to preserve: it was that guard being evaded.
Redirecting a merge to a repository the URL does not name is exactly what the
guard exists to prevent.

The refusal is already pinned on both paths by the existing case
test_bundled_repo_override_args_refuse_before_recording in
tests/fm-pr-merge.test.sh. On GitHub ("-dR wrong/repo") and on GitLab ("-yR
https://other.example/g/p") it asserts exit 1, the refusal wording, no pr= in
the task meta, no armed merge poll, and no forge merge command invoked, with a
control case proving a cluster that carries no repository override still
reaches the forge. No duplicate assertion was added. Both assertions were
confirmed to have teeth by narrowing the guard back to a bare -R and watching
each path fail.

This commit carries no file change: the guard and its coverage landed in
614853d, and this message exists so the pull request description states the
narrowing.

* no-mistakes(document): fix README pointer for GitLab watch and merge doc

* no-mistakes: apply CI fixes

* fix(bin): record a lost relay connection instead of an unanswered turn (#2788)

* no-mistakes: apply CI fixes

* fix(bin): drop a private record citation and narrow the review rule

Three corrections to the spoken interface that landed in #2767, plus one
fix carried over from that branch after its pull request had already been
merged.

The confidentiality fix. The module docstring of bin/fm-voice-relay.py
cited a private, gitignored fleet record by exact path and section number.
That widens what this public repository points at, and it cannot resolve
for any reader here, because the path has never been in the repository.
Both traps it pointed at are already described in full in the list
immediately below it, and docs/voice-relay.md carries the same two for
operators with no citation at all, so the pointer is removed and no claim
is weakened by losing it. Two comments that referred to "the survey" as
though it were something a reader could open are reworded the same way.
Neither exposed a path, so that half is comprehensibility rather than
confidentiality.

The review rule. .greptile/rules.md is kept, because its conditions are
right and deleting it would leave the next reviewer to re-litigate a
decision already argued out. What was wrong with it is narrower than its
existence: it read as settled repository policy, when whether VISION.md
itself should be reconciled is an open question belonging to the captain.
One sentence now says so, and says that the conditions listed below it are
what the interpretation depends on. That narrows the claim rather than
widening it.

The carried-over fix. The first commit on this branch is 7f98e797 from
fm/voice-relay-build-v4, taken verbatim rather than rewritten. It closes
the window where a transport failure was recorded and then erased, so a
run could be emitted as answered false with relay_error null. That matters
more than it looks: relay_error is the field that keeps an infrastructure
failure from being averaged into a latency figure, so the failure mode is
a dead connection wearing the costume of a slow reply. It landed fifteen
minutes after #2767 merged and so never reached the default branch.

* no-mistakes(review): name a reason on every unanswered-turn close path

* no-mistakes(review): guard the downlink body and pin frames to their turn

* no-mistakes(review): attribute reply audio to its own turn and tell endings apart

* no-mistakes(review): tell a cut-short reply from an unanswered turn

* no-mistakes(review): discard reply audio arriving after the output closes

* no-mistakes(review): count discarded reply audio on the speaker path too

* no-mistakes(review): keep a reason off a turn already answered in full

* no-mistakes(review): say a reset cut a reply short, not that none arrived

* no-mistakes(review): read one turn's audio count once, and hush a tidy exit

* no-mistakes(document): fix stale session-end relay_error claim in voice-relay guide

* fix(composer): stop a blocked pi pane from proving an empty composer (#2811)

A pi worker parked on an interactive prompt - a permission dialog, a
question menu, a trust dialog - reports agent_status=blocked, because it
is waiting on a human keystroke. Pi draws that menu above its separator
pair, so the composer region between the rules is blank and structure
alone looks like a free composer. _fm_composer_pi_verdict admitted
blocked alongside idle and done, so the shared classifier reported an
affirmatively empty composer for exactly the pane where typing is unsafe.

Every "is it safe to type here?" consumer reads that verdict and proceeds
only on an affirmative empty, so both are told yes on a parked prompt:
the away-mode injection guard in bin/fm-supervise-daemon.sh, and fm-send's
pre-type refusal. The keys then answer the menu instead of composing a
message - the highlighted default is selected, the text is discarded, and
the record attributes a decision to a human who never made it.

blocked now defers to unknown, which every consumer already treats as
fail-closed. idle and done still prove an empty composer, so ordinary
steering is unchanged, and Cursor is unaffected because its always-blocked
panes never reach this pi-only branch.

Regression coverage lands first at both levels: the verdict owner
(a blocked pi defers) and the herdr adapter (a parked pi prompt is not an
empty composer).

* fix(bin): require project clone roots during fleet sync (#2849)

* fix(bin): require a clone root before fleet-sync touches a project

Git repository discovery walks upward, so `git -C projects/<dir>` on a plain
directory nested under projects/ resolves to the enclosing repository - in a
firstmate home, the firstmate checkout itself. fm-fleet-sync.sh guarded its
candidates with `rev-parse --is-inside-work-tree`, which such a directory
passes, so every later git call read, pruned and fast-forwarded firstmate's own
default branch and reported it under the project directory's label. A running
session's AGENTS.md changed underneath it, and the report named a project that
had nothing to do with the change.

Require each candidate to be the root of its own work tree before any other git
command: compare `rev-parse --show-toplevel` against the directory's own
physical path. Both sides are physical, so a symlinked clone still compares
equal. Anything else is skipped by name, naming the repository that would have
been touched, and bootstrap relays that as a FLEET_SYNC line.

Regression coverage reproduces the wrong-repo fast-forward against a home nested
inside another repository, in both the whole-fleet and single-project forms, and
pins that a symlinked clone dir still syncs.

* no-mistakes(review): Keep enclosing fixture clean during clone-root regression

* fix(bin): retry transient Lavish poll interruptions (#2846)

* fix(procevent): retry a transient Lavish poll interruption quietly

A live Lavish listener can be cut short by the server with exactly

    error: Lavish Editor poll response was interrupted
    code: SERVER_ERROR

while the session's marks remain available. Firstmate registered raw
`lavish-axi poll` output, so the generic process-event runner captured
that transient response as a result and woke the whole fleet over what is
really an internal retry.

The Lavish adapter now registers its own listener command, which reruns
the published blocking poll up to 12 times at 5 second intervals for that
one exact two-line response. The match is deliberately narrow: real
feedback, ended and missing sessions, any other SERVER_ERROR, and the same
interruption still standing once the bound is spent all pass straight
through and are captured and announced as before. The retry is a Lavish
fact, so the generic runner stays adapter-agnostic.

`FM_LAVISH_POLL_RETRY_DELAY` is a bounded 0 to 60 second override for the
interval only, refused rather than rounded when malformed, so a test can
exercise the real bound without waiting it out.

* no-mistakes(review): Harden Lavish retry matching, validation, and cleanup

* no-mistakes(review): Bound Lavish retry staging and stabilize regression

* no-mistakes(document): docs: explain Lavish retry adoption

* no-mistakes(lint): Restore Lavish trap ShellCheck suppression

* fix(brief): stop the documented {TASK} fill from corrupting the Herdr gate (#2838)

The unguarded Herdr declaration quoted `{TASK}` in its own prose while the
scaffold instructs firstmate to replace every `{TASK}` placeholder. The
documented global replace therefore spliced the whole task body into the
middle of the safety gate's sentence, silently destroying the one contract
that exists precisely because the scaffold cannot inspect the task text.

Reword the gate to refer to the task text filled in above, leaving the
placeholder only at its genuine fill site. Rewording rather than renaming the
token keeps the unfilled-charter guards in fm-home-seed.sh and
fm-remote-home-seed.sh working unchanged.

Add a regression test that performs the documented global fill on ship and
scout scaffolds and asserts the body lands once and the gate survives.

* fix(bin): resolve the busy-state lock mtime with the platform's own stat form (#2837)

The writer lock's stale-lock branch read the lock's mtime with
`stat -f %m ... || stat -c %Y ...`. On GNU coreutils `-f` is filesystem
stat, so it consumed the format string as a path, complained on stderr,
printed a partial filesystem dump ("  File: ...") on stdout, and still
exited 0. The GNU form in the fallback therefore never ran, and the
following arithmetic evaluated the word `File`, aborting the writer under
`set -u` with "File: unbound variable".

fm-teardown.sh died there after returning the worktree, leaving
state/<id>.meta, .status, .busy-gen, .busy-state, .busy-state.lock/ and
.turn-ended behind. The surviving metadata kept the watcher monitoring an
endpoint whose agent was gone, so a finished task produced stale wakes
forever, and every re-run died identically because the abandoned lock was
never broken.

Detect the platform once and pick the right stat form, the pattern
bin/fm-watch.sh already documents, and treat any non-numeric result as
"just created" so a future portability surprise degrades to a lock-timeout
refusal rather than killing teardown mid-way.

* fix(stow): add opt-in pass horizon for memory decay (#2850)

* fix(stow): give memory decay a per-pass horizon so the clock fires

The tiered decay clocks were wall-clock only, while admission is per-pass:
each /stow admits the findings that pass produced. In a home that stows
daily those two rates diverge by the stow cadence, an entry the fleet keeps
exercising never reaches 30 days unreinforced, and memory only grows while
the pass reports decay evaluated.

Give each dated marker an optional unreinforced-pass counter and make both
tiers stale at whichever horizon comes first: 10 passes or 30 days for
aging, 3 passes or 7 days for perishable. Reinforcement clears the counter
and nothing else does, so the existing evidence-based restamp rule stays
the only way an entry renews its lease. An absent /N means zero, so entries
that stay exercised carry no extra marker bytes, and a rarely stowed home
keeps its current behaviour through the unchanged date horizon.

* no-mistakes(document): Align stow workflow with dual decay clocks

* fix(stow): make the per-pass decay horizon opt-in

The unreinforced-pass horizon shipped as a new default archival cadence,
which is a product default rather than a restoration of the existing
wall-clock contract. Keep the 30-day and 7-day horizons as the only
default clock, and put the 10-pass and 3-pass horizons behind an explicit
opt-in: config/stow-pass-horizon for the firstmate home, and the file's
own header pointer for the public skill.

With the opt-in absent no counter is written and no counter is read, so a
home that does not ask for it decays exactly as it does today.

* no-mistakes(review): Preserve frozen counters and correct archive provenance

* test(watcher): stop fixture confirmation budgets racing real child startup (#2876)

tests/fm-watcher-lock.test.sh passed in isolation but failed intermittently
under full-suite and ambient concurrent load. bin/fm-watch-arm.sh computes its
confirmation deadline immediately after forking the real child watcher, so the
child's entire fork, exec, lock acquisition and beacon publication has to land
inside that wall clock. Two cases shrank that budget to one second, leaving a
two-second window for work measured at 3.1-4.9s under CPU oversubscription, so
the arm honestly reported "FAILED - no live watcher with a fresh beacon" and
their premises collapsed. A third case ran on the production budget, but its
child must also execute a registered check before exiting: measured at 1.9-2.3s
idle and 9.1-13.1s under load, against an 11s budget.

The two cases that must confirm a real child now hold the arm to production's
own budget instead of a shrunken fixture one, the immediate-wake case gets an
explicit budget with headroom over its measured loaded cost, and the two waits
for the arm's typed failure are sized off the largest production default rather
than a fixed eight seconds.

No bin/ change and no default behavior change: the lock's fail-closed semantics,
SIGSTOP handling, stale-heartbeat detection and the arm's typed failures are
untouched. Verified 4/4 green at 3x CPU oversubscription (loadavg 75-80) after
3/3 red before the change, and CONTRIBUTING.md records the convention.

* fix(bin): deterministically order remote tool paths (#2870)

* fix(bin): order discovered tool installs by the shell's own expansion

fm_remote_job_compose_operator_path built the asdf and mise install
directories with `compgen -G`, which does not sort. Bash sorts glob
matches in pathexp.c, on the shell's own pathname-expansion path only;
`compgen -G` reaches the same glob_filename through pcomplete.c, which
sorts nothing. On bash 3.2 (macOS /bin/bash) and every bash before 5.3
that handed the composition raw readdir order, so which install of a
multi-version tool a remote job resolved was decided by directory order
on disk rather than by this composition.

Expand the globs at the call sites and let the function take the matches,
so the composition and the documented portable-PATH contract are the same
operation. Quoting the account home at the call site also stops a home
whose name contains glob metacharacters from being reinterpreted.

The colocated regression pins both the order and the mechanism: bash 5.3
moved sorting into the glob library, so an order-only assertion cannot
see the defect there.

* no-mistakes(review): Remove source-reading PATH regression guard

* fix(bin): prevent routed secondmate work from stranding (#2848)

* fix: surface stalled secondmate queues and wake handoffs

* no-mistakes(review): Make handoff wakes retryable and stall alerts crash-safe

* no-mistakes(review): Prevent duplicate handoff wakes and cover remote delivery

* no-mistakes(review): Serialize local handoffs and preserve pre-move wake intent

* no-mistakes(review): Serialize teardown with handoffs and retain remote wake confirmation

* no-mistakes(review): Reconcile correlated handoff wake delivery after crashes

* no-mistakes(review): Keep failed wakes retryable and isolate stall receipts

* no-mistakes(review): Reset known-undelivered wake attempts for durable retries

* no-mistakes(review): Refuse duplicate sends for unresolved delivery attempts

* no-mistakes(review): Atomically restore retryability after reconciled send failures

* no-mistakes(review): Serialize delivery confirmation with reconciliation

* no-mistakes(document): Document routed wake and stall supervision

* no-mistakes(lint): Fix ShellCheck expansion and subshell warnings

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes(review): Retire stale wake state and defer pre-move wakes

* no-mistakes(review): Secure markers, bind batches, and preserve teardown routes

* no-mistakes(review): Preserve unresolved prepared wakes across unrelated handoffs

* no-mistakes(review): Preserve prepared wakes before unrelated moving handoffs

* no-mistakes(document): Document prepared wake batch ownership

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes(review): Make local wake retirement recoverable

* no-mistakes(document): Clarify handoff recovery and teardown documentation

* fix: make macOS inbox test path portable (#2857)

* feat(bin): deliver local steers through durable task inboxes (#2856)

* feat(bin): steer local tasks by durable inbox record plus constant doorbell

Stage 1 (local steers) of the captain-adopted reframe in
data/fm-send-reliability-reframe-s1/report.md: an ordinary fm-send text
steer to a task recorded in this home is appended as a sequenced durable
record under state/<id>.inbox/ and the terminal receives only one constant
self-describing doorbell line, best-effort. The worker acknowledges by
moving the record into handled/; the watcher re-rings an unacknowledged
message on an idle pane and escalates once as an ordinary stale wake.
--resolve-key closes decisions at enqueue time, because the durable
enqueue IS delivery to the task's record. bin/fm-task-inbox-lib.sh owns
the record format, doorbell line, and re-ring ladder.

The typed plane remains for what must reach the terminal itself:
lifecycle keys, harness-native slash and codex $-skill invocations,
explicit backend targets, and the remote secondmate leg (unchanged until
the remote inbox leg ships separately). The composer classifier is
demoted from delivery proof to an advisory ring guard that skips only on
a proven pending verdict.

Verified live against claude, codex, opencode, pi, grok, and muse: each
real worker read its record, acted, and acked with the mv
(docs/verification/runtime-backends.md "Steering-inbox doorbell").

* docs(verification): flag the grok 1.0.5 composer-matrix staleness observed by the doorbell run

* test(captain-hold): read the chat-channel answer from the durable inbox record

* test: migrate fm-control's marker contrast to the inbox record and fix macOS wc padding in the tool-update suite

* no-mistakes(review): Harden inbox locking, teardown races, and acknowledgements

* no-mistakes(review): Serialize watcher actions with inbox acknowledgements

* no-mistakes(review): Bound metadata locking and tighten acknowledgement rechecks

* no-mistakes(review): Preserve exact inbox bytes and harden delivery recovery

* no-mistakes(review): Harden watcher bookkeeping against concurrent inbox teardown

* no-mistakes(document): Update inbox and typed-plane documentation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* revert(pipeline): keep parser-native secondmate marking and the both-failed exit out of stage 1

The CI monitor's fix changed the secondmate marking contract for
parser-native invocations (appending the marker after the text) and
softened the both-commit-and-marker-failed branch to exit 0. The merge
authority ruled the marking question out of scope for this stage-1
transport PR (follow-up: fm-send-secondmate-harness-invocation-r1) and
ruled the both-failed case a loud nonzero local failure. Restore both,
keeping the monitor's legitimate migrations and hardening.

* no-mistakes(document): Document inbox and typed-plane boundaries

* no-mistakes(document): Scope backend transport docs to typed plane

* no-mistakes(document): Clarify inbox attempt-budget documentation

* no-mistakes: apply CI fixes

* fix(send): the durable record alone governs the inbox exit status

Captain-refined ruling on the F2/Greptile finding: the durable inbox
record is what delivers the steer, so pending-reply bookkeeping trouble
after a successful enqueue never exits nonzero - a resend-inviting status
would make automated callers enqueue the delivered instruction again
under a new sequence. With the recovery marker stored the watcher
reconciles silently; with the commit and marker both lost the send
surfaces a distinct reply-tracking-degraded do-not-resend warning and
still exits 0. Nonzero remains only where nothing was delivered (or a
decision close needs its manual command). Regression: record durable +
both bookkeeping writes lost -> exit 0, one record, no duplicate.

* no-mistakes(review): Preserve inbox ordering with drain-all doorbells

* no-mistakes(review): Surface unwritable inbox ladder bookkeeping

* no-mistakes(review): Silence ladder failures after inbox acknowledgement

* no-mistakes(document): Update steering inbox documentation

* no-mistakes: apply CI fixes

* feat(bin): add fast local lint mode (#2891)

* feat: add fast local lint mode

* fix: preserve complete fm-lint help

* fix: isolate fast lint mode

* no-mistakes(document): Clarify lint mode documentation ownership

* no-mistakes: apply CI fixes

* feat(bin): deliver remote steers through durable inboxes (#2901)

* feat(bin): deliver remote secondmate steers through durable task inboxes

Stage 2 of the inbox+doorbell steer channel (stage 1: #2856). A remote
secondmate steer now crosses fm-on.sh as a durable record written
idempotently into the remote home's steering inbox plus a best-effort
remote doorbell, and the last typed-payload steer transport is deleted:

- fm-remote-secondmate-control.sh cmd_send writes the record via the new
  fm_task_inbox_write_idempotent and rings the doorbell; it no longer
  types the payload through an inner fm-send at an explicit pane target.
- fm-send.sh routes every remote text steer (harness-native included,
  which marking already reduced to chat) onto the remote inbox leg,
  retries the identical leg once on ssh 255, closes --resolve-key
  decisions at enqueue for remote too, and preserves a marked request's
  reply expectation when completion stays unknown. The exit-3-as-
  delivered remap, the 255 do-not-resend trap, and the remote typed
  submit block are removed.
- fm-task-inbox-lib.sh owns the idempotent enqueue: an exact-body re-run
  lands on the existing record, handled or not, so an ambiguous
  transport can always be safely re-run.
- Tests pin the new contract end to end (record + doorbell + no typed
  payload across ssh, one-record idempotence under an ambiguous
  transport, enqueue-time decision close, loud real failures, and the
  deleted typed-payload behaviors gone), and AGENTS.md plus
  docs/remote-secondmates.md describe the remote leg's new semantics.

* no-mistakes(review): Harden remote inbox delivery against lifecycle races

* no-mistakes(review): Enable correlation-preserving remote steer resends

* no-mistakes(review): Fail closed on stale correlation resends

* no-mistakes(review): Include home context in remote resend commands

* no-mistakes(review): Lock and revalidate remote parent routes

* no-mistakes(document): Clarify remote steer retry documentation

* no-mistakes: apply CI fixes

* feat: add persistent Pi supervision branch (#2858)

* wip: forked supervision on Pi (checkpoint before docs)

* fix(pi-branch): harden mirror delivery, fallback encoding, and session replacement

Peek-then-shift mirror flush so a failed append retries instead of dropping;
durable mirror cursor commits only after delivery into the branch;
the main fallback wake is operational-encoded like every watcher injection;
session_shutdown quiesces the generation and session_start re-arms, so /new
and /resume no longer kill the branch permanently. Registers the extension in
the strict typecheck, adds the dispatch handshake test, the branch extension
suite, the bash-level regression suite, the session-start replay test, and
the opt-in real-SDK live guard.

* test(fixtures): carry the branch-dispatch lib and lease lib into isolated fixtures

The watcher extension now imports lib/fm-branch-dispatch.ts and fm-teardown
sources fm-lease-lib.sh, so every fixture that copies or symlinks those
files in isolation gains the new sibling.

* no-mistakes(review): Prevent shutdown wake loss and serialize lease claims

* no-mistakes(review): Durably hand off wakes and retain portable leases

* no-mistakes(review): Require durable reports and clear disposed branch leases

* no-mistakes(review): Enforce per-wake outcomes and quiescent lease cleanup

* no-mistakes(review): Require wake acknowledgements and tighten branch lifecycle boundaries

* no-mistakes(review): Require complete acknowledgements and replay cleanup failures

* no-mistakes(review): Bind supervision to lock ownership and durable delivery

* no-mistakes(review): Activate branch lazily after session lock acquisition

* no-mistakes(review): Preserve undelivered mirror context across extension rebinds

* no-mistakes(review): Acknowledge startup replay only after main delivery

* no-mistakes(review): Isolate replay metadata from untrusted digest content

* no-mistakes(review): Reject duplicate reports for active wake sequences

* no-mistakes(review): Retain failed fallbacks and deduplicate outcome replay

* no-mistakes(review): Deduplicate durable outcomes and cache delivery receipts

* no-mistakes(review): Anchor wake sequence matching to outcome fields

* no-mistakes(document): Clarify Pi supervision durability contracts

* no-mistakes(lint): Fix ShellCheck issues in branch supervision scripts

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* refactor(pi-branch): collapse to confused-agent-grade guards per captain decision

Captain decision A: the lease/actor guards target the CONFUSED-AGENT threat
model bin/fm-gate-refuse-lib.sh already documents; adversarial-grade
separation is impossible in the shared-process design and is filed as
separate follow-up work. Rip out the machinery that chased it: the
generation fence and shell-provenance markers, the wrapper-tagged ancestry
walks, guard auto-claim with per-script release traps, the pending-wake
files and ack-receipt correlation (the durable wake queue already
re-presents anything unacknowledged), the delivery-receipt store with
contiguous cursor advancement, the session-start replay-metadata channel,
and the branch tool quiescence counters.

Keep the behaviors the board requires, each on its simplest implementation:
lazy per-action session-lock ownership (cold start activates after the lock
lands; a secondary session stays inert), mirror durability across extension
rebinds via the durable cursor, replay-exactly-once from the one read
cursor, the awaited operational-encoded fallback, per-generation stray-lease
cleanup, session-lock-bound lease liveness (a recycled pid or a non-Pi home
never honors a leftover lease), the loud accidental-override guards
(readonly actor prelude, cross-actor claim refusal), and the role-partition
refinements (no forced teardown, no direct relaunch for the branch).
Default-on-for-Pi is unchanged.

* no-mistakes(review): Enforce lock ownership and serialize lease mutations

* no-mistakes(review): Synchronize guard cleanup and bind leases to lock owner

* no-mistakes(review): Report outcomes before acknowledging durable wakes

* no-mistakes(review): Restrict leases to Pi and instruct main claims

* no-mistakes(review): Reject malformed lease locks and torn outcome tails

* no-mistakes(review): Validate complete outcome tails before appending

* no-mistakes(review): Guard branch side effects across session replacements

* no-mistakes(document): Update Pi supervision durability and lease documentation

* no-mistakes(lint): Suppress intentional nested-shell expansion warning

* no-mistakes: apply CI fixes

* fix(pi-branch): authorize lease releases by caller

* fix(lint): break redundant source-analysis path in fm-lease-lib.sh

fm-lease-lib.sh's lazy fallback source of fm-wake-lib.sh gave ShellCheck's
--external-sources traversal a second path into an already 1540-line file
that fm-send.sh and fm-teardown.sh also source directly, blowing up the
recursive analysis past CI's lint timeout. Mark it a source=/dev/null
analysis boundary, matching the existing fm-task-inbox-lib.sh convention.

Also restores bin/fm-lint.sh and tests/fm-lint.test.sh to the shared
serial-lint definition (dropping an unrelated parallel-sharding change
that was itself hanging and masked this root cause).

* no-mistakes(document): Correct lease caller-authorization documentation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* fix(bin): parallelize startup network sweeps (#2927)

* feat(bin): parallelize session-start remote secondmate network sweeps

Run per-secondmate liveness and convergence probes concurrently and overlap clone refresh, while replaying each mate's fail-closed diagnostic in original order. Ignore scratchpad* so untracked scratch no longer blocks remote sync.

Co-authored-by: Cursor <cursoragent@cursor.com>

* no-mistakes(document): Document parallel startup network sweeps

* no-mistakes(lint): Fix empty environment assignment lint warning

* no-mistakes: apply CI fixes

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* test: handle absent watcher wake queues (#2845)

* fix(tests): count declared-pause wakes without crashing on an absent queue

The exited-declared-pause case counts queued stale wakes by handing
state/.wake-queue straight to awk. A watcher that queues nothing never
creates that file, and awk aborts on a missing path before its END rule
runs, so the count collapses to the empty string. The next comparison
then fails as an integer-expression error and surfaces as a wake flood
with no number, hiding the real contract breach the following grep names.

Read the queue the way the drain-count assertion at the end of this file
already does: silence awk's open error and default an absent queue to
zero. Applied to all four counts in this case, including the live
external-decision gate pair whose queue an acknowledged drain can also
leave behind. An absent queue now reports "did not use the bounded
paused recheck", while a genuine flood still fails with its real count.

Fixes #2628

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* style(pi): distinguish routine and captain supervision merge notes by icon (#2934)

* style(pi): restyle supervision merge notes with a sailboat and matching pad

Secondary-session notes were flush against the TUI edge and fully tinted.
Use the sailboat prefix, Pi's default outputPad, boat-only color, and dim remainder so they sit like real messages.

* style(pi): distinguish routine and captain merge notes by icon only

Visible notes now lead with a sailboat or anchor, then only the dim outcome.
Drop the branch-merged wording and verdict brackets so the icon is the only kind signal.

* docs(pi): add the approved multi-brain architecture poster (#2938)

The markdown contract stays the owner; the still is only the visual of the idea.

* feat(pi): default branch supervision and route heartbeats (#2939)

* fix(bin): bound remote job worker supervisor restarts (#2942)

* fix(bin): bound remote worker supervisors

* no-mistakes(review): release incumbent supervisor before starting its replacement

* no-mistakes(review): wait out a healthy same-root supervisor instead of replacing it

* no-mistakes(review): narrow remote worker change to restart accounting only

* no-mistakes(document): clarify supervisor restart guard is a lifetime total

* fix: safely split supervision wake handling by actor (#2953)

* feat(bin,pi): per-actor wake consume, silent success gating, merge-poll dedup

Three related fixes to the shared wake-drain and Pi supervision-branch
dispatch machinery so a routine success is never main-blocking and a
mixed queue can safely split between actors.

1. Successful routine results no longer create main-blocking wake rows.
   fm-startup-network.sh only enqueues a check: startup-network wake when
   the deferred result is actionable (state is not "done", or the report
   carries a bootstrap-diagnostics actionable prefix); a clean success
   stays durable in the report file without ever waking the agent.

2. Per-actor wake-drain consume contract. bin/fm-wake-drain.sh now scopes
   presentation and --ack-through to the current actor
   (bin/fm-lease-lib.sh's fm_lease_actor): main keeps the original
   whole-queue cutoff behavior, unaffected. A branch actor
   (FM_SUPERVISION_ACTOR=branch, set only inside the Pi supervision
   branch's own bash tool calls) is scoped to an explicit eligible-row
   snapshot instead of a cutoff comparison, so it can never remove a row
   it was not granted - the fix for the swallow risk that used to force
   an all-or-nothing whole-queue fallback to main.
   .pi/extensions/lib/fm-branch-dispatch.ts's scopeForUnreadWake is the
   single owner of eligibility: a check-kind row (merge-confirmation
   polls, Relay mentions, credential/auth failures) is now excluded
   rather than vetoing the whole scan for a non-heartbeat wake, while a
   heartbeat review keeps its original all-or-nothing rule unchanged.
   writeEligibleRowsSnapshot publishes the exact eligible sequence
   numbers before every branch prompt; fm-primary-pi-watch.ts's offer
   still refuses a check-kind trigger outright so a main-only close is
   never itself routed to the branch.

3. A repeat identical merged-PR-poll result for an already-notified task
   is absorbed instead of enqueued again. A poll's own retirement state
   is scoped to one registration and cannot see a prior registration's
   outcome, so a task re-registered after its merge was already surfaced
   would otherwise wake main a second time for the same event.
   bin/fm-pr-lib.sh's new per-task pr-poll-merge-notified marker survives
   across re-registrations to catch that case; the first notification for
   a task still reaches main unchanged.

Regression tests colocated in tests/fm-startup-network.test.sh,
tests/fm-wake-queue.test.sh (including the mixed-queue no-swallow
property), tests/fm-pi-branch-extension.test.sh, and
tests/fm-pr-check-security.test.sh. docs/watcher-continuity.md and
docs/pi-supervision-branch.md updated for the new contracts.

* no-mistakes(review): Bind merge deduplication to canonical PR identity

* no-mistakes(review): Serialize wake row ownership across main and branch

* no-mistakes(review): Bind branch grants and deduplicate within actor claims

* no-mistakes(review): Fallback main-owned wake claims to main delivery

* no-mistakes(review): Clarify silent startup success guidance

* no-mistakes(review): Release residual branch grants after settled prompts

* no-mistakes(review): Reject truncated wake rows as corrupted

* no-mistakes(document): Document per-actor routing and silent startup success

* no-mistakes(lint): Fix ShellCheck findings in wake grant and startup test

* no-mistakes: apply CI fixes

* fix(pi): hide branch outcomes tool rows in Calm (#3024)

* Hide branch outcome tool in Pi Calm

* no-mistakes(review): Preserve stock outcomes rendering and document tool audit

* no-mistakes(review): Document branch read tool audit disposition

* no-mistakes(review): Match stock outcomes output sanitization

* no-mistakes(document): Document Calm custom-tool visibility

* fix: bind no-mistakes attestations to PR head (#3027)

* fix: delegate no-mistakes PR gate to pinned action

* no-mistakes(document): Document commit-bound no-mistakes attestations

* feat(pi): add persistent supervision branch model selection (#3028)

* feat(pi): let operators pin a cheaper supervision-branch model

Supervision is an easier job than the captain's own conversation, so the
Pi supervision branch does not need main's model. A new /supervision-model
command opens Pi's own selector over Pi's own catalog of credentialed
models, plus a "Follow main" entry, and persists the pick as one
<provider>/<model-id> line in this home's gitignored
config/supervision-branch-model. Firstmate keeps no model catalog of its
own.

The branch resolves the pin at every branch build - the first wake of a
cold start and the reopen after /new, /resume, /fork, or reload - so the
choice survives all of them, and picking also releases the live branch so
the next wake reopens the same persistent branch conversation under the
new model. An absent, unreadable, or unparseable file means no pin and
keeps today's behavior byte for byte: no model option is passed and Pi
picks the branch's model exactly as before.

A pin naming a model Pi cannot hand back is never silently downgraded
onto main's model: the branch refuses to build and the wake falls back to
the captain-facing main path naming the unusable pin, which is the
extension's existing failure direction.

The choice is home-local and not part of secondmate inherited
configuration, matching the Pi Calm preference precedent.

docs/configuration.md owns the operator-facing schema. Portable
regressions cover pin-present on create and reopen, pin-absent default,
the command's persistence, cancellation, and live rebind, and both
unusable and unparseable pins. The opt-in real-SDK guard proves the
vendor surface the pin reads and that an explicit model wins over the
model a reopened session recorded.

* no-mistakes(review): Fix supervision model runtime and rebind races

* no-mistakes(review): Restrict supervision picker to isolated runtime models

* no-mistakes(document): Document supervision branch model selection

* fix(pi): make the supervision model pin authoritative on every reopen

Clearing the pin with "Follow main" removed the file but the next branch
build reopened the persistent branch session with no explicit model
override, so Pi restored the model that session had recorded - the old
pinned model - while the command reported that the branch now follows
main. The same gap meant an absent pin did not reliably mean
same-model-as-main once a home had pinned once.

The pin file's current state now decides the model on every branch build,
create and reopen alike, overriding Pi's session-state restore. With a
pin, that model. With no pin, main's own current model is applied
explicitly, tracked from the contexts Pi already hands the extension plus
its model_select event, since the branch is built at wake time with no
context of its own. Only when main's model is unknown, or this home's
stored credentials cannot run it in the isolated branch runtime, does a
build fall back to passing no override at all, which is the behavior from
before the pin existed; the branch is never refused over model choice.

The command's notification now reports the model actually applied, and
says plainly when clearing the pin could not apply main's model instead
of claiming a change that did not take effect.

No credential handling changes: the branch still relies entirely on the
stored credentials its own runtime already holds, and the picker stays
restricted to models that runtime can resolve.

Colocated regressions cover pin present on create and reopen, clearing
the pin returning a reopened branch to main's model and specifically not
the old pinned one, an unparseable pin behaving as no pin, and the
unknown-main-model fallback to no override.

* no-mistakes(review): Make unpinned supervision follow main model changes

* no-mistakes(document): Correct supervision model documentation

* feat(pi): let /supervision-model pick branch reasoning effort (#3079)

* feat(pi): let /supervision-model pick the branch's reasoning effort

Supervision is an easier job than the captain's own conversation, so the
Pi supervision branch does not need main's reasoning effort any more than
it needs main's model. /supervision-model now settles both in one flow:
the existing model picker, then a follow-up effort picker built from Pi's
own supported thinking levels for the model just chosen. Firstmate keeps
no effort catalog of its own; the menu, the clamp, and the vocabulary all
come from Pi.

The pick persists as one line in this home's gitignored
config/supervision-branch-effort, independent of the model pin: a captain
may pin a model, an effort, both, or neither. The effort pin's current
state decides the branch effort on every branch build - the first wake of
a cold start and the reopen after /new, /resume, /fork, or reload - and
overrides Pi's restore of whatever level a reopened branch session
recorded, which is what keeps "Follow main" honest. With no pin, main's
own current effort is applied explicitly and followed live through Pi's
thinking_level_select event, the same way an unpinned branch already
follows main's model, and the two selections now share one build revision
so either change invalidates an in-flight build.

The branch is never refused over effort. Pi owns the clamp, so a pinned
level the branch's model cannot run becomes that model's nearest supported
level while the captain's raw pick is kept for a model that supports it,
and the command reports the level the branch will really run at rather
than the raw pin. A token Pi would not recognize at all is treated as no
pin rather than passed to that clamp, which would otherwise collapse a
typo into the model's lowest level. Only when main's effort cannot be read
either does a build pass no effort override at all, which is the behavior
from before this file existed.

Pi's own effort vocabulary is pinned by a bidirectional type assertion
against Pi's getThinkingLevel return type, so the tracked strict typecheck
against the installed package fails the moment Pi adds or removes a level.

docs/configuration.md owns the operator-facing schema for both pins.
Portable regressions cover the pin on create and reopen, model-only and
effort-only pins working independently, clearing a pin returning the
branch to main's effort, live-follow of a mid-session change, the clamp,
an unrecognized token, the unknown-main-effort fallback, and the command's
two-step flow, persistence, cancellation, and honest reporting. The opt-in
real-SDK guard proves the vendor surface all of that rests on, and also
repairs a pre-existing gap that left it unable to load the extension at
all.

* no-mistakes(review): Resolve effective branch effort honestly

* no-mistakes(document): Clarify Pi-owned effort picker behavior

* fix: keep routine supervision noise out of captain chat (#3093)

* fix(supervision): silence empty board closes and decouple the heartbeat

Two unrelated sources of noise put routine supervision events in the
captain's chat.

An empty Lavish board close - the captain reads a review surface, says
nothing, and closes it - became a check wake whose entire content was
that nothing happened. Suppress it at its source instead of routing it
anywhere: the generic runner gains a `silent` adapter seam mirroring the
existing `terminal` one, and the Lavish adapter answers it for exactly
one positively-determined shape, an `ended` session carrying no queued
content block. A silenced result is recorded durably handled so it does
not return on a later reconcile. Everything else announces unchanged - a
`Send & End` close carrying the captain's real answer, an `ended` result
still carrying content, a waiting or missing session, an unreadable
result, and every adapter that implements no `silent` command at all.
The keyed-answer feed is untouched, so suppressing an announcement never
suppresses the captain's own answer.

A fleet heartbeat was deferred to main merely because some unrelated
check row happened to be sitting unread, which put a routine fleet
review in the chat for a reason that had nothing to do with the fleet. A
check row is permanently main-owned, so it is now excluded from a
heartbeat claim rather than vetoing the scan, exactly as in every other
mode. What all-or-nothing guarantees is unchanged: the branch takes
every branch-ownable unread row or none of them, and an unresolvable
task-local row, an unknown row kind, or an unreadable queue still defers
the whole review to main. Main is still woken for the check on that
check's own triggering close, so nothing starves.

Main-only classes are unchanged and now each covered by a test: Relay
mentions, credential failures, merge confirmations, real board answers,
and watcher-failure repair. The per-actor acknowledgement and
no-cross-swallow properties are untouched.

* no-mistakes(review): Fail closed on all Lavish content headers

* no-mistakes(review): Suppress false unacknowledged status for silenced results

* fix(bin): stop a correlation token from hiding and stranding decisions (#1967)

* fix(classify): read the decision fold through a correlation token

status_line_verb stripped a trailing [key=...] from a status line's prefix
but left everything else glued to the verb, so a line carrying the
correlation token bin/fm-pending-reply-lib.sh embeds and a secondmate
echoes back matched no arm of _fm_decision_fold_line. Such a line folded as
ordinary status in both directions: a needs-decision or blocked opener never
opened its key, and a resolved or captain-held closer never closed one. The
same glued verb also hid correlated done and blocked lines from
status_is_captain_relevant and status_is_terminal_verb, and let correlated
working and resolved lines leak through the free-text fallback the
nonterminal guard was meant to stop.

The verb parse now walks whole words and drops only a token of the exact
shape a firstmate library writes - corr=<16 hex>, plus the bracketed form
bin/fm-secondmate-report.sh emits - before or after the key token, unkeyed,
or doubled. An arbitrary name=value word is deliberately NOT skipped:
skipping unknown tokens would let free text carrying an equals sign reduce
to a bare verb and impersonate a transition, which is the takeover the
strict parse and _fm_decision_key_transition_allowed exist to prevent. A
prefix with no corr= substring is returned byte-for-byte as before, so every
line without a token keeps its exact historical verb.

FM_OPEN_DECISIONS_FOLD_VERSION goes to 3, because every cursor persisted
under the previous reading carries an open set computed while correlated
lines were invisible and must be rebuilt from byte 0.

Measured over a real 383-line status log: 254 lines keep byte-identical
captain-relevance, pause, terminal-verb and captain-held verdicts, and all
129 changed lines carry a valid token - 14 correlated done/blocked/
needs-decision lines become captain-visible, and 20 correlated
working/resolved lines stop being escalated on prose alone.

* fix(review): Captain, block token-first decision impersonation

* fix(document): Clarify normalized status verb ownership

* fix(classify): reconcile the correlation-token read with the tag-stop parser

Rebasing onto main put this change beside #2280, which made verb parsing
stop at the first "[name=value]" tag. Both edit status_line_verb with
different intents, so the resolution keeps both rules rather than letting
one overwrite the other:

- #2280's tag stop is kept verbatim and now owns every BRACKETED tag,
  including the "[corr=...]" form fm-secondmate-report.sh writes. The
  bracket-unwrapping arm this branch had added to the token test is
  therefore removed as unreachable.
- This branch's token walk is kept and narrowed to the UNBRACKETED token
  fm-pending-reply-lib.sh writes, which the tag stop does not reach.

Two consequences of standing beside #2280 rather than before it:

The fold version had collided at 4: #2280 spent it on the tag-stop
parser and this branch had spent it on the token read. A cursor
persisted under #2280's reading predates this one and must still be
rebuilt, so the version moves to 5.

A bracketed impostor is dropped from the malformed-token list. On main
today "resolved [corr=deadbeef] [key=victim]:" already reads as the bare
verb, as does "resolved [anything at all] [key=victim]:", because the tag
stop ends the parse at the opening bracket regardless of content. That is
#2280's reviewed contract; asserting otherwise here would narrow it. The
unbracketed impostors it owns stay strict and still fold as prose.

Adds a consumer test for the two verb-string case arms that postdate this
branch: fm-supervise-daemon.sh's transient-stale arm and
fm-crew-state.sh's map_log_state.

* fix(review): Captain: Seed cursor migration fixture with version four

* fix(document): Clarify voice status normalization ownership

* fix(bin): Cursor-Park unter Pi-Host ohne Cursor-Identität stilllegen (#3115)

* fix(bin): Cursor-Park unter Pi-Host stilllegen.

pi-cursor-sdk lädt .cursor/hooks.json in die Pi-Sitzung und parkte einen zweiten Watcher; das erzeugte rearm-resurface und brach laufende Rückfragen ab. Bei PI_CODING_AGENT=true beendet der Park sofort, native cursor-agent Primaries bleiben unverändert.

* fix(bin): Cursor-Park trotz PI-Leak nur ohne Cursor-Identität stilllegen.

Stand-down gilt nur bei PI_CODING_AGENT=true ohne CURSOR_AGENT und ohne CURSOR_INVOKED_AS. Handgestartete cursor-agent Primaries mit geerbtem PI-Marker parken weiter.

* no-mistakes(document): Document Cursor park Pi-host stand-down

* fix(bin): no-mistakes-Mindestversion auf 1.46.0 anheben.

Die PR-Attestierung verlangt ab 1.46.0 strukturierte Pipeline-Schritte; der Bootstrap-Floor blieb bei 1.31.2 und ließ zu alte Builds zu.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(pi): make supervision model picker searchable and scrollable (#3099)

* fix(pi): make /supervision-model's model list bounded and searchable

Pi's generic extension selector renders every option at once with no
search box, so a real eligible catalog ran off the top of the terminal.
The model step now draws the same rows through Pi's own SelectList - the
bounded scrolling primitive behind Pi's /model picker - with Pi's own
Input and fuzzy filter above it for search, keeping 'Follow main' first,
the branch-runtime eligibility filter intact, and the pick branch-only.

Pi's ModelSelectorComponent is deliberately not reused: its selection
handler writes the captain's default model through Pi's settings manager,
which would move main's conversation as a side effect of pinning the
branch. The effort step's menu is a handful of levels and stays on Pi's
plain selector dialog.

* no-mistakes(document): Clarify supervision picker documentation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes(document): Document searchable supervision model picker

* no-mistakes: apply CI fixes

* fix(bin): durably report merged pull requests (#3104)

* fix: make a landed merge leave a durable outcome

A merge was the one lifecycle event that left no record outside the
merging agent's memory. bin/fm-pr-merge.sh ended at the forge call, and a
home merging under standing authority never waits for the merge poll that
would otherwise confirm it, so three real merges reached the captain as
silence.

bin/fm-merge-outcome-lib.sh is the single owner of that record. A
secondmate home reports the landed PR upward on the same parent reply
channel its terminal-outcome backstop already uses; a main home records
it on the durable wake queue. The record is at most once per task and
canonical PR identity, and only a merge that actually landed produces one.

The merge poll feeds that same channel when it detects a merge this home
did not perform, so the captain's own forge merge and a merge firstmate
performed itself produce one consistent outcome instead of two reporting
paths. No new state file and no second watch path.

Two smaller gaps from the same failure:

- A mate charter listed its report triggers without naming a landed
  merge. Under standing merge authority nothing is ever "ready for
  review", so the enumerated list silently omitted the case that matters.
- A secondmate home seeded without its parent binding failed every
  terminal-outcome report for the same reason, and the diagnostic never
  named the binding. It does now.

* no-mistakes(review): Harden durable merge outcome reporting

* no-mistakes(review): Make merge race regression deterministic

* no-mistakes(review): Make merge outcomes retry-idempotent and forge-confirmed

* no-mistakes(review): Unify merge publication under canonical outcome marker

* no-mistakes(review): Publish merge outcomes before committing dedup markers

* no-mistakes(review): Document at-least-once merge outcome recovery

* no-mistakes(review): Use supported GitHub confirmation and update recovery docs

* no-mistakes(review): Preserve distinct merge wakes by PR identity

* no-mistakes(document): Document durable merge outcome semantics

* no-mistakes(test): Make merge outcome interleaving test deterministic

* no-mistakes(document): Clarify merge outcome documentation ownership

* fix(lint): keep the merge-outcome library an analysis boundary

bin/fm-watch.sh followed the new merge-outcome library's source graph,
which reaches the wake queue, PR identity, and secondmate parent
libraries. Expanding that inside an already-large lint root pushed
ShellCheck's external-source analysis past the bounded CI lint worker:
the Lint job was killed with SIGTERM after five silent minutes, twice,
having emitted no diagnostics at all.

Make it an analysis boundary, exactly as the transition and inbox owners
directly above and below it already are and for the same stated reason.
Coverage is unchanged because the library is a canonical lint root in its
own right and is still linted as one.

Measured locally: the watcher goes from not terminating within 120s to
9s clean, and the library alone lints in 1s clean.

* fix(bearings): preserve projections through inventory mismatches (#3129)

* fix(bearings): keep an inventory-mismatch home readable, and mark warnings as repairs

A backlog-vs-metadata inventory mismatch inside a secondmate home was being
reported as "we cannot read that home", which discarded that home's open
captain calls, queued work, landed work, and live workers from the whole
Bearings digest. The main home already treats the identical mismatch as a
harmless disclosure; this makes the secondmate path agree.

- fm-fleet-snapshot.sh: the invalidity gate now passes orphan_in_flight,
  unowned_current, and terminal_in_flight through the partial-structured
  carve-out alongside child_current_unavailable, so those homes keep their
  decisions, holds, queued, landed, and live work and leave unreadable[].
  missing_backlog and unstructured_current stay on the discard path, because
  there the backlog itself is untrustworthy.
- fm-fleet-snapshot.sh: the same three kinds no longer collapse the home's own
  classification to "unknown"; the real captain_decision / active_child_work /
  externally_held classification survives and invalidity carries the warning.
  An unavailable child state still collapses it, including when a mismatch
  masks it under strict-invalidity precedence.
- secondmate_landed.partial now keys on partial-structured trust rather than an
  unknown state, so an inventory-mismatch home is still disclosed as partial.

Ask the home that owns the wrong books to fix them:

- bin/fm-secondmate-reconcile.sh sends exactly one reconcile instruction per
  mismatch episode through the ordinary steering transport. A persistent
  mismatch keeps its episode identity and never re-nags; a changed mismatch
  earns one more ask; a repaired one is forgotten so a recurrence is asked
  about again. The parent never touches the mate's own files, and a failed send
  records nothing so the next run retries it.

Give integrity warnings their own look on the board:

- charted rows take an optional kind of "queued" (the default) or "warning".
  A warning badges "needs repair" instead of "waiting" and is excluded from the
  Charted Next count, so alarms stop reading as dispatchable queued work. No
  fifth board section, and every existing payload stays valid.

Tests pin the new policy behaviorally: the retained surfaces and classification
for all three mismatch kinds, the still-discarding unstructured_current and
missing_backlog cases, the once-per-episode reconcile ask through real durable
steering records, and the board rendering exercised through the shipped
template under a minimal DOM shim.

* no-mistakes(review): Make reconcile dedupe atomic and warnings non-dispatchable

* no-mistakes(review): Preserve reconcile identity and reject stale snapshots

* no-mistakes(review): Order snapshots uniquely and canonicalize episode identities

* no-mistakes(review): Add fire-and-forget reconcile and separate warning overflow

* no-mistakes(review): Exclude fire-and-forget from escalation and track reconcile background

* no-mistakes(review): Run reconcile enqueue inline across all adapters

* no-mistakes(review): Track reconcile clears across strict-invalidity homes

* no-mistakes(review): Persist reconcile transitions atomically

* no-mistakes(document): Document reconcile and fire-and-forget contracts

* refactor(bearings): replace the reconcile episode dedupe with a 4-hour cooldown

The reconcile ask needed to fire once per problem without nagging on every
recap. The episode-precise record that tried to do that had to be correct in
every direction at once - order two concurrent snapshots, tell a repair from a
new problem, and never lose a clear - and each direction it got wrong either
swallowed a nudge or sent a duplicate.

A per-home cooldown removes the whole class. One durable timestamp per home,
one nudge per four hours, and nothing to get stale, mis-order, or
mis-classify: a home in mismatch is asked once, later recaps stay silent, and
a mismatch still sitting there after the window earns one gentle re-nudge.

- bin/fm-secondmate-reconcile.sh: state/<id>.reconcile-nudged holds the epoch
  second of the last ask; FM_RECONCILE_COOLDOWN_SECONDS names the window. The
  episode identity, ordering generation, pending/clear transitions, and
  delivery-identity reuse are all gone. A known-undelivered send starts no
  cooldown so the next run retries it; an unconfirmed one does, because a
  duplicate ask is worse than one the mate may already hold.
- bin/fm-fleet-snapshot.sh, bin/fm-bearings-snapshot.sh: drop the snapshot
  `observation` monotonic identity, which existed only to order those records.
- bin/fm-teardown.sh: retire the cooldown record with the endpoint's other
  runtime artifacts, so reseeding a retired id is not silenced by its
  predecessor's window.

The inline durable fire-and-forget send is unchanged, and the projection fix
and the warning surface are untouched.

Tests follow the behavior: the cooldown suite now pins one ask per window, the
re-nudge after it, the four-hour boundary, per-home independence, and that the
ask stays out of a re-ring ladder that still rings an ordinary steer beside it.
The obsolete observation-ordering test is deleted with the machinery it covered.

* no-mistakes(review): Serialize reconcile cooldown commits with mate lifecycle

* no-mistakes(review): Reject stale reconcile snapshots across mate reincarnations

* no-mistakes(review): Start reconcile cooldown after delivery completes

* no-mistakes(review): Keep reconcile sends nonblocking and remove pending residue

* no-mistakes(document): Document reconcile skip and stale-endpoint behavior

* no-mistakes(lint): Fix reconcile test subshell lint warning

* no-mistakes: apply CI fixes

* fix(bin): reconcile markerless remote secondmates safely (#3140)

* fix(bin): stop dropping reconcile nudges for markerless remote secondmates

A persistent remote secondmate's parent-side state/<id>.meta never carries
spawn_gen: bin/fm-spawn.sh's spawn_remote_secondmate() is its sole writer and
never writes one, because that incarnation identity does not apply to a
remote route. fm-secondmate-reconcile.sh's row filter required a non-empty
spawn_gen matching an identifier regex, so every such row was silently
dropped before the per-row loop ever saw it: no sent/stale/failed line, no
cooldown record, nothing sent, and no trace of why.

Give a legitimately markerless persistent remote secondmate a safe substitute
identity - its recorded remote_host - instead of weakening the spawn_gen
check for rows that do have a generation:

- bin/fm-secondmate-reconcile.sh: carry host through the row projection for
  both fm-fleet-snapshot.v1 and fm-bearings.v1 documents, and admit an empty
  spawn_gen instead of filtering the row out. A new revalidate_identity()
  compares the sampled spawn_gen against current metadata when one was
  sampled (unchanged), or the sampled host against the metadata's
  remote_host when none was sampled and the metadata still carries no
  spawn_gen of its own. A row with neither a spawn_gen nor a host has no
  safe identity at all and fails loudly instead of vanishing, exactly the
  visibility the original bug lacked.
- Rows now join on the ASCII unit separator rather than @tsv: bash's
  IFS-whitespace read collapses consecutive tabs, which would have silently
  dropped a legitimately empty field again.
- bin/fm-bearings-snapshot.sh: thread host through the secondmate_reconcile
  projection so the fm-bearings.v1 path (the one bearings itself feeds to
  the reconcile hook) carries the same substitute identity.
- tests/fm-secondmate-reconcile.test.sh: end-to-end coverage through the real
  remote transport (fm-on.sh + fm-remote-secondmate-control.sh against a
  genuinely seeded remote home) for a markerless mate nudged once per
  cooldown window, a stale/replaced remote route refused exactly like the
  existing local spawn_gen case, and a row with no identity at all failing
  loudly rather than being swallowed.

* no-mistakes(review): Enforce markerless remote host identity during final delivery

* no-mistakes(document): Document markerless remote reconciliation safety

* fix(bin): hand a busy declared pause to the away-mode daemon once, undecorated (#3147)

* fix(watch): hand a busy declared pause to the away-mode daemon undecorated

While away mode is active the daemon owns triage and the watcher reverts to
one-shot, handing over plain wake identities the daemon classifies itself. The
busy-turn bound was the one stale path that did not: with afk active it ran the
wedge timer, so the daemon received a wake already decorated as a possible wedge.

That decoration outranks the daemon's own verdict. handle_wake escalates an
enriched wedge reason before its pause classification can apply, so a crew that
declared the wait itself - a `paused:` external wait or a verified captain-held
transfer holding a live foreground call - was wedge-escalated once per
FM_STALE…
lytv pushed a commit to lytv/mymate that referenced this pull request Sep 8, 2026
* fix(bin): strip every bracket tag, not just [key=...], from a status verb

status_line_verb only stripped a leading "[key=...]" token before the
colon, so a remote secondmate reply's leading "[corr=...]" correlation
tag stayed glued onto the returned verb word ("needs-decision
[corr=...]" instead of "needs-decision"). The open-decisions fold's
verb match then silently failed to recognize the line at all, so
fm-send --resolve-key refused to close a decision that was plainly
open on the status line.

Generalize the parser to strip every "[name=value]" tag before the
colon, in any order and count, so local and remote replies fold
identically.

* no-mistakes(review): Invalidate stale decision cursors after parser fix

* no-mistakes(document): Clarify status metadata verb parsing
lytv pushed a commit to lytv/mymate that referenced this pull request Sep 8, 2026
kunchenguid#1967)

* fix(classify): read the decision fold through a correlation token

status_line_verb stripped a trailing [key=...] from a status line's prefix
but left everything else glued to the verb, so a line carrying the
correlation token bin/fm-pending-reply-lib.sh embeds and a secondmate
echoes back matched no arm of _fm_decision_fold_line. Such a line folded as
ordinary status in both directions: a needs-decision or blocked opener never
opened its key, and a resolved or captain-held closer never closed one. The
same glued verb also hid correlated done and blocked lines from
status_is_captain_relevant and status_is_terminal_verb, and let correlated
working and resolved lines leak through the free-text fallback the
nonterminal guard was meant to stop.

The verb parse now walks whole words and drops only a token of the exact
shape a firstmate library writes - corr=<16 hex>, plus the bracketed form
bin/fm-secondmate-report.sh emits - before or after the key token, unkeyed,
or doubled. An arbitrary name=value word is deliberately NOT skipped:
skipping unknown tokens would let free text carrying an equals sign reduce
to a bare verb and impersonate a transition, which is the takeover the
strict parse and _fm_decision_key_transition_allowed exist to prevent. A
prefix with no corr= substring is returned byte-for-byte as before, so every
line without a token keeps its exact historical verb.

FM_OPEN_DECISIONS_FOLD_VERSION goes to 3, because every cursor persisted
under the previous reading carries an open set computed while correlated
lines were invisible and must be rebuilt from byte 0.

Measured over a real 383-line status log: 254 lines keep byte-identical
captain-relevance, pause, terminal-verb and captain-held verdicts, and all
129 changed lines carry a valid token - 14 correlated done/blocked/
needs-decision lines become captain-visible, and 20 correlated
working/resolved lines stop being escalated on prose alone.

* fix(review): Captain, block token-first decision impersonation

* fix(document): Clarify normalized status verb ownership

* fix(classify): reconcile the correlation-token read with the tag-stop parser

Rebasing onto main put this change beside kunchenguid#2280, which made verb parsing
stop at the first "[name=value]" tag. Both edit status_line_verb with
different intents, so the resolution keeps both rules rather than letting
one overwrite the other:

- kunchenguid#2280's tag stop is kept verbatim and now owns every BRACKETED tag,
  including the "[corr=...]" form fm-secondmate-report.sh writes. The
  bracket-unwrapping arm this branch had added to the token test is
  therefore removed as unreachable.
- This branch's token walk is kept and narrowed to the UNBRACKETED token
  fm-pending-reply-lib.sh writes, which the tag stop does not reach.

Two consequences of standing beside kunchenguid#2280 rather than before it:

The fold version had collided at 4: kunchenguid#2280 spent it on the tag-stop
parser and this branch had spent it on the token read. A cursor
persisted under kunchenguid#2280's reading predates this one and must still be
rebuilt, so the version moves to 5.

A bracketed impostor is dropped from the malformed-token list. On main
today "resolved [corr=deadbeef] [key=victim]:" already reads as the bare
verb, as does "resolved [anything at all] [key=victim]:", because the tag
stop ends the parse at the opening bracket regardless of content. That is
kunchenguid#2280's reviewed contract; asserting otherwise here would narrow it. The
unbracketed impostors it owns stay strict and still fold as prose.

Adds a consumer test for the two verb-string case arms that postdate this
branch: fm-supervise-daemon.sh's transient-stale arm and
fm-crew-state.sh's map_log_state.

* fix(review): Captain: Seed cursor migration fixture with version four

* fix(document): Clarify voice status normalization ownership
cipherholdingsllc added a commit to cipherlab-ai/firstmate that referenced this pull request Sep 11, 2026
…w scoring (#7)

* fix(cmux): classify borderless Claude composers (#2029)

* fix(cmux): classify borderless Claude composer

* no-mistakes(review): Normalize cmux NBSP prompts across locales

* no-mistakes(document): Document cmux borderless Claude composer classification

* docs(stow): generalize read-before-write in the public stow skill (#2091)

The public installer-facing stow skill scoped its classify-then-replace
discipline to TODO/BACKLOG items only, so findings routed to a memory file
had no stated rule against a blind append or a wholesale overwrite.

Step 6 now classifies every finding against the destination's current
contents as new, duplicate, superseding, or obsolete, and states the
considered replacement each classification implies. The outcomes follow the
tiered-memory contract already in the file: an obsolete entry is refreshed,
archived, or replaced in a way that preserves its fact, a duplicate folds
into the entry that already carries it, and a superseded body worth keeping
leaves through step 7's existing exits rather than a second recovery
mechanism.

* fix: resurface durable supervision work after re-arm (#2065)

* fix(watcher): resurface durable work after downtime

* no-mistakes(review): Make watcher rearm recovery durable and cursor-safe

* no-mistakes(review): Persist safe recovery markers across migration lock recovery

* no-mistakes(review): Retain stale lock when recovery marker publication fails

* no-mistakes(review): Preserve delivery-gap recovery and quarantine malformed markers

* no-mistakes(review): Serialize recovery consumption and report acknowledgment failures

* no-mistakes(review): Centralize recovery publication before clearing watcher evidence

* no-mistakes(review): Guarantee recovery evidence across queue and lock handoffs

* no-mistakes(review): Publish recovery evidence before durable wake commits

* no-mistakes(review): Replace recovery marker Perl dependency with Node

* no-mistakes(review): Keep interrupted wakes durable until handling acknowledgment

* no-mistakes(review): Add post-handling durable wake acknowledgements

* no-mistakes(review): Enforce post-handling acknowledgement across recovery and AFK return

* no-mistakes(review): Bind wake acknowledgements to recovery generations

* no-mistakes(review): Align wake regressions with generation-bound acknowledgements

* no-mistakes(document): Document durable re-arm recovery semantics

* no-mistakes(lint): Resolve ShellCheck warnings in recovery and watcher tests

* no-mistakes: apply CI fixes

* test(watcher): assert post-handling wake replay

* no-mistakes(review): Prevent successor loops and adopt legacy wake generations

* no-mistakes(review): Rearm durable wakes without recursive successor recovery

* no-mistakes(review): Align recovery tests with handling marker state

* no-mistakes(review): Delay handling transition until successor launch is established

* no-mistakes(review): Confirm wake handling only after successful prompt delivery

* no-mistakes(review): Acknowledge AFK wakes only after evidence publication

* no-mistakes(review): Prevent AFK wake loss before post-handling acknowledgement

* no-mistakes(document): Document durable wake acknowledgement semantics

* no-mistakes(lint): Suppress false positive for recovery action output

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* ci: measure Herdr automation on Windows runners (#2100)

* ci: add Windows Herdr automation spike

* ci: run Windows spike on its pull request

* fix: wait for Windows Herdr command output

* fix: run ANSI probe in pane shell

* ci: keep Windows Herdr spike manually triggered

* docs: clarify Windows Herdr spike verdict

* feat(ahoy): guide captains through open decisions (#2099)

* Add guided ahoy decision flow

* no-mistakes(document): Document guided Ahoy decision flow

* fix(stow): enforce startup-memory budget decisions (#2110)

* Harden stow memory budget policy

* Refine internal stow offload policy

* no-mistakes(review): Enforce shared-budget decisions and autonomous offload

* fix(spawn): refresh pooled worktrees from origin before launch (#2116)

* fix(spawn): refresh pooled worktree base

* no-mistakes(document): Document spawn base-freshness invariant

* no-mistakes: apply CI fixes

* fix(composer): unify safe classification across backends (#2102)

* refactor(composer): one shape owner behind thin capture adapters, whole matrix fixed

Consolidate every composer shape - bordered boxes (all families, geometry,
titled bottom borders), bare agent-glyph rows and their wrap regions,
opencode's left bar, and pi's identity-gated separator pair - into
fm_composer_classify_screen in bin/fm-composer-lib.sh. Adapters now
contribute only a capture and a declarative capability descriptor
(styled/cursor/identity/rows); capability differences change how confidently
a shape is judged, never what the shapes are, so a new harness shape is
teachable in exactly one place.

Correctness fixes landed as part of the consolidation (audit
data/fm-composer-consolidation-audit-s1):
- locale-safe Unicode-space normalization in the shared owner (closes the
  fleet-wide half of #1988; cmux's local byte-exact NBSP case deleted;
  naming converges with PR #1995's normalization primitive)
- muse's bare glyph joins the shared set, unbreaking muse on herdr/cmux/orca
- orca learns the borderless bare shape, drops its backward-paged composer
  window, and can no longer classify a stale startup banner as the composer
- tmux tolerates a titled bottom border, unbreaking grok steering
- the left-bar shape makes opencode readable on every backend
- zellij gets a real classifier through dump-screen --ansi, replacing the
  content-diff submit heuristic that could confirm an undelivered message
  and close a --resolve-key decision (the fleet's only false positive)
- fm-spawn's kimi launch-readiness regex (the fourth shape copy) now routes
  through the shared classifier

The strict blank-row posture applies fleet-wide (captain decision
blank-row-injection-posture): no positive container proof = unknown = defer,
replacing tmux's permissive blank-cursor-row rule. Away-mode injection was
re-validated end to end on real tmux (defer on partial input and unproven
rows, clean delivery with swallowed-Enter retry into proven-empty
composers). The tmux submit core gains a baseline-idle turn-started
conversion so pi steering stays confirmed while its working screen hides
the composer; busy conversion without that baseline remains forbidden.

Plain-capture backends now degrade a glyph row carrying trailing text to
unknown instead of a false pending, per the approved capability rule.

Portable regressions pin the full byte-capture matrix from the audit under
a UTF-8 locale and LC_ALL=C, the strict-vs-permissive divergence, and
deliberate signal separation; the opt-in live guard
(tests/fm-composer-matrix-live-e2e.test.sh) verified every installed
harness against the real classifier, recorded in
docs/verification/runtime-backends.md.

* no-mistakes(review): Fix Pi glyph ambiguity and complete profile matrix

* no-mistakes(review): Preserve bare verdict when Pi identity probe is absent

* no-mistakes(review): Harden composer structure and titled-border geometry

* no-mistakes(review): Require proven idle baseline and strict Zellij guard

* no-mistakes(review): Reject box bottom borders as composer input rows

* no-mistakes(review): Prove Zellij probe typing before classifier retries

* no-mistakes(review): Preserve Pi identity uncertainty and scan full left-bar drafts

* no-mistakes(review): Verify Zellij text lands before submitting

* no-mistakes(review): Scope Zellij typing verification to selected composer content

* no-mistakes(review): Verify Zellij pastes through composer-scoped content deltas

* no-mistakes(review): Prove wrapped bare Zellij pastes through composer extraction

* no-mistakes(review): Invalidate stale cursorless composers below dead shell prompts

* no-mistakes(review): Handle shell prompt placeholders in composer extraction

* no-mistakes(review): Classify cursorless bare continuation regions safely

* no-mistakes(review): Reject stale cursorless containers below live activity

* no-mistakes(review): Preserve prompt glyphs in wrapped Zellij pastes

* no-mistakes(review): Reject live shell rows during composer extraction

* no-mistakes(review): Preserve wrapped glyph continuations through submit retries

* no-mistakes(review): Scope idle placeholders to proven positions

* no-mistakes(review): Restore boxed placeholders and live prompt reanchoring

* no-mistakes(review): Fix Zellij placeholder and wrapped glyph paste proof

* no-mistakes(document): Align composer architecture documentation

* no-mistakes(lint): Fix ShellCheck warnings in composer refactor

* no-mistakes: apply CI fixes

* docs(verification): record the trusted-checkout live matrix rerun

The pipeline's isolated gate worktree is untrusted, so claude, grok, and
muse stopped at first-launch trust dialogs there (the guard refuses to
confirm them by design). This rerun from the trusted checkout at the final
validated head verified all six installed harnesses, the strict blank-row
deferral, and the hardened zellij false-positive probe live.

* no-mistakes(document): Align composer verification evidence

* no-mistakes: apply CI fixes

* no-mistakes(review): Restore proven box bottom-cursor classification

* no-mistakes(review): Preserve styled placeholder-like drafts as pending

* no-mistakes(document): Align composer safety and Zellij delivery documentation

* no-mistakes: apply CI fixes

* docs(verification): refresh the live matrix with the final-head trusted rerun

The post-validation rerun from the trusted checkout verified all six
installed harnesses at the branch's final head, including Claude 2.1.227
(auto-updated since the audit's captures) and Grok, which the untrusted
gate worktree could not verify past their first-launch trust dialogs.

* fix(spawn): gate Pi TUI mode by CLI capability (#2117)

* fix(spawn): gate Pi regular TUI flag by capability

* no-mistakes(review): Document conditional Pi TUI capability detection

* no-mistakes(review): Pin Pi probing and launch to one executable

* no-mistakes(review): Preserve literal pinned Pi paths and update documentation

* no-mistakes(review): Defer pinned Pi path insertion until final substitution

* no-mistakes(document): Document version-safe Pi launch probing

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* docs(vision): elevate experience, pain narrative, and distro virtues (#2147)

* docs(vision): elevate experience, pain narrative, and distro virtues

Fold the captain's public vision framing into VISION.md: peace of mind as a
primary goal, multi-session context-switch pain as the problem one interface
solves, clone-and-run setup ease, self-evolution including community, and
explicit harness/backend orthogonality. Reconcile experience-as-garnish into
experience-as-purpose and update aligns/resists accordingly.

* docs(vision): state the experience goal positively

Drop the negative "not a smart workflow / useful tool / impressive technology"
pretext. Lead straight into the positive experience north star.

* feat(bin): reconcile inactive terminal crew outcomes (#2167)

* fix: reconcile inactive terminal outcomes

* fix: stream secondmate summary inputs

* no-mistakes(review): Fix reconciliation locking and request delivery retries

* no-mistakes(review): Prevent retries after unknown request delivery

* no-mistakes(document): Clarify inactive reconciliation cadence and receipts

* no-mistakes(lint): Quote terminal status arguments in reconciliation tests

* refactor: simplify inactive outcome reconciliation

* no-mistakes(review): Bound inactive reconciliation scans with durable progress

* no-mistakes(review): Bound reconciliation and deduplicate recovery notices

* no-mistakes(document): Document inactive outcome reconciliation contracts

* no-mistakes(review): Reject relative local secondmate parent routes

* no-mistakes(review): Key terminal receipts by spawn incarnation

* no-mistakes(review): Stabilize legacy receipts and lock reconciliation snapshots

* no-mistakes(review): Fail closed on invalid secondmate identity markers

* no-mistakes(document): Document durable inactive-outcome reconciliation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* ci: raise Herdr test timeout (#2191)

* fix: refresh stale Pi instructions after compaction (#2163)

* fix(session-start): refresh drifted instructions on stale rebuilds

* test(session-start): prove Pi instruction refresh end to end

* no-mistakes(review): Fix stale instruction refresh and baseline integrity

* no-mistakes(review): Preserve true-start baselines across Pi continuations

* no-mistakes(review): Correct Pi continuation classification and live expectation

* no-mistakes(review): Correct Pi continuation coverage documentation

* no-mistakes(review): Fix read-only refresh and exact Pi session restores

* no-mistakes(review): Classify Pi create-if-missing sessions correctly

* no-mistakes(review): Classify named Pi sessions using immutable headers

* no-mistakes(review): Correct Codex interactive coverage diagnostic

* no-mistakes(document): Document immutable Pi compaction instruction refresh

* no-mistakes(document): Correct Pi refresh documentation and validation claims

* feat: add deterministic condition-to-action watcher (#2200)

* feat(bin): add deterministic condition->action watch adapter on the process-event channel

Register a (condition, action) pair once with bin/fm-procevent-when.sh and the
existing process-to-event runner polls the condition tokenlessly, fires the
action at most once on a stable true, and wakes firstmate exactly once with the
captured outcome - instead of burning an agent turn per re-check.

The pair is stored privately under state/when/ and hash-bound by a trust record
the same way fm-check-register.sh binds a custom check, so a mutated spec is
refused without executing anything. A durable exclusive fired marker claimed
before the action makes restarts and re-polls unable to double-fire; every
failure path (mutated spec, condition error past budget, expired deadline,
failed action, uncaptured earlier fire) ends in a terminal captured outcome
that wakes firstmate rather than a silent retry. Eligibility stays a firstmate
judgment: only exact, safe, reversible actions may be bound, and judgment-
needing or destructive actions keep the wake-and-decide flow.

* no-mistakes(review): Harden when watcher concurrency, deadlines, timeouts, and output

* no-mistakes(test): Bind watcher actions to registered executable bytes

* no-mistakes(document): Correct condition-action watcher documentation

* no-mistakes(document): Clarify outcome wake re-announcement

* no-mistakes: apply CI fixes

* fix(bin): honor a decision key stated after the verb colon (#2202)

The open-decisions fold only recognized a [key=<slug>] token between the
verb and the colon (needs-decision [key=x]: note). The common worker
shape with the colon first (needs-decision: [key=x] note) silently
folded its stated key into the shared "default" bucket, so two open
decisions could collapse into one record and fm-send --resolve-key <x>
refused to close the decision it plainly named.

A complete token at the head of the note is now an equivalent stated-key
position for every keyed verb, shared by the whole-file and incremental
folds through the one _fm_decision_key owner. The documented
before-colon position wins when both are present, a token deeper in the
note stays prose, a bare keyless line still folds to "default", and a
stated-but-malformed slug is rejected rather than rewritten to
"default". A consumed note-head token is stripped from the note so both
positions yield identical records, and the incremental fold version is
bumped so persisted cursors folded under the old interpretation are
rebuilt from the authoritative log.

Fixes #2109

* fix(bin): prevent watcher recovery acknowledgement livelock (#2212)

* fix(bin): keep a recovery acknowledgement valid across republication

A watcher cycle that opened and closed while the model handled its drained
wakes minted a fresh recovery generation, which invalidated the exact
acknowledgement the drain had just printed. That acknowledgement then consumed
nothing, so the marker stayed pending and every later arm spent its whole cycle
re-announcing the same recovery instead of supervising - a livelock the home
could not leave on its own.

A downtime publication now reuses the generation of an outstanding handling
episode, so a close during the handling window cannot orphan the printed
acknowledgement. The acknowledgement itself separates its two facts: queue-row
consumption is bound to the monotonic --ack-through sequence and always
happens, while only retiring the episode is bound to --recovery-generation. A
generation that moved on is a non-fatal result that names its own remedy
instead of a refusal that consumes nothing.

* no-mistakes(review): Preserve recovery generations and consume stale acknowledgements safely

* no-mistakes(document): Document sequence-bound recovery acknowledgements

* feat(fmx-respond): consume Relay conversation chains (#2206)

* feat(fmx-respond): consume in_reply_to_chain conversation context

The relay's poll payload can carry in_reply_to_chain, an oldest-first
transcript of the surrounding conversation, but the mention-handling
procedure only ever read the immediate in_reply_to parent, so referents
like "this" in a standalone mention stayed unresolvable even when
context was delivered.

Teach fmx-respond to read the chain when present (optional and
backward-compatible: often absent today, kind label not required),
resolve referents against the whole transcript, and extend the
untrusted-content framing to every chain entry including the upcoming
kind=history entries. Document the field's wire shape in
docs/configuration.md as the firstmate-side owner.

* no-mistakes(document): Document Relay chain context ownership

* fix: parse decision verbs before status metadata tags (#2280)

* fix(bin): strip every bracket tag, not just [key=...], from a status verb

status_line_verb only stripped a leading "[key=...]" token before the
colon, so a remote secondmate reply's leading "[corr=...]" correlation
tag stayed glued onto the returned verb word ("needs-decision
[corr=...]" instead of "needs-decision"). The open-decisions fold's
verb match then silently failed to recognize the line at all, so
fm-send --resolve-key refused to close a decision that was plainly
open on the status line.

Generalize the parser to strip every "[name=value]" tag before the
colon, in any order and count, so local and remote replies fold
identically.

* no-mistakes(review): Invalidate stale decision cursors after parser fix

* no-mistakes(document): Clarify status metadata verb parsing

* fix(bin): collapse duplicate supervision wakes (#2287)

* fix: collapse duplicate supervision wakes without losing legitimate updates

One remote-secondmate note produced two handling turns (a procevent check
wake published before autohandle, then a signal wake for the same mirrored
bytes), already-ingested replays such as a cursor-loss whole-log recapture
still woke with nothing to do, this home's own bookkeeping closes (fm-send
--resolve-key, the pending-reply escalation close, the captain-held
transfer) re-woke the session that wrote them, and turn-ended-only wakes
were annotated with already-announced status lines that looked like fresh
progress.

Dedup rules, each at its layer's one owner:
- fm-procevent.sh: an adapter may declare 'self-announcing'; the runner
  then applies first and publishes a check wake only for what remains
  unhandled. fm-procevent-remote-reply.sh declares it: the mirrored status
  append is the single announcement, so a fully applied capture publishes
  nothing and a byte-identical replay stays completely quiet. All other
  adapters keep strict publish-before-apply.
- fm-wake-lib.sh: fm_wake_signal_sig/seen_path/seen_current now own the
  watcher's signal signature and .seen-* marker format, plus
  fm_wake_status_append_self_announced, the guarded bookkeeping append
  that advances the marker only over exactly its own bytes and fails
  toward waking on any pending or interleaved foreign write.
- fm-send.sh, fm-pending-reply-lib.sh, fm-decision-hold.sh: bookkeeping
  closes go through that guarded append; escalation opens stay plain
  appends because a new blocker must wake.
- fm-wake-lib.sh annotations: a historical (turn-ended-only) row skips its
  status annotation only when the file's signature provably matches the
  seen marker; anything unannounced keeps annotating.
- fm-classify-lib.sh: a kind=secondmate task's status signal is never
  absorbed as provably-working, because that stream is the routed-reply
  channel the parent must read.

Also fixes a pre-existing exit-path deadlock the regression run reproduced:
a TERM inside a recovery-marker critical section left fm_lock_try_acquire
spinning against this same process's abandoned hold; a self-held lock is
now reclaimed (a subshell still waits on its parent's live hold).

Regression tests drive the real wake functions and executables in both
directions: each duplicate case collapses, while a new remote reply, new
decision, new blocker, merge result, failure, first status change, and a
later different note on the same task all still wake.

* no-mistakes(document): Document wake deduplication contracts

* feat: add Cursor CLI crew harness (#2238)

* feat(harness): add Cursor Agent CLI adapter

# Conflicts:
#	bin/fm-spawn.sh

* fix(composer): read cursor-agent's reverse-video placeholder as idle

cursor-agent renders its idle composer placeholder dim (SGR 2) but paints the
cell under the terminal cursor in reverse video (SGR 0;7). Reverse video is
neither dim nor a dark truecolor foreground, so the shared ghost stripper keeps
that one character and an idle composer reduces to a lone `P`. Judged on its
own, that remnant reads `pending` on a genuinely idle pane, which defers
away-mode escalation indefinitely on the styled cursorless backends.

Teach the ONE fleet-wide classifier the shape instead of adding an adapter-local
copy: register `→` as an agent prompt glyph so the composer row is structurally
findable at all (without it the bottom-most shape is a stale shell prompt echo
in the scrollback), add both verified placeholders to the idle set, and consult
the styling-independent plain row when the styled row is only a remnant.

The plain-row branch demands the remnant be a proper, strictly shorter substring
of a plain row matching a fully anchored placeholder. Real typed text is
uniformly bright, so stripping leaves it equal to the plain row and it stays
`pending` - verified live against a pane where the typed text was exactly the
placeholder string.

Verified live on cursor-agent 2026.08.11-e8db854; the regression pins the real
captured bytes and asserts the remnant survives stripping, so the case cannot go
vacuous if the stripper later learns SGR 7.

Co-authored-by: Amplify Logic AI <lars@sockinator.co>

* feat(cursor): narrow cursor identity and order its marker before CLAUDECODE

Cursor ships two executable names - `cursor-agent` and the legacy alias `agent`
- and runs as a bundled node script, so tmux reports the pane command as a bare
`node`. Neither `agent` nor `node` can be trusted by name, so identity gets one
owner in bin/fm-cursor-lib.sh that demands cursor's own name or install tree in
the path or argv[0], from the structural signal only. Probing an arbitrary pid's
executable during a liveness poll would execute a stranger's binary, which is
the hazard that rule exists to close.

Two consequences wired up:

Detection. cursor-agent does NOT clear an inherited CLAUDECODE, so a cursor
worker launched under a claude primary carries both markers and whichever is
tested first wins. The cursor markers are ordered ahead of the CLAUDECODE check;
fm-spawn additionally clears foreign markers at the launch boundary. Both are
kept deliberately - launch sanitization only covers sessions fm-spawn started,
while the ordering also covers a cursor session started by hand. Verified live
that CURSOR_INVOKED_AS is set on the agent process and CURSOR_AGENT=1 on the
child/tool processes fm-harness.sh actually runs as.

Pane liveness. A cursor pane now classifies `agent`. An unrelated node or agent
stays `other`, which the liveness callers already fold into `ambiguous` rather
than `dead`, so a stranger's node pane is never reported agent-free.

Resolution prints the STABLE launcher rather than the canonical target: identity
is proven through canonicalization, but cursor's canonical path carries a
version its own auto-update replaces, and pinning that would strand a task on a
version that can vanish.

The regression drives the two identity signals apart - a cursor-named executable
outside any cursor tree, and a non-cursor-named alias inside one - and asserts
each carries a verdict alone, so no single vendor string is load-bearing. Its
negative controls are real spawned processes, not fixtures.

Verified live on cursor-agent 2026.08.11-e8db854.

Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* feat(cursor): classify cursor busy state from its own turn transcript

Cursor shipped as "unknown cursor-unverified" on the premise that it exposes no
semantic turn lifecycle, only a rendered "Working" footer. That premise is
wrong: cursor-agent persists an append-only JSONL transcript per conversation
and brackets every submitted turn with a role:user open and a typed turn_ended
close. Verified live on 2026.08.11-e8db854, including the interrupt path, where
Escape closes the turn with status "aborted" - so this source covers manual
interruption, which Claude's Stop hook does not.

That makes it a genuine pull source in the muse mould rather than the rendered
text the redesign forbids: no writer, no arm, no gen, nothing seeded that could
never be cleared. Cursor's `ctrl+c to stop` footer stays out of the verdict, and
herdr's narrower native streaming state cannot stand in for it either.

Binding deliberately does not reconstruct cursor's workspace-slug directory
name. That slug collapses path separators, so rebuilding it would be a guess
that could bind the wrong pane; cursor records the exact absolute workspace path
in each project's .workspace-trusted, and the binding matches on that. A
conversation recorded as prior at spawn is excluded, so a relaunch in a reused
worktree folds its own turn rather than its predecessor's. Requiring a unique
remaining conversation keeps zero and several both unknown, because neither
proves anything about the current turn.

The regression pins the fold with real transcript files and asserts the
dangerous direction stays closed: an unresolvable binding, a record-free file,
an unclaimed workspace, and a workspace-path PREFIX all read unknown, never
idle. The prefix case uses an opaque fixture slug so a slug-rebuilding
implementation cannot pass it.

Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* feat(cursor): make the cursor launch runnable and give it lifecycle control

Five gaps that together kept a cursor crewmate from being drivable end to end.

Launch. The template invoked `cursor agent`, but `cursor` is not the CLI - the
installed names are `cursor-agent` and the legacy alias `agent` - so the command
could not run at all on a machine with a normal cursor install. It now resolves
through the verified owner, which also refuses a spawn loudly instead of leaving
a pane that dies with command-not-found and reads as a wedged worker.

Session binding. fm-spawn writes state/<id>.cursor-session so the busy fold can
find this pane's transcript, and teardown removes it.

Lifecycle control. No cursor PR touched fm-control-lib.sh, so
`fm-control <id> interrupt|exit|relaunch` could not drive a cursor worker at
all. Verified live: interrupt is a single Escape, exit is /exit, and cursor does
NOT repollute its composer with the cancelled prompt, so unlike muse it needs no
clear key. Secondmate is refused, matching the spawn refusal.

Submit acknowledgement. cursor parks its terminal cursor outside its composer,
so the composer verdict on tmux is always `unknown` and a submit could never be
acknowledged from the composer alone. The submit core's existing idle-to-busy
transition covers that case, but only if the pane's busy footer is recognised,
so cursor's `ctrl+c to stop` joins the harness-less default union the submit
cores read. The TOKEN is matched rather than the spinner verb: the same version
rendered both `Working` and `Running` in consecutive turns.

Bootstrap. A configured cursor crew harness with no cursor executable is now a
loud MISSING diagnostic rather than a first-spawn failure, and it accepts either
installed name.

Interrupt cancellation is deliberately left unconfirmed. The transcript does
type an aborted close, but its post-interrupt write latency measured as
variable - sometimes seconds, sometimes not within twenty - so a claim built on
it would be unreliable. Normal turn completion is prompt, which is what the busy
fold actually depends on.

Two inherited tests are corrected rather than deleted: the busy test asserted
cursor could have no semantic source, and the launch test pinned the literal
`cursor agent` string. Both now pin the verified behaviour, including that the
launch never allocates a second worktree.

Co-authored-by: ABHISHAKE KUMAR BOJJA <abojja@uvic.ca>
Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* docs(cursor): record the verified crewmate facts and extend the drift guard

The inherited cursor entry was written against 2026.08.04-aaa8809 and several of
its claims no longer hold: it named `cursor agent` as the binary (not the CLI
name), listed six Grok model ids of which the live catalog now returns two, and
recorded busy state, exit, interrupt, and skill invocation as unverified.

Replaced with what was measured against 2026.08.11-e8db854, including the two
facts most likely to be rediscovered painfully: cursor runs as a bundled node
script so its pane title is a bare `node`, and it parks its terminal cursor
outside its composer, which makes the tmux composer verdict permanently
`unknown` by design rather than a defect to chase.

Model ids now route to `--list-models` for the account instead of a fixed list,
since that list is exactly what drifted.

The live drift guard covers cursor, resolving it through the same verified owner
fm-spawn uses and passing --trust so the probe cannot hang on the workspace
prompt. Run against every installed harness: 8 checked, all alive, with cursor
reporting title='node' foreground=[.../cursor-agent] - the drift shape this
guard exists to catch.

Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* docs(agents): record the cursor session-binding state file

The state/ layout section is the inventory every session reads; a busy-source
binding that fm-spawn writes and teardown removes belongs in it alongside muse's.

* no-mistakes(review): Sanitize ambient Cursor marker in harness tests

* no-mistakes(review): Validate Cursor models against live catalog

* no-mistakes(review): Reject unsupported secondmates before binary preflight

* no-mistakes(review): Narrow Cursor ancestry detection to structured process identity

* no-mistakes(review): Parse Cursor transcripts and sanitize inherited markers

* no-mistakes(review): Handle malformed Cursor transcript records safely

* no-mistakes(review): Validate malformed Cursor closes in fallback parser

* no-mistakes(review): Retire stale Cursor bindings during relaunch

* no-mistakes(review): Fix Cursor drift guard command variable

* no-mistakes(review): Narrow Cursor identity to versioned install trees

* no-mistakes(document): Document Cursor harness boundaries

* refactor(composer): move the delivery busy footers to the shared owner

The per-harness rendered busy footers lived in bin/fm-tmux-lib.sh under
FM_TMUX_* names, so cursor's `ctrl+c to stop` signature - and every other
harness's - was reachable only from tmux. That placement was wrong on its own
terms: herdr, zellij, cmux, and orca run the same harnesses and face the same
question these footers answer, which is whether a submitted Enter actually
landed. Nothing about the signature is tmux-specific.

Moved verbatim into bin/fm-composer-lib.sh, the shared composer/delivery owner
every backend already sources, and renamed to FM_DELIVERY_* so the names stop
claiming a scope they never had. All five adapters now reach cursor's signature;
verified per adapter rather than assumed.

The boundary the move must not blur is stated where it now lives: this is a
DELIVERY guard, never a worker-state source. Confirming a keystroke landed is a
different question from asking what a worker is doing, and bin/fm-busy-lib.sh
remains the semantic owner that forbids classifying a harness from rendered
text. Cursor still classifies only from its transcript fold, which is already
backend-agnostic because it folds a file rather than reading a pane - the same
verdict on all six backends.

The old FM_TMUX_* aliases are dropped rather than kept as dead shims: nothing
outside the moved block referenced them except fm-busy-lib.sh's grok fallback,
which now reads the new name. The documented operator override, FM_BUSY_REGEX,
is untouched.

Also removes a dead duplicate CURSOR_INVOKED_AS check in bin/fm-harness.sh,
unreachable behind the marker check above it.

* no-mistakes(review): Correct shared delivery guard ownership references

* no-mistakes(document): Document shared delivery guards and Cursor backend limits

* no-mistakes: apply CI fixes

* fix(composer): bound a bare composer's wrap region at a half-block rule

A live cursor crewmate on herdr classified its IDLE composer as `pending`, and
fm-send consequently exited 1 with "delivery unconfirmed" on a message that had
actually landed. The cause is not cursor-specific.

Herdr draws a composer's top and bottom rules with the half-block glyphs U+2584
and U+2580 rather than the box-drawing family. fm_composer_row_has_edge knew
only the box-drawing set, so no box was detected; the composer was found as a
BARE row, and its wrap region - which extends while rows are non-blank and carry
no structural edge - walked straight through the composer's own closing rule and
swallowed the model and path footer below it. That footer is real text, so the
region classified pending on a genuinely idle pane.

Teaching the shared edge detector the half-block glyphs bounds the region at the
closing rule. Measured on the captured bytes of a real herdr cursor pane: the
same capture that read `pending` now reads `empty`.

This is a shared shape-path change, so it is deliberately narrow - it adds
glyphs to the edge vocabulary and changes no verdict logic - and the whole
composer and backend suite is green, including the other harnesses' herdr
fixtures.

The regression pins the real captured shape and asserts the footer content is
genuinely present, so the case cannot pass vacuously if the region were ever
bounded for some unrelated reason.

* fix(herdr): confirm a cursor submit from the rendered-footer transition

Herdr's composer-shape fix made an idle cursor pane classify `empty`, but
`fm-send` still exited 1 with "delivery unconfirmed" on messages that had
actually landed. Live measurement found the second, independent cause.

Herdr reports a cursor pane `agent_status=blocked` in EVERY state - idle,
mid-turn, and after - so the submit path's idle-baseline native confirmation is
structurally unreachable for cursor and every send falls into the composer
branch. That branch reads cursor's mid-turn composer row, which renders its own
`Add a follow-up` placeholder beside a right-aligned `ctrl+c to stop`. That
token is composer content, so the verdict is `pending` on a composer holding no
user text at all, and the Enter-retry budget then reports pending.

The escape is the same semantic signal the native path uses, read from the
pane's verified busy footer instead of native agent-state, and it is the
rendered-footer twin of the tmux submit core's turn-started confirmation: an
idle-to-busy transition ACROSS our Enter proves the harness accepted the
submission. The baseline is taken before the first Enter and only when the
native baseline was not legibly idle, so the idle-baseline path still never
reads pane content and a pane already mid-turn before we typed keeps reporting
`pending` rather than borrowing another turn as proof of this delivery.

The composer verdict is deliberately NOT relaxed. A right-aligned status token
on the composer row stays content for every other caller, including the
away-mode pre-injection guard, and the shared cursorless submit core is left
untouched so zellij, cmux, and Orca keep the behavior their own follow-up owns.

Verified live on herdr 0.8.0 and cursor-agent 2026.08.11-e8db854 in an isolated
lab session: `fm-send` now exits 0 and the steer executes, interrupt cancels a
running turn, `/exit` stops the agent, and teardown clears the record. All seven
panes of the running default session classify identically before and after the
shape fix, so no other harness regressed.

* no-mistakes(review): Prevent working Herdr baselines from falsely confirming delivery

* no-mistakes(document): Correct Cursor harness and backend documentation

---------

Co-authored-by: ABHISHAKE KUMAR BOJJA <abojja@uvic.ca>
Co-authored-by: Amplify Logic AI <lars@sockinator.co>
Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* fix(bin): require quota-axi 0.1.25 (#2300)

* fix: raise quota-axi floor to 0.1.25 for Cursor CLI quota awareness

Homes on latest main need quota-axi #87 so Desktop-absent CLI machines report a fresh Cursor quota instead of a false sign-in-required.

* no-mistakes(document): Update quota floor documentation pointer

* fix(bin): prevent false Pi watcher alarms during hand-offs (#2304)

* fix(guard): stop the false send-time watcher-down alarm on Pi primaries

On a Pi primary the watcher process is not the liveness signal. The Pi
extension tears the watcher down on every actionable wake and spawns the
replacement itself, so the singleton lock is legitimately unheld between
cycles: every one of the 799 cycles in a live primary's ledger ends with
lock_after=pid:none, and a live capture caught the guard verdict flipping to
no-watcher during one hand-off with the beacon 63s old.

bin/fm-guard.sh classified Pi as a persistent-watcher harness, which demands a
live identity-matched lock holder at all times, so any guarded command landing
in a hand-off painted the full WATCHER DOWN - SUPERVISION IS OFF banner and
told firstmate to repair a cycle the extension already owns and is restoring.

Add an extension supervision model for pi and pi-signed. A live
identity-matched watcher stays the ordinary healthy state; an unheld lock is
healthy only while the beacon is fresh within grace AND a live Pi session
provably owns continuity - both primary extensions recorded in their state
markers at their current on-disk builds by the process named in state/.lock,
with that process still alive. Without that proof the banner fires exactly as
before, so an unloaded, version-drifted, or exited Pi session is loud
immediately and a cycle the extension never restores is loud once the beacon
passes grace. The queued-wake warning, the PID-strict turn-end guard, and
every other primary's detection are untouched.

Fold session-start's duplicate Pi marker predicate into the shared library so
the ownership contract has one owner.

* no-mistakes(review): Restrict Pi hand-off tolerance to unheld watcher locks

* no-mistakes(document): Document Pi watcher hand-off supervision

* feat: support Cursor Agent CLI as a primary harness (#2305)

* feat(cursor): add Cursor Agent CLI primary hooks, park supervision, and session start

Register a tracked project-scope .cursor/hooks.json for Cursor's stop,
sessionStart, preCompact, and preToolUse steps.

bin/fm-turnend-guard-cursor.sh owns Cursor's turn boundary as a park: it
foregrounds the watcher arm, holds the boundary open until an actionable close,
and returns that wake as one follow-up. Exit 2 is a silent no-op on Cursor's
stop step, so the adapter never uses it. The follow-up loop is bounded twice,
by Cursor's own loop_limit and by the payload's loop_count.

bin/fm-sessionstart-cursor.sh delivers the digest as additional_context at
sessionStart, and stages it for the next turn boundary at preCompact, which
cannot inject context.

Cursor also loads the tracked Claude settings, so bin/fm-hook-host-lib.sh lets
each tracked Claude-shaped entrypoint stand down on a Cursor-delivered payload
rather than running every covered event twice.

bin/fm-tmux-lib.sh reclassifies a Cursor pane's composer cursorlessly, because
Cursor parks its terminal cursor outside the composer, which restores a genuine
composer-empty proof and unblocks away-mode escalation delivery.

* feat(cursor): make Cursor Agent CLI a verified primary harness

Resolve Cursor in the session-lock ancestry through bin/fm-cursor-lib.sh, which
a Cursor primary needs before it can hold its own home lock, and classify its
stop-hook park under the autoarm supervision model so the mid-turn pull guard
stops reporting a healthy between-turns watcher as down.

Read a Cursor pane's composer cursorlessly on tmux, gated on Cursor's own
structural process identity, which restores a genuine composer-empty proof and
lets away-mode escalations reach a Cursor primary with no daemon change.

Lift the secondmate refusals in bin/fm-spawn.sh and bin/fm-control-lib.sh now
that the supervision protocol exists and is recorded.

Cover the whole surface with a portable regression over real processes, an
opt-in live guard against the installed cursor-agent, and dated per-harness
evidence.

* docs(cursor): record Cursor as a verified primary across the owning surfaces

Update the turn-end guard, session-start, arm-seatbelt, cd-guard, watcher
continuity, architecture, configuration, README, and harness-adapters owners,
and add dated live evidence to the supervision and runtime-backend verification
records. Correct the recorded Cursor tmux composer verdict: the cursor-anchored
read is still blind, but the composite reader is no longer unknown.

Lift the remaining remote-secondmate refusal missed in the previous commit, and
add the new libs to the existing fixtures that copy a fixed dependency list.

* refactor(cursor): name the park's stand-down condition for both its causes

Also record that Cursor's preCompact firing itself is not yet live-verified,
while the static evidence that it cannot inject context, and the staging path
that follows from it, both are.

* test: give the pretool fixtures their new dependency and one lint owner

The cd-guard fixture copies a fixed dependency list and now needs the shared
hook-host predicate. Both pretool suites also asserted cleanliness with a bare
shellcheck call, a second and weaker copy of the lint definition that
bin/fm-lint.sh owns: it omits --external-sources, so it failed the moment these
checkers sourced a shared library. They now delegate to that owner.

* test: assert the cursor secondmate contract instead of its removed refusal

A cursor secondmate now launches, so the suite asserts what its park actually
needs: --trust so the home's project hooks load at all, its own home pinned as
the workspace, and the autoarm supervision model inherited across the launch.

* no-mistakes(review): Serialize Cursor wakes and bind staged context

* no-mistakes(review): Serialize Cursor context and nag state commits

* no-mistakes(review): Enforce Cursor ceiling before staged context delivery

* no-mistakes(review): Serialize Cursor claims and staged context

* no-mistakes(review): Serialize Cursor ownership and state commits

* no-mistakes(review): Protect Cursor context across session takeover

* no-mistakes(review): Preserve Cursor context across session takeover

* no-mistakes(review): Enforce owner-keyed Cursor staged context

* no-mistakes(review): Atomically claim Cursor follow-ups and staged context

* no-mistakes(review): Defer Cursor preCompact staging and simplify supersession

* no-mistakes(review): Serialize Cursor park commits and defer preCompact

* no-mistakes(review): Stop Cursor parks after session takeover

* no-mistakes(test): Route Cursor preCompact context through stop follow-up

* no-mistakes(document): Update Cursor primary documentation

* revert(cursor): cut preCompact staging from this change

Carrying a compaction digest across two concurrently running stop hooks kept
producing races that could deliver it twice or strand it indefinitely, and
closing them kept enlarging a critical section inside a hook Cursor awaits at
the turn boundary. Native preCompact firing was never observed either, so the
surface has no empirical basis yet.

Remove the adapter, its registration, its staged path in the park, and its
tests, and record the surface as deferred and uncovered alongside the Codex
interactive TUI. A regression now asserts preCompact stays unregistered so it
cannot return without its own design and evidence.

This change ships the proven core only: the turn-end follow-up park, the
run-tier session start, and away-mode delivery.

* no-mistakes(review): Correct Cursor park supersession documentation

* no-mistakes(document): Clarify Cursor run-tier verification ownership

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

---------

Co-authored-by: kunchenguid <kun-1@kunchenguid.com>

* feat(bin): add decline and repair paths for decision holds (#2330)

* feat(bin): add unrouted close paths to the captain decision gate

A captain who declines a held decision leaves no follow-up work to route,
so `resolve` could not express that answer: it requires at least one
`--routed-to` task. The only way to close such a hold was a direct
`tasks-axi done`, which never writes the durable resolution record the
completion gate reads, so the originating investigation could no longer
pass `verify` and its cleanup stayed blocked.

Add two close paths that route no work:

- `decline` closes an actively held hold with a recorded captain decision
  and no routed task. It refuses while any task is still blocked by the
  hold, because releasing routed work without recording it is `resolve`'s
  job.
- `repair` records the missing resolution block on a hold that was already
  closed outside this script. It never reopens a hold and never clears a
  dependency edge, and it refuses a hold that is still actively held.

Both require a non-empty captain decision file and share `resolve`'s
digest-based retry identity, so an exact retry is idempotent while a
changed decision is rejected. The recorded body now also names which path
closed the hold, and each routed entry regains its own line.

The gate itself is unchanged: an unanswered decision still fails
completion and blocks teardown, and neither new path can close a hold
without the captain's recorded word.

* fix(bin): require captain-hold provenance before repairing a decision

`repair` checked only that the backlog item was kind captain and Done, so
an ordinary captain-kind task that was never held for the captain could be
closed, repaired, and then pass the completion gate.

tasks-axi keeps `hold_kind` through a close, so it is the surviving proof
that an identity really was a captain hold. Require it before writing the
resolution record, and cover the case in the gate regression.

* no-mistakes(document): Correct decision-hold lifecycle documentation

* fix(bin): surface buried wake status lines once (#2331)

* fix(bin): surface buried status notes on wake drain

A note: answer immediately followed by a routine note was dropped because
annotations kept only the newest line and note: never enters OPEN DECISIONS.
Present every unread note and pending-reply resolution since the last drain
cursor, and annotate every unread line on a queued signal.

* no-mistakes(review): Fix unread status cursor races and overflow

* no-mistakes(review): Preserve cursors when status span reads fail

* no-mistakes(review): Make status presentation transactional under I/O failures

* no-mistakes(review): Simplify unread status cursor and presentation locking

* no-mistakes(review): Align cursor failure regressions with transactional presentation

* no-mistakes(review): Retire stale presentation cursors during task teardown

* no-mistakes(review): Preserve routine status until signal annotation

* no-mistakes(review): Correct unread status cap documentation

* no-mistakes(document): Document unread wake status presentation

* no-mistakes(lint): Fix wake surfacing ShellCheck warnings

* no-mistakes: apply CI fixes

* feat: add max Calm presentation level (#2334)

* feat(calm): add a max presentation level that hides mid-turn working notes

Calm's home-local preference becomes a three-state level instead of a
boolean: "off" is stock Pi, "on" is today's Calm, and "max" is Calm plus
hiding the assistant text of messages the model did not end its response
with. `/calm max` selects it from any state, a plain `/calm` steps max
back to ordinary Calm and otherwise keeps the existing on/off cycle, and
any other argument keeps that cycle too.

`config/calm` now persists "max" as its own literal value, so a session
start, resume, fork, or reload restores the stored level rather than
treating it as unrecognized and dropping to off.

The hide rule keys on Pi's intrinsic per-message stopReason: "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, because suppressing it would also stop a genuine reply from
streaming. The existing assistant layout adapter filters the blocks out
of the same shallow presentation copy it already uses for collapsed
thinking, so the message, model context, session storage, /export, and
delivery are untouched and a hidden mid-turn row collapses to zero
height. The new "assistant-working-note" class keeps that choice in the
visibility policy owner, where ordinary Calm keeps it visible.

* no-mistakes(document): Clarify Calm max persistence and taxonomy

* feat(calm): hide mid-turn working notes by default (#2339)

* feat(calm): make hiding mid-turn working notes the ordinary Calm state

Calm collapses back to the two-state on/off toggle it was before the max
presentation level, with max's hide rule promoted into ordinary Calm.
Calm on now hides mid-turn assistant working notes in addition to what it
already hid, and the /calm command parses no argument again.

The hide rule itself is unchanged: assistant text is removed from the
shallow presentation copy when the message's own stopReason is "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, so a genuine reply still streams. The message, model context,
session storage, /export, and delivery remain untouched.

config/calm persists only "on" and "off" again, but the reader still maps
a persisted "max" to on so a home upgraded from the removed level keeps
Calm on instead of dropping to off.

The mid-turn hide is now default behavior rather than an opt-in level, so
docs/calm.md documents it for users, docs/configuration.md records the
two written values plus the legacy max mapping, and the feasibility
taxonomy drops its level-scoped wording.

* no-mistakes(document): Document ordinary Calm working-note hiding

* chore: store no-mistakes test evidence in the repo (#2355)

* chore: ignore scratchpad/ at the repo root (#2359)

* fix(ci): fail hung Herdr behavior runs in 20 minutes (#2413)

A wedged family-run step was occupying the runner until the 75-minute
job cap; bound that step so cleanup and timing artifacts still upload.

* fix: keep the public promise reachable when work is routed to a second mate (#2457)

The lightweight Relay follow-up link lives in the answering home's own
state/<task-id>.meta, so it can only bind work that home owns. When a
Relay-linked request is routed to a second mate, the task record lives in the
second mate's home, fm-x-link.sh failed with a bare "no such task ...meta", and
nothing else picked the promise up: only the soft acknowledgement was ever
posted. The typed promised-final path already supports --work-home
secondmate:<id>; the playbook simply never chose it.

- fmx-respond now states the routing rule crisply: a task in this home takes the
  lightweight link, and second-mate-routed work takes a promised-final
  commitment bound to that home, registered up front with the brief command
  carried into the routed worker's instructions.
- fm-x-link.sh refuses a task with no local record by naming the registered
  second mate whose home actually holds it and printing the promised-final
  registration command, with the exact --work-home when the match is
  unambiguous. A home with no registered second mates keeps the plain error.
- fm-backlog-handoff.sh reports, after a successful move, any moved key that
  still owes a public reply bound to main/<key>, since that binding no longer
  names the home owning the work. The move itself is never blocked.

Docs and the secondmate handoff prose follow the same rule. Tests cover the
refusal, its scoping, the unchanged local-link path, and both handoff outcomes
at the script boundary.

* docs(skills): add remote-secondmate recovery hint for false-negative verdicts (#2456)

* fix(skills): hint that remote secondmate liveness verdicts false-negative

fm-crew-state and fm-send routinely misreport a live remote secondmate
as dead; confirm against the pane before relaunching, and relaunch only
through fm-spawn.sh, never raw herdr pane surgery.

* no-mistakes: apply CI fixes

* fix(calm): keep Pi's export confirmation visible (#2461)

Pi 0.83.0 added a status line to every tool-expansion change, and Pi
updates the previous status line in place when two status messages
arrive back to back. Calm's post-export redraw cycled tool expansion on
the macrotask right after Pi printed "Session exported to: <path>", so
both expansion status lines coalesced over that confirmation and the
captain was left with no record of where their export landed.

Calm now repaints only the tool rows it presents, by invalidating each
row through the render context Pi hands its render slots, and requests
the surrounding redraw through setStatus. Neither appends to the
transcript. The repaint is still needed because Pi can re-render a row
asynchronously - the built-in edit row invalidates itself once its diff
is ready - and that re-render can land inside the window where /export
forces stock rendering.

The real-terminal /export case now asserts the confirmation is still on
screen after the redraw has settled, and that the redraw restored every
Calm-hidden row, instead of only racing the moment the confirmation
first appeared.

* feat(stow): add open-record persistence to /stow before reset (#2488)

* feat(stow): persist the open records a session is holding

/stow curated memory and captured session knowledge, but never touched
record state, while AGENTS.md called it an "unfinished-work sweep" and the
receipt declared the session "safe to reset" - wording that implied a
record-correctness guarantee stow does not make. A shipped PR with no
backlog item, a queued umbrella whose phases had merged, and four decision
holds left open after their answers shipped all survived repeated stows.

Add a bounded pass that files record state from the same volatile input the
rest of stow already uses: the open threads in context, minutes before the
reset destroys them. It creates a record for an unfiled thread and corrects
one the session knows is wrong, through the owning path, and states its
boundary as part of the contract - it never enumerates the backlog, lists
holds, or queries a forge, because it cannot be a reconciliation and must
not be read as one.

Correct the wording in AGENTS.md and the completion receipt so reset-safe
means what it actually guarantees: nothing this session knew was lost.

* no-mistakes(review): correct stow decision-hold inspection to read hold via tasks-axi

* no-mistakes(document): note /stow open-record persistence in README command catalog

* refactor(stow): state open-record persistence as principle, not procedure

The first version enumerated triggers, named commands, and prescribed an
ordered procedure. That is too rigid for an agent skill: it invites literal
execution of a checklist instead of judgment, and every enumerated example
is a way for the guidance to go stale.

Reduce it to the intent - before a reset, the important open work you are
holding in context must end up durably recorded rather than dying with the
session, filing what is unfiled and correcting what is stale - and let the
agent judge importance, the record, and the owning write path.

Keep the scope bound, since it is a decided contract and not a mechanic:
this covers the open work the session is holding, never a reconciliation of
durable records against repository or forge reality. The wording
corrections in AGENTS.md and the completion receipt are unchanged.

* fix(decisions): close decision holds at answer time via one general keyed-answer path (#2490)

* fix(decisions): close captain holds at answer time

Firstmate had two "a decision is open" ledgers with asymmetric closing
mechanics. The live status-log ledger closes atomically at answer time,
because bin/fm-send.sh --resolve-key makes answering a decision be the
act that closes it. The durable backlog hold ledger had no such coupling:
answering and recording were two separate acts, and only the first was
forced by the workflow.

That asymmetry lost four real captain decisions. Their answers were
captured durably to disk, keyed character for character by the hold
decision keys, acknowledged, and even implemented and shipped, yet the
holds stayed open for two days and the captain was asked to re-answer
decisions already on his own disk.

Give the hold ledger the same answer-time-closure property:

- bin/fm-decision-hold.sh gains an `answer` subcommand, the hold ledger's
  counterpart to --resolve-key. It shares one unrouted close
  implementation with `decline`, so it carries every existing guard - the
  captain decision file, the active-hold requirement, retry identity, and
  the refusal to release still-routed work - and differs only in the
  resolution mode it records. `decline` keeps its stronger meaning that
  the answer routes no follow-up work at all.
- bin/fm-procevent-lavish.sh wires the channel that actually carried the
  lost answers. `arm --decisions-origin` binds a deck to the origin whose
  holds it carries, `answers` reads the structured choices out of a
  captured poll result, `close-decisions` maps each key to its hold and
  closes it through the command above, and `autohandle` lets the runner
  apply that at capture time.

Safety is preserved rather than traded away. Only rows tagged `choice`
are read, so freeform captain prose cannot forge a decision key. Closure
is confined to the one bound origin. The decision text is a pure function
of the captured result, so a replayed capture is idempotent. A hold that
is absent, already closed, or still blocking routed work is skipped and
left for `resolve`, never forced. A deck armed without the binding
touches no hold at all. And autohandle deliberately never reports full
handling, because recording an answer is transcription while acting on it
is firstmate's judgement - so the check wake still reaches the handler.

fm-send --resolve-key is untouched.

* no-mistakes(document): document state/lavish-decisions binding dir in AGENTS.md state inventory

* refactor(decisions): make keyed-answer closure one general capability

The previous pass gave holds answer-time closure but built it as bespoke
Lavish wiring: the review adapter carried the source-to-origin binding,
mapped keys to hold identities, wrote decision records, decided what to
skip, and closed holds itself. That treated a review deck as a special
decision source. It is not - it is an ephemeral discussion format that
happens to carry answers.

Collapse it into ONE general capability with one owner.

bin/fm-decision-hold.sh now owns the whole of "a keyed answer closes its
matching hold":
- `answers <origin> --source <provenance>` is the channel-agnostic
  intake. It reads key/answer/label lines on stdin, maps each key to its
  hold, and closes it through the same `answer` path, so every guard
  applies identically whatever channel the answer came from. --source is
  provenance recorded in the decision, never a behavior switch; there is
  no per-channel branch and no knowledge of chat, decks, or transports.
- `bind`/`unbind`/`binding` own the source-to-origin binding for any
  channel whose answers arrive detached from their origin.

Every channel is now an ordinary caller that only turns what it received
into keyed lines:
- bin/fm-send.sh (chat) feeds the intake for a key that names an active
  hold. This also fixes a real gap: once `complete` transfers a decision
  to its hold it closes the live status copy, so --resolve-key alone
  could never answer a transferred decision.
- bin/fm-procevent.sh feeds it generically. A bound source's captured
  result goes to `<adapter> answers <result-file>` and whatever that
  prints is piped into the intake. The runner names no adapter, parses
  no result, and carries no decision rule, so any future adapter with an
  `answers` command works with no change here.
- bin/fm-procevent-lavish.sh keeps only `answers`, which reports the
  structured choices a review captured and stops. It maps nothing to a
  hold and closes nothing; it lost ~160 lines of decision logic.

Feeding is independent of handling, so it never acknowledges a result
and never suppresses a wake - recording an answer is transcription,
acting on it stays firstmate's judgement.

The regression that proves closure now drives a FIXTURE adapter that is
not the review adapter, so what is proven is that any bound channel
reaches the intake rather than that one channel is wired specially. A
new regression drives the real fm-send over a stubbed transport for the
chat side. Every prior guarantee still holds, and fm-send's status-log
behavior is unchanged.

* no-mistakes(review): test(decisions): drop source-content grep from hold-closure regression

* fix(memory): emit a real @AGENTS.md pointer instead of a CLAUDE.md symlink (#2512)

A Write aimed at CLAUDE.md followed the symlink and destroyed AGENTS.md.
The installer now creates and migrates to a recoverable two-line pointer file.

* fix(ci): keep CLAUDE.md pointer check valid (#2515)

* ci: gate GitHub workflows with pinned actionlint (#2517)

* fix(lint): catch malformed GitHub workflows before merge

A self-broken ci.yml cannot report its own breakage, so parse every
workflow in the local lint path that no-mistakes already runs.

* fix(lint): pin actionlint instead of Ruby for workflow lint

A self-broken ci.yml still has to fail in the local lint path, and the
named tool for that gate is actionlint, not a new Ruby runtime.

* no-mistakes(document): Clarify pinned workflow lint documentation

* fix: install pinned lint tools across supported platforms (#2546)

* fix: install pinned shellcheck and actionlint on macOS and linux arm64

The installers were hardcoded to linux amd64 and sha256sum, so a Mac
dev could not satisfy the refuse-on-mismatch lint gate. Select the
official per-platform archive and checksum, and fall back to shasum -a 256.

* no-mistakes(document): Document cross-platform pinned lint installers

* docs: reconcile test-evidence docs with store_in_repo: true (#2548)

.no-mistakes.yaml has set test.evidence.store_in_repo: true since #2355, but
CONTRIBUTING.md, docs/configuration.md, and docs/architecture.md still described
the old policy of keeping evidence out of the repo in a temp directory.

The current no-mistakes behavior for store_in_repo: true is to publish each run's
test evidence to the orphan no-mistakes/evidence branch and link it from the PR
body. That branch shares no history with code branches, so evidence never enters
a pushed feature branch or the default branch, and CI's tracked personal fleet
paths rule stays accurate.

Docs only. No change to .no-mistakes.yaml or any workflow.

* docs: clarify test evidence branch storage (#2549)

* docs: correct test evidence storage comment in .no-mistakes.yaml

* no-mistakes: apply CI fixes

* docs: hint that live scouts may host their own Lavish review loop (#2563)

Make that a first-class option in always-loaded instructions so firstmate does not default to mediating and tearing the scout down between iteration rounds.

* fix(bin): report remote secondmate delivery and state truthfully (#2570)

* fix(bin): report remote secondmate delivery and state truthfully

A steer to a remote secondmate crosses fm-on.sh to a host-local fm-send
leg whose unconfirmed submit read-back (verdict=pending, typically a busy
mate whose harness queues the steer) was flattened into exit 1, so the
parent printed "error: text not submitted" / "error: text not sent" and
discarded the …
KD5694 added a commit to KD5694/firstmate that referenced this pull request Sep 11, 2026
…imary (#1)

* fix: safely split supervision wake handling by actor (#2953)

* feat(bin,pi): per-actor wake consume, silent success gating, merge-poll dedup

Three related fixes to the shared wake-drain and Pi supervision-branch
dispatch machinery so a routine success is never main-blocking and a
mixed queue can safely split between actors.

1. Successful routine results no longer create main-blocking wake rows.
   fm-startup-network.sh only enqueues a check: startup-network wake when
   the deferred result is actionable (state is not "done", or the report
   carries a bootstrap-diagnostics actionable prefix); a clean success
   stays durable in the report file without ever waking the agent.

2. Per-actor wake-drain consume contract. bin/fm-wake-drain.sh now scopes
   presentation and --ack-through to the current actor
   (bin/fm-lease-lib.sh's fm_lease_actor): main keeps the original
   whole-queue cutoff behavior, unaffected. A branch actor
   (FM_SUPERVISION_ACTOR=branch, set only inside the Pi supervision
   branch's own bash tool calls) is scoped to an explicit eligible-row
   snapshot instead of a cutoff comparison, so it can never remove a row
   it was not granted - the fix for the swallow risk that used to force
   an all-or-nothing whole-queue fallback to main.
   .pi/extensions/lib/fm-branch-dispatch.ts's scopeForUnreadWake is the
   single owner of eligibility: a check-kind row (merge-confirmation
   polls, Relay mentions, credential/auth failures) is now excluded
   rather than vetoing the whole scan for a non-heartbeat wake, while a
   heartbeat review keeps its original all-or-nothing rule unchanged.
   writeEligibleRowsSnapshot publishes the exact eligible sequence
   numbers before every branch prompt; fm-primary-pi-watch.ts's offer
   still refuses a check-kind trigger outright so a main-only close is
   never itself routed to the branch.

3. A repeat identical merged-PR-poll result for an already-notified task
   is absorbed instead of enqueued again. A poll's own retirement state
   is scoped to one registration and cannot see a prior registration's
   outcome, so a task re-registered after its merge was already surfaced
   would otherwise wake main a second time for the same event.
   bin/fm-pr-lib.sh's new per-task pr-poll-merge-notified marker survives
   across re-registrations to catch that case; the first notification for
   a task still reaches main unchanged.

Regression tests colocated in tests/fm-startup-network.test.sh,
tests/fm-wake-queue.test.sh (including the mixed-queue no-swallow
property), tests/fm-pi-branch-extension.test.sh, and
tests/fm-pr-check-security.test.sh. docs/watcher-continuity.md and
docs/pi-supervision-branch.md updated for the new contracts.

* no-mistakes(review): Bind merge deduplication to canonical PR identity

* no-mistakes(review): Serialize wake row ownership across main and branch

* no-mistakes(review): Bind branch grants and deduplicate within actor claims

* no-mistakes(review): Fallback main-owned wake claims to main delivery

* no-mistakes(review): Clarify silent startup success guidance

* no-mistakes(review): Release residual branch grants after settled prompts

* no-mistakes(review): Reject truncated wake rows as corrupted

* no-mistakes(document): Document per-actor routing and silent startup success

* no-mistakes(lint): Fix ShellCheck findings in wake grant and startup test

* no-mistakes: apply CI fixes

* fix(pi): hide branch outcomes tool rows in Calm (#3024)

* Hide branch outcome tool in Pi Calm

* no-mistakes(review): Preserve stock outcomes rendering and document tool audit

* no-mistakes(review): Document branch read tool audit disposition

* no-mistakes(review): Match stock outcomes output sanitization

* no-mistakes(document): Document Calm custom-tool visibility

* fix: bind no-mistakes attestations to PR head (#3027)

* fix: delegate no-mistakes PR gate to pinned action

* no-mistakes(document): Document commit-bound no-mistakes attestations

* feat(pi): add persistent supervision branch model selection (#3028)

* feat(pi): let operators pin a cheaper supervision-branch model

Supervision is an easier job than the captain's own conversation, so the
Pi supervision branch does not need main's model. A new /supervision-model
command opens Pi's own selector over Pi's own catalog of credentialed
models, plus a "Follow main" entry, and persists the pick as one
<provider>/<model-id> line in this home's gitignored
config/supervision-branch-model. Firstmate keeps no model catalog of its
own.

The branch resolves the pin at every branch build - the first wake of a
cold start and the reopen after /new, /resume, /fork, or reload - so the
choice survives all of them, and picking also releases the live branch so
the next wake reopens the same persistent branch conversation under the
new model. An absent, unreadable, or unparseable file means no pin and
keeps today's behavior byte for byte: no model option is passed and Pi
picks the branch's model exactly as before.

A pin naming a model Pi cannot hand back is never silently downgraded
onto main's model: the branch refuses to build and the wake falls back to
the captain-facing main path naming the unusable pin, which is the
extension's existing failure direction.

The choice is home-local and not part of secondmate inherited
configuration, matching the Pi Calm preference precedent.

docs/configuration.md owns the operator-facing schema. Portable
regressions cover pin-present on create and reopen, pin-absent default,
the command's persistence, cancellation, and live rebind, and both
unusable and unparseable pins. The opt-in real-SDK guard proves the
vendor surface the pin reads and that an explicit model wins over the
model a reopened session recorded.

* no-mistakes(review): Fix supervision model runtime and rebind races

* no-mistakes(review): Restrict supervision picker to isolated runtime models

* no-mistakes(document): Document supervision branch model selection

* fix(pi): make the supervision model pin authoritative on every reopen

Clearing the pin with "Follow main" removed the file but the next branch
build reopened the persistent branch session with no explicit model
override, so Pi restored the model that session had recorded - the old
pinned model - while the command reported that the branch now follows
main. The same gap meant an absent pin did not reliably mean
same-model-as-main once a home had pinned once.

The pin file's current state now decides the model on every branch build,
create and reopen alike, overriding Pi's session-state restore. With a
pin, that model. With no pin, main's own current model is applied
explicitly, tracked from the contexts Pi already hands the extension plus
its model_select event, since the branch is built at wake time with no
context of its own. Only when main's model is unknown, or this home's
stored credentials cannot run it in the isolated branch runtime, does a
build fall back to passing no override at all, which is the behavior from
before the pin existed; the branch is never refused over model choice.

The command's notification now reports the model actually applied, and
says plainly when clearing the pin could not apply main's model instead
of claiming a change that did not take effect.

No credential handling changes: the branch still relies entirely on the
stored credentials its own runtime already holds, and the picker stays
restricted to models that runtime can resolve.

Colocated regressions cover pin present on create and reopen, clearing
the pin returning a reopened branch to main's model and specifically not
the old pinned one, an unparseable pin behaving as no pin, and the
unknown-main-model fallback to no override.

* no-mistakes(review): Make unpinned supervision follow main model changes

* no-mistakes(document): Correct supervision model documentation

* feat(pi): let /supervision-model pick branch reasoning effort (#3079)

* feat(pi): let /supervision-model pick the branch's reasoning effort

Supervision is an easier job than the captain's own conversation, so the
Pi supervision branch does not need main's reasoning effort any more than
it needs main's model. /supervision-model now settles both in one flow:
the existing model picker, then a follow-up effort picker built from Pi's
own supported thinking levels for the model just chosen. Firstmate keeps
no effort catalog of its own; the menu, the clamp, and the vocabulary all
come from Pi.

The pick persists as one line in this home's gitignored
config/supervision-branch-effort, independent of the model pin: a captain
may pin a model, an effort, both, or neither. The effort pin's current
state decides the branch effort on every branch build - the first wake of
a cold start and the reopen after /new, /resume, /fork, or reload - and
overrides Pi's restore of whatever level a reopened branch session
recorded, which is what keeps "Follow main" honest. With no pin, main's
own current effort is applied explicitly and followed live through Pi's
thinking_level_select event, the same way an unpinned branch already
follows main's model, and the two selections now share one build revision
so either change invalidates an in-flight build.

The branch is never refused over effort. Pi owns the clamp, so a pinned
level the branch's model cannot run becomes that model's nearest supported
level while the captain's raw pick is kept for a model that supports it,
and the command reports the level the branch will really run at rather
than the raw pin. A token Pi would not recognize at all is treated as no
pin rather than passed to that clamp, which would otherwise collapse a
typo into the model's lowest level. Only when main's effort cannot be read
either does a build pass no effort override at all, which is the behavior
from before this file existed.

Pi's own effort vocabulary is pinned by a bidirectional type assertion
against Pi's getThinkingLevel return type, so the tracked strict typecheck
against the installed package fails the moment Pi adds or removes a level.

docs/configuration.md owns the operator-facing schema for both pins.
Portable regressions cover the pin on create and reopen, model-only and
effort-only pins working independently, clearing a pin returning the
branch to main's effort, live-follow of a mid-session change, the clamp,
an unrecognized token, the unknown-main-effort fallback, and the command's
two-step flow, persistence, cancellation, and honest reporting. The opt-in
real-SDK guard proves the vendor surface all of that rests on, and also
repairs a pre-existing gap that left it unable to load the extension at
all.

* no-mistakes(review): Resolve effective branch effort honestly

* no-mistakes(document): Clarify Pi-owned effort picker behavior

* fix: keep routine supervision noise out of captain chat (#3093)

* fix(supervision): silence empty board closes and decouple the heartbeat

Two unrelated sources of noise put routine supervision events in the
captain's chat.

An empty Lavish board close - the captain reads a review surface, says
nothing, and closes it - became a check wake whose entire content was
that nothing happened. Suppress it at its source instead of routing it
anywhere: the generic runner gains a `silent` adapter seam mirroring the
existing `terminal` one, and the Lavish adapter answers it for exactly
one positively-determined shape, an `ended` session carrying no queued
content block. A silenced result is recorded durably handled so it does
not return on a later reconcile. Everything else announces unchanged - a
`Send & End` close carrying the captain's real answer, an `ended` result
still carrying content, a waiting or missing session, an unreadable
result, and every adapter that implements no `silent` command at all.
The keyed-answer feed is untouched, so suppressing an announcement never
suppresses the captain's own answer.

A fleet heartbeat was deferred to main merely because some unrelated
check row happened to be sitting unread, which put a routine fleet
review in the chat for a reason that had nothing to do with the fleet. A
check row is permanently main-owned, so it is now excluded from a
heartbeat claim rather than vetoing the scan, exactly as in every other
mode. What all-or-nothing guarantees is unchanged: the branch takes
every branch-ownable unread row or none of them, and an unresolvable
task-local row, an unknown row kind, or an unreadable queue still defers
the whole review to main. Main is still woken for the check on that
check's own triggering close, so nothing starves.

Main-only classes are unchanged and now each covered by a test: Relay
mentions, credential failures, merge confirmations, real board answers,
and watcher-failure repair. The per-actor acknowledgement and
no-cross-swallow properties are untouched.

* no-mistakes(review): Fail closed on all Lavish content headers

* no-mistakes(review): Suppress false unacknowledged status for silenced results

* fix(bin): stop a correlation token from hiding and stranding decisions (#1967)

* fix(classify): read the decision fold through a correlation token

status_line_verb stripped a trailing [key=...] from a status line's prefix
but left everything else glued to the verb, so a line carrying the
correlation token bin/fm-pending-reply-lib.sh embeds and a secondmate
echoes back matched no arm of _fm_decision_fold_line. Such a line folded as
ordinary status in both directions: a needs-decision or blocked opener never
opened its key, and a resolved or captain-held closer never closed one. The
same glued verb also hid correlated done and blocked lines from
status_is_captain_relevant and status_is_terminal_verb, and let correlated
working and resolved lines leak through the free-text fallback the
nonterminal guard was meant to stop.

The verb parse now walks whole words and drops only a token of the exact
shape a firstmate library writes - corr=<16 hex>, plus the bracketed form
bin/fm-secondmate-report.sh emits - before or after the key token, unkeyed,
or doubled. An arbitrary name=value word is deliberately NOT skipped:
skipping unknown tokens would let free text carrying an equals sign reduce
to a bare verb and impersonate a transition, which is the takeover the
strict parse and _fm_decision_key_transition_allowed exist to prevent. A
prefix with no corr= substring is returned byte-for-byte as before, so every
line without a token keeps its exact historical verb.

FM_OPEN_DECISIONS_FOLD_VERSION goes to 3, because every cursor persisted
under the previous reading carries an open set computed while correlated
lines were invisible and must be rebuilt from byte 0.

Measured over a real 383-line status log: 254 lines keep byte-identical
captain-relevance, pause, terminal-verb and captain-held verdicts, and all
129 changed lines carry a valid token - 14 correlated done/blocked/
needs-decision lines become captain-visible, and 20 correlated
working/resolved lines stop being escalated on prose alone.

* fix(review): Captain, block token-first decision impersonation

* fix(document): Clarify normalized status verb ownership

* fix(classify): reconcile the correlation-token read with the tag-stop parser

Rebasing onto main put this change beside #2280, which made verb parsing
stop at the first "[name=value]" tag. Both edit status_line_verb with
different intents, so the resolution keeps both rules rather than letting
one overwrite the other:

- #2280's tag stop is kept verbatim and now owns every BRACKETED tag,
  including the "[corr=...]" form fm-secondmate-report.sh writes. The
  bracket-unwrapping arm this branch had added to the token test is
  therefore removed as unreachable.
- This branch's token walk is kept and narrowed to the UNBRACKETED token
  fm-pending-reply-lib.sh writes, which the tag stop does not reach.

Two consequences of standing beside #2280 rather than before it:

The fold version had collided at 4: #2280 spent it on the tag-stop
parser and this branch had spent it on the token read. A cursor
persisted under #2280's reading predates this one and must still be
rebuilt, so the version moves to 5.

A bracketed impostor is dropped from the malformed-token list. On main
today "resolved [corr=deadbeef] [key=victim]:" already reads as the bare
verb, as does "resolved [anything at all] [key=victim]:", because the tag
stop ends the parse at the opening bracket regardless of content. That is
#2280's reviewed contract; asserting otherwise here would narrow it. The
unbracketed impostors it owns stay strict and still fold as prose.

Adds a consumer test for the two verb-string case arms that postdate this
branch: fm-supervise-daemon.sh's transient-stale arm and
fm-crew-state.sh's map_log_state.

* fix(review): Captain: Seed cursor migration fixture with version four

* fix(document): Clarify voice status normalization ownership

* fix(bin): Cursor-Park unter Pi-Host ohne Cursor-Identität stilllegen (#3115)

* fix(bin): Cursor-Park unter Pi-Host stilllegen.

pi-cursor-sdk lädt .cursor/hooks.json in die Pi-Sitzung und parkte einen zweiten Watcher; das erzeugte rearm-resurface und brach laufende Rückfragen ab. Bei PI_CODING_AGENT=true beendet der Park sofort, native cursor-agent Primaries bleiben unverändert.

* fix(bin): Cursor-Park trotz PI-Leak nur ohne Cursor-Identität stilllegen.

Stand-down gilt nur bei PI_CODING_AGENT=true ohne CURSOR_AGENT und ohne CURSOR_INVOKED_AS. Handgestartete cursor-agent Primaries mit geerbtem PI-Marker parken weiter.

* no-mistakes(document): Document Cursor park Pi-host stand-down

* fix(bin): no-mistakes-Mindestversion auf 1.46.0 anheben.

Die PR-Attestierung verlangt ab 1.46.0 strukturierte Pipeline-Schritte; der Bootstrap-Floor blieb bei 1.31.2 und ließ zu alte Builds zu.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(pi): make supervision model picker searchable and scrollable (#3099)

* fix(pi): make /supervision-model's model list bounded and searchable

Pi's generic extension selector renders every option at once with no
search box, so a real eligible catalog ran off the top of the terminal.
The model step now draws the same rows through Pi's own SelectList - the
bounded scrolling primitive behind Pi's /model picker - with Pi's own
Input and fuzzy filter above it for search, keeping 'Follow main' first,
the branch-runtime eligibility filter intact, and the pick branch-only.

Pi's ModelSelectorComponent is deliberately not reused: its selection
handler writes the captain's default model through Pi's settings manager,
which would move main's conversation as a side effect of pinning the
branch. The effort step's menu is a handful of levels and stays on Pi's
plain selector dialog.

* no-mistakes(document): Clarify supervision picker documentation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes(document): Document searchable supervision model picker

* no-mistakes: apply CI fixes

* fix(bin): durably report merged pull requests (#3104)

* fix: make a landed merge leave a durable outcome

A merge was the one lifecycle event that left no record outside the
merging agent's memory. bin/fm-pr-merge.sh ended at the forge call, and a
home merging under standing authority never waits for the merge poll that
would otherwise confirm it, so three real merges reached the captain as
silence.

bin/fm-merge-outcome-lib.sh is the single owner of that record. A
secondmate home reports the landed PR upward on the same parent reply
channel its terminal-outcome backstop already uses; a main home records
it on the durable wake queue. The record is at most once per task and
canonical PR identity, and only a merge that actually landed produces one.

The merge poll feeds that same channel when it detects a merge this home
did not perform, so the captain's own forge merge and a merge firstmate
performed itself produce one consistent outcome instead of two reporting
paths. No new state file and no second watch path.

Two smaller gaps from the same failure:

- A mate charter listed its report triggers without naming a landed
  merge. Under standing merge authority nothing is ever "ready for
  review", so the enumerated list silently omitted the case that matters.
- A secondmate home seeded without its parent binding failed every
  terminal-outcome report for the same reason, and the diagnostic never
  named the binding. It does now.

* no-mistakes(review): Harden durable merge outcome reporting

* no-mistakes(review): Make merge race regression deterministic

* no-mistakes(review): Make merge outcomes retry-idempotent and forge-confirmed

* no-mistakes(review): Unify merge publication under canonical outcome marker

* no-mistakes(review): Publish merge outcomes before committing dedup markers

* no-mistakes(review): Document at-least-once merge outcome recovery

* no-mistakes(review): Use supported GitHub confirmation and update recovery docs

* no-mistakes(review): Preserve distinct merge wakes by PR identity

* no-mistakes(document): Document durable merge outcome semantics

* no-mistakes(test): Make merge outcome interleaving test deterministic

* no-mistakes(document): Clarify merge outcome documentation ownership

* fix(lint): keep the merge-outcome library an analysis boundary

bin/fm-watch.sh followed the new merge-outcome library's source graph,
which reaches the wake queue, PR identity, and secondmate parent
libraries. Expanding that inside an already-large lint root pushed
ShellCheck's external-source analysis past the bounded CI lint worker:
the Lint job was killed with SIGTERM after five silent minutes, twice,
having emitted no diagnostics at all.

Make it an analysis boundary, exactly as the transition and inbox owners
directly above and below it already are and for the same stated reason.
Coverage is unchanged because the library is a canonical lint root in its
own right and is still linted as one.

Measured locally: the watcher goes from not terminating within 120s to
9s clean, and the library alone lints in 1s clean.

* fix(bearings): preserve projections through inventory mismatches (#3129)

* fix(bearings): keep an inventory-mismatch home readable, and mark warnings as repairs

A backlog-vs-metadata inventory mismatch inside a secondmate home was being
reported as "we cannot read that home", which discarded that home's open
captain calls, queued work, landed work, and live workers from the whole
Bearings digest. The main home already treats the identical mismatch as a
harmless disclosure; this makes the secondmate path agree.

- fm-fleet-snapshot.sh: the invalidity gate now passes orphan_in_flight,
  unowned_current, and terminal_in_flight through the partial-structured
  carve-out alongside child_current_unavailable, so those homes keep their
  decisions, holds, queued, landed, and live work and leave unreadable[].
  missing_backlog and unstructured_current stay on the discard path, because
  there the backlog itself is untrustworthy.
- fm-fleet-snapshot.sh: the same three kinds no longer collapse the home's own
  classification to "unknown"; the real captain_decision / active_child_work /
  externally_held classification survives and invalidity carries the warning.
  An unavailable child state still collapses it, including when a mismatch
  masks it under strict-invalidity precedence.
- secondmate_landed.partial now keys on partial-structured trust rather than an
  unknown state, so an inventory-mismatch home is still disclosed as partial.

Ask the home that owns the wrong books to fix them:

- bin/fm-secondmate-reconcile.sh sends exactly one reconcile instruction per
  mismatch episode through the ordinary steering transport. A persistent
  mismatch keeps its episode identity and never re-nags; a changed mismatch
  earns one more ask; a repaired one is forgotten so a recurrence is asked
  about again. The parent never touches the mate's own files, and a failed send
  records nothing so the next run retries it.

Give integrity warnings their own look on the board:

- charted rows take an optional kind of "queued" (the default) or "warning".
  A warning badges "needs repair" instead of "waiting" and is excluded from the
  Charted Next count, so alarms stop reading as dispatchable queued work. No
  fifth board section, and every existing payload stays valid.

Tests pin the new policy behaviorally: the retained surfaces and classification
for all three mismatch kinds, the still-discarding unstructured_current and
missing_backlog cases, the once-per-episode reconcile ask through real durable
steering records, and the board rendering exercised through the shipped
template under a minimal DOM shim.

* no-mistakes(review): Make reconcile dedupe atomic and warnings non-dispatchable

* no-mistakes(review): Preserve reconcile identity and reject stale snapshots

* no-mistakes(review): Order snapshots uniquely and canonicalize episode identities

* no-mistakes(review): Add fire-and-forget reconcile and separate warning overflow

* no-mistakes(review): Exclude fire-and-forget from escalation and track reconcile background

* no-mistakes(review): Run reconcile enqueue inline across all adapters

* no-mistakes(review): Track reconcile clears across strict-invalidity homes

* no-mistakes(review): Persist reconcile transitions atomically

* no-mistakes(document): Document reconcile and fire-and-forget contracts

* refactor(bearings): replace the reconcile episode dedupe with a 4-hour cooldown

The reconcile ask needed to fire once per problem without nagging on every
recap. The episode-precise record that tried to do that had to be correct in
every direction at once - order two concurrent snapshots, tell a repair from a
new problem, and never lose a clear - and each direction it got wrong either
swallowed a nudge or sent a duplicate.

A per-home cooldown removes the whole class. One durable timestamp per home,
one nudge per four hours, and nothing to get stale, mis-order, or
mis-classify: a home in mismatch is asked once, later recaps stay silent, and
a mismatch still sitting there after the window earns one gentle re-nudge.

- bin/fm-secondmate-reconcile.sh: state/<id>.reconcile-nudged holds the epoch
  second of the last ask; FM_RECONCILE_COOLDOWN_SECONDS names the window. The
  episode identity, ordering generation, pending/clear transitions, and
  delivery-identity reuse are all gone. A known-undelivered send starts no
  cooldown so the next run retries it; an unconfirmed one does, because a
  duplicate ask is worse than one the mate may already hold.
- bin/fm-fleet-snapshot.sh, bin/fm-bearings-snapshot.sh: drop the snapshot
  `observation` monotonic identity, which existed only to order those records.
- bin/fm-teardown.sh: retire the cooldown record with the endpoint's other
  runtime artifacts, so reseeding a retired id is not silenced by its
  predecessor's window.

The inline durable fire-and-forget send is unchanged, and the projection fix
and the warning surface are untouched.

Tests follow the behavior: the cooldown suite now pins one ask per window, the
re-nudge after it, the four-hour boundary, per-home independence, and that the
ask stays out of a re-ring ladder that still rings an ordinary steer beside it.
The obsolete observation-ordering test is deleted with the machinery it covered.

* no-mistakes(review): Serialize reconcile cooldown commits with mate lifecycle

* no-mistakes(review): Reject stale reconcile snapshots across mate reincarnations

* no-mistakes(review): Start reconcile cooldown after delivery completes

* no-mistakes(review): Keep reconcile sends nonblocking and remove pending residue

* no-mistakes(document): Document reconcile skip and stale-endpoint behavior

* no-mistakes(lint): Fix reconcile test subshell lint warning

* no-mistakes: apply CI fixes

* fix(bin): reconcile markerless remote secondmates safely (#3140)

* fix(bin): stop dropping reconcile nudges for markerless remote secondmates

A persistent remote secondmate's parent-side state/<id>.meta never carries
spawn_gen: bin/fm-spawn.sh's spawn_remote_secondmate() is its sole writer and
never writes one, because that incarnation identity does not apply to a
remote route. fm-secondmate-reconcile.sh's row filter required a non-empty
spawn_gen matching an identifier regex, so every such row was silently
dropped before the per-row loop ever saw it: no sent/stale/failed line, no
cooldown record, nothing sent, and no trace of why.

Give a legitimately markerless persistent remote secondmate a safe substitute
identity - its recorded remote_host - instead of weakening the spawn_gen
check for rows that do have a generation:

- bin/fm-secondmate-reconcile.sh: carry host through the row projection for
  both fm-fleet-snapshot.v1 and fm-bearings.v1 documents, and admit an empty
  spawn_gen instead of filtering the row out. A new revalidate_identity()
  compares the sampled spawn_gen against current metadata when one was
  sampled (unchanged), or the sampled host against the metadata's
  remote_host when none was sampled and the metadata still carries no
  spawn_gen of its own. A row with neither a spawn_gen nor a host has no
  safe identity at all and fails loudly instead of vanishing, exactly the
  visibility the original bug lacked.
- Rows now join on the ASCII unit separator rather than @tsv: bash's
  IFS-whitespace read collapses consecutive tabs, which would have silently
  dropped a legitimately empty field again.
- bin/fm-bearings-snapshot.sh: thread host through the secondmate_reconcile
  projection so the fm-bearings.v1 path (the one bearings itself feeds to
  the reconcile hook) carries the same substitute identity.
- tests/fm-secondmate-reconcile.test.sh: end-to-end coverage through the real
  remote transport (fm-on.sh + fm-remote-secondmate-control.sh against a
  genuinely seeded remote home) for a markerless mate nudged once per
  cooldown window, a stale/replaced remote route refused exactly like the
  existing local spawn_gen case, and a row with no identity at all failing
  loudly rather than being swallowed.

* no-mistakes(review): Enforce markerless remote host identity during final delivery

* no-mistakes(document): Document markerless remote reconciliation safety

* fix(bin): hand a busy declared pause to the away-mode daemon once, undecorated (#3147)

* fix(watch): hand a busy declared pause to the away-mode daemon undecorated

While away mode is active the daemon owns triage and the watcher reverts to
one-shot, handing over plain wake identities the daemon classifies itself. The
busy-turn bound was the one stale path that did not: with afk active it ran the
wedge timer, so the daemon received a wake already decorated as a possible wedge.

That decoration outranks the daemon's own verdict. handle_wake escalates an
enriched wedge reason before its pause classification can apply, so a crew that
declared the wait itself - a `paused:` external wait or a verified captain-held
transfer holding a live foreground call - was wedge-escalated once per
FM_STALE_ESCALATE_SECS for as long as the wait lasted, the escalation count
climbing into demand-deep-inspection on a pane nobody needed to inspect.
Measured on the pre-fix tree, five consecutive re-arms produced five escalations.

busy_turn_bound_check now reads the declaration before the afk branch: away mode
hands off the plain window identity, one-shot per distinct stale hash, leaving
normal-mode pause bookkeeping unwritten because the daemon owns it there. The
daemon then classifies the wait itself and self-handles it on the long cadence.
Normal-mode behavior is unchanged, and lifting the declaration still restores the
busy-pane wedge escalation on the same pane.

The regression covers all three: the undecorated handoff with no wedge timer or
escalation counter, the one-shot on re-arm that the escalation ladder used to
climb, and the restored wedge escalation once the declaration is lifted.

* no-mistakes(review): key afk busy-pause handoff on declaration, clear wedge state

* no-mistakes(document): docs: scope away-mode busy-bound handoff to declared waits

* no-mistakes(document): docs: note afk busy-bound handoff in watcher header

---------

Co-authored-by: Talon Stark <talonstark@gmail.com>

* fix(bin): name the stale submodule pin behind a pooled slot refusal (#3121)

* fix(bin): explain a pooled slot's stale submodule refusal

A pool slot whose submodule pin moved is refused with "is not clean; refusing
to discard uncommitted work", while the operator's own `git status` in that slot
reads clean. The message names no submodule, no pin, and no remedy, so the
refusal is unreadable and the slot looks wedged for no reason. That is the
failure that jammed three slots in a row when a submodule pin moved.

The refusal itself was never the bug and is unchanged: the gate still refuses,
and still touches nothing. It now distinguishes the one case it can prove and
says what it found - the submodule, the pin the slot has, the pin the base
records, and the command that clears it.

The diagnosis is deliberately conservative, because ` M <path>` alone cannot
tell a stale pin from real work. An entry is reported as stale only when every
reported entry is a gitlink whose submodule is internally clean and whose
recorded pin actually differs. A submodule holding uncommitted work, untracked
files, or an unpushed commit therefore keeps the original uncommitted-work
refusal, even when its pin is also stale - the remedy command would be wrong
there, and the conservative refusal is the safe answer.

Nothing is converged, synced, initialized, or deleted. There is no new failure
path: a slot that launched before still launches, a slot that refused before
still refuses, and projects that configure a submodule `ignore` are read exactly
as before. Paths are read with core.quotePath=false so a non-ASCII submodule is
named rather than falling back to the unreadable message.

Tests keep the reproductions that prove the message is accurate: the stale-pin
diagnosis (which fails against the previous refusal), work inside a submodule
still refused as uncommitted work, and a stale pin carrying real work refused
conservatively rather than called stale. Each asserts the slot is left untouched.

* no-mistakes(review): require remote containment before calling a submodule pin stale

* fix(bin): stop printing a remedy the containment check cannot stand behind

The stale-pin diagnosis printed `git submodule update --checkout` as the command
that clears the slot. The containment check behind it reads local refs only and
never fetches, because this gate has to stay usable offline. A remote-tracking
ref that has gone stale - its upstream branch deleted or force-pushed, and never
pruned - still reads as containment, so a commit that is really unpushed can look
contained and that command would move the submodule off it.

Naming the submodule and both pins is the whole point of the diagnosis: it turns
"is not clean", on a slot whose own `git status` reads clean, into a statement of
which submodule drifted and where it drifted from. The operator can choose the
remedy from that, seeing the whole picture. Printing an instruction that rests on
a judgement which can be fooled is worse than printing none, so it is dropped.

The limitation is now stated where it applies, in the script header and beside
the check itself, rather than left for a reader to discover.

No fetch is added: the gate stays offline-safe by design. Nothing else changes -
the same conditions are refused, the slot is still never touched, and a submodule
carrying real work or an unpushed commit still keeps the conservative
uncommitted-work refusal.

* no-mistakes(review): bound submodule containment probe to first commit

* fix(pi): prevent stale captain outcome re-emissions (#3154)

* fix(pi): type captain supervision outcomes so main relays them

A captain-relevant branch outcome reached main as a bare user message with
no marker of origin or required action, written in main's own captain-facing
voice, landing in a tail that often already held several such notes. Pi keeps
only a custom message's content when it builds the provider request, so
customType and display never reach the model and content was the only place
that identity could live.

Main could not tell an incoming outcome from its own earlier answer and
sometimes re-emitted that answer instead of relaying the outcome, losing it.
Measured against real Pi 0.84.1 on openai-codex/gpt-5.6-sol: 6 failures in 24
turns, rising to 3 in 6 once one stale answer was already in the tail, which
is how one captain conversation saw six identical messages in a row. The same
scenario with the outcome typed failed 0 times in 14 turns.

Wrap only the captain-verdict note in the branch-outcome operational kind
owned by bin/fm-operational-input.sh. Delivery is otherwise unchanged: still
display: false, still one triggerTurn follow-up, so the turn remains the
single captain-visible outcome and no hidden note is ever shown twice.
Routine notes stay plain because their renderer reads the glyph off the front
of that same string. An outcome that cannot be encoded degrades to the same
instruction as plain text rather than being lost, matching this file's stated
failure direction.

The existing assertions could not catch this: they pin the sendMessage
options and never look at what main receives. Add a portable regression that
classifies the delivered payload with the real protocol executable, and a
live guard that runs the real Pi SDK's own convertToLlm to prove content is
the entire model-visible payload.

* no-mistakes(document): Document typed Pi captain outcomes

* fix(bin): keep a declared wait on the pause cadence under a busy pane or enriched wedge (#3155)

* fix(bin): keep a busy pane from retiring a still-declared wait's window

The away-mode daemon's pause re-surface recheck (housekeeping step 2b) read a
busy pane as "the crew resumed" and dropped the declared-wait marker, without
re-reading that the crew's own latest status line still declared the wait.

That inference is not safe, because a declared wait can legitimately hold a pane
busy: a worker sitting on a long foreground call keeps that call live for as long
as the wait lasts. The marker is then cleared while the declaration still stands,
and migrate_watcher_pause_markers recreates it with a fresh timestamp on the very
next tick, so the window restarts forever and the wait never matures into its one
bounded recheck.

Away mode makes that terminal. Since the watcher half landed, a busy pane under a
declared wait is handed to the daemon exactly once per declaration and never woken
again while the declaration stands (bin/fm-watch.sh, busy_turn_bound_check), so
this recheck is the only thing left that can re-surface the pane at all. Measured
end to end on a throwaway state root, away mode active, a pi pane busy past
FM_BUSY_TURN_MAX_SECS, status still `paused:`, over six PAUSE_RESURFACE_SECS
windows: 0 captain-facing rechecks before this change, 6 after - one per window,
with the marker reset each time.

The fix drops only the busy arm of the 2b probe, leaving it an endpoint-readability
check: exit code 2 still means the capture failed, so the endpoint is gone and the
marker goes. The loop head above already drops the marker the moment the status
line stops declaring the wait, so nothing else is needed to end the routing, and
the reconcile path runs before the probe ever reads a pane.

tests/fm-daemon.test.sh: test_housekeeping_paused_resumed_cleared pinned the old
inference on purpose - its fixture's status line still read `paused:` while the
pane was busy, and its comment read "A pause whose pane became busy again (the
crew resumed)". Its fixture now resumes the way a crew actually resumes, by
appending a non-declaring status line, and it asserts its own busy verdict first
so it cannot silently decay into the idle-pane case that
test_housekeeping_paused_unpaused_cleared already covers. What it pins is now the
inverse guard: a busy pane must not GATE the clear either, so an over-correction
that kept the marker alive whenever the pane is busy would fail it.

test_housekeeping_busy_declared_wait_matures_its_window is the new regression, over
both declaration forms. It asserts the busy verdict, then that ticks inside the
window neither escalate nor let the marker be recreated with a fresh timestamp,
then exactly one recheck past the window named for the right human and never a
wedge, then silence on the next tick inside the reset window. It fails on
unmodified main with "produced 0 escalations past its window, expected exactly
one".

Refs #3149

* fix(bin): let a declared wait outrank an enriched wedge escalation

handle_wake classifies a stale wake through classify_stale, which returns a
`pause` verdict for a crew whose latest status line declares an external wait or
a verified captain-held transfer. It then threw that verdict away whenever the
wake reason matched `idle *s, possible wedge, escalation *`, so the watcher's
enriched wedge decoration outranked the crew's own declaration and a healthy
declared wait was escalated once per FM_STALE_ESCALATE_SECS for as long as the
wait lasted.

The enriched reason earns its precedence over the daemon's cheaper status-log
absorption honestly - it carries the watcher's escalation count and its explicit
"do not re-absorb on the run-step/pane state alone" demand. A `pause` verdict is
not run-step or pane state. It is the crew's own declaration that this pane waits
by design, which is precisely the question the wedge timer cannot answer for
itself, so it is the one verdict that decoration must not override.

The two classifications genuinely disagree in steady state rather than only in a
race: a crew that declares `paused:` while its no-mistakes run is still attributed
to its code reads `working` to the watcher's pause_state_class, so the watcher
takes the wedge timer while the daemon's classify_stale reads the status log and
correctly returns `pause`.

The wait stays bounded, not silenced. Absorbing to the pause action records the
declared-wait marker and drops wedge aging, and housekeeping (2b) then owns the
re-surface, so the pane still reaches the captain - once per PAUSE_RESURFACE_SECS
as an explicit "recheck whether the wait still holds", instead of once per
FM_STALE_ESCALATE_SECS as a possible wedge. Measured on a throwaway state root
over five wedge cadences for one declared wait: 5 escalations climbing to
demand-deep-inspection before this change, 0 after, with the one bounded recheck
still delivered.

tests/fm-daemon.test.sh: test_stale_diagnostic_wedge_survives_busy_housekeeping's
`paused` case pinned the old precedence on purpose, asserting exactly one
escalation carrying the demand-deep-inspection payload. That case now asserts the
pause cadence instead - no escalation inside the window, pause tracking recorded -
while the `working` and `prior-terminal` cases keep asserting the enriched wedge
verbatim, so the override itself is still pinned everywhere it is correct.

test_enriched_wedge_under_declared_wait_uses_pause_cadence is the new regression.
It asserts the fixture's own classifier verdict is a pause first, so the case
cannot go vacuous, then drives four consecutive wedge-cadence deliveries in both
the plain and demand-deep-inspection forms through the real handle_wake and
housekeeping pair, then matures the window for exactly one awaiting-external
recheck, then lifts the declaration and requires the same enriched wedge to
escalate again unchanged. It fails on unmodified main at the first delivery.

Refs #3149

* no-mistakes(review): align afk skill recheck wording with still-declared contract

* no-mistakes(document): daemon doc comments: pause window ages on declaration

---------

Co-authored-by: Talon Stark <talonstark@gmail.com>

* fix(bin): recover Claude auto-arm from hung claims (#3156)

* fix(bin): make Claude auto-arm continuity self-heal past a hung claim

On a Claude primary, a Stop-hook auto-arm process that hung mid-arm held
the single-flight owner lock with its epoch ledger frozen at
outcome=arming, and the abandonment proof read any live lock holder in
arming as legitimately deciding forever. Every later Stop firing exited
0 at the lock, the turn-end guard kept deferring to the hung owner as
recovery under way, and the watcher was never auto-re-armed again for
the rest of the session - supervision survived only on manual arms and
lapsed between them (the 2026-08-26 watcher flap).

Corrections layered onto the lock-held-across-arm shape each reopened
the same concurrency class one level down, so this replaces the claim
machinery wholesale with a generation-based optimistic design:

- The epoch ledger's monotonic sequence IS the claim generation; the
  two-line entry (classic epoch record plus the claimant's MANDATORY
  pid-identity) is the claim. Every firing defers to a live OPEN claim:
  outcome arming, owner alive, identity recomputes and matches, and not
  stuck (entry and watcher beacon both older than the guard grace).
- A finished, dead, identity-mismatched, identityless, or stuck claim
  is superseded by simply taking the next generation - no signalling or
  revocation of a steady-state predecessor.
- No mutex is held across arming or output; the owner lock survives
  only as a micro-mutex around individual ledger writes. A superseded
  owner goes completely silent: ownership is re-verified before every
  arm invocation, episode-state mutation, ledger write, and
  continuation.
- The irrevocable commit point of a translation is the exit status
  (the harness delivers the collected stderr only on exit 2), so the
  owned terminal ledger write is the atomic commit: the winning
  generation exits 2 unconditionally after it, a refused one exits 0
  silently even after printing, and the once-per-episode failure notice
  commits in the same owned critical section as the winning failed
  write. Two bounded residuals are documented accepted intent: an owner
  dying between its owned write and its own exit, and a hung old-build
  owner resuming during the one legacy upgrade window.
- The pre-generation lock-holding claim shape keeps defer-or-reclaim
  behavior through a legacy shim: a live identity-verified stuck owner
  is retired via TERM (with a queued TERM sufficient when the owner is
  stopped) before its lock is removed, an unverified or identityless
  pid is never signalled but never blocks a proven-abandoned reclaim,
  and the lock's identity evidence is grafted into the ledger
  (mtime-preserving) so pid-reuse protection survives the lock.
- The guard reads the same predicates for recovery ownership and its
  terminal fail-open (which re-checks for a live open claim under the
  held locks before committing the attended alarm), with ledger reads
  anchored to line 1 so the identity line can never confuse them.

Behavioral regression coverage exercises all three edge classes through
the real hook and guard - a live open claim defers with no lock held, a
stuck claim is superseded and the home re-arms, and an end-to-end run
with a genuinely hung owner shows a concurrent firing deferring
promptly mid-arm, a later firing superseding the stuck owner, and the
superseded owner exiting silently without a second translation - plus
the identityless/reused-pid loopholes, the superseded-owner arm
boundary, and the legacy TERM, SIGSTOP, and signal-free reclaim paths.

* no-mistakes(review): Refuse auto-arm commits when notice marker creation fails

* no-mistakes(review): Make episode reset atomic with generation ownership

* no-mistakes(document): Update auto-arm generation and commit documentation

* fix(bin): verify the real GitHub merge outcome instead of reporting an unproved merge (#3064)

* fix(pr): verify GitHub merge outcome

* no-mistakes(review): Captain, fixed forge-only merge verification, queue guidance, metadata propagation

* no-mistakes(document): Correct forge-specific merge documentation

* no-mistakes(review): Captain: forge-only queue fix, focused tests pass

* no-mistakes(review): Captain: suppress closed-state guidance and prove parent regression

* no-mistakes(review): Captain: remove history proof; retain executable regressions

* no-mistakes(document): Clarify GitHub recording timing in architecture docs

* no-mistakes(document): Clarify outcome-aware PR merge recording documentation

* no-mistakes: apply CI fixes

* Revert "no-mistakes: apply CI fixes"

This reverts commit c326cfa9430c6173eedc8ff7f27d19d0552daf01.

The automatic CI repair round removed the up-front `gh` prerequisite check
while keeping the `gh` dependency: `bin/fm-pr-merge.sh` still calls
`gh api graphql` for the outcome read and `gh api` for the branch-rules read.
That left the same hard requirement without the clear named error, and review
immediately raised a new finding for exactly the failure the check prevents -
`gh-axi pr merge` landing the merge while the follow-up read fails, so the PR
metadata is never recorded.

The check is also symmetric with the GitLab arm directly above it, which
already refuses up front when `glab` or `jq` is missing, on the stated
principle that a missing tool should be a named prerequisite rather than a
merge that is armed and then refused for an unexplained reason.

The workflows this round was chasing sit at `action_required` because this is
a fork pull request; no code change can turn them green.

* fix(pr): keep PR bookkeeping when a merge outcome read fails

On the GitHub path a merge call that returned success was followed by
`github_read_outcome || exit 1`, so a transient API failure, rate limit,
or network blip during the read dropped out of the script before
`record_pr_metadata` ever ran. The merge could have landed while `pr=`
went unrecorded and the merge poll was never armed - bookkeeping lost on
a real merge. The failure path just above already recorded metadata
before exiting, so the error path was more careful than the success one.

Record the PR before that refusal. Recording arms the later merge poll
and is not a success claim, which is the same reasoning that keeps
`record_pr_metadata` on the gh-axi failure path. The refusal itself is
unchanged: exit stays non-zero and the message still names the concrete
observed state. Metadata is withheld only when the read succeeds and
proves the pull request neither merged nor queued.

Pin it with a case that stubs `gh api graphql` into failure after a
successful `gh-axi pr merge`, asserting both the non-zero exit and the
recorded metadata.

* no-mistakes(review): Aggregate queue rules and report conflicts explicitly

* fix(pr): keep the merge abstraction reachable and its bookkeeping intact

Two holes remained in the outcome-verified GitHub merge path, both on
installations where gh-axi is present but gh is not.

The verification preflight refused before bin/fm-pr-merge.sh ever reached
the configured gh-axi merge abstraction, so an installation without gh
could no longer merge at all. gh-axi now performs the merge unconditionally
and the queue-aware gh read became an optional enrichment: with gh on PATH
its GraphQL view still separates merged from queued, and without gh the
gh-axi view still proves a landed merge while every outcome it cannot prove
refuses.

The PR metadata recording sat behind the outcome read, so a merge that
landed before that read failed lost pr= and its merge poll. Recording now
happens once, before either forge call, which arms the poll without
claiming a landed outcome and leaves teardown a PR identity to verify
against no matter how the read ends.

Rebasing onto main also restored the durable merge-outcome reporting and
the GitLab landed-state confirmation that the conflict resolution dropped.

Tests pin each fix through the executable interface: the merge abstraction
is reached and verified with gh absent, a failed fallback read keeps its
bookkeeping, and a mock that snapshots the task meta during the forge call
proves pr= is recorded before the merge can land.

* no-mistakes(review): fix(pr): de-dup queue methods, fall back on failed gh read, refresh contracts

* no-mistakes(review): fix(pr): quote forge output and explain armed auto-merge on refusal

* no-mistakes(review): fix(pr): claim auto-merge armed only when the forge accepted it

* no-mistakes(review): fix(pr): tell the operator what each GitHub refusal could not observe

* no-mistakes(review): fix(pr): gate every forge-acceptance claim on a successful merge

* no-mistakes(document): align merge docs with verified GitHub outcome contract

* fix(pi): prevent duplicate captain outcome reports (#3184)

* fix(pi): stop reporting one merge to the captain twice

The supervision branch's captain-outcome note told main, unconditionally,
that the note "is not your own earlier output" and to relay it now. When
main had already reported the same event, that assertion was false and the
order turned the correct response - saying nothing new - into a mechanical
re-report, so the captain saw one merge reported twice in 16 seconds.

Two independent changes, both needed:

- The relay instruction is now conditional. It still names itself as a
  supervision outcome so main cannot mistake it for its own earlier answer
  (the silent loss that instruction exists to prevent), and it now lets
  main stay quiet about an outcome it has already given the captain.

- The merge case is closed at its source rather than left to that judgment.
  One merge reaches a home on two independent paths by design - main's own
  permanently main-owned merge poll, and the branch's task-local status
  wake - and main's captain-facing text only reaches the branch's mirror at
  main's turn end, so the branch can escalate before it could possibly see
  the captain was already told. bin/fm-pr-merge-notified.sh answers that
  question from bin/fm-pr-lib.sh's canonical merge-notification marker, so
  the answer holds regardless of mirror timing. A captain outcome naming an
  already-published merge is delivered as the ordinary rendered note
  instead of opening a follow-up turn: still appended, still visible, still
  recorded with the verdict the branch decided, minus the wasted turn.

Any error, timeout, or unreadable state relays the outcome. A duplicate
announces itself; a lost outcome does not.

Regression coverage drives the real delivery path in both directions: a new
outcome must still reach the captain in exactly one follow-up turn even
beside an unrelated published merge, and an already-published merge must
open no second turn while a different PR in the same task still does. The
merge path's real producer and this new consumer are exercised end to end
in tests/fm-pr-merge.test.sh.

Pi-only by construction: the delivery path lives in .pi/extensions, so no
other harness loads it, and the new script only reads existing markers.

* no-mistakes(review): Document accepted latest-marker suppression residual

* no-mistakes(review): Recheck ownership before merge outcome delivery

* no-mistakes(document): Document merge-outcome suppression exception

* refactor(pi): drop the source-level merge suppression, keep the envelope fix

The captain reviewed this branch and judged the source-level duplicate
suppression overly complicated for the problem it solved, and asked for
the change to be reduced to the envelope wording alone.

Remove the mergeIntoMain downgrade path, bin/fm-pr-merge-notified.sh, and
every test and document that existed only for it. What remains is the
conditional captain-outcome instruction: main is told to stay quiet about
an outcome it has already reported and to relay anything else, which
covers the duplicate without a second mechanism.

The silent-loss protection is untouched - the note is still typed,
self-describing, and delivered as one invisible follow-up turn - and the
behavioral tests still assert that, now requiring both halves of the
conditional instruction.

* no-mistakes(ci): Clarified in code comments and owned documentation that this is intentionally an M1-only, model-facing conditional relay fix—not source-level suppression—addressing Greptile’s mistaken scope expectation without changing runtime behavior. Net diff remains 3 files and 27 insertions. Verified with fm-pi-branch-extension tests, fm-lint, doc audience check, and git diff --check; all passed

* no-mistakes(ci): Strengthened the runtime delivery test to verify the captain outcome retains its required self-description and outcome text. Verified with `bash tests/fm-pi-branch-extension.test.sh`, `bin/fm-lint.sh`, `bin/fm-doc-audience-check.sh`, and `git diff --check`; all passed. The outer pipeline can now commit and attest the new head

* fix(bin): prioritize active pipeline-owned crew runs (#3194)

* fix(bin): bind the live pipeline-owned run instead of a superseded failed row

fm-crew-state.sh bound a superseded FAILED no-mistakes run to a task instead
of the LIVE replacement run: the live run's pipeline-owned lane head is not a
git object in the task worktree, so head-equality attribution rejected it and
the coarse runs-list fallback silently continued past the RUNNING row onto an
older failed row whose head equalled the stale worktree HEAD. The home summary
then flipped invalid and Bearings hid the home's live work (F10).

Attribution precedence now follows the daemon's own identity:
- An ACTIVE run for the task's branch binds without head equality while
  branch_sync.state is pipeline_owned (fm_nm_run_is_pipeline_owned_active);
  the pipeline owning the branch is itself the attribution.
- A genuinely failed run with no later run on the branch still reports failed
  through the unchanged head-equality path - real failures are not hidden.
- In the coarse runs scan, an unresolvable head is unknown attribution and
  stops the scan (fm_nm_head_resolvable) instead of falling through to an
  older row; a resolvable-but-mismatched head keeps the historical
  reused-branch skip.

The exemption never applies to a terminal run and requires pipeline_owned
specifically, both pinned by negative-control tests. Fixture shape verified
against the live incident run's real axi status output.

* no-mistakes(document): Updated run-attribution documentation ownership

* fix(pi): surface requested outcomes without replaying fleet events (#3211)

* fix(pi): surface requested supervision outcomes

* no-mistakes(review): Mirror in-flight captain requests before branch dispatch

* no-mistakes(review): Exercise real branch ownership and main outcome access

* no-mistakes(review): Preserve request tails and align verdict guidance

* no-mistakes(review): Preserve complete current captain requests

* no-mistakes(review): Require visible requested outcomes and realistic classification

* no-mistakes(document): Align supervision outcome documentation

* no-mistakes(ci): Fixed Greptile’s runtime-ordering finding. The extension now stages Pi’s authoritative `before_agent_start` prompt before SessionManager persistence and suppresses the later duplicate entry. Updated docs and behavioral regression to reproduce real Pi ordering and verify each prompt is mirrored exactly once. Passed branch-extension tests, supervision tests, strict Pi typecheck, full lint, and diff checks

* no-mistakes(review): Use canonical operational input classification

* no-mistakes(review): Filter legacy operational inputs canonically

* no-mistakes(document): Clarify captain request mirroring boundary

* no-mistakes(ci): Fixed the CI time-boundary failure in tests/fm-public-followup.test.sh by pinning its clock, including context-registry setup. This prevents follow-up fixtures from expiring based on wall time. Verified the full regression suite passes, project-owned lint passes, and git diff checks are clean

* no-mistakes(document): Clarify captain-visible supervision outcome documentation

* feat(bin): add concurrent bounded remote transport lanes (#3210)

* feat(bin): per-home remote transport lanes with cancellation, bounded send, and closed stdin

All remote commands for every home on one host used to serialize through one
single-job-at-a-time worker on one shared queue: a timed-out caller abandoned a
staged job that kept running, retries convoyed behind it, fm-send's remote leg
had no time bound, and staging captured the caller's stdin to EOF so any
fm-on.sh caller with an open stdin wedged staging indefinitely.

- The worker now serves one lane per staged home: same-home jobs run strictly
  FIFO in a new staging-sequence order while different homes run concurrently,
  each lane as its own top-level worker process (a backgrounded subshell does
  not reliably reap dead children, so a zombie group leader kept a finished
  command's process group signalable). Long-poll preemption is lane-scoped.
- A caller that disconnects or times out cancels its job: the entrypoint marks
  the record on any post-staging exit and probes its parent so a dead ssh
  channel cancels without a signal; the worker skips cancelled queued jobs,
  terminates a running cancelled job's process group, and reaps the record.
- fm-send's remote leg is bounded by FM_SEND_REMOTE_BUDGET (default 30s) and a
  bound hit exits through the existing unconfirmed-delivery contract, which
  stays idempotent because the remote enqueue deduplicates.
- fm-on.sh defaults the remote command's stdin to /dev/null; the three payload
  callers pass the new --stdin flag. Abandoned .stage.* litter is age-reaped.
- The job execution deadline no longer loses up to a second to clock
  truncation.

* no-mistakes(review): Protect live stages and validate send budgets early

* no-mistakes(review): Preserve sequence lock ownership during stale recovery

* no-mistakes(review): Allocate job sequences at publication boundary

* no-mistakes(review): Bound remote keys and extend stale lock recovery

* no-mistakes(document): Document bounded remote transport behavior

* no-mistakes(lint): Suppress intentional deferred-expansion lint warning

* no-mistakes(ci): Fixed stale sequence-lock recovery by reconciling the counter against published job records before allocating the next sequence, preventing duplicate sequences and same-home FIFO violations. Added a behavioral regression test reproducing displacement after publication and verifying execution order. Passed fm-remote-transport-lanes.test.sh, fm-remote-job.test.sh, fm-lint.sh, and git diff --check

* no-mistakes(review): Use atomic sequence claims and lossless lane keys

* no-mistakes(review): Recover regressed sequence hints and rate-limit claim reaping

* no-mistakes(review): Restrict worker heartbeats to serving loop

* no-mistakes(review): Verify supervisor identity before lane recovery signals

* no-mistakes(review): Verify tracked lane and claim owner identities

* no-mistakes(document): Clarify remote lane and transport contracts

* no-mistakes(ci): Fixed the CI time-boundary failure by pinning fm-public-followup tests to a deterministic clock, including context-registry setup. Verified tests/fm-public-followup.test.sh, tests/fm-remote-transport-lanes.test.sh, shellcheck, and git diff --check

* no-mistakes(review): Preserve assigned lane ownership of queued jobs

* no-mistakes(review): Reserve homes owned by foreign queued lanes

* no-mistakes(review): Preserve completed results during crash recovery

* no-mistakes(review): Harden claim cleanup, expiry, and cancellation races

* no-mistakes(review): Verify process groups and reap abandoned results

* no-mistakes(review): Stop leaderless groups and reap cancelled publications

* no-mistakes(document): Correct remote transport lifecycle documentation

* no-mistakes(lint): Quote done state comparisons for ShellCheck

* fix(bin): accelerate and bound changed test runs (#3250)

* fix(tests): make the changed-file map select per script and stabilize a budget flake

The changed-file map's bin/ fallback resolved a direct test reference to that
test's whole FAMILY. bin/fm-push-transition-lib.sh is named by exactly one
real-Herdr E2E, so a one-line change to it selected all 12 real-herdr-gated
scripts, including a 341s presentation E2E with no dependency on it.

Resolve direct test references per script, and keep resolving consumer bin/
scripts through the curated map so recorded family-level coupling survives.

Also fix a load-sensitive flake: the tool-update budget deadline is whole-second
granular, so a test budget of 1 left headroom anywhere in (0, 1] seconds and the
first budget check could already read as exhausted.

* feat(bin): make suite wall clock a result and let a family's concurrency be proven

--max-wall-ms fails a run whose wall clock exceeds the caller's budget, after
reporting the per-script results. A suite that stays green while outgrowing its
caller's invocation budget is the regression that got an agent killed mid-run
and retried invisibly, so duration has to be a result rather than a log note.

--pool on the isolation-proof harness runs the same concurrent proof over a
whole family, so 'is this family safe to parallelize?' is answered by a command
instead of a guess. Measured watcher-wake-lock and refused it: 3 of 18 scripts
fail under concurrency on wall-clock assertions about reaching the next poll.

* perf(bin): schedule the changed suite concurrently, longest first

The watcher-wake-lock family is proven…
Alchemy86 added a commit to Alchemy86/firstmate that referenced this pull request Sep 14, 2026
* fix(remote-job): stop workers abandoned by a pruned code root (#1927)

29 fm-remote-job-worker.sh processes were found running at ppid 1, 1-2 days
old, each still polling and appending to a log inside a no-mistakes gate
worktree that had already been returned.

Three things combined to make that possible:

- The recorded worker.pid is the serving child, not the restart supervisor
  above it, so a teardown that stops that one pid only makes the supervisor
  respawn. The Linux start path also left the worker tree in the launching
  command's process group, so there was no group to signal instead.
- Neither the serving loop nor the supervisor ever rechecked whether its
  configured FM_ROOT still existed, so a worker launched from a worktree
  outlived that worktree indefinitely.
- The supervisor restarted a failing child with a fixed 0.1s delay and no
  bound, which is what grew the logs (~66MB/day measured).

The Linux start path now puts the worker tree in its own process group, and
fm_remote_job_stop_worker_tree signals that whole group - refusing any group
whose leader is not itself a worker, so a worker from an older build or from
launchd's own session is still stopped safely as a single process. The worker
stops itself once its code root stops being a Firstmate checkout, confirmed
across a grace window so an ordinary transient cannot stop a healthy worker.
The supervisor backs off and gives up rather than restarting forever.

bin/fm-remote-job-reap-orphans.sh is the belt-and-suspenders sweep for workers
already orphaned that way, wired into fm-teardown.sh. Its reap condition is
exactly "the code root named in the worker's own command line is gone", which
is why the account's healthy LaunchAgent worker and every live remote
secondmate worker are never candidates.

The two suites that leaked these in the first place now stop the worker tree
rather than the recorded pid alone.

* feat(bin): lint only the changed shard locally, full lint in CI (#1925)

* fix(bin): lint only the changed shard locally, full lint in CI

Two ships hitting fm-lint.sh at once could spike CPU to 190% and load
to 8.58 on a captain's Mac, even though each run finishes quickly.
fm-lint.sh now defaults to linting only the canonical-set files
changed since the merge-base with origin/main (including uncommitted
edits) on an ordinary local branch, using plain local git with no
network calls. It still lints the full canonical set in CI
(GITHUB_ACTIONS=true or CI=true), on the main branch, or whenever no
merge-base can be found, so CI coverage never depends on a local diff.
Explicit paths keep bypassing this selection entirely.

* no-mistakes: apply CI fixes

* feat(bin): add deterministic agent lifecycle control (#1568)

* feat(bin): add deterministic agent lifecycle control

Separate firstmate's data plane from its control plane.

bin/fm-send.sh is the data plane: conversational text, always
routing-marked for a kind=secondmate target. That marking is right for a
message and wrong for a lifecycle command - a marked "/quit" arrives as
ordinary chat the agent reasons about instead of executing.

bin/fm-control.sh is the control plane: allowlisted interrupt, exit, and
transactional relaunch verbs addressed to an exact task id, with
per-harness mechanics owned by the executable bin/fm-control-lib.sh
rather than improvised in agent prose, and a verified postcondition for
every action. There is no arbitrary-text and no raw-key entry point.

relaunch runs as a transaction with a durable journal: it resolves the
profile, proves the work it must preserve is recoverable, records the
required progress note, stops the old agent, then delegates the launch
to its single owner, bin/fm-spawn.sh --relaunch, which adopts the
recorded endpoint and worktree instead of creating either. A refusal
before the stop leaves the record and instructions byte-identical; a
failure after it reports the concrete state rather than claiming an
agent that is not running. Teardown and discard stay separate and
explicit.

exit and relaunch require a backend with a recovery-grade agent-state
classifier, so zellij, orca, and cmux are refused rather than reported
as successful blind. A remotely placed secondmate is refused by name,
because its agent runs on a host where none of these postconditions can
be read.

* fix(control): resolve a recorded harness to its adapter before retiring wiring

fm-spawn arms per-task harness wiring on prefixes, because a task
launched from a raw command records that command's basename rather than
the exact adapter name. The control plane's retirement tables are keyed
by the exact adapter, so a task recorded as `grok-2` had its turn-end
token, private registry entry, and worktree hook pointer armed and never
retired - leaving a registry entry that outlived the agent that owned
it.

State the prefix rule once, in the capability owner, and resolve the
recorded value through it before every table lookup. bin/fm-send.sh's
composer-clear lookup reads the same owner instead of keeping its own
copy of which adapters need one.

* test(control): pin muse session-binding retirement across a harness switch

* no-mistakes(review): Resolve prefixed harnesses across lifecycle control verbs

* no-mistakes(review): Report interrupt delivery without fabricating cancellation state

* no-mistakes(review): Clear disabled relaunch trace context atomically

* no-mistakes(review): Clarify control interrupts and restore legacy send state

* no-mistakes(review): Refuse ambiguous relaunches and report exit delivery

* no-mistakes(review): Revalidate interrupts and accept interrupt-stopped exits

* no-mistakes(review): Lock descendant tasks before forced recursive teardown

* no-mistakes(document): Align lifecycle adapter documentation with control plane

* no-mistakes: apply CI fixes

* fix(bin): serialize fresh task publication with forced teardown

Forced secondmate teardown enumerated a home's task set, locked what it
found, then re-enumerated while removing. A fresh spawn takes only its
own per-task lock, so a record published inside that window was
invisible to the preflight and visible to the cleanup: it was
destructively processed while never lifecycle-locked.

Reproduced with real agents. A record published 0.249s after teardown
began was removed, its window closed, and its worktree returned to the
pool - while both commands reported success. A per-task lock cannot
protect a task that does not exist yet.

Add a per-home task-set lock guarding WHICH tasks a home has, as opposed
to the metadata lock guarding one task's record. Teardown takes it per
home, parent before child, before enumerating and holds it through
cleanup. A fresh spawn takes it before its own per-task locks and holds
it through publication; a relaunch is exempt, because it republishes an
existing task already covered by that task's control lock.

Either the spawn publishes first and the teardown's preflight covers it,
or the teardown owns the set and the spawn refuses. Both directions fail
closed, and both are pinned by tests that hold the lock rather than
racing on timing.

* no-mistakes(review): Serialize remote secondmate publication with forced teardown

* no-mistakes(review): Preserve remote spawn routing and state initialization

* no-mistakes(review): Serialize teardown when descendant state is absent

* no-mistakes(review): Cover symlinked descendant state refusal

* no-mistakes(document): Document task-set serialization safeguards

* no-mistakes(lint): Isolate task-set lock path resolution

* no-mistakes: apply CI fixes

* feat(stow): add tiered decaying memory management (#1984)

* feat(stow): tiered decaying memory with captain-gated offload to local excluded skills

Implement the captain-adopted /stow redesign from the v2 tiering report as
amended by the adoption decision:

- Per-entry trailing HTML-comment markers with three tiers named for their
  handling: pinned (no clock, no eviction), aging (stale after 30 days),
  perishable (stale after 7 days, mandatory checkable expiry condition).
- File-scoped defaults (captain.md and captain-shared.md pinned,
  learnings.md aging) with a self-describing legend line per file header.
- Reinforcement requires session evidence; re-reading memory never counts.
- Archive-not-delete: stale and budget-evicted entries move with provenance
  to the never-injected data/memory-archive.md; prune always means the cold
  tier, and a stale unique fact is never deleted.
- Captain-gated over-budget offload: staleness evaluated before scope, the
  sweep runs only when still over budget after decay and consolidation,
  proposals go through the receipt plus one durable captain-held backlog
  item, migration runs through the destination's normal path, and the
  memory entry leaves only once the destination is live.
- Offload destination per the adoption decision: a user-owned skill under
  .agents/skills/<freeform-name>/ excluded via the local .git/info/exclude,
  with the hard rule that stow never creates or writes a tracked skill.
- Five graduation moves, receipt verbs archived and proposed-offload, and
  the one-time non-destructive migration of unmarked legacy entries.

The public skills/stow/SKILL.md mirrors the generic parts (markers, decay,
archive exit, user-approved on-demand offload exit, migration) with no
firstmate-specific paths.

The load-bearing assumption that a git-excluded skill is still discovered
was verified empirically against Claude Code 2.1.226 (direct
.git/info/exclude scratch-repo test plus an in-repo ignored-probe test);
the dated evidence is recorded in docs/verification/stow-memory.md.

The graduation list's deletion move is deliberately narrowed to duplicates
already preserved by a stronger owner, reconciling the v2 report's retained
'deletion of a stale entry' wording with its own prune-always-archives
rule.

* no-mistakes(review): Persist legacy migration grace across stow passes

* no-mistakes(review): Enforce archival invariants and exempt default-pinned legacy entries

* no-mistakes(review): Clarify offload scope, archive placement, and marker boundaries

* no-mistakes(review): Enforce aging fallback and verify excluded skill loading

* no-mistakes(review): Fix stow decay, pinned offload, and archival safeguards

* no-mistakes(review): Preserve pinned entries, approvals, and archive provenance

* no-mistakes(review): Restrict stow mutations to editable memory files

* no-mistakes(review): Clarify skill destinations, collision checks, and migration legends

* no-mistakes(review): Resolve exclude paths for linked worktrees

* no-mistakes(review): Secure per-home excluded skill migration

* no-mistakes(test): Require explicit tier markers on new stow entries

* no-mistakes(test): Route missing shared legends to primary owner

* no-mistakes(document): Align stow documentation with tiered memory

* fix(stow): converge the pass on an over-budget home (dogfood D1-D3)

The dogfood run against a copy of the real over-budget home showed the
pass increasing the deficit from 624 to 1,107 estimated tokens and the
relief ladder provably unable to reach budget. Three skill-text fixes:

- D1: markers become single-token spellings (<!--a:DATE-->, <!--p:DATE-->,
  <!--P-->, <!--g-->), entries matching a pinned file default carry no
  marker, the per-file policy legend collapses to a one-line pointer
  naming the stow skill as the scheme owner, and marker/pointer bytes are
  explicitly counted content - roughly 76% less metadata cost on the
  dogfooded home's first installment.
- D2: the eviction rung gains a convergence precondition - total the
  eligible pool first, and when archiving all of it cannot reach budget,
  skip eviction entirely, archive nothing for budget reasons, and report
  the exempt pinned floor as the concrete inability in the final step.
- D3: budget eviction considers only dated aging entries; <!--g-->
  legacy-grace entries are ineligible until their grace cycle resolves,
  so eviction cannot cancel promised grace or invert against validation.

Public skill mirrors the D1 marker/pointer changes; D2/D3 are internal
because the public skill has no budget ladder.

* no-mistakes(test): Enforce evidence-only reinforcement during stow migration

* no-mistakes(document): Clarify stow receipt marker actions

* docs: add project vision (#1997)

* docs: add firstmate vision

* no-mistakes(test): Classify VISION.md as public product documentation

* no-mistakes(document): Restore approved one-file vision diff

* no-mistakes: apply CI fixes

* fix(spawn): force regular Pi TUI for crews (#2005)

* fix(spawn): force regular Pi TUI for crews

* no-mistakes(document): Documented Pi regular TUI launch mode

* fix(cmux): classify borderless Claude composers (#2029)

* fix(cmux): classify borderless Claude composer

* no-mistakes(review): Normalize cmux NBSP prompts across locales

* no-mistakes(document): Document cmux borderless Claude composer classification

* docs(stow): generalize read-before-write in the public stow skill (#2091)

The public installer-facing stow skill scoped its classify-then-replace
discipline to TODO/BACKLOG items only, so findings routed to a memory file
had no stated rule against a blind append or a wholesale overwrite.

Step 6 now classifies every finding against the destination's current
contents as new, duplicate, superseding, or obsolete, and states the
considered replacement each classification implies. The outcomes follow the
tiered-memory contract already in the file: an obsolete entry is refreshed,
archived, or replaced in a way that preserves its fact, a duplicate folds
into the entry that already carries it, and a superseded body worth keeping
leaves through step 7's existing exits rather than a second recovery
mechanism.

* fix: resurface durable supervision work after re-arm (#2065)

* fix(watcher): resurface durable work after downtime

* no-mistakes(review): Make watcher rearm recovery durable and cursor-safe

* no-mistakes(review): Persist safe recovery markers across migration lock recovery

* no-mistakes(review): Retain stale lock when recovery marker publication fails

* no-mistakes(review): Preserve delivery-gap recovery and quarantine malformed markers

* no-mistakes(review): Serialize recovery consumption and report acknowledgment failures

* no-mistakes(review): Centralize recovery publication before clearing watcher evidence

* no-mistakes(review): Guarantee recovery evidence across queue and lock handoffs

* no-mistakes(review): Publish recovery evidence before durable wake commits

* no-mistakes(review): Replace recovery marker Perl dependency with Node

* no-mistakes(review): Keep interrupted wakes durable until handling acknowledgment

* no-mistakes(review): Add post-handling durable wake acknowledgements

* no-mistakes(review): Enforce post-handling acknowledgement across recovery and AFK return

* no-mistakes(review): Bind wake acknowledgements to recovery generations

* no-mistakes(review): Align wake regressions with generation-bound acknowledgements

* no-mistakes(document): Document durable re-arm recovery semantics

* no-mistakes(lint): Resolve ShellCheck warnings in recovery and watcher tests

* no-mistakes: apply CI fixes

* test(watcher): assert post-handling wake replay

* no-mistakes(review): Prevent successor loops and adopt legacy wake generations

* no-mistakes(review): Rearm durable wakes without recursive successor recovery

* no-mistakes(review): Align recovery tests with handling marker state

* no-mistakes(review): Delay handling transition until successor launch is established

* no-mistakes(review): Confirm wake handling only after successful prompt delivery

* no-mistakes(review): Acknowledge AFK wakes only after evidence publication

* no-mistakes(review): Prevent AFK wake loss before post-handling acknowledgement

* no-mistakes(document): Document durable wake acknowledgement semantics

* no-mistakes(lint): Suppress false positive for recovery action output

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* ci: measure Herdr automation on Windows runners (#2100)

* ci: add Windows Herdr automation spike

* ci: run Windows spike on its pull request

* fix: wait for Windows Herdr command output

* fix: run ANSI probe in pane shell

* ci: keep Windows Herdr spike manually triggered

* docs: clarify Windows Herdr spike verdict

* feat(ahoy): guide captains through open decisions (#2099)

* Add guided ahoy decision flow

* no-mistakes(document): Document guided Ahoy decision flow

* fix(stow): enforce startup-memory budget decisions (#2110)

* Harden stow memory budget policy

* Refine internal stow offload policy

* no-mistakes(review): Enforce shared-budget decisions and autonomous offload

* fix(spawn): refresh pooled worktrees from origin before launch (#2116)

* fix(spawn): refresh pooled worktree base

* no-mistakes(document): Document spawn base-freshness invariant

* no-mistakes: apply CI fixes

* fix(composer): unify safe classification across backends (#2102)

* refactor(composer): one shape owner behind thin capture adapters, whole matrix fixed

Consolidate every composer shape - bordered boxes (all families, geometry,
titled bottom borders), bare agent-glyph rows and their wrap regions,
opencode's left bar, and pi's identity-gated separator pair - into
fm_composer_classify_screen in bin/fm-composer-lib.sh. Adapters now
contribute only a capture and a declarative capability descriptor
(styled/cursor/identity/rows); capability differences change how confidently
a shape is judged, never what the shapes are, so a new harness shape is
teachable in exactly one place.

Correctness fixes landed as part of the consolidation (audit
data/fm-composer-consolidation-audit-s1):
- locale-safe Unicode-space normalization in the shared owner (closes the
  fleet-wide half of #1988; cmux's local byte-exact NBSP case deleted;
  naming converges with PR #1995's normalization primitive)
- muse's bare glyph joins the shared set, unbreaking muse on herdr/cmux/orca
- orca learns the borderless bare shape, drops its backward-paged composer
  window, and can no longer classify a stale startup banner as the composer
- tmux tolerates a titled bottom border, unbreaking grok steering
- the left-bar shape makes opencode readable on every backend
- zellij gets a real classifier through dump-screen --ansi, replacing the
  content-diff submit heuristic that could confirm an undelivered message
  and close a --resolve-key decision (the fleet's only false positive)
- fm-spawn's kimi launch-readiness regex (the fourth shape copy) now routes
  through the shared classifier

The strict blank-row posture applies fleet-wide (captain decision
blank-row-injection-posture): no positive container proof = unknown = defer,
replacing tmux's permissive blank-cursor-row rule. Away-mode injection was
re-validated end to end on real tmux (defer on partial input and unproven
rows, clean delivery with swallowed-Enter retry into proven-empty
composers). The tmux submit core gains a baseline-idle turn-started
conversion so pi steering stays confirmed while its working screen hides
the composer; busy conversion without that baseline remains forbidden.

Plain-capture backends now degrade a glyph row carrying trailing text to
unknown instead of a false pending, per the approved capability rule.

Portable regressions pin the full byte-capture matrix from the audit under
a UTF-8 locale and LC_ALL=C, the strict-vs-permissive divergence, and
deliberate signal separation; the opt-in live guard
(tests/fm-composer-matrix-live-e2e.test.sh) verified every installed
harness against the real classifier, recorded in
docs/verification/runtime-backends.md.

* no-mistakes(review): Fix Pi glyph ambiguity and complete profile matrix

* no-mistakes(review): Preserve bare verdict when Pi identity probe is absent

* no-mistakes(review): Harden composer structure and titled-border geometry

* no-mistakes(review): Require proven idle baseline and strict Zellij guard

* no-mistakes(review): Reject box bottom borders as composer input rows

* no-mistakes(review): Prove Zellij probe typing before classifier retries

* no-mistakes(review): Preserve Pi identity uncertainty and scan full left-bar drafts

* no-mistakes(review): Verify Zellij text lands before submitting

* no-mistakes(review): Scope Zellij typing verification to selected composer content

* no-mistakes(review): Verify Zellij pastes through composer-scoped content deltas

* no-mistakes(review): Prove wrapped bare Zellij pastes through composer extraction

* no-mistakes(review): Invalidate stale cursorless composers below dead shell prompts

* no-mistakes(review): Handle shell prompt placeholders in composer extraction

* no-mistakes(review): Classify cursorless bare continuation regions safely

* no-mistakes(review): Reject stale cursorless containers below live activity

* no-mistakes(review): Preserve prompt glyphs in wrapped Zellij pastes

* no-mistakes(review): Reject live shell rows during composer extraction

* no-mistakes(review): Preserve wrapped glyph continuations through submit retries

* no-mistakes(review): Scope idle placeholders to proven positions

* no-mistakes(review): Restore boxed placeholders and live prompt reanchoring

* no-mistakes(review): Fix Zellij placeholder and wrapped glyph paste proof

* no-mistakes(document): Align composer architecture documentation

* no-mistakes(lint): Fix ShellCheck warnings in composer refactor

* no-mistakes: apply CI fixes

* docs(verification): record the trusted-checkout live matrix rerun

The pipeline's isolated gate worktree is untrusted, so claude, grok, and
muse stopped at first-launch trust dialogs there (the guard refuses to
confirm them by design). This rerun from the trusted checkout at the final
validated head verified all six installed harnesses, the strict blank-row
deferral, and the hardened zellij false-positive probe live.

* no-mistakes(document): Align composer verification evidence

* no-mistakes: apply CI fixes

* no-mistakes(review): Restore proven box bottom-cursor classification

* no-mistakes(review): Preserve styled placeholder-like drafts as pending

* no-mistakes(document): Align composer safety and Zellij delivery documentation

* no-mistakes: apply CI fixes

* docs(verification): refresh the live matrix with the final-head trusted rerun

The post-validation rerun from the trusted checkout verified all six
installed harnesses at the branch's final head, including Claude 2.1.227
(auto-updated since the audit's captures) and Grok, which the untrusted
gate worktree could not verify past their first-launch trust dialogs.

* fix(spawn): gate Pi TUI mode by CLI capability (#2117)

* fix(spawn): gate Pi regular TUI flag by capability

* no-mistakes(review): Document conditional Pi TUI capability detection

* no-mistakes(review): Pin Pi probing and launch to one executable

* no-mistakes(review): Preserve literal pinned Pi paths and update documentation

* no-mistakes(review): Defer pinned Pi path insertion until final substitution

* no-mistakes(document): Document version-safe Pi launch probing

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* docs(vision): elevate experience, pain narrative, and distro virtues (#2147)

* docs(vision): elevate experience, pain narrative, and distro virtues

Fold the captain's public vision framing into VISION.md: peace of mind as a
primary goal, multi-session context-switch pain as the problem one interface
solves, clone-and-run setup ease, self-evolution including community, and
explicit harness/backend orthogonality. Reconcile experience-as-garnish into
experience-as-purpose and update aligns/resists accordingly.

* docs(vision): state the experience goal positively

Drop the negative "not a smart workflow / useful tool / impressive technology"
pretext. Lead straight into the positive experience north star.

* feat(bin): reconcile inactive terminal crew outcomes (#2167)

* fix: reconcile inactive terminal outcomes

* fix: stream secondmate summary inputs

* no-mistakes(review): Fix reconciliation locking and request delivery retries

* no-mistakes(review): Prevent retries after unknown request delivery

* no-mistakes(document): Clarify inactive reconciliation cadence and receipts

* no-mistakes(lint): Quote terminal status arguments in reconciliation tests

* refactor: simplify inactive outcome reconciliation

* no-mistakes(review): Bound inactive reconciliation scans with durable progress

* no-mistakes(review): Bound reconciliation and deduplicate recovery notices

* no-mistakes(document): Document inactive outcome reconciliation contracts

* no-mistakes(review): Reject relative local secondmate parent routes

* no-mistakes(review): Key terminal receipts by spawn incarnation

* no-mistakes(review): Stabilize legacy receipts and lock reconciliation snapshots

* no-mistakes(review): Fail closed on invalid secondmate identity markers

* no-mistakes(document): Document durable inactive-outcome reconciliation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* ci: raise Herdr test timeout (#2191)

* fix: refresh stale Pi instructions after compaction (#2163)

* fix(session-start): refresh drifted instructions on stale rebuilds

* test(session-start): prove Pi instruction refresh end to end

* no-mistakes(review): Fix stale instruction refresh and baseline integrity

* no-mistakes(review): Preserve true-start baselines across Pi continuations

* no-mistakes(review): Correct Pi continuation classification and live expectation

* no-mistakes(review): Correct Pi continuation coverage documentation

* no-mistakes(review): Fix read-only refresh and exact Pi session restores

* no-mistakes(review): Classify Pi create-if-missing sessions correctly

* no-mistakes(review): Classify named Pi sessions using immutable headers

* no-mistakes(review): Correct Codex interactive coverage diagnostic

* no-mistakes(document): Document immutable Pi compaction instruction refresh

* no-mistakes(document): Correct Pi refresh documentation and validation claims

* feat: add deterministic condition-to-action watcher (#2200)

* feat(bin): add deterministic condition->action watch adapter on the process-event channel

Register a (condition, action) pair once with bin/fm-procevent-when.sh and the
existing process-to-event runner polls the condition tokenlessly, fires the
action at most once on a stable true, and wakes firstmate exactly once with the
captured outcome - instead of burning an agent turn per re-check.

The pair is stored privately under state/when/ and hash-bound by a trust record
the same way fm-check-register.sh binds a custom check, so a mutated spec is
refused without executing anything. A durable exclusive fired marker claimed
before the action makes restarts and re-polls unable to double-fire; every
failure path (mutated spec, condition error past budget, expired deadline,
failed action, uncaptured earlier fire) ends in a terminal captured outcome
that wakes firstmate rather than a silent retry. Eligibility stays a firstmate
judgment: only exact, safe, reversible actions may be bound, and judgment-
needing or destructive actions keep the wake-and-decide flow.

* no-mistakes(review): Harden when watcher concurrency, deadlines, timeouts, and output

* no-mistakes(test): Bind watcher actions to registered executable bytes

* no-mistakes(document): Correct condition-action watcher documentation

* no-mistakes(document): Clarify outcome wake re-announcement

* no-mistakes: apply CI fixes

* fix(bin): honor a decision key stated after the verb colon (#2202)

The open-decisions fold only recognized a [key=<slug>] token between the
verb and the colon (needs-decision [key=x]: note). The common worker
shape with the colon first (needs-decision: [key=x] note) silently
folded its stated key into the shared "default" bucket, so two open
decisions could collapse into one record and fm-send --resolve-key <x>
refused to close the decision it plainly named.

A complete token at the head of the note is now an equivalent stated-key
position for every keyed verb, shared by the whole-file and incremental
folds through the one _fm_decision_key owner. The documented
before-colon position wins when both are present, a token deeper in the
note stays prose, a bare keyless line still folds to "default", and a
stated-but-malformed slug is rejected rather than rewritten to
"default". A consumed note-head token is stripped from the note so both
positions yield identical records, and the incremental fold version is
bumped so persisted cursors folded under the old interpretation are
rebuilt from the authoritative log.

Fixes #2109

* fix(bin): prevent watcher recovery acknowledgement livelock (#2212)

* fix(bin): keep a recovery acknowledgement valid across republication

A watcher cycle that opened and closed while the model handled its drained
wakes minted a fresh recovery generation, which invalidated the exact
acknowledgement the drain had just printed. That acknowledgement then consumed
nothing, so the marker stayed pending and every later arm spent its whole cycle
re-announcing the same recovery instead of supervising - a livelock the home
could not leave on its own.

A downtime publication now reuses the generation of an outstanding handling
episode, so a close during the handling window cannot orphan the printed
acknowledgement. The acknowledgement itself separates its two facts: queue-row
consumption is bound to the monotonic --ack-through sequence and always
happens, while only retiring the episode is bound to --recovery-generation. A
generation that moved on is a non-fatal result that names its own remedy
instead of a refusal that consumes nothing.

* no-mistakes(review): Preserve recovery generations and consume stale acknowledgements safely

* no-mistakes(document): Document sequence-bound recovery acknowledgements

* feat(fmx-respond): consume Relay conversation chains (#2206)

* feat(fmx-respond): consume in_reply_to_chain conversation context

The relay's poll payload can carry in_reply_to_chain, an oldest-first
transcript of the surrounding conversation, but the mention-handling
procedure only ever read the immediate in_reply_to parent, so referents
like "this" in a standalone mention stayed unresolvable even when
context was delivered.

Teach fmx-respond to read the chain when present (optional and
backward-compatible: often absent today, kind label not required),
resolve referents against the whole transcript, and extend the
untrusted-content framing to every chain entry including the upcoming
kind=history entries. Document the field's wire shape in
docs/configuration.md as the firstmate-side owner.

* no-mistakes(document): Document Relay chain context ownership

* fix: parse decision verbs before status metadata tags (#2280)

* fix(bin): strip every bracket tag, not just [key=...], from a status verb

status_line_verb only stripped a leading "[key=...]" token before the
colon, so a remote secondmate reply's leading "[corr=...]" correlation
tag stayed glued onto the returned verb word ("needs-decision
[corr=...]" instead of "needs-decision"). The open-decisions fold's
verb match then silently failed to recognize the line at all, so
fm-send --resolve-key refused to close a decision that was plainly
open on the status line.

Generalize the parser to strip every "[name=value]" tag before the
colon, in any order and count, so local and remote replies fold
identically.

* no-mistakes(review): Invalidate stale decision cursors after parser fix

* no-mistakes(document): Clarify status metadata verb parsing

* fix(bin): collapse duplicate supervision wakes (#2287)

* fix: collapse duplicate supervision wakes without losing legitimate updates

One remote-secondmate note produced two handling turns (a procevent check
wake published before autohandle, then a signal wake for the same mirrored
bytes), already-ingested replays such as a cursor-loss whole-log recapture
still woke with nothing to do, this home's own bookkeeping closes (fm-send
--resolve-key, the pending-reply escalation close, the captain-held
transfer) re-woke the session that wrote them, and turn-ended-only wakes
were annotated with already-announced status lines that looked like fresh
progress.

Dedup rules, each at its layer's one owner:
- fm-procevent.sh: an adapter may declare 'self-announcing'; the runner
  then applies first and publishes a check wake only for what remains
  unhandled. fm-procevent-remote-reply.sh declares it: the mirrored status
  append is the single announcement, so a fully applied capture publishes
  nothing and a byte-identical replay stays completely quiet. All other
  adapters keep strict publish-before-apply.
- fm-wake-lib.sh: fm_wake_signal_sig/seen_path/seen_current now own the
  watcher's signal signature and .seen-* marker format, plus
  fm_wake_status_append_self_announced, the guarded bookkeeping append
  that advances the marker only over exactly its own bytes and fails
  toward waking on any pending or interleaved foreign write.
- fm-send.sh, fm-pending-reply-lib.sh, fm-decision-hold.sh: bookkeeping
  closes go through that guarded append; escalation opens stay plain
  appends because a new blocker must wake.
- fm-wake-lib.sh annotations: a historical (turn-ended-only) row skips its
  status annotation only when the file's signature provably matches the
  seen marker; anything unannounced keeps annotating.
- fm-classify-lib.sh: a kind=secondmate task's status signal is never
  absorbed as provably-working, because that stream is the routed-reply
  channel the parent must read.

Also fixes a pre-existing exit-path deadlock the regression run reproduced:
a TERM inside a recovery-marker critical section left fm_lock_try_acquire
spinning against this same process's abandoned hold; a self-held lock is
now reclaimed (a subshell still waits on its parent's live hold).

Regression tests drive the real wake functions and executables in both
directions: each duplicate case collapses, while a new remote reply, new
decision, new blocker, merge result, failure, first status change, and a
later different note on the same task all still wake.

* no-mistakes(document): Document wake deduplication contracts

* feat: add Cursor CLI crew harness (#2238)

* feat(harness): add Cursor Agent CLI adapter

# Conflicts:
#	bin/fm-spawn.sh

* fix(composer): read cursor-agent's reverse-video placeholder as idle

cursor-agent renders its idle composer placeholder dim (SGR 2) but paints the
cell under the terminal cursor in reverse video (SGR 0;7). Reverse video is
neither dim nor a dark truecolor foreground, so the shared ghost stripper keeps
that one character and an idle composer reduces to a lone `P`. Judged on its
own, that remnant reads `pending` on a genuinely idle pane, which defers
away-mode escalation indefinitely on the styled cursorless backends.

Teach the ONE fleet-wide classifier the shape instead of adding an adapter-local
copy: register `→` as an agent prompt glyph so the composer row is structurally
findable at all (without it the bottom-most shape is a stale shell prompt echo
in the scrollback), add both verified placeholders to the idle set, and consult
the styling-independent plain row when the styled row is only a remnant.

The plain-row branch demands the remnant be a proper, strictly shorter substring
of a plain row matching a fully anchored placeholder. Real typed text is
uniformly bright, so stripping leaves it equal to the plain row and it stays
`pending` - verified live against a pane where the typed text was exactly the
placeholder string.

Verified live on cursor-agent 2026.08.11-e8db854; the regression pins the real
captured bytes and asserts the remnant survives stripping, so the case cannot go
vacuous if the stripper later learns SGR 7.

Co-authored-by: Amplify Logic AI <lars@sockinator.co>

* feat(cursor): narrow cursor identity and order its marker before CLAUDECODE

Cursor ships two executable names - `cursor-agent` and the legacy alias `agent`
- and runs as a bundled node script, so tmux reports the pane command as a bare
`node`. Neither `agent` nor `node` can be trusted by name, so identity gets one
owner in bin/fm-cursor-lib.sh that demands cursor's own name or install tree in
the path or argv[0], from the structural signal only. Probing an arbitrary pid's
executable during a liveness poll would execute a stranger's binary, which is
the hazard that rule exists to close.

Two consequences wired up:

Detection. cursor-agent does NOT clear an inherited CLAUDECODE, so a cursor
worker launched under a claude primary carries both markers and whichever is
tested first wins. The cursor markers are ordered ahead of the CLAUDECODE check;
fm-spawn additionally clears foreign markers at the launch boundary. Both are
kept deliberately - launch sanitization only covers sessions fm-spawn started,
while the ordering also covers a cursor session started by hand. Verified live
that CURSOR_INVOKED_AS is set on the agent process and CURSOR_AGENT=1 on the
child/tool processes fm-harness.sh actually runs as.

Pane liveness. A cursor pane now classifies `agent`. An unrelated node or agent
stays `other`, which the liveness callers already fold into `ambiguous` rather
than `dead`, so a stranger's node pane is never reported agent-free.

Resolution prints the STABLE launcher rather than the canonical target: identity
is proven through canonicalization, but cursor's canonical path carries a
version its own auto-update replaces, and pinning that would strand a task on a
version that can vanish.

The regression drives the two identity signals apart - a cursor-named executable
outside any cursor tree, and a non-cursor-named alias inside one - and asserts
each carries a verdict alone, so no single vendor string is load-bearing. Its
negative controls are real spawned processes, not fixtures.

Verified live on cursor-agent 2026.08.11-e8db854.

Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* feat(cursor): classify cursor busy state from its own turn transcript

Cursor shipped as "unknown cursor-unverified" on the premise that it exposes no
semantic turn lifecycle, only a rendered "Working" footer. That premise is
wrong: cursor-agent persists an append-only JSONL transcript per conversation
and brackets every submitted turn with a role:user open and a typed turn_ended
close. Verified live on 2026.08.11-e8db854, including the interrupt path, where
Escape closes the turn with status "aborted" - so this source covers manual
interruption, which Claude's Stop hook does not.

That makes it a genuine pull source in the muse mould rather than the rendered
text the redesign forbids: no writer, no arm, no gen, nothing seeded that could
never be cleared. Cursor's `ctrl+c to stop` footer stays out of the verdict, and
herdr's narrower native streaming state cannot stand in for it either.

Binding deliberately does not reconstruct cursor's workspace-slug directory
name. That slug collapses path separators, so rebuilding it would be a guess
that could bind the wrong pane; cursor records the exact absolute workspace path
in each project's .workspace-trusted, and the binding matches on that. A
conversation recorded as prior at spawn is excluded, so a relaunch in a reused
worktree folds its own turn rather than its predecessor's. Requiring a unique
remaining conversation keeps zero and several both unknown, because neither
proves anything about the current turn.

The regression pins the fold with real transcript files and asserts the
dangerous direction stays closed: an unresolvable binding, a record-free file,
an unclaimed workspace, and a workspace-path PREFIX all read unknown, never
idle. The prefix case uses an opaque fixture slug so a slug-rebuilding
implementation cannot pass it.

Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* feat(cursor): make the cursor launch runnable and give it lifecycle control

Five gaps that together kept a cursor crewmate from being drivable end to end.

Launch. The template invoked `cursor agent`, but `cursor` is not the CLI - the
installed names are `cursor-agent` and the legacy alias `agent` - so the command
could not run at all on a machine with a normal cursor install. It now resolves
through the verified owner, which also refuses a spawn loudly instead of leaving
a pane that dies with command-not-found and reads as a wedged worker.

Session binding. fm-spawn writes state/<id>.cursor-session so the busy fold can
find this pane's transcript, and teardown removes it.

Lifecycle control. No cursor PR touched fm-control-lib.sh, so
`fm-control <id> interrupt|exit|relaunch` could not drive a cursor worker at
all. Verified live: interrupt is a single Escape, exit is /exit, and cursor does
NOT repollute its composer with the cancelled prompt, so unlike muse it needs no
clear key. Secondmate is refused, matching the spawn refusal.

Submit acknowledgement. cursor parks its terminal cursor outside its composer,
so the composer verdict on tmux is always `unknown` and a submit could never be
acknowledged from the composer alone. The submit core's existing idle-to-busy
transition covers that case, but only if the pane's busy footer is recognised,
so cursor's `ctrl+c to stop` joins the harness-less default union the submit
cores read. The TOKEN is matched rather than the spinner verb: the same version
rendered both `Working` and `Running` in consecutive turns.

Bootstrap. A configured cursor crew harness with no cursor executable is now a
loud MISSING diagnostic rather than a first-spawn failure, and it accepts either
installed name.

Interrupt cancellation is deliberately left unconfirmed. The transcript does
type an aborted close, but its post-interrupt write latency measured as
variable - sometimes seconds, sometimes not within twenty - so a claim built on
it would be unreliable. Normal turn completion is prompt, which is what the busy
fold actually depends on.

Two inherited tests are corrected rather than deleted: the busy test asserted
cursor could have no semantic source, and the launch test pinned the literal
`cursor agent` string. Both now pin the verified behaviour, including that the
launch never allocates a second worktree.

Co-authored-by: ABHISHAKE KUMAR BOJJA <abojja@uvic.ca>
Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* docs(cursor): record the verified crewmate facts and extend the drift guard

The inherited cursor entry was written against 2026.08.04-aaa8809 and several of
its claims no longer hold: it named `cursor agent` as the binary (not the CLI
name), listed six Grok model ids of which the live catalog now returns two, and
recorded busy state, exit, interrupt, and skill invocation as unverified.

Replaced with what was measured against 2026.08.11-e8db854, including the two
facts most likely to be rediscovered painfully: cursor runs as a bundled node
script so its pane title is a bare `node`, and it parks its terminal cursor
outside its composer, which makes the tmux composer verdict permanently
`unknown` by design rather than a defect to chase.

Model ids now route to `--list-models` for the account instead of a fixed list,
since that list is exactly what drifted.

The live drift guard covers cursor, resolving it through the same verified owner
fm-spawn uses and passing --trust so the probe cannot hang on the workspace
prompt. Run against every installed harness: 8 checked, all alive, with cursor
reporting title='node' foreground=[.../cursor-agent] - the drift shape this
guard exists to catch.

Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* docs(agents): record the cursor session-binding state file

The state/ layout section is the inventory every session reads; a busy-source
binding that fm-spawn writes and teardown removes belongs in it alongside muse's.

* no-mistakes(review): Sanitize ambient Cursor marker in harness tests

* no-mistakes(review): Validate Cursor models against live catalog

* no-mistakes(review): Reject unsupported secondmates before binary preflight

* no-mistakes(review): Narrow Cursor ancestry detection to structured process identity

* no-mistakes(review): Parse Cursor transcripts and sanitize inherited markers

* no-mistakes(review): Handle malformed Cursor transcript records safely

* no-mistakes(review): Validate malformed Cursor closes in fallback parser

* no-mistakes(review): Retire stale Cursor bindings during relaunch

* no-mistakes(review): Fix Cursor drift guard command variable

* no-mistakes(review): Narrow Cursor identity to versioned install trees

* no-mistakes(document): Document Cursor harness boundaries

* refactor(composer): move the delivery busy footers to the shared owner

The per-harness rendered busy footers lived in bin/fm-tmux-lib.sh under
FM_TMUX_* names, so cursor's `ctrl+c to stop` signature - and every other
harness's - was reachable only from tmux. That placement was wrong on its own
terms: herdr, zellij, cmux, and orca run the same harnesses and face the same
question these footers answer, which is whether a submitted Enter actually
landed. Nothing about the signature is tmux-specific.

Moved verbatim into bin/fm-composer-lib.sh, the shared composer/delivery owner
every backend already sources, and renamed to FM_DELIVERY_* so the names stop
claiming a scope they never had. All five adapters now reach cursor's signature;
verified per adapter rather than assumed.

The boundary the move must not blur is stated where it now lives: this is a
DELIVERY guard, never a worker-state source. Confirming a keystroke landed is a
different question from asking what a worker is doing, and bin/fm-busy-lib.sh
remains the semantic owner that forbids classifying a harness from rendered
text. Cursor still classifies only from its transcript fold, which is already
backend-agnostic because it folds a file rather than reading a pane - the same
verdict on all six backends.

The old FM_TMUX_* aliases are dropped rather than kept as dead shims: nothing
outside the moved block referenced them except fm-busy-lib.sh's grok fallback,
which now reads the new name. The documented operator override, FM_BUSY_REGEX,
is untouched.

Also removes a dead duplicate CURSOR_INVOKED_AS check in bin/fm-harness.sh,
unreachable behind the marker check above it.

* no-mistakes(review): Correct shared delivery guard ownership references

* no-mistakes(document): Document shared delivery guards and Cursor backend limits

* no-mistakes: apply CI fixes

* fix(composer): bound a bare composer's wrap region at a half-block rule

A live cursor crewmate on herdr classified its IDLE composer as `pending`, and
fm-send consequently exited 1 with "delivery unconfirmed" on a message that had
actually landed. The cause is not cursor-specific.

Herdr draws a composer's top and bottom rules with the half-block glyphs U+2584
and U+2580 rather than the box-drawing family. fm_composer_row_has_edge knew
only the box-drawing set, so no box was detected; the composer was found as a
BARE row, and its wrap region - which extends while rows are non-blank and carry
no structural edge - walked straight through the composer's own closing rule and
swallowed the model and path footer below it. That footer is real text, so the
region classified pending on a genuinely idle pane.

Teaching the shared edge detector the half-block glyphs bounds the region at the
closing rule. Measured on the captured bytes of a real herdr cursor pane: the
same capture that read `pending` now reads `empty`.

This is a shared shape-path change, so it is deliberately narrow - it adds
glyphs to the edge vocabulary and changes no verdict logic - and the whole
composer and backend suite is green, including the other harnesses' herdr
fixtures.

The regression pins the real captured shape and asserts the footer content is
genuinely present, so the case cannot pass vacuously if the region were ever
bounded for some unrelated reason.

* fix(herdr): confirm a cursor submit from the rendered-footer transition

Herdr's composer-shape fix made an idle cursor pane classify `empty`, but
`fm-send` still exited 1 with "delivery unconfirmed" on messages that had
actually landed. Live measurement found the second, independent cause.

Herdr reports a cursor pane `agent_status=blocked` in EVERY state - idle,
mid-turn, and after - so the submit path's idle-baseline native confirmation is
structurally unreachable for cursor and every send falls into the composer
branch. That branch reads cursor's mid-turn composer row, which renders its own
`Add a follow-up` placeholder beside a right-aligned `ctrl+c to stop`. That
token is composer content, so the verdict is `pending` on a composer holding no
user text at all, and the Enter-retry budget then reports pending.

The escape is the same semantic signal the native path uses, read from the
pane's verified busy footer instead of native agent-state, and it is the
rendered-footer twin of the tmux submit core's turn-started confirmation: an
idle-to-busy transition ACROSS our Enter proves the harness accepted the
submission. The baseline is taken before the first Enter and only when the
native baseline was not legibly idle, so the idle-baseline path still never
reads pane content and a pane already mid-turn before we typed keeps reporting
`pending` rather than borrowing another turn as proof of this delivery.

The composer verdict is deliberately NOT relaxed. A right-aligned status token
on the composer row stays content for every other caller, including the
away-mode pre-injection guard, and the shared cursorless submit core is left
untouched so zellij, cmux, and Orca keep the behavior their own follow-up owns.

Verified live on herdr 0.8.0 and cursor-agent 2026.08.11-e8db854 in an isolated
lab session: `fm-send` now exits 0 and the steer executes, interrupt cancels a
running turn, `/exit` stops the agent, and teardown clears the record. All seven
panes of the running default session classify identically before and after the
shape fix, so no other harness regressed.

* no-mistakes(review): Prevent working Herdr baselines from falsely confirming delivery

* no-mistakes(document): Correct Cursor harness and backend documentation

---------

Co-authored-by: ABHISHAKE KUMAR BOJJA <abojja@uvic.ca>
Co-authored-by: Amplify Logic AI <lars@sockinator.co>
Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* fix(bin): require quota-axi 0.1.25 (#2300)

* fix: raise quota-axi floor to 0.1.25 for Cursor CLI quota awareness

Homes on latest main need quota-axi #87 so Desktop-absent CLI machines report a fresh Cursor quota instead of a false sign-in-required.

* no-mistakes(document): Update quota floor documentation pointer

* fix(bin): prevent false Pi watcher alarms during hand-offs (#2304)

* fix(guard): stop the false send-time watcher-down alarm on Pi primaries

On a Pi primary the watcher process is not the liveness signal. The Pi
extension tears the watcher down on every actionable wake and spawns the
replacement itself, so the singleton lock is legitimately unheld between
cycles: every one of the 799 cycles in a live primary's ledger ends with
lock_after=pid:none, and a live capture caught the guard verdict flipping to
no-watcher during one hand-off with the beacon 63s old.

bin/fm-guard.sh classified Pi as a persistent-watcher harness, which demands a
live identity-matched lock holder at all times, so any guarded command landing
in a hand-off painted the full WATCHER DOWN - SUPERVISION IS OFF banner and
told firstmate to repair a cycle the extension already owns and is restoring.

Add an extension supervision model for pi and pi-signed. A live
identity-matched watcher stays the ordinary healthy state; an unheld lock is
healthy only while the beacon is fresh within grace AND a live Pi session
provably owns continuity - both primary extensions recorded in their state
markers at their current on-disk builds by the process named in state/.lock,
with that process still alive. Without that proof the banner fires exactly as
before, so an unloaded, version-drifted, or exited Pi session is loud
immediately and a cycle the extension never restores is loud once the beacon
passes grace. The queued-wake warning, the PID-strict turn-end guard, and
every other primary's detection are untouched.

Fold session-start's duplicate Pi marker predicate into the shared library so
the ownership contract has one owner.

* no-mistakes(review): Restrict Pi hand-off tolerance to unheld watcher locks

* no-mistakes(document): Document Pi watcher hand-off supervision

* feat: support Cursor Agent CLI as a primary harness (#2305)

* feat(cursor): add Cursor Agent CLI primary hooks, park supervision, and session start

Register a tracked project-scope .cursor/hooks.json for Cursor's stop,
sessionStart, preCompact, and preToolUse steps.

bin/fm-turnend-guard-cursor.sh owns Cursor's turn boundary as a park: it
foregrounds the watcher arm, holds the boundary open until an actionable close,
and returns that wake as one follow-up. Exit 2 is a silent no-op on Cursor's
stop step, so the adapter never uses it. The follow-up loop is bounded twice,
by Cursor's own loop_limit and by the payload's loop_count.

bin/fm-sessionstart-cursor.sh delivers the digest as additional_context at
sessionStart, and stages it for the next turn boundary at preCompact, which
cannot inject context.

Cursor also loads the tracked Claude settings, so bin/fm-hook-host-lib.sh lets
each tracked Claude-shaped entrypoint stand down on a Cursor-delivered payload
rather than running every covered event twice.

bin/fm-tmux-lib.sh reclassifies a Cursor pane's composer cursorlessly, because
Cursor parks its terminal cursor outside the composer, which restores a genuine
composer-empty proof and unblocks away-mode escalation delivery.

* feat(cursor): make Cursor Agent CLI a verified primary harness

Resolve Cursor in the session-lock ancestry through bin/fm-cursor-lib.sh, which
a Cursor primary needs before it can hold its own home lock, and classify its
stop-hook park under the autoarm supervision model so the mid-turn pull guard
stops reporting a healthy between-turns watcher as down.

Read a Cursor pane's composer cursorlessly on tmux, gated on Cursor's own
structural process identity, which restores a genuine composer-empty proof and
lets away-mode escalations reach a Cursor primary with no daemon change.

Lift the secondmate refusals in bin/fm-spawn.sh and bin/fm-control-lib.sh now
that the supervision protocol exists and is recorded.

Cover the whole surface with a portable regression over real processes, an
opt-in live guard against the installed cursor-agent, and dated per-harness
evidence.

* docs(cursor): record Cursor as a verified primary across the owning surfaces

Update the turn-end guard, session-start, arm-seatbelt, cd-guard, watcher
continuity, architecture, configuration, README, and harness-adapters owners,
and add dated live evidence to the supervision and runtime-backend verification
records. Correct the recorded Cursor tmux composer verdict: the cursor-anchored
read is still blind, but the composite reader is no longer unknown.

Lift the remaining remote-secondmate refusal missed in the previous commit, and
add the new libs to the existing fixtures that copy a fixed dependency list.

* refactor(cursor): name the park's stand-down condition for both its causes

Also record that Cursor's preCompact firing itself is not yet live-verified,
while the static evidence that it cannot inject context, and the staging path
that follows from it, both are.

* test: give the pretool fixtures their new dependency and one lint owner

The cd-guard fixture copies a fixed dependency list and now needs the shared
hook-host predicate. Both pretool suites also asserted cleanliness with a bare
shellcheck call, a second and weaker copy of the lint definition that
bin/fm-lint.sh owns: it omits --external-sources, so it failed the moment these
checkers sourced a shared library. They now delegate to that owner.

* test: assert the cursor secondmate contract instead of its removed refusal

A cursor secondmate now launches, so the suite asserts what its park actually
needs: --trust so the home's project hooks load at all, its own home pinned as
the workspace, and the autoarm supervision model inherited across the launch.

* no-mistakes(review): Serialize Cursor wakes and bind staged context

* no-mistakes(review): Serialize Cursor context and nag state commits

* no-mistakes(review): Enforce Cursor ceiling before staged context delivery

* no-mistakes(review): Serialize Cursor claims and staged context

* no-mistakes(review): Serialize Cursor ownership and state commits

* no-mistakes(review): Protect Cursor context across session takeover

* no-mistakes(review): Preserve Cursor context across session takeover

* no-mistakes(review): Enforce owner-keyed Cursor staged context

* no-mistakes(review): Atomically claim Cursor follow-ups and staged context

* no-mistakes(review): Defer Cursor preCompact staging and simplify supersession

* no-mistakes(review): Serialize Cursor park commits and defer preCompact

* no-mistakes(review): Stop Cursor parks after session takeover

* no-mistakes(test): Route Cursor preCompact context through stop follow-up

* no-mistakes(document): Update Cursor primary documentation

* revert(cursor): cut preCompact staging from this change

Carrying a compaction digest across two concurrently running stop hooks kept
producing races that could deliver it twice or strand it indefinitely, and
closing them kept enlarging a critical section inside a hook Cursor awaits at
the turn boundary. Native preCompact firing was never observed either, so the
surface has no empirical basis yet.

Remove the adapter, its registration, its staged path in the park, and its
tests, and record the surface as deferred and uncovered alongside the Codex
interactive TUI. A regression now asserts preCompact stays unregistered so it
cannot return without its own design and evidence.

This change ships the proven core only: the turn-end follow-up park, the
run-tier session start, and away-mode delivery.

* no-mistakes(review): Correct Cursor park supersession documentation

* no-mistakes(document): Clarify Cursor run-tier verification ownership

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

---------

Co-authored-by: kunchenguid <kun-1@kunchenguid.com>

* feat(bin): add decline and repair paths for decision holds (#2330)

* feat(bin): add unrouted close paths to the captain decision gate

A captain who declines a held decision leaves no follow-up work to route,
so `resolve` could not express that answer: it requires at least one
`--routed-to` task. The only way to close such a hold was a direct
`tasks-axi done`, which never writes the durable resolution record the
completion gate reads, so the originating investigation could no longer
pass `verify` and its cleanup stayed blocked.

Add two close paths that route no work:

- `decline` closes an actively held hold with a recorded captain decision
  and no routed task. It refuses while any task is still blocked by the
  hold, because releasing routed work without recording it is `resolve`'s
  job.
- `repair` records the missing resolution block on a hold that was already
  closed outside this script. It never reopens a hold and never clears a
  dependency edge, and it refuses a hold that is still actively held.

Both require a non-empty captain decision file and share `resolve`'s
digest-based retry identity, so an exact retry is idempotent while a
changed decision is rejected. The recorded body now also names which path
closed the hold, and each routed entry regains its own line.

The gate itself is unchanged: an unanswered decision still fails
completion and blocks teardown, and neither new path can close a hold
without the captain's recorded word.

* fix(bin): require captain-hold provenance before repairing a decision

`repair` checked only that the backlog item was kind captain and Done, so
an ordinary captain-kind task that was never held for the captain could be
closed, repaired, and then pass the completion gate.

tasks-axi keeps `hold_kind` through a close, so it is the surviving proof
that an identity really was a captain hold. Require it before writing the
resolution record, and cover the case in the gate regression.

* no-mistakes(document): Correct decision-hold lifecycle documentation

* fix(bin): surface buried wake status lines once (#2331)

* fix(bin): surface buried status notes on wake drain

A note: answer immediately followed by a routine note was dropped because
annotations kept only the newest line and note: never enters OPEN DECISIONS.
Present every unread note and pending-reply resolution since the last drain
cursor, and annotate every unread line on a queued signal.

* no-mistakes(review): Fix unread status cursor races and overflow

* no-mistakes(review): Preserve cursors when status span reads fail

* no-mistakes(review): Make status presentation transactional under I/O failures

* no-mistakes(review): Simplify unread status cursor and presentation locking

* no-mistakes(review): Align cursor failure regressions with transactional presentation

* no-mistakes(review): Retire stale presentation cursors during task teardown

* no-mistakes(review): Preserve routine status until signal annotation

* no-mistakes(review): Correct unread status cap documentation

* no-mistakes(document): Document unread wake status presentation

* no-mistakes(lint): Fix wake surfacing ShellCheck warnings

* no-mistakes: apply CI fixes

* feat: add max Calm presentation level (#2334)

* feat(calm): add a max presentation level that hides mid-turn working notes

Calm's home-local preference becomes a three-state level instead of a
boolean: "off" is stock Pi, "on" is today's Calm, and "max" is Calm plus
hiding the assistant text of messages the model did not end its response
with. `/calm max` selects it from any state, a plain `/calm` steps max
back to ordinary Calm and otherwise keeps the existing on/off cycle, and
any other argument keeps that cycle too.

`config/calm` now persists "max" as its own literal value, so a session
start, resume, fork, or reload restores the stored level rather than
treating it as unrecognized and dropping to off.

The hide rule keys on Pi's intrinsic per-message stopReason: "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, because suppressing it would also stop a genuine reply from
streaming. The existing assistant layout adapter filters the blocks out
of the same shallow presentation copy it already uses for collapsed
thinking, so the message, model context, session storage, /export, and
delivery are untouched and a hidden mid-turn row collapses to zero
height. The new "assistant-working-note" class keeps that choice in the
visibility policy owner, where ordinary Calm keeps it visible.

* no-mistakes(document): Clarify Calm max persistence and taxonomy

* feat(calm): hide mid-turn working notes by default (#2339)

* feat(calm): make hiding mid-turn working notes the ordinary Calm state

Calm collapses back to the two-state on/off toggle it was before the max
presentation level, with max's hide rule promoted into ordinary Calm.
Calm on now hides mid-turn assistant working notes in addition to what it
already hid, and the /calm command parses no argument again.

The hide rule itself is unchanged: assistant text is removed from the
shallow presentation copy when the message's own stopReason is "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, so a genuine reply still streams. The message, model context,
session storage, /export, and delivery remain untouched.

config/calm persists only "on" and "off" again, but the reader still maps
a persisted "max" to on so a home upgraded from the removed level keeps
Calm on instead of dropping to off.

The mid-turn hide is now default behavior rather than an opt-in level, so
docs/calm.md documents it for users, docs/configuration.md records the
two written values plus the legacy max mapping, and the feasibility
taxonomy drops its level-scoped wording.

* no-mistakes(document): Document ordinary Calm working-note hiding

* chore: store no-mistakes test evidence in the repo (#2355)

* chore: ignore scratchpad/ at the repo root (#2359)

* fix(ci): fail hung Herdr behavior runs in 20 minutes (#2413)

A wedged family-run step was occupying the runner until the 75-minute
job cap; bound that step so cleanup and timing artifacts still upload.

* fix: keep the public promise reachable when work is routed to a second mate (#2457)

The lightweight Relay follow-up link lives in the answering home's own
state/<task-id>.meta, so it can only bind work that home owns. When a
Relay-linked request is routed to a second mate, the task record lives in the
second mate's home, fm-x-link.sh failed with a bare "no such task ...meta", and
nothing else picked the promise up: only the soft acknowledgement was ever
posted. The typed promised-final path already supports --work-home
secondmate:<id>; the playbook simply never chose it.

- fmx-respond now states the routing rule crisply: a task in this home takes the
  lightweight link, and second-mate-route…
yelenplays added a commit to yelenplays/firstmate that referenced this pull request Sep 18, 2026
…in operations (#33)

* feat(bin): report watched tooling updates that are available or installed but inert (#2684)

* feat(checks): report tool updates that are available or installed but inert

Firstmate had no way to notice that tooling this home depends on needs an
update, and no way at all to notice the worse case: an update that installed
correctly and then did nothing.

That second case is why this exists. A tool that self-installs into
~/.local/bin while a version manager keeps its own older copy earlier on PATH
looks completely up to date to anything that asks only "is a newer version
published". On 2026-08-20 a Herdr update landed at 0.8.2 while an older 0.8.0
copy stayed earlier on PATH, so every Herdr command failed on a protocol
mismatch and firstmate could not read its own fleet.

bin/fm-tool-update-check.sh reports the two conditions separately:

  <tool> update available      a newer version exists at the update source.
  <tool> update not in effect  a newer copy is installed on this host, but
                               PATH still resolves an older one.

PATH skew is measured, never inferred. Every executable copy of a watched
command on PATH is asked for its own version and those answers are compared,
so one lookup cannot hide the skew, and a directory name is never read as a
version because a version manager's "latest" directory can hold an older
build. A copy that will not report a version is a check failure, not a pass.

The watched tools live in local, gitignored config/watched-tools.json, so
adding a tool is a config edit rather than a code change, and the file is
never propagated to another home. Update sources cover both shapes: a local
clone's commit distance from its remote branch, and a command's own version
and update announcement, including a tool like no-mistakes that prints its
version on one command and announces a new release on another.

The check prints one line when something needs attention and prints nothing
otherwise, so it rides the existing watcher state-check contract with its
trust binding instead of introducing a schedule of its own, and
state/.tool-updates keeps the same pending update from being reported on
every poll.

The check only reports. It never installs, updates, reorders PATH, touches a
version manager, or fetches into a watched repository; every git probe is
read-only.

Tests cover the skew case as a regression, and it was verified by mutation:
removing the skew report, or stopping after the first PATH hit as a single
lookup would, each make that test fail.

* no-mistakes(review): fix tool update check probe reporting, budget, and shim write

* no-mistakes(review): keep sweeps alive on broken patterns and oversized budgets

* no-mistakes(review): roll back failed arm, widen budget clamp, bound repo probe

* no-mistakes(review): guard git probes at the budget, record uncut findings

* no-mistakes(document): fix stale watched-tool report-record wording in docs and header

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

The behavior shard's watch-triage suite failed on the new worktree-write wedge
tests. Those five tests are the only ones in the file that do not use its
standard waits. They give a fixed 3 second liveness budget to the one poll that
now spawns the bounded worktree walk, and 4 seconds to an escalating watcher
where every other test in the file gives 10. On a loaded runner that poll
outlives the fixed budget, so the round is reaped before the deferral it asserts
on is recorded, and the test reports a lost deferral instead of the deferral
under test. Wait for a completed poll cycle through the file's own
wait_poll_cycle, which is what its header documents this hazard for, and use the
file's standard 100 tick exit budget.

Verified against a load that reproduces the failure: 11 of 12 runs failed
before, 8 of 8 pass after. Verified by mutation too, so the waits still prove
the behavior: removing the write deferral, and keeping a finished deferral chain
across an idle-timer repair, each still fail their test.

* fix: decouple ask-user decisions from yolo (#2764)

* fix: treat yolo as merge authority only, not ask-user finding authority

Yolo on/off was documented as also deciding no-mistakes ask-user findings, which hid firstmate's duty to judge unambiguous-toward-design findings itself. Keep every safety boundary; this is a contract clarification, not a relaxation.

* no-mistakes(document): Clarify yolo documentation ownership and merge posture

* feat(bin): add a spoken interface that answers from records and hands work over (#2767)

* feat(voice): spoken round trip on Nova Sonic 2 with a measured relay cost

Step one of the spoken interface: the laptop captures and plays audio, this
desktop holds the model session, and no AWS credential leaves the desktop.

Measured, amazon.nova-2-sonic-v1:0 in eu-north-1, end of speech to first byte
of reply audio, 6 runs each, all answered, on a question that forces a records
read:

  relay path   1.229 1.379 1.428 1.447 1.481 1.516  median 1.438
  direct       1.147 1.179 1.203 1.237 1.244 1.317  median 1.220

The relay costs about 0.22s of the median. The direct figure reproduces the
earlier survey, which is what makes it a usable control. Excluded: the
captain's own ssh round trip, microphone capture, and speaker output. This
desktop has no microphone and no speaker, so every run used audio files.

Three pieces:

  bin/fm-voice-relay.py    holds the conversation on this host
  bin/fm_voice_records.py  what a spoken answer may read, and the handover
  bin/fm-voice-client.py   the laptop end; audio devices UNVERIFIED
  bin/fm_voice_frame.py    the wire format both machines share

Real work is handed to the existing bin/fm-inbox.sh rather than a second
queueing surface, and the agent says it is handing over rather than answering
as firstmate.

Read scope: Done history and free-form note bodies are never assembled at any
scope, so the wide default cannot reach the places commercial detail
accumulates. config/voice-read-scope narrows it to counts only, and
config/voice-read-deny excludes a named item in one line. The boundary is an
executable test that widening the reader fails.

Push to talk is the default because it is cheaper and the choice is still open;
--listen open-mic is the single flip.

Two traps worth knowing: a clip with no trailing silence is never answered, and
the end of a reply is contentEnd with stopReason END_TURN, not completionEnd.
A second user turn in one session is treated as barge-in unconditionally, and
an interrupted turn that calls a tool is lost, so the session reconnects per
turn and gives up conversational memory. That is the concrete thing step three
has to solve.

* no-mistakes(review): fix voice relay credential reuse, frame validation and record parsing

* no-mistakes(review): test uplink header guard, bound unknown expiry, align state dir

* no-mistakes(review): decide deny per item, guard turn failures, bound ambient credentials

* no-mistakes(review): read account config from home, harden deny and turn failures

* no-mistakes(review): close status verb set, fix inbox help, pair data override

* no-mistakes(review): keep profile-free relay alive, unblock loop, fix dead assertion

* no-mistakes(review): hide finished pull requests, refuse open mic, keep suite offline

* no-mistakes(review): survive reader failures, release devices, fix claims

A failure while handling a model event, or while sending a tool result,
left the reader task dead with ended and turn_done clear, and close()
re-raised the stored failure on every await. One dropped stream became a
relay that could never build another session. The reader now reports the
session over in a finally whatever killed it, and close() absorbs the
task the same way it already absorbed its sends.

The laptop client releases what it already started when a later startup
step refuses, SystemExit from the handshake wait included, and names a
device refusal instead of leaking a raw PortAudio error. Whether it
releases correctly against a real device is still unverified here.

The records docstring claimed every reading was filtered to open ids.
Only the pull request count and list are; the worker count and the state
histogram cover every live runtime record, finished ids included,
because a meta file still on disk still needs tearing down.

The finished-work deny half of the suite asserted things that held with
the deny list absent. It is replaced by a deny on an open title, which
removes the row and says so while the count stays honest.

* no-mistakes(review): name reader failures, split file and device refusals

A failure inside the model reader released the waiting turn and told
nobody. The session was not marked spent, no notice reached the client,
and the client waits for a reply end or a notice, so the captain got
their whole timeout of silence and then a record saying the turn went
unanswered with nothing about why. Both ends of the relay now name a
failed turn through one function, once per turn, and --self-test carries
the cause in relay_error the way the client's own record does.

Two things that are not failures stay that way. A stream that simply
ends is the end of a session, which serve still reads on its own terms.
A stream that goes away because close() asked it to is an ordinary
renew, and announcing it would have put a failure notice in front of the
captain on every turn.

On the laptop end, the refusal that became a device error covered the
file-backed playback and capture too, so a mistyped --in-file was
reported as an audio device failure and the advice named the flag that
had just failed. The file ends now report the path and the flag that
chose it and stay an OSError; the device ends keep the device advice and
name the flag for that end. The device paths remain unrun here, so only
the file halves are covered by a test.

* no-mistakes(test): survive model session end, order client turn frames

* no-mistakes(document): sync voice relay docs with reviewed relay behavior

* no-mistakes(document): re-measure relay latency and correct its cause

* no-mistakes(document): correct measurement date and name the unmeasured SSH hop

* no-mistakes(document): describe the unpublished control measurement, fix list formatting

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* fix(bin): preserve Relay follow-up loops until explicit disposition (#2763)

* fix: keep Relay public loops open until retire

Delivering a promised-final reply was deleting the only record that tied a public thread to later work, so a follow-on ship silently owed no closing reply. Retain the registration after delivery, rechain follow-on work onto the same thread, and make retire --reason the only close.

* no-mistakes(review): Propagate public follow-up registration removal failures

* no-mistakes(review): Persist retire receipts and align parent resolution

* no-mistakes(review): Make rechain resumable after partial obligation creation

* no-mistakes(review): Repair follow-up state, briefs, and expiry escalation

* no-mistakes(review): Serialize follow-up delivery stamps with retirement

* no-mistakes(review): Serialize rechain claims and protect registration terminal states

* no-mistakes(review): Avoid reporting retired delivery loops as open

* no-mistakes(document): Refresh public-loop documentation and verification evidence

* no-mistakes: apply CI fixes

* no-mistakes(review): Preserve delivered follow-up bindings during registration replay

* no-mistakes(review): Harden public follow-up retirement and rechain races

* no-mistakes(review): Fail closed on unresolved secondmate retirement

* no-mistakes(review): Bind secondmate cleanup to its recorded canonical home

* no-mistakes(review): Fix rechain command output and expiry validation

* no-mistakes(review): Validate brief keys and warn on remote promotion

* no-mistakes(document): Document retained public follow-up loops

* no-mistakes(lint): Remove unused bounded-wait loop variable

* feat(bin): merge GitLab merge requests through the guarded PR merge path (#2779)

* feat(bin): merge GitLab merge requests through the guarded PR merge path

bin/fm-pr-lib.sh already parses a GitLab merge request URL for the watcher,
but bin/fm-pr-merge.sh refused every non-github provider, so a merge request
had to be merged by hand and got none of the recording, guards, or audit
trail a pull request gets.

The merge path now dispatches on the parsed provider. A GitHub URL keeps its
exact previous behavior. A GitLab URL is addressed through glab by the project
URL rebuilt from the parsed host and path, so a merge request on any instance
resolves and no host is hardcoded, and no merge-method flag is added because
the project's own merge method is what should apply.

A GitLab merge happens only after one live read of the merge request confirms
it is open, detailed_merge_status is mergeable, has_conflicts is false,
blocking_discussions_resolved is true, and the head pipeline succeeded at the
exact current head. Every failing condition is reported, not just the first.
The verified head is bound to the merge with glab's --sha, so a push landing
between the read and the merge fails the merge instead of landing commits
nothing verified. Recorded metadata is never the authority for any of this: a
rebase moves the head and leaves a recorded value stale, so a recorded head
that disagrees with the live one is reported rather than trusted, and the
recorded value is read before the recording step because that step drops a
GitLab head it cannot resolve.

* no-mistakes(review): reject bundled -R clusters and make tool-absence cases host-independent

* no-mistakes(test): state authorised GitHub narrowing of bundled -R guard

This branch NARROWS GitHub behaviour. The narrowing was authorised
deliberately rather than slipping in by accident, and it applies to both
providers, GitHub and GitLab alike, because a script that guards one provider
and not the other is a trap for the next reader.

What bin/fm-pr-merge.sh now refuses is extra merge arguments containing a
bundled short-option cluster that includes R, for example "-dR other/repo".
The forge CLIs expand such a cluster one character at a time, so it carries
"--repo other/repo", and that later value wins over the repository the URL
named. Before this change, "fm-pr-merge.sh <task> <github-url> -- -dR
other/repo" reached "gh-axi pr merge 12 --repo example/repo --squash -dR
other/repo" and exited 0 with pr= recorded and the merge poll armed. It now
exits 1 with "extra merge arguments must not override the repository", records
nothing, and invokes no forge merge command. Every other GitHub invocation is
byte-identical to the base commit.

Closing that hole honours the existing rule rather than departing from it. The
file header already forbids --repo and -R because the repository must come
only from the URL, so a bundled cluster carrying a repository override was
never legitimate behaviour to preserve: it was that guard being evaded.
Redirecting a merge to a repository the URL does not name is exactly what the
guard exists to prevent.

The refusal is already pinned on both paths by the existing case
test_bundled_repo_override_args_refuse_before_recording in
tests/fm-pr-merge.test.sh. On GitHub ("-dR wrong/repo") and on GitLab ("-yR
https://other.example/g/p") it asserts exit 1, the refusal wording, no pr= in
the task meta, no armed merge poll, and no forge merge command invoked, with a
control case proving a cluster that carries no repository override still
reaches the forge. No duplicate assertion was added. Both assertions were
confirmed to have teeth by narrowing the guard back to a bare -R and watching
each path fail.

This commit carries no file change: the guard and its coverage landed in
614853d, and this message exists so the pull request description states the
narrowing.

* no-mistakes(document): fix README pointer for GitLab watch and merge doc

* no-mistakes: apply CI fixes

* fix(bin): record a lost relay connection instead of an unanswered turn (#2788)

* no-mistakes: apply CI fixes

* fix(bin): drop a private record citation and narrow the review rule

Three corrections to the spoken interface that landed in #2767, plus one
fix carried over from that branch after its pull request had already been
merged.

The confidentiality fix. The module docstring of bin/fm-voice-relay.py
cited a private, gitignored fleet record by exact path and section number.
That widens what this public repository points at, and it cannot resolve
for any reader here, because the path has never been in the repository.
Both traps it pointed at are already described in full in the list
immediately below it, and docs/voice-relay.md carries the same two for
operators with no citation at all, so the pointer is removed and no claim
is weakened by losing it. Two comments that referred to "the survey" as
though it were something a reader could open are reworded the same way.
Neither exposed a path, so that half is comprehensibility rather than
confidentiality.

The review rule. .greptile/rules.md is kept, because its conditions are
right and deleting it would leave the next reviewer to re-litigate a
decision already argued out. What was wrong with it is narrower than its
existence: it read as settled repository policy, when whether VISION.md
itself should be reconciled is an open question belonging to the captain.
One sentence now says so, and says that the conditions listed below it are
what the interpretation depends on. That narrows the claim rather than
widening it.

The carried-over fix. The first commit on this branch is 7f98e797 from
fm/voice-relay-build-v4, taken verbatim rather than rewritten. It closes
the window where a transport failure was recorded and then erased, so a
run could be emitted as answered false with relay_error null. That matters
more than it looks: relay_error is the field that keeps an infrastructure
failure from being averaged into a latency figure, so the failure mode is
a dead connection wearing the costume of a slow reply. It landed fifteen
minutes after #2767 merged and so never reached the default branch.

* no-mistakes(review): name a reason on every unanswered-turn close path

* no-mistakes(review): guard the downlink body and pin frames to their turn

* no-mistakes(review): attribute reply audio to its own turn and tell endings apart

* no-mistakes(review): tell a cut-short reply from an unanswered turn

* no-mistakes(review): discard reply audio arriving after the output closes

* no-mistakes(review): count discarded reply audio on the speaker path too

* no-mistakes(review): keep a reason off a turn already answered in full

* no-mistakes(review): say a reset cut a reply short, not that none arrived

* no-mistakes(review): read one turn's audio count once, and hush a tidy exit

* no-mistakes(document): fix stale session-end relay_error claim in voice-relay guide

* fix(composer): stop a blocked pi pane from proving an empty composer (#2811)

A pi worker parked on an interactive prompt - a permission dialog, a
question menu, a trust dialog - reports agent_status=blocked, because it
is waiting on a human keystroke. Pi draws that menu above its separator
pair, so the composer region between the rules is blank and structure
alone looks like a free composer. _fm_composer_pi_verdict admitted
blocked alongside idle and done, so the shared classifier reported an
affirmatively empty composer for exactly the pane where typing is unsafe.

Every "is it safe to type here?" consumer reads that verdict and proceeds
only on an affirmative empty, so both are told yes on a parked prompt:
the away-mode injection guard in bin/fm-supervise-daemon.sh, and fm-send's
pre-type refusal. The keys then answer the menu instead of composing a
message - the highlighted default is selected, the text is discarded, and
the record attributes a decision to a human who never made it.

blocked now defers to unknown, which every consumer already treats as
fail-closed. idle and done still prove an empty composer, so ordinary
steering is unchanged, and Cursor is unaffected because its always-blocked
panes never reach this pi-only branch.

Regression coverage lands first at both levels: the verdict owner
(a blocked pi defers) and the herdr adapter (a parked pi prompt is not an
empty composer).

* fix(bin): require project clone roots during fleet sync (#2849)

* fix(bin): require a clone root before fleet-sync touches a project

Git repository discovery walks upward, so `git -C projects/<dir>` on a plain
directory nested under projects/ resolves to the enclosing repository - in a
firstmate home, the firstmate checkout itself. fm-fleet-sync.sh guarded its
candidates with `rev-parse --is-inside-work-tree`, which such a directory
passes, so every later git call read, pruned and fast-forwarded firstmate's own
default branch and reported it under the project directory's label. A running
session's AGENTS.md changed underneath it, and the report named a project that
had nothing to do with the change.

Require each candidate to be the root of its own work tree before any other git
command: compare `rev-parse --show-toplevel` against the directory's own
physical path. Both sides are physical, so a symlinked clone still compares
equal. Anything else is skipped by name, naming the repository that would have
been touched, and bootstrap relays that as a FLEET_SYNC line.

Regression coverage reproduces the wrong-repo fast-forward against a home nested
inside another repository, in both the whole-fleet and single-project forms, and
pins that a symlinked clone dir still syncs.

* no-mistakes(review): Keep enclosing fixture clean during clone-root regression

* fix(bin): retry transient Lavish poll interruptions (#2846)

* fix(procevent): retry a transient Lavish poll interruption quietly

A live Lavish listener can be cut short by the server with exactly

    error: Lavish Editor poll response was interrupted
    code: SERVER_ERROR

while the session's marks remain available. Firstmate registered raw
`lavish-axi poll` output, so the generic process-event runner captured
that transient response as a result and woke the whole fleet over what is
really an internal retry.

The Lavish adapter now registers its own listener command, which reruns
the published blocking poll up to 12 times at 5 second intervals for that
one exact two-line response. The match is deliberately narrow: real
feedback, ended and missing sessions, any other SERVER_ERROR, and the same
interruption still standing once the bound is spent all pass straight
through and are captured and announced as before. The retry is a Lavish
fact, so the generic runner stays adapter-agnostic.

`FM_LAVISH_POLL_RETRY_DELAY` is a bounded 0 to 60 second override for the
interval only, refused rather than rounded when malformed, so a test can
exercise the real bound without waiting it out.

* no-mistakes(review): Harden Lavish retry matching, validation, and cleanup

* no-mistakes(review): Bound Lavish retry staging and stabilize regression

* no-mistakes(document): docs: explain Lavish retry adoption

* no-mistakes(lint): Restore Lavish trap ShellCheck suppression

* fix(brief): stop the documented {TASK} fill from corrupting the Herdr gate (#2838)

The unguarded Herdr declaration quoted `{TASK}` in its own prose while the
scaffold instructs firstmate to replace every `{TASK}` placeholder. The
documented global replace therefore spliced the whole task body into the
middle of the safety gate's sentence, silently destroying the one contract
that exists precisely because the scaffold cannot inspect the task text.

Reword the gate to refer to the task text filled in above, leaving the
placeholder only at its genuine fill site. Rewording rather than renaming the
token keeps the unfilled-charter guards in fm-home-seed.sh and
fm-remote-home-seed.sh working unchanged.

Add a regression test that performs the documented global fill on ship and
scout scaffolds and asserts the body lands once and the gate survives.

* fix(bin): resolve the busy-state lock mtime with the platform's own stat form (#2837)

The writer lock's stale-lock branch read the lock's mtime with
`stat -f %m ... || stat -c %Y ...`. On GNU coreutils `-f` is filesystem
stat, so it consumed the format string as a path, complained on stderr,
printed a partial filesystem dump ("  File: ...") on stdout, and still
exited 0. The GNU form in the fallback therefore never ran, and the
following arithmetic evaluated the word `File`, aborting the writer under
`set -u` with "File: unbound variable".

fm-teardown.sh died there after returning the worktree, leaving
state/<id>.meta, .status, .busy-gen, .busy-state, .busy-state.lock/ and
.turn-ended behind. The surviving metadata kept the watcher monitoring an
endpoint whose agent was gone, so a finished task produced stale wakes
forever, and every re-run died identically because the abandoned lock was
never broken.

Detect the platform once and pick the right stat form, the pattern
bin/fm-watch.sh already documents, and treat any non-numeric result as
"just created" so a future portability surprise degrades to a lock-timeout
refusal rather than killing teardown mid-way.

* fix(stow): add opt-in pass horizon for memory decay (#2850)

* fix(stow): give memory decay a per-pass horizon so the clock fires

The tiered decay clocks were wall-clock only, while admission is per-pass:
each /stow admits the findings that pass produced. In a home that stows
daily those two rates diverge by the stow cadence, an entry the fleet keeps
exercising never reaches 30 days unreinforced, and memory only grows while
the pass reports decay evaluated.

Give each dated marker an optional unreinforced-pass counter and make both
tiers stale at whichever horizon comes first: 10 passes or 30 days for
aging, 3 passes or 7 days for perishable. Reinforcement clears the counter
and nothing else does, so the existing evidence-based restamp rule stays
the only way an entry renews its lease. An absent /N means zero, so entries
that stay exercised carry no extra marker bytes, and a rarely stowed home
keeps its current behaviour through the unchanged date horizon.

* no-mistakes(document): Align stow workflow with dual decay clocks

* fix(stow): make the per-pass decay horizon opt-in

The unreinforced-pass horizon shipped as a new default archival cadence,
which is a product default rather than a restoration of the existing
wall-clock contract. Keep the 30-day and 7-day horizons as the only
default clock, and put the 10-pass and 3-pass horizons behind an explicit
opt-in: config/stow-pass-horizon for the firstmate home, and the file's
own header pointer for the public skill.

With the opt-in absent no counter is written and no counter is read, so a
home that does not ask for it decays exactly as it does today.

* no-mistakes(review): Preserve frozen counters and correct archive provenance

* test(watcher): stop fixture confirmation budgets racing real child startup (#2876)

tests/fm-watcher-lock.test.sh passed in isolation but failed intermittently
under full-suite and ambient concurrent load. bin/fm-watch-arm.sh computes its
confirmation deadline immediately after forking the real child watcher, so the
child's entire fork, exec, lock acquisition and beacon publication has to land
inside that wall clock. Two cases shrank that budget to one second, leaving a
two-second window for work measured at 3.1-4.9s under CPU oversubscription, so
the arm honestly reported "FAILED - no live watcher with a fresh beacon" and
their premises collapsed. A third case ran on the production budget, but its
child must also execute a registered check before exiting: measured at 1.9-2.3s
idle and 9.1-13.1s under load, against an 11s budget.

The two cases that must confirm a real child now hold the arm to production's
own budget instead of a shrunken fixture one, the immediate-wake case gets an
explicit budget with headroom over its measured loaded cost, and the two waits
for the arm's typed failure are sized off the largest production default rather
than a fixed eight seconds.

No bin/ change and no default behavior change: the lock's fail-closed semantics,
SIGSTOP handling, stale-heartbeat detection and the arm's typed failures are
untouched. Verified 4/4 green at 3x CPU oversubscription (loadavg 75-80) after
3/3 red before the change, and CONTRIBUTING.md records the convention.

* fix(bin): deterministically order remote tool paths (#2870)

* fix(bin): order discovered tool installs by the shell's own expansion

fm_remote_job_compose_operator_path built the asdf and mise install
directories with `compgen -G`, which does not sort. Bash sorts glob
matches in pathexp.c, on the shell's own pathname-expansion path only;
`compgen -G` reaches the same glob_filename through pcomplete.c, which
sorts nothing. On bash 3.2 (macOS /bin/bash) and every bash before 5.3
that handed the composition raw readdir order, so which install of a
multi-version tool a remote job resolved was decided by directory order
on disk rather than by this composition.

Expand the globs at the call sites and let the function take the matches,
so the composition and the documented portable-PATH contract are the same
operation. Quoting the account home at the call site also stops a home
whose name contains glob metacharacters from being reinterpreted.

The colocated regression pins both the order and the mechanism: bash 5.3
moved sorting into the glob library, so an order-only assertion cannot
see the defect there.

* no-mistakes(review): Remove source-reading PATH regression guard

* fix(bin): prevent routed secondmate work from stranding (#2848)

* fix: surface stalled secondmate queues and wake handoffs

* no-mistakes(review): Make handoff wakes retryable and stall alerts crash-safe

* no-mistakes(review): Prevent duplicate handoff wakes and cover remote delivery

* no-mistakes(review): Serialize local handoffs and preserve pre-move wake intent

* no-mistakes(review): Serialize teardown with handoffs and retain remote wake confirmation

* no-mistakes(review): Reconcile correlated handoff wake delivery after crashes

* no-mistakes(review): Keep failed wakes retryable and isolate stall receipts

* no-mistakes(review): Reset known-undelivered wake attempts for durable retries

* no-mistakes(review): Refuse duplicate sends for unresolved delivery attempts

* no-mistakes(review): Atomically restore retryability after reconciled send failures

* no-mistakes(review): Serialize delivery confirmation with reconciliation

* no-mistakes(document): Document routed wake and stall supervision

* no-mistakes(lint): Fix ShellCheck expansion and subshell warnings

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes(review): Retire stale wake state and defer pre-move wakes

* no-mistakes(review): Secure markers, bind batches, and preserve teardown routes

* no-mistakes(review): Preserve unresolved prepared wakes across unrelated handoffs

* no-mistakes(review): Preserve prepared wakes before unrelated moving handoffs

* no-mistakes(document): Document prepared wake batch ownership

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes(review): Make local wake retirement recoverable

* no-mistakes(document): Clarify handoff recovery and teardown documentation

* fix: make macOS inbox test path portable (#2857)

* feat(bin): deliver local steers through durable task inboxes (#2856)

* feat(bin): steer local tasks by durable inbox record plus constant doorbell

Stage 1 (local steers) of the captain-adopted reframe in
data/fm-send-reliability-reframe-s1/report.md: an ordinary fm-send text
steer to a task recorded in this home is appended as a sequenced durable
record under state/<id>.inbox/ and the terminal receives only one constant
self-describing doorbell line, best-effort. The worker acknowledges by
moving the record into handled/; the watcher re-rings an unacknowledged
message on an idle pane and escalates once as an ordinary stale wake.
--resolve-key closes decisions at enqueue time, because the durable
enqueue IS delivery to the task's record. bin/fm-task-inbox-lib.sh owns
the record format, doorbell line, and re-ring ladder.

The typed plane remains for what must reach the terminal itself:
lifecycle keys, harness-native slash and codex $-skill invocations,
explicit backend targets, and the remote secondmate leg (unchanged until
the remote inbox leg ships separately). The composer classifier is
demoted from delivery proof to an advisory ring guard that skips only on
a proven pending verdict.

Verified live against claude, codex, opencode, pi, grok, and muse: each
real worker read its record, acted, and acked with the mv
(docs/verification/runtime-backends.md "Steering-inbox doorbell").

* docs(verification): flag the grok 1.0.5 composer-matrix staleness observed by the doorbell run

* test(captain-hold): read the chat-channel answer from the durable inbox record

* test: migrate fm-control's marker contrast to the inbox record and fix macOS wc padding in the tool-update suite

* no-mistakes(review): Harden inbox locking, teardown races, and acknowledgements

* no-mistakes(review): Serialize watcher actions with inbox acknowledgements

* no-mistakes(review): Bound metadata locking and tighten acknowledgement rechecks

* no-mistakes(review): Preserve exact inbox bytes and harden delivery recovery

* no-mistakes(review): Harden watcher bookkeeping against concurrent inbox teardown

* no-mistakes(document): Update inbox and typed-plane documentation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* revert(pipeline): keep parser-native secondmate marking and the both-failed exit out of stage 1

The CI monitor's fix changed the secondmate marking contract for
parser-native invocations (appending the marker after the text) and
softened the both-commit-and-marker-failed branch to exit 0. The merge
authority ruled the marking question out of scope for this stage-1
transport PR (follow-up: fm-send-secondmate-harness-invocation-r1) and
ruled the both-failed case a loud nonzero local failure. Restore both,
keeping the monitor's legitimate migrations and hardening.

* no-mistakes(document): Document inbox and typed-plane boundaries

* no-mistakes(document): Scope backend transport docs to typed plane

* no-mistakes(document): Clarify inbox attempt-budget documentation

* no-mistakes: apply CI fixes

* fix(send): the durable record alone governs the inbox exit status

Captain-refined ruling on the F2/Greptile finding: the durable inbox
record is what delivers the steer, so pending-reply bookkeeping trouble
after a successful enqueue never exits nonzero - a resend-inviting status
would make automated callers enqueue the delivered instruction again
under a new sequence. With the recovery marker stored the watcher
reconciles silently; with the commit and marker both lost the send
surfaces a distinct reply-tracking-degraded do-not-resend warning and
still exits 0. Nonzero remains only where nothing was delivered (or a
decision close needs its manual command). Regression: record durable +
both bookkeeping writes lost -> exit 0, one record, no duplicate.

* no-mistakes(review): Preserve inbox ordering with drain-all doorbells

* no-mistakes(review): Surface unwritable inbox ladder bookkeeping

* no-mistakes(review): Silence ladder failures after inbox acknowledgement

* no-mistakes(document): Update steering inbox documentation

* no-mistakes: apply CI fixes

* feat(bin): add fast local lint mode (#2891)

* feat: add fast local lint mode

* fix: preserve complete fm-lint help

* fix: isolate fast lint mode

* no-mistakes(document): Clarify lint mode documentation ownership

* no-mistakes: apply CI fixes

* feat(bin): deliver remote steers through durable inboxes (#2901)

* feat(bin): deliver remote secondmate steers through durable task inboxes

Stage 2 of the inbox+doorbell steer channel (stage 1: #2856). A remote
secondmate steer now crosses fm-on.sh as a durable record written
idempotently into the remote home's steering inbox plus a best-effort
remote doorbell, and the last typed-payload steer transport is deleted:

- fm-remote-secondmate-control.sh cmd_send writes the record via the new
  fm_task_inbox_write_idempotent and rings the doorbell; it no longer
  types the payload through an inner fm-send at an explicit pane target.
- fm-send.sh routes every remote text steer (harness-native included,
  which marking already reduced to chat) onto the remote inbox leg,
  retries the identical leg once on ssh 255, closes --resolve-key
  decisions at enqueue for remote too, and preserves a marked request's
  reply expectation when completion stays unknown. The exit-3-as-
  delivered remap, the 255 do-not-resend trap, and the remote typed
  submit block are removed.
- fm-task-inbox-lib.sh owns the idempotent enqueue: an exact-body re-run
  lands on the existing record, handled or not, so an ambiguous
  transport can always be safely re-run.
- Tests pin the new contract end to end (record + doorbell + no typed
  payload across ssh, one-record idempotence under an ambiguous
  transport, enqueue-time decision close, loud real failures, and the
  deleted typed-payload behaviors gone), and AGENTS.md plus
  docs/remote-secondmates.md describe the remote leg's new semantics.

* no-mistakes(review): Harden remote inbox delivery against lifecycle races

* no-mistakes(review): Enable correlation-preserving remote steer resends

* no-mistakes(review): Fail closed on stale correlation resends

* no-mistakes(review): Include home context in remote resend commands

* no-mistakes(review): Lock and revalidate remote parent routes

* no-mistakes(document): Clarify remote steer retry documentation

* no-mistakes: apply CI fixes

* feat: add persistent Pi supervision branch (#2858)

* wip: forked supervision on Pi (checkpoint before docs)

* fix(pi-branch): harden mirror delivery, fallback encoding, and session replacement

Peek-then-shift mirror flush so a failed append retries instead of dropping;
durable mirror cursor commits only after delivery into the branch;
the main fallback wake is operational-encoded like every watcher injection;
session_shutdown quiesces the generation and session_start re-arms, so /new
and /resume no longer kill the branch permanently. Registers the extension in
the strict typecheck, adds the dispatch handshake test, the branch extension
suite, the bash-level regression suite, the session-start replay test, and
the opt-in real-SDK live guard.

* test(fixtures): carry the branch-dispatch lib and lease lib into isolated fixtures

The watcher extension now imports lib/fm-branch-dispatch.ts and fm-teardown
sources fm-lease-lib.sh, so every fixture that copies or symlinks those
files in isolation gains the new sibling.

* no-mistakes(review): Prevent shutdown wake loss and serialize lease claims

* no-mistakes(review): Durably hand off wakes and retain portable leases

* no-mistakes(review): Require durable reports and clear disposed branch leases

* no-mistakes(review): Enforce per-wake outcomes and quiescent lease cleanup

* no-mistakes(review): Require wake acknowledgements and tighten branch lifecycle boundaries

* no-mistakes(review): Require complete acknowledgements and replay cleanup failures

* no-mistakes(review): Bind supervision to lock ownership and durable delivery

* no-mistakes(review): Activate branch lazily after session lock acquisition

* no-mistakes(review): Preserve undelivered mirror context across extension rebinds

* no-mistakes(review): Acknowledge startup replay only after main delivery

* no-mistakes(review): Isolate replay metadata from untrusted digest content

* no-mistakes(review): Reject duplicate reports for active wake sequences

* no-mistakes(review): Retain failed fallbacks and deduplicate outcome replay

* no-mistakes(review): Deduplicate durable outcomes and cache delivery receipts

* no-mistakes(review): Anchor wake sequence matching to outcome fields

* no-mistakes(document): Clarify Pi supervision durability contracts

* no-mistakes(lint): Fix ShellCheck issues in branch supervision scripts

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* refactor(pi-branch): collapse to confused-agent-grade guards per captain decision

Captain decision A: the lease/actor guards target the CONFUSED-AGENT threat
model bin/fm-gate-refuse-lib.sh already documents; adversarial-grade
separation is impossible in the shared-process design and is filed as
separate follow-up work. Rip out the machinery that chased it: the
generation fence and shell-provenance markers, the wrapper-tagged ancestry
walks, guard auto-claim with per-script release traps, the pending-wake
files and ack-receipt correlation (the durable wake queue already
re-presents anything unacknowledged), the delivery-receipt store with
contiguous cursor advancement, the session-start replay-metadata channel,
and the branch tool quiescence counters.

Keep the behaviors the board requires, each on its simplest implementation:
lazy per-action session-lock ownership (cold start activates after the lock
lands; a secondary session stays inert), mirror durability across extension
rebinds via the durable cursor, replay-exactly-once from the one read
cursor, the awaited operational-encoded fallback, per-generation stray-lease
cleanup, session-lock-bound lease liveness (a recycled pid or a non-Pi home
never honors a leftover lease), the loud accidental-override guards
(readonly actor prelude, cross-actor claim refusal), and the role-partition
refinements (no forced teardown, no direct relaunch for the branch).
Default-on-for-Pi is unchanged.

* no-mistakes(review): Enforce lock ownership and serialize lease mutations

* no-mistakes(review): Synchronize guard cleanup and bind leases to lock owner

* no-mistakes(review): Report outcomes before acknowledging durable wakes

* no-mistakes(review): Restrict leases to Pi and instruct main claims

* no-mistakes(review): Reject malformed lease locks and torn outcome tails

* no-mistakes(review): Validate complete outcome tails before appending

* no-mistakes(review): Guard branch side effects across session replacements

* no-mistakes(document): Update Pi supervision durability and lease documentation

* no-mistakes(lint): Suppress intentional nested-shell expansion warning

* no-mistakes: apply CI fixes

* fix(pi-branch): authorize lease releases by caller

* fix(lint): break redundant source-analysis path in fm-lease-lib.sh

fm-lease-lib.sh's lazy fallback source of fm-wake-lib.sh gave ShellCheck's
--external-sources traversal a second path into an already 1540-line file
that fm-send.sh and fm-teardown.sh also source directly, blowing up the
recursive analysis past CI's lint timeout. Mark it a source=/dev/null
analysis boundary, matching the existing fm-task-inbox-lib.sh convention.

Also restores bin/fm-lint.sh and tests/fm-lint.test.sh to the shared
serial-lint definition (dropping an unrelated parallel-sharding change
that was itself hanging and masked this root cause).

* no-mistakes(document): Correct lease caller-authorization documentation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* fix(bin): parallelize startup network sweeps (#2927)

* feat(bin): parallelize session-start remote secondmate network sweeps

Run per-secondmate liveness and convergence probes concurrently and overlap clone refresh, while replaying each mate's fail-closed diagnostic in original order. Ignore scratchpad* so untracked scratch no longer blocks remote sync.

Co-authored-by: Cursor <cursoragent@cursor.com>

* no-mistakes(document): Document parallel startup network sweeps

* no-mistakes(lint): Fix empty environment assignment lint warning

* no-mistakes: apply CI fixes

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* test: handle absent watcher wake queues (#2845)

* fix(tests): count declared-pause wakes without crashing on an absent queue

The exited-declared-pause case counts queued stale wakes by handing
state/.wake-queue straight to awk. A watcher that queues nothing never
creates that file, and awk aborts on a missing path before its END rule
runs, so the count collapses to the empty string. The next comparison
then fails as an integer-expression error and surfaces as a wake flood
with no number, hiding the real contract breach the following grep names.

Read the queue the way the drain-count assertion at the end of this file
already does: silence awk's open error and default an absent queue to
zero. Applied to all four counts in this case, including the live
external-decision gate pair whose queue an acknowledged drain can also
leave behind. An absent queue now reports "did not use the bounded
paused recheck", while a genuine flood still fails with its real count.

Fixes #2628

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* style(pi): distinguish routine and captain supervision merge notes by icon (#2934)

* style(pi): restyle supervision merge notes with a sailboat and matching pad

Secondary-session notes were flush against the TUI edge and fully tinted.
Use the sailboat prefix, Pi's default outputPad, boat-only color, and dim remainder so they sit like real messages.

* style(pi): distinguish routine and captain merge notes by icon only

Visible notes now lead with a sailboat or anchor, then only the dim outcome.
Drop the branch-merged wording and verdict brackets so the icon is the only kind signal.

* docs(pi): add the approved multi-brain architecture poster (#2938)

The markdown contract stays the owner; the still is only the visual of the idea.

* feat(pi): default branch supervision and route heartbeats (#2939)

* fix(bin): bound remote job worker supervisor restarts (#2942)

* fix(bin): bound remote worker supervisors

* no-mistakes(review): release incumbent supervisor before starting its replacement

* no-mistakes(review): wait out a healthy same-root supervisor instead of replacing it

* no-mistakes(review): narrow remote worker change to restart accounting only

* no-mistakes(document): clarify supervisor restart guard is a lifetime total

* fix: safely split supervision wake handling by actor (#2953)

* feat(bin,pi): per-actor wake consume, silent success gating, merge-poll dedup

Three related fixes to the shared wake-drain and Pi supervision-branch
dispatch machinery so a routine success is never main-blocking and a
mixed queue can safely split between actors.

1. Successful routine results no longer create main-blocking wake rows.
   fm-startup-network.sh only enqueues a check: startup-network wake when
   the deferred result is actionable (state is not "done", or the report
   carries a bootstrap-diagnostics actionable prefix); a clean success
   stays durable in the report file without ever waking the agent.

2. Per-actor wake-drain consume contract. bin/fm-wake-drain.sh now scopes
   presentation and --ack-through to the current actor
   (bin/fm-lease-lib.sh's fm_lease_actor): main keeps the original
   whole-queue cutoff behavior, unaffected. A branch actor
   (FM_SUPERVISION_ACTOR=branch, set only inside the Pi supervision
   branch's own bash tool calls) is scoped to an explicit eligible-row
   snapshot instead of a cutoff comparison, so it can never remove a row
   it was not granted - the fix for the swallow risk that used to force
   an all-or-nothing whole-queue fallback to main.
   .pi/extensions/lib/fm-branch-dispatch.ts's scopeForUnreadWake is the
   single owner of eligibility: a check-kind row (merge-confirmation
   polls, Relay mentions, credential/auth failures) is now excluded
   rather than vetoing the whole scan for a non-heartbeat wake, while a
   heartbeat review keeps its original all-or-nothing rule unchanged.
   writeEligibleRowsSnapshot publishes the exact eligible sequence
   numbers before every branch prompt; fm-primary-pi-watch.ts's offer
   still refuses a check-kind trigger outright so a main-only close is
   never itself routed to the branch.

3. A repeat identical merged-PR-poll result for an already-notified task
   is absorbed instead of enqueued again. A poll's own retirement state
   is scoped to one registration and cannot see a prior registration's
   outcome, so a task re-registered after its merge was already surfaced
   would otherwise wake main a second time for the same event.
   bin/fm-pr-lib.sh's new per-task pr-poll-merge-notified marker survives
   across re-registrations to catch that case; the first notification for
   a task still reaches main unchanged.

Regression tests colocated in tests/fm-startup-network.test.sh,
tests/fm-wake-queue.test.sh (including the mixed-queue no-swallow
property), tests/fm-pi-branch-extension.test.sh, and
tests/fm-pr-check-security.test.sh. docs/watcher-continuity.md and
docs/pi-supervision-branch.md updated for the new contracts.

* no-mistakes(review): Bind merge deduplication to canonical PR identity

* no-mistakes(review): Serialize wake row ownership across main and branch

* no-mistakes(review): Bind branch grants and deduplicate within actor claims

* no-mistakes(review): Fallback main-owned wake claims to main delivery

* no-mistakes(review): Clarify silent startup success guidance

* no-mistakes(review): Release residual branch grants after settled prompts

* no-mistakes(review): Reject truncated wake rows as corrupted

* no-mistakes(document): Document per-actor routing and silent startup success

* no-mistakes(lint): Fix ShellCheck findings in wake grant and startup test

* no-mistakes: apply CI fixes

* fix(pi): hide branch outcomes tool rows in Calm (#3024)

* Hide branch outcome tool in Pi Calm

* no-mistakes(review): Preserve stock outcomes rendering and document tool audit

* no-mistakes(review): Document branch read tool audit disposition

* no-mistakes(review): Match stock outcomes output sanitization

* no-mistakes(document): Document Calm custom-tool visibility

* fix: bind no-mistakes attestations to PR head (#3027)

* fix: delegate no-mistakes PR gate to pinned action

* no-mistakes(document): Document commit-bound no-mistakes attestations

* feat(pi): add persistent supervision branch model selection (#3028)

* feat(pi): let operators pin a cheaper supervision-branch model

Supervision is an easier job than the captain's own conversation, so the
Pi supervision branch does not need main's model. A new /supervision-model
command opens Pi's own selector over Pi's own catalog of credentialed
models, plus a "Follow main" entry, and persists the pick as one
<provider>/<model-id> line in this home's gitignored
config/supervision-branch-model. Firstmate keeps no model catalog of its
own.

The branch resolves the pin at every branch build - the first wake of a
cold start and the reopen after /new, /resume, /fork, or reload - so the
choice survives all of them, and picking also releases the live branch so
the next wake reopens the same persistent branch conversation under the
new model. An absent, unreadable, or unparseable file means no pin and
keeps today's behavior byte for byte: no model option is passed and Pi
picks the branch's model exactly as before.

A pin naming a model Pi cannot hand back is never silently downgraded
onto main's model: the branch refuses to build and the wake falls back to
the captain-facing main path naming the unusable pin, which is the
extension's existing failure direction.

The choice is home-local and not part of secondmate inherited
configuration, matching the Pi Calm preference precedent.

docs/configuration.md owns the operator-facing schema. Portable
regressions cover pin-present on create and reopen, pin-absent default,
the command's persistence, cancellation, and live rebind, and both
unusable and unparseable pins. The opt-in real-SDK guard proves the
vendor surface the pin reads and that an explicit model wins over the
model a reopened session recorded.

* no-mistakes(review): Fix supervision model runtime and rebind races

* no-mistakes(review): Restrict supervision picker to isolated runtime models

* no-mistakes(document): Document supervision branch model selection

* fix(pi): make the supervision model pin authoritative on every reopen

Clearing the pin with "Follow main" removed the file but the next branch
build reopened the persistent branch session with no explicit model
override, so Pi restored the model that session had recorded - the old
pinned model - while the command reported that the branch now follows
main. The same gap meant an absent pin did not reliably mean
same-model-as-main once a home had pinned once.

The pin file's current state now decides the model on every branch build,
create and reopen alike, overriding Pi's session-state restore. With a
pin, that model. With no pin, main's own current model is applied
explicitly, tracked from the contexts Pi already hands the extension plus
its model_select event, since the branch is built at wake time with no
context of its own. Only when main's model is unknown, or this home's
stored credentials cannot run it in the isolated branch runtime, does a
build fall back to passing no override at all, which is the behavior from
before the pin existed; the branch is never refused over model choice.

The command's notification now reports the model actually applied, and
says plainly when clearing the pin could not apply main's model instead
of claiming a change that did not take effect.

No credential handling changes: the branch still relies entirely on the
stored credentials its own runtime already holds, and the picker stays
restricted to models that runtime can resolve.

Colocated regressions cover pin present on create and reopen, clearing
the pin returning a reopened branch to main's model and specifically not
the old pinned one, an unparseable pin behaving as no pin, and the
unknown-main-model fallback to no override.

* no-mistakes(review): Make unpinned supervision follow main model changes

* no-mistakes(document): Correct supervision model documentation

* feat(pi): let /supervision-model pick branch reasoning effort (#3079)

* feat(pi): let /supervision-model pick the branch's reasoning effort

Supervision is an easier job than the captain's own conversation, so the
Pi supervision branch does not need main's reasoning effort any more than
it needs main's model. /supervision-model now settles both in one flow:
the existing model picker, then a follow-up effort picker built from Pi's
own supported thinking levels for the model just chosen. Firstmate keeps
no effort catalog of its own; the menu, the clamp, and the vocabulary all
come from Pi.

The pick persists as one line in this home's gitignored
config/supervision-branch-effort, independent of the model pin: a captain
may pin a model, an effort, both, or neither. The effort pin's current
state decides the branch effort on every branch build - the first wake of
a cold start and the reopen after /new, /resume, /fork, or reload - and
overrides Pi's restore of whatever level a reopened branch session
recorded, which is what keeps "Follow main" honest. With no pin, main's
own current effort is applied explicitly and followed live through Pi's
thinking_level_select event, the same way an unpinned branch already
follows main's model, and the two selections now share one build revision
so either change invalidates an in-flight build.

The branch is never refused over effort. Pi owns the clamp, so a pinned
level the branch's model cannot run becomes that model's nearest supported
level while the captain's raw pick is kept for a model that supports it,
and the command reports the level the branch will really run at rather
than the raw pin. A token Pi would not recognize at all is treated as no
pin rather than passed to that clamp, which would otherwise collapse a
typo into the model's lowest level. Only when main's effort cannot be read
either does a build pass no effort override at all, which is the behavior
from before this file existed.

Pi's own effort vocabulary is pinned by a bidirectional type assertion
against Pi's getThinkingLevel return type, so the tracked strict typecheck
against the installed package fails the moment Pi adds or removes a level.

docs/configuration.md owns the operator-facing schema for both pins.
Portable regressions cover the pin on create and reopen, model-only and
effort-only pins working independently, clearing a pin returning the
branch to main's effort, live-follow of a mid-session change, the clamp,
an unrecognized token, the unknown-main-effort fallback, and the command's
two-step flow, persistence, cancellation, and honest reporting. The opt-in
real-SDK guard proves the vendor surface all of that rests on, and also
repairs a pre-existing gap that left it unable to load the extension at
all.

* no-mistakes(review): Resolve effective branch effort honestly

* no-mistakes(document): Clarify Pi-owned effort picker behavior

* fix: keep routine supervision noise out of captain chat (#3093)

* fix(supervision): silence empty board closes and decouple the heartbeat

Two unrelated sources of noise put routine supervision events in the
captain's chat.

An empty Lavish board close - the captain reads a review surface, says
nothing, and closes it - became a check wake whose entire content was
that nothing happened. Suppress it at its source instead of routing it
anywhere: the generic runner gains a `silent` adapter seam mirroring the
existing `terminal` one, and the Lavish adapter answers it for exactly
one positively-determined shape, an `ended` session carrying no queued
content block. A silenced result is recorded durably handled so it does
not return on a later reconcile. Everything else announces unchanged - a
`Send & End` close carrying the captain's real answer, an `ended` result
still carrying content, a waiting or missing session, an unreadable
result, and every adapter that implements no `silent` command at all.
The keyed-answer feed is untouched, so suppressing an announcement never
suppresses the captain's own answer.

A fleet heartbeat was deferred to main merely because some unrelated
check row happened to be sitting unread, which put a routine fleet
review in the chat for a reason that had nothing to do with the fleet. A
check row is permanently main-owned, so it is now excluded from a
heartbeat claim rather than vetoing the scan, exactly as in every other
mode. What all-or-nothing guarantees is unchanged: the branch takes
every branch-ownable unread row or none of them, and an unresolvable
task-local row, an unknown row kind, or an unreadable queue still defers
the whole review to main. Main is still woken for the check on that
check's own triggering close, so nothing starves.

Main-only classes are unchanged and now each covered by a test: Relay
mentions, credential failures, merge confirmations, real board answers,
and watcher-failure repair. The per-actor acknowledgement and
no-cross-swallow properties are untouched.

* no-mistakes(review): Fail closed on all Lavish content headers

* no-mistakes(review): Suppress false unacknowledged status for silenced results

* fix(bin): stop a correlation token from hiding and stranding decisions (#1967)

* fix(classify): read the decision fold through a correlation token

status_line_verb stripped a trailing [key=...] from a status line's prefix
but left everything else glued to the verb, so a line carrying the
correlation token bin/fm-pending-reply-lib.sh embeds and a secondmate
echoes back matched no arm of _fm_decision_fold_line. Such a line folded as
ordinary status in both directions: a needs-decision or blocked opener never
opened its key, and a resolved or captain-held closer never closed one. The
same glued verb also hid correlated done and blocked lines from
status_is_captain_relevant and status_is_terminal_verb, and let correlated
working and resolved lines leak through the free-text fallback the
nonterminal guard was meant to stop.

The verb parse now walks whole words and drops only a token of the exact
shape a firstmate library writes - corr=<16 hex>, plus the bracketed form
bin/fm-secondmate-report.sh emits - before or after the key token, unkeyed,
or doubled. An arbitrary name=value word is deliberately NOT skipped:
skipping unknown tokens would let free text carrying an equals sign reduce
to a bare verb and impersonate a transition, which is the takeover the
strict parse and _fm_decision_key_transition_allowed exist to prevent. A
prefix with no corr= substring is returned byte-for-byte as before, so every
line without a token keeps its exact historical verb.

FM_OPEN_DECISIONS_FOLD_VERSION goes to 3, because every cursor persisted
under the previous reading carries an open set computed while correlated
lines were invisible and must be rebuilt from byte 0.

Measured over a real 383-line status log: 254 lines keep byte-identical
captain-relevance, pause, terminal-verb and captain-held verdicts, and all
129 changed lines carry a valid token - 14 correlated done/blocked/
needs-decision lines become captain-visible, and 20 correlated
working/resolved lines stop being escalated on prose alone.

* fix(review): Captain, block token-first decision impersonation

* fix(document): Clarify normalized status verb ownership

* fix(classify): reconcile the correlation-token read with the tag-stop parser

Rebasing onto main put this change beside #2280, which made verb parsing
stop at the first "[name=value]" tag. Both edit status_line_verb with
different intents, so the resolution keeps both rules rather than letting
one overwrite the other:

- #2280's tag stop is kept verbatim and now owns every BRACKETED tag,
  including the "[corr=...]" form fm-secondmate-report.sh writes. The
  bracket-unwrapping arm this branch had added to the token test is
  therefore removed as unreachable.
- This branch's token walk is kept and narrowed to the UNBRACKETED token
  fm-pending-reply-lib.sh writes, which the tag stop does not reach.

Two consequences of standing beside #2280 rather than before it:

The fold version had collided at 4: #2280 spent it on the tag-stop
parser and this branch had spent it on the token read. A cursor
persisted under #2280's reading predates this one and must still be
rebuilt, so the version moves to 5.

A bracketed impostor is dropped from the malformed-token list. On main
today "resolved [corr=deadbeef] [key=victim]:" already reads as the bare
verb, as does "resolved [anything at all] [key=victim]:", because the tag
stop ends the parse at the opening bracket regardless of content. That is
#2280's reviewed contract; asserting otherwise here would narrow it. The
unbracketed impostors it owns stay strict and still fold as prose.

Adds a consumer test for the two verb-string case arms that postdate this
branch: fm-supervise-daemon.sh's transient-stale arm and
fm-crew-state.sh's map_log_state.

* fix(review): Captain: Seed cursor migration fixture with version four

* fix(document): Clarify voice status normalization ownership

* fix(bin): Cursor-Park unter Pi-Host ohne Cursor-Identität stilllegen (#3115)

* fix(bin): Cursor-Park unter Pi-Host stilllegen.

pi-cursor-sdk lädt .cursor/hooks.json in die Pi-Sitzung und parkte einen zweiten Watcher; das erzeugte rearm-resurface und brach laufende Rückfragen ab. Bei PI_CODING_AGENT=true beendet der Park sofort, native cursor-agent Primaries bleiben unverändert.

* fix(bin): Cursor-Park trotz PI-Leak nur ohne Cursor-Identität stilllegen.

Stand-down gilt nur bei PI_CODING_AGENT=true ohne CURSOR_AGENT und ohne CURSOR_INVOKED_AS. Handgestartete cursor-agent Primaries mit geerbtem PI-Marker parken weiter.

* no-mistakes(document): Document Cursor park Pi-host stand-down

* fix(bin): no-mistakes-Mindestversion auf 1.46.0 anheben.

Die PR-Attestierung verlangt ab 1.46.0 strukturierte Pipeline-Schritte; der Bootstrap-Floor blieb bei 1.31.2 und ließ zu alte Builds zu.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(pi): make supervision model picker searchable and scrollable (#3099)

* fix(pi): make /supervision-model's model list bounded and searchable

Pi's generic extension selector renders every option at once with no
search box, so a real eligible catalog ran off the top of the terminal.
The model step now draws the same rows through Pi's own SelectList - the
bounded scrolling primitive behind Pi's /model picker - with Pi's own
Input and fuzzy filter above it for search, keeping 'Follow main' first,
the branch-runtime eligibility filter intact, and the pick branch-only.

Pi's ModelSelectorComponent is deliberately not reused: its selection
handler writes the captain's default model through Pi's settings manager,
which would move main's conversation as a side effect of pinning the
branch. The effort step's menu is a handful of levels and stays on Pi's
plain selector dialog.

* no-mistakes(document): Clarify supervision picker documentation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes(docum…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant