Skip to content

feat: sync Firstmate upstream capabilities and reliability fixes - #43

Merged
MrGTV-love merged 83 commits into
mainfrom
fm/fm-upstream-sync
Oct 8, 2026
Merged

MrGTV-love merged 83 commits into
mainfrom
fm/fm-upstream-sync

Conversation

@MrGTV-love

@MrGTV-love MrGTV-love commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Intent

Check the firstmate upstream repo and pull the latest firstmate.

Context: this firstmate is a fork (origin = MrGTV-love/firstmate) of the upstream kunchenguid/firstmate. At intake origin/main was 35 commits ahead of and 60 commits behind upstream/main (merge-base eb219c8) and had never been synced. The fork's own 35 commits are landed local changes that must be kept. Upstream contains overlapping fixes, for example f50e9cd (recognize titled Claude top rules) next to the fork's PR 37 (Claude composers with session-titled borders), 6a7b391 (start tasks from a named base branch), 42dd906 (escalate inbox instructions blocked by busy workers), 0f34fab and 5838f10 (macOS Bash 3.2 fixes).

What Changed

  • Integrate upstream and fork changes, adding named task base branches, per-project worker capacity limits, worker tool exclusions, a disposable live supervision lab, pipeline spend attribution, and startup growth checks.
  • Enable the supervision host by default for Claude and improve away/quiet posture handling, captain-hold lifecycle management, return reporting, and watcher recovery across harness adapters.
  • Harden steering-inbox delivery with bounded busy-worker escalation, remote job and reply handling, titled Claude composer detection, and macOS Bash 3.2 timeout handling; update associated skills, documentation, and regression coverage.

Risk Assessment

⚠️ Medium: The broad upstream merge changes multiple stateful runtime and platform-specific paths, so integration risk remains moderate despite no new reportable defects under the recorded review decisions.

Testing

Merge inspection and ancestry checks, live Claude composer/inbox scenarios, real named-base launches and review output, admission guards, Bash 3.2 timeout/shutdown checks, and the focused composer suite passed; setup isolation issues were corrected and cleaned up, terminal captures and HTML replays were preserved, and all labs were removed. Codex was quota-limited, so its launch/base state—not model completion—was validated. No broad suite, lint, CI, push, or pipeline-control phase ran.

  • Live validation: ✅ go - 12 of 12 scenarios driven live against the product
Scenario Result Live Evidence
A titled Claude composer distinguishes an empty input from an unsent draft ✅ pass live Observed real Claude composer verdicts; actual terminal captures claude-titled-empty.txt and claude-titled-draft.txt.
Pasting an unequal-width titled rule cannot make a nonempty Claude draft look empty ✅ pass live Actual Claude terminal replay: unequal-width pasted rule; focused composer regression suite.
A non-ASCII titled rule cannot make a nonempty Claude draft look empty ✅ pass live Actual Claude terminal replay: non-ASCII titled rule; observed real Claude composer verdicts.
Scout and ship tasks start from the selected remote feature branch ✅ pass live Named-base scout HEAD, content, metadata, and Codex limitation; named-base ship launch state.
Reviewing a named-base task shows its changes without including the base branch's changes ✅ pass live Public review diff excludes base-branch changes.
Missing or contradictory base selections refuse task launch ✅ pass live Missing and mismatched base refusals.
An instruction blocked by a busy worker escalates once after the bounded deferral count ✅ pass live Persistent busy deferrals and single escalation.
A real worker acts on the durable steering instruction and acknowledges it ✅ pass live Real worker action and inbox acknowledgement.
Stopping a Bash 3.2 watcher interrupts a blocked pane capture promptly ✅ pass live Bash 3.2 watcher exits promptly during blocked capture.
The Bash 3.2 timeout fallback enforces deadlines and preserves ordinary command failures ✅ pass live Bash 3.2 timeout deadline and exit propagation.
An occupied project capacity defers dispatch without losing the queued task ✅ pass live Capacity deferral preserves queued backlog.
A harness unable to hide requested tools refuses launch instead of ignoring the exclusions ✅ pass live Unsupported tool exclusions refuse launch.
Evidence: Live validation observations and limitations

Source: Live validation observations and limitations

Firstmate upstream-sync live validation

Isolation: marked lab homes under the run worktree; named private fm-lab tmux socket; 120 columns by 40 rows. No default tmux or Herdr fleet operations. Initial long socket path failed, so the repository's owned short-socket helper supplied an ephemeral socket and its teardown contract.

Claude Code v2.1.294: the actual session-titled empty composer classified empty; a plain unsent draft classified pending. Independent pasted unequal-width and non-ASCII titled-rule drafts classified pending and unknown respectively, never empty. The surrounding real TUI can prove the unequal-width nested draft pending; exact rejected-region unknown cases also passed the focused composer suite under stock /bin/bash 3.2.57.

Named-base tasks: real fm-brief/fm-tasks-axi/fm-spawn launched a scout and a direct-PR ship at origin/feature/hub (4edbe9a5db884a620ab175257b3967fe44ca3c3b), retaining feature-only.txt and recording base_branch=feature/hub. Ship checkout starts detached; its immutable recorded branch is fm/sync-base-ship-live. Codex v0.160.0's existing account hit its usage limit, so no Codex model completion is claimed. The test driver created and committed a controlled task-only change in the disposable ship checkout; real fm-review-diff printed diff base: origin/feature/hub and only task-change.txt, excluding the base branch's feature-only.txt.

Adversarial admission: a missing remote base and a disagreement between flag and generated brief refused without metadata. With project-capacity notes 1 and occupied task records, spawn exited 75, named the holders, published no new record, and left the backlog queued. A nonempty crew-exclude-tools list with unsupported Codex refused rather than silently allowing the tool.

Inbox: real Claude lifecycle hooks called Firstmate's busy-state interface. Real fm-send persisted 001.msg and delivered the self-describing doorbell. Three independent watcher checks yielded a persisted first busy deferral, one stuck-busy escalation at the second deferral, and no duplicate escalation on the third check. After completing its synthetic hash benchmark, the actual Claude worker created inbox-acted.txt containing INBOX_DELIVERED and moved 001.msg into handled/.

macOS: the actual /bin/bash 3.2 watcher entered a real tmux capture delayed 60s by a disposable transport shim, then exited on TERM in 0.264s. The actual Bash timeout fallback terminated a 60s sleep at its 1s deadline with exit 124, never executed its following echo, and preserved ordinary exit 7; total driver duration was 1.616s.

Targeted automation: /bin/bash tests/fm-composer-lib.test.sh completed successfully, including restored pasted-title and non-ASCII refused-region coverage. The first run hit the driver's 120s timeout; the longer isolated run completed in 462.63s. No complete repository test suite, linters, formatters, CI, push, or pipeline-control commands ran.

Evidence: text captures plus HTML renderings of the actual terminal grid; these are terminal replays, not native desktop screenshots.
Evidence: Observed real Claude composer verdicts

Source: Observed real Claude composer verdicts

Claude Code v2.1.294, real 120x40 private pane.
Empty titled composer: empty
Unsent plain text: pending
Independent pasted-rule drafts within actual titled Claude composer:
width: pending
unicode: unknown
Exact rejected-region unknown classifications: exercised separately by /bin/bash tests/fm-composer-lib.test.sh.
Evidence: Actual Claude terminal replay: unequal-width pasted rule

Source: Actual Claude terminal replay: unequal-width pasted rule

