Skip to content

feat(bin): sync fork with upstream and repair merged-PR outcome reporting - #26

Merged
adibirzu merged 71 commits into
mainfrom
fm/fm-upstream-sync-0829
Aug 31, 2026
Merged

adibirzu merged 71 commits into
mainfrom
fm/fm-upstream-sync-0829

Conversation

@adibirzu

Copy link
Copy Markdown
Owner

Intent

Sync the captain's Firstmate fork (origin = https://github.com/adibirzu/firstmate.git) with upstream (https://github.com/kunchenguid/firstmate.git) while preserving every fork-only commit. Bring adibirzu/firstmate main up to date with kunchenguid/firstmate main by merging upstream into working branch fm/fm-upstream-sync-0829 and opening a PR against the fork. Do not push features to upstream. New work lands in the fork and is verified there; upstream is a source to pull from, not a destination.

Prefer a merge commit that preserves fork-only history over a rebase that rewrites it. Conflicts must keep fork-only features, skills, and scripts present and working (harness-creator, model fallback, OpenRouter capacity, Graphify, federation, quota-aware dispatch, spawn-capacity, graph-first, ask-user-authority independent of yolo) while keeping upstream safety, supervision, and bug-fix changes (proved GitHub merge outcomes, busy declared-pause cadence, Claude hung-claim recovery, Pi 0.84.4 renderer compatibility, bounded remote transport lanes). Respect AGENTS.md size and one-owner rules. If a conflict would drop a fork-only safety or routing contract, keep the fork contract.

A post-merge repair publishes a merged PR through one outcome path: do not mark notified before the shared merge-outcome reporter, or the first merge is swallowed. Map tests/assets so --changed can select the board-render suite.

