Skip to content

fix: restart every live second mate after updates - #3690

Merged
kunchenguid merged 2 commits into
mainfrom
fm/fm-updatefirstmate-always-restart-r1
Sep 4, 2026
Merged

kunchenguid merged 2 commits into
mainfrom
fm/fm-updatefirstmate-always-restart-r1

Conversation

@kunchenguid

Copy link
Copy Markdown
Owner

Intent

Captain (2026-09-04, exact ask): every home must restart on /updatefirstmate even if it was already on the right commit, because some harness features do not get reloaded until a clean restart.

Current behavior (the gap): a live second mate is restarted only when that pass advanced AGENTS.md or .agents/skills and the runtime can prove a restart; already-current homes are skipped with no restart; a bin-only advance does not restart; a missing instruction-file list is treated as unknown and becomes a re-read, never a replace.

Required behavior:

  • After a successful /updatefirstmate, EVERY live second mate home restarts (persist open records, then replace the agent), INCLUDING homes that were already on the target commit.
  • Re-read is NOT a substitute: harness launch-time features (hooks, flags, composer/feedback disable, and similar) only apply on a clean restart.
  • Keep persist-before-replace. Do NOT force, stash, or discard. A mate that cannot be restarted provably stays on the honest unreached/nudge path rather than being reported as reloaded.
  • Add a regression that fails if an already-current live second mate is omitted from the restart set.

This supersedes an abandoned targeted classification fix on branch fm/fm-remote-update-first-reporter-r1; do not revive that approach. The grounding evidence is a second mate named SSHHIP that was the first remote mate on the mac-home firstmate: on a /updatefirstmate pass its home was already at the target commit and its host reported no instruction-file diff, so the old classifier left it out of both the restart set and the nudge set entirely, and its agent kept running the launch-time harness wiring it had started with.

Delivery: no-mistakes, yolo off (a Firstmate instruction-surface change; the captain owns the merge).

What Changed

  • Restart every live second mate successfully left on the target commit, including already-current homes and non-instruction-only updates.
  • Preserve persist-before-replace behavior while routing unprovable restarts to the honest nudge path and leaving skipped or stopped mates untouched.
  • Add regression coverage for already-current local and remote mates and document the updated restart contract.

Risk Assessment

✅ Low: The change is well-bounded, satisfies the restart and fallback invariants, preserves persist-before-replace safety, and includes executable regressions for already-current and unprovable-runtime paths.

Testing

The pre-run changed-test baseline and both focused behavior suites passed; an end-to-end already-current SSHHIP fixture was selected for restart, persisted before replacement, restarted successfully rather than nudged, and retained an unchanged clean checkout.

Evidence: Already-current second mate update-and-restart transcript

Source: Already-current second mate update-and-restart transcript

=== 1. /updatefirstmate mechanical output ===
firstmate: already current
secondmate SSHHIP: already current
reread-firstmate: no
restart-secondmates: fm-SSHHIP
nudge-secondmates: none

=== 2. restart action consumed from update output ===
invocation: fm-secondmate-restart.sh fm-SSHHIP
restarted: SSHHIP (claude)
summary: 1 of 1 restarted, 0 nudged, 0 unreached
exit-status: 0