<!doctype html><meta charset="utf-8"><title>Claude width draft</title><style>body{background:#141414;color:#eee}pre{font:15px monospace;line-height:1.3}</style><pre> ▐▛███▛█   Claude Code v2.1.294
▝▜██████▀  Sonnet 5.5 · Claude Max
 ▝▝   ▝▝   ~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B


❯ Isolated live validation: use Bash to synchronously run python3 -c &quot;import time,hashlib; end=time.monotonic()+35; n=0
  while time.monotonic()&lt;end: hashlib.sha256(str(n).encode()).digest(); n+=1
  print(n)&quot; with a 60000ms timeout and run_in_background=false. This computes a bounded synthetic hash benchmark,
  writes no files, and needs no repository access. Reply with the hash count. Do not touch fleet state or source.

⏺ Bash(python3 -c &quot;import time,hashlib; end=time.monotonic()+35; n=0
      while time.monotonic()&lt;end: hashlib.sha256(str(n).encode()).digest(); n+=1…)
  ⎿  54832736
  ⎿  (timeout 1m)

❯ : Firstmate instruction waiting: list &quot;$FM_TASK_INBOX&quot;/*.msg in your &#x27;sync-claude.inbox&#x27; steering inbox, read and act
  on each in numeric order, then mv each into its handled/.

⏺ Hash count: 54832736.

  Now the inbox instruction.

  Read 1 file, listed 1 directory (ctrl+o to expand)

⏺ Bash(B=~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-mistakes/phase-live/h
      ome
      mkdir -p &quot;$B/data&quot; &quot;$FM_TASK_INBOX/handled&quot;…)
  ⎿  001.msg
     INBOX_DELIVERED

⏺ Inbox message 001 is handled: I wrote inbox-acted.txt containing INBOX_DELIVERED and moved the message into handled/.
  The hash count from the benchmark is 54832736.

✻ Cooked for 53s · done 8:04 AM

──────────────────────────────────────────────────────────────────────────────────────────── Firstmate sync validation ─
❯ [Pasted text #1 +4 lines]
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  ⏵⏵ bypass permissions on (shift+tab to cycle)

</pre>
Evidence: Actual Claude terminal replay: non-ASCII titled rule

Source: Actual Claude terminal replay: non-ASCII titled rule

<!doctype html><meta charset="utf-8"><title>Claude unicode draft</title><style>body{background:#141414;color:#eee}pre{font:15px monospace;line-height:1.3}</style><pre>can&#x27;t find window: 0
</pre>
Evidence: Named-base scout HEAD, content, metadata, and Codex limitation

Source: Named-base scout HEAD, content, metadata, and Codex limitation

Expected origin/feature/hub=4edbe9a5db884a620ab175257b3967fe44ca3c3b
Actual spawned worktree HEAD=4edbe9a5db884a620ab175257b3967fe44ca3c3b
feature-only.txt:
only on feature/hub

Task metadata:
window=primary:fm-sync-base-scout-live
endpoint_task_id=sync-base-scout-live
worktree=~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-mistakes/phase-live/pool/.treehouse/notes-06c218/1/notes
project=~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-mistakes/phase-live/home/projects/notes
harness=codex
kind=scout
tasktmp=/tmp/fm-sync-base-scout-live
base_branch=feature/hub
model=default
effort=default
skill_selection=unavailable
skill_selection_reason=no TypeSafe or OpenRouter key
spawn_gen=s1791464360.49188.22291

Actual Codex pane (model execution limit):

  # Definition of done
  Write your findings to `~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-
  mistakes/phase-live/home/data/sync-base-scout-live/report.md`.
  The report must stand alone: what you did, what you found, the evidence (commands run, output, file:line references),
  and what you recommend.
  If your deliverable is a visual artifact the captain will review and iterate on, use the lavish-axi rule: arm your
  board with bin/fm-procevent-lavish.sh arm <artifact.html> --for <task-id>; never run lavish-axi poll yourself. Re-arm
  with the reply after each nonterminal round to acknowledge it, route the board feedback through your steering inbox,
  write needs-decision [key=board-review] with the live board URL when the captain owes a decision, and stop at
  session_ended or an empty End without re-arming - acknowledge that final round with bin/fm-procevent.sh handled
  <source-id> <sequence> to conclude and retire your board.
  Before reporting done, read and follow `~/.no-mistakes/
  worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.agents/skills/captain-hold-lifecycle/SKILL.md` and pass its shared
  completion gate for the report and any visual review.
  When the report is complete, append `done [at=<epoch>]: {one-line conclusion}` to the status file and stop.
  If your findings reveal work that should ship (e.g. you reproduced a bug and the fix is clear), say so in the report;
  firstmate may promote this task in place, and you would then receive mode-specific ship instructions as a follow-up
  message.

  # Skill selection

  Existing mandatory skill triggers in these instructions and your skill index still apply first and unchanged.
  Skill selection was unavailable for this task (no TypeSafe or OpenRouter key). This does not mean no skill applies:
  check your skill index for skills that fit this task before starting work.


■ You’ve hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at
Oct 13th, 2026 10:57 PM.


  Approaching rate limits
  Switch to gpt-6-luna for lower credit usage?


› 1. Switch to gpt-6-luna                   Fast and affordable model for easier tasks.
  2. Keep current model
  3. Keep current model (never show again)  Hide future rate limit reminders about switching models

  enter select · esc back
Evidence: Named-base ship launch state

Source: Named-base ship launch state

fm-gate-refuse: gate agent lifecycle permitted only against lab home ~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-mistakes/phase-live/home
WARNING: watcher still down (same stale episode; last beat: 751s ago, grace 300s) - full banner already printed this episode.
WARNING: queued wakes pending - drain them with bin/fm-wake-drain.sh before anything else.
warn: no registry at ~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-mistakes/phase-live/home/data/projects.md; defaulting notes to no-mistakes off
notice: sync-base-ship-live ships mode=direct-PR while the standing posture for notes is no-mistakes - less rigor than the captain's standing posture; proceed only on a current explicit captain instruction or an intake judgment you can state
skill selection for sync-base-ship-live: unavailable - no TypeSafe or OpenRouter key
spawned sync-base-ship-live harness=codex kind=ship mode=direct-PR yolo=on window=primary:fm-sync-base-ship-live worktree=~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-mistakes/phase-live/pool/.treehouse/notes-06c218/2/notes

Expected remote base=4edbe9a5db884a620ab175257b3967fe44ca3c3b
Spawned HEAD=4edbe9a5db884a620ab175257b3967fe44ca3c3b
Initial checkout=detached (worker creates its recorded ship branch)
Recorded branch=fm/sync-base-ship-live

window=primary:fm-sync-base-ship-live
endpoint_task_id=sync-base-ship-live
worktree=~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-mistakes/phase-live/pool/.treehouse/notes-06c218/2/notes
project=~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-mistakes/phase-live/home/projects/notes
harness=codex
kind=ship
mode=direct-PR
yolo=on
branch=fm/sync-base-ship-live
tasktmp=/tmp/fm-sync-base-ship-live
base_branch=feature/hub
model=default
effort=default
skill_selection=unavailable
skill_selection_reason=no TypeSafe or OpenRouter key
spawn_gen=s1791464993.14773.26600
Evidence: Public review diff excludes base-branch changes

Source: Public review diff excludes base-branch changes

$ bin/fm-review-diff.sh sync-base-ship-live
WARNING: watcher still down (same stale episode; last beat: 788s ago, grace 300s) - full banner already printed this episode.
WARNING: queued wakes pending - drain them with bin/fm-wake-drain.sh before anything else.
diff base: origin/feature/hub
 task-change.txt | 1 +
 1 file changed, 1 insertion(+)

diff --git a/task-change.txt b/task-change.txt
new file mode 100644
index 0000000..b470bef
--- /dev/null
+++ b/task-change.txt
@@ -0,0 +1 @@
+task-only delta
Evidence: Missing and mismatched base refusals

Source: Missing and mismatched base refusals

$ fm-spawn.sh sync-base-missing notes --scout --base-branch feature/missing
fm-gate-refuse: gate agent lifecycle permitted only against lab home ~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-mistakes/phase-live/home
WARNING: watcher still down (same stale episode; last beat: 149s ago, grace 300s) - full banner already printed this episode.
warn: no registry at ~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-mistakes/phase-live/home/data/projects.md; defaulting notes to no-mistakes off
fatal: couldn't find remote ref refs/heads/feature/missing
error: could not fetch 'origin/feature/missing' for pooled worktree '~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-mistakes/phase-live/pool/.treehouse/notes-06c218/2/notes'; refusing to launch from a potentially stale base

exit=1; published_metadata=false

$ fm-spawn.sh sync-base-mismatch notes --scout --base-branch main
fm-gate-refuse: gate agent lifecycle permitted only against lab home ~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-mistakes/phase-live/home
WARNING: watcher still down (same stale episode; last beat: 171s ago, grace 300s) - full banner already printed this episode.
warn: no registry at ~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-mistakes/phase-live/home/data/projects.md; defaulting notes to no-mistakes off
error: ~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-mistakes/phase-live/home/data/sync-base-mismatch/brief.md must record Base branch: main and no other Base branch line to spawn with --base-branch main; scaffold it with bin/fm-brief.sh --base-branch main

exit=1; published_metadata=false
Evidence: Persistent busy deferrals and single escalation

Source: Persistent busy deferrals and single escalation

poll 1: exit=0

busy-state: 001.msg	1

poll 2: exit=0
stale: primary:fm-sync-claude (unread firstmate instruction: stuck-busy after 2 consecutive busy-deferred due doorbells; ~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-mistakes/phase-live/home/state/sync-claude.inbox/001.msg stays unhandled and no doorbell was typed; inspect the worker)

busy-state: 001.msg	2

wake-queue:
1791464618	2	stale	primary:fm-sync-claude	stale: primary:fm-sync-claude (unread firstmate instruction: stuck-busy after 2 consecutive busy-deferred due doorbells; ~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-mistakes/phase-live/home/state/sync-claude.inbox/001.msg stays unhandled and no doorbell was typed; inspect the worker)

poll 3: exit=0

busy-state: 001.msg	2

wake-queue:
1791464618	2	stale	primary:fm-sync-claude	stale: primary:fm-sync-claude (unread firstmate instruction: stuck-busy after 2 consecutive busy-deferred due doorbells; ~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-mistakes/phase-live/home/state/sync-claude.inbox/001.msg stays unhandled and no doorbell was typed; inspect the worker)
Evidence: Real worker action and inbox acknowledgement

Source: Real worker action and inbox acknowledgement

Acted file:
INBOX_DELIVERED

Handled record:
schema=fm-task-inbox.v1
at=2026-10-08T13:03:35Z
--
Isolated inbox delivery check: create ~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-mistakes/phase-live/home/data/inbox-acted.txt containing INBOX_DELIVERED, then acknowledge this message by moving it into handled as instructed. Do not invoke fleet lifecycle, pipelines, or edit any source. Reply one short line.

Pane:
 ▐▛███▛█   Claude Code v2.1.294
▝▜██████▀  Sonnet 5.5 · Claude Max
 ▝▝   ▝▝   ~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B


❯ Isolated live validation: use Bash to synchronously run python3 -c "import time,hashlib; end=time.monotonic()+35; n=0
  while time.monotonic()<end: hashlib.sha256(str(n).encode()).digest(); n+=1
  print(n)" with a 60000ms timeout and run_in_background=false. This computes a bounded synthetic hash benchmark,
  writes no files, and needs no repository access. Reply with the hash count. Do not touch fleet state or source.

⏺ Bash(python3 -c "import time,hashlib; end=time.monotonic()+35; n=0
      while time.monotonic()<end: hashlib.sha256(str(n).encode()).digest(); n+=1…)
  ⎿  54832736
  ⎿  (timeout 1m)

❯ : Firstmate instruction waiting: list "$FM_TASK_INBOX"/*.msg in your 'sync-claude.inbox' steering inbox, read and act
  on each in numeric order, then mv each into its handled/.

⏺ Hash count: 54832736.

  Now the inbox instruction.

  Read 1 file, listed 1 directory (ctrl+o to expand)

⏺ Bash(B=~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-mistakes/phase-live/h
      ome
      mkdir -p "$B/data" "$FM_TASK_INBOX/handled"…)
  ⎿  001.msg
     INBOX_DELIVERED

⏺ Inbox message 001 is handled: I wrote inbox-acted.txt containing INBOX_DELIVERED and moved the message into handled/.
  The hash count from the benchmark is 54832736.

✻ Cooked for 53s · done 8:04 AM

──────────────────────────────────────────────────────────────────────────────────────────── Firstmate sync validation ─
❯ 
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  ⏵⏵ bypass permissions on (shift+tab to cycle) · ← for agents
Evidence: Bash 3.2 watcher exits promptly during blocked capture

Source: Bash 3.2 watcher exits promptly during blocked capture

/bin/bash 3.2.57 bin/fm-watch.sh
Actual private tmux capture delayed 60s by an isolated transport shim; entered=True
TERM-to-exit=0.264s, exit=-15
Evidence: Bash 3.2 timeout deadline and exit propagation

Source: Bash 3.2 timeout deadline and exit propagation

/bin/bash 3.2.57; FM_TIMEOUT_MECHANISM_OVERRIDE=bash
command-started
deadline exit=124
ordinary exit=7
elapsed=1.616s
Evidence: Capacity deferral preserves queued backlog

Source: Capacity deferral preserves queued backlog

$ fm-spawn.sh sync-capacity-deferred notes --scout --base-branch feature/hub --harness codex
fm-gate-refuse: gate agent lifecycle permitted only against lab home ~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-mistakes/phase-live/home
WARNING: watcher still down (same stale episode; last beat: 593s ago, grace 300s) - full banner already printed this episode.
WARNING: queued wakes pending - drain them with bin/fm-wake-drain.sh before anything else.
deferred: project notes admits 1 worker(s) at once on this machine (~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-mistakes/phase-live/home/config/project-capacity) and 2 already hold a place (sync-base-scout-live, sync-base-scout); task sync-capacity-deferred was not launched and its backlog item stays queued - dispatch it again once one of them records its ready PR or is cleaned up
exit=75

$ fm-tasks-axi.sh show sync-capacity-deferred
task:
  id: sync-capacity-deferred
  title: Validate project capacity
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: scout
  repo: notes
  priority: "-"
  created: 2026-10-08
  closed: "-"
  deps: none
  links: none
  body: ""
Evidence: Unsupported tool exclusions refuse launch

Source: Unsupported tool exclusions refuse launch

$ fm-spawn.sh sync-capacity-deferred notes --scout --harness codex
fm-gate-refuse: gate agent lifecycle permitted only against lab home ~/.no-mistakes/worktrees/32d18ed9638d/01M4D1WYWEEBA66SMGDFKXD52B/.no-mistakes/phase-live/home
WARNING: watcher still down (same stale episode; last beat: 595s ago, grace 300s) - full banner already printed this episode.
WARNING: queued wakes pending - drain them with bin/fm-wake-drain.sh before anything else.
error: config/crew-exclude-tools lists tools to hide, but the codex runtime cannot hide tools, so this launch is refused rather than running with them available; empty the file or launch a runtime that supports exclusion (pi, pi-signed)
exit=1; published_metadata=False
Evidence: Disposable lab teardown

Source: Disposable lab teardown

Private fm-lab tmux server: stopped.
Owned socket directory: removed by bin/fm-lab-home.sh teardown.
Disposable project, homes, credential copy, transport shim, pool, and test scratch: removed.
Firstmate CLI tasktmp scaffolds for this run: removed.
Evidence retained only in the assigned evidence directory.

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - medium risk

✅ No issues found.

✅ No issues found.

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 11 of 11 scenarios driven live against the product
Scenario Result Live Evidence
Use an ASCII-titled Claude composer: empty input is recognized, single-line and multiline drafts are preserved, and exit succeeds only after clearing them ✅ pass live Disposable adaptation of tests/fm-claude-titled-composer-live-e2e.test.sh; claude-adversarial-live.log
Paste an unequal-width titled rule into a nonempty Claude draft: classification remains unknown and exit refuses without typing ✅ pass live Real Claude pasted titled-rule draft; adversarial composer classification and production exit refusals
Rename a Claude session to Café and type an unsent draft: classification remains unknown and exit leaves the draft intact ✅ pass live Real Claude Unicode-title draft; adversarial composer classification and production exit refusals
Steer an idle titled Claude worker through fm-send: the real worker handles the durable inbox message and moves it into handled ✅ pass live Adapted titled-composer E2E guard with FM_CLAUDE_TITLED_COMPOSER_LIVE_SEND=1 asserted the real handled/*.msg acknowledgement; claude-adversarial-live.log
Start a scout from feature/hub: its real Treehouse worktree matches origin/feature/hub and the worker reports feature-only content ✅ pass live Named-base spawn, worker report, and busy-inbox escalation
Request a base that disagrees with the brief: spawn refuses without publishing worker metadata ✅ pass live Named-base spawn, worker report, and busy-inbox escalation
Request a nonexistent origin base: spawn refuses rather than falling back to the default branch ✅ pass live Named-base spawn, worker report, and busy-inbox escalation
Scaffold a direct-PR ship for feature/hub: generated setup and PR instructions target feature/hub rather than the repository default ✅ pass live Generated named-base ship delivery contract
Send a steer while a real Claude worker is running a foreground command: the watcher preserves the unread record and escalates after two busy deferrals without a retry doorbell ✅ pass live Named-base spawn, worker report, and busy-inbox escalation; real busy Claude worker with queued steer
Run bounded commands under stock Bash 3.2: completed commands retain their exit status and an overlong command terminates with timeout status ✅ pass live Bash 3.2 watchdog completion and timeout: exit 7 preserved; bounded sleep returned 124
Stop a Bash 3.2 watcher during stalled live pane I/O: SIGTERM exits promptly and leaves no capture descendants ✅ pass live Bash 3.2 watcher TERM and capture-group cleanup: exit 143 after one second; both recorded descendants no longer existed
  • /bin/bash tests/fm-composer-lib.test.sh: the initial 120-second attempt timed out; the subsequent 600-second run completed successfully, including unequal-width and non-ASCII titled-rule regressions.
  • /bin/bash tests/.nm-claude-live.test.sh: disposable adaptation of the existing Claude titled-composer E2E guard, using a marked lab home and FM_CLAUDE_TITLED_COMPOSER_LIVE_SEND=1; exercised empty/pending classification, multiline drafts, inbox acknowledgement, and guarded exit.
  • /bin/bash tests/.nm-claude-adversarial.test.sh: drove actual pasted titled-rule and Unicode-session-title drafts through Claude, the production Herdr adapter, and fm-control.sh; repeated without model turns to capture the exact terminal surfaces.
  • /bin/bash tests/.nm-spawn-busy.test.sh: exercised fm-brief.sh and fm-spawn.sh --scout --harness claude --base-branch feature/hub --backend herdr against a disposable Git origin and real Treehouse pool; also attempted mismatched and nonexistent bases.
  • fm-send.sh based &#39;After your current work, acknowledge this durable live instruction.&#39;, followed by fm-watch.sh with FM_TASK_INBOX_GRACE_SECS=1 and FM_TASK_INBOX_BUSY_MAX=2; drained and acknowledged the preceding scout-report wake through fm-wake-drain.sh before rearming.
  • /bin/bash bin/fm-brief.sh ship-base probe --mode direct-PR --base-branch feature/hub --herdr-lab: inspected the generated public Setup and delivery contract.
  • /bin/bash -c &#39;. bin/fm-timeout-lib.sh; fm_exec_timed 5 1 /bin/bash -c ...&#39; and the corresponding one-second timeout scenario: observed exit-status preservation and bounded termination.
  • /bin/bash tests/.nm-watcher-term.test.sh: ran the real watcher against a real Herdr pane, deliberately held its transport read, sent SIGTERM, and verified prompt exit and capture-descendant cleanup.
  • All Herdr scenarios used named non-default sessions through bin/fm-herdr-lab.sh; teardown completed and all temporary drivers and worktree fixtures were removed.

✅ No issues found.

  • Live validation: ✅ go - 8 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Exit an empty session-titled Claude composer without deleting its endpoint ✅ pass live Claude live transcript: composer=empty, verified stopped worker, preserved endpoint.
Attempt exit with an ordinary unsent draft and preserve the draft ✅ pass live Claude live transcript and claude-plain-after.txt: composer=pending and exit refused without stopping the agent.
Paste an unequal-width titled rule and trailing prompt glyph into a draft ✅ pass live Claude live transcript and rendered terminal captures: composer=unknown and exit refused; focused comparison also reproduced the pre-fix unsafe verdict.
Paste a non-ASCII titled rule and trailing prompt glyph into a draft ✅ pass live Claude live transcript and rendered terminal captures: Café rule classified unknown and exit refused.
Send an instruction to an idle Claude worker and observe action plus acknowledgement ✅ pass live Claude live transcript: requested file created and 001.msg moved into handled/ by the real worker.
Leave an inbox instruction unhandled during foreground work and receive a bounded busy escalation ✅ pass live Native working-state JSON and durable wake queue: stuck-busy after two consecutive deferrals while the foreground command remained running, with no watcher doorbell typed.
Generate a named-base brief on Bash 3.2 and start a real scout from feature/hub ✅ pass live Named-base transcript, generated brief, and metadata: real Treehouse worker HEAD matched feature/hub and contained its branch-only file.
Request a base branch that conflicts with the generated brief ✅ pass live Named-base transcript: explicit Base branch mismatch refusal and no bad-base metadata publication.
Have the newly spawned scout perform model work and report its starting point ⏸️ untested no Real spawned Claude attempts could not complete a model turn. Tried the normal login copied into a disposable store, canonical and explicit isolated launch commands, restored login-shell proxy setting…
  • /bin/bash tests/fm-composer-lib.test.sh — reached the R1 regression checks successfully, then exceeded the 300-second command deadline; not reported as a complete suite pass.
  • /bin/bash .sync-composer-regression.sh .sync-before-composer.sh and /bin/bash .sync-composer-regression.sh bin/fm-composer-lib.sh — focused executable comparison reproduced unsafe pre-fix empty verdicts and confirmed post-fix unknown verdicts across adapter profiles and UTF-8/C locales.
  • bash .sync-live-validation.sh — drove installed Claude in guarded, non-default Herdr labs through titled-composer classification, draft refusal, inbox action/acknowledgement, busy-worker escalation, and idle exit.
  • FM_TASK_INBOX_GRACE_SECS=0 FM_TASK_INBOX_BUSY_MAX=2 FM_POLL=1 bin/fm-watch.sh — observed a durable stuck-busy wake while Claude's foreground sleep remained running.
  • /bin/bash bin/fm-brief.sh named-base baseproject --scout --base-branch feature/hub --herdr-lab — generated the brief using macOS Bash 3.2.
  • bin/fm-spawn.sh bad-base &#34;$BASE_PROJECT&#34; --scout --base-branch main --harness claude --backend herdr — refused a conflicting brief without publishing task metadata.
  • ONLY_NAMED=1 bash .sync-live-validation.sh — exercised real Treehouse allocation and Herdr spawning, compared worker HEAD with feature/hub, and attempted model work using isolated credentials, restored proxy routing, and workspace-local certificate bundles.
  • git merge-base --is-ancestor 4fb9c401ab6c053d8d38828099b3e160eef5937a 6ee6f8d31b6adfd7449740961da82f18d8a356d3 — confirmed preservation of the base history.
  • Captured visible terminal grids, native agent state, wake-queue records, generated brief, and task metadata; rendered terminal captures as HTML. Tore down owned Herdr labs and removed disposable drivers and fixtures.

✅ No issues found.

  • Live validation: ✅ go - 12 of 12 scenarios driven live against the product
Scenario Result Live Evidence
A titled Claude composer distinguishes an empty input from an unsent draft ✅ pass live Observed real Claude composer verdicts; actual terminal captures claude-titled-empty.txt and claude-titled-draft.txt.
Pasting an unequal-width titled rule cannot make a nonempty Claude draft look empty ✅ pass live Actual Claude terminal replay: unequal-width pasted rule; focused composer regression suite.
A non-ASCII titled rule cannot make a nonempty Claude draft look empty ✅ pass live Actual Claude terminal replay: non-ASCII titled rule; observed real Claude composer verdicts.
Scout and ship tasks start from the selected remote feature branch ✅ pass live Named-base scout HEAD, content, metadata, and Codex limitation; named-base ship launch state.
Reviewing a named-base task shows its changes without including the base branch's changes ✅ pass live Public review diff excludes base-branch changes.
Missing or contradictory base selections refuse task launch ✅ pass live Missing and mismatched base refusals.
An instruction blocked by a busy worker escalates once after the bounded deferral count ✅ pass live Persistent busy deferrals and single escalation.
A real worker acts on the durable steering instruction and acknowledges it ✅ pass live Real worker action and inbox acknowledgement.
Stopping a Bash 3.2 watcher interrupts a blocked pane capture promptly ✅ pass live Bash 3.2 watcher exits promptly during blocked capture.
The Bash 3.2 timeout fallback enforces deadlines and preserves ordinary command failures ✅ pass live Bash 3.2 timeout deadline and exit propagation.
An occupied project capacity defers dispatch without losing the queued task ✅ pass live Capacity deferral preserves queued backlog.
A harness unable to hide requested tools refuses launch instead of ignoring the exclusions ✅ pass live Unsupported tool exclusions refuse launch.
  • git diff --stat 32fbe8edbbd9d7c75990c3c259b7fb4fbf80de6c dbe79e81b258573cb848f9a49b12efb4bcc2eaaf and the corresponding commit log
  • git merge-base --is-ancestor &lt;commit&gt; dbe79e81b258573cb848f9a49b12efb4bcc2eaaf for the fork baseline and upstream commits f50e9cdb, 6a7b3919, 42dd906d, 0f34fab4, and 5838f105
  • bin/fm-lab-home.sh create &lt;lab-home&gt; and bin/fm-lab-home.sh tmux-dir &lt;lab-home&gt;; launched real Claude Code in a private 120×40 tmux -L fm-lab session
  • Typed and pasted independent drafts into real Claude, captured its terminal grid, and ran fm_tmux_composer_state primary:fm-sync-claude
  • env FM_TEST_SKIP_ORPHAN_REAP=1 TMPDIR=&#34;$PWD/.no-mistakes/phase-live&#34; /bin/bash tests/fm-composer-lib.test.sh completed successfully; the initial driver allowance of 120 seconds was insufficient
  • bin/fm-tasks-axi.sh add, /bin/bash bin/fm-brief.sh, and bin/fm-spawn.sh launched isolated scout and direct-PR ship tasks with --base-branch feature/hub
  • Inspected spawned worktree HEAD, feature-branch content, and persisted task metadata; drove missing-base and brief/flag disagreement refusals through bin/fm-spawn.sh
  • bin/fm-review-diff.sh sync-base-ship-live against a controlled task-only commit in the disposable ship worktree
  • bin/fm-send.sh sync-claude &lt;instruction&gt; against real Claude; actual Claude lifecycle hooks published Firstmate busy events
  • Three fresh /bin/bash processes sourced bin/fm-watch.sh and ran inbox_steer_check primary:fm-sync-claude sync-claude against the real busy worker
  • Observed the real Claude worker create inbox-acted.txt containing INBOX_DELIVERED and move its message into handled/
  • Launched /bin/bash bin/fm-watch.sh, delayed its actual tmux capture through a disposable transport shim, and sent TERM after capture began
  • Executed fm_run_timed under /bin/bash 3.2.57 with FM_TIMEOUT_MECHANISM_OVERRIDE=bash, verifying deadline exit 124 and ordinary exit 7
  • Drove project-capacity deferral and unsupported tool-exclusion refusal through the real bin/fm-spawn.sh
  • Stopped the private tmux server, ran bin/fm-lab-home.sh teardown, and removed disposable homes, projects, pools, credential copies, transport shims, and test scaffolds
⚠️ **Document** - 1 info

✅ No issues found.

✅ No issues found.

  • ℹ️ tests/fm-remote-delta-read.test.sh:11 - Comments at lines 11–14 and 54–55 incorrectly claim unchanged logs never execute snapshot capture. The reader always captures the first sample and captures every sample when timestamps lack subsecond precision; the assertion at lines 76–78 already acknowledges the first capture. Left unchanged because this phase explicitly prohibits test changes.
⚠️ **Lint** - 0 issues

✅ No issues found.

  • ⚠️ linter found issues (exit code 1)

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

✅ No issues found.

✅ No issues found.

kunchenguid and others added 30 commits September 28, 2026 17:19
…6064)

* fix(bin): read a live quiet record as a present captain at the host and watcher

A quiet record left without its daemon (a quiet start that never ran or was
interrupted) was read as away by the supervision host, so it parked a present
captain's main and held captain outcomes for a return that never comes, and
the watcher and daemon silenced captain-held rechecks on record presence.

The host's posture checks, the watcher's and daemon's captain-held silencing,
and the host's outcome path (branch report, drain BRANCH OUTCOMES, relocated
branch authority, the owners' away wake note, and the Codex checkpoint bound)
now ask the record owner's away-or-quiet reading, so only an away record is
away. A live away record keeps today's behavior.

* no-mistakes(document): Correct quiet-record documentation and supervision guidance

* no-mistakes(document): Clarify quiet-record posture and captain-held rechecks

* no-mistakes(document): Clarify quiet-record posture in documentation
…kunchenguid#6043)

* fix(bin): name an in-window engine latch in the return brief and drop the false handling GAP line

The away return brief said nothing had failed after the supervision host
latched on engine errors during the window, and printed a GAP: watcher
downtime line whenever a wake was merely being handled or queued at return.

The failures section now reads the host ledger and latch record and names
the latch time, the window's engine-error count, and whether the session
is still paused or recovered. An open recovery episode is reported as
information, and as a gap only when a queued episode outlived the return
grace or the marker cannot be read.

* no-mistakes(review): Fix latch trip time, drop marker-age grace, bound error count

* no-mistakes(review): Report paused latch without ledger trip row; bound errors

* no-mistakes(review): Never report a failed probe's latch row as trip time

* no-mistakes(review): Only a retained trip row marks a pre-window latch

* no-mistakes(document): Clarify return-brief latch and watcher-gap documentation

* no-mistakes(ci): Fixed Lint 1 by marking the shared cooldown constant as used by sourcing scripts. The repository lint command and diff check pass; the return test run was stopped by a 180-second timeout after its completed cases passed

* no-mistakes(ci): Fixed the return brief so the trip time and error count come from the same initial latch row, and ledger rows before the current session’s lock boundary cannot affect its latch report. Added real-script regressions for both findings. The return test suite, repository lint, and diff check pass

* no-mistakes(ci): Fixed the return brief’s restart cutoff so it retains in-window failures, prints one line per initial-trip row, and omits zero-error count wording. Added real-script restart regressions. The return test suite, ShellCheck, and diff check pass

* no-mistakes(ci): Fixed the return brief so a recorded trip followed by recovery stays recovered, while a later pause with a lost trip append gets a separate “trip time unavailable” line. Added a real-script regression that failed before the fix. The return test suite, ShellCheck, syntax checks, and diff check pass

* no-mistakes(ci): Fixed the false second latch during recovery. A real-script regression failed before the fix and passes now; the lost-second-trip test still passes. The return test suite, ShellCheck, syntax checks, and diff check pass
* fix(calm): name the Claude Code Calm plugin fm so supervision notes read "fm: "

Claude Code labels every mod transcript line with the plugin name, so the
notes rendered as "firstmate-calm: ⚓ ...". Rename the plugin to fm, update
the live guard to assert the fm: label, and document the one-time replay for
sessions resumed across the rename.

* no-mistakes(document): Clarify Calm plugin rename in documentation
…#6037)

* feat(bin): add fm-live-lab.sh, a one-command live supervision lab builder

* fix(bin): exact lab windows, per-lab task ids, self-safe teardown

* fix(bin): target lab windows by id, stop lab descendants, add readiness tests

* fix(bin): keep Claude's auto-updater off in live labs; list fm-live-lab.sh

* fix(bin): start the lab tmux server without user config

* no-mistakes(review): Scope lab teardown to its store, root, and task ids

* no-mistakes(review): Record selected user stores at up for check and down

* no-mistakes(document): Clarify live lab documentation and remove stale narratives

* no-mistakes(ci): Fixed the CI failure by checking for an existing lab root before looking up the harness executable. The affected behavioral test and shell syntax check pass; the refusal also works with Claude absent from PATH

* no-mistakes(ci): Fixed all four Greptile findings: teardown signals only recorded lab processes and their descendants; the worker gate is in its granted task directory and its path is exposed; readiness uses current crew state; and mate and worker IDs use 12 nonce hex digits. The CLI behavior tests pass, as do shell syntax, ShellCheck, and diff checks. The Claude no-host path is unchanged

* no-mistakes(ci): Fixed the CI test’s dependence on an installed Claude binary by supplying a test-local stub. The full fm-live-lab test, shell syntax check, and diff check pass

* no-mistakes(ci): Fixed all three selected findings in bin/fm-live-lab.sh: down waits for recorded processes and escalates before cleanup, PID roots are checked against recorded start times, and Claude primary trust is rechecked after mate/worker readiness. Added behavioral tests in tests/fm-live-lab.test.sh. bin/fm-lint.sh and tests/fm-live-lab.test.sh pass

* no-mistakes(ci): Fixed the pre-primary settle wait, worker gate instructions, unused retry variable, and teardown PID revalidation in bin/fm-live-lab.sh. Added behavioral tests in tests/fm-live-lab.test.sh. Both requested commands pass: tests/fm-live-lab.test.sh and bin/fm-lint.sh

* no-mistakes(ci): Fixed teardown to track pre-kill lab processes by PID and start time, including children orphaned when a root exits. Up now rejects an empty pane PID before calling ps. Added regression tests and a Linux-safe worker fixture. bin/fm-lint.sh and tests/fm-live-lab.test.sh pass

* no-mistakes(ci): Fixed teardown tracking for children spawned during shutdown and made the worker fixture verify its exact window with a Linux-available shell. Both requested checks pass. The lab test takes about 66 seconds locally, so the under-one-minute target remains unmet

* no-mistakes(ci): Fixed ci-2 and ci-4 in bin/fm-live-lab.sh and tests/fm-live-lab.test.sh. Teardown now tracks identity-checked members of captured lab process groups, including children orphaned during shutdown, without signaling the caller’s group or unrelated processes. Lint passed, and the lab test passed four times

* no-mistakes(ci): Fixed teardown so an observed-empty process group is permanently dropped, preventing a reused group ID from signalling unrelated work. Added a ps-shim regression test. The lab test, lint, and diff checks pass

* no-mistakes(ci): Fixed ci-1 in bin/fm-live-lab.sh and tests/fm-live-lab.test.sh. The TERM-born-child fixture now waits until its handler is installed before calling down. Down sends SIGKILL to identity-valid survivors on every pass from pass 20 onward and includes survivor process details if it must refuse cleanup. bin/fm-lint.sh and tests/fm-live-lab.test.sh pass locally; Linux CI remains to be verified

* no-mistakes(ci): Fixed down’s teardown wait to require two empty identity-checked scans separated by 0.5 seconds, and removed the unused test loop variable without changing the TERM-born-child test. The lab test, lint, and diff check pass locally
…unchenguid#6103)

* fix(bin): keep slow watcher cycles and preempted reply polls from breaking supervision

- fm_pending_reply_tick selects the records it has work for in one awk pass,
  so settled records cost no lock or fork and the walk no longer grows with
  the never-pruned store.
- An attached arm keeps following a live, identity-matched holder whose beacon
  went stale until the lock changes or the shared stall bound
  (fm_watcher_stall_bound), then fails with a typed stalled-holder line so the
  retry replaces the holder.
- The remote-reply adapter reports the job worker's preemption (exit 76) as a
  closed window, so the listener keeps its claim and polls again instead of
  being relaunched every watcher cycle.

* no-mistakes(document): Clarify watcher grace and attached-arm documentation
…geable is UNKNOWN (kunchenguid#6110)

* fix(bin): retry a bounded number of times when GitHub mergeable is UNKNOWN

Fixes kunchenguid#6020

bin/fm-pr-merge.sh refused a GitHub merge whenever the pull request's
mergeable field was not literally MERGEABLE. GitHub reports UNKNOWN for
a short while after a push or a base-branch change while it recomputes
mergeability, so a green, conflict-free pull request was refused as if
it could not be merged.

github_verify_mergeable now returns a distinct status when mergeable is
the only failing condition and reads UNKNOWN. The caller retries up to
5 times, 3 seconds apart (overridable in tests), re-reading and
re-checking every live condition on each attempt. Once the bound is
spent it reports mergeability as still being computed rather than
unmergeable, with the same nonzero exit as before. Every other refusal
(closed, draft, conflicting, red or missing checks, away authority,
queue protection) is unchanged and never retried.

* no-mistakes(ci): I fixed both review findings the way you asked. The full suite (`bash tests/fm-pr-merge.test.sh`) ran to completion. Its last lines showed all `ok`, and any failure would have stopped the run early. I watched the output through `tail`, so I didn't see the new test's own `ok` line directly. **ci-2 (`bin/fm-pr-merge.sh`), retry delay not validated.** What must hold: the retry wait is always a short, valid `sleep` argument, so a bad `FM_PR_GITHUB_MERGEABLE_RETRY_DELAY` can never trip `set -e` or hold the task lock for a long time. The retry loop is the only place that reads this variable. The script now reads the value once before the loop and accepts only whole numbers from 0 to 10. Anything else (empty, `abc`, `-1`, `1.5`, `11`, a huge number, leading spaces) falls back to 3. I ran those values through the check by hand and each came out as expected. The loop now sleeps on that checked value. **ci-1 (`tests/fm-pr-merge.test.sh`), no test for a check changing between UNKNOWN reads.** What must hold: every retry re-checks all live conditions, not just mergeable. The fake `gh pr view` in the test can now take an optional second word on each line of the mergeable sequence, which sets the first check's result. The new test `test_github_mergeable_unknown_retry_rechecks_checks` feeds `UNKNOWN`, then `UNKNOWN FAILURE`. It asserts: - exit code 1 after exactly 2 reads, - the refusal names `check 'ci' is not green`, - the message does not say mergeability is still being computed, - `pr merge` was never called. If a later change made the retry look only at mergeable, the loop would read UNKNOWN 5 times, end with the "still being computed" message, and this test would fail. I didn't run it against a deliberately broken script to confirm that. `bash -n` passes. `shellcheck` reports only the existing info-level notes about files it can't follow. Only `bin/fm-pr-merge.sh` and `tests/fm-pr-merge.test.sh` changed
kunchenguid#6112)

* fix(bin): converge every open owner onto a known terminal contribution

settle_final only cleared a stale error on retry, so an owner whose saved
row still said open kept projecting a merged or closed pull request as
open after another owner's row had already recorded the terminal
observation. Copy the known terminal observation to every owner whose
saved row is not itself terminal, keeping that owner's own pending and
notified state, and clear its error.

* no-mistakes(review): Carry terminal checked_at when converging existing owner rows

* no-mistakes(ci): I fixed Greptile finding ci-2 as you asked, with a change to tests/fm-contributions.test.sh only. The rule it enforces: when a retry converges an owner onto a URL that is already merged or closed, that owner gets the terminal owner's whole observation, not just its state. The same weak check appeared twice in test_interrupted_multi_owner_poll_settles_every_owner, so I fixed both: - **Open owner (line 784):** the check now also requires `.observation == $terminal[0].records[0].observation`. The existing checks for error, checked_at, pending and notified are unchanged. - **Errored owner (just below):** it only checked state and error before. It now reads the terminal owner's file and makes the same full-observation comparison. Adding the comparison alone would not have caught anything. The test fixtures gave both owners identical observations apart from `state`, so copying only the state would still have passed. In both cases I also set the terminal owner's observation head to HEAD_B, so the two observations now really differ. Verification: - The focused test passes against the current bin/fm-contributions.sh. - I temporarily changed `settle_final` so it copied only the state. The test then failed, reporting the owner still on the old head (HEAD_A). I restored the file afterwards, and `git status` shows only the test file modified. - The full tests/fm-contributions.test.sh suite exits 0. No product code changed. The other CI finding (ci-1, "Behavior portable serial 9") was left alone because you chose to ignore it
…nguid#6124)

* feat: run the supervision host by default on a Claude primary

An absent config/supervision-host on a Claude primary now reads as on with
the default engine, and a file holding `off` opts any home out. Cursor,
OpenCode, omp, Grok, and Codex stay file-gated, with `off` read as disabled
there too. Every reader asks fm_supervision_host_enabled instead of testing
the file, and non-bash readers query it through the lib's `enabled` entry.
A primary's `off` is not inherited by secondmates: each home keeps its own
supervision posture.

* test: pin the watcher-path posture in fixtures that assume no supervision host

Fixtures that drive the watcher arm or assert a non-host drain now write
an explicit off file, and fixtures that copy the Stop auto-arm or the
supervision instructions carry the engine lib they now source. The two
drain suites also stop reading the code root's config.

* fix: name the opt-out when an off home passes an attended wake to main

A host parked when the home writes off now logs that the home does not run
the supervision host, rather than claiming it has no engine.

* no-mistakes(document): Clarify Claude supervision defaults and historical evidence

* no-mistakes(ci): Fixed process leaks in the two added host tests. Each case now stops its recorded watcher and host/arm processes; fake hook sessions exit through session.stop. The full host suite passed before the final cleanup refinement, and both affected cases, bash syntax, ShellCheck, and diff checks passed afterward. CI runtime still needs confirmation
…start scope check (kunchenguid#6125)

* fix(bin): create the state dir on a fresh primary before the session-start scope check

fm_primary_scope_matches required an already-existing state directory, so
bin/fm-sessionstart-run.sh stood down on a fresh clone before anything could
create it. Split out fm_primary_root_matches so the run wrapper can confirm
primary-home identity first, create the gitignored state dir when it is
missing, and only then run the unchanged scope check.

* no-mistakes(document): Document session-start state dir creation on fresh clones

* no-mistakes(ci): I fixed the Greptile P1 the way you asked. When a fresh primary can't create `state/`, the run wrapper no longer stands down silently. **Invariant:** when an otherwise eligible fresh primary cannot create `state/`, startup must never fail silently. This path has only one site: the mkdir in `bin/fm-sessionstart-run.sh`. Other hooks and the nudge wrapper never create `state/`, so they have no equivalent failure. **What changed:** - **Run wrapper** (`bin/fm-sessionstart-run.sh`): it captures mkdir's error and prints one line to stderr before standing down as before (exit 0, or 3 for the Pi prerequisite). The line looks like `fm-sessionstart-run: startup could not create the state directory <path>: <reason>`. - **Test** (`tests/fm-sessionstart-nudge.test.sh`): the new case `test_run_reports_a_state_dir_it_cannot_create` uses a fresh primary with no `state/` and a read-only (0500) root. It checks four things: exit 0, no digest on stdout, no state dir created, and exactly one stderr line ending in "Permission denied". It fails without the fix and passes with it. - **Docs** (`docs/sessionstart-nudge.md`): I added one sentence describing the stderr line and one describing what the new test proves. **Verification:** I ran `tests/fm-sessionstart-nudge.test.sh`, and every test passes. `bin/fm-lint.sh` on the changed scripts (pinned ShellCheck 0.11.0) and `tests/fm-documentation-audiences.test.sh` also pass. As you asked, the wrapper still stands down with the ineligible-checkout status afterwards. It does not report this as a failed eligible startup, which is what the bot suggested
…ery (kunchenguid#6126)

* fix(bin): measure pending-reply grace from turn completion, not delivery

Fixes kunchenguid#6057

The pending-reply guard demanded a repost ("REPOST REQUIRED: previous
marked request had no correlated parent report") while the second
mate's correlated reply was already on its way.
fm_pending_reply_send_recovery measured its grace window from delivery
instead of from the request turn's completion, so any turn longer than
the grace fired the demand the moment the turn ended, before the reply
could have landed. The missed-report escalation had the same gap: it
fired the instant the recovery turn's completion was observed, with no
grace at all.

Both now measure grace from the relevant turn's completion (request
turn for the recovery repost, recovery turn for the escalation), and
both take one fresh, uncached read of the parent status file
immediately before firing, accepting a correlated line regardless of
its verb. Transport-failure escalations stay immediate, and the
one-repost limit is unchanged.

* no-mistakes(review): Document grace window as measured from turn completion

* no-mistakes(ci): Both Greptile findings were real and caused by this PR, so I fixed them. The full `tests/fm-pending-reply.test.sh` suite passes. **ci-1 (a reply could be overwritten by a repost).** The rule that must hold: a recovery send is recorded only if the record is still unresolved, checked under the same per-correlation lock that resolution uses. The escalation path already did this (`_fm_pending_reply_maybe_escalate_locked` reads fresh and publishes under one lock). The recovery path did not: `fm_pending_reply_send_recovery` did its fresh read through `fm_pending_reply_try_resolve`, which let go of the lock before the send was recorded. A reply landing in that gap could be overwritten, and the repost would go out anyway. Now `send_recovery` takes the lock once and, while holding it, re-checks that the phase is still `awaiting_report`, runs the fresh uncached read, and records the send (sender pid and identity, attempt time, phase `recovery_sending`). It releases the lock before actually sending, so the lock is not held during the send. It uses the same lock helpers the other lock wrappers use. Grace timing, the one-repost limit and the escalation path are unchanged. **ci-2 (the test would pass even without the fix).** In `test_recovery_fresh_status_read_resolves_before_firing`, the reply is still appended to the status file, but the stored file signature is then set to the file's new signature. That stands in for a same-size rewrite that the signature cache cannot see. The test first checks that a normal cached read misses the reply, then that the fresh read before sending catches it. I also added the same check for the fresh read before escalation, which the review said was uncovered. The test now sets its own send hook, so it no longer depends on one left over from an earlier test (that leftover had made failures exit silently). **Checks:** - I removed the fresh-read bypass at each site in turn and reran the suite. With it gone from recovery, the test fails with "recovery must not fire once a correlated reply has landed". With it gone from escalation, it fails with "the fresh pre-escalation read should have resolved the record, got escalated". With both in place, all tests pass. - Shellcheck with `-x` timed out locally. Without `-x` and ignoring SC1091, the only warnings are SC2034 on the existing `maybe_escalate` lock wrapper, which is not part of this change. The new code adds no warnings. Changes are in `bin/fm-pending-reply-lib.sh` and `tests/fm-pending-reply.test.sh`. Nothing is committed yet; a plain commit message such as "fix(bin): record the pending-reply recovery send under the fresh-read lock" fits the instruction

* no-mistakes(ci): ci-1 was real and caused by this PR. The same bug was also in the escalation path, so both are fixed. The full tests/fm-pending-reply.test.sh suite passes. The rule that must hold: a recovery repost or an escalation goes out only if the record's phase, read after the fresh-read resolve, is still what it was before. The resolver writes phase=resolved first and only then writes the other resolution fields. If one of those later writes fails, it returns an error even though the record is already resolved. Places this rule applies, both fixed: - Recovery (fm_pending_reply_send_recovery): the fresh-read resolve now runs first, and the phase is re-read right after it, whatever it returned. The send is recorded and made only if the phase is still exactly awaiting_report. This replaces the earlier phase check rather than adding a second one. - Escalation (_fm_pending_reply_maybe_escalate_locked): same bug. After a failed resolve it went on to publish the blocked line and set phase=escalated. One added line after the resolve call returns 1 without publishing if the phase has changed. Test: added test_partial_resolve_write_blocks_firing. It forces a failure on the resolved_epoch write after a correlated reply has landed. It checks that the recovery send hook is never called, that no escalation line is published, and that the phase stays resolved. The forced failure runs in a subshell so it can't affect later tests. Checks: - With the recovery fix reverted, the new test fails with "recovery must not fire after a partial resolve". - With the escalation fix reverted, it fails with "partial resolve should block escalation, got escalated". - With both fixes in, every test passes. - Shellcheck was run with SC1091 excluded and without -x, not through the repo's lint script. The only new message is one SC2329 info on the test's override function; other test overrides in the same file already get that same info, unsuppressed. Changed files: bin/fm-pending-reply-lib.sh and tests/fm-pending-reply.test.sh. Nothing is committed. Suggested plain commit message: "fix(bin): recheck pending-reply phase after the fresh read before sending
…to stderr (kunchenguid#6001)

* fix: provider-table lookup never writes a broken-pipe error to stderr

Fixes kunchenguid#5956

fm_quota_single_provider_for_harness returned from its while read loop
as soon as it found a match, closing the pipe while
fm_quota_single_provider_table's printf could still be writing.
Where SIGPIPE is ignored, as on GitHub Actions runners, bash then
prints "printf: write error: Broken pipe" on the resolver's stderr,
which intermittently broke the one-diagnostic-line assertions in
tests/fm-dispatch-resolve.test.sh.

Read the whole table before answering, the way
fm_control_harness_supported already does, so the writer always
finishes. Return values and output are unchanged.

Reproduced by running tests/fm-dispatch-resolve.test.sh with SIGPIPE
ignored on a single pinned core under CPU contention: 30 of 30 runs
failed before the fix, 0 of 30 after. Note: reproducing requires
setting the trap inside the tested shell because nice(1) resets an
inherited SIGPIPE ignore to SIG_DFL. tests/fm-quota-choose.test.sh
passes and bin/fm-lint.sh is clean.

* no-mistakes(ci): Fixed both Greptile findings the user chose to address. ci-1 (bin/fm-quota-axi-lib.sh:154). Invariant: looking up a harness must always end with status 0 and print the provider, even when the caller runs under `set -e`. The loop body `[ -z "$found" ] && [ "$harness" = "$1" ] && found=$provider` now ends in `|| :`. Every iteration succeeds and the whole table is still read. Only `fm_quota_single_provider_for_harness` loops over the table this way, so this is the one place the fix was needed. One caveat: on bash 5.3 the old code did not actually exit under `set -e`, because the `while` loop is not the function's last command, so the new `set -e` test would have passed before this fix too. The change makes the loop's success explicit, as the user asked. ci-2 (regression coverage). I added three cases to the existing `tests/fm-quota-choose.test.sh`, all calling the public lookup function after sourcing the library: 1. With SIGPIPE ignored (`trap "" PIPE`), it looks up every harness 200 times and checks that nothing reaches stderr. 2. A deterministic version of the race: the table function is wrapped so it writes the first row, pauses 0.2 s, then writes the rest. With SIGPIPE ignored, it checks that looking up `claude` prints `claude` and writes nothing to stderr. The stress loop alone reproduced the bug in only about 1 of 5 local runs, which is why this case exists. 3. A direct call under `set -e` prints `claude`. Verification: - `bash tests/fm-quota-choose.test.sh`: all pass. - Same test against the pre-PR library (fa48367, via `FM_ROOT_OVERRIDE`): fails with `printf: write error: Broken pipe`. The deterministic case failed in one run and the stress loop caught it in another. - `shellcheck` on both files: clean. - `tests/fm-dispatch-resolve.test.sh`: passes
…isioning (kunchenguid#6162)

* fix: survive Pi 0.99 rendering and Git 2.55 local-clone races

Pi 0.99 puts arguments on the stock tool header and leaves hidden custom messages in the export conversation column. Match that header, and keep Calm's boundary on the visible column. Clone a remote home with --no-local so a prune during Git's loose-object copy cannot fail the seed.

* no-mistakes(review): Stop SIGPIPE write errors; cover older Pi export and project clones

* no-mistakes(document): Clarify Calm export visibility and tool rendering

* no-mistakes(ci): Fixed the dispatch diagnostic to list every provider-less use/default profile in one line and added a multi-profile behavior test. Shortened supervision fixtures using the existing engine-grace and park-clock knobs; removed stray scratch files. Dispatch tests, syntax checks, and three targeted supervision cases passed. CI’s prior supervision duration was 751s; the single permitted local full-suite run timed out at 1200s, so an after-duration is not established. The cancelled serial check had no failure verdict. The outer executor should record the measured before/after duration in the PR body when available

* no-mistakes(review): Gate Pi 0.99 call headers by version; drop hidden-row assertion

* no-mistakes(review): Test stock call headers under Pi 0.87 and 0.99 stubs

* no-mistakes(test): Fix older-Pi queued-row test and verify park-boundary behavior

* no-mistakes(document): Clarify Pi Calm export and queued-turn documentation

* no-mistakes(ci): Fixed the stock macOS Bash 3.2 parse failure in tests/fm-calm-pi-extension.test.sh; its parse check passes. The watcher CI failure is in unchanged code: the isolated five-minute/66-minute case passes locally, but the CI log omits the drain error needed to establish its cause. No speculative watcher fix was made. The full local watcher suite timed out after 500 seconds
…6169)

* Prevent premature Lavish board handoffs

* Prove Lavish arm lacks reply acknowledgement

* Confirm Lavish replies before arming worker boards

* no-mistakes(review): Post Lavish reply only after locked arm eligibility checks

* no-mistakes(review): Fail Lavish reply closed on unknown version

* no-mistakes(document): Correct Lavish reply documentation and remove stale guidance

* no-mistakes(document): Clarify Lavish reply routing and remove duplicate version guidance
…#6154)

* feat: inherit the supervision-host opt-out from the primary

Move the supervision host's off opt-out out of config/supervision-host into
its own presence flag, config/supervision-host-off, and add that flag to the
primary-authoritative inherited config set. A primary that opts out now opts
every secondmate home out at spawn and convergence, and clearing it converges
them back. config/supervision-host stays the home-local engine choice.

Shape: config/supervision-host mixed two things, a fleet posture (off) and a
per-home engine and model. Only the posture should follow the primary, so it
becomes a separate presence flag that rides the existing inherited-config
mechanism (FM_INHERITABLE_CONFIG in bin/fm-config-inherit-lib.sh) with no new
machinery, while the engine line stays local. The parse stays in its one
owner, fm_supervision_host_enabled. There is no migration or compatibility
handling for a home that still holds off in config/supervision-host.

Primary off, mate on: inherited material is primary-authoritative by design,
so a mate cannot keep the host while the primary is opted out, and a mate's
own opt-out is removed at the next convergence while the primary has none.
Running the host on a mate is the primary's choice for the fleet; no override
mechanism is added.

Live validation (disposable bin/fm-live-lab.sh lab, Claude primary with a
real seeded secondmate, --supervision-host off):
- up: every readiness check ok, including "host: none running, as expected"
  and a live mate session; the spawned mate home held the inherited
  config/supervision-host-off and the gate read primary OFF, mate OFF.
- primary removed its opt-out, then bin/fm-config-push.sh reported
  "supervision-host-off: pushed - mirrored primary absence" and a config
  reread sent; the gate read primary ON, mate ON, and the live mate handled
  the reread.
- primary opted out again and pushed: "supervision-host-off: pushed", mate
  gate OFF.
- down stopped every lab process and left no lab process running.

Out of scope, follow-up: default-on for the other harnesses, away-daemon
retirement, rollout.

* no-mistakes(document): Document inherited supervision-host opt-out ownership

* no-mistakes(ci): Fixed ci-4: with `--supervision-host off --mate`, lab readiness now requires the inherited flag in the mate home and a disabled mate supervision-host gate. The focused behavior test, shellcheck, and diff checks pass. Left ci-1–ci-3 untouched as directed

* no-mistakes(test): Fix mate readiness HOST_OFF initialization in lab up

* no-mistakes(ci): Fixed Lint 2 by making the new test’s fixtures source resolvable to ShellCheck; its off/on readiness test and ShellCheck now pass locally. Behavior portable serial 5 failed in the unchanged remote-reply test at generation 7. That test passes locally, and no PR-caused defect was identified, so no remote-reply code was changed
…nguid#6179)

* fix(tests): cut the fixed sleeps in supervision-host cycles

The serial CI lane keeps brushing its 30-minute cap because
fm-supervision-host.test.sh spends ~903s of the job, and per the
run-36635306527 case profile the top nine cases are all multi-cycle
ones (3-10 park/close/turn cycles each): every close waits out the
host's sleep $POLL in await_close plus a watcher sleep $FM_POLL scan
cycle, and every engine turn waits out the fixed sleep 1 descendant
snapshot. That is ~3s of pure sleep per cycle before any real work.

The host poll now accepts positive decimal seconds through a new
seconds_or validator (FM_SUPERVISION_HOST_POLL), and the engine turn's
snapshot loop takes FM_SUPERVISION_ENGINE_SNAPSHOT_SECONDS, also a
positive decimal defaulting to one second - the smallest seam at each
wait's single owner. The suite drives them at 0.2 alongside the
existing FM_POLL=0.5 and FM_ARM_ATTACH_POLL=0.2 knobs, so the real
poll loops still run. The park-boundary case moves onto the injected
test clock instead of a real 3s wait, per-case cleanup polls the host
pid rather than sleeping a full second, and the proof-by-absence
windows (flood re-escalation, successor re-announce, watcher
persistence, recovery staying off main) shrink from 2-3s to 1s, which
still spans two watcher polls at the test cadence.

Every assertion, process lifecycle, and reaping path is unchanged;
production defaults stay at one second. Isolated case timings on a
contended host, base vs branch: attended-latch 54.3->34.6s,
undelivered-dialog 67.7->59.1s, away-latch 46.5->30.5s, held-cadence
47.9->21.6s, unreadable-mirror 39.2->38.5s, park-limit 18.2->12.3s,
registration-fallback 14.1->10.0s, first-cycle-status 12.6->8.4s,
latch-scope 16.7->16.3s. Full suite: 65/65 pass. fm-lint and
shellcheck clean.

* no-mistakes(review): Wait for scan lock release before duplicate check

* no-mistakes(document): Correct supervision snapshot cadence documentation

* fix(tests): keep production poll cadence, probe exits at 0.1s

The fractional poll cadences multiplied the cost of each loop body:
full process-table scans in the engine turn and process refreshes in
await_close ran five times more often, which swamped the thin CI runner
and nearly doubled every multi-cycle case (serial 5 was cancelled at its
30-minute limit on run 36635306527's successor). Restore the production
cadence and notice arm/engine exits with a cheap kill -0 probe at a
tenth of a second between the one-second bodies instead: strictly less
dead time than baseline with no added CPU.

Also hold each injected-clock park bound well past its case's
wall-clock checks so a host that ignored the test clock fails instead
of silently passing at a real-time boundary, and restore the shortened
proof windows (watcher liveness, recovery-off-main absence, first-cycle
stream) to their baseline depth.

* no-mistakes(document): Clarify supervision engine snapshot documentation
…henguid#6192)

* fix: rebalance portable CI from current duration measurements

* no-mistakes(test): Test serial packing boundary and verify endpoint timeout cleanup

* no-mistakes(document): Clarify timeout guidance and remove duplicated packing estimates
…nguid#6216)

* fix(bin): run no repository hook when core.hooksPath is empty

The per-task hook wrapper refused every commit in a repository whose own
config sets core.hooksPath to the empty string, because git rev-parse
--git-path hooks fails on it. Plain git reads that setting as no hooks, so
the wrapper now runs none; every other lookup failure still refuses and
shows git's error.

Fixes kunchenguid#6171

* no-mistakes(review): Refuse commits when core.hooksPath is a valueless key

* no-mistakes(document): Document empty core.hooksPath handling in commit attribution docs

* no-mistakes(ci): When the wrapper refuses a commit, Git's hook-lookup error now shows up once instead of twice. That required changing one line in the wrapper, and the tests were extended so both bad-config cases would catch the duplicate. Invariant: when the wrapper refuses, Git's lookup error must appear exactly once. In the failure path, the only Git call besides the deliberate second lookup is the `git config --get --type=path core.hooksPath` check in `runtime_chain_body` (`bin/fm-git-strip-ai-trailers.sh:168`). That check prints the same error, so it was the one place to fix. I added `2>/dev/null` to it. Its exit status still decides the outcome: an empty value still runs no hook, and anything else goes on to the second lookup, which prints Git's error once, and the commit is refused. Tests (`tests/fm-git-strip-ai-trailers.test.sh`): - The unresolvable-path test (`~fm-no-such-user-6171/hooks`) now requires `failed to expand user dir` to appear exactly once in the refused commit's output. - The valueless-key test now requires `missing value for 'core.hookspath'` to appear exactly once. - Pre-existing bug in the unresolvable-path test: its `git add` ran after the bad config was set, so it failed silently (exit 128) and the "refused commit" had nothing staged. The test now stages the file before writing the config, the same way the valueless test does, so a real commit gets refused. - The empty-string test is unchanged and still passes, so an empty `core.hooksPath` still runs no hook. Verification: - With the wrapper change reverted, both new checks fail with `expected '1', got '2'`. With the change in place, the whole suite passes. - `bash -n` passes. shellcheck shows only an info-level SC1091 note about sourcing `lib.sh`, which was already there before this change. - `git status` lists only the two intended files
…kunchenguid#6213)

* fix(bin): let a stale record on a reassigned slot retire records-only

When a pool slot's owner claim names another task, the stale record's
teardown touches nothing under the slot, so the exclusive-slot record scan
no longer refuses it. Full teardowns of a slot this task still claims, or
one with no claim, keep the refusal.

Fixes kunchenguid#6184

* no-mistakes(document): Note claim-over-record precedence for reassigned teardown slots
…uid#6240)

* fix(bin): keep the steering doorbell short under deep homes

The doorbell printed the task inbox's absolute path twice, so under a deep
home it grew to about 290 characters and a Herdr submit reported it never
reached the pane on every re-ring. It now names the inbox once by its short
<task>.inbox name and points at the full path the worker's brief already
gives, so its length no longer depends on the home's depth.

Fixes kunchenguid#6120

* no-mistakes(review): Export FM_TASK_INBOX at launch and name it in doorbell

* no-mistakes(ci): ci-1 (Behavior portable serial 9) was caused by this PR, and I fixed it in the test. tests/fm-claude-trust.test.sh failed with "the launch command did not carry a brief doorbell". Its claude_launch_doorbell helper stripped exactly two leading `export ...;` statements before reading the final prompt argument. This PR adds a third one (`export FM_TASK_INBOX=...`) to every launch, so the helper was reading the wrong command. The invariant: a test that parses the launch command must skip every leading export statement, however many there are. I checked every test that parses the launch this way. The only other ones are the two helpers in tests/fm-spawn-dispatch-profile.test.sh, and they already loop over all exports. The kimi and dispatch-profile exact-string checks were updated earlier in this PR. The fix makes claude_launch_doorbell use the same loop (`while [[ "$command" == export\ *\;* ]]; do command=${command#*; }; done`) and then take the last argument. The ordinary path still works: the claude spawn test and the secondmate-clone spawn test both resolve the brief record through the same helper. Verified locally: `bash tests/fm-claude-trust.test.sh` exits 0 with no failing cases. ci-2 (Behavior tests (Herdr)) was not caused by this change, and I made no code change for it. In tests/fm-backend-herdr-presentation-e2e.test.sh, the concurrent secondmate recovery failed with "herdr presentation recovery could not acquire its session lock; refusing a concurrent resume". Two reasons it is not this PR: - The same failure, in the same test and case, happened on run 36655209015 for the unrelated branch fm/fm-contributions-old-gh-compat about 14 hours earlier. - This PR's change cannot lengthen how long the lock is held. The launch is written to a file and sent to the pane as `. launch.N.sh`, so the extra export changes neither the pane submit nor the lock hold time. The cause is a race that was already there: spawn_herdr_presentation_order_lock_acquire gives up after 5 seconds, and a concurrent real-Herdr recovery can hold the lock longer. Fixing that means changing the product's lock timeout, which is outside this PR. It should be tracked separately, and a rerun of the Herdr job is expected to pass. The only file changed is tests/fm-claude-trust.test.sh
… no turns (kunchenguid#4859)

* fix(dod): drive no-mistakes with one foreground call, not a background poll

The brief told workers to background the drive call and poll `axi status`
because one call "routinely outlives what your harness lets a single
command run". That advice contradicts the tool it drives: `no-mistakes
axi run --help` documents `--wait` with an 8m default, existing precisely
"so an agent harness with a 10-minute tool cap gets a structured return
instead of an unbounded hang".

Following the old text, a worker could never idle - a backgrounded call
returns in milliseconds, so it does not wait at all - and each attempt
leaked a live timer that later fired as a paid wake. Tell workers to make
one foreground call, let it block, and repeat it when it returns on
elapsed wait rather than on a gate or outcome.

Also drops the generalisation that told workers on any unestablished
harness to assume a command cap and use the same shape, which exported
the defect to harnesses with no such cap.

* fix(bin): let a waiting worker spend no turns until it is answered

A worker waiting on a decision, a pipeline gate, CI, or a heavy-test slot
kept taking model turns: the brief told it to list its inbox at any natural
checkpoint, and six automatic senders nudged secondmates whatever their open
decisions.

- The ship and scout briefs gain one Waiting section: end the turn after
  needs-decision or blocked, and hold an external wait inside ONE blocking
  command bounded by the harness's own command ceiling. The checkpoint clause
  is deleted. Forbidding the wrong shapes is not enough on its own, so the
  section also names the blocking foreground `until` loop as the wait a Claude
  Code worker may use, because that harness can refuse a sleep-then-check
  command while pointing at backgrounding, which is the one shape a waiting
  worker must not take.
- fm-send --automatic defers (exit 4, nothing written or rung) while the
  target has an open decision or blocker of its own; every automatic sender
  passes it and keeps its retry state, and the pending-reply recovery waits
  the same way.
- The two senders that report the result classified it by matching the text of
  the send's captured output against `deferred:*`. fm-send runs bin/fm-guard.sh
  as a supervision warning, and that guard prints its worktree-tangle banner
  whenever the primary checkout is on a feature branch, which is exactly what a
  CI pull-request checkout is. The banner lands ahead of the `deferred:` line,
  so the match fell through and a waiting mate was reported as a failed send,
  with the banner as the reason. Both senders now classify on fm-send's exit
  status, which is the contract the deferral is actually stated in, and select
  the `deferred:` line out of the output rather than assuming it came first.

The third root cause, a no-mistakes definition of done that backgrounded the
drive call and polled axi status, is fixed by this branch's parent commit
"drive no-mistakes with one foreground call, not a background poll"; this
commit takes that text as is and adds the regression test.

Upstream's spawn abort path no longer calls the lease-return helper at all,
so the fork's missing-helper guard and its pin-feature test line are moot
here and are not ported.

The command ceilings each harness enforces, and the probes behind the named
Claude Code wait, are recorded in docs/verification/runtime-backends.md.

* no-mistakes(review): Exempt captain holds, quiet deferred reconcile, clarify worker pauses

* no-mistakes(document): Document deferred automatic nudges, rereads, and reply recovery

* no-mistakes(document): Ring unlanded fire-and-forget steers exactly once more

* no-mistakes(ci): The failing check, "PR must be raised via no-mistakes", reads the pipeline's attestation record, which says document=skipped. No file in the repository can change that record, so I did not touch the check or the PR body. As you said, the no-mistakes rerun after this run finishes will re-execute the document step and record document=completed. The one change is the documentation sentence you ordered. It adds a line to docs/remote-secondmates.md, right after the line saying the remote host runs no re-ring ladder of its own: "A fire-and-forget record, such as a reconcile ask, gets its single retry ring only on the local plane: the remote steer leg owes no re-ring, so a swallowed remote doorbell for one waits for the next ring into that inbox, and a remote-side retry is known follow-up scope." No behavior changed. Checks: tests/fm-documentation-audiences.test.sh passes (4/4) and bin/fm-lint.sh is clean. The change is left uncommitted in the working tree for the pipeline to pick up

* no-mistakes(review): Hold automatic wakes until a mate's own decision closes

* no-mistakes(document): Document watcher delivery of deferred remote re-read nudges

* no-mistakes(review): Merge duplicate elapsed-wait reattach instructions in DOD

* no-mistakes(test): Resolve merged default decision in remote-reply recovery fixture

* no-mistakes(test): Source classify lib so config-push retry-deferred honors open decisions

* no-mistakes(ci): Fixed a flaky test that also fails on main. Neither this PR's bin/fm-brief.sh nor its bin/fm-dod-lib.sh change is involved: bin/fm-dispatch-resolve.sh sources neither file. Another branch (fm-attended-cutover-smoothing-s1, run 36343879084) failed the same shard 8 check the same way, on a different case ("a rule-criterion match prints one diagnostic line, got 2"). Root cause: `fm_quota_single_provider_for_harness` in bin/fm-quota-axi-lib.sh returned from its `while read` loop as soon as it found a match. That closed the pipe while `fm_quota_single_provider_table`'s `printf` was sometimes still writing. GitHub Actions runners ignore SIGPIPE, so bash printed `fm-quota-axi-lib.sh: line 138: printf: write error: Broken pipe` to the resolver's stderr. That is the extra line. I reproduced it locally by running the test with SIGPIPE ignored: 2 of 20 runs failed, one with the resolver's diagnostic line plus two broken-pipe lines. Invariant: looking up a harness in the provider table must never make the table writer fail. The only reader of that table is this function, and all of the resolver's lookups (line 208 without stderr redirected, line 222 with it) go through it. So the fix is in that one place: read the whole table, then print the match. The same file now shows it reads the full table first, like `fm_control_harness_supported` does. Return values and output are unchanged. Verification: with SIGPIPE ignored, tests/fm-dispatch-resolve.test.sh failed 0 of 30 runs after the fix (2 of 20 before). tests/fm-dispatch-resolve.test.sh, tests/fm-brief.test.sh, tests/fm-send-inbox.test.sh, tests/fm-quota-choose.test.sh and tests/fm-quota-array-dispatch-live-e2e.test.sh all pass, and shellcheck is clean. tests/fm-procevent-quota.test.sh fails locally with or without the change ("process-event state root is not a private directory"), so that failure comes from the local environment, not from this fix. No new test was added: the existing one-diagnostic-line assertions already catch this whenever SIGPIPE is ignored, as it is in CI

* Revert "no-mistakes(ci): Fixed a flaky test that also fails on main. Neither this PR's bin/fm-brief.sh nor its bin/fm-dod-lib.sh change is involved: bin/fm-dispatch-resolve.sh sources neither file. Another branch (fm-attended-cutover-smoothing-s1, run 36343879084) failed the same shard 8 check the same way, on a different case ("a rule-criterion match prints one diagnostic line, got 2"). Root cause: `fm_quota_single_provider_for_harness` in bin/fm-quota-axi-lib.sh returned from its `while read` loop as soon as it found a match. That closed the pipe while `fm_quota_single_provider_table`'s `printf` was sometimes still writing. GitHub Actions runners ignore SIGPIPE, so bash printed `fm-quota-axi-lib.sh: line 138: printf: write error: Broken pipe` to the resolver's stderr. That is the extra line. I reproduced it locally by running the test with SIGPIPE ignored: 2 of 20 runs failed, one with the resolver's diagnostic line plus two broken-pipe lines. Invariant: looking up a harness in the provider table must never make the table writer fail. The only reader of that table is this function, and all of the resolver's lookups (line 208 without stderr redirected, line 222 with it) go through it. So the fix is in that one place: read the whole table, then print the match. The same file now shows it reads the full table first, like `fm_control_harness_supported` does. Return values and output are unchanged. Verification: with SIGPIPE ignored, tests/fm-dispatch-resolve.test.sh failed 0 of 30 runs after the fix (2 of 20 before). tests/fm-dispatch-resolve.test.sh, tests/fm-brief.test.sh, tests/fm-send-inbox.test.sh, tests/fm-quota-choose.test.sh and tests/fm-quota-array-dispatch-live-e2e.test.sh all pass, and shellcheck is clean. tests/fm-procevent-quota.test.sh fails locally with or without the change ("process-event state root is not a private directory"), so that failure comes from the local environment, not from this fix. No new test was added: the existing one-diagnostic-line assertions already catch this whenever SIGPIPE is ignored, as it is in CI"

This reverts commit c719928.

* no-mistakes(review): Retry deferred local instruction nudges via the watcher

* no-mistakes(review): Document watcher retry for deferred local instruction nudges

* no-mistakes(ci): I fixed both review findings you selected (ci-1 and ci-3). I did not touch the deferral check in bin/fm-send.sh. ci-1 (bin/fm-config-push.sh, retry_deferred_rereads) - Rule that must hold: a deferred reread stays flagged until it is actually delivered. - Before the fix, the flag was removed before any of the steps that can skip a mate: the remote lock-path lookup, validate_secondmate_home, the local lock-path lookup, and the lock acquire. A skip at any of those dropped the flag, so the watcher lost track of the reread. - Now the flag is removed in one place only, when the send succeeds (rc 0). A skipped home, a busy lock, a deferred send (rc 4) or a failed send all leave it in place. The re-mark calls on a busy lock and on rc 4 were no longer needed, so I removed them. I updated the comment above the function to match. - Side effect: a send that keeps failing now stays flagged, so the watcher retries it on every poll and logs each failure. That follows your "don't clear until delivered" rule, but it replaces the old behaviour of leaving a failed send to the next config push or session start. - New test in tests/fm-secondmate-sync.test.sh: T8j "a deferred flag survives a skipped invalid home and is retried once it validates". It takes the home's marker away to make validation fail, checks that nothing is sent and the flag stays, then puts the marker back and checks that the nudge is delivered and both the flag and the retry marker are cleared. It fails on the old code and passes now. ci-3 (bin/fm-secondmate-restart.sh) - Rule that must hold: no automatic send wakes a mate that is waiting on its own open decision. - The two automatic sends in this script are the fallback reread nudge (fall_back_to_nudge) and the persist request. Both now pass --automatic. If a persist request is deferred, its correlation is discarded and the mate goes to the fallback nudge, which is also deferred, so the mate is reported as unreached. - New test in tests/fm-secondmate-restart.test.sh: T3b. It gives a mate an open needs-decision and runs a restart. It checks that both sends report as deferred, the mate's doorbell is never rung, its inbox gets no message, nothing is stopped, and the mate is reported as unreached with exit status 3. It fails on the old code and passes now. - The test marks the watcher as alive first. Without that, the watcher-down warning is printed first and becomes the reported reason instead of the deferral message. Verification - tests/fm-secondmate-sync.test.sh passes. - tests/fm-secondmate-restart.test.sh passes. - tests/fm-secondmate-harness.test.sh (the other test that exercises --retry-deferred) passes. - The fm-send-inbox test that covers automatic deferral passes. I only looked at the last lines of that run, not the whole file. - `shellcheck -x` on the four changed files is clean

* Pin autoarm supervision model in secondmate restart T3b

The fresh watcher beat the test writes proves a live watcher only under the
autoarm model; on CI hosts with no detected harness the persistent model
demands a lock-holding watcher, so the watcher-down banner became the
reported reason and the deferral assertion failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep deferred secondmate nudges retryable under the inheritance lock.

A bootstrap instruction nudge could write its deferral flag outside the lock the watcher retry holds, so a concurrent retry could delete a flag that had just been set. A restart fallback that is deferred now records the same marker and flag, so the watcher delivers it once the decision closes.

* no-mistakes(document): Document watcher retry of deferred restart re-read nudges

* Send secondmate reread and restart nudges immediately again.

Deferring those nudges let a later config push drop an incomplete transfer once the decision closed. They now send as they do on main.

* Make the no-turn wait opt-in behind config/wait-no-turns.

Homes that do not create the file keep the previous briefs, drive text, and sends.

* no-mistakes(document): Document wait-no-turns inbox wording change in configuration

* no-mistakes(review): Keep checkpoint inbox check; forbid only polling while waiting

* no-mistakes(ci): Fixed ci-2 (Greptile: a concurrent retry marker gets lost). The rule that was broken: the watcher may remove only the `.retry-ring` mark for the record it just processed. A newer mark written in the meantime is owed its own retry. `fm_task_inbox_clear_retry` is the one shared function that removes the mark, and I fixed it there. In `bin/fm-task-inbox-lib.sh` it now takes the record path. It compares the mark's content with that record's name and removes the mark only when they match. When the mark names a different record it returns success and leaves the mark alone. It still fails only when the processed record's own mark can't be removed. Both callers in `bin/fm-watch.sh` now pass `"$rec"`: the dead or missing pane path and the path after a retry ring. So the fix holds at both removal sites. Tests, in `tests/fm-task-inbox.test.sh`: - I added an optional `FM_RING_MARKS_RETRY` hook to the fake tmux. It writes a newer record's mark while the doorbell is being typed, which reproduces the race deterministically. - I added `test_watcher_retry_keeps_a_newer_mark`. The owed retry rings once, the newer mark survives, and a later check rings the newer record once and then clears its mark. The test fails without the fix ("the spent retry removed a newer record's mark written during its ring") and passes with it. - I updated the direct `clear_retry` call in the existing unit test to pass the record. Results: `tests/fm-task-inbox.test.sh` passes in full and `tests/fm-send-inbox.test.sh` passes 15/15. Shellcheck reports only SC1091 "not following sourced file" notices. As instructed, I didn't change the brief inbox wording

* no-mistakes(document): Fix stale wait-no-turns inbox wording in inbox lib comment

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kun Chen <kunchenguid@users.noreply.github.com>
…note (kunchenguid#6140)

* fix(bin): record Gerrit change URLs as close notes

Teardown's backlog_done_args hands every ship's recorded pr= URL to
fm_backlog_done as --pr, and tasks-axi refuses any --pr that is not a
canonical GitHub or Forgejo pull request. A Gerrit change URL therefore
left the item In flight after cleanup, and the pending backlog-close
record replayed into the same refusal at every session start.

fm_backlog_done now rewrites a --pr whose value fm_pr_url_parse reads
as a Gerrit change into --note "Gerrit change <url>". The mapping sits
at the tasks-axi call rather than in the pending-close record, so
records already written with --pr replay to a close unchanged. The
captain-held retain path records the URL in its deliverable line and
skips the update --pr it cannot make.

* no-mistakes(review): Note retained Gerrit change URL when captain answers early

* no-mistakes(document): Document Gerrit change URL handling in captain-hold retention
* perf(remote): separate active job sampling from dispatcher cadence

* no-mistakes(document): Link remote wait timing to its authoritative contract

* no-mistakes(ci): Fixed ci-1 with two narrowly scoped SC2030 annotations documenting intentional subshell-local legacy and active cadence overrides in tests/fm-remote-job.test.sh. Runtime behavior is unchanged. Reproduced the lint failure before the fix; afterward ShellCheck 0.11.0 with source following, Bash syntax validation, the complete remote-job behavior suite, and git diff --check all passed

* perf(supervision): reduce park, delta and dispatcher polling

* no-mistakes(document): Clarify poll latency contracts and authoritative documentation pointers
…henguid#6221)

* fix(bin): load backend sibling libraries under zsh

fm_backend_source kept each backend's sibling list in one space-separated
string and iterated it unquoted. zsh does not word-split an unquoted
expansion, so the readability check saw the whole list as one path and
refused every backend with more than one sibling. Hold the list in the
function's positional parameters instead, which needs no word splitting
in Bash 3.2, Bash 5, or zsh.

The existing zsh case in tests/fm-backend.test.sh covers it wherever zsh
is installed.

* test: run the Calm mod suite on stock Bash 3.2

The suite injected shell values into its generated Node scripts with the
${value@Q} transformation, which needs Bash 4.4. Stock macOS Bash 3.2
reports a bad substitution, so every case failed before it asserted
anything. Build each JavaScript string literal with JSON.stringify
through a small helper instead, which works on any Bash and is a valid
literal for any value.

* no-mistakes(review): fix(bin): rename zsh-special path local in fm_backend_source

* test: narrow the zsh backend claim to name matching

Under zsh the adapters locate their siblings through BASH_SOURCE, so a
successful fm_backend_source is not a full load. Assert only what the
contract states, and pass js_string values after -- so node never reads
a leading-dash value as its own option.

---------

Co-authored-by: Nova Agent B <novaagentb@gmail.com>
…tatus scans (kunchenguid#5263)

* fix(bin): exclude a remote mate's own parent channel from self-home scans

A remote secondmate home's outbound parent channel lives at state/parent-replies.status inside its own state dir, so the watcher's signal scan enumerated it as a task status file and the open-decisions fold classified it as a phantom task named parent-replies: every parent-channel append spun a spurious signal wake and a phantom open decision in the mate's own home.
fm-parent-channel-lib.sh gains fm_parent_channel_outbound_status, which resolves the channel into the mate's own state dir for the remote route only, and fm-classify-lib.sh's status_scan_parent_channel_exclude wraps it for the fleet-wide scans.
The watcher's scan_signals and heartbeat fail-safe backstop, the whole-file and incremental open-decisions folds, the presentation snapshot, and the unread-surface scan now skip exactly that resolved path.
The exclusion is home-shape-aware: a parent-replies.status in a main home or a local mate is an ordinary task log and keeps waking and folding, and every other status file is untouched.

* no-mistakes(review): exclude a remote mate's parent channel from the daemon heartbeat scan

* no-mistakes(document): Document remote mate parent-channel scan exclusion

* ci: retrigger portable serial 4

* no-mistakes(ci): CI check 'Behavior portable serial 7' failed in tests/fm-contributions.test.sh ('reservation poll failed'). CI stderr showed bin/fm-contributions.sh:345 arithmetic 'DEADLINE - 6\n90077104: syntax error in expression': the fixture's fake date returned a torn two-line clock value. Root cause: the fake forge wrapper in wrap_forge advances the shared controllable clock via a non-atomic read-modify-write ('$(cat $FORGE/clock) + 6' with truncate-in-place '> $FORGE/clock') while concurrent background gh calls run and the fake date reads the same file; an interleaved truncate+write publishes a half-written value (CI's torn '6\n90077104', tail of 1790077104) or an emptied-read value ('6'), which either breaks the poll's arithmetic (nonzero exit -> 'reservation poll failed') or defeats the 15-second reservation defer. This is a pre-existing test-fixture race, not caused by the PR's diff (base..target touches no contributions code; the same commit passed this shard in run 35711207830 earlier the same day). Fixed the flaky fixture at its root: clock_bump() now writes each new value to a per-process mktemp file in the same directory and publishes it with mv (atomic rename), so concurrent forge callers and the fake date always read one complete old-or-new clock; fault patterns and deltas are unchanged. Verified: minimal 3-way concurrency repro shows the old wrapper corrupting (12/32/38 outcomes incl. empty-read) while the rename-based wrapper never corrupts (20/20 clean); the full tests/fm-contributions.test.sh passes twice (all 38 assertions ok, incl. the reservation, budget-exhaustion, genuine-failure, shared-once, and latency tests); 10 isolated reservation runs pass; shellcheck rc=0; worktree contains only this one-file change

* no-mistakes(document): drop stale file-set copy in daemon catch-all comment
…6307)

* fix(bin): name the accepted verdict actors in fm-contributions help and refusal

* fix(ci): Updated tests/fm-contributions.test.sh to assert exactly captain, fleet, maintainer, and nobody in command-emitted help and refusal output. Three focused regressions passed; all three extra-actor mutations were rejected. ShellCheck, syntax, and diff checks passed. Production code remains unchanged
…chenguid#6306)

* fix(bin): recognise a clone root git names with different path spelling

fm-fleet-sync compared git's --show-toplevel with pwd -P as strings, so a clone
root that git recorded with different casing (case-insensitive volume) was
skipped as not a clone root and never refreshed. Compare filesystem identity
instead, which also covers symlink spelling.

* fix(document): Remove stale clone-root comparison comment
)

* test(calm): pin Pi's regular TUI mode where pane assertions read scrollback

Pi 1.0.0 defaults its TUI to a fullscreen alternate-screen mode whose
scrollable transcript is application-owned, so rows that leave the viewport
never enter terminal scrollback and tmux capture-pane -S can no longer see
them. The Pi Calm e2e launches now pass --tui-mode regular wherever the flag
exists so the transcript assertions keep reading real scrollback on both the
Pi 1.0.0 line and earlier Pi lines, which have no such flag and render
regular-only anyway.

* no-mistakes(document): Correct Pi TUI documentation and scrollback rationale
…ries (kunchenguid#6331)

* fix(bin): encode captain-hold reasons and reject self-inventory in complete

hold now stores a reason with parentheses, line breaks, or percent signs
through a reversible percent encoding that every reader decodes, instead of
refusing it. hold --origin records the origin on the held task, and complete
refuses the origin as its own inventory entry and an entry held for a
different origin; holds with no recorded origin are accepted and flagged.

* fix(review): Decode marked hold reasons consistently across readers

* fix(review): Remove unnecessary lifecycle test dispatch

* fix(review): Correct hold origin identity and inventory recovery

* fix(review): Record origins before placing backend holds

* fix(document): Clarify captain-hold validation and reason reader documentation

* fix(ci): Fixed both findings: failed backend holds restore the previous origin, and invalid base64/UTF-8 reasons remain verbatim. Added regressions and documented valid-literal ambiguity. Both failures were reproduced before fixes. Verification: 54 lifecycle tests and 9 wrapper tests passed; 7 Beads-specific cases skipped because tasks-axi is markdown-only. Focused lint and diff checks passed. No pipeline or publication actions performed
* fix(bin): take over the watcher cycle a main-only pass-through leaves

An attended main-only pass-through leaves a successor watcher cycle
running through main's handling turn. The session's next park attached
to that cycle instead of owning it, so the successor's arm, orphaned by
its host's exit, kept owning the watcher while the new park's arm polled
it twice a second until the next close or the park boundary, hours later
in a quiet second mate. A second-mate restart hit this every time, since
its persist request is a main-only close.

The host now records the successor it leaves for main, and the next
host's first cycle runs bin/fm-watch-arm.sh --take-over on it: when that
arm still owns the healthy watcher, the new arm stops it, reports a
reason the cycle delivered first, and otherwise owns a fresh cycle. The
stop's own downtime publication is undone over an acknowledged episode
when no wake was appended in between, so the handover wakes nobody.

* no-mistakes(review): Keep left-arm record until the orphaned arm is gone

* no-mistakes(review): Relinquish successor arm only after durably recording it

* no-mistakes(review): Relinquish successor only after its record reads back

* no-mistakes(document): Clarify successor takeover guarantees and authoritative documentation

* no-mistakes(ci): Fixed ci-1: acknowledgement restore now requires the exact taken-over arm/watcher ledger row with signal=TERM, awaited within a short bound. Otherwise takeover proceeds without erasing downtime. Added a self-exit regression confirmed failing before the fix and passing afterward; ordinary takeover tests and the full watcher-arm suite pass. Updated Generation reuse documentation. Syntax, diff checks, and ShellCheck pass with existing SC1091/SC2034 warnings excluded. ci-2 remains unchanged

* no-mistakes(document): Clarify watcher take-over recovery and restart limits
…cessor already closed (kunchenguid#6355)

* fix(bin): restore supervision host hand-back continuity

* no-mistakes(review): Scope host hand-back failure fallback to lost pending:handling

* no-mistakes(review): Remove stray scratch test copy tests/.tmp-rest.test.sh

* no-mistakes(test): Initialise successor globals so early hand-back survives set -u

* no-mistakes(document): Document host hand-back downtime failure and Claude lost-handback notice

* no-mistakes(ci): I fixed the Greptile finding. The rule that must hold: when the supervision host hands back an actionable wake, its rewake is refused, and no watcher is healthy, the hand-back still has to reach main as a delivered notice. That must be true whether the recovery marker is `pending:handling` or `announced:handling`. Only one place applies this check: the lost hand-back fallback in `bin/fm-claude-stop-autoarm.sh`. **Fix:** that check now accepts both `pending:handling:*` and `announced:handling:*` tokens (a one-line change). Nothing else in the fallback changed: - Refusals on any other marker, such as an already acknowledged one, still exit 0 silently and open no failure episode. - The notice is still sent once per episode, and repeats are recorded as `failed-suppressed`. **Tests:** - `tests/fm-claude-stop-autoarm.test.sh`: the lost hand-back test now runs as a shared helper with two variants, one writing a `pending:handling` marker and a new one writing `announced:handling` (`test_host_lost_announced_handback_notifies_once_per_episode`). - `tests/fm-supervision-host.test.sh`: the end-to-end test where downtime restoration fails is now a shared helper too, with a new `announced` variant (`test_claude_stop_hook_notifies_when_closed_announced_successor_downtime_restore_fails`). It moves the handling episode to `announced` before the host hands back. Without the fix the hook would exit 0 here; the test requires exit 2, `outcome=failed` and a delivered failure notice. **Verification (all under nice -n 10):** - The full `tests/fm-claude-stop-autoarm.test.sh` suite passed (rc=0), including both lost hand-back variants and the benign-refusal test. - In `tests/fm-supervision-host.test.sh` I ran only the four hand-back test functions, all passing (rc=0). The suite can't run single functions, so I used a temporary copy with a trimmed test list and deleted it afterwards; `git status` shows only the 3 intended files changed. - shellcheck is clean on all three changed files. I did not run `bin/fm-lint.sh`. - I did not run the new tests against the unfixed code; the claim that they fail without the fix comes from reading the old check
dubiousenvelope and others added 8 commits October 7, 2026 15:48
…te (kunchenguid#6809)

The OpenCode session plugin's shouldArm kept its own copy of the need
test that only looked for in-flight task records, while the turn-end
guard decides with fm_supervision_needed in bin/fm-supervision-lib.sh,
which also counts registered process-event sources and trusted custom
checks. With an empty fleet but any registered source or check, the
guard blocked every turn end while the plugin declined to arm - a loop
the guard's own repair line could not resolve because it names the
plugin as the fix.

The plugin now delegates the decision to the shared predicate through
bash, keeping the local away-record decline and the x-mode.env arm
override. OpenCode plugin test fixtures now carry the real predicate
their arming path sources, and the arm suite gains six cases asserting
the plugin's decision against the shared verdict over the same
synthetic state directories.

Co-authored-by: Mia Sun <mia@Bigs-Mac-mini.localdomain>
kunchenguid#6792)

* fix(bin): resolve a pending reply only from its own task's status line

Remote reply ingestion handed every corr= token in a mate's payload to
fm_pending_reply_try_resolve together with that mate's own status log,
so one mate echoing another mate's token resolved the other request.
Honor a status-file override only when it is the record's own
parent_status, and match the corr= token as a whole word.

Fixes kunchenguid#6538

* no-mistakes(document): docs: scope remote reply settlement to the asked mate
…nguid#6784)

agent-skill-trigger-index claims to be the complete agent-only trigger
index but omitted operational-home-layout, session-start-recovery,
validation-supervision, ship-landing, scout-completion, and
away-quiet-supervision. Add each with its own description's trigger,
placed beside the related entries.

The decision-hold-lifecycle redirect stub stays out, per triage.

Fixes kunchenguid#6503
# Conflicts:
#	.agents/skills/harness-adapters/references/harness/claude.md
#	.agents/skills/harness-adapters/references/harness/codex.md
#	.agents/skills/harness-adapters/references/harness/cursor.md
#	.agents/skills/harness-adapters/references/harness/grok.md
#	.agents/skills/harness-adapters/references/harness/omp.md
#	.agents/skills/harness-adapters/references/harness/opencode.md
#	.agents/skills/operational-home-layout/SKILL.md
#	.pi/extensions/fm-branch-supervision.ts
#	bin/backends/herdr.sh
#	bin/fm-brief.sh
#	bin/fm-classify-lib.sh
#	bin/fm-claude-trust.sh
#	bin/fm-composer-lib.sh
#	bin/fm-config-inherit-lib.sh
#	bin/fm-control.sh
#	bin/fm-git-strip-ai-trailers.sh
#	bin/fm-lint.sh
#	bin/fm-pending-reply-lib.sh
#	bin/fm-procevent-lavish.sh
#	bin/fm-remote-home-provision.sh
#	bin/fm-spawn.sh
#	bin/fm-teardown.sh
#	bin/fm-test-run.sh
#	bin/fm-timeout-lib.sh
#	bin/fm-watch.sh
#	docs/agent-control.md
#	docs/architecture.md
#	docs/calm-mode-feasibility.md
#	docs/calm.md
#	docs/configuration.md
#	docs/fm-test-portable-shards.md
#	docs/scripts.md
#	docs/verification/runtime-backends.md
#	docs/watcher-continuity.md
#	tests/fm-backend-herdr-presentation-e2e.test.sh
#	tests/fm-backend-herdr.test.sh
#	tests/fm-calm-claude-mod.test.sh
#	tests/fm-calm-pi-extension.test.sh
#	tests/fm-claude-trust.test.sh
#	tests/fm-control-relaunch.test.sh
#	tests/fm-kimi-harness.test.sh
#	tests/fm-remote-job.test.sh
#	tests/fm-spawn-dispatch-profile.test.sh
# Conflicts:
#	bin/fm-config-inherit-lib.sh
#	bin/fm-control.sh
#	bin/fm-spawn.sh
#	bin/fm-test-run.sh
#	docs/architecture.md
#	docs/fm-test-portable-shards.md
…Pi stand-in hosts

Move status_own_open_decisions next to status_open_decisions so pending-reply recovery, which sources the decision lib without the classify lib, still sees a mate's open decision. Let the supervision tools fall back to the reconstructed stock shell and call header when Pi's ToolExecutionComponent lacks the incremental update API, so upstream's per-version header test and the fork's delegation to the stock row both hold.
@MrGTV-love
MrGTV-love force-pushed the fm/fm-upstream-sync branch from d280d66 to 6027282 Compare October 8, 2026 11:50
@MrGTV-love MrGTV-love changed the title feat: sync upstream Firstmate features and reliability fixes feat: integrate upstream Firstmate capabilities and reliability fixes Oct 8, 2026
…tcher after approximately 30 seconds even though remote relaunch allows 120 seconds; earlier healthy spawn scenarios in the failing CI run took 41–56 seconds. Extended the test’s polling allowance to 150 seconds, preserving all relaunch, wake, ledger, endpoint, and route assertions. Updated the timeout documentation; production behavior and CI job timeouts are unchanged. Verification: /bin/bash tests/fm-remote-secondmate-lifecycle-e2e.test.sh completed with ALL TESTS PASSED. Two additional delayed smoke attempts reached the production 120-second relaunch timeout and correctly failed the retained success assertion rather than the premature watcher-wait deadline. Temporary smoke scaffolding was removed. No other pipeline phases were invoked
@MrGTV-love
MrGTV-love force-pushed the fm/fm-upstream-sync branch from 6027282 to 70383df Compare October 8, 2026 13:52
@MrGTV-love MrGTV-love changed the title feat: integrate upstream Firstmate capabilities and reliability fixes feat: sync Firstmate upstream capabilities and reliability fixes Oct 8, 2026
# Conflicts:
#	bin/fm-test-run.sh
#	docs/fm-test-portable-shards.md
…ged current origin/main (36675e0) into the branch, preserving both parents and merge history; merge commit is f73b91d. No rebase or push was performed. ci-1: corrected the retained-Gerrit fixture to use ordinary landed cleanup rather than forced-drop cleanup, preserving its retained-artifact record and change URL. ci-2: supplied the required drop file at the shared Herdr namespace teardown fixture boundary, allowing every namespace arm to reach lock validation without weakening production namespace or custody behavior. ci-4: split supervision coverage into 60 host cases and 13 host+hook cases, kept shared helpers in one file, released registered live fixtures between cases, and updated runner registration, family/changed-file selection, hints, documentation, and named consumers. Removed an obsolete timing-dependent Pi comparison and isolated coverage fixtures from production duration growth. Verification passed: nine focused captain-hold cases, nine teardown cases, all 13 hook cases, eight focused runner checks, and source-following ShellCheck for the changed split and runner tests. bin/fm-test-run.sh --check-coverage passes across nine serial shards, with a largest modeled shard of 1199053 ms against the 1200000 ms target. Verification limits: the full host suite did not pass locally; fresh-process smoke of its final 16 cases passed 10 and failed six watcher/engine timing assertions during observed high machine load. Load is not a proved cause. The host hint is a documented historical CI interval estimate; the hook hint is a successful local measurement, not new-head Ubuntu timing. CI job timeouts were unchanged. Temporary smoke scaffolding was removed, and no other pipeline phases or pipeline-control commands were invoked
…se start, origin/main remained at 5c4471c, already merged into this branch; merge history and accepted repairs were preserved. ci-1/ci-5: refreshed materially changed duration weights and added measured hints for newly merged tests, using successful Ubuntu CI markers only. Replaced historical split-supervision estimates with actual successful measurements and expanded the runner/workflow to ten serial shards. Coverage passes with zero unhinted serial members and a largest modeled shard of 1,121,293 ms against the unchanged 1,200,000 ms target. ci-2: corrected the shared watcher fixture’s unrelated cadence disablement and separated real retained-ledger publication from watcher delivery. Collector survival, no-overlap, actual overdue-obligation, and durable-wake assertions remain intact. ci-3: authorized unfinished-worker cleanup with retained captain discard words, without recording a PR handoff that would prematurely release capacity. ci-4: supplied recorded merged-PR proof for the gone-worktree spend fixture. Production teardown custody and process-audit behavior are unchanged. Also fixed both Python-wrapper fixtures to resolve sys.executable rather than embed a shell-based version-manager shim, and corrected the collector deadline assertion to permit valid early expiration while still forbidding a third source attempt. Documentation records measurement provenance and fixture contracts. No timeout values were changed. Verification passed: coverage guard; parsed workflow matrix and timeout contracts; exact-budget, producer-failure, and invalid-shard runner cases; retained collector and two sibling publication scenarios; the exact capacity-release scenario; 11 teardown spend/admission/custody cases; focused early-deadline scenarios across four reader inputs. Thirteen CPU-pass scenarios passed before a later probe exceeded its existing bound. Verification limits: broader runs encountered unchanged 10-second backlog-read, 120-second local-merge and watcher-poll, and 3-second CPU-probe bounds. The full runner attempt reached its command deadline after passing its initial selection cases. Full-suite or green-CI success is not claimed. Temporary verification scaffolds were removed. No push, other pipeline phase, or pipeline-control operation was invoked
@MrGTV-love
MrGTV-love merged commit fd9b8b0 into main Oct 8, 2026
22 checks passed
MrGTV-love added a commit that referenced this pull request Oct 11, 2026
fix(bin): merge upstream main through fb75c1f and record the squashed #43 sync as merged
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.