origin/main advanced to 18c7472 (opencode secondmate-home supervision arming, #23) during the sync; that commit is merged into this branch as well. Never merge the PR. Never push to upstream. Open the PR on origin (adibirzu/firstmate) only.

What Changed

  • Merged upstream/main and origin/main (fix(plugins): arm opencode supervision in secondmate homes #23) into the fork through merge commits that keep fork-only history intact, pulling in upstream supervision, safety, and bug-fix work across bin/, .pi/extensions/, docs/, and tests/: proved GitHub merge outcomes via the new bin/fm-merge-outcome-lib.sh, bounded concurrent remote transport lanes, per-home summary ledgers (bin/fm-home-summary-refresh.sh), markerless remote secondmate reconciliation (bin/fm-secondmate-reconcile.sh), Claude auto-arm hung-claim recovery, busy declared-pause cadence, persistent Pi supervision branch model selection (.pi/extensions/lib/fm-branch-model-picker.ts), and Pi 0.84.4 renderer compatibility.
  • Repaired merged-PR publication in bin/fm-watch.sh so a merged poll goes through the shared fm_merge_outcome_report path only: the pre-call fm_pr_poll_merge_mark_notified write and its local absorbed short-circuit are removed, since marking notified first made the first merge observation look already recorded and swallowed it.
  • Post-merge fixes: mapped tests/assets/* in bin/fm-test-run.sh so --changed selects the board-render suite; ran the coverage-guard comm comparisons under LC_ALL=C to match the already-LC_ALL=C sorts, with the locale constraint documented at the owner; dropped a duplicated $INBOX_SECTION block in bin/fm-brief.sh; and moved the fm-home-summary-refresh.sh call in bin/fm-spawn.sh after TREEHOUSE_LEASE_ABORT_CLEANUP=0 to close a lease-abort window.

Risk Assessment

✅ Low: The fix round is three small, verified corrections (one test-duplicate deletion, two-line brief dedup, one statement reorder in fm-spawn.sh), the merged-poll path now matches upstream exactly and is covered by existing regression tests, and no fork-only file or upstream safety guard was lost in either merge; the only remaining issue is a cosmetic missing deletion-guard in a new test-selection arm.

Testing

Ran the smallest set of suites that carry this branch's intent (11 suites total, all passing) plus two live fail-before/pass-after reproductions on the real CLI surface: the coverage guard under the ambient locale, and --changed selecting the board-render suite from a single tests/assets edit. The four merge-resolution behaviours the recorded decisions call for each have a named passing assertion, and the fork-only contracts (ask-user-authority, Graphify, model fallback, OpenRouter quota, spawn-capacity) are green after the merge. No UI, HTML, CSS or renderer surface changed in this diff — the changes are shell scripts, a generated prompt, and test mapping — so no screenshot applies; the generated briefs are attached as the actual prompt an agent receives, and the rendered board HTML plus its parsed render output are carried over from the earlier round for the board-render suite (this host has no Chrome binary, and that suite renders through a Node harness rather than a browser). Worktree left clean, all evidence written to the dedicated evidence directory.

Evidence: Coverage guard: before/after under ambient en_US.utf8 locale

Source: Coverage guard: before/after under ambient en_US.utf8 locale

# BEFORE the fix (bin/fm-test-run.sh as of merge commit 30324eb), ambient en_US.utf8: $ bin/fm-test-run.sh --check-coverage comm: file 2 is not in sorted order comm: input is not in sorted order exit=1 # AFTER the fix (85f62a3), same ambient locale: $ bin/fm-test-run.sh --check-coverage FM_TEST_COVERAGE ok total=196 parallel=24 serial=161 serial_shards=4 herdr=11 exit=0

$ locale | head -2
LANG=en_US.utf8
LANGUAGE=

# BEFORE the fix (bin/fm-test-run.sh as of merge commit 30324eb), ambient en_US.utf8:
$ bin/fm-test-run.sh --check-coverage
comm: file 2 is not in sorted order
comm: input is not in sorted order
exit=1

# AFTER the fix (85f62a3), same ambient locale:
$ bin/fm-test-run.sh --check-coverage
FM_TEST_COVERAGE ok total=196 parallel=24 serial=161 serial_shards=4 herdr=11
exit=0
Evidence: --changed selects the board-render suite from a single tests/assets edit

Source: --changed selects the board-render suite from a single tests/assets edit

=== BEFORE (base commit 18c7472) === $ bin/fm-test-run.sh --list --changed --base HEAD fm-test-run: no changed-test mapping for source path: tests/assets/board-render-harness.mjs exit=2 === AFTER (this branch, 85f62a3) === $ bin/fm-test-run.sh --list --changed --base HEAD tests/fm-bearings-board-render.test.sh tests/fm-bearings-snapshot.test.sh tests/fm-fleet-snapshot-view.test.sh tests/fm-home-summary-refresh.test.sh exit=0

# Intent: "Map tests/assets so --changed can select the board-render suite."
#
# Minimal reproduction: one comment line appended to
# tests/assets/board-render-harness.mjs is the ONLY working-tree change, so the
# selection below is attributable to that asset alone. The edit was reverted
# afterwards; `git status --porcelain` prints nothing.
#
# Transcript of the real commands run in this worktree:

=== BEFORE (base commit 18c7472, its own bin/fm-test-run.sh) ===
$ bin/fm-test-run.sh --list --changed --base HEAD
fm-test-run: no changed-test mapping for source path: tests/assets/board-render-harness.mjs
exit=2

=== AFTER (this branch, 85f62a3) ===
$ bin/fm-test-run.sh --list --changed --base HEAD
tests/fm-bearings-board-render.test.sh
tests/fm-bearings-snapshot.test.sh
tests/fm-fleet-snapshot-view.test.sh
tests/fm-home-summary-refresh.test.sh
exit=0

At the base commit the runner failed closed on the unmapped asset and refused
the whole --changed selection. On this branch the same edit selects the
board-render suite plus the other suites that reference the shared asset, and
that selected suite passes:

$ bin/fm-test-run.sh tests/fm-bearings-board-render.test.sh
FM_TEST_END 2026-08-29T17:10:02Z tests/fm-bearings-board-render.test.sh exit=0 duration_ms=1617 gate_skip=false
Evidence: Intent-bearing assertions at target commit 85f62a3

Source: Intent-bearing assertions at target commit 85f62a3

ok - fm-brief.sh: ship and scout briefs deliver the instruction-inbox contract exactly once ok - an interrupt during the post-publication refresh never returns a live task's lease ok - a repeatedly signalled shutdown still releases ownership for the next worker ok - barely healthy worker failures remain bounded by the restart guard ok - validated merged polls notify once and retire before the next watcher cycle ok - a repeat identical merged poll for an already-notified task is absorbed, never queued as a main-blocking row ok - a different merged PR for the same task gets its own first notification ok - GitHub and GitLab exact merged results share one retirement path FM_TEST_SUMMARY total=5 failed=0 skipped_gate=0 duration_ms=534145 (fork-only contracts) FM_TEST_SUMMARY total=6 failed=0 skipped_gate=0 duration_ms=48134

# Contract behaviour at target commit 85f62a3 (ambient locale en_US.utf8)
# Only the intent-bearing assertions are quoted; each line is the suite's own output.

$ bin/fm-test-run.sh tests/fm-test-run.test.sh tests/fm-brief.test.sh \
    tests/fm-spawn-worktree-settle.test.sh tests/fm-remote-job.test.sh \
    tests/fm-pr-check-security.test.sh

## merge-resolution repair 1 - bin/fm-brief.sh: inbox contract delivered once
ok - fm-brief.sh: ship and scout briefs deliver the instruction-inbox contract exactly once

## merge-resolution repair 2 - bin/fm-spawn.sh: lease-abort window closed
ok - an interrupt during the post-publication refresh never returns a live task's lease

## merge-resolution repair 3 - tests/fm-remote-job.test.sh: each case runs once, both pass
ok - a repeatedly signalled shutdown still releases ownership for the next worker
ok - barely healthy worker failures remain bounded by the restart guard

## intent: a merged PR is published through one outcome path
ok - validated merged polls notify once and retire before the next watcher cycle
ok - a repeat identical merged poll for an already-notified task is absorbed, never queued as a main-blocking row
ok - a different merged PR for the same task gets its own first notification
ok - GitHub and GitLab exact merged results share one retirement path

## intent: --changed can select the board-render suite (coverage guard + selection contracts)
ok - changed selection covers dependents and fails closed for unmapped source
ok - portable shard union, disjointness, and coverage guard hold

FM_TEST_SUMMARY total=5 failed=0 skipped_gate=0 duration_ms=534145

# Fork-only contracts still working after the merge
$ bin/fm-test-run.sh tests/fm-ask-user-authority.test.sh tests/fm-graphify.test.sh \
    tests/fm-model-fallback.test.sh tests/fm-openrouter-quota.test.sh \
    tests/fm-spawn-capacity.test.sh tests/fm-bearings-board-render.test.sh
FM_TEST_END 2026-08-29T17:09:14Z tests/fm-ask-user-authority.test.sh exit=0 duration_ms=106 gate_skip=false
FM_TEST_END 2026-08-29T17:09:15Z tests/fm-graphify.test.sh exit=0 duration_ms=1161 gate_skip=false
FM_TEST_END 2026-08-29T17:09:25Z tests/fm-model-fallback.test.sh exit=0 duration_ms=10450 gate_skip=false
FM_TEST_END 2026-08-29T17:09:47Z tests/fm-openrouter-quota.test.sh exit=0 duration_ms=21044 gate_skip=false
FM_TEST_END 2026-08-29T17:10:00Z tests/fm-spawn-capacity.test.sh exit=0 duration_ms=13531 gate_skip=false
FM_TEST_END 2026-08-29T17:10:02Z tests/fm-bearings-board-render.test.sh exit=0 duration_ms=1617 gate_skip=false
FM_TEST_SUMMARY total=6 failed=0 skipped_gate=0 duration_ms=48134
Evidence: Generated ship brief (the prompt handed to the agent) — one inbox block

Source: Generated ship brief (the prompt handed to the agent) — one inbox block

You are a crewmate: an autonomous worker agent managed by firstmate. Work on your own; do not wait for a human.

# Task
{TASK}

# Herdr lifecycle declaration - NOT ENABLED
**HARD SAFETY GATE:** this scaffold cannot inspect the task text filled in above.
If the task will start, stop, delete, restart, profile, or otherwise drive Herdr lifecycle behavior, stop and regenerate the brief with `--herdr-lab` before dispatch.
Do not add Herdr lifecycle commands to this unguarded brief by hand.

# Setup
You are in a disposable git worktree of sample-proj, at a detached HEAD on a clean default branch.

**Verify isolation before anything else.** Run `pwd -P` and `git rev-parse --show-toplevel`; both must resolve to the disposable task worktree you were launched in, such as a treehouse pool path or an Orca-managed worktree, not the primary checkout firstmate operates from.
The path check is authoritative: `git rev-parse --git-dir` and `git rev-parse --git-common-dir` can help inspect the repo, but they do not prove you are outside the primary checkout.
If the top-level path is the primary checkout or not the worktree you were launched in, STOP - do not branch or commit here - append `blocked: launched in primary checkout, not an isolated worktree` to the status file and stop.

1. First action: create your branch: `git checkout -b fm/demo-ship-1`
2. Run `no-mistakes doctor`; if it reports the repo is not initialized here, run `no-mistakes init`.

# Graph-first context
Before reading files to understand this project, query the code graph first: call the `graph_first` MCP tool, or run `devviz graph-first "<question>"`, or POST to `http://127.0.0.1:8000/api/kag/graph-first`.
It returns a bounded set of relevant files with freshness; use it to target your file reads, and never parse `data/graphs/*/graph.json` directly.
If the graph-first surface is unreachable, proceed with normal targeted file reads - it is an accelerator, never a blocker.

# Rules
1. Never push to the default branch. Never merge a PR.
2. Stay inside this worktree; modify nothing outside it.
3. Use gh-axi for GitHub operations and chrome-devtools-axi for browser operations.
4. Report status by appending one line:
   `echo "{state}: {one short line}" >> '/tmp/fm-brief-evidence.Z31b1x/state/demo-ship-1.status'`
   States: working, needs-decision, blocked, paused, done, failed.
   Each append wakes firstmate, so report sparingly: only phase changes a supervisor
   would act on (setup done, bug reproduced, fix implemented, validation passed) and the
   needs-decision/blocked/paused/done/failed states. No step-by-step FYI progress lines;
   firstmate reads your pane for that.
   A mid-task `working:` line (including setup complete) is nonterminal: do not end the
   turn after it; continue the same stage until a defined `done:` gate under Definition of done.
   Use `paused: {why}` - distinct from `blocked:` - ONLY when you are deliberately idling on a
   known external wait you expect to clear on its own (an upstream release, a rate-limit reset,
   a scheduled window): firstmate then leaves your idle pane alone and rechecks it on a long
   cadence instead of treating it as a possible wedge. Use `blocked:` when you are stuck and need help.
5. If you hit the same obstacle twice, append `blocked: {why}` and stop; firstmate will help.
6. If a decision belongs above the implementation worker (product choices, destructive actions, ask-user findings),
   append `needs-decision: {summary of options}` and stop. Firstmate will reply with the decision.
   A decision or blocker you opened stays open until a `resolved` line carrying its exact key lands; a later `done:` or `working:` line never closes it, even when the answer is what started that work.
   Firstmate's reply normally writes that closing line at answer time; when a blocker or wait clears WITHOUT a firstmate reply, append `resolved: {how it cleared}` yourself (same `[key=<slug>]` if you opened it with one) as you resume.
   If you already escalated an earlier decision or blocker on this task, give the new one its own `[key=<slug>]`, because every unkeyed escalation shares one identity and a second one cannot be tracked separately.
7. Never stop, restart, or update the shared `no-mistakes` daemon - it is one instance serving
   every lane/home, so restarting it kills other lanes' in-flight pipeline runs. On ANY no-mistakes
   daemon error, append `blocked: {the daemon error}` and stop; only firstmate manages the daemon.
8. A decision above your authority is reported only through rule 6's status-file mechanism, then you stop.
   Never render it as an interactive question, confirmation, menu, or any other construct that waits on a human reply - nobody reads this pane, so a prompt just burns your window idling for a reply that never comes.
9. Never arm your own watch, poll, sleep, or retry loop to wait on a step you already handed off, such as a validation run or CI.
   Firstmate already supervises every task centrally, and a worker-side loop burns turns for no signal firstmate lacks - follow that step's own response flow, or report and stop.

# Firstmate instruction inbox
Firstmate steers you through durable message files in '/tmp/fm-brief-evidence.Z31b1x/state/demo-ship-1.inbox'.
When a terminal message says an instruction is waiting there - and at any natural checkpoint when you are unsure - list '/tmp/fm-brief-evidence.Z31b1x/state/demo-ship-1.inbox'/*.msg, read and act on each message in numeric order, then acknowledge each handled message by moving it: `mv '/tmp/fm-brief-evidence.Z31b1x/state/demo-ship-1.inbox'/NNN.msg '/tmp/fm-brief-evidence.Z31b1x/state/demo-ship-1.inbox'/handled/`.
The move IS the acknowledgement: without it firstmate rings again and eventually treats you as stuck. An empty or absent inbox needs no action.

# Project memory
If `AGENTS.md` or `CLAUDE.md` already exists, or if this task produced durable project-intrinsic knowledge, run `/home/adi/.no-mistakes/worktrees/9737a3722f08/01M173ZB1K0K35PTBCF43YR10X/bin/fm-ensure-agents-md.sh .` in the worktree.
Record only project knowledge useful to almost every future session.
For anything the codebase already shows, prefer a pointer to the authoritative file, command, or doc over copying the detail.
If you touch a project `AGENTS.md` that lacks `## Maintaining this file`, add that short self-governance section from `/home/adi/.no-mistakes/worktrees/9737a3722f08/01M173ZB1K0K35PTBCF43YR10X/bin/fm-ensure-agents-md.sh` in the same pass.
Keep it proportionate: skip `AGENTS.md` edits for trivial tasks that produced no durable project knowledge.

# Definition of done
Delivery contract: mode=no-mistakes
The task is complete only when committed on your branch.
When you believe it is complete, append `done: {summary}` to the status file and stop.
Firstmate will then instruct you to run /no-mistakes to validate and ship a PR.

You drive no-mistakes by responding to its gates, not by implementing fixes.
Follow the guidance no-mistakes itself provides for the mechanics: it loads when you invoke /no-mistakes, and `no-mistakes axi run --help` plus the `help` lines in each `axi` response are authoritative and version-matched to the installed binary.
When starting no-mistakes, make `--intent` preserve all relevant content from this brief's `# Task` section plus every later accepted Firstmate requirement, clarification, constraint, exclusion, and supersession, carrying only each requirement's current accepted form; retain direct requirements instead of substituting a diff summary, and exclude generic operational, status, delivery, and other scaffold boilerplate unless it is task-specific.
Do not hand-edit, commit, or fix findings yourself while a run is active - the pipeline applies every fix.

Two firstmate-specific rules layer on top of that guidance:
- ask-user findings are never yours to answer: escalate to firstmate (rule 6) and stop.
  Firstmate applies `ask-user-authority` and obtains any required captain decision.
  When the decision comes back, feed it to the gate with `no-mistakes axi respond` and let the pipeline apply it - do not route the question to "the user" or implement the fix yourself.
- Avoid `--yes`: it would silently bypass firstmate's authority check and any required captain escalation.

After /no-mistakes reports CI green (the CI-ready return point - do not wait for it to keep monitoring in the background until merge), append `done: PR {url} checks green` and stop. You are finished.
Evidence: Generated scout brief — one inbox block

Source: Generated scout brief — one inbox block

You are a crewmate: an autonomous worker agent managed by firstmate. Work on your own; do not wait for a human.

# Task
{TASK}

# Herdr lifecycle declaration - NOT ENABLED
**HARD SAFETY GATE:** this scaffold cannot inspect the task text filled in above.
If the task will start, stop, delete, restart, profile, or otherwise drive Herdr lifecycle behavior, stop and regenerate the brief with `--herdr-lab` before dispatch.
Do not add Herdr lifecycle commands to this unguarded brief by hand.

# Setup
You are in a disposable git worktree of sample-proj, at a detached HEAD on a clean default branch.
This is a SCOUT task: the deliverable is a written report, not a PR.
The worktree is your laboratory - install, run, edit, and make scratch commits freely; all of it is discarded at teardown.
The report is the only thing that survives, so anything worth keeping must be in it.

# Graph-first context
Before reading files to understand this project, query the code graph first: call the `graph_first` MCP tool, or run `devviz graph-first "<question>"`, or POST to `http://127.0.0.1:8000/api/kag/graph-first`.
It returns a bounded set of relevant files with freshness; use it to target your file reads, and never parse `data/graphs/*/graph.json` directly.
If the graph-first surface is unreachable, proceed with normal targeted file reads - it is an accelerator, never a blocker.

# Rules
1. Never push to any remote and never open a PR.
2. Stay inside this worktree; the only files you may write outside it are the report and the status file below.
3. Use gh-axi for GitHub operations and chrome-devtools-axi for browser operations.
4. Report status by appending one line:
   `echo "{state}: {one short line}" >> '/tmp/fm-brief-evidence.Z31b1x/state/demo-scout-1.status'`
   States: working, needs-decision, blocked, paused, done, failed.
   Each append wakes firstmate, so report sparingly: only phase changes a supervisor
   would act on and the needs-decision/blocked/paused/done/failed states. No step-by-step
   FYI progress lines; firstmate reads your pane for that.
   Use `paused: {why}` - distinct from `blocked:` - ONLY when you are deliberately idling on a
   known external wait you expect to clear on its own (an upstream release, a rate-limit reset):
   firstmate then leaves your idle pane alone and rechecks it on a long cadence instead of
   treating it as a possible wedge. Use `blocked:` when you are stuck and need help.
5. If you hit the same obstacle twice, append `blocked: {why}` and stop; firstmate will help.
6. If a decision belongs to a human (product choices, destructive actions),
   append `needs-decision: {summary of options}` and stop. Firstmate will reply with the decision.
   A decision or blocker you opened stays open until a `resolved` line carrying its exact key lands; a later `done:` or `working:` line never closes it, even when the answer is what started that work.
   Firstmate's reply normally writes that closing line at answer time; when a blocker or wait clears WITHOUT a firstmate reply, append `resolved: {how it cleared}` yourself (same `[key=<slug>]` if you opened it with one) as you resume.
   If you already escalated an earlier decision or blocker on this task, give the new one its own `[key=<slug>]`, because every unkeyed escalation shares one identity and a second one cannot be tracked separately.
7. Never stop, restart, or update the shared `no-mistakes` daemon - it is one instance serving
   every lane/home, so restarting it kills other lanes' in-flight pipeline runs. On ANY no-mistakes
   daemon error, append `blocked: {the daemon error}` and stop; only firstmate manages the daemon.
8. A decision above your authority is reported only through rule 6's status-file mechanism, then you stop.
   Never render it as an interactive question, confirmation, menu, or any other construct that waits on a human reply - nobody reads this pane, so a prompt just burns your window idling for a reply that never comes.
9. Never arm your own watch, poll, sleep, or retry loop to wait on a step you already handed off, such as a validation run or CI.
   Firstmate already supervises every task centrally, and a worker-side loop burns turns for no signal firstmate lacks - follow that step's own response flow, or report and stop.

# Firstmate instruction inbox
Firstmate steers you through durable message files in '/tmp/fm-brief-evidence.Z31b1x/state/demo-scout-1.inbox'.
When a terminal message says an instruction is waiting there - and at any natural checkpoint when you are unsure - list '/tmp/fm-brief-evidence.Z31b1x/state/demo-scout-1.inbox'/*.msg, read and act on each message in numeric order, then acknowledge each handled message by moving it: `mv '/tmp/fm-brief-evidence.Z31b1x/state/demo-scout-1.inbox'/NNN.msg '/tmp/fm-brief-evidence.Z31b1x/state/demo-scout-1.inbox'/handled/`.
The move IS the acknowledgement: without it firstmate rings again and eventually treats you as stuck. An empty or absent inbox needs no action.

# Definition of done
Write your findings to `/tmp/fm-brief-evidence.Z31b1x/data/demo-scout-1/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, you may host the Lavish review loop yourself (poll, revise, re-serve, staying alive) instead of handing it back to firstmate.
Before reporting done, read and follow `/home/adi/.no-mistakes/worktrees/9737a3722f08/01M173ZB1K0K35PTBCF43YR10X/.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: {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.
Evidence: Parsed render output of the bearings board

Source: Parsed render output of the bearings board

{
  "stats": [
    {
      "n": 0,
      "label": "need you"
    },
    {
      "n": 1,
      "label": "underway"
    },
    {
      "n": 1,
      "label": "landed recently"
    },
    {
      "n": 1,
      "label": "charted next"
    }
  ],
  "charted": [
    {
      "title": "Map tests/assets so --changed selects the board-render suite",
      "sub": "queued behind the sync · adibirzu/firstmate",
      "badges": [
        {
          "tone": "warn",
          "text": "waiting"
        }
      ],
      "pickable": true
    },
    {
      "title": "Main inventory integrity",
      "sub": "main inventory · adibirzu/firstmate",
      "badges": [
        {
          "tone": "danger",
          "text": "needs repair"
        }
      ],
      "pickable": false
    }
  ],
  "empty": [],
  "more": [],
  "error": ""
}
Evidence: Merge shape: fork-only history preserved, no rebase rewrite

Source: Merge shape: fork-only history preserved, no rebase rewrite

# Merge shape: fork history preserved (no rebase rewrite)

$ git log --graph --oneline --decorate -14
* 05373f8 (HEAD, fm/fm-upstream-sync-0829) no-mistakes(review): remove merged duplicates and close spawn lease-abort window
*   30324eb merge: origin/main #23 into upstream-sync branch
|\  
| * 18c7472 (origin/main) fix(plugins): arm opencode supervision in secondmate homes (#23)
* | bdcce1f fix(watch): publish a merged PR through one outcome path
* |   41ad2b2 merge: sync fork with upstream/main, preserving fork-only work
|\ \  
| |/  
|/|   
| * f66be0f fix(pi): restore Pi 0.84.4 renderer compatibility (#3261)
| * 52b59a1 fix(pi): gate first provider call on startup context (#3158)
| * a390659 feat(bin): publish per-home summary ledgers (#3222)
| * 4207214 fix(bin): accelerate and bound changed test runs (#3250)
| * 1fd7ea2 feat(bin): add concurrent bounded remote transport lanes (#3210)
| * c651b59 fix(pi): surface requested outcomes without replaying fleet events (#3211)
| * bca584a fix(bin): prioritize active pipeline-owned crew runs (#3194)
| * 4f89f5b fix(pi): prevent duplicate captain outcome reports (#3184)
| * 7ee0c19 fix(bin): verify the real GitHub merge outcome instead of reporting an unproved merge (#3064)

$ git rev-list --merges --parents -n2 HEAD~1 HEAD~3  # merge commits keep both parents
30324eb parents=[bdcce1f 18c7472] merge: origin/main #23 into upstream-sync branch
41ad2b2 parents=[1a92f9e f66be0f] merge: sync fork with upstream/main, preserving fork-only work

$ git remote -v
origin	https://github.com/adibirzu/firstmate.git (fetch)
origin	https://github.com/adibirzu/firstmate.git (push)

# fork-only commits still reachable from HEAD (sample)
1a92f9e fix(watch): mark merged PR polls notified so one merge wakes the captain once (#24)
db5dd6d sync: bring fork main to upstream/main, preserving all fork-only work (#22)
53336ac feat(skills): add harness-creator skill pack with skills lock
3c544d6 feat(bin): add OpenRouter capacity reader with per-model cooldown state (#14)
ef411d6 feat(bin): add fleet-only Graphify orientation index with triggered consult and freshness checks (#12)
7fd119a feat(bin): land fleet federation, quota-aware dispatch, and runtime handoff, restoring main CI (#8)
7ec2522 feat(bin): add in-place runtime handoff and the quota statusline
0095a2b feat(spawn): refuse to spawn when the machine has no memory headroom
746c703 test(portability): stop the fleet suites assuming GNU sed, date and getent
a25901e test(federation): use the documented example operators, not this host's
31c3fb0 fix(herdr): scope the presentation lock namespace per operator
bb4d65c feat(fleet): cross-operator overflow on token exhaustion
Evidence: Earlier-round before/after reproductions of the merge-resolution defects

Source: Earlier-round before/after reproductions of the merge-resolution defects

# Merge-resolution repairs: before -> after (each reproduced, then fixed)

## 1. tests/fm-remote-job.test.sh - duplicated restart-guard block
$ git show 30324eb:tests/fm-remote-job.test.sh > /tmp/prefix.test.sh && bash /tmp/prefix.test.sh
ok - barely healthy worker failures remain bounded by the restart guard
ok - a repeatedly signalled shutdown still releases ownership for the next worker
not ok - the restart guard did not stop at the configured maximum
exit=1

$ bin/fm-test-run.sh tests/fm-remote-job.test.sh   # after 05373f8
ok - a repeatedly signalled shutdown still releases ownership for the next worker
ok - barely healthy worker failures remain bounded by the restart guard
ALL TESTS PASSED
FM_TEST_END 2026-08-29T16:26:36Z tests/fm-remote-job.test.sh exit=0 duration_ms=63373 gate_skip=false

## 2. bin/fm-watch.sh - the first merged PR was swallowed
$ (bin/fm-watch.sh reverted to 41ad2b2) bash tests/fm-pr-check-security.test.sh
not ok - first merged notification was not preserved: check: /tmp/fm-pr-check-security.yK2AT2/merged-retirement-once/home/state/z-stop.check.sh: stop-cycle
exit=1

$ bin/fm-test-run.sh tests/fm-pr-check-security.test.sh   # after bdcce1f
FM_TEST_END 2026-08-29T16:39:21Z tests/fm-pr-check-security.test.sh exit=0 duration_ms=344906 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=344966

## 3. bin/fm-brief.sh - the instruction inbox was emitted twice per brief
New regression test: tests/fm-brief.test.sh::test_instruction_inbox_contract_is_delivered_once_per_brief
(asserts on the generated brief - the prompt handed to the agent - not on the scaffold source)

$ (bin/fm-brief.sh reverted to the merged duplicate placement) bash tests/fm-brief.test.sh
not ok - ship brief delivers the instruction-inbox contract 2 times, expected exactly 1
exit=1

$ bash tests/fm-brief.test.sh   # after 05373f8
ok - fm-brief.sh: ship and scout briefs deliver the instruction-inbox contract exactly once
exit=0

Generated briefs are attached: brief-scout.generated.md, brief-ship.generated.md
  $ grep -c '^# Firstmate instruction inbox$' brief-{scout,ship}.generated.md  ->  1 and 1
  (pre-fix bin/fm-brief.sh produced 2 and 2)

## 4. bin/fm-spawn.sh - lease-abort window after the task record was published
New regression test: tests/fm-spawn-worktree-settle.test.sh::test_interrupt_during_the_post_publication_refresh_keeps_the_lease
It delivers a real SIGTERM from inside the post-publication home-summary refresh.

$ (bin/fm-spawn.sh reverted to refresh-before-disarm) bash tests/fm-spawn-worktree-settle.test.sh
not ok - the abort path returned the leased worktree of a task that already exists in state/
exit=1

$ bash tests/fm-spawn-worktree-settle.test.sh   # after 05373f8
ok - an interrupt during the post-publication refresh never returns a live task's lease
exit=0
- Outcome: 🔧 1 issue found → auto-fixed ✅ across 2 runs (45m50s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • 🚨 tests/fm-remote-job.test.sh:771 - The merge resolution duplicated a ~115-line block verbatim: lines 771-886 repeat lines 654-769 (the "repeatedly signalled shutdown" test and the "barely healthy worker" restart-guard test). The second copy fails deterministically. Both copies use the same RESTART_CHILD_LOG="$TMP_ROOT/restart-children" (line 730 and line 847), the fixture only ever appends to it (printf ... >> "$FM_TEST_SUPERVISOR_CHILD_LOG"), and nothing truncates or removes it. Failure sequence: first restart block runs 3 children -> log has 3 lines -> [ 3 -eq 3 ] passes at line 765; second restart block runs 3 more -> log has 6 lines -> [ &#34;$(wc -l &lt; &#34;$RESTART_CHILD_LOG&#34;)&#34; -eq 3 ] at line 882 is false -> fail &#34;the restart guard did not stop at the configured maximum&#34;. (If the reused $RESTART_STATE instead prevents the second supervisor from starting children, the &gt;-truncated $TMP_ROOT/restart-supervisor.err then lacks the guard message and assert_grep at line 884 fails instead — the suite fails either way.) Fix: delete the duplicated block at lines 771-886.
  • ⚠️ bin/fm-brief.sh:391 - The conflict resolution kept both parents' $INBOX_SECTION placements, so the scaffold emits the whole "# Firstmate instruction inbox" block twice. Both parents put it in the same place after rule 7 (fork side added $RULE_NO_PROMPT/$RULE_NO_POLL above it; upstream added the blank line + section), and the union produced a literal repeat. It occurs in two scaffolds: lines 389 and 391 (scout), and lines 515 and 517 (ship). Concrete result: bin/fm-brief.sh writes a brief in which every spawned scout/ship agent reads the identical 6-line inbox/doorbell/acknowledgement contract twice back-to-back — duplicated prompt tokens against the home's startup-memory budget and an instruction block that reads as a generation bug. No test asserts the section appears once, so nothing catches it. Fix: drop one of each pair (keep the fork ordering after $RULE_NO_POLL).
  • ⚠️ bin/fm-spawn.sh:3302 - The resolution appended the fork's TREEHOUSE_LEASE_ABORT_CLEANUP=0 (line 3307) after upstream's new &#34;$SCRIPT_DIR/fm-home-summary-refresh.sh&#34; --best-effort || true (line 3302), opening a window the fork's own comment says must not exist ("The task now exists in state/, so fm-teardown.sh owns returning its worktree. Returning it from here after this point would pull the lease out from under a live task."). Failure sequence: the task record is already published (RELAUNCH_REPLACEMENT_PENDING cleared at line 3294, set lock released at 3301), spawn then runs the external home-summary refresh, the operator interrupts (SIGINT/SIGTERM) during that subprocess, and the trap spawn_abort_cleanup EXIT at line 941 still sees TREEHOUSE_LEASE_ABORT_CLEANUP=1, so line 3300-ish cleanup calls fm_treehouse_return &#34;$PROJ_ABS&#34; &#34;$WT&#34; (line 865) and removes the worktree of a task that now exists in state/ and is owned by fm-teardown.sh. Before the merge that window was effectively zero. Fix: move TREEHOUSE_LEASE_ABORT_CLEANUP=0 above the fm-home-summary-refresh.sh call.

🔧 Fix: remove merged duplicates and close spawn lease-abort window
1 info still open:

  • ℹ️ bin/fm-test-run.sh:1252 - The new tests/assets/* arm resolves the family by grepping every repo test for the asset's basename, with no guard for a path that no longer exists. Its two sibling arms both guard exactly this: bin/* wraps the lookup in if [ -e &#34;$path&#34; ] ("A deleted script has no consuming suite left to select", :1277-1282) and tests/fixtures/*/* wraps it in if [ -d &#34;tests/fixtures/$fixture_ref&#34; ] (:1256-1266). Concrete failure: retire the board-render harness by deleting both tests/assets/board-render-harness.mjs and its reference in tests/fm-bearings-board-render.test.sh in one change; families_for_changed_path then finds no test mentioning board-render-harness.mjs, emits __unmapped__:tests/assets/board-render-harness.mjs, and select_changed calls die &#34;no changed-test mapping for source path&#34; (:1313), so --changed hard-fails on a legitimate retirement instead of selecting nothing. Fix: guard the arm the same way, e.g. if [ -e &#34;$path&#34; ]; then families_for_test_reference ... ; fi. This does not affect the intent-required mapping, which works today.
🔧 **Test** - 1 issue found → auto-fixed ✅
  • ⚠️ bin/fm-test-run.sh:828 - Pre-existing (also present at base commit 18c7472, not introduced by this change): the coverage guard in bin/fm-test-run.sh calls comm on files sorted with LC_ALL=C sort but does not force the C locale on comm itself. Under the ambient en_US.utf8 locale comm aborts with "file 2 is not in sorted order", --check-coverage exits 1 with no diagnostic, and tests/fm-test-run.test.sh fails inside test_portable_shard_union_and_coverage_guard. Running LC_ALL=C bin/fm-test-run.sh --check-coverage prints FM_TEST_COVERAGE ok total=196 parallel=24 serial=161 serial_shards=4 herdr=11 and LC_ALL=C bin/fm-test-run.sh tests/fm-test-run.test.sh passes. Flagging so the captain can decide whether to fix the locale sensitivity separately; it is outside this branch's recorded scope.
  • bin/fm-test-run.sh tests/fm-remote-job.test.sh — 32 cases pass, restart-guard and shutdown tests each run once
  • git show 30324eb:tests/fm-remote-job.test.sh &gt; /tmp/prefix.test.sh &amp;&amp; bash /tmp/prefix.test.sh — reproduces not ok - the restart guard did not stop at the configured maximum (exit 1)
  • bin/fm-test-run.sh tests/fm-pr-check-security.test.sh — passes (exit 0, 345s), covering test_merged_poll_retires_once, test_merged_poll_reregistration_after_notification_is_absorbed, test_self_merge_and_poll_publish_one_outcome
  • Reverted bin/fm-watch.sh to 41ad2b2, ran bash tests/fm-pr-check-security.test.sh — reproduces not ok - first merged notification was not preserved, then restored the file
  • Added and ran tests/fm-brief.test.sh::test_instruction_inbox_contract_is_delivered_once_per_brief; verified it fails (2 times, expected exactly 1) against the pre-fix duplicate placement of $INBOX_SECTION
  • Added and ran tests/fm-spawn-worktree-settle.test.sh::test_interrupt_during_the_post_publication_refresh_keeps_the_lease; verified it fails (the abort path returned the leased worktree of a task that already exists in state/) against the pre-fix refresh-before-disarm ordering
  • bin/fm-test-run.sh tests/fm-bearings-board-render.test.sh tests/fm-bearings-board.test.sh — pass
  • bin/fm-test-run.sh --list --changed --base 18c7472 — 185 scripts selected including tests/fm-bearings-board-render.test.sh; with the tests/assets/* case removed the same command dies with no changed-test mapping for source path: tests/assets/board-render-harness.mjs
  • bin/fm-test-run.sh tests/fm-ask-user-authority.test.sh tests/fm-dispatch-select.test.sh tests/fm-graphify.test.sh tests/fm-model-fallback.test.sh tests/fm-openrouter-quota.test.sh tests/fm-spawn-capacity.test.sh — 6/6 pass
  • LC_ALL=C bin/fm-test-run.sh tests/fm-test-run.test.sh and LC_ALL=C bin/fm-test-run.sh --check-coverage — pass (both fail under en_US.utf8 for a pre-existing locale reason)
  • Manual: generated real scout and ship briefs with bin/fm-brief.sh and counted # Firstmate instruction inbox occurrences (1 each; 2 each with the pre-fix script)
  • Manual: built a real bearings board with bin/fm-bearings-board.sh build and rendered it through node tests/assets/board-render-harness.mjs
  • LC_ALL=C bin/fm-test-run.sh tests/fm-brief.test.sh tests/fm-spawn-worktree-settle.test.sh — final confirmation, 2/2 pass

🔧 Fix: run coverage-guard comm under LC_ALL=C
✅ Re-checked - no issues remain.

  • bin/fm-test-run.sh --check-coverage under ambient en_US.utf8, compared against the same command run from the pre-fix runner at 30324eb (reproduced the comm: file 2 is not in sorted order failure, then the fix)
  • bin/fm-test-run.sh --list --changed --base HEAD with one comment line appended to tests/assets/board-render-harness.mjs, run against both the base-commit runner (18c7472) and this branch, then the edit reverted
  • bin/fm-test-run.sh tests/fm-test-run.test.sh tests/fm-brief.test.sh tests/fm-spawn-worktree-settle.test.sh tests/fm-remote-job.test.sh tests/fm-pr-check-security.test.sh (5 suites, serial, all exit=0)
  • tests/fm-brief.test.sh::test_instruction_inbox_contract_is_delivered_once_per_brief
  • tests/fm-spawn-worktree-settle.test.sh::test_interrupt_during_the_post_publication_refresh_keeps_the_lease
  • tests/fm-remote-job.test.sh — verified the de-duplicated restart-guard and repeatedly-signalled-shutdown cases each appear once and pass
  • bin/fm-test-run.sh tests/fm-ask-user-authority.test.sh tests/fm-graphify.test.sh tests/fm-model-fallback.test.sh tests/fm-openrouter-quota.test.sh tests/fm-spawn-capacity.test.sh tests/fm-bearings-board-render.test.sh (fork-only contracts + the suite --changed now selects, all exit=0)
  • Manual inspection of the generated briefs captured under evidence: grep -c &#39;^# Firstmate instruction inbox$&#39; returns 1 for both the ship and scout brief
  • git status --porcelain after each transient-artifact step, confirming the worktree is clean and no temp copies remain
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

mremond and others added 30 commits August 21, 2026 07:24
* fix(bin): signal a captain call resolved in the log but still held

A captain call has two records and closing one has never closed the
other: a `resolved [key=...]` line closes the status-log fold, while the
backlog task held for the captain closes only through
`fm-captain-hold.sh answer`. Answering on the status side alone left no
trace of the disagreement - the fold went quiet, the durable record kept
saying the captain owed an answer, and nothing warned. The defect was
never the separation; it was the silence.

Add `fm-captain-hold.sh diverged`, a read-only report of that
contradiction, and print it from `fm-wake-drain.sh` as a bounded RECORD
DIVERGENCE section beside OPEN DECISIONS on every drain. It flags one
condition: a task still open and still carrying the captain-hold
annotations whose key was closed on the status side by the resolve verb,
under the collapsed identity or the legacy derived one.

It closes nothing, ever. A captain call closed wrongly leaves review
entirely, which is worse than the noise, so both reconciliation
directions stay human-owned and the printed hint names both - a
resolution is not proof the captain ruled, since a call can dissolve on a
false premise or turn out to have been a question of fact.

Three states are deliberately not divergence: a `captain-held` close is
the verified transfer `complete` writes, a still-open keyed decision
belongs to the OPEN DECISIONS fold, and a captain call with no routed
work item is legitimate rather than incomplete, so routed work is no part
of the test.

`fm-classify-lib.sh` gains `status_key_closing_verb`, which reports how
the status side currently reads one key by replaying the existing
`_fm_decision_fold_line` rule rather than re-deriving it, so the two
closing verbs stay distinguishable in one place. The per-wake cost is one
`tasks-axi list`, one key scan per status log, and the precise per-key
fold only for a key that already names a still-open task; the call is
hard-bounded so a slow backlog tool can never delay wake presentation.

* fix(document): Correct divergence lifecycle documentation

* fix(document): Neutralize divergence lifecycle prose
…escalation while a worktree is written (kunchenguid#2524)

* fix(watch): re-arm supervision after an abandoned auto-arm claim

A Claude auto-arm cycle that armed, delivered one rewake, and exited left
its single-flight lock behind. Both Stop-event participants then deferred
to that lock forever, because its recorded pid was still live: the
turn-end guard read it as recovery under way and allowed the stop, and the
next Stop firing treated it as another owner and declined to arm. On
2026-08-14 a home with two tasks in flight lost supervision for about 40
minutes with no watcher process and no watcher lock, its beacon frozen at
the one delivery, and both crewmates' finished reports sat in the durable
queue until an operator drained it by hand.

Abandonment is now proven from the epoch ledger instead of inferred from
pid liveness. A lock whose holder pid matches the ledger's own owner_pid
while the recorded outcome is anything other than arming has already
finished its decision, so that claim is reclaimed under the lock's steal
mutex, stops counting as recovery ownership in the guard, and is cleared
by the guard's terminal check rather than deferred to. A failed clear
re-blocks instead of allowing a blind stop, and an arming entry stays in
flight however old it is, because its owner foregrounds the arm for the
whole watcher cycle.

Issue kunchenguid#2251's PR kunchenguid#2263 does not cover this failure. It is closed and
unmerged, lives entirely in bin/fm-watch-arm.sh, and retires the stalled
watcher and matching stale watcher lock of an arm that is currently
running. Here no arm and no watcher were running and no watcher lock
existed, so it has nothing to retire and the home stays blind.

tests/fm-claude-stop-autoarm.test.sh covers the reclaim, the still-arming
and unnamed-owner cases that must keep the gate closed, and the failed
clear. tests/fm-turnend-guard.test.sh covers the guard side of the same
boundary. Both fail without this change.

* fix(watch): defer a wedge escalation while the task worktree is written

The wedge detector had two inputs, rendered pane quietness and the run
step, and neither can see a crew that is writing source, then tests, then
documentation behind a static pane. On 2026-08-14 one crewmate produced
eight consecutive possible-wedge escalations in a single afternoon, three
of them demanding deep inspection, while it was demonstrably working and
then committed. Every one of them cost a supervision turn to disprove by
hand.

Add write activity inside the crew's own recorded worktree as a third
liveness input. crew_worktree_written_since compares the worktree against
the caller's existing idle-window timer file, so -newer needs no clock
arithmetic, no temp file, and no portable mtime write. The probe runs only
inside the branch that was about to escalate, which bounds it to one
pruned, depth-bounded walk per window per FM_STALE_ESCALATE_SECS and
leaves the per-poll stale sweep exactly as cheap as before.

Positive evidence defers rather than cancels. The idle timer restarts so
the next window probes again, the escalation counter is neither advanced
nor reset so a later genuine wedge keeps the demand-deep-inspection
history it earned, and a .writing-since marker ages the whole deferral
chain so the pane still re-surfaces once per FM_PAUSE_RESURFACE_SECS,
through the same throttle shape a declared pause already uses, labeled as
a recheck rather than a wedge. This can only reduce false positives: every
absence of evidence, including no recorded worktree, a torn-down worktree,
a missing anchor, and a failed walk, falls through to the unchanged
escalation schedule, so a crew that writes nothing still escalates on the
existing timetable.

What the signal cannot see, by design or by construction:

- CPU burn with no writes, such as a long compaction, is invisible. That
  case keeps the old behavior exactly.
- A commit-only phase writes only .git, which is pruned first so that
  firstmate's own read-only git commands against the worktree can never
  make the probe self-fulfilling.
- Writes under the pruned generated trees, or deeper than
  FM_WORKTREE_WRITE_MAXDEPTH, do not count.
- The probe cannot attribute a write to the crew, so a background build or
  another process touching the tree looks the same. The hourly re-surface
  is what bounds that, and a churny file cannot buy silence.
- The away-mode daemon's own escalation path is deliberately untouched.

tests/fm-watch-triage.test.sh covers the classifier including the .git
prune, both halves of the live case on one fixture (quiet plus writing
defers, quiet plus silent still escalates and counts), and the bounded
re-surface. All three fail without this change.

* no-mistakes(review): prove autoarm claims by identity; skip mate-home write probe

* no-mistakes(document): document away-mode wedge boundary and probe filesystem limit

* no-mistakes(document): qualify turn-end recovery condition for abandoned auto-arm claims

* fix(watch): keep a write deferral scoped to its own idle window

Two consistency gaps in the worktree write probe, both found while reviewing
the wedge-deferral change on this branch.

A write deferral is a bounded chain: its .writing-since marker ages the whole
chain so a churning worktree still re-surfaces once per resurface window. That
is only sound while the chain belongs to the current quiet stretch, so every
path that restarts the idle-window timer has to drop it too. Two did not: the
corrupt-timer repair in wedge_timer_check, and both first-sight branches for a
captain-relevant status. A chain left over from an earlier quiet stretch made
the first deferral of the new window re-surface immediately instead of after a
full fresh window.

FM_WORKTREE_WRITE_PRUNE is a skip list, so clearing it reads as "skip nothing"
and is the obvious way to widen the probe to the whole depth-bounded tree.
Instead an empty list reported no evidence at all, quietly costing the wedge
detector its third liveness input on a home that meant to widen the walk. An
empty list now widens the walk, and the header says so.

Neither change alters when a stall that writes nothing escalates.

Regressions in tests/fm-watch-triage.test.sh cover all three paths and each
one fails on the pre-fix code.

* no-mistakes(review): honor an empty write-prune, bound the probe, share window_key

* no-mistakes(document): align probe knob count and guard regression-coverage ownership

* no-mistakes(lint): silence deliberate single-quote SC2016 in write-prune env test
…lared pause (kunchenguid#2748)

* fix(bin): give a captain hold the same bounded pause cadence as a declared pause

Two supervisors read a finished task's last status line and disagreed about which
declarations mean an idle endpoint is expected. bin/fm-inactive-reconcile.sh
suppresses its inactive-outcome scan only on `captain-held`, while the away-mode
daemon's wedge path gated deferral on `paused` alone. Both read the LAST line, so
the two verbs are mutually exclusive and no finished task waiting on a person
could satisfy both at once. Marking 11 such tasks `captain-held:` silenced the
900s outcome scan and immediately produced five possible-wedge escalations in one
batch, because the 240s wedge detector no longer saw a pause verb.

fm-classify-lib.sh's status_is_paused_or_captain_held already owns the combined
question, and bin/fm-watch.sh's ordinary-crew wedge path already asked it. This
extends that same answer to the paths still asking the narrower one:

- bin/fm-supervise-daemon.sh, all six sites, which form one subsystem and have to
  move together. classify_stale returns the pause action, reconcile_pause_tracking
  and migrate_watcher_pause_markers record and migrate the marker, and
  housekeeping defers the wedge and then re-surfaces the recheck. Changing only
  the stale-persistence gate would defer the escalation while
  reconcile_pause_tracking recorded nothing, so the wedge marker would persist and
  the sweep would `continue` past it forever: quiet, but never re-surfacing.
- bin/fm-watch.sh's secondmate stale gate, whose downstream owner
  pause_state_class already treats both declarations identically.
- bin/fm-push-transition-lib.sh's absorb, where either declaration already names
  the human the transition would report and the wait is already durably recorded.

Quieting alone would be half a fix, so the bounded re-surface had to reach a hold
too. A hold has no current-state mapping, unlike `paused`, so authoritative crew
state reports it as unknown and pause_state_class received `none`. An ordinary
crew recovers pause classification from that state through confirmed agent death,
which proves no live decision gate is being silenced. A secondmate's endpoint
liveness is deliberately never read there, because an idle mate is healthy by
design, so that confirmation is unavailable by construction and cannot be
required: without recovering the classification for a mate, every caller silenced
a held mate outright and its hold would rot invisibly. That promotion is bounded
by the declared-wait guard at the top of the function, so it can only reclassify a
task that already declared a wait and shows no positive working evidence.

Two narrow `status_is_paused` calls are deliberately left alone.
bin/fm-crew-state.sh's map_log_state is a current-state reporting contract, not a
wedge path; reporting a hold as `paused` would erase the distinction
status_key_closing_verb and fm-captain-hold.sh depend on, where a `captain-held`
close is a verified durable transfer and a `resolved` close claims outright
settlement. fm-classify-lib.sh's call inside status_is_captain_relevant needs no
change because that function's own case list already returns non-relevant for
`captain-held`.

bin/fm-inactive-reconcile.sh keeps its `captain-held` suppression as it is. Its
guard exists because a finished task's crew state still reports done from a
higher-priority source than the log, and a declared pause needs no such guard: the
scan only reports done or failed, and nothing else reaches its record path.
Widening it would change a separate subsystem's reporting contract, which this
defect does not require.

Coverage extends the existing colocated patterns for these predicates and asserts
both halves. tests/fm-daemon.test.sh covers the classification, the wedge marker
converting to pause tracking with no escalation, the bounded re-surface with its
window reset, and the boundary case where an answered hold stops claiming the
cadence. tests/fm-watch-triage.test.sh covers a held secondmate re-surfacing on
the same bounded cadence without being labeled a wedge.
tests/fm-supervision-events.test.sh covers the absorbed push transition. Every one
of these fails on the pre-fix code except the answered-hold boundary case, which
is there to pin that the quieting was not widened too far.

The `paused:` workaround appended to those 11 tasks is live supervision state and
is untouched here. It can be retired once this lands.

* no-mistakes(review): name the captain in a held task's bounded recheck

* no-mistakes(document): extend declared-wait supervision docs to captain-held holds
…guid#2758)

* fix(lint): name the installer when ShellCheck or actionlint is missing

A missing actionlint exited 127 like a bare command-not-found. Fail with
exit 1 and point at the pinned installer, matching the missing-ShellCheck
path, without weakening the version pin.

* test: isolate kimi and muse detection from inherited Cursor markers

Harness detection checks CURSOR_AGENT before ancestry, so these
markerless-adapter cases failed when the suite itself ran under Cursor.
Clear the verified markers the same way the secondmate harness tests already do.

* no-mistakes(document): Document Muse Cursor marker cleanup
…lled but inert (kunchenguid#2684)

* feat(checks): report tool updates that are available or installed but inert

Firstmate had no way to notice that tooling this home depends on needs an
update, and no way at all to notice the worse case: an update that installed
correctly and then did nothing.

That second case is why this exists. A tool that self-installs into
~/.local/bin while a version manager keeps its own older copy earlier on PATH
looks completely up to date to anything that asks only "is a newer version
published". On 2026-08-20 a Herdr update landed at 0.8.2 while an older 0.8.0
copy stayed earlier on PATH, so every Herdr command failed on a protocol
mismatch and firstmate could not read its own fleet.

bin/fm-tool-update-check.sh reports the two conditions separately:

  <tool> update available      a newer version exists at the update source.
  <tool> update not in effect  a newer copy is installed on this host, but
                               PATH still resolves an older one.

PATH skew is measured, never inferred. Every executable copy of a watched
command on PATH is asked for its own version and those answers are compared,
so one lookup cannot hide the skew, and a directory name is never read as a
version because a version manager's "latest" directory can hold an older
build. A copy that will not report a version is a check failure, not a pass.

The watched tools live in local, gitignored config/watched-tools.json, so
adding a tool is a config edit rather than a code change, and the file is
never propagated to another home. Update sources cover both shapes: a local
clone's commit distance from its remote branch, and a command's own version
and update announcement, including a tool like no-mistakes that prints its
version on one command and announces a new release on another.

The check prints one line when something needs attention and prints nothing
otherwise, so it rides the existing watcher state-check contract with its
trust binding instead of introducing a schedule of its own, and
state/.tool-updates keeps the same pending update from being reported on
every poll.

The check only reports. It never installs, updates, reorders PATH, touches a
version manager, or fetches into a watched repository; every git probe is
read-only.

Tests cover the skew case as a regression, and it was verified by mutation:
removing the skew report, or stopping after the first PATH hit as a single
lookup would, each make that test fail.

* no-mistakes(review): fix tool update check probe reporting, budget, and shim write

* no-mistakes(review): keep sweeps alive on broken patterns and oversized budgets

* no-mistakes(review): roll back failed arm, widen budget clamp, bound repo probe

* no-mistakes(review): guard git probes at the budget, record uncut findings

* no-mistakes(document): fix stale watched-tool report-record wording in docs and header

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

The behavior shard's watch-triage suite failed on the new worktree-write wedge
tests. Those five tests are the only ones in the file that do not use its
standard waits. They give a fixed 3 second liveness budget to the one poll that
now spawns the bounded worktree walk, and 4 seconds to an escalating watcher
where every other test in the file gives 10. On a loaded runner that poll
outlives the fixed budget, so the round is reaped before the deferral it asserts
on is recorded, and the test reports a lost deferral instead of the deferral
under test. Wait for a completed poll cycle through the file's own
wait_poll_cycle, which is what its header documents this hazard for, and use the
file's standard 100 tick exit budget.

Verified against a load that reproduces the failure: 11 of 12 runs failed
before, 8 of 8 pass after. Verified by mutation too, so the waits still prove
the behavior: removing the write deferral, and keeping a finished deferral chain
across an idle-timer repair, each still fail their test.
* fix: treat yolo as merge authority only, not ask-user finding authority

Yolo on/off was documented as also deciding no-mistakes ask-user findings, which hid firstmate's duty to judge unambiguous-toward-design findings itself. Keep every safety boundary; this is a contract clarification, not a relaxation.

* no-mistakes(document): Clarify yolo documentation ownership and merge posture
… work over (kunchenguid#2767)

* feat(voice): spoken round trip on Nova Sonic 2 with a measured relay cost

Step one of the spoken interface: the laptop captures and plays audio, this
desktop holds the model session, and no AWS credential leaves the desktop.

Measured, amazon.nova-2-sonic-v1:0 in eu-north-1, end of speech to first byte
of reply audio, 6 runs each, all answered, on a question that forces a records
read:

  relay path   1.229 1.379 1.428 1.447 1.481 1.516  median 1.438
  direct       1.147 1.179 1.203 1.237 1.244 1.317  median 1.220

The relay costs about 0.22s of the median. The direct figure reproduces the
earlier survey, which is what makes it a usable control. Excluded: the
captain's own ssh round trip, microphone capture, and speaker output. This
desktop has no microphone and no speaker, so every run used audio files.

Three pieces:

  bin/fm-voice-relay.py    holds the conversation on this host
  bin/fm_voice_records.py  what a spoken answer may read, and the handover
  bin/fm-voice-client.py   the laptop end; audio devices UNVERIFIED
  bin/fm_voice_frame.py    the wire format both machines share

Real work is handed to the existing bin/fm-inbox.sh rather than a second
queueing surface, and the agent says it is handing over rather than answering
as firstmate.

Read scope: Done history and free-form note bodies are never assembled at any
scope, so the wide default cannot reach the places commercial detail
accumulates. config/voice-read-scope narrows it to counts only, and
config/voice-read-deny excludes a named item in one line. The boundary is an
executable test that widening the reader fails.

Push to talk is the default because it is cheaper and the choice is still open;
--listen open-mic is the single flip.

Two traps worth knowing: a clip with no trailing silence is never answered, and
the end of a reply is contentEnd with stopReason END_TURN, not completionEnd.
A second user turn in one session is treated as barge-in unconditionally, and
an interrupted turn that calls a tool is lost, so the session reconnects per
turn and gives up conversational memory. That is the concrete thing step three
has to solve.

* no-mistakes(review): fix voice relay credential reuse, frame validation and record parsing

* no-mistakes(review): test uplink header guard, bound unknown expiry, align state dir

* no-mistakes(review): decide deny per item, guard turn failures, bound ambient credentials

* no-mistakes(review): read account config from home, harden deny and turn failures

* no-mistakes(review): close status verb set, fix inbox help, pair data override

* no-mistakes(review): keep profile-free relay alive, unblock loop, fix dead assertion

* no-mistakes(review): hide finished pull requests, refuse open mic, keep suite offline

* no-mistakes(review): survive reader failures, release devices, fix claims

A failure while handling a model event, or while sending a tool result,
left the reader task dead with ended and turn_done clear, and close()
re-raised the stored failure on every await. One dropped stream became a
relay that could never build another session. The reader now reports the
session over in a finally whatever killed it, and close() absorbs the
task the same way it already absorbed its sends.

The laptop client releases what it already started when a later startup
step refuses, SystemExit from the handshake wait included, and names a
device refusal instead of leaking a raw PortAudio error. Whether it
releases correctly against a real device is still unverified here.

The records docstring claimed every reading was filtered to open ids.
Only the pull request count and list are; the worker count and the state
histogram cover every live runtime record, finished ids included,
because a meta file still on disk still needs tearing down.

The finished-work deny half of the suite asserted things that held with
the deny list absent. It is replaced by a deny on an open title, which
removes the row and says so while the count stays honest.

* no-mistakes(review): name reader failures, split file and device refusals

A failure inside the model reader released the waiting turn and told
nobody. The session was not marked spent, no notice reached the client,
and the client waits for a reply end or a notice, so the captain got
their whole timeout of silence and then a record saying the turn went
unanswered with nothing about why. Both ends of the relay now name a
failed turn through one function, once per turn, and --self-test carries
the cause in relay_error the way the client's own record does.

Two things that are not failures stay that way. A stream that simply
ends is the end of a session, which serve still reads on its own terms.
A stream that goes away because close() asked it to is an ordinary
renew, and announcing it would have put a failure notice in front of the
captain on every turn.

On the laptop end, the refusal that became a device error covered the
file-backed playback and capture too, so a mistyped --in-file was
reported as an audio device failure and the advice named the flag that
had just failed. The file ends now report the path and the flag that
chose it and stay an OSError; the device ends keep the device advice and
name the flag for that end. The device paths remain unrun here, so only
the file halves are covered by a test.

* no-mistakes(test): survive model session end, order client turn frames

* no-mistakes(document): sync voice relay docs with reviewed relay behavior

* no-mistakes(document): re-measure relay latency and correct its cause

* no-mistakes(document): correct measurement date and name the unmeasured SSH hop

* no-mistakes(document): describe the unpublished control measurement, fix list formatting

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes
…unchenguid#2763)

* fix: keep Relay public loops open until retire

Delivering a promised-final reply was deleting the only record that tied a public thread to later work, so a follow-on ship silently owed no closing reply. Retain the registration after delivery, rechain follow-on work onto the same thread, and make retire --reason the only close.

* no-mistakes(review): Propagate public follow-up registration removal failures

* no-mistakes(review): Persist retire receipts and align parent resolution

* no-mistakes(review): Make rechain resumable after partial obligation creation

* no-mistakes(review): Repair follow-up state, briefs, and expiry escalation

* no-mistakes(review): Serialize follow-up delivery stamps with retirement

* no-mistakes(review): Serialize rechain claims and protect registration terminal states

* no-mistakes(review): Avoid reporting retired delivery loops as open

* no-mistakes(document): Refresh public-loop documentation and verification evidence

* no-mistakes: apply CI fixes

* no-mistakes(review): Preserve delivered follow-up bindings during registration replay

* no-mistakes(review): Harden public follow-up retirement and rechain races

* no-mistakes(review): Fail closed on unresolved secondmate retirement

* no-mistakes(review): Bind secondmate cleanup to its recorded canonical home

* no-mistakes(review): Fix rechain command output and expiry validation

* no-mistakes(review): Validate brief keys and warn on remote promotion

* no-mistakes(document): Document retained public follow-up loops

* no-mistakes(lint): Remove unused bounded-wait loop variable
…ath (kunchenguid#2779)

* feat(bin): merge GitLab merge requests through the guarded PR merge path

bin/fm-pr-lib.sh already parses a GitLab merge request URL for the watcher,
but bin/fm-pr-merge.sh refused every non-github provider, so a merge request
had to be merged by hand and got none of the recording, guards, or audit
trail a pull request gets.

The merge path now dispatches on the parsed provider. A GitHub URL keeps its
exact previous behavior. A GitLab URL is addressed through glab by the project
URL rebuilt from the parsed host and path, so a merge request on any instance
resolves and no host is hardcoded, and no merge-method flag is added because
the project's own merge method is what should apply.

A GitLab merge happens only after one live read of the merge request confirms
it is open, detailed_merge_status is mergeable, has_conflicts is false,
blocking_discussions_resolved is true, and the head pipeline succeeded at the
exact current head. Every failing condition is reported, not just the first.
The verified head is bound to the merge with glab's --sha, so a push landing
between the read and the merge fails the merge instead of landing commits
nothing verified. Recorded metadata is never the authority for any of this: a
rebase moves the head and leaves a recorded value stale, so a recorded head
that disagrees with the live one is reported rather than trusted, and the
recorded value is read before the recording step because that step drops a
GitLab head it cannot resolve.

* no-mistakes(review): reject bundled -R clusters and make tool-absence cases host-independent

* no-mistakes(test): state authorised GitHub narrowing of bundled -R guard

This branch NARROWS GitHub behaviour. The narrowing was authorised
deliberately rather than slipping in by accident, and it applies to both
providers, GitHub and GitLab alike, because a script that guards one provider
and not the other is a trap for the next reader.

What bin/fm-pr-merge.sh now refuses is extra merge arguments containing a
bundled short-option cluster that includes R, for example "-dR other/repo".
The forge CLIs expand such a cluster one character at a time, so it carries
"--repo other/repo", and that later value wins over the repository the URL
named. Before this change, "fm-pr-merge.sh <task> <github-url> -- -dR
other/repo" reached "gh-axi pr merge 12 --repo example/repo --squash -dR
other/repo" and exited 0 with pr= recorded and the merge poll armed. It now
exits 1 with "extra merge arguments must not override the repository", records
nothing, and invokes no forge merge command. Every other GitHub invocation is
byte-identical to the base commit.

Closing that hole honours the existing rule rather than departing from it. The
file header already forbids --repo and -R because the repository must come
only from the URL, so a bundled cluster carrying a repository override was
never legitimate behaviour to preserve: it was that guard being evaded.
Redirecting a merge to a repository the URL does not name is exactly what the
guard exists to prevent.

The refusal is already pinned on both paths by the existing case
test_bundled_repo_override_args_refuse_before_recording in
tests/fm-pr-merge.test.sh. On GitHub ("-dR wrong/repo") and on GitLab ("-yR
https://other.example/g/p") it asserts exit 1, the refusal wording, no pr= in
the task meta, no armed merge poll, and no forge merge command invoked, with a
control case proving a cluster that carries no repository override still
reaches the forge. No duplicate assertion was added. Both assertions were
confirmed to have teeth by narrowing the guard back to a bare -R and watching
each path fail.

This commit carries no file change: the guard and its coverage landed in
614853d, and this message exists so the pull request description states the
narrowing.

* no-mistakes(document): fix README pointer for GitLab watch and merge doc

* no-mistakes: apply CI fixes
kunchenguid#2788)

* no-mistakes: apply CI fixes

* fix(bin): drop a private record citation and narrow the review rule

Three corrections to the spoken interface that landed in kunchenguid#2767, plus one
fix carried over from that branch after its pull request had already been
merged.

The confidentiality fix. The module docstring of bin/fm-voice-relay.py
cited a private, gitignored fleet record by exact path and section number.
That widens what this public repository points at, and it cannot resolve
for any reader here, because the path has never been in the repository.
Both traps it pointed at are already described in full in the list
immediately below it, and docs/voice-relay.md carries the same two for
operators with no citation at all, so the pointer is removed and no claim
is weakened by losing it. Two comments that referred to "the survey" as
though it were something a reader could open are reworded the same way.
Neither exposed a path, so that half is comprehensibility rather than
confidentiality.

The review rule. .greptile/rules.md is kept, because its conditions are
right and deleting it would leave the next reviewer to re-litigate a
decision already argued out. What was wrong with it is narrower than its
existence: it read as settled repository policy, when whether VISION.md
itself should be reconciled is an open question belonging to the captain.
One sentence now says so, and says that the conditions listed below it are
what the interpretation depends on. That narrows the claim rather than
widening it.

The carried-over fix. The first commit on this branch is 7f98e79 from
fm/voice-relay-build-v4, taken verbatim rather than rewritten. It closes
the window where a transport failure was recorded and then erased, so a
run could be emitted as answered false with relay_error null. That matters
more than it looks: relay_error is the field that keeps an infrastructure
failure from being averaged into a latency figure, so the failure mode is
a dead connection wearing the costume of a slow reply. It landed fifteen
minutes after kunchenguid#2767 merged and so never reached the default branch.

* no-mistakes(review): name a reason on every unanswered-turn close path

* no-mistakes(review): guard the downlink body and pin frames to their turn

* no-mistakes(review): attribute reply audio to its own turn and tell endings apart

* no-mistakes(review): tell a cut-short reply from an unanswered turn

* no-mistakes(review): discard reply audio arriving after the output closes

* no-mistakes(review): count discarded reply audio on the speaker path too

* no-mistakes(review): keep a reason off a turn already answered in full

* no-mistakes(review): say a reset cut a reply short, not that none arrived

* no-mistakes(review): read one turn's audio count once, and hush a tidy exit

* no-mistakes(document): fix stale session-end relay_error claim in voice-relay guide
…unchenguid#2811)

A pi worker parked on an interactive prompt - a permission dialog, a
question menu, a trust dialog - reports agent_status=blocked, because it
is waiting on a human keystroke. Pi draws that menu above its separator
pair, so the composer region between the rules is blank and structure
alone looks like a free composer. _fm_composer_pi_verdict admitted
blocked alongside idle and done, so the shared classifier reported an
affirmatively empty composer for exactly the pane where typing is unsafe.

Every "is it safe to type here?" consumer reads that verdict and proceeds
only on an affirmative empty, so both are told yes on a parked prompt:
the away-mode injection guard in bin/fm-supervise-daemon.sh, and fm-send's
pre-type refusal. The keys then answer the menu instead of composing a
message - the highlighted default is selected, the text is discarded, and
the record attributes a decision to a human who never made it.

blocked now defers to unknown, which every consumer already treats as
fail-closed. idle and done still prove an empty composer, so ordinary
steering is unchanged, and Cursor is unaffected because its always-blocked
panes never reach this pi-only branch.

Regression coverage lands first at both levels: the verdict owner
(a blocked pi defers) and the herdr adapter (a parked pi prompt is not an
empty composer).
…2849)

* fix(bin): require a clone root before fleet-sync touches a project

Git repository discovery walks upward, so `git -C projects/<dir>` on a plain
directory nested under projects/ resolves to the enclosing repository - in a
firstmate home, the firstmate checkout itself. fm-fleet-sync.sh guarded its
candidates with `rev-parse --is-inside-work-tree`, which such a directory
passes, so every later git call read, pruned and fast-forwarded firstmate's own
default branch and reported it under the project directory's label. A running
session's AGENTS.md changed underneath it, and the report named a project that
had nothing to do with the change.

Require each candidate to be the root of its own work tree before any other git
command: compare `rev-parse --show-toplevel` against the directory's own
physical path. Both sides are physical, so a symlinked clone still compares
equal. Anything else is skipped by name, naming the repository that would have
been touched, and bootstrap relays that as a FLEET_SYNC line.

Regression coverage reproduces the wrong-repo fast-forward against a home nested
inside another repository, in both the whole-fleet and single-project forms, and
pins that a symlinked clone dir still syncs.

* no-mistakes(review): Keep enclosing fixture clean during clone-root regression
* fix(procevent): retry a transient Lavish poll interruption quietly

A live Lavish listener can be cut short by the server with exactly

    error: Lavish Editor poll response was interrupted
    code: SERVER_ERROR

while the session's marks remain available. Firstmate registered raw
`lavish-axi poll` output, so the generic process-event runner captured
that transient response as a result and woke the whole fleet over what is
really an internal retry.

The Lavish adapter now registers its own listener command, which reruns
the published blocking poll up to 12 times at 5 second intervals for that
one exact two-line response. The match is deliberately narrow: real
feedback, ended and missing sessions, any other SERVER_ERROR, and the same
interruption still standing once the bound is spent all pass straight
through and are captured and announced as before. The retry is a Lavish
fact, so the generic runner stays adapter-agnostic.

`FM_LAVISH_POLL_RETRY_DELAY` is a bounded 0 to 60 second override for the
interval only, refused rather than rounded when malformed, so a test can
exercise the real bound without waiting it out.

* no-mistakes(review): Harden Lavish retry matching, validation, and cleanup

* no-mistakes(review): Bound Lavish retry staging and stabilize regression

* no-mistakes(document): docs: explain Lavish retry adoption

* no-mistakes(lint): Restore Lavish trap ShellCheck suppression
… gate (kunchenguid#2838)

The unguarded Herdr declaration quoted `{TASK}` in its own prose while the
scaffold instructs firstmate to replace every `{TASK}` placeholder. The
documented global replace therefore spliced the whole task body into the
middle of the safety gate's sentence, silently destroying the one contract
that exists precisely because the scaffold cannot inspect the task text.

Reword the gate to refer to the task text filled in above, leaving the
placeholder only at its genuine fill site. Rewording rather than renaming the
token keeps the unfilled-charter guards in fm-home-seed.sh and
fm-remote-home-seed.sh working unchanged.

Add a regression test that performs the documented global fill on ship and
scout scaffolds and asserts the body lands once and the gate survives.
…tat form (kunchenguid#2837)

The writer lock's stale-lock branch read the lock's mtime with
`stat -f %m ... || stat -c %Y ...`. On GNU coreutils `-f` is filesystem
stat, so it consumed the format string as a path, complained on stderr,
printed a partial filesystem dump ("  File: ...") on stdout, and still
exited 0. The GNU form in the fallback therefore never ran, and the
following arithmetic evaluated the word `File`, aborting the writer under
`set -u` with "File: unbound variable".

fm-teardown.sh died there after returning the worktree, leaving
state/<id>.meta, .status, .busy-gen, .busy-state, .busy-state.lock/ and
.turn-ended behind. The surviving metadata kept the watcher monitoring an
endpoint whose agent was gone, so a finished task produced stale wakes
forever, and every re-run died identically because the abandoned lock was
never broken.

Detect the platform once and pick the right stat form, the pattern
bin/fm-watch.sh already documents, and treat any non-numeric result as
"just created" so a future portability surprise degrades to a lock-timeout
refusal rather than killing teardown mid-way.
* fix(stow): give memory decay a per-pass horizon so the clock fires

The tiered decay clocks were wall-clock only, while admission is per-pass:
each /stow admits the findings that pass produced. In a home that stows
daily those two rates diverge by the stow cadence, an entry the fleet keeps
exercising never reaches 30 days unreinforced, and memory only grows while
the pass reports decay evaluated.

Give each dated marker an optional unreinforced-pass counter and make both
tiers stale at whichever horizon comes first: 10 passes or 30 days for
aging, 3 passes or 7 days for perishable. Reinforcement clears the counter
and nothing else does, so the existing evidence-based restamp rule stays
the only way an entry renews its lease. An absent /N means zero, so entries
that stay exercised carry no extra marker bytes, and a rarely stowed home
keeps its current behaviour through the unchanged date horizon.

* no-mistakes(document): Align stow workflow with dual decay clocks

* fix(stow): make the per-pass decay horizon opt-in

The unreinforced-pass horizon shipped as a new default archival cadence,
which is a product default rather than a restoration of the existing
wall-clock contract. Keep the 30-day and 7-day horizons as the only
default clock, and put the 10-pass and 3-pass horizons behind an explicit
opt-in: config/stow-pass-horizon for the firstmate home, and the file's
own header pointer for the public skill.

With the opt-in absent no counter is written and no counter is read, so a
home that does not ask for it decays exactly as it does today.

* no-mistakes(review): Preserve frozen counters and correct archive provenance
…artup (kunchenguid#2876)

tests/fm-watcher-lock.test.sh passed in isolation but failed intermittently
under full-suite and ambient concurrent load. bin/fm-watch-arm.sh computes its
confirmation deadline immediately after forking the real child watcher, so the
child's entire fork, exec, lock acquisition and beacon publication has to land
inside that wall clock. Two cases shrank that budget to one second, leaving a
two-second window for work measured at 3.1-4.9s under CPU oversubscription, so
the arm honestly reported "FAILED - no live watcher with a fresh beacon" and
their premises collapsed. A third case ran on the production budget, but its
child must also execute a registered check before exiting: measured at 1.9-2.3s
idle and 9.1-13.1s under load, against an 11s budget.

The two cases that must confirm a real child now hold the arm to production's
own budget instead of a shrunken fixture one, the immediate-wake case gets an
explicit budget with headroom over its measured loaded cost, and the two waits
for the arm's typed failure are sized off the largest production default rather
than a fixed eight seconds.

No bin/ change and no default behavior change: the lock's fail-closed semantics,
SIGSTOP handling, stale-heartbeat detection and the arm's typed failures are
untouched. Verified 4/4 green at 3x CPU oversubscription (loadavg 75-80) after
3/3 red before the change, and CONTRIBUTING.md records the convention.
* fix(bin): order discovered tool installs by the shell's own expansion

fm_remote_job_compose_operator_path built the asdf and mise install
directories with `compgen -G`, which does not sort. Bash sorts glob
matches in pathexp.c, on the shell's own pathname-expansion path only;
`compgen -G` reaches the same glob_filename through pcomplete.c, which
sorts nothing. On bash 3.2 (macOS /bin/bash) and every bash before 5.3
that handed the composition raw readdir order, so which install of a
multi-version tool a remote job resolved was decided by directory order
on disk rather than by this composition.

Expand the globs at the call sites and let the function take the matches,
so the composition and the documented portable-PATH contract are the same
operation. Quoting the account home at the call site also stops a home
whose name contains glob metacharacters from being reinterpreted.

The colocated regression pins both the order and the mechanism: bash 5.3
moved sorting into the glob library, so an order-only assertion cannot
see the defect there.

* no-mistakes(review): Remove source-reading PATH regression guard
…2848)

* fix: surface stalled secondmate queues and wake handoffs

* no-mistakes(review): Make handoff wakes retryable and stall alerts crash-safe

* no-mistakes(review): Prevent duplicate handoff wakes and cover remote delivery

* no-mistakes(review): Serialize local handoffs and preserve pre-move wake intent

* no-mistakes(review): Serialize teardown with handoffs and retain remote wake confirmation

* no-mistakes(review): Reconcile correlated handoff wake delivery after crashes

* no-mistakes(review): Keep failed wakes retryable and isolate stall receipts

* no-mistakes(review): Reset known-undelivered wake attempts for durable retries

* no-mistakes(review): Refuse duplicate sends for unresolved delivery attempts

* no-mistakes(review): Atomically restore retryability after reconciled send failures

* no-mistakes(review): Serialize delivery confirmation with reconciliation

* no-mistakes(document): Document routed wake and stall supervision

* no-mistakes(lint): Fix ShellCheck expansion and subshell warnings

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes(review): Retire stale wake state and defer pre-move wakes

* no-mistakes(review): Secure markers, bind batches, and preserve teardown routes

* no-mistakes(review): Preserve unresolved prepared wakes across unrelated handoffs

* no-mistakes(review): Preserve prepared wakes before unrelated moving handoffs

* no-mistakes(document): Document prepared wake batch ownership

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes(review): Make local wake retirement recoverable

* no-mistakes(document): Clarify handoff recovery and teardown documentation
…guid#2856)

* feat(bin): steer local tasks by durable inbox record plus constant doorbell

Stage 1 (local steers) of the captain-adopted reframe in
data/fm-send-reliability-reframe-s1/report.md: an ordinary fm-send text
steer to a task recorded in this home is appended as a sequenced durable
record under state/<id>.inbox/ and the terminal receives only one constant
self-describing doorbell line, best-effort. The worker acknowledges by
moving the record into handled/; the watcher re-rings an unacknowledged
message on an idle pane and escalates once as an ordinary stale wake.
--resolve-key closes decisions at enqueue time, because the durable
enqueue IS delivery to the task's record. bin/fm-task-inbox-lib.sh owns
the record format, doorbell line, and re-ring ladder.

The typed plane remains for what must reach the terminal itself:
lifecycle keys, harness-native slash and codex $-skill invocations,
explicit backend targets, and the remote secondmate leg (unchanged until
the remote inbox leg ships separately). The composer classifier is
demoted from delivery proof to an advisory ring guard that skips only on
a proven pending verdict.

Verified live against claude, codex, opencode, pi, grok, and muse: each
real worker read its record, acted, and acked with the mv
(docs/verification/runtime-backends.md "Steering-inbox doorbell").

* docs(verification): flag the grok 1.0.5 composer-matrix staleness observed by the doorbell run

* test(captain-hold): read the chat-channel answer from the durable inbox record

* test: migrate fm-control's marker contrast to the inbox record and fix macOS wc padding in the tool-update suite

* no-mistakes(review): Harden inbox locking, teardown races, and acknowledgements

* no-mistakes(review): Serialize watcher actions with inbox acknowledgements

* no-mistakes(review): Bound metadata locking and tighten acknowledgement rechecks

* no-mistakes(review): Preserve exact inbox bytes and harden delivery recovery

* no-mistakes(review): Harden watcher bookkeeping against concurrent inbox teardown

* no-mistakes(document): Update inbox and typed-plane documentation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* revert(pipeline): keep parser-native secondmate marking and the both-failed exit out of stage 1

The CI monitor's fix changed the secondmate marking contract for
parser-native invocations (appending the marker after the text) and
softened the both-commit-and-marker-failed branch to exit 0. The merge
authority ruled the marking question out of scope for this stage-1
transport PR (follow-up: fm-send-secondmate-harness-invocation-r1) and
ruled the both-failed case a loud nonzero local failure. Restore both,
keeping the monitor's legitimate migrations and hardening.

* no-mistakes(document): Document inbox and typed-plane boundaries

* no-mistakes(document): Scope backend transport docs to typed plane

* no-mistakes(document): Clarify inbox attempt-budget documentation

* no-mistakes: apply CI fixes

* fix(send): the durable record alone governs the inbox exit status

Captain-refined ruling on the F2/Greptile finding: the durable inbox
record is what delivers the steer, so pending-reply bookkeeping trouble
after a successful enqueue never exits nonzero - a resend-inviting status
would make automated callers enqueue the delivered instruction again
under a new sequence. With the recovery marker stored the watcher
reconciles silently; with the commit and marker both lost the send
surfaces a distinct reply-tracking-degraded do-not-resend warning and
still exits 0. Nonzero remains only where nothing was delivered (or a
decision close needs its manual command). Regression: record durable +
both bookkeeping writes lost -> exit 0, one record, no duplicate.

* no-mistakes(review): Preserve inbox ordering with drain-all doorbells

* no-mistakes(review): Surface unwritable inbox ladder bookkeeping

* no-mistakes(review): Silence ladder failures after inbox acknowledgement

* no-mistakes(document): Update steering inbox documentation

* no-mistakes: apply CI fixes
* feat: add fast local lint mode

* fix: preserve complete fm-lint help

* fix: isolate fast lint mode

* no-mistakes(document): Clarify lint mode documentation ownership

* no-mistakes: apply CI fixes
…#2901)

* feat(bin): deliver remote secondmate steers through durable task inboxes

Stage 2 of the inbox+doorbell steer channel (stage 1: kunchenguid#2856). A remote
secondmate steer now crosses fm-on.sh as a durable record written
idempotently into the remote home's steering inbox plus a best-effort
remote doorbell, and the last typed-payload steer transport is deleted:

- fm-remote-secondmate-control.sh cmd_send writes the record via the new
  fm_task_inbox_write_idempotent and rings the doorbell; it no longer
  types the payload through an inner fm-send at an explicit pane target.
- fm-send.sh routes every remote text steer (harness-native included,
  which marking already reduced to chat) onto the remote inbox leg,
  retries the identical leg once on ssh 255, closes --resolve-key
  decisions at enqueue for remote too, and preserves a marked request's
  reply expectation when completion stays unknown. The exit-3-as-
  delivered remap, the 255 do-not-resend trap, and the remote typed
  submit block are removed.
- fm-task-inbox-lib.sh owns the idempotent enqueue: an exact-body re-run
  lands on the existing record, handled or not, so an ambiguous
  transport can always be safely re-run.
- Tests pin the new contract end to end (record + doorbell + no typed
  payload across ssh, one-record idempotence under an ambiguous
  transport, enqueue-time decision close, loud real failures, and the
  deleted typed-payload behaviors gone), and AGENTS.md plus
  docs/remote-secondmates.md describe the remote leg's new semantics.

* no-mistakes(review): Harden remote inbox delivery against lifecycle races

* no-mistakes(review): Enable correlation-preserving remote steer resends

* no-mistakes(review): Fail closed on stale correlation resends

* no-mistakes(review): Include home context in remote resend commands

* no-mistakes(review): Lock and revalidate remote parent routes

* no-mistakes(document): Clarify remote steer retry documentation

* no-mistakes: apply CI fixes
* wip: forked supervision on Pi (checkpoint before docs)

* fix(pi-branch): harden mirror delivery, fallback encoding, and session replacement

Peek-then-shift mirror flush so a failed append retries instead of dropping;
durable mirror cursor commits only after delivery into the branch;
the main fallback wake is operational-encoded like every watcher injection;
session_shutdown quiesces the generation and session_start re-arms, so /new
and /resume no longer kill the branch permanently. Registers the extension in
the strict typecheck, adds the dispatch handshake test, the branch extension
suite, the bash-level regression suite, the session-start replay test, and
the opt-in real-SDK live guard.

* test(fixtures): carry the branch-dispatch lib and lease lib into isolated fixtures

The watcher extension now imports lib/fm-branch-dispatch.ts and fm-teardown
sources fm-lease-lib.sh, so every fixture that copies or symlinks those
files in isolation gains the new sibling.

* no-mistakes(review): Prevent shutdown wake loss and serialize lease claims

* no-mistakes(review): Durably hand off wakes and retain portable leases

* no-mistakes(review): Require durable reports and clear disposed branch leases

* no-mistakes(review): Enforce per-wake outcomes and quiescent lease cleanup

* no-mistakes(review): Require wake acknowledgements and tighten branch lifecycle boundaries

* no-mistakes(review): Require complete acknowledgements and replay cleanup failures

* no-mistakes(review): Bind supervision to lock ownership and durable delivery

* no-mistakes(review): Activate branch lazily after session lock acquisition

* no-mistakes(review): Preserve undelivered mirror context across extension rebinds

* no-mistakes(review): Acknowledge startup replay only after main delivery

* no-mistakes(review): Isolate replay metadata from untrusted digest content

* no-mistakes(review): Reject duplicate reports for active wake sequences

* no-mistakes(review): Retain failed fallbacks and deduplicate outcome replay

* no-mistakes(review): Deduplicate durable outcomes and cache delivery receipts

* no-mistakes(review): Anchor wake sequence matching to outcome fields

* no-mistakes(document): Clarify Pi supervision durability contracts

* no-mistakes(lint): Fix ShellCheck issues in branch supervision scripts

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* refactor(pi-branch): collapse to confused-agent-grade guards per captain decision

Captain decision A: the lease/actor guards target the CONFUSED-AGENT threat
model bin/fm-gate-refuse-lib.sh already documents; adversarial-grade
separation is impossible in the shared-process design and is filed as
separate follow-up work. Rip out the machinery that chased it: the
generation fence and shell-provenance markers, the wrapper-tagged ancestry
walks, guard auto-claim with per-script release traps, the pending-wake
files and ack-receipt correlation (the durable wake queue already
re-presents anything unacknowledged), the delivery-receipt store with
contiguous cursor advancement, the session-start replay-metadata channel,
and the branch tool quiescence counters.

Keep the behaviors the board requires, each on its simplest implementation:
lazy per-action session-lock ownership (cold start activates after the lock
lands; a secondary session stays inert), mirror durability across extension
rebinds via the durable cursor, replay-exactly-once from the one read
cursor, the awaited operational-encoded fallback, per-generation stray-lease
cleanup, session-lock-bound lease liveness (a recycled pid or a non-Pi home
never honors a leftover lease), the loud accidental-override guards
(readonly actor prelude, cross-actor claim refusal), and the role-partition
refinements (no forced teardown, no direct relaunch for the branch).
Default-on-for-Pi is unchanged.

* no-mistakes(review): Enforce lock ownership and serialize lease mutations

* no-mistakes(review): Synchronize guard cleanup and bind leases to lock owner

* no-mistakes(review): Report outcomes before acknowledging durable wakes

* no-mistakes(review): Restrict leases to Pi and instruct main claims

* no-mistakes(review): Reject malformed lease locks and torn outcome tails

* no-mistakes(review): Validate complete outcome tails before appending

* no-mistakes(review): Guard branch side effects across session replacements

* no-mistakes(document): Update Pi supervision durability and lease documentation

* no-mistakes(lint): Suppress intentional nested-shell expansion warning

* no-mistakes: apply CI fixes

* fix(pi-branch): authorize lease releases by caller

* fix(lint): break redundant source-analysis path in fm-lease-lib.sh

fm-lease-lib.sh's lazy fallback source of fm-wake-lib.sh gave ShellCheck's
--external-sources traversal a second path into an already 1540-line file
that fm-send.sh and fm-teardown.sh also source directly, blowing up the
recursive analysis past CI's lint timeout. Mark it a source=/dev/null
analysis boundary, matching the existing fm-task-inbox-lib.sh convention.

Also restores bin/fm-lint.sh and tests/fm-lint.test.sh to the shared
serial-lint definition (dropping an unrelated parallel-sharding change
that was itself hanging and masked this root cause).

* no-mistakes(document): Correct lease caller-authorization documentation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes
* feat(bin): parallelize session-start remote secondmate network sweeps

Run per-secondmate liveness and convergence probes concurrently and overlap clone refresh, while replaying each mate's fail-closed diagnostic in original order. Ignore scratchpad* so untracked scratch no longer blocks remote sync.

Co-authored-by: Cursor <cursoragent@cursor.com>

* no-mistakes(document): Document parallel startup network sweeps

* no-mistakes(lint): Fix empty environment assignment lint warning

* no-mistakes: apply CI fixes

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(tests): count declared-pause wakes without crashing on an absent queue

The exited-declared-pause case counts queued stale wakes by handing
state/.wake-queue straight to awk. A watcher that queues nothing never
creates that file, and awk aborts on a missing path before its END rule
runs, so the count collapses to the empty string. The next comparison
then fails as an integer-expression error and surfaces as a wake flood
with no number, hiding the real contract breach the following grep names.

Read the queue the way the drain-count assertion at the end of this file
already does: silence awk's open error and default an absent queue to
zero. Applied to all four counts in this case, including the live
external-decision gate pair whose queue an acknowledged drain can also
leave behind. An absent queue now reports "did not use the bounded
paused recheck", while a genuine flood still fails with its real count.

Fixes kunchenguid#2628

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes
… icon (kunchenguid#2934)

* style(pi): restyle supervision merge notes with a sailboat and matching pad

Secondary-session notes were flush against the TUI edge and fully tinted.
Use the sailboat prefix, Pi's default outputPad, boat-only color, and dim remainder so they sit like real messages.

* style(pi): distinguish routine and captain merge notes by icon only

Visible notes now lead with a sailboat or anchor, then only the dim outcome.
Drop the branch-merged wording and verdict brackets so the icon is the only kind signal.
…id#2938)

The markdown contract stays the owner; the still is only the visual of the idea.
* fix(bin): bound remote worker supervisors

* no-mistakes(review): release incumbent supervisor before starting its replacement

* no-mistakes(review): wait out a healthy same-root supervisor instead of replacing it

* no-mistakes(review): narrow remote worker change to restart accounting only

* no-mistakes(document): clarify supervisor restart guard is a lifetime total
kunchenguid and others added 27 commits August 26, 2026 20:51
* fix(pi): type captain supervision outcomes so main relays them

A captain-relevant branch outcome reached main as a bare user message with
no marker of origin or required action, written in main's own captain-facing
voice, landing in a tail that often already held several such notes. Pi keeps
only a custom message's content when it builds the provider request, so
customType and display never reach the model and content was the only place
that identity could live.

Main could not tell an incoming outcome from its own earlier answer and
sometimes re-emitted that answer instead of relaying the outcome, losing it.
Measured against real Pi 0.84.1 on openai-codex/gpt-5.6-sol: 6 failures in 24
turns, rising to 3 in 6 once one stale answer was already in the tail, which
is how one captain conversation saw six identical messages in a row. The same
scenario with the outcome typed failed 0 times in 14 turns.

Wrap only the captain-verdict note in the branch-outcome operational kind
owned by bin/fm-operational-input.sh. Delivery is otherwise unchanged: still
display: false, still one triggerTurn follow-up, so the turn remains the
single captain-visible outcome and no hidden note is ever shown twice.
Routine notes stay plain because their renderer reads the glyph off the front
of that same string. An outcome that cannot be encoded degrades to the same
instruction as plain text rather than being lost, matching this file's stated
failure direction.

The existing assertions could not catch this: they pin the sendMessage
options and never look at what main receives. Add a portable regression that
classifies the delivered payload with the real protocol executable, and a
live guard that runs the real Pi SDK's own convertToLlm to prove content is
the entire model-visible payload.

* no-mistakes(document): Document typed Pi captain outcomes
… or enriched wedge (kunchenguid#3155)

* fix(bin): keep a busy pane from retiring a still-declared wait's window

The away-mode daemon's pause re-surface recheck (housekeeping step 2b) read a
busy pane as "the crew resumed" and dropped the declared-wait marker, without
re-reading that the crew's own latest status line still declared the wait.

That inference is not safe, because a declared wait can legitimately hold a pane
busy: a worker sitting on a long foreground call keeps that call live for as long
as the wait lasts. The marker is then cleared while the declaration still stands,
and migrate_watcher_pause_markers recreates it with a fresh timestamp on the very
next tick, so the window restarts forever and the wait never matures into its one
bounded recheck.

Away mode makes that terminal. Since the watcher half landed, a busy pane under a
declared wait is handed to the daemon exactly once per declaration and never woken
again while the declaration stands (bin/fm-watch.sh, busy_turn_bound_check), so
this recheck is the only thing left that can re-surface the pane at all. Measured
end to end on a throwaway state root, away mode active, a pi pane busy past
FM_BUSY_TURN_MAX_SECS, status still `paused:`, over six PAUSE_RESURFACE_SECS
windows: 0 captain-facing rechecks before this change, 6 after - one per window,
with the marker reset each time.

The fix drops only the busy arm of the 2b probe, leaving it an endpoint-readability
check: exit code 2 still means the capture failed, so the endpoint is gone and the
marker goes. The loop head above already drops the marker the moment the status
line stops declaring the wait, so nothing else is needed to end the routing, and
the reconcile path runs before the probe ever reads a pane.

tests/fm-daemon.test.sh: test_housekeeping_paused_resumed_cleared pinned the old
inference on purpose - its fixture's status line still read `paused:` while the
pane was busy, and its comment read "A pause whose pane became busy again (the
crew resumed)". Its fixture now resumes the way a crew actually resumes, by
appending a non-declaring status line, and it asserts its own busy verdict first
so it cannot silently decay into the idle-pane case that
test_housekeeping_paused_unpaused_cleared already covers. What it pins is now the
inverse guard: a busy pane must not GATE the clear either, so an over-correction
that kept the marker alive whenever the pane is busy would fail it.

test_housekeeping_busy_declared_wait_matures_its_window is the new regression, over
both declaration forms. It asserts the busy verdict, then that ticks inside the
window neither escalate nor let the marker be recreated with a fresh timestamp,
then exactly one recheck past the window named for the right human and never a
wedge, then silence on the next tick inside the reset window. It fails on
unmodified main with "produced 0 escalations past its window, expected exactly
one".

Refs kunchenguid#3149

* fix(bin): let a declared wait outrank an enriched wedge escalation

handle_wake classifies a stale wake through classify_stale, which returns a
`pause` verdict for a crew whose latest status line declares an external wait or
a verified captain-held transfer. It then threw that verdict away whenever the
wake reason matched `idle *s, possible wedge, escalation *`, so the watcher's
enriched wedge decoration outranked the crew's own declaration and a healthy
declared wait was escalated once per FM_STALE_ESCALATE_SECS for as long as the
wait lasted.

The enriched reason earns its precedence over the daemon's cheaper status-log
absorption honestly - it carries the watcher's escalation count and its explicit
"do not re-absorb on the run-step/pane state alone" demand. A `pause` verdict is
not run-step or pane state. It is the crew's own declaration that this pane waits
by design, which is precisely the question the wedge timer cannot answer for
itself, so it is the one verdict that decoration must not override.

The two classifications genuinely disagree in steady state rather than only in a
race: a crew that declares `paused:` while its no-mistakes run is still attributed
to its code reads `working` to the watcher's pause_state_class, so the watcher
takes the wedge timer while the daemon's classify_stale reads the status log and
correctly returns `pause`.

The wait stays bounded, not silenced. Absorbing to the pause action records the
declared-wait marker and drops wedge aging, and housekeeping (2b) then owns the
re-surface, so the pane still reaches the captain - once per PAUSE_RESURFACE_SECS
as an explicit "recheck whether the wait still holds", instead of once per
FM_STALE_ESCALATE_SECS as a possible wedge. Measured on a throwaway state root
over five wedge cadences for one declared wait: 5 escalations climbing to
demand-deep-inspection before this change, 0 after, with the one bounded recheck
still delivered.

tests/fm-daemon.test.sh: test_stale_diagnostic_wedge_survives_busy_housekeeping's
`paused` case pinned the old precedence on purpose, asserting exactly one
escalation carrying the demand-deep-inspection payload. That case now asserts the
pause cadence instead - no escalation inside the window, pause tracking recorded -
while the `working` and `prior-terminal` cases keep asserting the enriched wedge
verbatim, so the override itself is still pinned everywhere it is correct.

test_enriched_wedge_under_declared_wait_uses_pause_cadence is the new regression.
It asserts the fixture's own classifier verdict is a pause first, so the case
cannot go vacuous, then drives four consecutive wedge-cadence deliveries in both
the plain and demand-deep-inspection forms through the real handle_wake and
housekeeping pair, then matures the window for exactly one awaiting-external
recheck, then lifts the declaration and requires the same enriched wedge to
escalate again unchanged. It fails on unmodified main at the first delivery.

Refs kunchenguid#3149

* no-mistakes(review): align afk skill recheck wording with still-declared contract

* no-mistakes(document): daemon doc comments: pause window ages on declaration

---------

Co-authored-by: Talon Stark <talonstark@gmail.com>
* fix(bin): make Claude auto-arm continuity self-heal past a hung claim

On a Claude primary, a Stop-hook auto-arm process that hung mid-arm held
the single-flight owner lock with its epoch ledger frozen at
outcome=arming, and the abandonment proof read any live lock holder in
arming as legitimately deciding forever. Every later Stop firing exited
0 at the lock, the turn-end guard kept deferring to the hung owner as
recovery under way, and the watcher was never auto-re-armed again for
the rest of the session - supervision survived only on manual arms and
lapsed between them (the 2026-08-26 watcher flap).

Corrections layered onto the lock-held-across-arm shape each reopened
the same concurrency class one level down, so this replaces the claim
machinery wholesale with a generation-based optimistic design:

- The epoch ledger's monotonic sequence IS the claim generation; the
  two-line entry (classic epoch record plus the claimant's MANDATORY
  pid-identity) is the claim. Every firing defers to a live OPEN claim:
  outcome arming, owner alive, identity recomputes and matches, and not
  stuck (entry and watcher beacon both older than the guard grace).
- A finished, dead, identity-mismatched, identityless, or stuck claim
  is superseded by simply taking the next generation - no signalling or
  revocation of a steady-state predecessor.
- No mutex is held across arming or output; the owner lock survives
  only as a micro-mutex around individual ledger writes. A superseded
  owner goes completely silent: ownership is re-verified before every
  arm invocation, episode-state mutation, ledger write, and
  continuation.
- The irrevocable commit point of a translation is the exit status
  (the harness delivers the collected stderr only on exit 2), so the
  owned terminal ledger write is the atomic commit: the winning
  generation exits 2 unconditionally after it, a refused one exits 0
  silently even after printing, and the once-per-episode failure notice
  commits in the same owned critical section as the winning failed
  write. Two bounded residuals are documented accepted intent: an owner
  dying between its owned write and its own exit, and a hung old-build
  owner resuming during the one legacy upgrade window.
- The pre-generation lock-holding claim shape keeps defer-or-reclaim
  behavior through a legacy shim: a live identity-verified stuck owner
  is retired via TERM (with a queued TERM sufficient when the owner is
  stopped) before its lock is removed, an unverified or identityless
  pid is never signalled but never blocks a proven-abandoned reclaim,
  and the lock's identity evidence is grafted into the ledger
  (mtime-preserving) so pid-reuse protection survives the lock.
- The guard reads the same predicates for recovery ownership and its
  terminal fail-open (which re-checks for a live open claim under the
  held locks before committing the attended alarm), with ledger reads
  anchored to line 1 so the identity line can never confuse them.

Behavioral regression coverage exercises all three edge classes through
the real hook and guard - a live open claim defers with no lock held, a
stuck claim is superseded and the home re-arms, and an end-to-end run
with a genuinely hung owner shows a concurrent firing deferring
promptly mid-arm, a later firing superseding the stuck owner, and the
superseded owner exiting silently without a second translation - plus
the identityless/reused-pid loopholes, the superseded-owner arm
boundary, and the legacy TERM, SIGSTOP, and signal-free reclaim paths.

* no-mistakes(review): Refuse auto-arm commits when notice marker creation fails

* no-mistakes(review): Make episode reset atomic with generation ownership

* no-mistakes(document): Update auto-arm generation and commit documentation
…n unproved merge (kunchenguid#3064)

* fix(pr): verify GitHub merge outcome

* no-mistakes(review): Captain, fixed forge-only merge verification, queue guidance, metadata propagation

* no-mistakes(document): Correct forge-specific merge documentation

* no-mistakes(review): Captain: forge-only queue fix, focused tests pass

* no-mistakes(review): Captain: suppress closed-state guidance and prove parent regression

* no-mistakes(review): Captain: remove history proof; retain executable regressions

* no-mistakes(document): Clarify GitHub recording timing in architecture docs

* no-mistakes(document): Clarify outcome-aware PR merge recording documentation

* no-mistakes: apply CI fixes

* Revert "no-mistakes: apply CI fixes"

This reverts commit c326cfa.

The automatic CI repair round removed the up-front `gh` prerequisite check
while keeping the `gh` dependency: `bin/fm-pr-merge.sh` still calls
`gh api graphql` for the outcome read and `gh api` for the branch-rules read.
That left the same hard requirement without the clear named error, and review
immediately raised a new finding for exactly the failure the check prevents -
`gh-axi pr merge` landing the merge while the follow-up read fails, so the PR
metadata is never recorded.

The check is also symmetric with the GitLab arm directly above it, which
already refuses up front when `glab` or `jq` is missing, on the stated
principle that a missing tool should be a named prerequisite rather than a
merge that is armed and then refused for an unexplained reason.

The workflows this round was chasing sit at `action_required` because this is
a fork pull request; no code change can turn them green.

* fix(pr): keep PR bookkeeping when a merge outcome read fails

On the GitHub path a merge call that returned success was followed by
`github_read_outcome || exit 1`, so a transient API failure, rate limit,
or network blip during the read dropped out of the script before
`record_pr_metadata` ever ran. The merge could have landed while `pr=`
went unrecorded and the merge poll was never armed - bookkeeping lost on
a real merge. The failure path just above already recorded metadata
before exiting, so the error path was more careful than the success one.

Record the PR before that refusal. Recording arms the later merge poll
and is not a success claim, which is the same reasoning that keeps
`record_pr_metadata` on the gh-axi failure path. The refusal itself is
unchanged: exit stays non-zero and the message still names the concrete
observed state. Metadata is withheld only when the read succeeds and
proves the pull request neither merged nor queued.

Pin it with a case that stubs `gh api graphql` into failure after a
successful `gh-axi pr merge`, asserting both the non-zero exit and the
recorded metadata.

* no-mistakes(review): Aggregate queue rules and report conflicts explicitly

* fix(pr): keep the merge abstraction reachable and its bookkeeping intact

Two holes remained in the outcome-verified GitHub merge path, both on
installations where gh-axi is present but gh is not.

The verification preflight refused before bin/fm-pr-merge.sh ever reached
the configured gh-axi merge abstraction, so an installation without gh
could no longer merge at all. gh-axi now performs the merge unconditionally
and the queue-aware gh read became an optional enrichment: with gh on PATH
its GraphQL view still separates merged from queued, and without gh the
gh-axi view still proves a landed merge while every outcome it cannot prove
refuses.

The PR metadata recording sat behind the outcome read, so a merge that
landed before that read failed lost pr= and its merge poll. Recording now
happens once, before either forge call, which arms the poll without
claiming a landed outcome and leaves teardown a PR identity to verify
against no matter how the read ends.

Rebasing onto main also restored the durable merge-outcome reporting and
the GitLab landed-state confirmation that the conflict resolution dropped.

Tests pin each fix through the executable interface: the merge abstraction
is reached and verified with gh absent, a failed fallback read keeps its
bookkeeping, and a mock that snapshots the task meta during the forge call
proves pr= is recorded before the merge can land.

* no-mistakes(review): fix(pr): de-dup queue methods, fall back on failed gh read, refresh contracts

* no-mistakes(review): fix(pr): quote forge output and explain armed auto-merge on refusal

* no-mistakes(review): fix(pr): claim auto-merge armed only when the forge accepted it

* no-mistakes(review): fix(pr): tell the operator what each GitHub refusal could not observe

* no-mistakes(review): fix(pr): gate every forge-acceptance claim on a successful merge

* no-mistakes(document): align merge docs with verified GitHub outcome contract
* fix(pi): stop reporting one merge to the captain twice

The supervision branch's captain-outcome note told main, unconditionally,
that the note "is not your own earlier output" and to relay it now. When
main had already reported the same event, that assertion was false and the
order turned the correct response - saying nothing new - into a mechanical
re-report, so the captain saw one merge reported twice in 16 seconds.

Two independent changes, both needed:

- The relay instruction is now conditional. It still names itself as a
  supervision outcome so main cannot mistake it for its own earlier answer
  (the silent loss that instruction exists to prevent), and it now lets
  main stay quiet about an outcome it has already given the captain.

- The merge case is closed at its source rather than left to that judgment.
  One merge reaches a home on two independent paths by design - main's own
  permanently main-owned merge poll, and the branch's task-local status
  wake - and main's captain-facing text only reaches the branch's mirror at
  main's turn end, so the branch can escalate before it could possibly see
  the captain was already told. bin/fm-pr-merge-notified.sh answers that
  question from bin/fm-pr-lib.sh's canonical merge-notification marker, so
  the answer holds regardless of mirror timing. A captain outcome naming an
  already-published merge is delivered as the ordinary rendered note
  instead of opening a follow-up turn: still appended, still visible, still
  recorded with the verdict the branch decided, minus the wasted turn.

Any error, timeout, or unreadable state relays the outcome. A duplicate
announces itself; a lost outcome does not.

Regression coverage drives the real delivery path in both directions: a new
outcome must still reach the captain in exactly one follow-up turn even
beside an unrelated published merge, and an already-published merge must
open no second turn while a different PR in the same task still does. The
merge path's real producer and this new consumer are exercised end to end
in tests/fm-pr-merge.test.sh.

Pi-only by construction: the delivery path lives in .pi/extensions, so no
other harness loads it, and the new script only reads existing markers.

* no-mistakes(review): Document accepted latest-marker suppression residual

* no-mistakes(review): Recheck ownership before merge outcome delivery

* no-mistakes(document): Document merge-outcome suppression exception

* refactor(pi): drop the source-level merge suppression, keep the envelope fix

The captain reviewed this branch and judged the source-level duplicate
suppression overly complicated for the problem it solved, and asked for
the change to be reduced to the envelope wording alone.

Remove the mergeIntoMain downgrade path, bin/fm-pr-merge-notified.sh, and
every test and document that existed only for it. What remains is the
conditional captain-outcome instruction: main is told to stay quiet about
an outcome it has already reported and to relay anything else, which
covers the duplicate without a second mechanism.

The silent-loss protection is untouched - the note is still typed,
self-describing, and delivered as one invisible follow-up turn - and the
behavioral tests still assert that, now requiring both halves of the
conditional instruction.

* no-mistakes(ci): Clarified in code comments and owned documentation that this is intentionally an M1-only, model-facing conditional relay fix—not source-level suppression—addressing Greptile’s mistaken scope expectation without changing runtime behavior. Net diff remains 3 files and 27 insertions. Verified with fm-pi-branch-extension tests, fm-lint, doc audience check, and git diff --check; all passed

* no-mistakes(ci): Strengthened the runtime delivery test to verify the captain outcome retains its required self-description and outcome text. Verified with `bash tests/fm-pi-branch-extension.test.sh`, `bin/fm-lint.sh`, `bin/fm-doc-audience-check.sh`, and `git diff --check`; all passed. The outer pipeline can now commit and attest the new head
* fix(bin): bind the live pipeline-owned run instead of a superseded failed row

fm-crew-state.sh bound a superseded FAILED no-mistakes run to a task instead
of the LIVE replacement run: the live run's pipeline-owned lane head is not a
git object in the task worktree, so head-equality attribution rejected it and
the coarse runs-list fallback silently continued past the RUNNING row onto an
older failed row whose head equalled the stale worktree HEAD. The home summary
then flipped invalid and Bearings hid the home's live work (F10).

Attribution precedence now follows the daemon's own identity:
- An ACTIVE run for the task's branch binds without head equality while
  branch_sync.state is pipeline_owned (fm_nm_run_is_pipeline_owned_active);
  the pipeline owning the branch is itself the attribution.
- A genuinely failed run with no later run on the branch still reports failed
  through the unchanged head-equality path - real failures are not hidden.
- In the coarse runs scan, an unresolvable head is unknown attribution and
  stops the scan (fm_nm_head_resolvable) instead of falling through to an
  older row; a resolvable-but-mismatched head keeps the historical
  reused-branch skip.

The exemption never applies to a terminal run and requires pipeline_owned
specifically, both pinned by negative-control tests. Fixture shape verified
against the live incident run's real axi status output.

* no-mistakes(document): Updated run-attribution documentation ownership
…unchenguid#3211)

* fix(pi): surface requested supervision outcomes

* no-mistakes(review): Mirror in-flight captain requests before branch dispatch

* no-mistakes(review): Exercise real branch ownership and main outcome access

* no-mistakes(review): Preserve request tails and align verdict guidance

* no-mistakes(review): Preserve complete current captain requests

* no-mistakes(review): Require visible requested outcomes and realistic classification

* no-mistakes(document): Align supervision outcome documentation

* no-mistakes(ci): Fixed Greptile’s runtime-ordering finding. The extension now stages Pi’s authoritative `before_agent_start` prompt before SessionManager persistence and suppresses the later duplicate entry. Updated docs and behavioral regression to reproduce real Pi ordering and verify each prompt is mirrored exactly once. Passed branch-extension tests, supervision tests, strict Pi typecheck, full lint, and diff checks

* no-mistakes(review): Use canonical operational input classification

* no-mistakes(review): Filter legacy operational inputs canonically

* no-mistakes(document): Clarify captain request mirroring boundary

* no-mistakes(ci): Fixed the CI time-boundary failure in tests/fm-public-followup.test.sh by pinning its clock, including context-registry setup. This prevents follow-up fixtures from expiring based on wall time. Verified the full regression suite passes, project-owned lint passes, and git diff checks are clean

* no-mistakes(document): Clarify captain-visible supervision outcome documentation
…#3210)

* feat(bin): per-home remote transport lanes with cancellation, bounded send, and closed stdin

All remote commands for every home on one host used to serialize through one
single-job-at-a-time worker on one shared queue: a timed-out caller abandoned a
staged job that kept running, retries convoyed behind it, fm-send's remote leg
had no time bound, and staging captured the caller's stdin to EOF so any
fm-on.sh caller with an open stdin wedged staging indefinitely.

- The worker now serves one lane per staged home: same-home jobs run strictly
  FIFO in a new staging-sequence order while different homes run concurrently,
  each lane as its own top-level worker process (a backgrounded subshell does
  not reliably reap dead children, so a zombie group leader kept a finished
  command's process group signalable). Long-poll preemption is lane-scoped.
- A caller that disconnects or times out cancels its job: the entrypoint marks
  the record on any post-staging exit and probes its parent so a dead ssh
  channel cancels without a signal; the worker skips cancelled queued jobs,
  terminates a running cancelled job's process group, and reaps the record.
- fm-send's remote leg is bounded by FM_SEND_REMOTE_BUDGET (default 30s) and a
  bound hit exits through the existing unconfirmed-delivery contract, which
  stays idempotent because the remote enqueue deduplicates.
- fm-on.sh defaults the remote command's stdin to /dev/null; the three payload
  callers pass the new --stdin flag. Abandoned .stage.* litter is age-reaped.
- The job execution deadline no longer loses up to a second to clock
  truncation.

* no-mistakes(review): Protect live stages and validate send budgets early

* no-mistakes(review): Preserve sequence lock ownership during stale recovery

* no-mistakes(review): Allocate job sequences at publication boundary

* no-mistakes(review): Bound remote keys and extend stale lock recovery

* no-mistakes(document): Document bounded remote transport behavior

* no-mistakes(lint): Suppress intentional deferred-expansion lint warning

* no-mistakes(ci): Fixed stale sequence-lock recovery by reconciling the counter against published job records before allocating the next sequence, preventing duplicate sequences and same-home FIFO violations. Added a behavioral regression test reproducing displacement after publication and verifying execution order. Passed fm-remote-transport-lanes.test.sh, fm-remote-job.test.sh, fm-lint.sh, and git diff --check

* no-mistakes(review): Use atomic sequence claims and lossless lane keys

* no-mistakes(review): Recover regressed sequence hints and rate-limit claim reaping

* no-mistakes(review): Restrict worker heartbeats to serving loop

* no-mistakes(review): Verify supervisor identity before lane recovery signals

* no-mistakes(review): Verify tracked lane and claim owner identities

* no-mistakes(document): Clarify remote lane and transport contracts

* no-mistakes(ci): Fixed the CI time-boundary failure by pinning fm-public-followup tests to a deterministic clock, including context-registry setup. Verified tests/fm-public-followup.test.sh, tests/fm-remote-transport-lanes.test.sh, shellcheck, and git diff --check

* no-mistakes(review): Preserve assigned lane ownership of queued jobs

* no-mistakes(review): Reserve homes owned by foreign queued lanes

* no-mistakes(review): Preserve completed results during crash recovery

* no-mistakes(review): Harden claim cleanup, expiry, and cancellation races

* no-mistakes(review): Verify process groups and reap abandoned results

* no-mistakes(review): Stop leaderless groups and reap cancelled publications

* no-mistakes(document): Correct remote transport lifecycle documentation

* no-mistakes(lint): Quote done state comparisons for ShellCheck
* fix(tests): make the changed-file map select per script and stabilize a budget flake

The changed-file map's bin/ fallback resolved a direct test reference to that
test's whole FAMILY. bin/fm-push-transition-lib.sh is named by exactly one
real-Herdr E2E, so a one-line change to it selected all 12 real-herdr-gated
scripts, including a 341s presentation E2E with no dependency on it.

Resolve direct test references per script, and keep resolving consumer bin/
scripts through the curated map so recorded family-level coupling survives.

Also fix a load-sensitive flake: the tool-update budget deadline is whole-second
granular, so a test budget of 1 left headroom anywhere in (0, 1] seconds and the
first budget check could already read as exhausted.

* feat(bin): make suite wall clock a result and let a family's concurrency be proven

--max-wall-ms fails a run whose wall clock exceeds the caller's budget, after
reporting the per-script results. A suite that stays green while outgrowing its
caller's invocation budget is the regression that got an agent killed mid-run
and retried invisibly, so duration has to be a result rather than a log note.

--pool on the isolation-proof harness runs the same concurrent proof over a
whole family, so 'is this family safe to parallelize?' is answered by a command
instead of a guess. Measured watcher-wake-lock and refused it: 3 of 18 scripts
fail under concurrency on wall-clock assertions about reaching the next poll.

* perf(bin): schedule the changed suite concurrently, longest first

The watcher-wake-lock family is proven concurrent-safe (two clean runs, 18
candidates, 0 failures at 4 workers; docs/fm-test-isolation-proof.md), so
--changed now schedules its proven-concurrent scripts with bounded parallelism
and runs any unproven remainder serially afterwards, never beside them.

Concurrent runs are ordered longest-hint-first. Workers are handed scripts in
order, so alphabetical order started the 193s fm-watch-triage last and stranded
it running alone: 395s wall against a 205s balanced four-worker sum.

An explicit --jobs keeps its strict refusal, so every CI lane is unchanged.

* fix(bin): bound a hung test instead of letting it hang the suite

tests/fm-calm-pi-extension.test.sh was observed running 17+ minutes against a
464ms recorded hint, and the suite had no per-script bound to stop it. An
unbounded suite is precisely what silently outruns a caller's invocation budget,
and --max-wall-ms is evaluated after the run so it cannot end one that never
finishes.

--per-script-timeout-secs terminates a script that outruns it and records exit
124, so the run still completes, accounts for the script, and fails. The
auto-concurrent --changed path applies 900s, far above the slowest real script
(the 341s Herdr presentation E2E), so it only ever converts a hang.

* no-mistakes(review): Enforce safe concurrency and descendant timeouts

* no-mistakes(review): Validate empty runs and isolation proof pools

* no-mistakes(review): Measure selection time in wall budget

* no-mistakes(review): Reap interrupted workers and bound finalization

* no-mistakes(review): Contain shutdown descendants and watchdog finalization

* no-mistakes(review): Honor remaining budget and close launch races

* no-mistakes(review): Restore timeout helper and simplify runner cleanup

* no-mistakes(review): Record isolation pool admission metadata

* no-mistakes(review): Bound Chrome reap and scope proof admission

* no-mistakes(review): Align proof scheduling and preserve budget summaries

* no-mistakes(review): Remove unreliable finalization watchdog

* no-mistakes(review): Freeze budget duration and enforce admission caps

* no-mistakes(document): Refresh test runner concurrency documentation

* no-mistakes(lint): Fix ShellCheck findings in test runner scripts

* no-mistakes(ci): Fixed Greptile’s concurrency-consent finding. `--changed` now remains serial by default; `--changed --jobs auto` explicitly opts into bounded concurrency and the automatic hang timeout. Updated documentation and added behavioral coverage proving serial default behavior, explicit concurrent scheduling, and refusal of `--jobs auto` outside `--changed`. Verified with `bash tests/fm-test-run.test.sh`, `bin/fm-lint.sh`, and `git diff --check`

* no-mistakes(review): Restore automatic changed-suite concurrency and timeout

* no-mistakes(review): Correct changed-suite contributor guidance

* no-mistakes(review): Reject gate-skipped isolation proofs

* no-mistakes(review): Correct automatic concurrency evidence

* no-mistakes(review): Isolate nested runner process groups

* no-mistakes(review): Remove unreliable signal cleanup machinery

* no-mistakes(test): Narrow changed-suite selection to executable contract owners

* no-mistakes(document): Document isolation proof skip and artifact semantics

* no-mistakes(ci): Fixed Greptile’s concurrency-consent finding. `--changed` now remains serial by default; bounded concurrency requires explicit `--jobs auto`. Updated behavioral coverage, contributor guidance, and isolation-proof commands accordingly. Verified with `tests/fm-test-run.test.sh`, `bin/fm-doc-audience-check.sh`, `bin/fm-lint.sh`, Bash syntax checks, and `git diff --check`; all passed

* no-mistakes(review): Restore plain changed-suite automatic concurrency

* no-mistakes(review): Record resolved changed-suite worker count

* fix(bin): keep a runner change selecting its whole curated family

A pipeline fix round narrowed the curated changed-file map so bin/fm-test-run.sh
and bin/fm-test-isolation-proof.sh selected only their own two contract tests,
and the documentation surfaces only the audience test. That cut this branch's
own changed selection from 33 scripts to 5.

The runner executes every pure-contract-unit script, so its contract test
passing proves its logic is right, not that the suite it drives still runs.
Narrowing it also makes any wall-clock claim about the changed suite trivially
true by not running the work.

Only the unmapped bin/* grep fallback resolves per script; curated mappings keep
their recorded family coupling.

* perf(bin): admit the pure-contract-unit family to bounded concurrency

A runner-file change selects pure-contract-unit, so that family decides the
changed suite's wall clock. With only watcher-wake-lock admitted, 14 of its 33
selected scripts fell to the serial tail and the selection measured 327.3s
against a 300s budget: the concurrent group was 19 scripts totalling 273.4s
while the tail alone was 215.7s.

bin/fm-test-isolation-proof.sh --pool pure-contract-unit --jobs 4 passes twice,
32 candidates, 0 failures, so the family is admitted on recorded evidence.

Full 33-script plain --changed: 327.3s -> 181.8s / 178.5s / 172.7s, 0 failures,
inside a 300000ms budget. Also states the per-script guard's derivation.

* no-mistakes(review): Align contract-unit concurrency cap with recorded proof

* no-mistakes(document): Record final changed-suite performance evidence

* fix(bin): keep an empty changed selection clean on stock macOS Bash

Under set -u, bash 3.2 treats "${arr[@]}" on an EMPTY array as an
unbound-variable error, while bash 4.4+ makes it a harmless no-op. The
concurrency work removed the early exit for an empty selection, so execution
fell through to the unguarded existence loop: on stock /bin/bash 3.2.57 a
contributor who changes only documentation and runs --changed got

  bin/fm-test-run.sh: line 1713: SCRIPTS[@]: unbound variable

with exit 1 and no summary, instead of a clean total=0 pass.

Restore the early exit, and guard every remaining array expansion reachable
with an empty selection. The reported duration is real elapsed invocation
time rather than a hardcoded zero, so a selection phase that outran
--max-wall-ms still fails.

Verified on this host with /bin/bash 3.2.57: exit 1 with the unbound-variable
error before, exit 0 with FM_TEST_SUMMARY total=0 after.

* no-mistakes(document): Document shell-bound changed-suite performance

---------

Co-authored-by: Kun Chen <kun-1@kunchenguid.com>
* feat(bin): publish per-home summary ledger

* no-mistakes(review): Bound and schedule home summary publication

* no-mistakes(review): Prove recurring watcher summary refresh cadence

* no-mistakes(review): Bound refresh workers and publish durable spawns

* no-mistakes(review): Fix atomic kill process-group coverage

* no-mistakes(review): Bound state initialization within refresh timeout

* no-mistakes(document): Document recurring bounded home-summary publication

* no-mistakes(review): Bound and log all best-effort refresh failures

* no-mistakes(review): Harden cadence and timeout regression coverage

* no-mistakes(document): Document home-summary runtime tuning

* no-mistakes(lint): Fix direct exit-code check in refresh test

* no-mistakes(ci): Fixed remote secondmate retirement recreating the deleted home: teardown now skips side-band summary refresh when its overridden state directory was removed. Verified with remote lifecycle E2E, teardown tests, home-summary tests, ShellCheck, and git diff checks

* no-mistakes(document): Clarify atomic home-summary publication guarantee
* fix(pi): gate first call on startup context

* no-mistakes(document): Correct Pi startup prerequisite verification date

* no-mistakes(review): Captain, fix startup process-group retirement after leader exit

* no-mistakes(review): Captain, release reload exit listeners on shutdown

* no-mistakes(review): Captain, complete startup exit lifecycle ownership

* no-mistakes(review): Captain, release empty startup process-group ownership promptly

* no-mistakes(review): Captain, supervise startup ownership and restore failure fallback

* no-mistakes(review): Captain, restore live Pi supervisor execution

* no-mistakes(document): docs: clarify Pi startup prerequisite delivery
* fix(pi): restore 0.84.4 adapter compatibility

* no-mistakes(review): Restore Pi collapsed and expanded outcome parity

* no-mistakes(review): Preserve Pi stock previews through capability probing

* no-mistakes(document): Document Pi 0.84.4 renderer compatibility
Bring origin/main up to date with kunchenguid/firstmate main (f66be0f)
while keeping every fork-only commit, including harness-creator, model
fallback, OpenRouter capacity, Graphify, federation, and quota-aware
dispatch.

Conflict resolution kept fork contracts (spawn-capacity, graph-first,
ask-user-authority independent of yolo, quota-array-dispatch,
merged-PR-notified-once) and took upstream safety (proved GitHub merge
outcomes, busy declared-pause cadence, Claude hung-claim recovery,
Pi 0.84.4 renderer compatibility, bounded remote transport lanes).
* fix(plugins): arm supervision in secondmate homes

The OpenCode watch-arm plugin wrongly excluded any root carrying a
.fm-secondmate-home marker and required a plain checkout (git-dir ==
git-common-dir). A treehouse-leased secondmate home is a linked worktree with
that marker, so it never armed its own supervision and depended on the shared
turn-end guard, whose fallback then fired spuriously.

Rework the gate into isArmEligibleRoot, mirroring bin/fm-primary-scope-lib.sh:
a genuine secondmate-home marker force-includes a primary home; only markerless
roots must be plain checkouts, so crewmate/scout task worktrees stay silent.
Export the predicate and cover it with regression tests over hermetic git
fixtures (plain primary, linked secondmate home, crewmate worktree, stray
marker).

* no-mistakes(review): mirror shell first-line marker parse in watch-arm plugin

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes(review): restore out-of-scope files; pin watch-arm predicate to shell owner

* no-mistakes(review): move arm-eligibility predicate to plugins lib; add arm-path regression

* no-mistakes(document): document opencode secondmate arm regression coverage

* no-mistakes(review): restore out-of-scope files; make arm-eligibility git spawn async

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* revert: drop unrelated daemon SIGTERM hardening from afk-inject test

Out of scope for the OpenCode watch-arm supervision-eligibility fix.
Belongs in its own change.

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes(document): move opencode plugin-loader rationale to its maintainer owner

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes
The shared merge-outcome reporter already owns the merge-notified
marker and duplicate suppression. Marking notified before that call
made the first merge look already recorded and swallowed it.

Map tests/assets/* so --changed can select the board-render suite.
Keep the opencode secondmate-home supervision arming that landed on
fork main while this branch was merging upstream.
…overy verification (#15)

* docs: record opencode 1.18.20 verification

* no-mistakes(review): Move OpenCode section below cleanup endpoint identity subsection

* no-mistakes(review): note 1.18.20 OpenRouter discovery re-verification in opencode heading

* no-mistakes(review): Scope opencode 1.18.20 qualifier, fix repro placeholder
* Fix watcher marker key collisions

Encode endpoint bytes into a versioned filename-safe key for every watcher marker.\n\nLegacy unversioned markers are intentionally ignored because their identity may be ambiguous; live endpoints begin with fresh v2 evidence rather than trusting or deleting shared legacy state.

* Share fast watcher marker keys

Use a pure-Bash v2 encoder and route away-mode watcher-marker cleanup through the shared key owner.

* no-mistakes(review): Mask marker bytes, share v2 key with herdr escalation

* no-mistakes(document): Document fm-marker-lib as watcher marker-key owner

* no-mistakes(lint): Feed cross-bash marker probe via heredoc, clearing SC2016

* test: seed remaining watcher fixtures with injective v2 keys

Main added write-deferral and secondmate-hold watcher tests after this
branch forked; they still named markers with the colliding tr fold.
Point those fixtures at the shared public v2 key so they exercise the
same identity the watcher now persists.

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes
Bring origin/main (88cce04) into fm/fm-upstream-sync-0829 to clear the
PR's conflict with its base. Merged rather than rebased so the fork-only
history and the two upstream-sync merge commits this PR is built from stay
intact.

One conflict, in docs/scripts.md: base rewrote the `fm-marker-lib.sh` row
to name it the single owner of the persistent per-window watcher marker
key (#18), while this branch had added an adjacent `fm-task-inbox-lib.sh`
row. Kept both - base's one-owner description is accurate in the merged
tree (fm-watch.sh, fm-supervise-daemon.sh, and backends/herdr.sh all call
the single fm_window_marker_key definition), and the fork's inbox row
documents a fork-only script that still exists.
…rmat

Resolve a semantic conflict the merge of origin/main (#18) could not flag.
#18 made fm-marker-lib.sh the single owner of the per-window marker key and
moved it to an injective `v2-<hex>` format, converting every fixture in
tests/fm-watch-triage.test.sh to the watch_marker_key oracle - base leaves
zero inline derivations in that file.

Two fixtures added on this branch (kunchenguid#3147, kunchenguid#3155) live in regions #18 never
touched, so git merged them cleanly while they kept deriving the key with
the pre-v2 `tr ':/.' '___'` form. They therefore seeded .stale-since-/
.wedge-escalations- markers under a name the watcher no longer reads, and
the away-mode lifted-pause phase waited for an escalation that could not
arrive:

  not ok - a lifted pause on an away-mode over-age busy pane no longer
           wedge-escalates

Point both at watch_marker_key, matching how base keys every other fixture
in this file. The task-keyed `_stale_key` families in fm-supervise-daemon.sh,
fm-push-transition-lib.sh and their fixtures are a separate contract that
base ships in the old form deliberately, and are left alone.
* fix(lock): bind Claude locks to session identity

* fix(lock): preserve shared session ownership checks

* no-mistakes(review): Bind workers and completion to lock identities

* no-mistakes(review): Snapshot lock ownership across workers and completion

* no-mistakes(document): Document session lock identity bindings

* test(lock): isolate fail-closed identity from ambient Claude env

An unidentifiable Claude acquisition must refuse without taking the
legacy compatibility path. The suite runner can inherit CLAUDECODE and
CLAUDE_CODE_SESSION_ID from a parent Claude session, which made those
cases identifiable and let them acquire the lock. Drop those markers so
the fail-closed assertions exercise a genuinely missing identity.

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes
`Require no-mistakes` failed on this PR with

  Pipeline attestation head_sha does not match the current PR head.
  attestation.head_sha: aae0fe9  PR head: dc2d00f

The attestation in the PR body was produced for aae0fe9. The gate then
advanced the head itself - `origin/main` moved to #18, that merge landed
as 59bc336, and dc2d00f repaired the fixtures it broke - and pushed those
commits (synchronize event 87) while the body still attested the older
head, so the required check reports a head mismatch.

CONTRIBUTING.md step 8 only described the two cases where the contributor
moved the head: a plain `git push` (permanently stale) and the transient
red between a gate push and the body rewrite that follows it. Neither
covers a head the gate advanced mid-pipeline, which is stale until the
gate pushes again and rebinds the attestation - exactly the remediation
the action's own error text gives.

Nothing in the tree is broken: `bin/fm-lint.sh` and
`bin/fm-test-run.sh --check-coverage` pass locally, and all 12 CI checks
are green on the attested head. Re-running the gate on this commit binds
the attestation to the current head and clears the check.
Brings adibirzu/firstmate main (c48a90f, session locks bound to session
identities) into the upstream-sync branch.

docs/sessionstart-nudge.md was the only conflict. Kept upstream's
description of the shared fm_session_lock_owned_by_current_session()
verifier that replaced the nudge wrapper's hard-coded eight-parent
ancestry walk, and kept the fork-only exit contract that qualifies the
exit-0 rule to ordinary transport paths and documents the run wrapper's
internal --pi-prerequisite silent exit 3.
…entity

Upstream #21 bound the session lock to a session identity and updated
every fixture it owned, including run_autoarm(), to export a controlled
CLAUDECODE / CLAUDE_CODE_SESSION_ID / CLAUDE_PID triple. The fork-only
run_autoarm_bg() background twin was invisible to that change, so after
the merge its hook could not prove ownership of the lock it had just
written, exited early, and never reached the reset boundary that
test_owner_mutex_contention_preserves_failure_episode_reset waits on.

Apply the same controlled identity there.
@adibirzu
adibirzu merged commit 226b138 into main Aug 31, 2026
12 of 13 checks passed
@adibirzu
adibirzu deleted the fm/fm-upstream-sync-0829 branch September 6, 2026 20:07
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.