=== 3. end-user-visible terminal chronology (persist before replace) ===
1:Firstmate instruction waiting: list /private/var/folders/5x/4nqprlbx0518k3ybcb1sz6gr0000gn/T/fm-secondmate-restart.DR6sOY/reviewer-e2e-24759/home/state/SSHHIP.inbox/*.msg and, in numeric order, read and act on each, then mv each handled file to /private/var/folders/5x/4nqprlbx0518k3ybcb1sz6gr0000gn/T/fm-secondmate-restart.DR6sOY/reviewer-e2e-24759/home/state/SSHHIP.inbox/handled/.
2:/exit

=== 4. home safety checks ===
HEAD-before: c7d7f8be1584b0c1b0bc5ce4ac972c396071c993
HEAD-after:  c7d7f8be1584b0c1b0bc5ce4ac972c396071c993
working-tree-status: 

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • bin/fm-test-run.sh --changed --exclude-family real-herdr-gated
  • Baseline (pre-run): bin/fm-test-run.sh --changed --exclude-family real-herdr-gated
  • bin/fm-test-run.sh tests/fm-update.test.sh
  • bin/fm-test-run.sh tests/fm-secondmate-restart.test.sh
  • Executed an isolated end-to-end SSHHIP scenario through real bin/fm-update.sh output and bin/fm-secondmate-restart.sh, recording restart selection, persist-before-exit chronology, and unchanged clean home state.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

/updatefirstmate only restarted a second mate when that pass advanced its
AGENTS.md or .agents/skills. An already-current home was skipped entirely, a
bin/-only advance was steered instead, and a remote host that could not report
its instruction diff was downgraded to a re-read. A running agent also freezes
its launch-time wiring - turn-end hooks, harness flags, per-harness feature
switches - and none of that is derivable from a file diff, so an unchanged
tracked surface is not evidence the agent is already on the current behavior.

Restart is now unconditional on a successful update of that home. Every live
second mate the pass leaves on the target commit is restarted, whether it
advanced or was already there.

The safety contract is unchanged: open records are persisted before the agent is
replaced, nothing is forced, stashed, or discarded, a home the pass had to skip
is not restarted at all, and a mate whose runtime cannot prove a restart keeps
the honest re-read path and is never reported as reloaded.

bin/fm-ff-lib.sh gains a settled-state hook that fires for a home left at the
base whether it advanced or was already there, and never for a skipped one; the
instruction-gated hook the session-start convergence sweep uses is untouched.

Regressions: fm-update pins the already-current mate into the restart set and
the unprovable one into the nudge set, and fm-secondmate-restart drives both
real commands end to end - an already-current home is named, persisted, and
genuinely replaced with its checkout untouched, while the unprovable one keeps
its running agent.
@greptile-apps

greptile-apps Bot commented Sep 4, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge, with the requested unconditional live-secondmate restart behavior guarded by existing update, persistence, capability, and lifecycle checks.

Successful local and remote updates now classify live settled mates for restart or fallback nudge, while skipped and stopped homes remain untouched and replacement still occurs only after persistence and repeated capability validation.

Reviews (1): Last reviewed commit: "no-mistakes(document): Document uncondit..." | Re-trigger Greptile

@kunchenguid
kunchenguid merged commit efbeb4f into main Sep 4, 2026
15 checks passed
@kunchenguid
kunchenguid deleted the fm/fm-updatefirstmate-always-restart-r1 branch September 4, 2026 05:23
Valentino-Sole added a commit to Valentino-Sole/firstmate that referenced this pull request Sep 4, 2026
* fix: start a fresh supervision branch for every main session (kunchenguid#3600)

* fix(pi): start a new supervision branch conversation per main session

The supervision branch reopened one recorded conversation forever, so
every main session start reloaded the current generated prompt and then
weeks of accumulated thread, where a superseded rule could still outweigh
today's.

The branch conversation is now scoped to one main session: the session
generation owns the recorded conversation, so a cold start, /new,
/resume, /fork, or a reload always builds a new one, while a rebuild
inside one session (a model or effort change) still continues that
session's own conversation.

The dialog mirror re-anchors with it. Its durable cursor records what the
previous branch conversation received, so a /resume or reload - which
keeps main's own session file - would otherwise leave the new branch
blind to dialog main itself still has. The reset is bounded by the
current main session, and the cursor keeps advancing incrementally within
it. The durable outcome store and its processed marker are untouched, so
unacknowledged captain-facing outcomes still re-present on the new main
session.

* no-mistakes(document): Document fresh Pi supervision conversations

* no-mistakes(ci): Fixed the flaky concurrent inbox failure. Lock acquisition now retries when a competing lock disappears between a failed claim and inspection. Added a behavioral regression covering that race. Verified the full inbox test four times, project lint, and git diff checks

* feat: restart second mates after instruction updates (kunchenguid#3614)

* feat(update): restart second mates whose instructions changed

/updatefirstmate pulled new bytes onto disk and then asked each advanced
second mate to re-read them. A running agent holds AGENTS.md and every
loaded skill frozen from launch and no verified harness offers a reload,
so that steer could not reach a loaded skill at all and left the mate
holding two contradictory copies of its own job description.

An eligible mate is now restarted instead, in the same home and endpoint,
through the existing transactional relaunch. The restart is gated on the
mate first writing down the open work it holds only in conversation - the
open-record half of /stow, never its memory sweeps - so an unregistered
captain call is flushed before the conversation is spent. Anything that
leaves the reload unprovable falls back to the old re-read message and is
reported as exactly that, never as a clean reload.

Remote mates take the same path: fm-remote-secondmate-control.sh gains a
relaunch verb whose host-local leg runs that same control plane, since the
mate is an ordinary local secondmate from its host's point of view. The
primary resolves the profile and passes it explicitly, because
config/secondmate-harness is not inherited and the file on that host
belongs to a different home.

fm-update.sh now splits its advanced live mates into a restart set and a
nudge residual, and both sets require a changed instruction surface, which
also closes the over-nudge against the session-start sweep. Restart is
stricter still: a bin/-only advance reloads itself on the next call, so it
never costs a conversation.

Colocated tests cover the gating, the persist-then-restart order, the
task-subset persist request, each unsafe fallback, the remote hop, and the
remote sync's new instruction-surface report.

* no-mistakes(review): Fix restart correlation, concurrent waits, and lifecycle reporting

* no-mistakes(review): Parallelize relaunches and classify replacement incarnations

* no-mistakes(review): Gate restart actions on live agent state

* no-mistakes(review): Handle failed restart workers without hanging

* no-mistakes(review): Nudge legacy remotes and preserve persist recovery

* no-mistakes(review): Document one-time secondmate restart rollout

* no-mistakes(review): Honor arrived replies and refresh remote profiles

* no-mistakes(review): Revert remote parent profile reconciliation

* no-mistakes(review): Reset remote profile defaults and honor published results

* no-mistakes(review): Preserve fallback nudges for unverifiable secondmates

* no-mistakes(document): Document second-mate restart update flow

* no-mistakes(lint): Fix ShellCheck warnings in restart scripts

* perf: accelerate local validation with bounded concurrency (kunchenguid#3644)

* perf(tests): route gate verification through the bounded concurrent runner

Local validation was the pipeline's dominant cost: across 67 recorded
no-mistakes agent sessions on this repo, 99.3% of command execution was
`bash tests/*.test.sh`, run strictly one script at a time, and 2% of those
calls were killed by an agent-guessed timeout and paid for twice.

Three changes, each measured:

- `.no-mistakes.yaml` pins `commands.test` to
  `bin/fm-test-run.sh --changed --exclude-family real-herdr-gated`. The runner
  already owns changed-file selection, bounded concurrency, the refusal of
  unproven scripts, and a generous automatic per-script bound, so the gate's
  baseline is neither a serial chain nor a guessed timeout. It stays
  intent-targeted - the Test step still runs its evidence agent on top - and
  excludes the live-Herdr family the required Herdr lane owns.

- `bin/fm-test-run.sh` gives a plain list of script paths the same bounded
  automatic scheduler and automatic bound that `--changed` gets. Naming several
  subjects is how a verification round asks for exactly those scripts. The
  curated selections are untouched: `--lane` still composes CI shards whose
  serial lane must stay serial, `--family` is what the required Herdr lane runs,
  and `--all` stays a deliberate complete regression.

- `pr-forge` is admitted to the concurrent-safe family registry on two
  consecutive clean proofs. `docs/fm-test-isolation-proof.md` records those,
  and records `secondmate` and `session-bootstrap` as refused with the exact
  script and reason each failed on, so the refusals are actionable rather than
  silent.

Measured on this host, 0 failures on both sides:

  verification round, 4 scripts   448s chained -> 231s through the runner (-48%)
  pr-forge family                 409.2s at 1 worker -> 237.9s at 4 (1.72x)
  watcher-wake-lock family        1311.1s at 1 worker -> 539.3s at 4 (2.43x)

A fourth lever was implemented and then removed because the measurement
refused it: raising the bounded-wait sample interval from 0.1s to 0.5s made
`fm-watch-triage.test.sh` slower, 435s and 440s against 390s and 393s
unchanged, back to back. Those sleeps are not overhead added to the clock -
they are how a test waits for a subject moving on fm-watch.sh's own one-second
cadence - so sampling less often only delays detection. It also broke
`fm-watcher-lock.test.sh`, which catches a transient rather than waiting for a
settled condition. CONTRIBUTING.md records that result so the experiment is not
repeated.

* no-mistakes(review): Separate concurrent runs by isolation proof family

* no-mistakes(review): Limit automatic timeouts to changed-file validation

* no-mistakes(document): Clarify validation concurrency documentation

* fix: copy PR URLs from durable records (kunchenguid#3648)

* fix: copy PR URLs from records or abstain, never assemble them

Supervision reported a plausible but dead PR link three times because its
prompt demanded a full https:// URL at a moment when only a PR number was
observable, so the model assembled an owner/repository from memory, and the PR
check then accepted that URL and wrote it into the task record, after which the
model kept defending its own tool-endorsed guess over the worker's real link.

Three changes close that chain without any live forge lookup, so private
forges are treated exactly like public ones:

- bin/fm-branch-prompt.sh no longer mandates a URL. Its new "PR identity: copy
  or abstain" section requires a URL to be copied verbatim from a durable
  record (the done: PR <url> status line, pr= metadata, or the backlog note),
  forbids assembling owner, repository, host, or number from memory, and has
  the branch report only the identifier it actually holds when no record names
  the URL yet, leaving the PR check unarmed until the worker's ready line
  arrives. AGENTS.md section 7 and 9 carry the same copy-or-abstain rule for
  main in place of the bare full-URL mandate.

- Worker briefs (bin/fm-brief.sh, ship and scout rules) require the full
  https:// URL wherever a PR is mentioned - status line, terminal, or summary -
  never a bare "PR 108", so the link is in view as early as the number is.

- bin/fm-pr-check.sh refuses, offline and before any side effect, a URL that
  the task's own done lines contradict, printing both spellings; a log naming
  no URL still records the argument as before. fm_pr_status_ready_urls in
  bin/fm-pr-lib.sh owns reading those lines. The refusal also reaches
  bin/fm-pr-merge.sh, so nothing merges under a contradicted URL.

Tests cover the offline refusal with zero side effects, the recorded spelling
being accepted, markdown-wrapped and punctuated URLs, working lines not
counting, the merge wrapper propagation, a self-hosted merge request with no
forge call, the prompt carrying the rule, and the brief carrying the worker
rule.

* no-mistakes(review): Remove stale PR URL enforcement

* no-mistakes(ci): Removed backlog notes as an accepted PR identity source. PR URLs may now be copied only from the task’s `done: PR <url>` status or canonical `pr=` metadata; otherwise supervision reports only the known identifier and leaves PR checking unarmed. Updated related guidance/docs and verified with branch-supervision tests, brief tests, ShellCheck, and `git diff --check`

* fix(bin): disable Claude feedback drafts for fleet launches (kunchenguid#3661)

* fix(bin): disable Claude's feedback-draft flow for fleet-launched agents

Scope --settings '{"feedbackDrafts":"off"}' to every Firstmate-launched
Claude crewmate and secondmate, so /bug and /feedback never queue or
submit a bug report on the captain's behalf. feedbackDrafts is the
documented settings key (Claude Code changelog 2.1.247); the
per-launch CLI flag never touches the captain's global settings.json.

Claude-Session: https://claude.ai/code/session_01XYAXXzr4oZx9NjZb1veeE3

* no-mistakes(review): Prevent managed settings from re-enabling Claude feedback drafts

* no-mistakes(document): Fix Claude feedback documentation formatting

* fix(bin): layer both feedback-draft controls for defense in depth

The prior --settings-only fix can be overridden by a managed Claude
settings policy (feedbackDrafts precedence). Keep CLAUDE_CODE_SEND_FEEDBACK=0
alongside --settings '{"feedbackDrafts":"off"}': either control alone
disables the SendFeedback tool, so a managed override of one still
leaves the other in force.

Claude-Session: https://claude.ai/code/session_01XYAXXzr4oZx9NjZb1veeE3

* no-mistakes(document): Document Claude feedback-draft suppression ownership

* feat(tests): run three more validation families concurrently (kunchenguid#3662)

* perf(tests): admit three more families to concurrent validation

The three families that `docs/fm-test-isolation-proof.md` recorded as refused
were not refused for concurrency. Each blocker was a test that decided a
property by wall clock, or a script filed where it cannot run. Fixing those
three things admits all three families and recovers 28.6 minutes of local
validation with no assertion removed or weakened.

- `tests/fm-backlog-handoff.test.sh` injected its pre-move crash by killing the
  handoff, sleeping a fixed second, then delegating the move to the real
  binary. Nothing ever killed the fake, so on a host slow enough for the case's
  next assertions to take longer than a second, the orphan woke and completed
  the very move the case requires left undone, and recovery then failed with
  `Task "pre-move-crash" not found in this backlog`. Watching the two backlogs
  during the injected crash showed exactly that, the item moving one second
  after the crash. All four crash injections in the file now go through a new
  `fm_fake_crash_injector` shim that signals the target and returns only once
  it is observably gone, and the pre-move fake never delegates the move at all.

- `tests/fm-session-start.test.sh` proved the startup digest does not block on
  a slow current-state read by timing the whole digest against a fixed
  eight-second sleep, which a loaded host exceeds without the property being
  violated. It now holds that read open until the case releases it and asserts,
  the moment the digest returns, that the read has not finished. A digest that
  waited would wait indefinitely rather than for an interval a slow host can
  out-run, so the assertion is stronger than the bound it replaces. Its scan
  budget moves to the maximum, because the old value left two seconds of margin
  over the fixed sleep and measured the host rather than the deadline that
  `tests/fm-inactive-reconcile.test.sh` owns.

- `fm-backend-herdr-focus-flash-e2e` was filed in the family map's catch-all,
  which put it in the portable serial lane, where Linux CI gate-skips it: that
  real-Herdr regression was running nowhere. It moves to `real-herdr-gated` and
  the required Herdr lane. `fm-claude-stop-autoarm-live-e2e` gate-skips on its
  opt-in variable and moves to `live-harness-optin`.

The 28 remaining ungrouped scripts become an enumerated `standalone` family
instead of admitting `unclassified` itself. `unclassified` is the family map's
`*)` arm, so admitting it would silently grant concurrency to every test added
afterwards, which is exactly the population with no proof. A new test still
lands in `unclassified` and stays serial, and `tests/fm-test-run.test.sh`
covers that split behaviorally.

Each family passes two consecutive four-worker proofs with zero failures. On
the production runner, `secondmate` goes 1233.1s to 453.4s, `session-bootstrap`
756.4s to 286.4s, and `standalone` 724.6s to 261.1s: 2.71x overall and 1713.2s
recovered. The whole suite runs 177 scripts in 52.6 minutes of wall clock
against 121 minutes of summed script time.

* no-mistakes(document): Refresh concurrent validation and shard documentation

* no-mistakes(ci): Fixed the real-Herdr focus-flash E2E race exposed by reclassification. Part C now starts its persistent child atomically via `pane run` and verifies stable child identity through Herdr’s public `process-info` interface, avoiding the racy send-text/send-keys sequence and platform-specific `ps` matching. Verified with bash syntax checking, ShellCheck, git diff checks, and the complete E2E test on Herdr 0.8.2

* feat: structure no-mistakes ask-user escalations (kunchenguid#3670)

* feat(brief): structure no-mistakes ask-user escalation as event + snapshot file

Crewmates escalating a no-mistakes ask-user gate now report one status
event naming every finding id plus a snapshot file holding the gate's
axi finding records verbatim (id, severity, file, line, description,
authority), using the same shape even for a single finding. The status
line never paraphrases. The format is defined once in fm-dod-lib.sh and
rendered into both the scout and ship rule 6 in fm-brief.sh, so a
promoted scout - whose rule 6 fm-promote.sh preserves unchanged - gets
the identical contract as a freshly-spawned no-mistakes ship worker.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PpiWaDerbYavTLPPtEjQei

* no-mistakes(review): Preserve ask-user escalation output contract

* no-mistakes(review): Align escalation format test expectation

* no-mistakes(review): Scope ask-user escalation instructions correctly

* no-mistakes(review): Remove ask-user from generic decision rules

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* fix(bin): require self-sufficient no-mistakes intent (kunchenguid#3671)

* fix(bin): require a self-sufficient no-mistakes intent

A no-mistakes worker's --intent is only as useful as the string it
passes. PR kunchenguid#3604 shipped with an intent that was only "do 1, 2, 3, 7
from the report": the real contract lived in a private scout report and
never reached --intent, so nobody holding that string plus the codebase
could have derived the specification.

This is pure instruction at the contract's one owner; no spawn-side or
promotion-side check is added.

- bin/fm-dod-lib.sh: the generated no-mistakes Definition of done now
  states that the --intent string must be self-sufficient (the string
  plus the codebase reconstructs roughly the same specification) and
  tells the worker to write the substance of any report, decision, or
  PR the captain's intent refers to into --intent rather than the
  pointer, while Firstmate build instructions and the worker's own
  decisions still stay out. The spawn-time overlay points back at that
  rule so its "supersedes" wording cannot cancel it, and the header's
  owner statement carries the rule.
- AGENTS.md section 11 and bin/fm-brief.sh's header ask Firstmate to
  include the substance of referenced material when filling
  ## Captain's intent, and section 11 points at the owner of the rule.
- tests/fm-brief.test.sh and tests/fm-task-delivery.test.sh assert the
  rendered brief and launch contract carry the rule.

Claude-Session: https://claude.ai/code/session_01YMhEe42q7BAAoN6RxNuzim

* no-mistakes(document): Replace incident-specific intent test commentary

* fix: accelerate local Bearings snapshot composition (kunchenguid#3499)

* Speed local fleet snapshot composition

* no-mistakes(review): Stabilize task inventory during concurrent snapshot composition

* no-mistakes(document): Document local snapshot observation concurrency

* no-mistakes(ci): Fixed CI failures by making empty task manifests compatible with stock macOS Bash 3.2, snapshotting task metadata before concurrent observations to prevent generation drift, strengthening the behavioral race regression, and updating the stock-Bash Bearings test count to 45. Verified fleet snapshot tests (15), Bearings tests (45), workflow lint tests, project lint, Bash 3.2 parsing, and diff checks

* no-mistakes(ci): Fixed the Linux CI failure caused by passing large backlog/task JSON through jq command-line arguments, which exceeded the per-argument size limit. Both inventory projections now stream large JSON inputs through stdin. Verified with fm-bearings-snapshot.test.sh (45 tests), fm-fleet-snapshot-view.test.sh (15 tests), Bash syntax, and git diff checks

* no-mistakes(ci): Fixed concurrent task teardown during metadata capture: vanished metadata is now omitted while genuine copy failures remain fatal. Added a deterministic public Bearings regression test and updated CI’s expected test count. Verified with the full Bearings suite, workflow-lint suite, Bash syntax checks, and git diff checks

* no-mistakes(ci): Fixed PR-caused CI and review issues: streamed large fleet JSON through jq stdin to avoid Linux argument limits, kept crew-state reads bound to captured metadata generations, and strengthened the behavioral race test. Bearings (46 tests), fleet snapshot (15 tests), crew-state, backend, lint, Bash syntax, and diff checks pass locally. Serial shard 5’s unrelated task-inbox segmentation fault appears infrastructural/flaky

* no-mistakes(ci): Fixed endpoint-state generation crossing by validating captured spawn_gen before and after local endpoint probes, falling back to exact metadata identity for legacy tasks. Stale probe results now become unknown instead of false unhealthy state. Added a behavioral relaunch-race regression test. Verified the full Bearings snapshot suite, shellcheck, bash syntax, and git diff checks

* fix(snapshot): keep live observations generation-coherent

* no-mistakes(review): Keep secondmate observations generation-bound without copying reports

* no-mistakes(document): Document generation-coherent snapshot observations

* test(bearings): measure local read overlap instead of wall-clock budget

The large-local-snapshot regression asserted that a whole snapshot
composed in under five seconds. That bound measures how loaded the host
is, not whether the per-task reads actually overlap, so it failed
intermittently on a contended machine: one run in six on a box at load
16-20, landing exactly on the five second boundary.

Time a serialized run and a concurrent run of the same workload instead
and require the concurrent one to save at least two seconds. Both runs
pay the same composition overhead, so the difference isolates the
overlap this change delivers. Five one-second reads serialize into five
seconds and overlap into about one, and re-serializing the reads
collapses the saving to roughly zero, so the assertion still fails
loudly if the concurrency regresses.

Also bump the pinned Bearings test count to 48, since rebasing onto the
current default branch picked up its captain-hold test.

* no-mistakes(review): Restore JSON-derived decision flags

* no-mistakes(review): Unify status-derived snapshot observations

* no-mistakes(ci): Updated the stock macOS Bash CI check’s Bearings test count from 48 to 49. Verified the full Bearings suite passes and emits exactly 49 TAP successes; git diff checks pass

* fix: prevent stale supervision wake loops (kunchenguid#3672)

* fix(bin): stop the supervision branch's stale-ack and ghost-report loops

Clean-slate implementation of the four authorized recommendations from the
supervision-ghost-retrigger analysis (items 1, 2, 3, and 7), in their minimal
form, superseding PR kunchenguid#3604:

- fm_branch_report refuses a task the wake being handled never named. The
  extension fixes the reportable task set from the eligible rows before each
  prompt (signal and stale rows resolve to their tasks, a heartbeat allows any
  task with a live record, fleet is always allowed), so a report typed from
  memory about a task whose records teardown already removed is never stored
  or delivered.
- An acknowledgement that consumes nothing says "nothing was acknowledged
  through N" and prints the exact --ack-through / --recovery-generation
  command for the current presented wake, instead of "re-run the drain",
  which re-fed the same stale acknowledgement in a loop.
- bin/fm-guard.sh no longer tells the branch actor to drain queued wakes
  while it is handling them; it names the granted rows instead.
- Teardown removes state/.<task>.branch-outcome-index for ordinary tasks and
  descendants; the index rebuild and the append-side index write both skip a
  task with neither a live record nor a status log, so the branch's report of
  a teardown it just performed is stored without recreating the index.

No new locking, no spawn-generation binding, and no retired-task refusal: the
branch can still report the outcome of a task it just tore down, and the
teardown test now proves that path end to end.

* fix(bin): narrow the branch report scope and guard silence to the minimal form

Apply the four review decisions on the clean-slate branch:

- A signal or stale prompt may report only the tasks its own rows resolve
  to; fleet is refused there too. A heartbeat review is not scoped by task
  at all, so the extension no longer tracks live task records and refuses
  nothing by task id during a fleet review.
- The outcome-index rebuild no longer skips retired tasks; the append-side
  skip alone keeps a torn-down task's index from being recreated.
- bin/fm-guard.sh keeps the queued-wakes warning silent for the branch actor
  instead of printing a replacement note.

* no-mistakes(document): Align supervision docs with scoped wake handling

* fix(bin): avoid fleet snapshot argument limits (kunchenguid#3677)

* Fix fleet snapshot large JSON transport

* no-mistakes(review): Captain: file-back fleet snapshot transport safely

* no-mistakes(review): Captain: file-back parent summary aggregation

* no-mistakes(ci): Rebased the PR's three commits onto f4d7875 and resolved the fleet snapshot conflict while preserving the base's task-observation lifecycle. Fixed Greptile's valid finding by recursively removing the private mktemp transport directory, so future transport files cannot cause cleanup to fail. Verified with tests/fm-home-summary-refresh.test.sh, bin/fm-lint.sh, git diff --check, and ancestry checks. All passed; the fix remains as an uncommitted worktree change for the outer executor

* fix(bin): attribute active runs with unfetched pipeline heads (kunchenguid#3681)

* fix(bin): recognize active pipeline fix rounds with unfetched run heads

A no-mistakes fix round advances the run head beyond the submitted head,
and the pipeline commits in its own checkout, so the task copy never
receives the new commit object. fm-crew-state's strict head rule rejected
the active row, the coarse runs-list scan skipped it and matched the
older failed row at the submitted head, and an active validation read as
failed (observed on model-routing-benchmark-hardening: active head
ac61c64 vs task copy at fb47636d).

fm_nm_runs_status_for_worktree in bin/fm-nm-run-lib.sh now owns
runs-ledger attribution: the branch's newest row alone decides, and a
newest row whose head cannot resolve locally is recognized only as a
provable pipeline-owned continuation - active (running) and anchored by
the immediately older row for the same branch having ended at exactly
this worktree's HEAD. The reader keeps the axi TOON as full detail for
that proven same-branch run. Unanchored, ancestor-anchored, and terminal
unresolvable rows stay unattributed, so branch-name coincidence and other
tasks' runs never match, and fm_nm_head_matches_worktree keeps its exact
prior semantics for teardown (verified by the full teardown suite).

Tests: reproduction regression for the unfetched active fix head (reads
working via full run-step detail), coarse-path continuation when axi
answers another branch, and negative controls for the unanchored active
row and the unresolvable terminal row with the historical fallback
preserved.

Ported onto upstream/main f4d7875, where kunchenguid#3194 independently added the
branch_sync custody exemption on the full axi-status path: both mechanisms
now coexist, each owning one surface (TOON custody on the full path, the
runs ledger on the coarse path). The port deletes the superseded coarse
scan-and-skip (nm_runs_status_for_branch) and its now caller-less helpers
(fm_nm_head_resolvable, nm_coarse_head_matches_worktree), renames the
exemption comment's "the one exemption" phrasing now that a second
complementary exemption exists, and points the stale
FM_CREW_STATE_RUNS_LIMIT comment at fm_nm_runs_status_for_worktree
(judge follow-up #1). The parent coarse-guard test's fixture is the
ledger-anchored continuation shape, so its expectation flips to the fixed
behavior (working via run-step, never the older failed row); a new
mismatched-anchor coarse negative control preserves that guard's original
no-anchor protection (pane answers, never the older row).

* no-mistakes(document): Clarify pipeline attribution documentation

* fix(bin): pre-register claude workspace trust at spawn time (kunchenguid#3663)

* fix(bin): pre-register claude workspace trust for task worktrees

A claude crewmate launched into a fresh task worktree met Claude Code's
interactive workspace-trust dialog before it ever read its brief, and firstmate
could not answer it: the key plane carries only Enter, Escape, and C-c with no
arrow navigation, and the dialog's selection starts on "No, exit", so the
documented Enter recipe ended the session instead of accepting it. Two workers
wedged this way and were unblocked only by hand-seeding the trust store per
path.

--dangerously-skip-permissions does not cover that gate. `claude --help`
records the dialog as skipped only in non-interactive mode, through -p or a
non-TTY stdout, and a crewmate pane is interactive, so there is no launch flag
to reach for.

fm-spawn now pre-registers the worktree through bin/fm-claude-trust.sh in the
existing claude branch, before the project settings that the same gate would
otherwise block, and refuses the spawn when that write fails rather than
launching a worker that would wedge.

The scope test is the safety property and is structural rather than a path
policy: the path must be a linked git worktree, sharing the spawning project's
common dir, whose top level is exactly the resolved argument. Git is the ground
truth, so the argument is never trusted on its own word, and a primary
checkout, an unrelated repo, a worktree subdirectory, a plain directory, and a
home directory are each refused rather than warned about or skipped. A
treehouse or orca path prefix was deliberately avoided because treehouse's root
is configurable, which would make a prefix both wrong and a new policy surface.
One structural test covers both worktree providers.

tests/fm-claude-trust.test.sh pins both halves, including a case where HOME is
itself a valid linked worktree so the home guard is proven load-bearing rather
than passing vacuously, plus the spawn-level proof that a claude spawn trusts
its worktree and launches with the brief pointed at the same store.

The adapter reference no longer tells a firstmate to press Enter on that
dialog, and the shared trust reference now names every harness surface: which
harnesses gate, which suppress at launch, which dodge the gate, which now
pre-registers, and that a claude secondmate is excluded by design.

The spawn fixture runs each spawn against a throwaway HOME so the suite cannot
write the developer's real store, isolating through HOME rather than
CLAUDE_CONFIG_DIR because the spawn forwards a set CLAUDE_CONFIG_DIR onto the
launch command that launch-shape assertions read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HNEN2GLnew27HFyfi4ms4v

* fix(bin): create the staged trust store exclusively

The staged store was written to a predictable pid-based path with a plain
write, which follows a symlink. Where the Claude config directory is writable
by another local account, that account could pre-create the path as a symlink
and redirect the write into another file the launching user owns.

The staged name now carries random bytes and is created with an exclusive
"wx" open, so an existing path is refused outright instead of followed. The
happy-path test also asserts no staged store survives the rename.

The durability comment now states the residual window plainly: the readback
proves the entry landed, not that it survives, because a vendor session that
rewrites the whole store afterwards can still drop it and no lock closes that
window when the writer is Claude itself. The worker then meets the dialog and
stalls, which reaches firstmate as the ordinary stale wake rather than as
silent success.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HNEN2GLnew27HFyfi4ms4v

* no-mistakes(review): neutralise CDPATH in claude trust scope guard

* no-mistakes(review): sandbox HOME in spawn tests, drop out-of-scope artifacts

* no-mistakes(review): refuse unresolvable git dir, compact store, fix secondmate doc

* no-mistakes(review): clear git env overrides, resolve symlinked store target

* no-mistakes(review): degrade without node, fix Pi gate claim, record trust proof

* no-mistakes(review): refuse without node, pin CLAUDE_CONFIG_DIR in spawn tests

* no-mistakes(review): refuse relative config dir and concurrent store modification

* no-mistakes(review): correct orca worktree claim, clean staged store on failure

* no-mistakes(review): restore pretty-printed store, correct trust dialog docs

* no-mistakes(review): arm trust gate before busy state to avoid orphans

* no-mistakes(document): record claude trust pre-registration in its owner docs

* no-mistakes(document): note orca limit for claude trust pre-registration

* no-mistakes(ci): Fixed the Greptile P1 on bin/fm-spawn.sh by moving the Claude trust gate earlier rather than adding cleanup machinery. Diagnosis: Greptile reported that when Claude trust registration fails on tmux/Zellij/cmux/non-projected Herdr, the exit runs after the backend endpoint and /tmp/fm-<id> were created, and the abort trap cleans neither. The endpoint half is pre-existing, deliberate architecture — the two refusals immediately above the gate (the 60s `treehouse get` timeout at fm-spawn.sh:2550 and `validate_spawn_worktree` at :2487) also exit with the endpoint live and direct the operator with "inspect window $T"; spawn_abort_cleanup only reclaims orca endpoints (already covered via ORCA_ABORT_CLEANUP) and herdr projections. The temp-root half was genuinely introduced by this PR: the gate was placed beside the busy-state arm, ~30 lines after `mkdir -p "$TASK_TMP/gotmp"`, and fm-teardown can only find that root through `tasktmp=` in a meta record a refused spawn never publishes. Root-cause fix (smallest correct change, no new subsystem): - bin/fm-spawn.sh — moved the `claude*` trust gate from inside the busy-arm block up to the first point $WT is known, immediately after the `freshen_spawn_worktree_base` block and before TASK_TMP creation, the STATE setup, and the relaunch `clear_relaunch_harness_wiring` retirement. A refusal now leaves no temp root, no retired relaunch wiring, and no busy record; only the endpoint remains, in the same class as the two refusals just above it. - bin/fm-spawn.sh — the refusal message now ends with "inspect window $T", matching the existing convention so control/teardown can identify the endpoint. $T is set for every backend on the non-secondmate path. - bin/fm-spawn.sh:196 — header note corrected from "before any state is armed" to "before any per-task state exists". - tests/fm-claude-trust.test.sh — the existing refused-spawn test's own comment claimed "before any task state exists" but only asserted busy state. Renamed to test_refused_spawn_leaves_no_task_state and added an assertion that /tmp/fm-<id> is absent, with the task id suffixed by the test process pid so the assertion reads only this run's path (a stale /tmp/fm-refusedspawn from the fixed-id version was in fact present on this box). No assertions on implementation source bytes. Verification run locally: - The new assertion fails against the pre-fix bin/fm-spawn.sh ("not ok - a refused spawn stranded a temp root no teardown can find") and passes after — a real before/after regression proof. - tests/fm-claude-trust.test.sh: 20/20 ok. - tests/fm-backend.test.sh, fm-backend-orca, fm-control-relaunch, fm-spawn-dispatch-profile, fm-trace-context-spawn, fm-gotmp: all pass. - tests/fm-backlog-atomicity.test.sh: rc=0, 79 assertions ok. - bin/fm-lint.sh (repo's single lint owner, pinned ShellCheck 0.11.0 + actionlint 1.7.12): clean. - No /tmp/fm-refusedspawn* leftovers after the runs. Scope respected: no trust subsystem, no policy layer, no config surface, no endpoint-cleanup mechanism added; the change is an ordering move plus one error-message clause and the test that pins it. Adapter references and docs made no ordering claim, so none needed updating. Changes are left uncommitted in the worktree for the outer executor

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix: restart every live second mate after updates (kunchenguid#3690)

* feat(update): restart every live second mate after a successful update

/updatefirstmate only restarted a second mate when that pass advanced its
AGENTS.md or .agents/skills. An already-current home was skipped entirely, a
bin/-only advance was steered instead, and a remote host that could not report
its instruction diff was downgraded to a re-read. A running agent also freezes
its launch-time wiring - turn-end hooks, harness flags, per-harness feature
switches - and none of that is derivable from a file diff, so an unchanged
tracked surface is not evidence the agent is already on the current behavior.

Restart is now unconditional on a successful update of that home. Every live
second mate the pass leaves on the target commit is restarted, whether it
advanced or was already there.

The safety contract is unchanged: open records are persisted before the agent is
replaced, nothing is forced, stashed, or discarded, a home the pass had to skip
is not restarted at all, and a mate whose runtime cannot prove a restart keeps
the honest re-read path and is never reported as reloaded.

bin/fm-ff-lib.sh gains a settled-state hook that fires for a home left at the
base whether it advanced or was already there, and never for a skipped one; the
instruction-gated hook the session-start convergence sweep uses is untouched.

Regressions: fm-update pins the already-current mate into the restart set and
the unprovable one into the nudge set, and fm-secondmate-restart drives both
real commands end to end - an already-current home is named, persisted, and
genuinely replaced with its checkout untouched, while the unprovable one keeps
its running agent.

* no-mistakes(document): Document unconditional secondmate restarts

* fix(bin): close pending-reply decisions via resolve-key (kunchenguid#3696)

* fix(bin): close reserved pending-reply keys via fm-send --resolve-key

fm-send wrote answered: notes that the reserved-key fold ignores, so
operator closes exited 0 while OPEN DECISIONS kept the decision open.
Speak the owning library's close vocabulary on that path, and refuse
when a reserved close cannot take effect.

* no-mistakes(review): Safely quote manual decision-close recovery commands

* no-mistakes(review): Reject unclosable overlong decision keys before sending

* no-mistakes(review): Remove contract suffix from open decisions hint

* no-mistakes(document): Document resolve-key line-cap refusal

* fix(bin): prevent false missed-reply escalations (kunchenguid#3697)

* fix(bin): stop false missed-reply escalations for same-basename self-home answers

A healthy secondmate that wrote corr= to its own state/<id>.status never matched the parent channel, so recovery confirmed and the record escalated as pending-reply-missed. Make the report helper resolve the parent channel itself, skip parent-replies.status as wrong-home, put a readable sighting path on the missed line, and restatement-copy only that same-basename self-home file onto the parent channel.

* no-mistakes(review): Resolve late replies before recovery escalation

* no-mistakes(review): Tighten reply routing and regression coverage

* no-mistakes(review): Preserve reply paths and require explicit home

* no-mistakes(review): Encode wrong-home paths before persistence

* no-mistakes(document): Document corrected secondmate reply routing

* no-mistakes(lint): Fix pending-reply ShellCheck warnings

* feat: add verified Gemini crewmate runtime (kunchenguid#3695)

* feat(harness): verify gemini as a crewmate runtime adapter

Adds Gemini CLI as a fourth dispatch target alongside claude, codex, and
grok, scoped to crewmate and scout work only. Every axis was proven against
gemini-cli 0.58.0 rather than inferred; docs/verification/runtime-backends.md
carries the dated evidence and names what stayed unverified.

Busy state is semantic, not rendered: BeforeAgent opens a turn and AfterAgent
and SessionEnd close it. AfterAgent also fires on a manual interrupt, so a
cancelled turn closes its own record.

Three findings shaped the wiring rather than a config line:

- --skip-trust and GEMINI_CLI_TRUST_WORKSPACE=true are presented by the CLI
  as equivalents and are not. A controlled A/B showed --skip-trust leaves
  project configuration unloaded, so workspace skills never load.
- The worktree's .gemini/settings.json is the PROJECT's committed settings
  file, unlike claude's settings.local.json. Firstmate's hooks therefore go
  to a firstmate-owned state/<id>.gemini-settings.json reached through
  GEMINI_CLI_SYSTEM_SETTINGS_PATH, which also works untrusted and merges
  with a project's own hooks instead of replacing them.
- The shipped CLI is a node bundle whose live process reports comm as
  MainThread, so ancestry cannot see it. GEMINI_CLI=1 is load-bearing and is
  tested before an inherited CLAUDECODE, and pane liveness identifies gemini
  from the script argument through the new bin/fm-gemini-lib.sh.

Gemini is refused for secondmates: it has no primary supervision protocol.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYdQkKfSEQrFUxtcTXZ66L

* test: clear gemini's marker in launch and detection expectations

Every non-gemini launch now clears GEMINI_CLI the way it already clears
cursor's markers, so the two tests that pin the exact launch prefix are
updated to match. The harness-detection tests that scrub foreign markers
before probing ancestry scrub GEMINI_CLI too, so running the suite from
inside a gemini session cannot produce a false verdict.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYdQkKfSEQrFUxtcTXZ66L

* docs: classify the gemini harness reference

The documentation inventory is the single classification owner for maintained
prose surfaces, and every surface must appear in it exactly once. The new
harness reference is agent-runtime, matching its siblings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYdQkKfSEQrFUxtcTXZ66L

* no-mistakes(review): Narrow Gemini ancestry detection

* no-mistakes(review): Restrict Gemini hooks to canonical launches

* no-mistakes(document): Document Gemini adapter support boundaries

* no-mistakes(ci): Fixed Gemini process identity when interpreter or script paths contain whitespace. Tmux liveness now uses NUL-delimited /proc argv on Linux, with the existing flattened ps fallback elsewhere. Added a real-process regression test. Verified with the Gemini harness test suite, full fm-lint, ShellCheck, and git diff --check. The CI and Require no-mistakes runs were action_required/attestation outcomes rather than code failures

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(teardown): conclude parked runs advanced past task copy (kunchenguid#3704)

* conclude parked runs the pipeline advanced past the task copy

A no-mistakes fix round commits in the daemon's own gate-repo clone, so a
run parked at a gate can carry a head whose object the task copy never
received. Teardown's strict object-local identity rule then declined to
conclude the run, and cleanup left it parked forever holding a fleet slot
(observed 2026-09-03; the same masking condition PR 3681 fixed on the
read path, now closing the teardown half its scope boundary deferred).

task_status_is_own_parked_run now falls back - only when the reported
head resolves to no local object - to the one shared runs-ledger
attribution rule fm_nm_runs_status_for_worktree (bin/fm-nm-run-lib.sh),
whose anchored continuation proof binds the branch's newest active row
to this worktree's exact submitted head. Foreign branches, stale
history, terminal rows, ancestor-only anchors, diverged newer rows, and
ambiguous multi-row shapes all still refuse, and runs that are actively
running, fixing, or in CI remain untouched: only the parked-at-a-gate
determination ever reaches the abort. No sqlite access, no fetches into
another task copy, no custody changes, no duplicated matching logic.

* tighten the parked-run ledger fallback and pin both judge corrections

The teardown ledger fallback now authorizes concluding this task's parked
run only when the shared runs-ledger rule's proved answer is the explicitly
active word (running): a terminal newest row - even anchored at exactly the
worktree's head - is finished history and never an abort authorization.
The read path may classify the same owner's answer; teardown's abort must
never fire for a run that already ended.

Two bounded pre-validation corrections from the implementation review:
- a fetched-object counterfactual pins the strict-rule path: a pipeline fix
  head fetched into the task copy aborts through object-local identity
  alone, with an empty ledger and a proof the runs query never fired;
- a negative fixture pins the tightened boundary: an unresolvable reported
  head with a terminal newest same-branch row anchored at the worktree head
  engages the ledger fallback and still refuses, so the refusal is the
  terminal-word boundary and not an earlier guard.

* no-mistakes(review): Bind teardown ledger fallback to validated run heads

* no-mistakes(review): Restore validated advanced-head ledger continuation

* no-mistakes(review): Reject invalid ledger dates and terminal statuses

* no-mistakes(document): Document teardown ledger scan limit

* feat(bin): show requested vs effective model in Herdr agent view

Track spawn-config requested_model separately from runtime-verified
effective_model, probe Claude/Pi transcripts for exact API ids, push
compact display metadata to Herdr, and preserve verified models across
relaunch/compaction hooks without inferring aliases as truth.

* fix(bin): keep re-probing effective model after first exact reading

fm-model-sync.sh only probed for the runtime-verified effective model
while it was still pending/UNKNOWN, so a session that later switched
models (manual switch, provider fallback) kept displaying the first
verified model forever and never appended a fallback-history entry.
Probe unconditionally instead; fm_model_record_effective already
no-ops when the probed value is unchanged, so this stays cheap.

Addresses the Greptile P1 finding on PR kunchenguid#3705's fm-model-sync.sh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwfFjeYQcz9cZ3vEZohmpm

* fix(bin): distinguish Cursor Grok, direct xAI Grok, and Anthropic Claude in the display

Kapitänskorrektur: harness alone conflated Cursor-hosted Grok models
(cursor-grok-4.6-*) and direct xAI Grok models (xai/grok-4.6) under
one generic label, and displayed Anthropic Claude without naming the
provider. Add fm_model_source_label, pattern-matched on the verified
exact model id, so the compact display always reads Cursor · Grok,
xAI · Grok, or Anthropic · Claude with the exact model id appended.
Falls back to the existing harness label for every other model. No
routing change: this only affects display strings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwfFjeYQcz9cZ3vEZohmpm

* fix(bin): wire model-sync into the Pi extension's turn lifecycle

fm-model-sync.sh was only invoked from Claude's SessionStart/
UserPromptSubmit/Stop hooks; the Pi harness's own extension
(state/<id>.pi-ext.ts) never called it, so a Pi-hosted session (e.g.
a pi/xai-grok crewmate) never refreshed its effective model after the
first probe and Herdr kept showing the stale value with no
fallback-history entry. Call fm-model-sync.sh from the same
agent_start/turn_end boundaries Pi already uses for busy-state and the
turn-end notification touch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwfFjeYQcz9cZ3vEZohmpm

* fix(bin): serialize fm-model-sync.sh's meta read-probe-write

Overlapping lifecycle events (Pi's agent_start/turn_end, Claude's
SessionStart/UserPromptSubmit/Stop) can invoke fm-model-sync.sh
concurrently for the same task. The unlocked read-probe-write let
interleaved runs revert a newer effective model, mismatch its
source, or duplicate a model-history entry. Serialize the critical
section through the same per-task meta lock fm-spawn.sh already uses
(fm_meta_lock_path + fm_lock_acquire_wait/fm_lock_release), released
before every exit path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwfFjeYQcz9cZ3vEZohmpm

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Arthur Haro <38157909+haroarthur@users.noreply.github.com>
Co-authored-by: Nicolas Payette <nicolas.payette@specira.ai>
Co-authored-by: Jon Roosevelt <rooseveltadvisors@gmail.com>
Co-authored-by: att430 <41454889+att430@users.noreply.github.com>
Co-authored-by: Valentino-Sole <171032438+Valentino-Sole@users.noreply.github.com>
dnth pushed a commit to dnth/firstmate that referenced this pull request Sep 5, 2026
* feat(update): restart every live second mate after a successful update

/updatefirstmate only restarted a second mate when that pass advanced its
AGENTS.md or .agents/skills. An already-current home was skipped entirely, a
bin/-only advance was steered instead, and a remote host that could not report
its instruction diff was downgraded to a re-read. A running agent also freezes
its launch-time wiring - turn-end hooks, harness flags, per-harness feature
switches - and none of that is derivable from a file diff, so an unchanged
tracked surface is not evidence the agent is already on the current behavior.

Restart is now unconditional on a successful update of that home. Every live
second mate the pass leaves on the target commit is restarted, whether it
advanced or was already there.

The safety contract is unchanged: open records are persisted before the agent is
replaced, nothing is forced, stashed, or discarded, a home the pass had to skip
is not restarted at all, and a mate whose runtime cannot prove a restart keeps
the honest re-read path and is never reported as reloaded.

bin/fm-ff-lib.sh gains a settled-state hook that fires for a home left at the
base whether it advanced or was already there, and never for a skipped one; the
instruction-gated hook the session-start convergence sweep uses is untouched.

Regressions: fm-update pins the already-current mate into the restart set and
the unprovable one into the nudge set, and fm-secondmate-restart drives both
real commands end to end - an already-current home is named, persisted, and
genuinely replaced with its checkout untouched, while the unprovable one keeps
its running agent.

* no-mistakes(document): Document unconditional secondmate restarts
dnth added a commit to dnth/firstmate that referenced this pull request Sep 5, 2026
* feat: restart second mates after instruction updates (kunchenguid#3614)

* feat(update): restart second mates whose instructions changed

/updatefirstmate pulled new bytes onto disk and then asked each advanced
second mate to re-read them. A running agent holds AGENTS.md and every
loaded skill frozen from launch and no verified harness offers a reload,
so that steer could not reach a loaded skill at all and left the mate
holding two contradictory copies of its own job description.

An eligible mate is now restarted instead, in the same home and endpoint,
through the existing transactional relaunch. The restart is gated on the
mate first writing down the open work it holds only in conversation - the
open-record half of /stow, never its memory sweeps - so an unregistered
captain call is flushed before the conversation is spent. Anything that
leaves the reload unprovable falls back to the old re-read message and is
reported as exactly that, never as a clean reload.

Remote mates take the same path: fm-remote-secondmate-control.sh gains a
relaunch verb whose host-local leg runs that same control plane, since the
mate is an ordinary local secondmate from its host's point of view. The
primary resolves the profile and passes it explicitly, because
config/secondmate-harness is not inherited and the file on that host
belongs to a different home.

fm-update.sh now splits its advanced live mates into a restart set and a
nudge residual, and both sets require a changed instruction surface, which
also closes the over-nudge against the session-start sweep. Restart is
stricter still: a bin/-only advance reloads itself on the next call, so it
never costs a conversation.

Colocated tests cover the gating, the persist-then-restart order, the
task-subset persist request, each unsafe fallback, the remote hop, and the
remote sync's new instruction-surface report.

* no-mistakes(review): Fix restart correlation, concurrent waits, and lifecycle reporting

* no-mistakes(review): Parallelize relaunches and classify replacement incarnations

* no-mistakes(review): Gate restart actions on live agent state

* no-mistakes(review): Handle failed restart workers without hanging

* no-mistakes(review): Nudge legacy remotes and preserve persist recovery

* no-mistakes(review): Document one-time secondmate restart rollout

* no-mistakes(review): Honor arrived replies and refresh remote profiles

* no-mistakes(review): Revert remote parent profile reconciliation

* no-mistakes(review): Reset remote profile defaults and honor published results

* no-mistakes(review): Preserve fallback nudges for unverifiable secondmates

* no-mistakes(document): Document second-mate restart update flow

* no-mistakes(lint): Fix ShellCheck warnings in restart scripts

* fix: restart every live second mate after updates (kunchenguid#3690)

* feat(update): restart every live second mate after a successful update

/updatefirstmate only restarted a second mate when that pass advanced its
AGENTS.md or .agents/skills. An already-current home was skipped entirely, a
bin/-only advance was steered instead, and a remote host that could not report
its instruction diff was downgraded to a re-read. A running agent also freezes
its launch-time wiring - turn-end hooks, harness flags, per-harness feature
switches - and none of that is derivable from a file diff, so an unchanged
tracked surface is not evidence the agent is already on the current behavior.

Restart is now unconditional on a successful update of that home. Every live
second mate the pass leaves on the target commit is restarted, whether it
advanced or was already there.

The safety contract is unchanged: open records are persisted before the agent is
replaced, nothing is forced, stashed, or discarded, a home the pass had to skip
is not restarted at all, and a mate whose runtime cannot prove a restart keeps
the honest re-read path and is never reported as reloaded.

bin/fm-ff-lib.sh gains a settled-state hook that fires for a home left at the
base whether it advanced or was already there, and never for a skipped one; the
instruction-gated hook the session-start convergence sweep uses is untouched.

Regressions: fm-update pins the already-current mate into the restart set and
the unprovable one into the nudge set, and fm-secondmate-restart drives both
real commands end to end - an already-current home is named, persisted, and
genuinely replaced with its checkout untouched, while the unprovable one keeps
its running agent.

* no-mistakes(document): Document unconditional secondmate restarts

* fix: restart live secondmates after self-update

* fix: enable OMP secondmate restart controls

* test(secondmate): add the OMP restart end-to-end regression

The OMP second mate restart had no behavioral regression: the pass was
proven only as far as capability. This adds the end-to-end test, plus the
seam and the artifact cleanup that test proved missing.

bin/fm-omp-secondmate-restart-lib.sh now owns the local OMP restart:
stop the home's session owner, prove it gone, retire the four session
artifacts bin/fm-spawn.sh refuses to launch over, launch the replacement
through bin/fm-spawn.sh <id> --secondmate, then reconcile the durable
record to the endpoint that launch published. The stop and the launch are
overridable through FM_RESTART_STOP_CMD and FM_RESTART_SPAWN_CMD, which
default to kill and bin/fm-spawn.sh, so production behavior is unchanged
when neither is set. bin/fm-secondmate-restart.sh routes a local
harness=omp mate there; every other harness and the remote hop are
untouched.

Without the artifact retirement an OMP restart could not complete at all:
.omp-ext.ts and .omp-ready survive the agent, and the launch owner refuses
a second mate launch while either is on disk.

tests/fm-secondmate-restart.test.sh T17 drives the whole real pass -
capability resolution, fm-send over the real OMP doorbell against a live
modelled session, the parent-owned persist gate, and the restart
orchestration - with only the stop and the launch replaced. Both stubs
record the world at the moment they ran, so the order is asserted rather
than assumed: the absence probe sees all four artifacts still present, and
the launch sees none.

Claude-Session: https://claude.ai/code/session_01Aq7cTZDgRxGYYAzPnaAwEh

* fix(secondmate): recover the OMP restart implementation and regress it end to end

The branch was missing the end-to-end OMP restart regression, and the
implementation it was supposed to cover was missing too: an earlier
validation run of this branch was cancelled after a fix was hand-committed
while it was active, which orphaned eight no-mistakes(review) fix commits
in the local gate. Those commits are the real implementation. They are
recovered here in full - their content is in the tree, squashed into this
commit rather than replayed as eight commit objects because the branch had
already been pushed past them. Nothing from them was dropped.

On top of that recovered implementation:

- fm_control_omp_secondmate_prepare_relaunch also proves the OMP home's
  session-lock owner gone before it retires anything. bin/fm-spawn.sh
  refuses a second mate launch while that pid is alive, and an artifact
  removed under a live owner is indistinguishable from a fresh one, so the
  next launch would accept a duplicate against a still-running mate. The
  endpoint proof stays first; both proofs precede the removal.
- FM_RESTART_STOP_CMD (default kill, invoked as "<cmd> <pid>" to stop and
  "<cmd> -0 <pid>" to probe) and FM_RESTART_SPAWN_CMD (default
  bin/fm-spawn.sh) make the two real side effects overridable. Unset, both
  are the real thing and behavior is unchanged.
- fm-control's agent_state passes the task's durable record to
  fm_backend_agent_state. Without it an OMP or Hermes endpoint cannot be
  attributed from its own process identity and reads ambiguous, which
  refused every lifecycle verb on a healthy OMP second mate - the relaunch
  could not take its first state reading.

tests/fm-secondmate-restart.test.sh T17 drives the whole real chain:
capability routing, fm-send over the real OMP doorbell against a live
modelled session, the parent-owned persist gate, and fm-control's relaunch
transaction with its OMP preparation step. Only the stop and the launch are
stubbed, and both stubs record the world at the moment they ran, so the
order is asserted rather than assumed: the absence probe sees all four
session artifacts still present, the launch sees none.

Claude-Session: https://claude.ai/code/session_01Aq7cTZDgRxGYYAzPnaAwEh

* no-mistakes(test): Fixed remote metadata propagation and cursor relaunch validation

* no-mistakes(document): Document relaunch endpoint behavior accurately

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
Valentino-Sole added a commit to Valentino-Sole/firstmate that referenced this pull request Sep 8, 2026
* fix: start a fresh supervision branch for every main session (kunchenguid#3600)

* fix(pi): start a new supervision branch conversation per main session

The supervision branch reopened one recorded conversation forever, so
every main session start reloaded the current generated prompt and then
weeks of accumulated thread, where a superseded rule could still outweigh
today's.

The branch conversation is now scoped to one main session: the session
generation owns the recorded conversation, so a cold start, /new,
/resume, /fork, or a reload always builds a new one, while a rebuild
inside one session (a model or effort change) still continues that
session's own conversation.

The dialog mirror re-anchors with it. Its durable cursor records what the
previous branch conversation received, so a /resume or reload - which
keeps main's own session file - would otherwise leave the new branch
blind to dialog main itself still has. The reset is bounded by the
current main session, and the cursor keeps advancing incrementally within
it. The durable outcome store and its processed marker are untouched, so
unacknowledged captain-facing outcomes still re-present on the new main
session.

* no-mistakes(document): Document fresh Pi supervision conversations

* no-mistakes(ci): Fixed the flaky concurrent inbox failure. Lock acquisition now retries when a competing lock disappears between a failed claim and inspection. Added a behavioral regression covering that race. Verified the full inbox test four times, project lint, and git diff checks

* feat: restart second mates after instruction updates (kunchenguid#3614)

* feat(update): restart second mates whose instructions changed

/updatefirstmate pulled new bytes onto disk and then asked each advanced
second mate to re-read them. A running agent holds AGENTS.md and every
loaded skill frozen from launch and no verified harness offers a reload,
so that steer could not reach a loaded skill at all and left the mate
holding two contradictory copies of its own job description.

An eligible mate is now restarted instead, in the same home and endpoint,
through the existing transactional relaunch. The restart is gated on the
mate first writing down the open work it holds only in conversation - the
open-record half of /stow, never its memory sweeps - so an unregistered
captain call is flushed before the conversation is spent. Anything that
leaves the reload unprovable falls back to the old re-read message and is
reported as exactly that, never as a clean reload.

Remote mates take the same path: fm-remote-secondmate-control.sh gains a
relaunch verb whose host-local leg runs that same control plane, since the
mate is an ordinary local secondmate from its host's point of view. The
primary resolves the profile and passes it explicitly, because
config/secondmate-harness is not inherited and the file on that host
belongs to a different home.

fm-update.sh now splits its advanced live mates into a restart set and a
nudge residual, and both sets require a changed instruction surface, which
also closes the over-nudge against the session-start sweep. Restart is
stricter still: a bin/-only advance reloads itself on the next call, so it
never costs a conversation.

Colocated tests cover the gating, the persist-then-restart order, the
task-subset persist request, each unsafe fallback, the remote hop, and the
remote sync's new instruction-surface report.

* no-mistakes(review): Fix restart correlation, concurrent waits, and lifecycle reporting

* no-mistakes(review): Parallelize relaunches and classify replacement incarnations

* no-mistakes(review): Gate restart actions on live agent state

* no-mistakes(review): Handle failed restart workers without hanging

* no-mistakes(review): Nudge legacy remotes and preserve persist recovery

* no-mistakes(review): Document one-time secondmate restart rollout

* no-mistakes(review): Honor arrived replies and refresh remote profiles

* no-mistakes(review): Revert remote parent profile reconciliation

* no-mistakes(review): Reset remote profile defaults and honor published results

* no-mistakes(review): Preserve fallback nudges for unverifiable secondmates

* no-mistakes(document): Document second-mate restart update flow

* no-mistakes(lint): Fix ShellCheck warnings in restart scripts

* perf: accelerate local validation with bounded concurrency (kunchenguid#3644)

* perf(tests): route gate verification through the bounded concurrent runner

Local validation was the pipeline's dominant cost: across 67 recorded
no-mistakes agent sessions on this repo, 99.3% of command execution was
`bash tests/*.test.sh`, run strictly one script at a time, and 2% of those
calls were killed by an agent-guessed timeout and paid for twice.

Three changes, each measured:

- `.no-mistakes.yaml` pins `commands.test` to
  `bin/fm-test-run.sh --changed --exclude-family real-herdr-gated`. The runner
  already owns changed-file selection, bounded concurrency, the refusal of
  unproven scripts, and a generous automatic per-script bound, so the gate's
  baseline is neither a serial chain nor a guessed timeout. It stays
  intent-targeted - the Test step still runs its evidence agent on top - and
  excludes the live-Herdr family the required Herdr lane owns.

- `bin/fm-test-run.sh` gives a plain list of script paths the same bounded
  automatic scheduler and automatic bound that `--changed` gets. Naming several
  subjects is how a verification round asks for exactly those scripts. The
  curated selections are untouched: `--lane` still composes CI shards whose
  serial lane must stay serial, `--family` is what the required Herdr lane runs,
  and `--all` stays a deliberate complete regression.

- `pr-forge` is admitted to the concurrent-safe family registry on two
  consecutive clean proofs. `docs/fm-test-isolation-proof.md` records those,
  and records `secondmate` and `session-bootstrap` as refused with the exact
  script and reason each failed on, so the refusals are actionable rather than
  silent.

Measured on this host, 0 failures on both sides:

  verification round, 4 scripts   448s chained -> 231s through the runner (-48%)
  pr-forge family                 409.2s at 1 worker -> 237.9s at 4 (1.72x)
  watcher-wake-lock family        1311.1s at 1 worker -> 539.3s at 4 (2.43x)

A fourth lever was implemented and then removed because the measurement
refused it: raising the bounded-wait sample interval from 0.1s to 0.5s made
`fm-watch-triage.test.sh` slower, 435s and 440s against 390s and 393s
unchanged, back to back. Those sleeps are not overhead added to the clock -
they are how a test waits for a subject moving on fm-watch.sh's own one-second
cadence - so sampling less often only delays detection. It also broke
`fm-watcher-lock.test.sh`, which catches a transient rather than waiting for a
settled condition. CONTRIBUTING.md records that result so the experiment is not
repeated.

* no-mistakes(review): Separate concurrent runs by isolation proof family

* no-mistakes(review): Limit automatic timeouts to changed-file validation

* no-mistakes(document): Clarify validation concurrency documentation

* fix: copy PR URLs from durable records (kunchenguid#3648)

* fix: copy PR URLs from records or abstain, never assemble them

Supervision reported a plausible but dead PR link three times because its
prompt demanded a full https:// URL at a moment when only a PR number was
observable, so the model assembled an owner/repository from memory, and the PR
check then accepted that URL and wrote it into the task record, after which the
model kept defending its own tool-endorsed guess over the worker's real link.

Three changes close that chain without any live forge lookup, so private
forges are treated exactly like public ones:

- bin/fm-branch-prompt.sh no longer mandates a URL. Its new "PR identity: copy
  or abstain" section requires a URL to be copied verbatim from a durable
  record (the done: PR <url> status line, pr= metadata, or the backlog note),
  forbids assembling owner, repository, host, or number from memory, and has
  the branch report only the identifier it actually holds when no record names
  the URL yet, leaving the PR check unarmed until the worker's ready line
  arrives. AGENTS.md section 7 and 9 carry the same copy-or-abstain rule for
  main in place of the bare full-URL mandate.

- Worker briefs (bin/fm-brief.sh, ship and scout rules) require the full
  https:// URL wherever a PR is mentioned - status line, terminal, or summary -
  never a bare "PR 108", so the link is in view as early as the number is.

- bin/fm-pr-check.sh refuses, offline and before any side effect, a URL that
  the task's own done lines contradict, printing both spellings; a log naming
  no URL still records the argument as before. fm_pr_status_ready_urls in
  bin/fm-pr-lib.sh owns reading those lines. The refusal also reaches
  bin/fm-pr-merge.sh, so nothing merges under a contradicted URL.

Tests cover the offline refusal with zero side effects, the recorded spelling
being accepted, markdown-wrapped and punctuated URLs, working lines not
counting, the merge wrapper propagation, a self-hosted merge request with no
forge call, the prompt carrying the rule, and the brief carrying the worker
rule.

* no-mistakes(review): Remove stale PR URL enforcement

* no-mistakes(ci): Removed backlog notes as an accepted PR identity source. PR URLs may now be copied only from the task’s `done: PR <url>` status or canonical `pr=` metadata; otherwise supervision reports only the known identifier and leaves PR checking unarmed. Updated related guidance/docs and verified with branch-supervision tests, brief tests, ShellCheck, and `git diff --check`

* fix(bin): disable Claude feedback drafts for fleet launches (kunchenguid#3661)

* fix(bin): disable Claude's feedback-draft flow for fleet-launched agents

Scope --settings '{"feedbackDrafts":"off"}' to every Firstmate-launched
Claude crewmate and secondmate, so /bug and /feedback never queue or
submit a bug report on the captain's behalf. feedbackDrafts is the
documented settings key (Claude Code changelog 2.1.247); the
per-launch CLI flag never touches the captain's global settings.json.

Claude-Session: https://claude.ai/code/session_01XYAXXzr4oZx9NjZb1veeE3

* no-mistakes(review): Prevent managed settings from re-enabling Claude feedback drafts

* no-mistakes(document): Fix Claude feedback documentation formatting

* fix(bin): layer both feedback-draft controls for defense in depth

The prior --settings-only fix can be overridden by a managed Claude
settings policy (feedbackDrafts precedence). Keep CLAUDE_CODE_SEND_FEEDBACK=0
alongside --settings '{"feedbackDrafts":"off"}': either control alone
disables the SendFeedback tool, so a managed override of one still
leaves the other in force.

Claude-Session: https://claude.ai/code/session_01XYAXXzr4oZx9NjZb1veeE3

* no-mistakes(document): Document Claude feedback-draft suppression ownership

* feat(tests): run three more validation families concurrently (kunchenguid#3662)

* perf(tests): admit three more families to concurrent validation

The three families that `docs/fm-test-isolation-proof.md` recorded as refused
were not refused for concurrency. Each blocker was a test that decided a
property by wall clock, or a script filed where it cannot run. Fixing those
three things admits all three families and recovers 28.6 minutes of local
validation with no assertion removed or weakened.

- `tests/fm-backlog-handoff.test.sh` injected its pre-move crash by killing the
  handoff, sleeping a fixed second, then delegating the move to the real
  binary. Nothing ever killed the fake, so on a host slow enough for the case's
  next assertions to take longer than a second, the orphan woke and completed
  the very move the case requires left undone, and recovery then failed with
  `Task "pre-move-crash" not found in this backlog`. Watching the two backlogs
  during the injected crash showed exactly that, the item moving one second
  after the crash. All four crash injections in the file now go through a new
  `fm_fake_crash_injector` shim that signals the target and returns only once
  it is observably gone, and the pre-move fake never delegates the move at all.

- `tests/fm-session-start.test.sh` proved the startup digest does not block on
  a slow current-state read by timing the whole digest against a fixed
  eight-second sleep, which a loaded host exceeds without the property being
  violated. It now holds that read open until the case releases it and asserts,
  the moment the digest returns, that the read has not finished. A digest that
  waited would wait indefinitely rather than for an interval a slow host can
  out-run, so the assertion is stronger than the bound it replaces. Its scan
  budget moves to the maximum, because the old value left two seconds of margin
  over the fixed sleep and measured the host rather than the deadline that
  `tests/fm-inactive-reconcile.test.sh` owns.

- `fm-backend-herdr-focus-flash-e2e` was filed in the family map's catch-all,
  which put it in the portable serial lane, where Linux CI gate-skips it: that
  real-Herdr regression was running nowhere. It moves to `real-herdr-gated` and
  the required Herdr lane. `fm-claude-stop-autoarm-live-e2e` gate-skips on its
  opt-in variable and moves to `live-harness-optin`.

The 28 remaining ungrouped scripts become an enumerated `standalone` family
instead of admitting `unclassified` itself. `unclassified` is the family map's
`*)` arm, so admitting it would silently grant concurrency to every test added
afterwards, which is exactly the population with no proof. A new test still
lands in `unclassified` and stays serial, and `tests/fm-test-run.test.sh`
covers that split behaviorally.

Each family passes two consecutive four-worker proofs with zero failures. On
the production runner, `secondmate` goes 1233.1s to 453.4s, `session-bootstrap`
756.4s to 286.4s, and `standalone` 724.6s to 261.1s: 2.71x overall and 1713.2s
recovered. The whole suite runs 177 scripts in 52.6 minutes of wall clock
against 121 minutes of summed script time.

* no-mistakes(document): Refresh concurrent validation and shard documentation

* no-mistakes(ci): Fixed the real-Herdr focus-flash E2E race exposed by reclassification. Part C now starts its persistent child atomically via `pane run` and verifies stable child identity through Herdr’s public `process-info` interface, avoiding the racy send-text/send-keys sequence and platform-specific `ps` matching. Verified with bash syntax checking, ShellCheck, git diff checks, and the complete E2E test on Herdr 0.8.2

* feat: structure no-mistakes ask-user escalations (kunchenguid#3670)

* feat(brief): structure no-mistakes ask-user escalation as event + snapshot file

Crewmates escalating a no-mistakes ask-user gate now report one status
event naming every finding id plus a snapshot file holding the gate's
axi finding records verbatim (id, severity, file, line, description,
authority), using the same shape even for a single finding. The status
line never paraphrases. The format is defined once in fm-dod-lib.sh and
rendered into both the scout and ship rule 6 in fm-brief.sh, so a
promoted scout - whose rule 6 fm-promote.sh preserves unchanged - gets
the identical contract as a freshly-spawned no-mistakes ship worker.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PpiWaDerbYavTLPPtEjQei

* no-mistakes(review): Preserve ask-user escalation output contract

* no-mistakes(review): Align escalation format test expectation

* no-mistakes(review): Scope ask-user escalation instructions correctly

* no-mistakes(review): Remove ask-user from generic decision rules

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* fix(bin): require self-sufficient no-mistakes intent (kunchenguid#3671)

* fix(bin): require a self-sufficient no-mistakes intent

A no-mistakes worker's --intent is only as useful as the string it
passes. PR kunchenguid#3604 shipped with an intent that was only "do 1, 2, 3, 7
from the report": the real contract lived in a private scout report and
never reached --intent, so nobody holding that string plus the codebase
could have derived the specification.

This is pure instruction at the contract's one owner; no spawn-side or
promotion-side check is added.

- bin/fm-dod-lib.sh: the generated no-mistakes Definition of done now
  states that the --intent string must be self-sufficient (the string
  plus the codebase reconstructs roughly the same specification) and
  tells the worker to write the substance of any report, decision, or
  PR the captain's intent refers to into --intent rather than the
  pointer, while Firstmate build instructions and the worker's own
  decisions still stay out. The spawn-time overlay points back at that
  rule so its "supersedes" wording cannot cancel it, and the header's
  owner statement carries the rule.
- AGENTS.md section 11 and bin/fm-brief.sh's header ask Firstmate to
  include the substance of referenced material when filling
  ## Captain's intent, and section 11 points at the owner of the rule.
- tests/fm-brief.test.sh and tests/fm-task-delivery.test.sh assert the
  rendered brief and launch contract carry the rule.

Claude-Session: https://claude.ai/code/session_01YMhEe42q7BAAoN6RxNuzim

* no-mistakes(document): Replace incident-specific intent test commentary

* fix: accelerate local Bearings snapshot composition (kunchenguid#3499)

* Speed local fleet snapshot composition

* no-mistakes(review): Stabilize task inventory during concurrent snapshot composition

* no-mistakes(document): Document local snapshot observation concurrency

* no-mistakes(ci): Fixed CI failures by making empty task manifests compatible with stock macOS Bash 3.2, snapshotting task metadata before concurrent observations to prevent generation drift, strengthening the behavioral race regression, and updating the stock-Bash Bearings test count to 45. Verified fleet snapshot tests (15), Bearings tests (45), workflow lint tests, project lint, Bash 3.2 parsing, and diff checks

* no-mistakes(ci): Fixed the Linux CI failure caused by passing large backlog/task JSON through jq command-line arguments, which exceeded the per-argument size limit. Both inventory projections now stream large JSON inputs through stdin. Verified with fm-bearings-snapshot.test.sh (45 tests), fm-fleet-snapshot-view.test.sh (15 tests), Bash syntax, and git diff checks

* no-mistakes(ci): Fixed concurrent task teardown during metadata capture: vanished metadata is now omitted while genuine copy failures remain fatal. Added a deterministic public Bearings regression test and updated CI’s expected test count. Verified with the full Bearings suite, workflow-lint suite, Bash syntax checks, and git diff checks

* no-mistakes(ci): Fixed PR-caused CI and review issues: streamed large fleet JSON through jq stdin to avoid Linux argument limits, kept crew-state reads bound to captured metadata generations, and strengthened the behavioral race test. Bearings (46 tests), fleet snapshot (15 tests), crew-state, backend, lint, Bash syntax, and diff checks pass locally. Serial shard 5’s unrelated task-inbox segmentation fault appears infrastructural/flaky

* no-mistakes(ci): Fixed endpoint-state generation crossing by validating captured spawn_gen before and after local endpoint probes, falling back to exact metadata identity for legacy tasks. Stale probe results now become unknown instead of false unhealthy state. Added a behavioral relaunch-race regression test. Verified the full Bearings snapshot suite, shellcheck, bash syntax, and git diff checks

* fix(snapshot): keep live observations generation-coherent

* no-mistakes(review): Keep secondmate observations generation-bound without copying reports

* no-mistakes(document): Document generation-coherent snapshot observations

* test(bearings): measure local read overlap instead of wall-clock budget

The large-local-snapshot regression asserted that a whole snapshot
composed in under five seconds. That bound measures how loaded the host
is, not whether the per-task reads actually overlap, so it failed
intermittently on a contended machine: one run in six on a box at load
16-20, landing exactly on the five second boundary.

Time a serialized run and a concurrent run of the same workload instead
and require the concurrent one to save at least two seconds. Both runs
pay the same composition overhead, so the difference isolates the
overlap this change delivers. Five one-second reads serialize into five
seconds and overlap into about one, and re-serializing the reads
collapses the saving to roughly zero, so the assertion still fails
loudly if the concurrency regresses.

Also bump the pinned Bearings test count to 48, since rebasing onto the
current default branch picked up its captain-hold test.

* no-mistakes(review): Restore JSON-derived decision flags

* no-mistakes(review): Unify status-derived snapshot observations

* no-mistakes(ci): Updated the stock macOS Bash CI check’s Bearings test count from 48 to 49. Verified the full Bearings suite passes and emits exactly 49 TAP successes; git diff checks pass

* fix: prevent stale supervision wake loops (kunchenguid#3672)

* fix(bin): stop the supervision branch's stale-ack and ghost-report loops

Clean-slate implementation of the four authorized recommendations from the
supervision-ghost-retrigger analysis (items 1, 2, 3, and 7), in their minimal
form, superseding PR kunchenguid#3604:

- fm_branch_report refuses a task the wake being handled never named. The
  extension fixes the reportable task set from the eligible rows before each
  prompt (signal and stale rows resolve to their tasks, a heartbeat allows any
  task with a live record, fleet is always allowed), so a report typed from
  memory about a task whose records teardown already removed is never stored
  or delivered.
- An acknowledgement that consumes nothing says "nothing was acknowledged
  through N" and prints the exact --ack-through / --recovery-generation
  command for the current presented wake, instead of "re-run the drain",
  which re-fed the same stale acknowledgement in a loop.
- bin/fm-guard.sh no longer tells the branch actor to drain queued wakes
  while it is handling them; it names the granted rows instead.
- Teardown removes state/.<task>.branch-outcome-index for ordinary tasks and
  descendants; the index rebuild and the append-side index write both skip a
  task with neither a live record nor a status log, so the branch's report of
  a teardown it just performed is stored without recreating the index.

No new locking, no spawn-generation binding, and no retired-task refusal: the
branch can still report the outcome of a task it just tore down, and the
teardown test now proves that path end to end.

* fix(bin): narrow the branch report scope and guard silence to the minimal form

Apply the four review decisions on the clean-slate branch:

- A signal or stale prompt may report only the tasks its own rows resolve
  to; fleet is refused there too. A heartbeat review is not scoped by task
  at all, so the extension no longer tracks live task records and refuses
  nothing by task id during a fleet review.
- The outcome-index rebuild no longer skips retired tasks; the append-side
  skip alone keeps a torn-down task's index from being recreated.
- bin/fm-guard.sh keeps the queued-wakes warning silent for the branch actor
  instead of printing a replacement note.

* no-mistakes(document): Align supervision docs with scoped wake handling

* fix(bin): avoid fleet snapshot argument limits (kunchenguid#3677)

* Fix fleet snapshot large JSON transport

* no-mistakes(review): Captain: file-back fleet snapshot transport safely

* no-mistakes(review): Captain: file-back parent summary aggregation

* no-mistakes(ci): Rebased the PR's three commits onto f4d7875 and resolved the fleet snapshot conflict while preserving the base's task-observation lifecycle. Fixed Greptile's valid finding by recursively removing the private mktemp transport directory, so future transport files cannot cause cleanup to fail. Verified with tests/fm-home-summary-refresh.test.sh, bin/fm-lint.sh, git diff --check, and ancestry checks. All passed; the fix remains as an uncommitted worktree change for the outer executor

* fix(bin): attribute active runs with unfetched pipeline heads (kunchenguid#3681)

* fix(bin): recognize active pipeline fix rounds with unfetched run heads

A no-mistakes fix round advances the run head beyond the submitted head,
and the pipeline commits in its own checkout, so the task copy never
receives the new commit object. fm-crew-state's strict head rule rejected
the active row, the coarse runs-list scan skipped it and matched the
older failed row at the submitted head, and an active validation read as
failed (observed on model-routing-benchmark-hardening: active head
ac61c64 vs task copy at fb47636d).

fm_nm_runs_status_for_worktree in bin/fm-nm-run-lib.sh now owns
runs-ledger attribution: the branch's newest row alone decides, and a
newest row whose head cannot resolve locally is recognized only as a
provable pipeline-owned continuation - active (running) and anchored by
the immediately older row for the same branch having ended at exactly
this worktree's HEAD. The reader keeps the axi TOON as full detail for
that proven same-branch run. Unanchored, ancestor-anchored, and terminal
unresolvable rows stay unattributed, so branch-name coincidence and other
tasks' runs never match, and fm_nm_head_matches_worktree keeps its exact
prior semantics for teardown (verified by the full teardown suite).

Tests: reproduction regression for the unfetched active fix head (reads
working via full run-step detail), coarse-path continuation when axi
answers another branch, and negative controls for the unanchored active
row and the unresolvable terminal row with the historical fallback
preserved.

Ported onto upstream/main f4d7875, where kunchenguid#3194 independently added the
branch_sync custody exemption on the full axi-status path: both mechanisms
now coexist, each owning one surface (TOON custody on the full path, the
runs ledger on the coarse path). The port deletes the superseded coarse
scan-and-skip (nm_runs_status_for_branch) and its now caller-less helpers
(fm_nm_head_resolvable, nm_coarse_head_matches_worktree), renames the
exemption comment's "the one exemption" phrasing now that a second
complementary exemption exists, and points the stale
FM_CREW_STATE_RUNS_LIMIT comment at fm_nm_runs_status_for_worktree
(judge follow-up #1). The parent coarse-guard test's fixture is the
ledger-anchored continuation shape, so its expectation flips to the fixed
behavior (working via run-step, never the older failed row); a new
mismatched-anchor coarse negative control preserves that guard's original
no-anchor protection (pane answers, never the older row).

* no-mistakes(document): Clarify pipeline attribution documentation

* fix(bin): pre-register claude workspace trust at spawn time (kunchenguid#3663)

* fix(bin): pre-register claude workspace trust for task worktrees

A claude crewmate launched into a fresh task worktree met Claude Code's
interactive workspace-trust dialog before it ever read its brief, and firstmate
could not answer it: the key plane carries only Enter, Escape, and C-c with no
arrow navigation, and the dialog's selection starts on "No, exit", so the
documented Enter recipe ended the session instead of accepting it. Two workers
wedged this way and were unblocked only by hand-seeding the trust store per
path.

--dangerously-skip-permissions does not cover that gate. `claude --help`
records the dialog as skipped only in non-interactive mode, through -p or a
non-TTY stdout, and a crewmate pane is interactive, so there is no launch flag
to reach for.

fm-spawn now pre-registers the worktree through bin/fm-claude-trust.sh in the
existing claude branch, before the project settings that the same gate would
otherwise block, and refuses the spawn when that write fails rather than
launching a worker that would wedge.

The scope test is the safety property and is structural rather than a path
policy: the path must be a linked git worktree, sharing the spawning project's
common dir, whose top level is exactly the resolved argument. Git is the ground
truth, so the argument is never trusted on its own word, and a primary
checkout, an unrelated repo, a worktree subdirectory, a plain directory, and a
home directory are each refused rather than warned about or skipped. A
treehouse or orca path prefix was deliberately avoided because treehouse's root
is configurable, which would make a prefix both wrong and a new policy surface.
One structural test covers both worktree providers.

tests/fm-claude-trust.test.sh pins both halves, including a case where HOME is
itself a valid linked worktree so the home guard is proven load-bearing rather
than passing vacuously, plus the spawn-level proof that a claude spawn trusts
its worktree and launches with the brief pointed at the same store.

The adapter reference no longer tells a firstmate to press Enter on that
dialog, and the shared trust reference now names every harness surface: which
harnesses gate, which suppress at launch, which dodge the gate, which now
pre-registers, and that a claude secondmate is excluded by design.

The spawn fixture runs each spawn against a throwaway HOME so the suite cannot
write the developer's real store, isolating through HOME rather than
CLAUDE_CONFIG_DIR because the spawn forwards a set CLAUDE_CONFIG_DIR onto the
launch command that launch-shape assertions read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HNEN2GLnew27HFyfi4ms4v

* fix(bin): create the staged trust store exclusively

The staged store was written to a predictable pid-based path with a plain
write, which follows a symlink. Where the Claude config directory is writable
by another local account, that account could pre-create the path as a symlink
and redirect the write into another file the launching user owns.

The staged name now carries random bytes and is created with an exclusive
"wx" open, so an existing path is refused outright instead of followed. The
happy-path test also asserts no staged store survives the rename.

The durability comment now states the residual window plainly: the readback
proves the entry landed, not that it survives, because a vendor session that
rewrites the whole store afterwards can still drop it and no lock closes that
window when the writer is Claude itself. The worker then meets the dialog and
stalls, which reaches firstmate as the ordinary stale wake rather than as
silent success.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HNEN2GLnew27HFyfi4ms4v

* no-mistakes(review): neutralise CDPATH in claude trust scope guard

* no-mistakes(review): sandbox HOME in spawn tests, drop out-of-scope artifacts

* no-mistakes(review): refuse unresolvable git dir, compact store, fix secondmate doc

* no-mistakes(review): clear git env overrides, resolve symlinked store target

* no-mistakes(review): degrade without node, fix Pi gate claim, record trust proof

* no-mistakes(review): refuse without node, pin CLAUDE_CONFIG_DIR in spawn tests

* no-mistakes(review): refuse relative config dir and concurrent store modification

* no-mistakes(review): correct orca worktree claim, clean staged store on failure

* no-mistakes(review): restore pretty-printed store, correct trust dialog docs

* no-mistakes(review): arm trust gate before busy state to avoid orphans

* no-mistakes(document): record claude trust pre-registration in its owner docs

* no-mistakes(document): note orca limit for claude trust pre-registration

* no-mistakes(ci): Fixed the Greptile P1 on bin/fm-spawn.sh by moving the Claude trust gate earlier rather than adding cleanup machinery. Diagnosis: Greptile reported that when Claude trust registration fails on tmux/Zellij/cmux/non-projected Herdr, the exit runs after the backend endpoint and /tmp/fm-<id> were created, and the abort trap cleans neither. The endpoint half is pre-existing, deliberate architecture — the two refusals immediately above the gate (the 60s `treehouse get` timeout at fm-spawn.sh:2550 and `validate_spawn_worktree` at :2487) also exit with the endpoint live and direct the operator with "inspect window $T"; spawn_abort_cleanup only reclaims orca endpoints (already covered via ORCA_ABORT_CLEANUP) and herdr projections. The temp-root half was genuinely introduced by this PR: the gate was placed beside the busy-state arm, ~30 lines after `mkdir -p "$TASK_TMP/gotmp"`, and fm-teardown can only find that root through `tasktmp=` in a meta record a refused spawn never publishes. Root-cause fix (smallest correct change, no new subsystem): - bin/fm-spawn.sh — moved the `claude*` trust gate from inside the busy-arm block up to the first point $WT is known, immediately after the `freshen_spawn_worktree_base` block and before TASK_TMP creation, the STATE setup, and the relaunch `clear_relaunch_harness_wiring` retirement. A refusal now leaves no temp root, no retired relaunch wiring, and no busy record; only the endpoint remains, in the same class as the two refusals just above it. - bin/fm-spawn.sh — the refusal message now ends with "inspect window $T", matching the existing convention so control/teardown can identify the endpoint. $T is set for every backend on the non-secondmate path. - bin/fm-spawn.sh:196 — header note corrected from "before any state is armed" to "before any per-task state exists". - tests/fm-claude-trust.test.sh — the existing refused-spawn test's own comment claimed "before any task state exists" but only asserted busy state. Renamed to test_refused_spawn_leaves_no_task_state and added an assertion that /tmp/fm-<id> is absent, with the task id suffixed by the test process pid so the assertion reads only this run's path (a stale /tmp/fm-refusedspawn from the fixed-id version was in fact present on this box). No assertions on implementation source bytes. Verification run locally: - The new assertion fails against the pre-fix bin/fm-spawn.sh ("not ok - a refused spawn stranded a temp root no teardown can find") and passes after — a real before/after regression proof. - tests/fm-claude-trust.test.sh: 20/20 ok. - tests/fm-backend.test.sh, fm-backend-orca, fm-control-relaunch, fm-spawn-dispatch-profile, fm-trace-context-spawn, fm-gotmp: all pass. - tests/fm-backlog-atomicity.test.sh: rc=0, 79 assertions ok. - bin/fm-lint.sh (repo's single lint owner, pinned ShellCheck 0.11.0 + actionlint 1.7.12): clean. - No /tmp/fm-refusedspawn* leftovers after the runs. Scope respected: no trust subsystem, no policy layer, no config surface, no endpoint-cleanup mechanism added; the change is an ordering move plus one error-message clause and the test that pins it. Adapter references and docs made no ordering claim, so none needed updating. Changes are left uncommitted in the worktree for the outer executor

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix: restart every live second mate after updates (kunchenguid#3690)

* feat(update): restart every live second mate after a successful update

/updatefirstmate only restarted a second mate when that pass advanced its
AGENTS.md or .agents/skills. An already-current home was skipped entirely, a
bin/-only advance was steered instead, and a remote host that could not report
its instruction diff was downgraded to a re-read. A running agent also freezes
its launch-time wiring - turn-end hooks, harness flags, per-harness feature
switches - and none of that is derivable from a file diff, so an unchanged
tracked surface is not evidence the agent is already on the current behavior.

Restart is now unconditional on a successful update of that home. Every live
second mate the pass leaves on the target commit is restarted, whether it
advanced or was already there.

The safety contract is unchanged: open records are persisted before the agent is
replaced, nothing is forced, stashed, or discarded, a home the pass had to skip
is not restarted at all, and a mate whose runtime cannot prove a restart keeps
the honest re-read path and is never reported as reloaded.

bin/fm-ff-lib.sh gains a settled-state hook that fires for a home left at the
base whether it advanced or was already there, and never for a skipped one; the
instruction-gated hook the session-start convergence sweep uses is untouched.

Regressions: fm-update pins the already-current mate into the restart set and
the unprovable one into the nudge set, and fm-secondmate-restart drives both
real commands end to end - an already-current home is named, persisted, and
genuinely replaced with its checkout untouched, while the unprovable one keeps
its running agent.

* no-mistakes(document): Document unconditional secondmate restarts

* fix(bin): close pending-reply decisions via resolve-key (kunchenguid#3696)

* fix(bin): close reserved pending-reply keys via fm-send --resolve-key

fm-send wrote answered: notes that the reserved-key fold ignores, so
operator closes exited 0 while OPEN DECISIONS kept the decision open.
Speak the owning library's close vocabulary on that path, and refuse
when a reserved close cannot take effect.

* no-mistakes(review): Safely quote manual decision-close recovery commands

* no-mistakes(review): Reject unclosable overlong decision keys before sending

* no-mistakes(review): Remove contract suffix from open decisions hint

* no-mistakes(document): Document resolve-key line-cap refusal

* fix(bin): prevent false missed-reply escalations (kunchenguid#3697)

* fix(bin): stop false missed-reply escalations for same-basename self-home answers

A healthy secondmate that wrote corr= to its own state/<id>.status never matched the parent channel, so recovery confirmed and the record escalated as pending-reply-missed. Make the report helper resolve the parent channel itself, skip parent-replies.status as wrong-home, put a readable sighting path on the missed line, and restatement-copy only that same-basename self-home file onto the parent channel.

* no-mistakes(review): Resolve late replies before recovery escalation

* no-mistakes(review): Tighten reply routing and regression coverage

* no-mistakes(review): Preserve reply paths and require explicit home

* no-mistakes(review): Encode wrong-home paths before persistence

* no-mistakes(document): Document corrected secondmate reply routing

* no-mistakes(lint): Fix pending-reply ShellCheck warnings

* feat: add verified Gemini crewmate runtime (kunchenguid#3695)

* feat(harness): verify gemini as a crewmate runtime adapter

Adds Gemini CLI as a fourth dispatch target alongside claude, codex, and
grok, scoped to crewmate and scout work only. Every axis was proven against
gemini-cli 0.58.0 rather than inferred; docs/verification/runtime-backends.md
carries the dated evidence and names what stayed unverified.

Busy state is semantic, not rendered: BeforeAgent opens a turn and AfterAgent
and SessionEnd close it. AfterAgent also fires on a manual interrupt, so a
cancelled turn closes its own record.

Three findings shaped the wiring rather than a config line:

- --skip-trust and GEMINI_CLI_TRUST_WORKSPACE=true are presented by the CLI
  as equivalents and are not. A controlled A/B showed --skip-trust leaves
  project configuration unloaded, so workspace skills never load.
- The worktree's .gemini/settings.json is the PROJECT's committed settings
  file, unlike claude's settings.local.json. Firstmate's hooks therefore go
  to a firstmate-owned state/<id>.gemini-settings.json reached through
  GEMINI_CLI_SYSTEM_SETTINGS_PATH, which also works untrusted and merges
  with a project's own hooks instead of replacing them.
- The shipped CLI is a node bundle whose live process reports comm as
  MainThread, so ancestry cannot see it. GEMINI_CLI=1 is load-bearing and is
  tested before an inherited CLAUDECODE, and pane liveness identifies gemini
  from the script argument through the new bin/fm-gemini-lib.sh.

Gemini is refused for secondmates: it has no primary supervision protocol.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYdQkKfSEQrFUxtcTXZ66L

* test: clear gemini's marker in launch and detection expectations

Every non-gemini launch now clears GEMINI_CLI the way it already clears
cursor's markers, so the two tests that pin the exact launch prefix are
updated to match. The harness-detection tests that scrub foreign markers
before probing ancestry scrub GEMINI_CLI too, so running the suite from
inside a gemini session cannot produce a false verdict.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYdQkKfSEQrFUxtcTXZ66L

* docs: classify the gemini harness reference

The documentation inventory is the single classification owner for maintained
prose surfaces, and every surface must appear in it exactly once. The new
harness reference is agent-runtime, matching its siblings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYdQkKfSEQrFUxtcTXZ66L

* no-mistakes(review): Narrow Gemini ancestry detection

* no-mistakes(review): Restrict Gemini hooks to canonical launches

* no-mistakes(document): Document Gemini adapter support boundaries

* no-mistakes(ci): Fixed Gemini process identity when interpreter or script paths contain whitespace. Tmux liveness now uses NUL-delimited /proc argv on Linux, with the existing flattened ps fallback elsewhere. Added a real-process regression test. Verified with the Gemini harness test suite, full fm-lint, ShellCheck, and git diff --check. The CI and Require no-mistakes runs were action_required/attestation outcomes rather than code failures

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(teardown): conclude parked runs advanced past task copy (kunchenguid#3704)

* conclude parked runs the pipeline advanced past the task copy

A no-mistakes fix round commits in the daemon's own gate-repo clone, so a
run parked at a gate can carry a head whose object the task copy never
received. Teardown's strict object-local identity rule then declined to
conclude the run, and cleanup left it parked forever holding a fleet slot
(observed 2026-09-03; the same masking condition PR 3681 fixed on the
read path, now closing the teardown half its scope boundary deferred).

task_status_is_own_parked_run now falls back - only when the reported
head resolves to no local object - to the one shared runs-ledger
attribution rule fm_nm_runs_status_for_worktree (bin/fm-nm-run-lib.sh),
whose anchored continuation proof binds the branch's newest active row
to this worktree's exact submitted head. Foreign branches, stale
history, terminal rows, ancestor-only anchors, diverged newer rows, and
ambiguous multi-row shapes all still refuse, and runs that are actively
running, fixing, or in CI remain untouched: only the parked-at-a-gate
determination ever reaches the abort. No sqlite access, no fetches into
another task copy, no custody changes, no duplicated matching logic.

* tighten the parked-run ledger fallback and pin both judge corrections

The teardown ledger fallback now authorizes concluding this task's parked
run only when the shared runs-ledger rule's proved answer is the explicitly
active word (running): a terminal newest row - even anchored at exactly the
worktree's head - is finished history and never an abort authorization.
The read path may classify the same owner's answer; teardown's abort must
never fire for a run that already ended.

Two bounded pre-validation corrections from the implementation review:
- a fetched-object counterfactual pins the strict-rule path: a pipeline fix
  head fetched into the task copy aborts through object-local identity
  alone, with an empty ledger and a proof the runs query never fired;
- a negative fixture pins the tightened boundary: an unresolvable reported
  head with a terminal newest same-branch row anchored at the worktree head
  engages the ledger fallback and still refuses, so the refusal is the
  terminal-word boundary and not an earlier guard.

* no-mistakes(review): Bind teardown ledger fallback to validated run heads

* no-mistakes(review): Restore validated advanced-head ledger continuation

* no-mistakes(review): Reject invalid ledger dates and terminal statuses

* no-mistakes(document): Document teardown ledger scan limit

* feat(bin): show requested vs effective model in Herdr agent view

Track spawn-config requested_model separately from runtime-verified
effective_model, probe Claude/Pi transcripts for exact API ids, push
compact display metadata to Herdr, and preserve verified models across
relaunch/compaction hooks without inferring aliases as truth.

* fix(bin): keep re-probing effective model after first exact reading

fm-model-sync.sh only probed for the runtime-verified effective model
while it was still pending/UNKNOWN, so a session that later switched
models (manual switch, provider fallback) kept displaying the first
verified model forever and never appended a fallback-history entry.
Probe unconditionally instead; fm_model_record_effective already
no-ops when the probed value is unchanged, so this stays cheap.

Addresses the Greptile P1 finding on PR kunchenguid#3705's fm-model-sync.sh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwfFjeYQcz9cZ3vEZohmpm

* fix(bin): distinguish Cursor Grok, direct xAI Grok, and Anthropic Claude in the display

Kapitänskorrektur: harness alone conflated Cursor-hosted Grok models
(cursor-grok-4.6-*) and direct xAI Grok models (xai/grok-4.6) under
one generic label, and displayed Anthropic Claude without naming the
provider. Add fm_model_source_label, pattern-matched on the verified
exact model id, so the compact display always reads Cursor · Grok,
xAI · Grok, or Anthropic · Claude with the exact model id appended.
Falls back to the existing harness label for every other model. No
routing change: this only affects display strings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwfFjeYQcz9cZ3vEZohmpm

* fix(bin): wire model-sync into the Pi extension's turn lifecycle

fm-model-sync.sh was only invoked from Claude's SessionStart/
UserPromptSubmit/Stop hooks; the Pi harness's own extension
(state/<id>.pi-ext.ts) never called it, so a Pi-hosted session (e.g.
a pi/xai-grok crewmate) never refreshed its effective model after the
first probe and Herdr kept showing the stale value with no
fallback-history entry. Call fm-model-sync.sh from the same
agent_start/turn_end boundaries Pi already uses for busy-state and the
turn-end notification touch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwfFjeYQcz9cZ3vEZohmpm

* fix(bin): serialize fm-model-sync.sh's meta read-probe-write

Overlapping lifecycle events (Pi's agent_start/turn_end, Claude's
SessionStart/UserPromptSubmit/Stop) can invoke fm-model-sync.sh
concurrently for the same task. The unlocked read-probe-write let
interleaved runs revert a newer effective model, mismatch its
source, or duplicate a model-history entry. Serialize the critical
section through the same per-task meta lock fm-spawn.sh already uses
(fm_meta_lock_path + fm_lock_acquire_wait/fm_lock_release), released
before every exit path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwfFjeYQcz9cZ3vEZohmpm

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Arthur Haro <38157909+haroarthur@users.noreply.github.com>
Co-authored-by: Nicolas Payette <nicolas.payette@specira.ai>
Co-authored-by: Jon Roosevelt <rooseveltadvisors@gmail.com>
Co-authored-by: att430 <41454889+att430@users.noreply.github.com>
Co-authored-by: Valentino-Sole <171032438+Valentino-Sole@users.noreply.github.com>
lytv pushed a commit to lytv/mymate that referenced this pull request Sep 8, 2026
* feat(update): restart every live second mate after a successful update

/updatefirstmate only restarted a second mate when that pass advanced its
AGENTS.md or .agents/skills. An already-current home was skipped entirely, a
bin/-only advance was steered instead, and a remote host that could not report
its instruction diff was downgraded to a re-read. A running agent also freezes
its launch-time wiring - turn-end hooks, harness flags, per-harness feature
switches - and none of that is derivable from a file diff, so an unchanged
tracked surface is not evidence the agent is already on the current behavior.

Restart is now unconditional on a successful update of that home. Every live
second mate the pass leaves on the target commit is restarted, whether it
advanced or was already there.

The safety contract is unchanged: open records are persisted before the agent is
replaced, nothing is forced, stashed, or discarded, a home the pass had to skip
is not restarted at all, and a mate whose runtime cannot prove a restart keeps
the honest re-read path and is never reported as reloaded.

bin/fm-ff-lib.sh gains a settled-state hook that fires for a home left at the
base whether it advanced or was already there, and never for a skipped one; the
instruction-gated hook the session-start convergence sweep uses is untouched.

Regressions: fm-update pins the already-current mate into the restart set and
the unprovable one into the nudge set, and fm-secondmate-restart drives both
real commands end to end - an already-current home is named, persisted, and
genuinely replaced with its checkout untouched, while the unprovable one keeps
its running agent.

* no-mistakes(document): Document unconditional secondmate restarts
BenWilcox8 pushed a commit to BenWilcox8/firstmate that referenced this pull request Sep 12, 2026
* feat(update): restart every live second mate after a successful update

/updatefirstmate only restarted a second mate when that pass advanced its
AGENTS.md or .agents/skills. An already-current home was skipped entirely, a
bin/-only advance was steered instead, and a remote host that could not report
its instruction diff was downgraded to a re-read. A running agent also freezes
its launch-time wiring - turn-end hooks, harness flags, per-harness feature
switches - and none of that is derivable from a file diff, so an unchanged
tracked surface is not evidence the agent is already on the current behavior.

Restart is now unconditional on a successful update of that home. Every live
second mate the pass leaves on the target commit is restarted, whether it
advanced or was already there.

The safety contract is unchanged: open records are persisted before the agent is
replaced, nothing is forced, stashed, or discarded, a home the pass had to skip
is not restarted at all, and a mate whose runtime cannot prove a restart keeps
the honest re-read path and is never reported as reloaded.

bin/fm-ff-lib.sh gains a settled-state hook that fires for a home left at the
base whether it advanced or was already there, and never for a skipped one; the
instruction-gated hook the session-start convergence sweep uses is untouched.

Regressions: fm-update pins the already-current mate into the restart set and
the unprovable one into the nudge set, and fm-secondmate-restart drives both
real commands end to end - an already-current home is named, persisted, and
genuinely replaced with its checkout untouched, while the unprovable one keeps
its running agent.

* no-mistakes(document): Document unconditional secondmate restarts
friesentius pushed a commit to friesentius/firstmate that referenced this pull request Sep 21, 2026
* feat(update): restart every live second mate after a successful update

/updatefirstmate only restarted a second mate when that pass advanced its
AGENTS.md or .agents/skills. An already-current home was skipped entirely, a
bin/-only advance was steered instead, and a remote host that could not report
its instruction diff was downgraded to a re-read. A running agent also freezes
its launch-time wiring - turn-end hooks, harness flags, per-harness feature
switches - and none of that is derivable from a file diff, so an unchanged
tracked surface is not evidence the agent is already on the current behavior.

Restart is now unconditional on a successful update of that home. Every live
second mate the pass leaves on the target commit is restarted, whether it
advanced or was already there.

The safety contract is unchanged: open records are persisted before the agent is
replaced, nothing is forced, stashed, or discarded, a home the pass had to skip
is not restarted at all, and a mate whose runtime cannot prove a restart keeps
the honest re-read path and is never reported as reloaded.

bin/fm-ff-lib.sh gains a settled-state hook that fires for a home left at the
base whether it advanced or was already there, and never for a skipped one; the
instruction-gated hook the session-start convergence sweep uses is untouched.

Regressions: fm-update pins the already-current mate into the restart set and
the unprovable one into the nudge set, and fm-secondmate-restart drives both
real commands end to end - an already-current home is named, persisted, and
genuinely replaced with its checkout untouched, while the unprovable one keeps
its running agent.

* no-mistakes(document): Document unconditional secondmate restarts
friesentius pushed a commit to friesentius/firstmate that referenced this pull request Sep 21, 2026
* feat(update): restart every live second mate after a successful update

/updatefirstmate only restarted a second mate when that pass advanced its
AGENTS.md or .agents/skills. An already-current home was skipped entirely, a
bin/-only advance was steered instead, and a remote host that could not report
its instruction diff was downgraded to a re-read. A running agent also freezes
its launch-time wiring - turn-end hooks, harness flags, per-harness feature
switches - and none of that is derivable from a file diff, so an unchanged
tracked surface is not evidence the agent is already on the current behavior.

Restart is now unconditional on a successful update of that home. Every live
second mate the pass leaves on the target commit is restarted, whether it
advanced or was already there.

The safety contract is unchanged: open records are persisted before the agent is
replaced, nothing is forced, stashed, or discarded, a home the pass had to skip
is not restarted at all, and a mate whose runtime cannot prove a restart keeps
the honest re-read path and is never reported as reloaded.

bin/fm-ff-lib.sh gains a settled-state hook that fires for a home left at the
base whether it advanced or was already there, and never for a skipped one; the
instruction-gated hook the session-start convergence sweep uses is untouched.

Regressions: fm-update pins the already-current mate into the restart set and
the unprovable one into the nudge set, and fm-secondmate-restart drives both
real commands end to end - an already-current home is named, persisted, and
genuinely replaced with its checkout untouched, while the unprovable one keeps
its running agent.

* no-mistakes(document): Document unconditional secondmate restarts
friesentius added a commit to friesentius/firstmate that referenced this pull request Sep 21, 2026
…g handling (#6)

* feat: restart second mates after instruction updates (#3614)

* feat(update): restart second mates whose instructions changed

/updatefirstmate pulled new bytes onto disk and then asked each advanced
second mate to re-read them. A running agent holds AGENTS.md and every
loaded skill frozen from launch and no verified harness offers a reload,
so that steer could not reach a loaded skill at all and left the mate
holding two contradictory copies of its own job description.

An eligible mate is now restarted instead, in the same home and endpoint,
through the existing transactional relaunch. The restart is gated on the
mate first writing down the open work it holds only in conversation - the
open-record half of /stow, never its memory sweeps - so an unregistered
captain call is flushed before the conversation is spent. Anything that
leaves the reload unprovable falls back to the old re-read message and is
reported as exactly that, never as a clean reload.

Remote mates take the same path: fm-remote-secondmate-control.sh gains a
relaunch verb whose host-local leg runs that same control plane, since the
mate is an ordinary local secondmate from its host's point of view. The
primary resolves the profile and passes it explicitly, because
config/secondmate-harness is not inherited and the file on that host
belongs to a different home.

fm-update.sh now splits its advanced live mates into a restart set and a
nudge residual, and both sets require a changed instruction surface, which
also closes the over-nudge against the session-start sweep. Restart is
stricter still: a bin/-only advance reloads itself on the next call, so it
never costs a conversation.

Colocated tests cover the gating, the persist-then-restart order, the
task-subset persist request, each unsafe fallback, the remote hop, and the
remote sync's new instruction-surface report.

* no-mistakes(review): Fix restart correlation, concurrent waits, and lifecycle reporting

* no-mistakes(review): Parallelize relaunches and classify replacement incarnations

* no-mistakes(review): Gate restart actions on live agent state

* no-mistakes(review): Handle failed restart workers without hanging

* no-mistakes(review): Nudge legacy remotes and preserve persist recovery

* no-mistakes(review): Document one-time secondmate restart rollout

* no-mistakes(review): Honor arrived replies and refresh remote profiles

* no-mistakes(review): Revert remote parent profile reconciliation

* no-mistakes(review): Reset remote profile defaults and honor published results

* no-mistakes(review): Preserve fallback nudges for unverifiable secondmates

* no-mistakes(document): Document second-mate restart update flow

* no-mistakes(lint): Fix ShellCheck warnings in restart scripts

* perf: accelerate local validation with bounded concurrency (#3644)

* perf(tests): route gate verification through the bounded concurrent runner

Local validation was the pipeline's dominant cost: across 67 recorded
no-mistakes agent sessions on this repo, 99.3% of command execution was
`bash tests/*.test.sh`, run strictly one script at a time, and 2% of those
calls were killed by an agent-guessed timeout and paid for twice.

Three changes, each measured:

- `.no-mistakes.yaml` pins `commands.test` to
  `bin/fm-test-run.sh --changed --exclude-family real-herdr-gated`. The runner
  already owns changed-file selection, bounded concurrency, the refusal of
  unproven scripts, and a generous automatic per-script bound, so the gate's
  baseline is neither a serial chain nor a guessed timeout. It stays
  intent-targeted - the Test step still runs its evidence agent on top - and
  excludes the live-Herdr family the required Herdr lane owns.

- `bin/fm-test-run.sh` gives a plain list of script paths the same bounded
  automatic scheduler and automatic bound that `--changed` gets. Naming several
  subjects is how a verification round asks for exactly those scripts. The
  curated selections are untouched: `--lane` still composes CI shards whose
  serial lane must stay serial, `--family` is what the required Herdr lane runs,
  and `--all` stays a deliberate complete regression.

- `pr-forge` is admitted to the concurrent-safe family registry on two
  consecutive clean proofs. `docs/fm-test-isolation-proof.md` records those,
  and records `secondmate` and `session-bootstrap` as refused with the exact
  script and reason each failed on, so the refusals are actionable rather than
  silent.

Measured on this host, 0 failures on both sides:

  verification round, 4 scripts   448s chained -> 231s through the runner (-48%)
  pr-forge family                 409.2s at 1 worker -> 237.9s at 4 (1.72x)
  watcher-wake-lock family        1311.1s at 1 worker -> 539.3s at 4 (2.43x)

A fourth lever was implemented and then removed because the measurement
refused it: raising the bounded-wait sample interval from 0.1s to 0.5s made
`fm-watch-triage.test.sh` slower, 435s and 440s against 390s and 393s
unchanged, back to back. Those sleeps are not overhead added to the clock -
they are how a test waits for a subject moving on fm-watch.sh's own one-second
cadence - so sampling less often only delays detection. It also broke
`fm-watcher-lock.test.sh`, which catches a transient rather than waiting for a
settled condition. CONTRIBUTING.md records that result so the experiment is not
repeated.

* no-mistakes(review): Separate concurrent runs by isolation proof family

* no-mistakes(review): Limit automatic timeouts to changed-file validation

* no-mistakes(document): Clarify validation concurrency documentation

* fix: copy PR URLs from durable records (#3648)

* fix: copy PR URLs from records or abstain, never assemble them

Supervision reported a plausible but dead PR link three times because its
prompt demanded a full https:// URL at a moment when only a PR number was
observable, so the model assembled an owner/repository from memory, and the PR
check then accepted that URL and wrote it into the task record, after which the
model kept defending its own tool-endorsed guess over the worker's real link.

Three changes close that chain without any live forge lookup, so private
forges are treated exactly like public ones:

- bin/fm-branch-prompt.sh no longer mandates a URL. Its new "PR identity: copy
  or abstain" section requires a URL to be copied verbatim from a durable
  record (the done: PR <url> status line, pr= metadata, or the backlog note),
  forbids assembling owner, repository, host, or number from memory, and has
  the branch report only the identifier it actually holds when no record names
  the URL yet, leaving the PR check unarmed until the worker's ready line
  arrives. AGENTS.md section 7 and 9 carry the same copy-or-abstain rule for
  main in place of the bare full-URL mandate.

- Worker briefs (bin/fm-brief.sh, ship and scout rules) require the full
  https:// URL wherever a PR is mentioned - status line, terminal, or summary -
  never a bare "PR 108", so the link is in view as early as the number is.

- bin/fm-pr-check.sh refuses, offline and before any side effect, a URL that
  the task's own done lines contradict, printing both spellings; a log naming
  no URL still records the argument as before. fm_pr_status_ready_urls in
  bin/fm-pr-lib.sh owns reading those lines. The refusal also reaches
  bin/fm-pr-merge.sh, so nothing merges under a contradicted URL.

Tests cover the offline refusal with zero side effects, the recorded spelling
being accepted, markdown-wrapped and punctuated URLs, working lines not
counting, the merge wrapper propagation, a self-hosted merge request with no
forge call, the prompt carrying the rule, and the brief carrying the worker
rule.

* no-mistakes(review): Remove stale PR URL enforcement

* no-mistakes(ci): Removed backlog notes as an accepted PR identity source. PR URLs may now be copied only from the task’s `done: PR <url>` status or canonical `pr=` metadata; otherwise supervision reports only the known identifier and leaves PR checking unarmed. Updated related guidance/docs and verified with branch-supervision tests, brief tests, ShellCheck, and `git diff --check`

* fix(bin): disable Claude feedback drafts for fleet launches (#3661)

* fix(bin): disable Claude's feedback-draft flow for fleet-launched agents

Scope --settings '{"feedbackDrafts":"off"}' to every Firstmate-launched
Claude crewmate and secondmate, so /bug and /feedback never queue or
submit a bug report on the captain's behalf. feedbackDrafts is the
documented settings key (Claude Code changelog 2.1.247); the
per-launch CLI flag never touches the captain's global settings.json.

Claude-Session: https://claude.ai/code/session_01XYAXXzr4oZx9NjZb1veeE3

* no-mistakes(review): Prevent managed settings from re-enabling Claude feedback drafts

* no-mistakes(document): Fix Claude feedback documentation formatting

* fix(bin): layer both feedback-draft controls for defense in depth

The prior --settings-only fix can be overridden by a managed Claude
settings policy (feedbackDrafts precedence). Keep CLAUDE_CODE_SEND_FEEDBACK=0
alongside --settings '{"feedbackDrafts":"off"}': either control alone
disables the SendFeedback tool, so a managed override of one still
leaves the other in force.

Claude-Session: https://claude.ai/code/session_01XYAXXzr4oZx9NjZb1veeE3

* no-mistakes(document): Document Claude feedback-draft suppression ownership

* feat(tests): run three more validation families concurrently (#3662)

* perf(tests): admit three more families to concurrent validation

The three families that `docs/fm-test-isolation-proof.md` recorded as refused
were not refused for concurrency. Each blocker was a test that decided a
property by wall clock, or a script filed where it cannot run. Fixing those
three things admits all three families and recovers 28.6 minutes of local
validation with no assertion removed or weakened.

- `tests/fm-backlog-handoff.test.sh` injected its pre-move crash by killing the
  handoff, sleeping a fixed second, then delegating the move to the real
  binary. Nothing ever killed the fake, so on a host slow enough for the case's
  next assertions to take longer than a second, the orphan woke and completed
  the very move the case requires left undone, and recovery then failed with
  `Task "pre-move-crash" not found in this backlog`. Watching the two backlogs
  during the injected crash showed exactly that, the item moving one second
  after the crash. All four crash injections in the file now go through a new
  `fm_fake_crash_injector` shim that signals the target and returns only once
  it is observably gone, and the pre-move fake never delegates the move at all.

- `tests/fm-session-start.test.sh` proved the startup digest does not block on
  a slow current-state read by timing the whole digest against a fixed
  eight-second sleep, which a loaded host exceeds without the property being
  violated. It now holds that read open until the case releases it and asserts,
  the moment the digest returns, that the read has not finished. A digest that
  waited would wait indefinitely rather than for an interval a slow host can
  out-run, so the assertion is stronger than the bound it replaces. Its scan
  budget moves to the maximum, because the old value left two seconds of margin
  over the fixed sleep and measured the host rather than the deadline that
  `tests/fm-inactive-reconcile.test.sh` owns.

- `fm-backend-herdr-focus-flash-e2e` was filed in the family map's catch-all,
  which put it in the portable serial lane, where Linux CI gate-skips it: that
  real-Herdr regression was running nowhere. It moves to `real-herdr-gated` and
  the required Herdr lane. `fm-claude-stop-autoarm-live-e2e` gate-skips on its
  opt-in variable and moves to `live-harness-optin`.

The 28 remaining ungrouped scripts become an enumerated `standalone` family
instead of admitting `unclassified` itself. `unclassified` is the family map's
`*)` arm, so admitting it would silently grant concurrency to every test added
afterwards, which is exactly the population with no proof. A new test still
lands in `unclassified` and stays serial, and `tests/fm-test-run.test.sh`
covers that split behaviorally.

Each family passes two consecutive four-worker proofs with zero failures. On
the production runner, `secondmate` goes 1233.1s to 453.4s, `session-bootstrap`
756.4s to 286.4s, and `standalone` 724.6s to 261.1s: 2.71x overall and 1713.2s
recovered. The whole suite runs 177 scripts in 52.6 minutes of wall clock
against 121 minutes of summed script time.

* no-mistakes(document): Refresh concurrent validation and shard documentation

* no-mistakes(ci): Fixed the real-Herdr focus-flash E2E race exposed by reclassification. Part C now starts its persistent child atomically via `pane run` and verifies stable child identity through Herdr’s public `process-info` interface, avoiding the racy send-text/send-keys sequence and platform-specific `ps` matching. Verified with bash syntax checking, ShellCheck, git diff checks, and the complete E2E test on Herdr 0.8.2

* feat: structure no-mistakes ask-user escalations (#3670)

* feat(brief): structure no-mistakes ask-user escalation as event + snapshot file

Crewmates escalating a no-mistakes ask-user gate now report one status
event naming every finding id plus a snapshot file holding the gate's
axi finding records verbatim (id, severity, file, line, description,
authority), using the same shape even for a single finding. The status
line never paraphrases. The format is defined once in fm-dod-lib.sh and
rendered into both the scout and ship rule 6 in fm-brief.sh, so a
promoted scout - whose rule 6 fm-promote.sh preserves unchanged - gets
the identical contract as a freshly-spawned no-mistakes ship worker.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PpiWaDerbYavTLPPtEjQei

* no-mistakes(review): Preserve ask-user escalation output contract

* no-mistakes(review): Align escalation format test expectation

* no-mistakes(review): Scope ask-user escalation instructions correctly

* no-mistakes(review): Remove ask-user from generic decision rules

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* fix(bin): require self-sufficient no-mistakes intent (#3671)

* fix(bin): require a self-sufficient no-mistakes intent

A no-mistakes worker's --intent is only as useful as the string it
passes. PR #3604 shipped with an intent that was only "do 1, 2, 3, 7
from the report": the real contract lived in a private scout report and
never reached --intent, so nobody holding that string plus the codebase
could have derived the specification.

This is pure instruction at the contract's one owner; no spawn-side or
promotion-side check is added.

- bin/fm-dod-lib.sh: the generated no-mistakes Definition of done now
  states that the --intent string must be self-sufficient (the string
  plus the codebase reconstructs roughly the same specification) and
  tells the worker to write the substance of any report, decision, or
  PR the captain's intent refers to into --intent rather than the
  pointer, while Firstmate build instructions and the worker's own
  decisions still stay out. The spawn-time overlay points back at that
  rule so its "supersedes" wording cannot cancel it, and the header's
  owner statement carries the rule.
- AGENTS.md section 11 and bin/fm-brief.sh's header ask Firstmate to
  include the substance of referenced material when filling
  ## Captain's intent, and section 11 points at the owner of the rule.
- tests/fm-brief.test.sh and tests/fm-task-delivery.test.sh assert the
  rendered brief and launch contract carry the rule.

Claude-Session: https://claude.ai/code/session_01YMhEe42q7BAAoN6RxNuzim

* no-mistakes(document): Replace incident-specific intent test commentary

* fix: accelerate local Bearings snapshot composition (#3499)

* Speed local fleet snapshot composition

* no-mistakes(review): Stabilize task inventory during concurrent snapshot composition

* no-mistakes(document): Document local snapshot observation concurrency

* no-mistakes(ci): Fixed CI failures by making empty task manifests compatible with stock macOS Bash 3.2, snapshotting task metadata before concurrent observations to prevent generation drift, strengthening the behavioral race regression, and updating the stock-Bash Bearings test count to 45. Verified fleet snapshot tests (15), Bearings tests (45), workflow lint tests, project lint, Bash 3.2 parsing, and diff checks

* no-mistakes(ci): Fixed the Linux CI failure caused by passing large backlog/task JSON through jq command-line arguments, which exceeded the per-argument size limit. Both inventory projections now stream large JSON inputs through stdin. Verified with fm-bearings-snapshot.test.sh (45 tests), fm-fleet-snapshot-view.test.sh (15 tests), Bash syntax, and git diff checks

* no-mistakes(ci): Fixed concurrent task teardown during metadata capture: vanished metadata is now omitted while genuine copy failures remain fatal. Added a deterministic public Bearings regression test and updated CI’s expected test count. Verified with the full Bearings suite, workflow-lint suite, Bash syntax checks, and git diff checks

* no-mistakes(ci): Fixed PR-caused CI and review issues: streamed large fleet JSON through jq stdin to avoid Linux argument limits, kept crew-state reads bound to captured metadata generations, and strengthened the behavioral race test. Bearings (46 tests), fleet snapshot (15 tests), crew-state, backend, lint, Bash syntax, and diff checks pass locally. Serial shard 5’s unrelated task-inbox segmentation fault appears infrastructural/flaky

* no-mistakes(ci): Fixed endpoint-state generation crossing by validating captured spawn_gen before and after local endpoint probes, falling back to exact metadata identity for legacy tasks. Stale probe results now become unknown instead of false unhealthy state. Added a behavioral relaunch-race regression test. Verified the full Bearings snapshot suite, shellcheck, bash syntax, and git diff checks

* fix(snapshot): keep live observations generation-coherent

* no-mistakes(review): Keep secondmate observations generation-bound without copying reports

* no-mistakes(document): Document generation-coherent snapshot observations

* test(bearings): measure local read overlap instead of wall-clock budget

The large-local-snapshot regression asserted that a whole snapshot
composed in under five seconds. That bound measures how loaded the host
is, not whether the per-task reads actually overlap, so it failed
intermittently on a contended machine: one run in six on a box at load
16-20, landing exactly on the five second boundary.

Time a serialized run and a concurrent run of the same workload instead
and require the concurrent one to save at least two seconds. Both runs
pay the same composition overhead, so the difference isolates the
overlap this change delivers. Five one-second reads serialize into five
seconds and overlap into about one, and re-serializing the reads
collapses the saving to roughly zero, so the assertion still fails
loudly if the concurrency regresses.

Also bump the pinned Bearings test count to 48, since rebasing onto the
current default branch picked up its captain-hold test.

* no-mistakes(review): Restore JSON-derived decision flags

* no-mistakes(review): Unify status-derived snapshot observations

* no-mistakes(ci): Updated the stock macOS Bash CI check’s Bearings test count from 48 to 49. Verified the full Bearings suite passes and emits exactly 49 TAP successes; git diff checks pass

* fix: prevent stale supervision wake loops (#3672)

* fix(bin): stop the supervision branch's stale-ack and ghost-report loops

Clean-slate implementation of the four authorized recommendations from the
supervision-ghost-retrigger analysis (items 1, 2, 3, and 7), in their minimal
form, superseding PR #3604:

- fm_branch_report refuses a task the wake being handled never named. The
  extension fixes the reportable task set from the eligible rows before each
  prompt (signal and stale rows resolve to their tasks, a heartbeat allows any
  task with a live record, fleet is always allowed), so a report typed from
  memory about a task whose records teardown already removed is never stored
  or delivered.
- An acknowledgement that consumes nothing says "nothing was acknowledged
  through N" and prints the exact --ack-through / --recovery-generation
  command for the current presented wake, instead of "re-run the drain",
  which re-fed the same stale acknowledgement in a loop.
- bin/fm-guard.sh no longer tells the branch actor to drain queued wakes
  while it is handling them; it names the granted rows instead.
- Teardown removes state/.<task>.branch-outcome-index for ordinary tasks and
  descendants; the index rebuild and the append-side index write both skip a
  task with neither a live record nor a status log, so the branch's report of
  a teardown it just performed is stored without recreating the index.

No new locking, no spawn-generation binding, and no retired-task refusal: the
branch can still report the outcome of a task it just tore down, and the
teardown test now proves that path end to end.

* fix(bin): narrow the branch report scope and guard silence to the minimal form

Apply the four review decisions on the clean-slate branch:

- A signal or stale prompt may report only the tasks its own rows resolve
  to; fleet is refused there too. A heartbeat review is not scoped by task
  at all, so the extension no longer tracks live task records and refuses
  nothing by task id during a fleet review.
- The outcome-index rebuild no longer skips retired tasks; the append-side
  skip alone keeps a torn-down task's index from being recreated.
- bin/fm-guard.sh keeps the queued-wakes warning silent for the branch actor
  instead of printing a replacement note.

* no-mistakes(document): Align supervision docs with scoped wake handling

* fix(bin): avoid fleet snapshot argument limits (#3677)

* Fix fleet snapshot large JSON transport

* no-mistakes(review): Captain: file-back fleet snapshot transport safely

* no-mistakes(review): Captain: file-back parent summary aggregation

* no-mistakes(ci): Rebased the PR's three commits onto f4d7875824ecc5e274b4bb896f10c1e1f207b7e4 and resolved the fleet snapshot conflict while preserving the base's task-observation lifecycle. Fixed Greptile's valid finding by recursively removing the private mktemp transport directory, so future transport files cannot cause cleanup to fail. Verified with tests/fm-home-summary-refresh.test.sh, bin/fm-lint.sh, git diff --check, and ancestry checks. All passed; the fix remains as an uncommitted worktree change for the outer executor

* fix(bin): attribute active runs with unfetched pipeline heads (#3681)

* fix(bin): recognize active pipeline fix rounds with unfetched run heads

A no-mistakes fix round advances the run head beyond the submitted head,
and the pipeline commits in its own checkout, so the task copy never
receives the new commit object. fm-crew-state's strict head rule rejected
the active row, the coarse runs-list scan skipped it and matched the
older failed row at the submitted head, and an active validation read as
failed (observed on model-routing-benchmark-hardening: active head
ac61c64b vs task copy at fb47636d).

fm_nm_runs_status_for_worktree in bin/fm-nm-run-lib.sh now owns
runs-ledger attribution: the branch's newest row alone decides, and a
newest row whose head cannot resolve locally is recognized only as a
provable pipeline-owned continuation - active (running) and anchored by
the immediately older row for the same branch having ended at exactly
this worktree's HEAD. The reader keeps the axi TOON as full detail for
that proven same-branch run. Unanchored, ancestor-anchored, and terminal
unresolvable rows stay unattributed, so branch-name coincidence and other
tasks' runs never match, and fm_nm_head_matches_worktree keeps its exact
prior semantics for teardown (verified by the full teardown suite).

Tests: reproduction regression for the unfetched active fix head (reads
working via full run-step detail), coarse-path continuation when axi
answers another branch, and negative controls for the unanchored active
row and the unresolvable terminal row with the historical fallback
preserved.

Ported onto upstream/main f4d78758, where #3194 independently added the
branch_sync custody exemption on the full axi-status path: both mechanisms
now coexist, each owning one surface (TOON custody on the full path, the
runs ledger on the coarse path). The port deletes the superseded coarse
scan-and-skip (nm_runs_status_for_branch) and its now caller-less helpers
(fm_nm_head_resolvable, nm_coarse_head_matches_worktree), renames the
exemption comment's "the one exemption" phrasing now that a second
complementary exemption exists, and points the stale
FM_CREW_STATE_RUNS_LIMIT comment at fm_nm_runs_status_for_worktree
(judge follow-up #1). The parent coarse-guard test's fixture is the
ledger-anchored continuation shape, so its expectation flips to the fixed
behavior (working via run-step, never the older failed row); a new
mismatched-anchor coarse negative control preserves that guard's original
no-anchor protection (pane answers, never the older row).

* no-mistakes(document): Clarify pipeline attribution documentation

* fix(bin): pre-register claude workspace trust at spawn time (#3663)

* fix(bin): pre-register claude workspace trust for task worktrees

A claude crewmate launched into a fresh task worktree met Claude Code's
interactive workspace-trust dialog before it ever read its brief, and firstmate
could not answer it: the key plane carries only Enter, Escape, and C-c with no
arrow navigation, and the dialog's selection starts on "No, exit", so the
documented Enter recipe ended the session instead of accepting it. Two workers
wedged this way and were unblocked only by hand-seeding the trust store per
path.

--dangerously-skip-permissions does not cover that gate. `claude --help`
records the dialog as skipped only in non-interactive mode, through -p or a
non-TTY stdout, and a crewmate pane is interactive, so there is no launch flag
to reach for.

fm-spawn now pre-registers the worktree through bin/fm-claude-trust.sh in the
existing claude branch, before the project settings that the same gate would
otherwise block, and refuses the spawn when that write fails rather than
launching a worker that would wedge.

The scope test is the safety property and is structural rather than a path
policy: the path must be a linked git worktree, sharing the spawning project's
common dir, whose top level is exactly the resolved argument. Git is the ground
truth, so the argument is never trusted on its own word, and a primary
checkout, an unrelated repo, a worktree subdirectory, a plain directory, and a
home directory are each refused rather than warned about or skipped. A
treehouse or orca path prefix was deliberately avoided because treehouse's root
is configurable, which would make a prefix both wrong and a new policy surface.
One structural test covers both worktree providers.

tests/fm-claude-trust.test.sh pins both halves, including a case where HOME is
itself a valid linked worktree so the home guard is proven load-bearing rather
than passing vacuously, plus the spawn-level proof that a claude spawn trusts
its worktree and launches with the brief pointed at the same store.

The adapter reference no longer tells a firstmate to press Enter on that
dialog, and the shared trust reference now names every harness surface: which
harnesses gate, which suppress at launch, which dodge the gate, which now
pre-registers, and that a claude secondmate is excluded by design.

The spawn fixture runs each spawn against a throwaway HOME so the suite cannot
write the developer's real store, isolating through HOME rather than
CLAUDE_CONFIG_DIR because the spawn forwards a set CLAUDE_CONFIG_DIR onto the
launch command that launch-shape assertions read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HNEN2GLnew27HFyfi4ms4v

* fix(bin): create the staged trust store exclusively

The staged store was written to a predictable pid-based path with a plain
write, which follows a symlink. Where the Claude config directory is writable
by another local account, that account could pre-create the path as a symlink
and redirect the write into another file the launching user owns.

The staged name now carries random bytes and is created with an exclusive
"wx" open, so an existing path is refused outright instead of followed. The
happy-path test also asserts no staged store survives the rename.

The durability comment now states the residual window plainly: the readback
proves the entry landed, not that it survives, because a vendor session that
rewrites the whole store afterwards can still drop it and no lock closes that
window when the writer is Claude itself. The worker then meets the dialog and
stalls, which reaches firstmate as the ordinary stale wake rather than as
silent success.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HNEN2GLnew27HFyfi4ms4v

* no-mistakes(review): neutralise CDPATH in claude trust scope guard

* no-mistakes(review): sandbox HOME in spawn tests, drop out-of-scope artifacts

* no-mistakes(review): refuse unresolvable git dir, compact store, fix secondmate doc

* no-mistakes(review): clear git env overrides, resolve symlinked store target

* no-mistakes(review): degrade without node, fix Pi gate claim, record trust proof

* no-mistakes(review): refuse without node, pin CLAUDE_CONFIG_DIR in spawn tests

* no-mistakes(review): refuse relative config dir and concurrent store modification

* no-mistakes(review): correct orca worktree claim, clean staged store on failure

* no-mistakes(review): restore pretty-printed store, correct trust dialog docs

* no-mistakes(review): arm trust gate before busy state to avoid orphans

* no-mistakes(document): record claude trust pre-registration in its owner docs

* no-mistakes(document): note orca limit for claude trust pre-registration

* no-mistakes(ci): Fixed the Greptile P1 on bin/fm-spawn.sh by moving the Claude trust gate earlier rather than adding cleanup machinery. Diagnosis: Greptile reported that when Claude trust registration fails on tmux/Zellij/cmux/non-projected Herdr, the exit runs after the backend endpoint and /tmp/fm-<id> were created, and the abort trap cleans neither. The endpoint half is pre-existing, deliberate architecture — the two refusals immediately above the gate (the 60s `treehouse get` timeout at fm-spawn.sh:2550 and `validate_spawn_worktree` at :2487) also exit with the endpoint live and direct the operator with "inspect window $T"; spawn_abort_cleanup only reclaims orca endpoints (already covered via ORCA_ABORT_CLEANUP) and herdr projections. The temp-root half was genuinely introduced by this PR: the gate was placed beside the busy-state arm, ~30 lines after `mkdir -p "$TASK_TMP/gotmp"`, and fm-teardown can only find that root through `tasktmp=` in a meta record a refused spawn never publishes. Root-cause fix (smallest correct change, no new subsystem): - bin/fm-spawn.sh — moved the `claude*` trust gate from inside the busy-arm block up to the first point $WT is known, immediately after the `freshen_spawn_worktree_base` block and before TASK_TMP creation, the STATE setup, and the relaunch `clear_relaunch_harness_wiring` retirement. A refusal now leaves no temp root, no retired relaunch wiring, and no busy record; only the endpoint remains, in the same class as the two refusals just above it. - bin/fm-spawn.sh — the refusal message now ends with "inspect window $T", matching the existing convention so control/teardown can identify the endpoint. $T is set for every backend on the non-secondmate path. - bin/fm-spawn.sh:196 — header note corrected from "before any state is armed" to "before any per-task state exists". - tests/fm-claude-trust.test.sh — the existing refused-spawn test's own comment claimed "before any task state exists" but only asserted busy state. Renamed to test_refused_spawn_leaves_no_task_state and added an assertion that /tmp/fm-<id> is absent, with the task id suffixed by the test process pid so the assertion reads only this run's path (a stale /tmp/fm-refusedspawn from the fixed-id version was in fact present on this box). No assertions on implementation source bytes. Verification run locally: - The new assertion fails against the pre-fix bin/fm-spawn.sh ("not ok - a refused spawn stranded a temp root no teardown can find") and passes after — a real before/after regression proof. - tests/fm-claude-trust.test.sh: 20/20 ok. - tests/fm-backend.test.sh, fm-backend-orca, fm-control-relaunch, fm-spawn-dispatch-profile, fm-trace-context-spawn, fm-gotmp: all pass. - tests/fm-backlog-atomicity.test.sh: rc=0, 79 assertions ok. - bin/fm-lint.sh (repo's single lint owner, pinned ShellCheck 0.11.0 + actionlint 1.7.12): clean. - No /tmp/fm-refusedspawn* leftovers after the runs. Scope respected: no trust subsystem, no policy layer, no config surface, no endpoint-cleanup mechanism added; the change is an ordering move plus one error-message clause and the test that pins it. Adapter references and docs made no ordering claim, so none needed updating. Changes are left uncommitted in the worktree for the outer executor

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix: restart every live second mate after updates (#3690)

* feat(update): restart every live second mate after a successful update

/updatefirstmate only restarted a second mate when that pass advanced its
AGENTS.md or .agents/skills. An already-current home was skipped entirely, a
bin/-only advance was steered instead, and a remote host that could not report
its instruction diff was downgraded to a re-read. A running agent also freezes
its launch-time wiring - turn-end hooks, harness flags, per-harness feature
switches - and none of that is derivable from a file diff, so an unchanged
tracked surface is not evidence the agent is already on the current behavior.

Restart is now unconditional on a successful update of that home. Every live
second mate the pass leaves on the target commit is restarted, whether it
advanced or was already there.

The safety contract is unchanged: open records are persisted before the agent is
replaced, nothing is forced, stashed, or discarded, a home the pass had to skip
is not restarted at all, and a mate whose runtime cannot prove a restart keeps
the honest re-read path and is never reported as reloaded.

bin/fm-ff-lib.sh gains a settled-state hook that fires for a home left at the
base whether it advanced or was already there, and never for a skipped one; the
instruction-gated hook the session-start convergence sweep uses is untouched.

Regressions: fm-update pins the already-current mate into the restart set and
the unprovable one into the nudge set, and fm-secondmate-restart drives both
real commands end to end - an already-current home is named, persisted, and
genuinely replaced with its checkout untouched, while the unprovable one keeps
its running agent.

* no-mistakes(document): Document unconditional secondmate restarts

* fix(bin): close pending-reply decisions via resolve-key (#3696)

* fix(bin): close reserved pending-reply keys via fm-send --resolve-key

fm-send wrote answered: notes that the reserved-key fold ignores, so
operator closes exited 0 while OPEN DECISIONS kept the decision open.
Speak the owning library's close vocabulary on that path, and refuse
when a reserved close cannot take effect.

* no-mistakes(review): Safely quote manual decision-close recovery commands

* no-mistakes(review): Reject unclosable overlong decision keys before sending

* no-mistakes(review): Remove contract suffix from open decisions hint

* no-mistakes(document): Document resolve-key line-cap refusal

* fix(bin): prevent false missed-reply escalations (#3697)

* fix(bin): stop false missed-reply escalations for same-basename self-home answers

A healthy secondmate that wrote corr= to its own state/<id>.status never matched the parent channel, so recovery confirmed and the record escalated as pending-reply-missed. Make the report helper resolve the parent channel itself, skip parent-replies.status as wrong-home, put a readable sighting path on the missed line, and restatement-copy only that same-basename self-home file onto the parent channel.

* no-mistakes(review): Resolve late replies before recovery escalation

* no-mistakes(review): Tighten reply routing and regression coverage

* no-mistakes(review): Preserve reply paths and require explicit home

* no-mistakes(review): Encode wrong-home paths before persistence

* no-mistakes(document): Document corrected secondmate reply routing

* no-mistakes(lint): Fix pending-reply ShellCheck warnings

* feat: add verified Gemini crewmate runtime (#3695)

* feat(harness): verify gemini as a crewmate runtime adapter

Adds Gemini CLI as a fourth dispatch target alongside claude, codex, and
grok, scoped to crewmate and scout work only. Every axis was proven against
gemini-cli 0.58.0 rather than inferred; docs/verification/runtime-backends.md
carries the dated evidence and names what stayed unverified.

Busy state is semantic, not rendered: BeforeAgent opens a turn and AfterAgent
and SessionEnd close it. AfterAgent also fires on a manual interrupt, so a
cancelled turn closes its own record.

Three findings shaped the wiring rather than a config line:

- --skip-trust and GEMINI_CLI_TRUST_WORKSPACE=true are presented by the CLI
  as equivalents and are not. A controlled A/B showed --skip-trust leaves
  project configuration unloaded, so workspace skills never load.
- The worktree's .gemini/settings.json is the PROJECT's committed settings
  file, unlike claude's settings.local.json. Firstmate's hooks therefore go
  to a firstmate-owned state/<id>.gemini-settings.json reached through
  GEMINI_CLI_SYSTEM_SETTINGS_PATH, which also works untrusted and merges
  with a project's own hooks instead of replacing them.
- The shipped CLI is a node bundle whose live process reports comm as
  MainThread, so ancestry cannot see it. GEMINI_CLI=1 is load-bearing and is
  tested before an inherited CLAUDECODE, and pane liveness identifies gemini
  from the script argument through the new bin/fm-gemini-lib.sh.

Gemini is refused for secondmates: it has no primary supervision protocol.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYdQkKfSEQrFUxtcTXZ66L

* test: clear gemini's marker in launch and detection expectations

Every non-gemini launch now clears GEMINI_CLI the way it already clears
cursor's markers, so the two tests that pin the exact launch prefix are
updated to match. The harness-detection tests that scrub foreign markers
before probing ancestry scrub GEMINI_CLI too, so running the suite from
inside a gemini session cannot produce a false verdict.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYdQkKfSEQrFUxtcTXZ66L

* docs: classify the gemini harness reference

The documentation inventory is the single classification owner for maintained
prose surfaces, and every surface must appear in it exactly once. The new
harness reference is agent-runtime, matching its siblings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYdQkKfSEQrFUxtcTXZ66L

* no-mistakes(review): Narrow Gemini ancestry detection

* no-mistakes(review): Restrict Gemini hooks to canonical launches

* no-mistakes(document): Document Gemini adapter support boundaries

* no-mistakes(ci): Fixed Gemini process identity when interpreter or script paths contain whitespace. Tmux liveness now uses NUL-delimited /proc argv on Linux, with the existing flattened ps fallback elsewhere. Added a real-process regression test. Verified with the Gemini harness test suite, full fm-lint, ShellCheck, and git diff --check. The CI and Require no-mistakes runs were action_required/attestation outcomes rather than code failures

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(teardown): conclude parked runs advanced past task copy (#3704)

* conclude parked runs the pipeline advanced past the task copy

A no-mistakes fix round commits in the daemon's own gate-repo clone, so a
run parked at a gate can carry a head whose object the task copy never
received. Teardown's strict object-local identity rule then declined to
conclude the run, and cleanup left it parked forever holding a fleet slot
(observed 2026-09-03; the same masking condition PR 3681 fixed on the
read path, now closing the teardown half its scope boundary deferred).

task_status_is_own_parked_run now falls back - only when the reported
head resolves to no local object - to the one shared runs-ledger
attribution rule fm_nm_runs_status_for_worktree (bin/fm-nm-run-lib.sh),
whose anchored continuation proof binds the branch's newest active row
to this worktree's exact submitted head. Foreign branches, stale
history, terminal rows, ancestor-only anchors, diverged newer rows, and
ambiguous multi-row shapes all still refuse, and runs that are actively
running, fixing, or in CI remain untouched: only the parked-at-a-gate
determination ever reaches the abort. No sqlite access, no fetches into
another task copy, no custody changes, no duplicated matching logic.

* tighten the parked-run ledger fallback and pin both judge corrections

The teardown ledger fallback now authorizes concluding this task's parked
run only when the shared runs-ledger rule's proved answer is the explicitly
active word (running): a terminal newest row - even anchored at exactly the
worktree's head - is finished history and never an abort authorization.
The read path may classify the same owner's answer; teardown's abort must
never fire for a run that already ended.

Two bounded pre-validation corrections from the implementation review:
- a fetched-object counterfactual pins the strict-rule path: a pipeline fix
  head fetched into the task copy aborts through object-local identity
  alone, with an empty ledger and a proof the runs query never fired;
- a negative fixture pins the tightened boundary: an unresolvable reported
  head with a terminal newest same-branch row anchored at the worktree head
  engages the ledger fallback and still refuses, so the refusal is the
  terminal-word boundary and not an earlier guard.

* no-mistakes(review): Bind teardown ledger fallback to validated run heads

* no-mistakes(review): Restore validated advanced-head ledger continuation

* no-mistakes(review): Reject invalid ledger dates and terminal statuses

* no-mistakes(document): Document teardown ledger scan limit

* fix: classify captain holds from structured state (#3508)

* Keep parked and aged undated captain holds off live Captain's Call.

Bearings was treating undated parked-style holds as live calls; mark those phrasings deferred and project holds older than a configurable 14-day since date as Charted Next gates instead.

* no-mistakes(review): Bound parked marker matching to lexical tokens

* no-mistakes(review): Age undated holds from durable hold-set dates

* no-mistakes(review): Reset re-held timestamps and scan full bodies

* no-mistakes(review): Preserve timestamp precision and prioritize parked suppression

* no-mistakes(document): Document undated captain-hold aging

* no-mistakes(ci): Fixed stock Bash CI test-count expectations (16 snapshot, 45 Bearings). Prevented fresh holds on old tasks from aging via stale `since` dates by aging only stamped holds. Added behavioral regressions and verified both suites plus Bash 3.2 parsing

* no-mistakes(ci): account for rebased snapshot regression

* no-mistakes(review): Restore legacy hold aging and mandate wrapper

* no-mistakes(review): Restrict hold stamps to canonical leading lines

* no-mistakes(review): Exclude historical answers and deduplicate revealed holds

* no-mistakes(document): Correct captain-hold projection documentation

* no-mistakes(ci): Rebased onto 8988af2 and resolved Bearings conflicts. Fixed the hold timestamp race by persisting and verifying the timestamp before publishing the captain hold; failures now leave the task unheld. Added behavioral coverage for ordering and failure handling. Preserved the required parked-phrase projection behavior. Relevant snapshot, Bearings, lifecycle, syntax, and ShellCheck validations pass

* no-mistakes(review): Bound current prose before historical resolutions

* no-mistakes(review): Preserve hold age across interrupted answers

* no-mistakes(review): Preserve leading hold stamps until answer closure

* no-mistakes(review): Normalize answer bodies on matching retries

* no-mistakes(review): Document concurrent re-hold age-basis limitation

* no-mistakes(document): Refresh captain hold lifecycle documentation

* no-mistakes(ci): Fixed both CI failures. Updated the macOS Bash snapshot expectation from 45 to 46 Bearings tests. Narrowed parked-style deferral matching to explicit hold-reason prefixes while preserving legacy explicit markers and preventing contextual prose from hiding active decisions. Added behavioral regression coverage. Verified with stock Bash 3.2: 17 fleet snapshot tests and 46 Bearings tests pass; full lint and workflow validation also pass

* no-mistakes(ci): Fixed Greptile’s P1 finding by restricting parked-style deferral phrases to complete hold-reason markers. Contextual reasons beginning with “not urgent,” “queued opportunity,” or “captain-gated” now remain visible decisions. Added behavioral coverage through the real fleet and Bearings snapshot paths and updated documentation. Verified both snapshot suites under Bash 3.2 (17 fleet tests and 46 Bearings tests), syntax checks, and git diff checks. The no-mistakes attestation failure is external/stale and requires the outer pipeline to refresh it for the new head

* no-mistakes(ci): Fixed parked-style undated captain holds disappearing from the default Bearings board. They now project to Charted Next with omitted[] disclosure, while --all-decisions reveals them and removes the safety gate. Added behavioral coverage for the reported “not urgent” case and aligned documentation. Verified fm-bearings-snapshot, fleet snapshot view, and captain-hold lifecycle tests; shellcheck, bash syntax, and git diff checks pass

* no-mistakes(test): Stabilize concurrency budget and provision timeout tests

* no-mistakes(document): Correct captain hold documentation details

* no-mistakes(ci): Fixed hold-reason parsing so commas in contextual reasons are preserved and do not incorrectly defer live Captain's Call decisions. Added end-to-end fleet/Bearings regression coverage. Reworked the flaky Herdr timeout test to assert observable late-launch behavior rather than process-ID liveness. Verified both snapshot suites, Herdr test 5 consecutive times, shell syntax, shellcheck, and git diff checks

* Restore the Herdr lab timeout test to its main version.

The stabilization rounds reworked tests/fm-herdr-lab.test.sh while chasing a
load-induced flake, replacing the fake server's wall-clock delay with a
SIGSTOP'd process and asserting that the blocked process is gone after a
timed-out provision. A stopped process does not die from SIGTERM, so that
assertion fails on Linux and the portable parallel shard stayed red.

That test is unrelated to the undated captain-hold projection this branch
delivers and was identical to main before these rounds, so restore main's
version exactly. It still proves that a timed-out provision cancels its late
launch before teardown.

* no-mistakes(review): Preserve metadata-like prose in captain hold reasons

* no-mistakes(review): Resurface due dated captain holds

* no-mistakes(review): Distinguish parked holds from explicit deferrals

* no-mistakes(review): Invalidate legacy secondmate summary caches

* no-mistakes(review): Keep blocked deferred holds in Charted Next

* no-mistakes(review): Count blocked deferred holds in omission disclosure

* no-mistakes(document): Correct captain-hold projection documentation

* no-mistakes(ci): Fixed both CI failures. Updated the macOS Bearings test count to 51. Preserved the v1 summary schema for compatibility while rejecting hold-bearing summaries missing the new aging fields, preventing stale caches from restoring noisy calls. Verified fleet snapshot, Bearings snapshot (51 tests), home-summary refresh, secondmate reconciliation, Bash 3.2 parsing, and diff checks

* no-mistakes(ci): Fixed Greptile’s valid finding: `--all-decisions` now reveals deferred/aged captain holds even when blocked, for both main and secondmate homes, and removes their duplicate Charted Next gates. Added behavioral regression coverage and updated documentation. The prose-classifier finding was not applied because exact complete-phrase matching is explicitly required by the author intent; contextual wording remains live. Verified with Bearings and fleet snapshot tests, `bin/fm-lint.sh`, Bash syntax checking, and `git diff --check`

* no-mistakes(ci): Fixed the actionable-state bug in Bearings: an arrived parked-style hold is live only when it is not explicitly non-actionable, so blocked due holds remain gated by default and are revealed by --all-decisions. Added behavioral regression coverage for that case. Preserved complete-reason parked-style classification as required by the author intent. Verified with tests/fm-bearings-snapshot.test.sh, bin/fm-lint.sh, and git diff --check

* Show why a revealed captain hold is deferred.

Under --all-decisions a deferred hold is revealed and its Charted Next gate
is removed, but the revealed row carried only the bare hold reason. A
date-deferred or blocked hold therefore read exactly like a genuine live
decision, because the until date, the age, and the blocking work only ever
appeared on the gate row that the reveal replaces.

Annotate a row that is revealed because it is deferred with the same
vocabulary the gate uses - until <date>, held <n>d, and the blocking work -
so the expanded view reads as deferred-but-shown. A genuinely live call is
left unannotated, and the default board is unchanged.

* Classify captain holds from structured fields alone.

Bucket membership was decided by several independent expressions, and two of
them matched hold reason or body prose. That produced a recurring class of
defects: holds that fell through every bucket and vanished from the board, and
live decisions silently suppressed because their wording happened to contain a
marker word - a reason of "non-deferred release choice" matched DEFERRED and
disappeared.

Replace all of it with one total classifier over structured fields only:
hold_kind, state, hold_until, unresolved_blocker_ids, and the machine-written
hold-set timestamp. Every captain hold gets exactly one hold_bucket - blocked,
dated, aged, or live - so no hold can fall through and none can match two.
captain_actionable is exactly the live bucket, and the --all-decisions reveal
is a property of the bucket rather than a second filter.

No hold reason or body prose is matched anywhere in the projection, so wording
can no longer hide, reveal, or reclassify a decision. A hold that is superseded
or no longer required is closed through the hold lifecycle instead of lingering
as an open hold flagged by a keyword.

* no-mistakes(review): Preserve working captain holds across bucket surfaces

* no-mistakes(review): Reject pre-classifier secondmate summary caches

* no-mistakes(review): Preserve complete live hold summaries

* no-mistakes(review): Clarify working hold decision bucket semantics

* no-mistakes(review): Reveal bounded remote holds and preserve blocker notes

* no-mistakes(review): Make blocker overflow explicit in hold summaries

* no-mistakes(document): Correct captain-hold projection documentation

* no-mistakes(ci): Updated the stock macOS Bash CI snapshot expectation from 17 to 18 tests. Verified the suite under Bash 3.2.57: all 18 tests pass. `git diff --check` also passes

* no-mistakes(ci): Updated the stock macOS Bash CI expectation from 51 to 53 Bearings tests. Verified all 53 pass under Bash 3.2.57; git diff --check passes

* fix(pi): keep supervision outcome delivery responsive (#3767)

* fix(pi): deliver supervision outcomes off Pi's render thread

The supervision branch runs inside the captain's own Pi process, and Pi
runs extensions, their tools, and their event handlers on the single
JavaScript thread that also draws the TUI and reads the keyboard. Every
delivered outcome ran roughly five bash script invocations plus several
`ps` calls through spawnSync on that thread, so the TUI could not repaint
or echo a keystroke for the whole chain - the subsecond freeze the
captain saw every time a routine or captain-facing outcome arrived.

Convert the delivery path's subprocess calls to an awaited spawn behind a
serializing queue. lib/fm-async-exec.ts is the single owner of the
awaited-spawn replacement and returns the same capture shape and failure
verdicts spawnSync returned. Awaiting yields the thread, so what the
single thread used to guarantee for free is now an explicit queue: every
delivery, acknowledgement, and turn-boundary reconciliation runs as one
unit of it, preserving the durable append before anything visible, one
delivery at a time in sequence order, the read cursor advanced before the
next reader sees a row, and one ownership activation per generation.
Cancellation is preserved by the generation and lock-ownership rechecks
the awaits are placed around.

Two reads stay synchronous because Pi's own API is synchronous there, not
as an optimization: its bash spawn hook is typed as a plain function, and
the watcher reads offer.accepted the moment its dispatch event returns, so
a session that does not own the fleet lock must still refuse a wake
without waiting. Both walk the lock's process ancestry in full every time,
never cached, because reparenting and pid reuse can invalidate a
remembered chain and that answer decides ownership rather than hinting at
it. The store scripts and their durability contracts are unchanged.

Measured through the real fm_branch_report tool and real bin/ scripts with
a 1 ms interval timer, the largest block of the JS thread falls from 273
to 2.0 ms for a routine outcome, 286 to 2.0 ms for a captain outcome, and
134 to 1.9 ms for main's acknowledgement, against a 1.3-2.2 ms idle floor.
In a real Pi 0.82.0 TUI the worst keystroke echo while two outcomes arrive
falls from 676.9 ms to 36.8 ms, against a 22.6 ms extension-free floor.

Regressions: a delivery must leave the event loop running (zero timer
ticks before this change, in 250 ms), interleaved reports stay ordered and
exactly once, a session replaced mid-delivery neither loses nor duplicates
an outcome, and a failing store script surfaces without losing or doubling
one. The real-TUI half is an opt-in live guard that types into an isolated
Pi pane while outcomes are delivered and fails if echo leaves the class of
the same machine's own floor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013bzoWyr2EcJGBKuoUjVRSp

* no-mistakes(review): Revalidate ownership and bound asynchronous subprocess output

* no-mistakes(ci): Fixed CI defects: routine outcomes now persist a sequence-keyed delivery receipt before awaiting cursor advancement, preventing duplicate delivery after mark-read failure. Corrected the session-replacement test to exercise an actual asynchronous ps ancestry lookup. Targeted behavioral tests, strict Pi typecheck, ShellCheck, and diff checks pass. The full extension test remains locally blocked by an unrelated stock-render assertion under the installed Pi runtime. The no-mistakes attestation failure is external pipeline state (test was previously skipped), not a source defect

* fix(pi): keep the declined routine receipt out and skip the renderer case below its Pi floor

Four follow-ups on the same branch, plus one revert.

Revert the routine-delivery receipt a CI auto-fix round added. It introduced a
new persisted `fm-branch-routine-delivery` entry, written into the captain's
transcript for every routine note, to deduplicate a note whose cursor write
failed. That is a change to the delivery contract, which this task is not
authorized to make: the approved work is the asynchronous conversion with the
existing durability contract preserved. The ownership re-read and output
bounding from the review round are kept - both are genuine asynchronous
correctness, not contract changes - as is that round's use of a real parent pid
so the replacement regression traverses an actual ps subprocess.

Record the routine gap instead of closing it. A routine note is a plain
message with no sequence-keyed record, so a mark-read failure after delivery
makes the next reconciliation send it once more; a captain row cannot
duplicate that way because its visible entry is found by store sequence. That
asymmetry predates moving delivery off the render thread. It is now stated at
the call site and in the delivery-contract docs, tracked as
fm-pi-routine-delivery-idempotency-followup-r1, and pinned by a regression
that proves the routine note is re-delivered exactly once more and never
again, the captain entry stays single, and the store keeps both rows.

Give the stock-renderer case a Pi version floor. It compares the extension's
renderers against Pi's stock rendering, so its verdict only means anything
against the contract those renderers target: since 0.84.4 the stock renderer
no longer supplies an implicit reset at multiline boundaries and the extension
emits that reset itself, so an older installed Pi differs legitimately. It now
names the installed version and the floor and skips, while a package whose
version cannot be read at all still fails.

Make the responsiveness regression's second signal a fraction rather than a
millisecond budget. A loaded machine that deschedules the process inflates an
absolute stall budget into a false failure, but it inflates the delivery's own
wall time too, so requiring the worst stall to be a minority of that wall time
holds under load. Synchronous delivery sits near 1.0 there whatever the load,
and the tick-count signal still reads zero on it.

Replace the test-family mapping for the Pi extension libraries with per-script
targeting. Routing them to whole families - or leaving them unmapped, which
widens through the reference scan to each referencing suite's entire family -
selected dozens of suites with nothing to do with Pi and pulled an unrelated
flake into the run. The changed-file selection drops from 112 scripts to 61.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013bzoWyr2EcJGBKuoUjVRSp

* no-mistakes(document): Clarify asynchronous execution documentation

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix: avoid duplicate AGENTS.md governance for marked projects (#3763)

* fix(memory): honor explicit project maintenance guidance

* no-mistakes(test): Blocked by pre-existing Bash and Muse fixture failures

* no-mistakes(test): Remove accidentally tracked test attribution report

* no-mistakes(ci): Restricted the marker to the exact first line, preventing fenced examples from suppressing governance, and corrected the documentation. Regression failed before the fix; all 18 helper tests, focused ShellCheck, documentation validation, and diff checks pass. CI and Require no-mistakes report action_required with zero jobs executed; those external checks remain unresolved

* fix: protect primary checkout when spawning from linked homes (#3783)

* fix(spawn): refuse repository primary from linked spawning homes

Compare the resolved task git directory with the spawning repository's
common git directory before refreshing a fresh copy or relaunching a task.
This protects the primary even when the spawning project is a linked home.
Keep pooled copies accepted and preserve recorded work on relaunch.

Fixes #3741.

Verification for the pipeline PR body:
- Red on origin/main 1820316b66ac2c68e244dd04a02512859ee8c1f4 with the new
  regression and unchanged production code: bin/fm-test-run.sh
  tests/fm-spawn-pool-base-freshen.test.sh exited 1 with
  "linked spawning home accepted primary as a disposable copy".
- Green after the guard: the complete pool-base-freshen and control-relaunch
  suites passed through bin/fm-test-run.sh, covering primary and symlink
  refusal before fetch/reset, spawning-directory refusal, scout acceptance,
  and committed plus unfinished work preserved during linked-home relaunch.
- The worktree-settle suite passed on pristine main and the final branch.
  An earlier loaded-host run exceeded its five-second assertion (6s);
  the final retry passed without changing code or the assertion.
- Test fixture commits ran with GIT_CONFIG_COUNT=1,
  GIT_CONFIG_KEY_0=commit.gpgsign, GIT_CONFIG_VALUE_0=false.
- bin/fm-lint.sh and /bin/bash -n for all three changed scripts passed.

The upstream cwd-selection cause remains outside this change.

* no-mistakes(document): Clarify spawn isolation ownership and relaunch preservation

* fix(bin): stop reading an unanswered backend probe as a dead endpoint (#3785)

* fix: read a failed herdr CLI as unreachable, not a gone backend target

The no-run fallback in bin/fm-crew-state.sh collapsed every failed pane
capture into 'backend target gone', which downstream consumers treat as
positive death evidence - so a herdr CLI that errors or stalls under load
briefly scored dozens of live claims dead on a busy box. Only a successful
herdr answer proving the pane absent (fm_backend_agent_state's 'missing',
backed by pane get answering pane_not_found) may now read as gone; every
other verdict reports 'backend unreachable' with the endpoint state, which
is never positive death evidence. Adds a behavior test: an always-failing
fake herdr reads unknown/unreachable, never gone.

* test: pin the herdr suite's ambient home to a marker-free fixture

FM_HOME defaults to the suite's own root when unset, and any secondmate-
marked checkout (every treehouse crew home carries .fm-secondmate-home)
flips the default workspace label to 2ndmate-*, so the ambiguous-label
placement test found zero firstmate matches and fell into the create path
instead of refusing (expected exit 3, got 1) - deterministically green in
CI, deterministically red from a crew home. Export a marker-free ambient
FM_HOME fixture; per-test FM_HOME prefixes still override it.

* fix: classify herdr endpoint answers instead of every non-missing verdict

Review decision (firstmate, 2026-09-05): a failed pane capture is not
itself evidence of death, but neither is every non-missing classifier
verdict a failed answer. missing (pane get answered pane_not_found) and
dead (pane present, agent_not_found husk) keep gone-class text so a
stale-claim sweep may still reclaim them; an alive answer falls through
to the normal busy/state flow instead of being discarded when only the
heavy 200-line scrollback read failed; only when the cheap pane get /
agent get calls themselves fail to answer does the line read
'backend unreachable'. Adds the two missing cases: alive with a failed
scrollback read stays live, and a husk pane still reads gone.

* no-mistakes(review): route tmux through agent-state classifier; drop test stall

* no-mistakes(review): narrow inaccurate tmux socket and alive-arm fallback comments

* no-mistakes(document): document classifier-backed endpoint verdicts in crew-state contract

* fix(bin): preserve subshell lock ownership on Bash 3.2 (#3789)

* fix: distinguish subshell wake-lock owners on stock Bash

Restore distinct process ownership for issue #3743 using the existing PID helper, consistently across lock publication, reclaim, release, role checks, and bounded handoff.

The existing wake-queue regression fails on pristine upstream Bash 3.2 with rc=13. The complete suite now passes on Bash 3.2.57 and Bash 5.3.15, with added coverage for ownership when BASHPID is unset. Canonical lint and stock-Bash syntax checks pass.

* no-mistakes(document): Correct lock grace-period documentation

* no-mistakes(ci): Captain, fixed all 14 SC2031 false positives with nine ShellCheck source-boundary annotations across three tests. Full CI-mode lint and the complete wake-queue suite on stock Bash 3.2 passed. Runtime behavior is unchanged

* fix(bin): resolve captain holds and legacy teardowns on non-markdown backends (#3782)

* fix(bin): close legacy records on the Beads backend honestly

Two pre-Beads reads blocked honest closure of leftover records:

1. fm-captain-hold.sh complete/verify resolved attested legacy hold ids
   only against the live backend and the pre-collapse derived identity, so
   a home whose holds fm-hold-migration rehomed under fm- ids failed with
   an empty-name absence message (the resolve failure was swallowed by the
   command substitution feeding verify_hold_durable). Resolution now falls
  …
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