Skip to content

feat(bin): merge upstream kunchenguid/firstmate main into the fork - #52

Merged
talktejas merged 224 commits into
mainfrom
fm/fm-merge-upstream-1006
Oct 6, 2026
Merged

talktejas merged 224 commits into
mainfrom
fm/fm-merge-upstream-1006

Conversation

@talktejas

@talktejas talktejas commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Merge instruction

Merge this pull request with a merge commit, not a squash.
It brings in kunchenguid/firstmate main (214 commits, through 5838f10). A squash would drop upstream's ancestry and make the next upstream merge replay all of them as conflicts.

What happened to the fork's own work

Upstream won every conflict. Follow-up commits fix only what the merge broke.

Survived (upstream has nothing landed for it)

  • The Jev checks and their docs/configuration.md sections: routine-wake triage for declared waits, house-rules check, already-exists search, finished check, review finding sort, pull request risk level, failed check sort, commit check, escalation screen, helper model pick, intake routing, worker health line.
  • The command center and automatic capture of every message sent to the captain.
  • The guard that refuses a primary session's own work inside a project.
  • The project development-branch store (.firstmate-base, registry base=), now only as a default that feeds upstream's --base-branch path where upstream accepts a named base. It is skipped on Gerrit projects, local-only tasks, scouts on local-only projects, and clones with no origin. Fleet sync still tracks the declared branch.
  • The durable pool claim on a task's working copy, and the refusal to launch into a copy another live task record still names.
  • Captain notes listed on every drain, and fm-inbox.sh pending.
  • Answer receipts, so a captain call whose finished row was pruned still verifies.
  • The launch-environment scrub and the INHERITED ENVIRONMENT line at session start.
  • The "Before you write any code" section in every brief.
  • Cleanup that names the step that failed.
  • The distinct exit code for a delivered steer whose decision close failed.
  • The bounded stale alarm for delivered work whose merge poll is armed.
  • The Lavish connection patch, the bearings board fixes, and the installer download retries in CI.

Dropped because upstream now covers it

  • Per-task spawn --base and its base= record: upstream's --base-branch.
  • Local-only landing and cleanup on a declared or per-task branch: upstream lands local-only work on the default branch.
  • Fire-and-forget re-read nudges to second mates: upstream's correlated nudge.
  • Note-head timestamps on status lines: upstream's [at=<epoch>] stamp, which the command center now reads.
  • Our contributions poll timeout work and the Jev "contributions timeout" triage class.
  • The dispatch resolver's small questions.
  • The generalised bounded-run helper: upstream's bin/fm-timeout-lib.sh.
  • Completion-gate inventory replacement and --repair-reason.
  • Our tie-break for two records naming one pool slot, our wake-queue stall-test fix, and the CI hold on an old Pi version.

Not reconciled: nothing.

To know after landing

  • A brief written before this change for a project that declares a development branch is refused at spawn and must be re-scaffolded.
  • Upstream raised the minimum tasks-axi to 0.2.6.
  • The remote second-mate lifecycle test fails one timing-bounded case on the build machine; it fails the same way on untouched upstream and on the fork's current main.
  • The "Behavior portable serial 2" check needed three attempts on the final head: two were cancelled at its 30-minute limit and the third passed in 17 minutes. Upstream's own runs of that shard swing between 16 and 30 minutes on the same test (tests/fm-supervision-host.test.sh), so it is runner luck, not this merge; shard headroom is separate follow-up work.

Intent

Firstmate runs from our fork, talktejas/firstmate, and updates itself only from that fork.
The fork has fallen behind the original repository, kunchenguid/firstmate: on 2026-10-06 the original's main held 214 commits the fork's main lacks, and the fork held 56 the original lacks.
Merge the original's main into the fork so the fork carries every one of those upstream changes.
Standing rule for this repository: upstream wins every conflict ("for firstmate repo upsteream wins every conflict").
Where upstream has landed code for something we also changed, take theirs; our own work survives only where upstream has nothing landed for it.
It arrives as one pull request into the fork's main for approval.

What Changed

  • Merges upstream/main (kunchenguid/firstmate, 214 commits) into the fork in one merge commit, taking upstream's side of every textual conflict. This brings in upstream's supervision host and engine, fleet ledger, live lab, host mirror, worker accounts, Devin harness, Gerrit forge detection, pipeline spend report, AI-trailer stripping, the reworked spawn, watch, wake, inbox and captain-hold scripts, plus their skills, docs and tests.
  • Re-attaches fork-only features to upstream's reworked code where upstream has nothing equivalent: the Jev checks (house-rules and already-exists steps, PR risk line, routine-wake triage, commit-check launch setting), the project development-branch store (now routed through upstream's --base-branch, and gated where upstream refuses a named base), the durable pool lease, captain notes on every drain and fm-inbox.sh pending, answer receipts for pruned captain calls, the launch-environment scrub, and failed-cleanup-step messages.
  • Drops fork work that upstream now provides — per-task spawn --base, note-head status timestamps (and tests/fm-classify-status-timestamp.test.sh), contributions poll timeout handling, dispatch-resolver small questions, the bounded-run helper generalisation, completion-gate inventory replacement and --repair-reason, and the CI hold on an old Pi version — and updates fork-only code, tests and docs for upstream's renamed helpers, stricter lint and sourced launch script.

🤖 Generated with Claude Code

Risk Assessment

⚠️ Medium: No substantiated defects remain and the merge conforms to the upstream-wins intent, but it is a 400-file merge re-integrating fork features into core spawn, teardown, wake and watcher paths, and this phase verified it by reading only, with the test step still to run.

Testing

Checked merge ancestry against the live upstream and fork remotes, ran seven targeted behaviour tests (base-branch spawn, brief, turn-end guard, secondmate restart, pool base freshen, task delivery, remote secondmate lifecycle e2e), and drove the project-base default gate by hand in an isolated temp home. Six tests pass and the manual gate transcript matches every expected case. The remote secondmate lifecycle e2e fails at one timing-bounded case; the same case fails on snapshots of pristine upstream 5838f10 and fork base cdc2e57, so it is a pre-existing host issue left to remote CI. No visual evidence: the change has no rendered UI surface that was exercised.

  • Live validation: ✅ go - 8 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Merging the branch gives the fork every upstream commit: all 214 commits of kunchenguid/firstmate main are reachable from the branch head, on top of the fork's current main ✅ pass live git ls-remote of both remotes plus git rev-list counts: upstream main 5838f10 is the merge's second parent, 0 upstream commits missing from HEAD, 214 not in base, 56 fork-only, base cdc2e57 equals for…
Adversarial: no unresolved conflict markers are left anywhere in the merged tree ✅ pass live git grep for conflict marker lines at HEAD returned nothing
Upstream wins the re-read nudge conflict: the nudge scripts, fm-merge-local and their tests are exactly upstream's, and the secondmate restart flow works ✅ pass live git diff --quiet 5838f10 HEAD on six files (all identical); fm-secondmate-restart.log exit=0
A task spawned with no --base-branch on a project that declares a development branch starts on that branch and records base_branch ✅ pass live fm-spawn-base-branch.log (cold slot starts on develop and records it; scout also starts from the recorded base; brief records the declared base); project-base-default-live.txt
Adversarial: the declared base is silently skipped where upstream refuses a named base — local-only task, scout on a local-only project, Gerrit project, clone with no origin ✅ pass live fm-spawn-base-branch.log cases 7-10 (real spawn, asserts worktree HEAD and meta); project-base-default-live.txt shows empty output and exit 0 for each skip case
A project with no declared base, or an unregistered project, still spawns from the default branch as upstream does; a malformed or missing declared branch is rejected ✅ pass live fm-spawn-base-branch.log cases 3, 5, 6, 17; fm-spawn-pool-base-freshen.log exit=0
Brief scaffolding and task delivery (including local-only landing and Gerrit) work after the merge ✅ pass live fm-brief.log exit=0; fm-task-delivery.log exit=0
Turn-end guard works with the merged settings: all nine tracked Claude hook entries are inert under grok and live under Claude ✅ pass live fm-turnend-guard.log exit=0, line 'tracked .claude/settings.json entries: 9 inert under grok, the documented subagent exception still armed, all live under Claude'
Upstream's correlated re-read reaches a remote secondmate and its reply is awaited (remote secondmate lifecycle e2e) ⏸️ untested no The test's earlier concurrent-spawn case times out on this WSL2 host on upstream and on the fork's main alike, so the script exits before the re-read assertions. This host cannot run that case; remote…
Evidence: Project base default gate, driven by hand

Source: Project base default gate, driven by hand

plain resolver (nm) -> develop --task-default no-mistakes, no-mistakes project -> develop --task-default local-only -> (empty, exit 0) --task-default '' (scout), local-only project -> (empty, exit 0) --task-default '' (scout), no-mistakes project -> develop --task-default no-mistakes / '' on gerrit project -> (empty, exit 0) --task-default no-mistakes, clone with no origin -> (empty, exit 0)

\### plain resolver
$ ~/.no-mistakes/worktrees/3605d2c32b02/01M481SHCZ981908NP0QSTB1Q6/bin/fm-project-base.sh /tmp/nm-live.tRWNoT/proj nm
develop
[exit 0]
\### --task-default: ship on no-mistakes project (expect develop)
$ ~/.no-mistakes/worktrees/3605d2c32b02/01M481SHCZ981908NP0QSTB1Q6/bin/fm-project-base.sh --task-default no-mistakes /tmp/nm-live.tRWNoT/proj nm
develop
[exit 0]
\### --task-default: local-only task (expect empty)
$ ~/.no-mistakes/worktrees/3605d2c32b02/01M481SHCZ981908NP0QSTB1Q6/bin/fm-project-base.sh --task-default local-only /tmp/nm-live.tRWNoT/proj nm
[exit 0]
\### --task-default: scout (empty mode) on local-only project (expect empty)
$ ~/.no-mistakes/worktrees/3605d2c32b02/01M481SHCZ981908NP0QSTB1Q6/bin/fm-project-base.sh --task-default  /tmp/nm-live.tRWNoT/proj lo
[exit 0]
\### --task-default: scout on no-mistakes project (expect develop)
$ ~/.no-mistakes/worktrees/3605d2c32b02/01M481SHCZ981908NP0QSTB1Q6/bin/fm-project-base.sh --task-default  /tmp/nm-live.tRWNoT/proj nm
develop
[exit 0]
\### --task-default: gerrit project, ship and scout (expect empty)
$ ~/.no-mistakes/worktrees/3605d2c32b02/01M481SHCZ981908NP0QSTB1Q6/bin/fm-project-base.sh --task-default no-mistakes /tmp/nm-live.tRWNoT/proj ger
[exit 0]
$ ~/.no-mistakes/worktrees/3605d2c32b02/01M481SHCZ981908NP0QSTB1Q6/bin/fm-project-base.sh --task-default  /tmp/nm-live.tRWNoT/proj ger
[exit 0]
\### --task-default: clone with no origin (expect empty)
$ ~/.no-mistakes/worktrees/3605d2c32b02/01M481SHCZ981908NP0QSTB1Q6/bin/fm-project-base.sh --task-default no-mistakes /tmp/nm-live.tRWNoT/noorigin noorg
[exit 0]
\### project-mode readers
$ ~/.no-mistakes/worktrees/3605d2c32b02/01M481SHCZ981908NP0QSTB1Q6/bin/fm-project-mode.sh nm
no-mistakes off
[exit 0]
$ ~/.no-mistakes/worktrees/3605d2c32b02/01M481SHCZ981908NP0QSTB1Q6/bin/fm-project-mode.sh --forge ger
gerrit
[exit 0]
$ ~/.no-mistakes/worktrees/3605d2c32b02/01M481SHCZ981908NP0QSTB1Q6/bin/fm-project-mode.sh --base nm
develop
[exit 0]
Evidence: Merge ancestry against live remotes
kunchenguid/firstmate main = 5838f105008cb24bc999a819d3c5b50c2fd5675d (merge 21e3ac6 second parent)
talktejas/firstmate main = cdc2e5728d07a94e1c6ebaf9fcf2b5cdffdd504f (base, ancestor of HEAD)
upstream commits missing from HEAD: 0
upstream commits not in base: 214
fork commits not in upstream: 56
conflict markers at HEAD: none
Evidence: Base-branch spawn test log

Source: Base-branch spawn test log

ok - base= parses alongside the delivery mode without changing any existing line's mode
ok - a cold slot for a develop-based project starts the worker on develop and records it
ok - a project with no base record still starts from the repo default branch
ok - a project whose default branch is the right base passes the assertion
ok - an unregistered project falls back to the default branch and spawns as today
ok - a base= record naming a branch the repo does not have is refused
ok - a local-only task ignores the project's declared base
ok - a scout on a local-only project ignores the project's declared base
ok - a scout on a Gerrit project ignores the project's declared base
ok - a task on a clone with no origin ignores the project's declared base
ok - a brief that does not record the project's declared base is refused
ok - a scaffolded brief records the project's declared base, except for local-only
ok - a scout also starts from the project's recorded base branch
ok - a declaration landed only on the development branch is still read by a clone on the default branch
ok - the repository's own declaration wins over a home-private registry record
ok - the private registry remains the fallback for a project that has not adopted the file
ok - a malformed declaration is rejected rather than handed to git
ok - the resolver reads a declaration from an unchecked-out branch under stock /bin/bash
# all fm-spawn-base-branch tests passed
exit=0
Evidence: Turn-end guard test log

Source: Turn-end guard test log

ok - fm_supervision_unhealthy: false with no state/*.meta at all
ok - fm_supervision_unhealthy: true with in-flight task and no beacon ever
ok - fm_supervision_unhealthy: true with in-flight task and a beacon far outside the grace window
ok - fm_supervision_unhealthy: false with in-flight task and a fresh beacon
ok - fm_supervision_status: FM_SUP_QUEUE_PENDING tracks state/.wake-queue
ok - fm_supervision_needed: X-mode relay poll needs supervision
ok - fm_supervision_unhealthy: source-only home needs supervision
ok - fm_supervision_needed: a registered custom check needs supervision with no task in flight
ok - fm_supervision_needed: a registered check whose bytes drifted still needs supervision
ok - fm_supervision_needed: false for a check.sh with no registration binding
ok - fm_supervision_needed: a task PR poll without a trust binding is not a registered custom check
ok - fm_supervision_status: the relay shim is not counted as a registered custom check
ok - fm-turnend-guard: silent no-op with nothing in flight
ok - fm-turnend-guard: blocks when a fresh beacon has no live watcher lock
ok - fm-turnend-guard: non-Claude path blocks a source-only home
ok - fm-turnend-guard: blocks on a dead watcher lock even when the beacon is fresh
ok - fm-turnend-guard: silent no-op with a live watcher lock and fresh beacon
ok - fm-turnend-guard: healthy non-Claude harness paths ignore Claude episode contention
ok - fm-turnend-guard: blocks on a live watcher lock with an ancient beacon
ok - fm-turnend-guard: blocks with the exact required reason in the primary when unhealthy
ok - fm-turnend-guard: blocks from active FM_HOME state, not only repo-root state
ok - fm-turnend-guard: X-mode repair reason sources the cadence config
ok - fm-turnend-guard: X-mode-only supervision remains guarded in default mode
ok - fm-turnend-guard: registered-check-only supervision is named in the block banner
ok - fm-turnend-guard: ignores stale repo-root state when FM_HOME is set
ok - fm-turnend-guard: uses FM_STATE_OVERRIDE ahead of FM_HOME/state
ok - fm-turnend-guard: stop_hook_active=true always allows the stop (never blocks twice in one turn)
ok - fm-turnend-guard: blocks a blind turn end in a secondmate's own home (.fm-secondmate-home no longer excludes it)
ok - fm-turnend-guard: idle-by-default - silent in a secondmate home with nothing in flight
ok - fm-turnend-guard: stop_hook_active=true allows the stop in a secondmate home (never blocks twice in one turn)
ok - fm-turnend-guard: secondmate deferred-death recovery - silent while watched, forces re-arm once the watcher exits
ok - fm-turnend-guard: inert in a secondmate's own child worktree (linked git worktree) even when unhealthy
ok - fm-turnend-guard: blocks a blind turn end in a treehouse-leased LINKED secondmate home (marker force-include)
ok - fm-turnend-guard: an invalid (empty) marker cannot spoof inclusion; linked worktree stays exempt
ok - fm-turnend-guard: a non-ASCII marker cannot spoof inclusion; linked worktree stays exempt
ok - fm-turnend-guard: inert in a crewmate/scout task worktree (linked git worktree) even when unhealthy
ok - fm-turnend-guard: fails open (never blocks) when jq is missing
ok - fm-turnend-guard: silent no-op on empty stdin
ok - fm-turnend-guard: runs well under the generous timing margin (1s)
ok - fm-turnend-guard-grok: forces one explicitly marked same-session resume when the shared predicate blocks
ok - fm-turnend-guard-grok: legacy environment loop guard prevents a nested resume loop
ok - fm-turnend-guard-grok: native false delegates blocking feedback with zero resume processes
ok - fm-turnend-guard-grok: native true remains bounded and starts no resume process
ok - fm-turnend-guard-grok: both spellings are typed and camelCase has deterministic precedence
ok - fm-turnend-guard-grok: malformed, invalidly typed, and missing-prerequisite payloads start neither path
ok - fm-turnend-guard-grok: missing jq and no-supervision-needed stops stay silent and bounded
ok - tracked .claude/settings.json entries: 9 inert under grok, the documented subagent exception still armed, all live under Claude
ok - .codex/hooks.json: Stop hook uses hook process root when payload cwd is outside
ok - .codex/hooks.json: Stop hook ignores nested git root guard scripts
ok - .opencode primary plugin: guard path is anchored to worktree, not directory
ok - .pi primary extension: no-tool and multi-tool runs each inject exactly one guard follow-up
ok - .pi primary extension: delivery failure resets the logical-run latch
ok - fm-turnend-guard --claude: re-blocks a loop-guarded stop while unhealthy and unclaimed (incident regression)
ok - fm-turnend-guard --claude: X-mode-only homes re-block when auto-arm recovery is absent
ok - fm-turnend-guard --claude: a live arming epoch advances once and repeated observation is idempotent
ok - fm-turnend-guard --claude: repeated failed-to-arming races make bounded monotonic progress
ok - fm-turnend-guard --claude: terminal owner boundary excludes a concurrent start without deadlock
ok - fm-turnend-guard --claude: fresh rewake epoch prevents a duplicate continuation for the same event
ok - fm-turnend-guard --claude: an abandoned auto-arm claim no longer allows a blind stop (incident regression)
ok - fm-turnend-guard --claude: a claim whose pid was reused stops counting as recovery even while its entry reads arming
ok - fm-turnend-guard --claude: a hung owner frozen at arming with no watcher beat no longer allows a blind stop
ok - fm-turnend-guard --claude: a live open generation claim owns recovery with no lock held
ok - fm-turnend-guard --claude: a stuck generation claim no longer allows a blind stop
ok - fm-turnend-guard --claude: the terminal path clears an abandoned claim instead of stepping aside silently
ok - fm-turnend-guard --claude: fresh failed epochs preserve and advance monotonic fail-open progression
ok - fm-turnend-guard --claude: integrated fresh failures reach one bounded fail-open, stop continuation, and reset on recovery
ok - fm-turnend-guard --claude: an inert auto-arm's frozen epoch reaches one bounded fail-open and resets on recovery
ok - fm-turnend-guard --claude: a frozen unverified epoch spends the budget yet still blocks
ok - fm-turnend-guard --claude: reset contention preserves all episode state until retry
ok - fm-turnend-guard --claude: concurrent auto-arm and guard resets are idempotent and deadlock-free
ok - fm-turnend-guard --claude: stale rewake epoch does not allow a blind stop
ok - fm-turnend-guard --claude: budget exhaustion alone cannot permit a blind stop
ok - fm-turnend-guard --claude: verified fail-open is loud, bounded, attended, and non-repeating
ok - fm-turnend-guard --claude: fail-open requires both exhausted retries and consumed notice
ok - fm-turnend-guard --claude: away ownership excludes the Stop-autoarm fail-open
ok - fm-turnend-guard --claude: positive watcher recovery resets failure episode state
ok - fm-turnend-guard --claude: bounded claim wait avoids a token-consuming forced continuation
ok - fm-turnend-guard --claude: secondmate home re-blocks unclaimed and allows auto-arm-claimed stops
ok - fm-turnend-guard: a live away-mode daemon satisfies supervision with no watcher holding the lock
ok - fm-turnend-guard: away-mode daemon ownership survives a leftover dead watcher lock
ok - fm-turnend-guard: away mode with no daemon and no watcher still blocks
ok - fm-turnend-guard: away mode blocks on a dead away-mode daemon
ok - fm-turnend-guard: away mode blocks on a pid-reused away-mode daemon lock
ok - fm-turnend-guard: away-mode daemon ownership never substitutes for a fresh beacon
ok - fm-turnend-guard: a daemon lock proves nothing while away mode is off
ok - fm-turnend-guard: away-mode beacon freshness uses the poll-derived grace, not the flat default
ok - fm-turnend-guard: a dead away-mode daemon still blocks under the poll-derived grace
ok - fm-turnend-guard: the poll-derived grace is bounded, not unlimited
ok - fm-turnend-guard: with away mode off, the poll-derived grace never applies
exit=0
Evidence: Secondmate restart test log

Source: Secondmate restart test log

ok - T1 persist is a gate, and asks for open records and task status only
ok - T2 the mate persists before anything is stopped
ok - T2b an arrived persist answer is resolved before timeout
ok - T2c a reply between the preliminary scan and timeout decision wins
ok - T3 a runtime that cannot prove a restart falls back to the re-read message
ok - T4 every named mate is accounted for, including one this home does not know
ok - T5 a refused restart leaves the mate running and reports an unknown outcome
ok - T8 a local restart re-resolves this home's pin and reports the runtime that came up
ok - native Ultra survives local restart and the remote restart transport
ok - T6 a remote mate restarts through the host-local control plane over the fm-on hop
ok - T7 an unreachable host is reported honestly instead of claimed as reloaded
ok - T9 the persist gate retains its explicitly allocated correlation
ok - T10 pending persist answers are polled as one fleet
ok - T11 post-stop restart failure is never misreported as a nudge
ok - T12 relaunch waits do not block fleet persistence polling
ok - T13 a dead restart worker cannot hang the parent
ok - T14 a result published during reaping is honored
ok - T15 an already-current live mate is named by the update pass and genuinely restarted
ok - T16 an already-current mate with an unprovable runtime keeps the honest nudge path
# all fm-secondmate-restart tests passed
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/already-current-16523/home/state/sm1.git-hooks/sendemail-validate': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/already-current-16523/home/state/sm1.git-hooks/pre-auto-gc': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/already-current-16523/home/state/sm1.git-hooks/post-rewrite': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/already-current-16523/home/state/sm1.git-hooks/pre-push': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/already-current-16523/home/state/sm1.git-hooks/post-merge': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/already-current-16523/home/state/sm1.git-hooks/post-checkout': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/already-current-16523/home/state/sm1.git-hooks/pre-rebase': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/already-current-16523/home/state/sm1.git-hooks/post-commit': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/already-current-16523/home/state/sm1.git-hooks/prepare-commit-msg': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/already-current-16523/home/state/sm1.git-hooks/pre-merge-commit': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/already-current-16523/home/state/sm1.git-hooks/pre-commit': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/already-current-16523/home/state/sm1.git-hooks/post-applypatch': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/already-current-16523/home/state/sm1.git-hooks/pre-applypatch': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/already-current-16523/home/state/sm1.git-hooks/applypatch-msg': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/already-current-16523/home/state/sm1.git-hooks/commit-msg': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/relaunch-polling-21452/home/state/sm2.git-hooks/sendemail-validate': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/relaunch-polling-21452/home/state/sm2.git-hooks/pre-auto-gc': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/relaunch-polling-21452/home/state/sm2.git-hooks/post-rewrite': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/relaunch-polling-21452/home/state/sm2.git-hooks/pre-push': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/relaunch-polling-21452/home/state/sm2.git-hooks/post-merge': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/relaunch-polling-21452/home/state/sm2.git-hooks/post-checkout': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/relaunch-polling-21452/home/state/sm2.git-hooks/pre-rebase': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/relaunch-polling-21452/home/state/sm2.git-hooks/post-commit': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/relaunch-polling-21452/home/state/sm2.git-hooks/prepare-commit-msg': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/relaunch-polling-21452/home/state/sm2.git-hooks/pre-merge-commit': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/relaunch-polling-21452/home/state/sm2.git-hooks/pre-commit': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/relaunch-polling-21452/home/state/sm2.git-hooks/post-applypatch': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/relaunch-polling-21452/home/state/sm2.git-hooks/pre-applypatch': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/relaunch-polling-21452/home/state/sm2.git-hooks/applypatch-msg': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/relaunch-polling-21452/home/state/sm2.git-hooks/commit-msg': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/post-stop-4756/home/state/sm1.git-hooks/sendemail-validate': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/post-stop-4756/home/state/sm1.git-hooks/pre-auto-gc': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/post-stop-4756/home/state/sm1.git-hooks/post-rewrite': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/post-stop-4756/home/state/sm1.git-hooks/pre-push': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/post-stop-4756/home/state/sm1.git-hooks/post-merge': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/post-stop-4756/home/state/sm1.git-hooks/post-checkout': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/post-stop-4756/home/state/sm1.git-hooks/pre-rebase': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/post-stop-4756/home/state/sm1.git-hooks/post-commit': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/post-stop-4756/home/state/sm1.git-hooks/prepare-commit-msg': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/post-stop-4756/home/state/sm1.git-hooks/pre-merge-commit': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/post-stop-4756/home/state/sm1.git-hooks/pre-commit': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/post-stop-4756/home/state/sm1.git-hooks/post-applypatch': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/post-stop-4756/home/state/sm1.git-hooks/pre-applypatch': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/post-stop-4756/home/state/sm1.git-hooks/applypatch-msg': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/post-stop-4756/home/state/sm1.git-hooks/commit-msg': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/concurrent-waits-13129/home/state/sm2.git-hooks/sendemail-validate': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/concurrent-waits-13129/home/state/sm2.git-hooks/pre-auto-gc': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/concurrent-waits-13129/home/state/sm2.git-hooks/post-rewrite': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/concurrent-waits-13129/home/state/sm2.git-hooks/pre-push': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/concurrent-waits-13129/home/state/sm2.git-hooks/post-merge': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/concurrent-waits-13129/home/state/sm2.git-hooks/post-checkout': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.

... [4685 bytes truncated] ...

mission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/pin-4693/home/state/sm1.git-hooks/applypatch-msg': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/pin-4693/home/state/sm1.git-hooks/commit-msg': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/unknown-24503/home/state/sm1.git-hooks/sendemail-validate': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/unknown-24503/home/state/sm1.git-hooks/pre-auto-gc': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/unknown-24503/home/state/sm1.git-hooks/post-rewrite': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/unknown-24503/home/state/sm1.git-hooks/pre-push': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/unknown-24503/home/state/sm1.git-hooks/post-merge': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/unknown-24503/home/state/sm1.git-hooks/post-checkout': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/unknown-24503/home/state/sm1.git-hooks/pre-rebase': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/unknown-24503/home/state/sm1.git-hooks/post-commit': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/unknown-24503/home/state/sm1.git-hooks/prepare-commit-msg': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/unknown-24503/home/state/sm1.git-hooks/pre-merge-commit': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/unknown-24503/home/state/sm1.git-hooks/pre-commit': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/unknown-24503/home/state/sm1.git-hooks/post-applypatch': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/unknown-24503/home/state/sm1.git-hooks/pre-applypatch': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/unknown-24503/home/state/sm1.git-hooks/applypatch-msg': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/unknown-24503/home/state/sm1.git-hooks/commit-msg': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/answer-at-timeout-decision-14810/home/state/sm1.git-hooks/sendemail-validate': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/answer-at-timeout-decision-14810/home/state/sm1.git-hooks/pre-auto-gc': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/answer-at-timeout-decision-14810/home/state/sm1.git-hooks/post-rewrite': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/answer-at-timeout-decision-14810/home/state/sm1.git-hooks/pre-push': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/answer-at-timeout-decision-14810/home/state/sm1.git-hooks/post-merge': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/answer-at-timeout-decision-14810/home/state/sm1.git-hooks/post-checkout': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/answer-at-timeout-decision-14810/home/state/sm1.git-hooks/pre-rebase': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/answer-at-timeout-decision-14810/home/state/sm1.git-hooks/post-commit': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/answer-at-timeout-decision-14810/home/state/sm1.git-hooks/prepare-commit-msg': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/answer-at-timeout-decision-14810/home/state/sm1.git-hooks/pre-merge-commit': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/answer-at-timeout-decision-14810/home/state/sm1.git-hooks/pre-commit': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/answer-at-timeout-decision-14810/home/state/sm1.git-hooks/post-applypatch': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/answer-at-timeout-decision-14810/home/state/sm1.git-hooks/pre-applypatch': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/answer-at-timeout-decision-14810/home/state/sm1.git-hooks/applypatch-msg': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/answer-at-timeout-decision-14810/home/state/sm1.git-hooks/commit-msg': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/arrived-at-bound-31786/home/state/sm1.git-hooks/sendemail-validate': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/arrived-at-bound-31786/home/state/sm1.git-hooks/pre-auto-gc': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/arrived-at-bound-31786/home/state/sm1.git-hooks/post-rewrite': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/arrived-at-bound-31786/home/state/sm1.git-hooks/pre-push': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/arrived-at-bound-31786/home/state/sm1.git-hooks/post-merge': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/arrived-at-bound-31786/home/state/sm1.git-hooks/post-checkout': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/arrived-at-bound-31786/home/state/sm1.git-hooks/pre-rebase': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/arrived-at-bound-31786/home/state/sm1.git-hooks/post-commit': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/arrived-at-bound-31786/home/state/sm1.git-hooks/prepare-commit-msg': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/arrived-at-bound-31786/home/state/sm1.git-hooks/pre-merge-commit': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/arrived-at-bound-31786/home/state/sm1.git-hooks/pre-commit': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/arrived-at-bound-31786/home/state/sm1.git-hooks/post-applypatch': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/arrived-at-bound-31786/home/state/sm1.git-hooks/pre-applypatch': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/arrived-at-bound-31786/home/state/sm1.git-hooks/applypatch-msg': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/arrived-at-bound-31786/home/state/sm1.git-hooks/commit-msg': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/order-15727/home/state/sm1.git-hooks/sendemail-validate': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/order-15727/home/state/sm1.git-hooks/pre-auto-gc': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/order-15727/home/state/sm1.git-hooks/post-rewrite': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/order-15727/home/state/sm1.git-hooks/pre-push': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/order-15727/home/state/sm1.git-hooks/post-merge': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/order-15727/home/state/sm1.git-hooks/post-checkout': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/order-15727/home/state/sm1.git-hooks/pre-rebase': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/order-15727/home/state/sm1.git-hooks/post-commit': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/order-15727/home/state/sm1.git-hooks/prepare-commit-msg': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/order-15727/home/state/sm1.git-hooks/pre-merge-commit': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/order-15727/home/state/sm1.git-hooks/pre-commit': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/order-15727/home/state/sm1.git-hooks/post-applypatch': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/order-15727/home/state/sm1.git-hooks/pre-applypatch': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/order-15727/home/state/sm1.git-hooks/applypatch-msg': Permission denied
rm: cannot remove '/tmp/fm-secondmate-restart.vnYgLA/order-15727/home/state/sm1.git-hooks/commit-msg': Permission denied
exit=0
Evidence: Brief test log

Source: Brief test log

ok - fm-brief: scaffolds leave the worker role scope to the launch boundary and keep the secondmate contract
ok - fm-brief.sh: bash -n succeeds
/tmp/fm-brief.WUzKgW/heredoc-in-substitution.sh:2
ok - fm-brief.sh: no heredoc is nested inside a command substitution (Bash 3.2 parse-safe)
ok - fm-brief.sh: --help renders the complete header
ok - fm-brief.sh: no-mistakes/direct-PR/local-only briefs generate cleanly
ok - fm-brief.sh: ship --mode is required and closed-set validated
ok - fm-brief.sh: the explicit ship mode wins over the registered posture
ok - fm-brief.sh: --yolo and scout/secondmate --mode are refused, never silently dropped
ok - fm-brief.sh: faster paths use configured authority without stacked review
ok - fm-brief.sh: no-mistakes DOD keeps its apostrophe prose and bans --yes outright
ok - fm-brief.sh: no-mistakes DOD detects a green PR from the drive call, not a status poll
ok - fm-brief.sh: PR-based done requires a non-draft PR; a deliberate draft declares a wait
ok - fm-brief.sh: no-mistakes ask-user findings use one event plus a verbatim snapshot
ok - fm-brief.sh: ship project-memory wording bounds edits to corrections of wrong information
ok - fm-brief.sh: --herdr-lab emits the complete hard safety contract
ok - fm-brief.sh: --herdr-lab uses its quoted Firstmate-owned helper path
ok - fm-brief.sh: ship and scout scaffolds make omitted Herdr intent fail-visible
ok - fm-brief.sh: the documented {TASK} and {FIRSTMATE_SPEC} fills cannot corrupt the Herdr safety gate
ok - fm-brief.sh: Herdr lab contract covers scouts and rejects secondmate misuse
ok - fm-brief.sh: --no-projects scaffolds a project-less charter and guards misuse
ok - fm-brief.sh: marked requests avoid generic acknowledgements and preserve material reporting
ok - fm-brief.sh: relative directory inputs ignore CDPATH, render stable absolute charter paths, or fail loudly
ok - fm-brief.sh: custom pause verb renders in every scaffold
ok - fm-brief.sh: ship and scout scaffolds declare a validation-round pause once, then hold it
ok - fm-brief.sh: investigation and visual-review completions load the shared decision policy
ok - fm-brief: scout and secondmate code paths still scaffold well-formed briefs
ok - fm-brief.sh: scout Lavish hosting follows the bootstrap lavish-axi floor
ok - fm-brief.sh: every ship and scout brief carries the study-existing-first section before # Task
ok - fm-promote.sh: promotion preserves the study-existing-first section from the scout brief
ok - fm-brief: workers end the turn on a decision, wait in one bounded shell command, and never poll
ok - fm-brief: without config/wait-no-turns the brief and drive text stay as they were
ok - fm-brief.sh: the home brief include lands last on ship and scout, verbatim, and fails closed
ok - fm-brief.sh: --base-branch records the base, targets the PR at it, and is refused where no PR carries it
ok - fm-brief.sh: --branch-prefix omitted defaults every ship mode to fm/<task-id>
ok - fm-brief.sh: a --branch-prefix override renders identically across every generated section
ok - fm-brief.sh: an empty --branch-prefix override resolves to a bare <task-id> branch
ok - fm-brief.sh: --branch-prefix is refused on scout and secondmate scaffolds
ok - fm-brief.sh: --branch-prefix value is validated against embedded spaces and a leading dash
Switched to a new branch '$(touch${IFS}/tmp/fm-brief.WUzKgW/branch-prefix-shell-safe-marker)brief-branch-safe-g3'
ok - fm-brief.sh: ref-format-valid shell metacharacters stay literal in generated branch commands
ok - fm-brief.sh: every crewmate scaffold forbids administering the shared worktree pool
exit=0

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

⏭️ **Rebase** - skipped

Step was skipped.

⚠️ **Review** - 1 info
  • 🚨 tests/fm-session-start.test.sh:1577 - Upstream's test_abnormal_digest_death_banners_and_exits_zero finds the digest child to kill by looking for FM_SESSION_START_STAGE_FILE= in /proc/&lt;pid&gt;/environ. The fork's surviving change in bin/fm-session-start.sh (lines 335-365) hands the stage file over as a private --_child &lt;file&gt; argument and unsets that variable in the parent, so no process carries the marker. Trace on Linux: the fake ps walks the ancestry, never matches, target stays empty, nothing is killed, the digest completes normally, and the assertion for STARTUP TRUNCATED - SESSION START DIED UNEXPECTEDLY (exit 143...) fails. Traced by reading, not run (review does not run tests). Fix: make the fake ps identify the digest child by its own argv (the bash process running fm-session-start.sh --_child ..., not the timeout wrapper above it), and update the comment at 1555-1565 that still describes the env hand-off.
  • ⚠️ tests/fm-remote-secondmate-lifecycle-e2e.test.sh:1066 - Intent: "upstream wins every conflict". Two conflict hunks in this file (lines 1066-1069 and 1131-1134) end on the fork's side. The merge commit took upstream's lines (correlated reread: newest_remote_inbox_corr, await_reply_result ... $SID.2.result / $SID.3.result, fm-procevent-remote-reply.sh handle), then fa41dbf replaced all 16 of them with the fork's delivery= / no corr= assertions. The reason is real: the fork's fire-and-forget reread nudge (bin/fm-config-push.sh, bin/fm-bootstrap.sh, bin/fm-secondmate-restart.sh, fm_secondmate_nudge_delivery_id) auto-merged and upstream's assertions would fail against it. Decision needed: keep the fork's fire-and-forget nudge with its test (an exception to the standing rule), or drop the nudge change and restore upstream's test lines.
  • ⚠️ bin/fm-spawn.sh:3457 - Intent: "Where upstream has landed code for something we also changed, take theirs; our own work survives only where upstream has nothing landed for it." Upstream landed a per-task base (6a7b391, --base-branch, base_branch= in meta) and the fork's per-task --base was correctly dropped for it. But the fork's project-level development-branch store (bin/fm-project-base.sh, .firstmate-base, registry base=) was re-attached inside upstream's conflict hunks: freshen_spawn_worktree_base (bin/fm-spawn.sh:3457-3487), bin/fm-merge-local.sh:110, bin/fm-teardown.sh:1938, bin/fm-project-mode.sh --base. Two behaviours now differ from upstream. (1) A spawn with no --base-branch on a project that declares a base resets to origin/&lt;declared&gt; instead of origin's default, yet records no base_branch=, so upstream's readers (bin/fm-review-diff.sh:79, the teardown content check at bin/fm-teardown.sh:1181, the worker's PR-target wording in fm_dod_block) still use the default branch for a task that started elsewhere. (2) In local-only mode the worktree resets to the local declared branch with no fetch and fm-merge-local.sh lands on it, while upstream's fm_base_branch_valid refuses any named base for local-only. fa41dbf's message lists this as deliberately kept. Decision needed: is the project-level store 'nothing landed upstream' (keep), or is it superseded by upstream's named base branch (remove the store and its three call sites)?
  • ℹ️ bin/fm-session-start.sh:196 - The one conflict hunk in this file (header comment) keeps the fork's wording for the --_child hand-off and the INHERITED ENVIRONMENT report, merged with upstream's new 'ANY nonzero child exit' sentence. The code it describes is fork-only on lines upstream did not touch since the merge base, so the comment matches the code; taking upstream's text verbatim would have described a mechanism the file no longer uses. Noted only as a literal exception to 'upstream wins every conflict'; it is the cause of the test break reported separately.
  • ℹ️ bin/fm-teardown.sh:2367 - Checked and clean: the merge's second parent is upstream 5838f10 with all 214 upstream commits reachable, and no upstream file is absent from the final tree. Replaying the merge gives 42 conflicted files; the features fa41dbf says it dropped are byte-identical to upstream (fm-classify-lib.sh, fm-contributions.sh, fm-dispatch-resolve.sh, fm-backlog-transition-lib.sh, fm-secondmate-report.sh and their tests). No function removed by upstream or dropped from the fork is still referenced in bin/ or tests/. The fork's durable pool lease sits in a spawn region upstream did not change since the merge base, and the fork's two-record slot tie-break was dropped for upstream's equivalent at this line. Only four auto-merged files were hand-edited afterwards (CI workflow and three tests, all removing coverage of dropped features). One capability is lost as a consequence of the rule: the fork's per-task base worked for local-only projects, upstream's --base-branch refuses them.

🔧 Fix applied.
3 issues (2 warnings, 1 info) still open:

  • ⚠️ bin/fm-brief.sh:308 - The fix round's project-base default skips only local-only, but upstream's fm_base_branch_valid (bin/fm-dod-lib.sh:175) also refuses a named base on any forge other than none. Sequence: a project registered forge=gerrit that declares a development branch (.firstmate-base or registry base=). fm-brief.sh &lt;id&gt; &lt;proj&gt; --mode no-mistakes --forge gerrit with no --base-branch now sets BASE_BRANCH from the store and exits with fm-brief.sh --base-branch: a base branch is not supported on forge=gerrit, a flag nobody passed, and there is no opt-out. A scout on the same project scaffolds (brief FORGE is always none for scouts) but fm-spawn.sh then refuses at bin/fm-spawn.sh:3143 using the registry forge, and removing the brief's Base branch line does not help because spawn re-applies the store. Before this round such a project scaffolded and spawned (the store was applied in the worktree refresh without validation). Decision needed: should the store be skipped wherever upstream refuses a named base (gerrit as well as local-only, so the task starts from the default branch), or is the hard refusal intended for a Gerrit project that declares a base?
  • ⚠️ bin/fm-spawn.sh:3131 - The instruction for this round was "For local-only tasks do not apply the store at all", but the gate tests the task's $MODE, and a scout has no mode (--mode is refused for scouts in both fm-brief.sh and fm-spawn.sh). So on a project registered local-only that declares a base, a scout still gets the store: the brief records Base branch: develop and the meta records base_branch=develop. Two consequences. (1) fm-promote.sh &lt;id&gt; --mode local-only reads that meta field and refuses at bin/fm-promote.sh:202 (a base branch cannot ship mode=local-only), so a scout on such a project can no longer be promoted to the project's own delivery mode; before this round the scout recorded no base_branch= and promotion worked. (2) If the project clone has no origin, the scout spawn itself now fails in freshen_spawn_worktree_base (has no origin, so it cannot start from base branch), where it previously returned early. The same condition is duplicated at bin/fm-brief.sh:308. Decision needed: should the default also be skipped for scouts when the project's registered posture is local-only (gating on the registry posture rather than the task mode), or should scouts on local-only projects keep the declared base and lose local-only promotion?
  • ℹ️ bin/fm-spawn.sh:3144 - Consequence of routing the store through upstream's path, noted for awareness: a brief scaffolded before this change (no Base branch: line) for a project that declares a base is now refused at spawn with must record Base branch: &lt;declared&gt; ... to spawn with --base-branch &lt;declared&gt;, and must be re-scaffolded. The same refusal fires if the resolved declaration changes between scaffold and spawn. It fails closed and the message names the working remedy, though it refers to a --base-branch flag the operator did not pass.

🔧 Fix applied.
2 issues (1 warning, 1 info) still open:

  • ⚠️ docs/architecture.md:390 - Stale sentence left behind by the first fix round. It still says: "For a local-only project it is equally where work lands: bin/fm-merge-local.sh fast-forwards the resolved branch and bin/fm-teardown.sh measures merged work against that same branch". That round restored both scripts to upstream (bin/fm-merge-local.sh is now byte-identical to upstream and lands on the default branch; bin/fm-teardown.sh's local-only check uses default_branch), and docs/scripts.md:88 was updated to say "local default branch". The sentence also contradicts the line directly above it, which says a local-only task ignores the declaration. An agent reading this would expect a local-only project that declares develop to land on develop, when it lands on the default branch. Fix: delete the sentence (or restate it as: local-only work lands on the repository default branch regardless of the declaration).
  • ℹ️ bin/fm-project-base.sh:58 - Checked by reading the code, no tests run. (1) Re-read nudge: bin/fm-config-push.sh, bin/fm-secondmate-restart.sh, bin/fm-secondmate-nudge-lib.sh, bin/fm-merge-local.sh, tests/fm-secondmate-restart.test.sh and tests/fm-remote-secondmate-lifecycle-e2e.test.sh are now byte-identical to upstream 5838f10; the remaining bin/fm-bootstrap.sh difference is only the fork's pool-lease flag check. (2) Project-base default: --task-default is the one gate both bin/fm-brief.sh:309 and bin/fm-spawn.sh:3132 call. Traced: local-only ship (mode passed, refused by fm_base_branch_valid, skipped); scout on a local-only project (empty mode falls back to the registered posture, skipped in both brief and spawn, so they agree and no base_branch= is recorded); Gerrit-registered project (registry forge, skipped for ship and scout); clone with no origin (skipped before validation); malformed registry forge (exit 3, skipped). An explicit --base-branch never enters the gate, so upstream's refusals are unchanged. The three new tests drive a real spawn and assert the worktree HEAD and the persisted meta, and would have failed against the previous round's code. The extra [ &#34;$FORGE&#34; = none ] in bin/fm-brief.sh:308 only covers a brief whose --forge gerrit disagrees with the registry, which spawn refuses anyway.

🔧 Fix applied.
1 info still open:

  • ℹ️ bin/fm-spawn.sh:3131 - No new defects found; noted for awareness, checked by reading code only (no tests run). (1) Intent conformance: upstream tip 5838f10 is an ancestor of HEAD, no upstream file is deleted, no conflict markers remain, and every changed shell script passes bash -n. Rebuilding a mechanical upstream-wins merge in a temporary object store and diffing it against HEAD shows the manual reconciliation only moves further toward upstream (for example bin/fm-classify-lib.sh, bin/fm-dispatch-resolve.sh, bin/fm-contributions.sh, bin/fm-merge-local.sh and the secondmate nudge scripts are now byte-identical to upstream). (2) Where fork code replaces upstream lines (pool lease in bin/fm-spawn.sh, collect_local_firstmate_states moved to bin/fm-wake-lib.sh, fm-send.sh exit 4, the --_child hand-off in bin/fm-session-start.sh, the merge-poll stale bound in bin/fm-watch.sh), upstream changed none of those regions since the merge base, so they are fork work surviving where upstream landed nothing. (3) Fix-round code: the --task-default gate in bin/fm-project-base.sh:58 is the single gate both bin/fm-brief.sh:309 and bin/fm-spawn.sh:3132 call; traced a scout (empty mode falls back to registered posture), a local-only ship, a Gerrit-registered project, a clone with no origin and an unregistered project, and brief and spawn agree in each. A declared branch missing on origin still refuses at spawn, as the pre-merge fork did. fm-captain-hold.sh receipts were adapted to upstream's three-field result (&lt;id&gt; pruned-resolution pruned), which the completion gate parses correctly. (4) The last round's doc correction is in place and no other maintained prose repeats the stale landing claim. No post-merge test addition asserts on implementation source text.
⚠️ **Test** - 1 info
  • ℹ️ tests/fm-remote-secondmate-lifecycle-e2e.test.sh:974 - This test fails on this host at 'remote spawn never reached its blocked inheritance write' (a 30-second bounded wait), run in parallel and alone. The identical case fails on snapshots of pristine upstream 5838f10 and of the fork's current main cdc2e57, and the file is byte-identical to upstream, so the merge did not introduce it. The correlated re-read assertions later in the file were not reached locally; remote CI must cover them.
  • Live validation: ✅ go - 8 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Merging the branch gives the fork every upstream commit: all 214 commits of kunchenguid/firstmate main are reachable from the branch head, on top of the fork's current main ✅ pass live git ls-remote of both remotes plus git rev-list counts: upstream main 5838f10 is the merge's second parent, 0 upstream commits missing from HEAD, 214 not in base, 56 fork-only, base cdc2e57 equals for…
Adversarial: no unresolved conflict markers are left anywhere in the merged tree ✅ pass live git grep for conflict marker lines at HEAD returned nothing
Upstream wins the re-read nudge conflict: the nudge scripts, fm-merge-local and their tests are exactly upstream's, and the secondmate restart flow works ✅ pass live git diff --quiet 5838f10 HEAD on six files (all identical); fm-secondmate-restart.log exit=0
A task spawned with no --base-branch on a project that declares a development branch starts on that branch and records base_branch ✅ pass live fm-spawn-base-branch.log (cold slot starts on develop and records it; scout also starts from the recorded base; brief records the declared base); project-base-default-live.txt
Adversarial: the declared base is silently skipped where upstream refuses a named base — local-only task, scout on a local-only project, Gerrit project, clone with no origin ✅ pass live fm-spawn-base-branch.log cases 7-10 (real spawn, asserts worktree HEAD and meta); project-base-default-live.txt shows empty output and exit 0 for each skip case
A project with no declared base, or an unregistered project, still spawns from the default branch as upstream does; a malformed or missing declared branch is rejected ✅ pass live fm-spawn-base-branch.log cases 3, 5, 6, 17; fm-spawn-pool-base-freshen.log exit=0
Brief scaffolding and task delivery (including local-only landing and Gerrit) work after the merge ✅ pass live fm-brief.log exit=0; fm-task-delivery.log exit=0
Turn-end guard works with the merged settings: all nine tracked Claude hook entries are inert under grok and live under Claude ✅ pass live fm-turnend-guard.log exit=0, line 'tracked .claude/settings.json entries: 9 inert under grok, the documented subagent exception still armed, all live under Claude'
Upstream's correlated re-read reaches a remote secondmate and its reply is awaited (remote secondmate lifecycle e2e) ⏸️ untested no The test's earlier concurrent-spawn case times out on this WSL2 host on upstream and on the fork's main alike, so the script exits before the re-read assertions. This host cannot run that case; remote…
  • git rev-list --count 5838f10 ^HEAD (0), git rev-list --count 5838f10 ^cdc2e57 (214), git rev-list --count cdc2e57 ^5838f10 (56), git merge-base --is-ancestor cdc2e57 HEAD
  • git ls-remote https://github.com/kunchenguid/firstmate main (5838f10, equals the merge's second parent) and git ls-remote https://github.com/talktejas/firstmate main (cdc2e57, equals the base)
  • git grep -nE &#39;^( | ) &#39; HEAD (no conflict markers)
  • git diff --quiet 5838f10 HEAD -- &lt;file&gt; for bin/fm-config-push.sh, bin/fm-secondmate-restart.sh, bin/fm-secondmate-nudge-lib.sh, bin/fm-merge-local.sh, tests/fm-secondmate-restart.test.sh, tests/fm-remote-secondmate-lifecycle-e2e.test.sh (all identical to upstream)
  • bash tests/fm-spawn-base-branch.test.sh (pass, 18 cases)
  • bash tests/fm-brief.test.sh (pass)
  • bash tests/fm-turnend-guard.test.sh (pass, 9 grok-guarded entries)
  • bash tests/fm-secondmate-restart.test.sh (pass)
  • bash tests/fm-spawn-pool-base-freshen.test.sh (pass)
  • bash tests/fm-task-delivery.test.sh (pass)
  • bash tests/fm-remote-secondmate-lifecycle-e2e.test.sh (fails at 'remote spawn never reached its blocked inheritance write', in parallel and alone; 16 cases pass before it)
  • Same e2e test on git archive snapshots of upstream 5838f10 and fork base cdc2e57 in /tmp (same case fails on both)
  • Manual: bin/fm-project-base.sh [--task-default &lt;mode&gt;] &lt;clone&gt; &lt;project&gt; and bin/fm-project-mode.sh against a temp home with no-mistakes, local-only, gerrit and no-origin projects
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

RooseveltAdvisors and others added 30 commits September 18, 2026 11:48
…id#4854)

Captain holds have no due semantics and are a hold kind, not a Beads issue
type. The create path now waives due.required and maps to native type task.

Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(bin): launch every spawned agent with the compact adviser disabled

Every crewmate, scout, and secondmate Firstmate launches now starts with
COMPACT_ADVISER_DISABLE=1, on a fresh spawn and on a relaunch alike, so an
unattended session never activates the compact adviser.
The value is unconditional: no configuration file gates it and there is no
override, unlike the trace carrier beside it.

Three carriers deliver it, because no single one covers every launch shape.
The pane shell receives an export beside GOTMPDIR, so the agent's own children
inherit it too.
The launch command carries an explicit assignment, prepended outermost so it
wins over any ambient value the pane already held.
The cleared launch environment sets it again at the `env -i` boundary and keeps
COMPACT_ADVISER_DISABLE in the fixed operational floor, which is what preserves
the switch when config/launch-env-allowlist empties the environment, and what
delivers it on a remote host that never had the value.

bin/fm-control.sh relaunch, the bootstrap secondmate relaunch, and the remote
secondmate transport all rebuild their launch through bin/fm-spawn.sh, so they
inherit the same floor.
The captain's own primary session is untouched.

The two new suites drive the real spawn and then execute the launch command the
pane actually received, with the harness replaced by a probe that prints its own
environment, rather than matching script text.
They cover ship and secondmate launches with the allowlist absent and enabled,
the pane export and its ordering, fm-control.sh relaunch, and the full parent to
remote-host chain.

* no-mistakes(review): Export compact-adviser disable across compound launches

* no-mistakes(document): Document spawned-agent compact-adviser environment guarantee
…henguid#4894)

* fix(bin): let a background Claude session keep owning its session lock

Session-lock ownership was decided by process ancestry alone. Under an
unattended Claude session the model loop runs in a transient bg-spare
bridged to the front-end by a shared daemon; when that bridge is
recycled the contiguous claude-named ancestry from a hook to the
recorded owner breaks while the owner pid stays alive, so the Stop
auto-arm stood down as a foreign live owner, the turn-end guard ended
every turn with its read-only diagnostic, and fm-lock.sh refused - a
self-sustaining outage until restart.

Ownership is now ancestry membership OR a trusted same-session id,
never id-first:

- fm-session-lock-lib.sh accepts CLAUDE_CODE_SESSION_ID only when
  CLAUDE_PID is a Claude-shaped member of the current contiguous run,
  compares it against the id recorded in state/.lock-session, and
  requires the recorded pid to still be a live harness. No id, no
  sidecar, an untrusted id, a different id, or a dead recorded pid
  leaves the ancestry verdict unchanged. Ids are never read from ps
  argv.
- fm-lock.sh accepts a same-session holder at both refusal sites,
  writes, refreshes, and clears the sidecar only under its claim lock
  (including the early already-mine exit, skipped only while the
  deferred startup sweep leases that lock), keeps it byte-identical
  across a same-session confirmation, records CLAUDE_PID on lock line 1
  for a session with a trusted id so a shared daemon or front-end that
  outlives the session never keeps a dead session's lock alive, never
  rewrites a live line 1 on a same-session confirmation, and names the
  recorded id in the live-owner refusal.
- The .lock line-1 format is unchanged, so every reader that takes the
  whole first line as the pid keeps working; the guard's foreign-owner
  exit is unchanged and inherits the fix through the shared predicate.

Tests: the ancestry suite drives the ancestry and id signals apart in a
deterministic process table (asserting the divergence) and runs a real
orphaned front-end/daemon/pty-host/spare tree through six phases with
the real lock, auto-arm, and guard scripts; the foreign-owner repro
keeps its negative control and adds a same-id positive control.

Disclosure: no live unattended Claude background session ran on the
verifying machine. The topology is documented by the real process
listings in kunchenguid#3902, kunchenguid#2314, kunchenguid#3398, and kunchenguid#4066; coverage is the structural
predicate plus the executable fixtures, not a live pass.

Residual: bin/fm-sessionstart-nudge.sh keeps its own private ancestry
walk (it only decides whether to print a nudge) and may nudge on a
resume in the recycled case.

Out of scope, deliberately: no structured lock format, no guard budget
changes, no daemon-identity rejection, no fork lineage.

* no-mistakes(review): Wait for claim lock; revert failed sidecars

* no-mistakes(review): Revalidate ownership after wait; restore sidecars

* no-mistakes(review): Roll back sidecar by publication phase

* no-mistakes(review): Restore sidecar only if lock line is unchanged

* no-mistakes(review): Trust session ids without a spelling allowlist

* no-mistakes(review): Disarm sidecar rollback before backup cleanup

* no-mistakes(document): Updated session-lock ownership documentation
* feat: park main under the away posture on Pi

While the away-posture record exists on a Pi primary, the supervision branch
takes every actionable wake, no processing turn opens on main, captain rows
accumulate for the return brief, and main's standing authority relocates to
the branch through the existing guarded scripts.

- lib/fm-branch-dispatch.ts: read the record at every routing decision; while
  it exists claim check, decision-owned, and heartbeat rows too, keeping the
  two broken-queue vetoes; expose checkSeqs so a claimed check row lifts task
  scoping.
- fm-primary-pi-watch.ts: offer every actionable row under the record; a
  declined wake and every watcher-failure alarm still reach main.
- fm-branch-supervision.ts: drop the legacy .afk decline; append a fixed
  POSTURE: AWAY tail carrying the record's read-back verbatim per wake; open no
  processing request while the record exists, re-checked immediately before a
  request would open and at every run boundary; present the accumulated rows
  at the first run boundary after archive.
- fm-lease-lib.sh: fm_lease_forbid_branch passes the branch for opted-in
  actions only while fm-afk-contract.sh validate succeeds on a confirmed live
  record; PR merge, fresh spawn, and decision answer opt in, local landing
  never does.
- fm-send.sh: a --resolve-key naming an open needs-decision or captain-held
  task is a decision answer and meets the partition; blocked: keys stay
  steering.
- fm-spawn.sh: enforce the record's spend cap for a fresh ordinary spawn by
  either actor; relaunches and secondmates exempt.
- fm-branch-prompt.sh: fixed Postures section and the verbatim
  ask-user-authority policy; the prefix stays byte-stable.
- fm-afk-return.sh: count what the away session handled from the store.
- docs, afk skill, AGENTS.md stub: main parked on Pi, green merge gate
  absolute while away.
- tests: watcher and branch extension suites, fleet-record, merge, and
  decision-answer suites cover the relocation, the vetoes, the tail, the
  parked processing turn, the cancellation, the re-presentation, and the
  spend cap; dated live-guard evidence recorded.

* no-mistakes(review): Refuse branch merge after preflight archive race

* no-mistakes(review): Fix away wake, spawn, and processing races

* no-mistakes(review): Suppress parked processing; narrow away-only rejection

* no-mistakes(review): Abort dedicated processing; gate branch spawn once

* no-mistakes(review): Stamp away-only on the dispatch offer

* no-mistakes(review): Treat invalid away records as spend-cap absence

* no-mistakes(review): Drop spawn test hook; abort processing-opened runs

* no-mistakes(review): Bind abort to opening prompt; cap-read absence

* no-mistakes(review): Limit away branch spawn to queued work only

* no-mistakes(document): Correct AFK posture documentation
* ci: simplify CI job timeouts to a three-tier policy

Replace the scattered per-job timeout values (10m parallel, 25m lint, 30m
serial, 10m macOS) with three readable tiers, each a hang tripwire with
headroom rather than a packing estimate:

- fast (5m): coverage guard, repo invariants, timing aggregate
- normal (30m, one shared budget): lint partitions, portable parallel
  shards, portable serial shards, macOS stock Bash
- heavy (Herdr only): 20m step tripwire on the family run so always()
  cleanup still runs, under a 75m job-level last-resort backstop

The workflow's header comment states the policy and points at
docs/fm-test-portable-shards.md "Timeouts", which now owns it, and each
job names its tier beside timeout-minutes. tests/fm-ci-workflow.test.sh
asserts the policy against the parsed workflow instead of the old
per-job minute values: every job joins exactly one tier, exactly three
distinct job-level values exist, the fast tier stays within 5-10
minutes, the normal budget stays at least double the modeled parallel
lane sum reported by fm-test-run.sh --check-coverage, and the Herdr step
tripwire stays below its job backstop with an always() cleanup after it.

Concurrency supersession, shard counts, lane membership, and fail-fast
settings are unchanged.

* no-mistakes(review): Decouple the normal timeout from packing estimates

* no-mistakes(review): Assert Herdr teardown follows the family run

* no-mistakes(review): Pin Herdr family-run timeout to 20 minutes

* no-mistakes(review): Ignore comments when identifying Herdr steps

* no-mistakes(review): Identify Herdr steps by declarative ids

* no-mistakes(document): Clarify authoritative three-tier timeout policy
…nchenguid#4895)

* fix(bin): keep supervisor status closes from waking the same home

A drain that already folded OPEN DECISIONS has presented those bytes even
when the watcher has no matching seen marker. Treat that fold, and the
presentation cursor, as known so the bookkeeping close stays quiet while
later worker lines still signal.

* no-mistakes(review): Keep folded worker failures waking past supervisor closes

* no-mistakes(review): Wake on unlisted folded worker lines; batch multi-key closes

* no-mistakes(review): Stop folded worker resolved lines from counting as already read

* no-mistakes(document): Correct self-announced close marker contract in docs
* Stop steering operators away from Herdr

* no-mistakes(review): Neutralize remaining Herdr opt-out documentation wording
…enguid#4973)

* fix(bin): treat a live no-mistakes run as current after rebase

A running run on the task's branch is authoritative regardless of head.
Matching only the local head made a rebased in-flight run look failed.

* no-mistakes(review): restrict coarse live-any-head to foreign-branch answers

* no-mistakes(review): reject gate-parked runs from the executing predicate

* no-mistakes(review): hoist gate-marker patterns into single run-lib owner

* no-mistakes(review): require live daemon for head-free run binding

* no-mistakes(review): require answered daemon-down before unbinding live runs

* no-mistakes(review): extend daemon guard to anchored continuation routes

* no-mistakes(review): delete live-any-head; restore dead-daemon verdict

* no-mistakes(review): keep parked gates parked; name dead daemon everywhere

* no-mistakes(review): set dead-daemon verdict instead of emitting early

* no-mistakes(review): align selected route with legacy dead-daemon handling

* no-mistakes(review): drop unproven-record binds; narrow coarse gate reading

* no-mistakes(review): narrow header, drop vestigial guard, retarget tests

* no-mistakes(review): revert coarse gate override; require answered-down probe

* no-mistakes(review): cache one daemon probe; stop duplicating run id

* no-mistakes(review): restrict coarse dead-daemon verdict to moved-off rows

* no-mistakes(review): delete coarse dead-daemon extension and gate note

* no-mistakes(review): delete remaining coarse dead-daemon block and stale docs

* no-mistakes(document): document rebase-safe live-run bind and unverified-record verdict
…4994)

* fix(bin): stage the launch command in a private file and type a short source line

A long launch line typed while the fresh pane shell is still busy waits in the
terminal's canonical line buffer, which drops input past about 1,024 bytes on
macOS, so the pane was left at an unfinished command with no agent running.
fm-spawn now writes the assembled command to the task's own temp root under
umask 077 and types only a short line that sources it.

Refs kunchenguid#4559

* fix(bin): keep the per-task temp root private before staging the launch command

The root lives at a predictable path under /tmp and now holds the whole launch
command. Create it with mode 0700, refuse one that already exists as anything but
a directory owned by this user that nobody else can write, and tighten an owned
one, so no other local user can plant or swap the staged file.

Refs kunchenguid#4559

* fix(bin): enforce private staged launch file mode

* test(spawn): cover long staged Claude launches

* no-mistakes(review): Namespace launch files and prove truncation staging

* no-mistakes(review): Use immutable per-spawn launch filenames

* no-mistakes(document): Document staged launch delivery safeguards

* no-mistakes(ci): Updated eight behavior tests/fakes to execute or inspect immutable staged launch files instead of expecting inline launch commands. This restores Muse, secondmate lifecycle/restart, remote trace/parent binding, compact-adviser, and Orca coverage. All affected tests, dispatch-profile regression, fixture tests, syntax checks, ShellCheck, and git diff checks pass

---------

Co-authored-by: Vytautas Stankus <svycka@gmail.com>
* Add isolated Herdr runbook to test instructions

* no-mistakes(review): Drop substring matching from test.instructions contract

* no-mistakes(review): Assert commands.test key absence in YAML

* Drop unit-first sentence and instructions contract test

Captain-scoped follow-up on the Herdr-lab test.instructions ship:
keep the lab safety runbook only, and leave the no-mistakes contract
test focused on commands.test absence.
…uid#4873) (kunchenguid#5001)

* docs(vision): accept vendor-semantics and 9k contract-ceiling amendments (kunchenguid#4873)

Replace the pixels-of-today's-UI rule with a quarantined, version-pinned
surface-adapter exception recorded as standing debt. Cap the always-loaded
contract at 9,000 words and require prune-or-trigger before a crossing change
lands.

Co-authored-by: Kun Chen <kunchenguid@users.noreply.github.com>

* docs(vision): restore accepted three-sentence vendor-semantics form (kunchenguid#4873)

Replace the compressed paraphrase with the issue's accepted wording:
a named quarantined version-pinned adapter, expected to break, recorded
as standing debt that never hardens into a shared contract.

Co-authored-by: Kun Chen <kunchenguid@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kun Chen <kunchenguid@users.noreply.github.com>
…or-owed gate (kunchenguid#4974)

* fix(watch): recheck a gate awaiting a human instead of wedge-escalating it

A lane whose validation run is parked at a gate waiting on a human
decision is correctly quiet, but nothing in its status line says so: the
evidence is the pipeline's own gate state rather than anything the worker
wrote. The wedge timer read that silence as a suspected wedge and climbed
the escalation ladder for as long as the wait lasted, and each escalation
cost a supervising turn. The landed declared-wait consult does not reach
it, because a live ordinary crewmate never reports a declared pause, and
raising FM_STALE_ESCALATE_SECS would delay genuine wedge detection for
every lane by the same amount.

The threshold now reads a second, independent record when the status line
accounts for nothing: whether the crew's current state is a gate whose
answer is owed by a human. That is minted only from the gate's own
findings table, by a row whose `action` column is exactly `ask-user`,
located by position out of the table header the way nm_gate_step_row
already reads its row - never searched for over the run payload, where a
finding's free-text description or a branch name satisfies a search just
as well. A gate awaiting the CREWMATE's own answer keeps the unchanged
escalation schedule, reason and demand-deep-inspection wording, because a
crewmate that goes quiet before answering its own gate is exactly the
wedge the ladder exists to catch.

Each kind of wait now carries the human it is on, the action that clears
it, and whether that human is the captain as data alongside the verdict,
rather than as wording chosen per branch where the recheck is written, so
the deferral cannot word one kind of wait as another and a new kind
cannot ship without deciding all of them. A parked gate has no written
record of when its wait began, so its recheck publishes no wait age at
all rather than one read from the quiet window this deferral resets on
every pass, which would report the same small number for a gate of any
age. Like every other captain-facing recheck here it is absorbed in
silence while the away-posture record exists, arming no throttle, so the
recheck is owed in full the moment the record is archived.

The consult runs only in the at-threshold branch that was about to
escalate, beside the worktree walk already there, and only for lanes
whose status line explained nothing.

Closes kunchenguid#3055

* no-mistakes(review): require an unanswered decision before deferring a parked gate

* no-mistakes(review): reset the away-silenced timer, fail-safe findings parse, US-joined wait records

* test(watch): pass the pane hash wedge_timer_check now takes

Upstream gave wedge_timer_check a sixth <pane-hash> argument for its
dead-record probe. The malformed-wait-record rounds drive the real function
directly, so they pass one, and stub fm_backend_agent_state to a live agent so
the probe that runs after a refused deferral keeps the unchanged ladder rather
than reading a backend the child shell has none of.

* no-mistakes(review): Bind parked-gate wait to its run, owe it firstmate

* no-mistakes(document): correct wait-kind count, crew-state reader scope, gate-key coupling

* feat(watch): make the parked-gate wait deferral opt-in

The wedge timer deferring a lane parked at a validation gate is new
supervision behaviour rather than a restored one, and it decides which
lanes give up the escalation ladder, so it now ships as a default-off
per-home option instead of changing every home on upgrade.

config/wedge-defer-parked-gate arms it. The flag is read before the
decision fold, so an unconfigured home spends no fold or current-state
read, writes no record, and keeps the unchanged escalation schedule,
reasons and demand-deep-inspection wording; a test counts the reader
calls in both directions to pin that.

It is not inherited by secondmate homes: each home supervises its own
crew and owns that trade separately, the same reason
config/turnend-churn-absorb is home-local.

The away-posture absorb returns to leaving the idle timer alone, which
it had restarted only because the costly consult could reach it. A
parked-gate wait is owed to the supervisor rather than the captain, so
it never enters that branch, and the recheck owed on return is again
owed in full the moment the record is archived.

* test(watch): pin that the away-silenced hold leaves the idle timer alone

The absorb no longer restarts the timer, so the recheck owed on return is
owed in full rather than a cadence into the return. Nothing asserted
that, so a restart could be reintroduced silently.

* no-mistakes(review): document away-silence rationale, pin captured gate component

* no-mistakes(test): anchor gate row scan to the braced findings header

* no-mistakes(document): pin same-block gate row invariant in crew-state comment
…uid#5007)

* fix(control): let the owning seat reclaim a task whose endpoint is gone

A destroyed pane or workspace made `missing` a terminal state. Relaunch
accepted only `dead` and said to stop the agent first; exit refused
`missing` and said to reconcile the task first; there is no reconcile
verb. Each command named the other as its prerequisite, so a task whose
terminal went away could not be reclaimed by anything, and a no-mistakes
approval it was parked on had no seat left to answer it.

`missing` is agent-free a fortiori: there is no endpoint, so there is no
agent in it. Widen the existing guards rather than add a verb.

- fm-spawn --relaunch accepts a positively proven `missing` and creates
  one fresh endpoint in the recorded worktree; the record it already
  republishes rebinds the task to it. A `dead` endpoint is still adopted
  in place.
- fm-control exit reports `endpoint-gone` instead of dying, so the
  relaunch transaction's stop step no longer dead-ends, and re-resolves
  the endpoint from the record before verifying the replacement.

The duplicate-agent refusal is untouched: both verdicts come from the
same recovery-grade classifier, which claims `missing` only from positive
absence, so `alive`, `ambiguous`, and `unreadable` all still refuse. The
backends' own create paths refuse a live same-labeled endpoint as a
second independent guard. The worktree, its branch, commits, uncommitted
changes, armed poll and registration, record rows, and status log are all
untouched - a reclaim is a recovery, never a teardown.

A secondmate is excluded: its gone-endpoint recovery already has one
owner in the session-start liveness sweep, so relaunch refuses and names
it rather than becoming a second path to the same outcome.

Tests reproduce both halves of the deadlock, the reclaim succeeding,
unlanded work surviving it, and the refusals that still hold.

* no-mistakes(review): prove endpoint absence per backend before reclaim rebinds

* no-mistakes(review): give exit and relaunch one absence proof; pin herdr rebind session

* no-mistakes(review): narrow endpoint reclaim to herdr; tmux refuses honestly

* no-mistakes(review): stop refusals and docs asserting unestablished causes

* no-mistakes(review): stop herdr fixture helper losing tmp-root registration

* no-mistakes(review): document workspace drift and absence-probe server residue

* no-mistakes(review): correct rebind limitation to its one reachable case

* no-mistakes(review): stop claiming reclaim leaves instructions untouched

* no-mistakes(document): scope fm-control-lib purity claim, note reclaim coverage

* no-mistakes(rebase): read the staged launch file in the herdr fixture

Rebasing onto main picked up kunchenguid#4994, which stages a long worker launch
command into a script and delivers the short `. '<path>'` line instead of
the literal command. The tmux fake and tests/fixtures.sh were updated for
that; the herdr fake this branch adds was written before it and still
keyed "an agent now exists on this pane" off the literal
`encode launch-brief` text, so after the rebase it never marked the
rebound pane live and the reclaim's alive-wait read `dead`.

Dereference the staged file first, exactly as the tmux fake above does.
Test-fixture only; no production path changes.

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

* no-mistakes(document): note reclaim placement in herdr and scripts inventories

---------

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

* test(status): reproduce missing event emission time

* wip(status): preserve optional event emission time

* test(status): document indirect clock stub invocation

* no-mistakes(review): Preserve historical status bytes during reply recovery

* no-mistakes(test): Fix timestamped status assertions and remote fixture dependencies

* no-mistakes(review): Preserve captain regex overrides for timestamped status events

* no-mistakes(document): Clarify status event timing and publication contracts

* no-mistakes(lint): Quote literal done to satisfy ShellCheck

* no-mistakes(ci): Captain, updated .github/workflows/ci.yml to expect 19 snapshot tests instead of 18, matching the PR’s added regression. Reproduced the failure before the fix. Stock Bash 3.2.57 verification passed: parse sweep, 19 snapshot tests, 53 Bearings tests, and the public-followup regression. Workflow lint and diff checks passed

* no-mistakes(test): Preserve terminal notifications with malformed timestamp tags

* no-mistakes(test): Stamp Rovo spawn failures with emission time

* no-mistakes(document): Verify status event documentation

* no-mistakes(lint): Fix ShellCheck quoting in status emission-time tests

* no-mistakes(ci): Captain, fixed four lifecycle assertions to accept emission timestamps while preserving publication and retry checks. Reproduced the CI failure before the fix. The lifecycle suite now passes with six Beads capability skips; syntax, targeted ShellCheck, and diff checks passed

* no-mistakes(ci): Captain, fixed malformed timestamp colons hiding actionable events using shared normalization. Original bytes and unknown ages are preserved. Regression reproduced before the fix; classifier and remote-reply suites, targeted lint, syntax, and diff checks passed

* no-mistakes(review): Stamp remote escalations at call sites, drop new flag

* no-mistakes(review): Accept stamped escalation and close lines in test assertions

* no-mistakes(review): Restore reserved-key answered-note guard for stamped closes

* test(status): accept optional emission time in PR-provenance assertions

The kunchenguid#4148 provenance test landed on main with exact unstamped greps.
Parent-channel lines from this branch carry [at=<epoch>], so strip only
that tag before the same exact match. No production change.

* no-mistakes(review): Accept stamped ready signal in PR fallback scrape

* no-mistakes(review): Drop relay flag, stamp parent events at call sites

* no-mistakes(review): Stamp worker terminal-signal instructions, revert fm-on fixture

* no-mistakes(review): Accept optional stamp in live cmux drift guard

* no-mistakes(review): Restore original test invocation order in two suites

* no-mistakes(review): Strip only well-formed numeric status time tags

* no-mistakes(document): Drop stale unstamped PR-ready line spelling from channel doc

* no-mistakes(review): Stamp agy spawn-failure status lines with event time

* fix(bin): normalize status event times in-shell and freeze the budget test clock

Two paths made a status event's emission time cost more than it should.

The captain-relevance fallback piped every line through awk to drop a
well-formed `[at=<epoch>]` tag before matching, so a supervisor sweep paid a
fork per line just to prepare a regex match. Shell parameter expansion does the
same strip with no fork, and the retry-dedup scan now reuses that one helper
instead of carrying a second copy of the rule in awk. The copies had already
drifted: the shell side stripped tags from lines with no colon, which the awk
rule left whole, so a colonless line could be mistaken for one already
recorded. One definition, checked against the awk rule it replaces over the
edge cases and a 4000-line fuzz.

tests/fm-contributions.test.sh froze its fixture clock only in exhaust mode. In
hang mode the poll set DEADLINE to the real now plus a one-second budget, and
when the second ticked before the first forge call the loop broke without ever
calling gh: forge/calls was never written and the assertion failed reading a
missing file. Freezing the clock in both modes removes the dependence on wall
time; the bounded call is still cut by the real timeout, so the observation the
test asserts still starts.

Emission time stays optional on new status records, and legacy or malformed
lines keep an unknown age.

* no-mistakes(review): Stamp ask-user escalation line and fix Kimi status assertion

* no-mistakes(document): Drop stale unstamped done-line spelling from watcher docs

* test: fold emission-time snapshot coverage into the fixture case

Drop the incidental ci.yml 18-to-19 count hunk so the PR no longer
touches workflows. Keep every emission-time assertion by folding it
into test_fixture_snapshot_json.

* no-mistakes(review): replace brief date substitution with epoch placeholder; drop emitted_at_epoch

* no-mistakes(review): align untimed normalizer with epoch parser; tolerate placeholder stamp in PR scrape

* no-mistakes(review): strip undelimited at-tags; correct brief stamp header

* no-mistakes(review): normalize stamps at both captain-regex sites; restore mtime freshness

* no-mistakes(review): strip colon-bearing stamps for relevance; fix headers and test oracles

* no-mistakes(review): narrow escalation match to stamp tolerance; pin note verb

* no-mistakes(review): read note and key past colon-bearing stamps

* test(status): keep inactive reconcile assertions stamp-tolerant

These two oracles were made stamp-tolerant while resolving one of the
branch's merges from main. The rebase drops merge commits, so that
adaptation was lost and both assertions went back to matching an exact
substring that a stamped line no longer contains: the tag lands before
the colon, so "failed [key=k]: ..." is now "failed [key=k] [at=N]: ...".
Strip a well-formed tag before matching, as the branch's other oracles do.

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

* no-mistakes(review): unstamp fold colon tests; reserve stamp width in cap

* no-mistakes(document): correct stale unstamped status-line spellings in docs

* no-mistakes(document): quote brief-test literals for lint; correct stamp-helper contract comments

* no-mistakes(ci): rename subshell-local epoch in delivery-race stub

The serialization test overrides fm_pending_reply_mark_delivered inside a
(..) subshell. Its `epoch` local collided with the same name in
status_line_at_epoch/status_stamp_line, which this branch added and this
suite now calls at top level, so ShellCheck 0.11.0 reported SC2030 and
failed Lint 2. The stub already prefixes its other locals with `pending_`
for the same reason; `epoch` was the leftover.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: ship clean Lavish host fixes

* no-mistakes(review): Fix Lavish classifications and fail-closed host loading

* no-mistakes(review): Restore Lavish host state across retries and launches

* no-mistakes(review): Preserve destination Lavish host when configuration is absent

* no-mistakes(document): Document Lavish status and host guarantees
…#5076)

* feat(afk): make the captain's away words the whole mandate

Retire the clause fields, verb list, never-set scan, refused records, and
the per-task merge-grant list from the away-posture record. The record is
now version 2: the captain's words verbatim plus expected return, spend
cap, and reach line; a version 1 record still validates, reads, and
archives so a live away window is never broken by the upgrade.

The supervision branch reads the words at the tail of every wake and acts
on them by its own judgment through the guarded scripts under standing
authority, never by analogy, holding for the return on doubt, and opens
each such outcome summary with "per your away instructions:" so the
return brief can render the words beside the session's account. While the
record exists any green merge runs under away authority (ledger tag
"away"); red merges, --allow-red, asynchronous and queued merges, and
local-only landing stay refused. The branch may file a backlog item the
words explicitly call for before dispatching it under the spend cap.

Tests drive fm-afk-contract.sh, fm-afk-launch.sh, fm-afk-return.sh, and
fm-pr-merge.sh as commands: version 2 written, version 1 read, retired
flags and subcommands refused by name, green merges landing under the
record, red and waived-red refused, the record lock still closing the
authority-read window, and the Pi away tail carrying the words.

* no-mistakes(review): carry the away read-back to the session verbatim

* no-mistakes(review): match the exact away-action marker in the return brief

* no-mistakes(review): refuse a words block truncated by a damaged line

* no-mistakes(document): Refresh away-role contract documentation
…unchenguid#5049)

* fix(bin): render the remote charter's steering-inbox path host-local

A freshly provisioned remote secondmate read a parent-home absolute
steering-inbox path in its charter - a location that exists on no route -
and spent its first turn discovering the gap and filing a blocked
decision for what was a render defect. The seed's remote-copy rewrite now
maps the inbox to the route's host-local parent-route inbox, exactly as
it already maps the reply-log path, so every mention - bare path, listing,
and handled/ acknowledgement - lands host-local.

Both rewrites also become plain assignments, because a quoted substitution
nested inside a double-quoted printf argument leaks literal quotes into
the replacement text on stock macOS bash. The lifecycle suite pins the
corrected render both directions against the real seed, provisioning,
and delivery route, sharing one fixture value between the render truth
and the delivery truth.

Closes kunchenguid#5012

* no-mistakes(document): document remote charter's host-local steering inbox
)

* feat(procevent): route worker-owned Lavish rounds

* no-mistakes(review): drop duplicate artifact field from task-owned registration

* no-mistakes(review): post worker reply once, fix ring label, keep re-arm atomic

* no-mistakes(review): keep worker board owned until terminal round acknowledged

* no-mistakes(review): refuse every retirement of an open worker-owned round

* no-mistakes(review): use real lavish reply flag, isolate reply generations

* no-mistakes(review): drop .posted marker for best-effort reply posting

* no-mistakes(review): consume staged reply after listener setup, refuse orphaned captures

* no-mistakes(review): require a reachable owner, redeliver open rounds, roll back failed re-arms

* no-mistakes(review): re-arm only to acknowledge an open round

* no-mistakes(review): conclude only a still-open terminal round

* no-mistakes(review): record the acknowledgement before retiring the board

* no-mistakes(review): retain the registration across a conclude, qualify terminal docs

* no-mistakes(document): Document worker-owned Lavish round lifecycle
…unchenguid#5107)

* fix(bin): reserve contribution observation budget

* no-mistakes(review): Strengthen slow-read regression test to exceed the poll budget
…ness JSON (kunchenguid#5103)

* feat(bin): add idempotent inbox orders, receipts, replies, and readiness

Let a caller supply a request id when publishing a captain inbox note so a
retry returns the original note instead of creating a second one, including
across the crash window between save and wake announcement. Separate saved
from announced so a failed wake is repairable without enqueueing again.
Add bounded receipts JSON with omission disclosure, a durable primary reply
against a note id, and a read-only readiness projection that can say
unknown instead of inferring liveness from a lock file.

* no-mistakes(review): fix(bin): honest inbox announce, reply cursor, and readiness verdict

* fix(bin): resolve ready from lock-holder ancestry; drop lock status --json

Remove the extra JSON surface from fm-lock.sh so its human status still
always exits zero. Have the readiness projection classify the inspected
home from the lock-holder pid via fm-harness.sh ancestry, with an explicit
FM_SUPERVISION_MODEL still winning and an unknown model when there is no
holder. Prove the yes path when that ancestry names a known harness.

* no-mistakes(review): Harden inbox announce, receipts reads, and reply sequence cursor

* no-mistakes(document): Note read-only lock inspection in scripts inventory

* no-mistakes(lint): Pass missing id argument to malformed-reply test printf

---------

Co-authored-by: cliflacata-svg <304148223+cliflacata-svg@users.noreply.github.com>
…ending text (kunchenguid#5118)

* fix(composer): stop a harness footer row from reading as a composer holding text

A harness draws its own furniture below the composer - a user statusLine, a
permission-mode hint - and the cursorless "bottom-most shape wins" rule looks
exactly there. `→` (U+2192) is Cursor's prompt glyph but ordinary text
everywhere else, so a statusLine opening with `→` was selected as a bare
composer, swallowed the hint row beneath it as wrapped input, and answered
`pending` on a visibly empty pane. `fm_task_inbox_ring` defers on exactly that
verdict, and `bin/fm-watch.sh`'s re-ring calls the same function, so the first
doorbell and every retry were skipped and the worker never saw the steer.

Measured live on 2026-09-20: three of five Claude Code 2.1.236 worker panes on
Herdr 0.8.0 had genuinely empty composers and every one of them was refused.

A separator pair that closed over a bare agent-glyph row is a proven composer
container, so the contiguous non-blank rows below its closing rule are that
composer's footer and are no longer composer candidates. The demotion is bounded
by all three of its own preconditions: a blank row ends the zone, a pair that
closed over no glyph row demotes nothing, and a shape with no separator pair at
all (Cursor's half-block rules) is untouched. Real unsubmitted text in that same
composer, including a stray SGR mouse report left by a click in the pane, still
reads `pending`.

Pinned by two portable regressions and by a new cursorless arm on the live
composer-matrix guard, which re-reads each harness's already-proven-idle pane
the way every non-tmux backend reads it and fails naming the harness and
version when that read is `pending`.

* no-mistakes(review): make composer footer-zone demotion shape-independent

* no-mistakes(review): make footer-zone demotion refuse-only and drop rescan

* no-mistakes(lint): quote probe-absent sentinel to clear ShellCheck SC2100

---------

Co-authored-by: Koen Muller <koen@catapult.nl>
…5115)

Co-authored-by: guanchengh-lgtm <271917158+guanchengh-lgtm@users.noreply.github.com>
… an unreadable runs table (kunchenguid#5114)

* fix(bin): stop misreading a no-run branch as an unreadable runs table

Defect: when `no-mistakes axi status`'s overview is truncated (a task's
own branch has zero rows among the shown ones), fm_nm_select_run's
Python fallback derived the repo identity for its direct SQLite query
from a `repo: <path>` line it expected in the overview text. The real
CLI never emits that line, truncated or not (see the genuine capture at
tests/captures/no-mistakes-v1.70.1/overview.toon, which has only
`count:`/`runs[...]:`), so the lookup always failed and reported
"unreadable runs table" for a task that simply has no run on its
branch. On a fleet with many concurrent runs, every idle-branch task
hits the truncated-overview path routinely, so this fired every few
minutes and drowned genuine unreadable/blocked verdicts in noise.

Fix: derive the repo identity from the task worktree path instead,
which is exactly the value `no-mistakes` records as a repo's
`working_path` (confirmed against the existing capped-overview test
fixtures, which already register repos by worktree path). A worktree
path that is not absolute cannot be matched and still reads as
unreadable rather than being guessed at. Also raise the reader's
SQLite busy timeout from 1s to 30s so ordinary lock contention on a
busy fleet cannot masquerade as an unreadable database.

Safety: every other verdict byte-for-byte unchanged - the repo lookup
still requires exactly one matching row (a genuinely corrupt or
mismatched repos table still reports unreadable, per the existing
`repo` failure-mode test), the branch query and row validation are
untouched, and a zero-row result for the branch still flows through
the same recursive re-parse that already turns an empty `runs[0]{...}`
table into `absent`. Added a regression test
(test_capped_overview_without_repo_line_and_no_runs_reports_absent)
that reproduces the real overview shape - capped, zero rows for the
task's branch, no `repo: ` line - and asserts the crew state falls
through to the pane/busy verdict instead of reporting unknown or
"unreadable". Full fm-crew-state.test.sh suite passes unchanged
otherwise.

* fix: recovered same-branch inventory awk misreads empty result as unreadable

fm_nm_select_run's deep SQLite reader rebuilds a `count:`/`runs[...]:`
overview and re-runs it through the same awk selection pass. When that
rebuilt inventory has zero rows for the branch, the row-matching loop never
executes, so its counters (`seen`) stay at awk's uninitialized empty string
while `expected` and `shown` are plain strings parsed from the header text.
Comparing an uninitialized value against a non-numeric string uses string
comparison, so "" != "0" is true, and the END block takes the "unreadable
runs table" branch instead of falling through to the correct "absent"
verdict for a branch with genuinely zero runs.

Coerce the affected END comparisons with `+0` so they are always numeric,
matching seen/expected/shown/total regardless of whether awk classified
them as strings or numeric strings. A truncated or genuinely malformed
inventory still differs numerically and still reports unreadable.

* no-mistakes(review): bound capped-overview inventory reader and canonicalize worktree lookup

* no-mistakes(review): match recorded repo path first, tolerate duplicate spellings

* no-mistakes(review): revert repo lookup to exact working_path match

* no-mistakes(document): note state-db inventory read under crew-state nm timeout
…ort (kunchenguid#5141)

* fix(bin): require a non-draft pull request before a PR-based done report

A PR-based ship could report done, and merge monitoring could be armed, while the pull request was still a draft. A draft cannot be merged, so the poll waited for an event that could not occur and nobody was asked to merge.

The PR-based definitions of done now require reading the pull request back from the forge and confirming it is not a draft, and a lane that deliberately holds a draft declares a wait instead of done.
bin/fm-pr-check.sh refuses to arm merge monitoring on a draft, naming the draft state, and treats an unreadable draft state as before.
The draft reading now lives in bin/fm-pr-lib.sh and bin/fm-pr-merge.sh uses it, with its refusal to merge a draft unchanged.

Closes kunchenguid#4757

* fix(review): Skip arm-time draft refusal when fm-pr-merge records metadata
* fix(bin): accept quota-axi schema 6 snapshots keyed by provider + accountKey

quota-axi 0.1.47 emits schemaVersion 6 once a provider expands to more
than one account: every provider row carries an accountKey and one
provider id may appear on several rows. fm_quota_json_valid accepted
only schema 5 with unique provider ids, so fm-dispatch-resolve.sh,
fm-quota-choose.sh, and fm-procevent-quota.sh all rejected the live
snapshot and quota-informed dispatch was dead against the current tool.

- bin/fm-quota-axi-lib.sh: the validator accepts schema 6 with
  accountKey required on every row and uniqueness on
  provider + accountKey; schema 5 keeps its exact rules. FM_QUOTA_ROW_JQ
  is the one join every consumer uses: schema 5 binds by provider alone,
  schema 6 binds to the row keyed by the candidate's Pi lane, else the
  provider's default row, else no row (unmeasured, never blocked, never
  by position or summed across accounts).
- bin/fm-quota-choose.sh: accepts schema 6 JSON and the TOON accountKey
  column, and joins through the shared function.
- bin/fm-dispatch-resolve.sh and bin/fm-procevent-quota.sh: join through
  the shared function; an expanded provider with no row for the
  candidate's account is reported as such.
- tests: schema 6 fixtures shaped like the real snapshot, each paired
  with a schema 5 case on the same path; every new case fails on the
  previous scripts and passes now.
- docs: the two sentences naming the row join describe the schema 6 key.

* no-mistakes(review): Fix native Codex quota and expanded provider watches

* no-mistakes(review): Align native Codex account matching across dispatch paths

* no-mistakes(document): Align quota documentation with account-aware snapshots

* no-mistakes(document): Align quota dispatch documentation with account matching

* fix(bin): keep CI lint and the quota watch test portable

- bin/fm-quota-axi-lib.sh: FM_QUOTA_ROW_JQ is read only by the scripts
  that source this library, so full-mode ShellCheck reported SC2034 on
  the assignment; mark it alongside the existing SC2016 disable.
- tests/fm-procevent-quota.test.sh: the schema 6 provider-watch
  assertions used rg, which CI runners do not install, so the case
  failed with 'rg: command not found' rather than on behavior; use grep
  like the rest of the file.

* no-mistakes(document): Documented schema-version account-row compatibility
* test: repair Claude live auto-arm regression

* no-mistakes(review): Assert SessionStart digest completeness within its hook_response event

* no-mistakes(document): Consolidate Claude live verification references
Roll the shared require-no-mistakes action to the tagged v1.80.1 SHA and grant pull-requests: read so the check can read PR bodies.
…nchenguid#5174)

* fix: preserve Pi watcher ownership across session replacement

* no-mistakes(document): Scope Pi predecessor retention away from omp

* no-mistakes(ci): Diagnosed all three failing checks; only one was code-caused. (ci-3, genuine) Stock macOS Bash snapshot compatibility: `tests/fm-pi-watch-extension.test.sh` failed the macOS Bash 3.2 `bash -n` parse sweep with `line 4265: unexpected EOF while looking for matching '`. I built GNU Bash 3.2.0 from source locally and reproduced it. Root cause: the PR added a comment containing an apostrophe (`// Replacement shutdown deliberately retains module 2's established arm until`) inside a quoted here-document (`<<'EOF'`) nested inside a `$(...)` command substitution. Bash 3.2 has a parser bug (fixed in later bash) where an unmatched single quote inside such a here-doc body is treated as opening a shell quote and never closed, aborting the whole file parse. The base commit parses cleanly under Bash 3.2, confirming this PR introduced the break. Minimal fix: reworded the comment to remove the apostrophe (`... retains the established module-2 arm until`), preserving meaning. Verified `bin/fm-lint.sh --list-files` (the 6 changed shell files) now all pass `/tmp/bash-3.2/bash -n`; Bash 5 also parses. (ci-1, infrastructure) Behavior portable serial 8: GitHub API shows the `Run portable serial shard 8` step conclusion=success; only `Upload portable serial shard 8 timing artifact` failed with `Failed to FinalizeArtifact ... (403) Forbidden`. This is a transient artifact-service/cancellation failure, not a test or code failure. No change. (ci-2, infrastructure) Lint 1: fetched the job log via the GitHub API; it ends with `##[error]The runner has received a shutdown signal...` then exit 143. The step was cancelled mid-run, not a ShellCheck finding. Independently ran `bin/fm-lint.sh --partition 1of2 --telemetry ...` locally with pinned ShellCheck 0.11.0 and actionlint 1.7.12: exited rc=0 (no findings). No change. The only code change is the apostrophe removal in tests/fm-pi-watch-extension.test.sh; no other files modified
…d#5236)

* fix(bin): retire windowless leftovers and stop claiming a Pi daemon teardown

Catch-up correctly refuses while a leftover task record has no status file.
Cleanup used to deadlock on those same records when they also had no spawn_gen and no window, so they lingered and wedged every later away-mode return. Teardown now treats a windowless leftover as a missing-endpoint legacy record, and stop reports that no daemon terminal was running when none was launched.

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

* no-mistakes(review): Narrow windowless teardown exception to tmux legacy leftovers

* no-mistakes(review): Validate windowless leftover identity via shared endpoint validator

* no-mistakes(review): Refuse windowless leftovers carrying other backends' endpoint identity

* no-mistakes(document): Clarify windowless teardown retry documentation

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
RibatTRW and others added 29 commits October 2, 2026 07:23
…ommands (kunchenguid#5963)

* fix(composer): read a titled Claude top rule as the composer's edge

A named Claude Code session draws its title into the composer's top rule.
The strict separator predicate rejected that row, so the closing rule read
as a lower unmatched separator and an idle, empty composer classified
unknown on every cursorless backend, refusing fm-send, exit, and relaunch.

Spare a bare agent-glyph row sandwiched between a width-proven titled rule
and the screen's only unmatched separator directly below it. The strict
separator predicate, dead-shell rule, and blank-row posture are unchanged.

Fixes kunchenguid#5601
Fixes kunchenguid#5558

* no-mistakes(test): Keep Claude's grey slash command in Herdr payload proof

* no-mistakes(test): Make missing-herdr version check hermetic to installed herdr
…#6387)

* fix: pre-approve Pi trust for seeded secondmate homes

Unattended first launches of Firstmate-seeded Pi secondmate homes stalled on
"Trust project folder?" until Enter. Probe --approve like --tui-mode and pass
it only for --secondmate when help advertises it (.fm-secondmate-home signal),
leaving ordinary workers and older Pi unchanged.

* no-mistakes(document): Consolidate Pi seeded-home trust documentation ownership

* no-mistakes(ci): Fixed Lint 1’s unused polling counter. Diagnosed Behavior portable serial 4 as a pre-existing delta-reader test clock race; replaced timing-dependent rewrite and deletion with deterministic executable-boundary synchronization. ShellCheck, Bash syntax checks, and git diff --check passed. Delta-reader tests passed three consecutive runs; all three live Pi trust cases passed. Production behavior unchanged
…--external-sources (kunchenguid#6443)

* fix(lint): retry memory-bound roots without external sources

* no-mistakes(review): Make fallback tests portable and correct source-following telemetry

* no-mistakes(review): Remove committed parity fixtures and use disposable test roots

* no-mistakes(document): Document ShellCheck memory fallback and telemetry

* no-mistakes(review): Cover bounded and unbounded fallback RSS behavior

* no-mistakes(document): Correct stale lint fallback documentation

* no-mistakes(document): Correct stale lint test documentation

* no-mistakes(ci): The memory fallback (the retry without --external-sources) now gets only the time left in its root's original deadline, so it can no longer outlast the CI job. Invariant: one root's first attempt plus its fallback must fit inside a single FM_LINT_ROOT_SECONDS deadline, plus the cleanup grace. Only one site started a new deadline: the fallback call in fm_lint_run_root. The deadline is the only budget involved, because the memory limit already applies to each process separately. Changes in bin/fm-lint.sh: - fm_lint_exec_root now takes a <seconds> argument instead of always reading FM_LINT_INTERNAL_ROOT_SECS. - The first attempt passes the full deadline. - The fallback passes floor((start + deadline - now) / 1000) seconds. - When bounds are enforced and less than 1 second is left, no retry starts. fm_exec_timed rejects 0 seconds, so the retry cannot run with no time. The root keeps reason=memory, and the shard output says "no time left in its Ns deadline to retry without it". - Unbounded local runs have no deadline and behave as before. - The header comment now describes the shared deadline. Changes in tests/fm-lint.test.sh: a new test, test_memory_fallback_spends_only_the_remaining_root_deadline, runs only on hosts that can enforce bounds. It uses a 6 s deadline and 1 s grace. - Case 1: the first attempt runs 3 s and then fails with memory status 251. The test asserts one fallback ran, reported reason=timeout, and the root's recorded duration is under 7000 ms. - Case 2: the first attempt runs 5.2 s. The test asserts no fallback starts, the skip is explained, and the sidecar records memory with source-following 1. Verification: - Full `nice -n 10 bash tests/fm-lint.test.sh` passed, including the new test, in about 5 minutes. - Case 1 run against the HEAD script: the root took 9168 ms, so the under-7000 ms check fails before the fix. - `bin/fm-lint.sh bin/fm-lint.sh tests/fm-lint.test.sh` reported no findings. - The CI workflow is unchanged, so the Test step still runs only tests/fm-lint.test.sh with nice -n 10 and the 12 GiB ShellCheck limit
…tension log opt-in (kunchenguid#5489)

* Fix Pi watcher successor-gap confirmations and add extension log

Accept an already-acknowledged handling confirmation as a no-op when the
generation matches, confirm the restoration's own recovery token with a
superseded (not rejected) outcome on generation mismatch, retire an arm on
confirm failure only when the failed token names that exact pid, and record
restore attempts, readiness timeouts, and confirm results in the bounded
state/.watch-extension.log. Regression tests: already-acked no-op plus
mismatch/dead-pid/lock-mismatch rejections and the manual-restart churn
contract in fm-watch-arm.test.sh, and a mid-restore marker advance with no
rejection appendix in fm-pi-watch-extension.test.sh.

* Treat a dead arm child as an empty slot so repair and retry recover

startArm and scheduleRetry answered unchanged while holding a ChildProcess
whose OS process was already gone but whose close had not fired, so neither
the repair tool nor the retry timer started anything until that close fired.
Gate slot occupancy on a liveness check (exit/signal codes plus pid probe)
and start a fresh arm instead, with a regression test driving the repair
tool against a dead-but-unclosed child.

* no-mistakes(document): Document new Pi extension log knob

* no-mistakes(review): Fix confirm-failure retire token match, add distinct-pid test

* no-mistakes(document): Clarify retire guard needs pid and generation

* Make the Pi extension diagnostic log opt-in and default-off

Only a positive FM_WATCH_EXTENSION_LOG_KEEP_LINES enables
state/.watch-extension.log. Unset, empty, non-numeric, zero, and
negative values disable logging entirely, so the default run writes
nothing and never creates the file. The shared positiveInteger
fallback semantics stay untouched for the retry and timeout knobs.
docs/configuration.md owns the knob contract and
docs/watcher-continuity.md points at it. Tests: the superseded-delivery
case runs opted in, and a new case proves unset, zero, and non-numeric
values create no log file while delivery still succeeds.

* no-mistakes(document): Qualify extension-log coverage bullet as opt-in

* no-mistakes(ci): The two reported checks (CI run 36372002913, Require no-mistakes run 36372069208) show conclusion action_required with 0 jobs and no logs because this is a fork PR (RibatTRW/firstmate) and GitHub is holding the workflow runs awaiting maintainer approval; that gate is external to the code and needs a maintainer to approve the runs. While verifying the change locally I found a real defect the approved CI run would hit: the PR's new test test_handling_delivered_rejects_a_superseded_generation failed deterministically. Invariant violated: reopen-announced (a non-successor/manual arm start) mints a fresh recovery generation only when the durable wake queue holds unrecovered work; an announced episode with an empty queue must be left untouched so idle arm starts never churn generations (the [ -s queue ] guard from kunchenguid#4819, relied on by bin/fm-watch.sh:2413 and covered by the append-reopens and announcement-bound sibling tests). The test called reopen with an empty queue and expected churn, so the fix establishes the queued-work precondition (append one wake, re-announce, re-read the generation) before asserting the reopen mints and the old confirmation mismatches. Test-only change, 14 lines in tests/fm-watch-arm.test.sh. Verified: fm-watch-arm.test.sh 25/25 ok on two consecutive runs, fm-pi-watch-extension.test.sh 55/55 ok, and shellcheck reports only one pre-existing warning outside the edited region

* no-mistakes(document): Restore blank line in watcher-continuity docs

* Route superseded Pi deliveries like confirmed ones and cover the retire guard

A superseded handling confirmation now falls through to the normal delivery
path, so an accepting supervision branch owns the wake instead of main.
Scope the watcher-continuity token-pinned confirmation and narrowed retire
rule to Pi, since omp and OpenCode still confirm against the current
successor. Add tests that fail when the retire guard, the scheduled-retry
gate, or the deferred-close gate is reverted, relabel the churned-generation
characterization test, and use a reaped pid for the dead-pid rejection.
…es (kunchenguid#5863)

* fix(pi): silence unacknowledged processing retry replies

Suppress autonomous processing prose before persistence and during streaming while retaining tool calls, signed reasoning, and retryable outcomes. Restore ordinary output after acknowledgement or a user message.

Fixes kunchenguid#4954

* no-mistakes(review): Silence only processing retries, keep first presentation visible

* fix(pi): preserve differing processing retry replies
…henguid#6530)

Pi 1.0.1's createToolHtmlRenderer reads getToolRenderers and ignores
getToolDefinition. Calm /export still includes stock grep HTML; the
fixture has to pass the lookup key the installed Pi actually reads.
* fix(procevent-quota): tolerate consecutive slow quota-axi reads

The quota allowance poll treated any quota_json failure as terminal, so one
slow quota-axi --json (measured max ~29s under a 48s derived bound) shut the
watch down until someone re-armed it, and the detail always said
"missing/incompatible". Tolerate three consecutive failed or timed-out reads
before going terminal, reset the streak on any good read, and report a
timeout distinctly from a missing or incompatible tool. Each timed poll runs
exactly one bounded --version and one bounded --json: validate the captured
version text through fm_quota_axi_version_compatible rather than launching a
second probe, and describe a mixed failure streak by count plus last cause.

* no-mistakes(document): Document quota polling failure tolerance

* no-mistakes(ci): Fixed ci-2 and ci-3. Permanent quota read failures (rc 2 missing, rc 3 incompatible) now report on the first poll, while transient rc 1/4 failures retain the existing three-failure retry behavior. The missing-binary test now uses an isolated PATH without quota-axi and asserts both permanent failures stop at condition_polls: 1. Verification passed: tests/fm-procevent-quota.test.sh, canonical fast lint for both changed files, bash syntax checks, and git diff --check

* no-mistakes(review): Classify untimed quota version failures as transient

* no-mistakes(document): Clarify quota polling failure budget
…enguid#6505)

* fix(teardown): clarify scratch guidance and dirty worktree refusals

Keep ship proof material outside the task worktree and distinguish untracked-only leftovers from tracked edits without changing cleanup guards.

Fixes kunchenguid#6319

* fix(ci): Fixed ci-3 only. Both promotion outputs now replace the scout restriction and require external scratch storage and a clean worktree before done. Verification: 27 delivery tests passed, five mutations caught, restored test passed, pinned ShellCheck and syntax/whitespace checks passed. ci-1, ci-2, and ci-4 remain untouched
…nguid#6518)

* Escalate inbox instructions stuck behind a busy worker

Count consecutive busy-deferred due doorbells durably and escalate at the configured bound without typing into the worker pane.

Fixes kunchenguid#6445

* fix(review): Fix inbox escalation deduplication and busy streak resets

* fix(review): Preserve busy inbox escalations through daemon supervision

* fix(document): Correct busy-inbox escalation documentation

* fix(ci): Fixed SC2034 in tests/fm-task-inbox.test.sh by including the loop counter in the failure diagnostic. Source-aware lint, all 34 inbox tests, and git diff --check pass. Behavior portable serial 6 reproduces identically on base 1f3e769 and target 78156b8 with Pi 1.0.1: an unrelated renderer API change breaks the unchanged Calm test. No Calm changes made; that failure is addressed separately by kunchenguid#6516. Logs retained in scratchpad-ci/

* fix(ci): Fixed ci-2, ci-3, and ci-4: successor failures surface, reset alerts deduplicate, and oversized busy limits fall back to two. Passed 47 inbox tests, 7 focused daemon checks, all 13 mutation checks, lint, documentation checks, and diff checks. Evidence: scratchpad-ci-selected/summary.json. ci-1 remains unchanged and unwaived. Fresh live Herdr proof remains with the outer driver
* fix: prevent healthy remote job worker turnover

* no-mistakes(review): Serialize full LaunchAgent repair and verify launchd-tracked owners

* no-mistakes(review): Let launchd-tracked unpublished spawns start before reloading

* no-mistakes(document): Document remote worker heartbeat and serialized LaunchAgent recovery

* no-mistakes(ci): Full CI log showed the idle-worker regression exceeded its outdated command budget (82 versus 80) after independent heartbeat ownership checks were added. Raised the budget to 120 while retaining the separate busy-poll sleep limit. Remote-job and LaunchAgent executable tests passed, as did bash syntax validation and git diff --check. No production behavior changed

* no-mistakes(ci): Fixed missing readiness recovery under verified live ownership, preserving the serving PID and private file mode. Added executable regressions for deletion during a blocked sweep and stale readiness diagnostics without LaunchAgent reload. Deletion regression failed before the fix. Both remote-job test suites, bash syntax validation, and git diff --check passed

* no-mistakes(ci): Full CI log identified a flaky ownership-loss test racing an already-authorized heartbeat refresh. Replaced backdating and a fixed sleep with bounded observation of readiness expiry through the public probe. Production behavior unchanged. Remote-job and LaunchAgent executable suites passed; bash syntax validation and git diff --check passed

* fix: wait for launchd bootout cleanup

* no-mistakes(document): Document remote worker recovery and read-only turnover verification

* no-mistakes(review): Publish worker identity before lock owner records

* no-mistakes(document): Document worker identity publication safety invariant

* no-mistakes(ci): Fixed ci-1: replacement workers discard predecessor readiness before publishing identity and roll back identity if lock-owner recording fails. Added executable regressions reproducing both defects. LaunchAgent and orphan-reap suites, shell syntax checks, and git diff --check passed. No live service state was modified

* no-mistakes(ci): Restored lock-owner-before-identity publication and removed the identity rollback and reordering-only tests. Retained an executable regression proving predecessor readiness is rejected until replacement startup completes. LaunchAgent and orphan-reap suites, shell syntax checks, and git diff --check passed. Other changes remain intact; the retained-identity interrupted-repair edge remains out of scope. No live service state was modified
* fix(remote-job): reap expired seq claims with one directory walk

The hourly claim sweep forked uname+stat per .seq-claims entry and blocked
serving for ~85s at ~17k dirs. Delete expired empty claim dirs with a single
find -exec rmdir batch and cache the host uname for remaining mtime reads.

* no-mistakes(review): Restore original path mtime helper and drop uname cache

* no-mistakes(test): Restore claim retention eligibility; focused retention and serving tests pass

* no-mistakes(document): Document single-walk claim cleanup and regression entrypoints

* no-mistakes(ci): Fixed Lint 2’s reproduced SC1091 by adding the tests/lib.sh ShellCheck source directive to the retention test. Runtime behavior is unchanged. ShellCheck passed for both new claim tests; Bash syntax, the retention behavior test, and git diff --check passed

* fix(tests): keep fixture registries out of git worktree roots

A TMPDIR pointed at a repository root placed live .fm-test-* registries
beside tracked files, and a concurrent git add during the claim-walk CI
fix round committed three of them. Route registries and fixture roots
through a TMPDIR that refuses git worktree roots, remove the stray files,
and pin the escape with a behavioral cleanup test.

* no-mistakes(review): Preserve whole-second claim expiry in single-walk sweep

* no-mistakes(document): Correct temporary-directory resolution documentation

* no-mistakes(ci): Fixed ci-3 by changing only the stale, fresh, and read-only orphan fixture paths in tests/fm-test-fixture-cleanup.test.sh to use $FM_TEST_TMPDIR. All seven tests passed both normally and with TMPDIR set to the worktree root. Shell syntax and git diff --check passed
…t-in (kunchenguid#5354)

* feat(bin): record each task's no-mistakes pipeline spend at cleanup

no-mistakes keeps every pipeline agent invocation's token usage only in its
local agent_invocations records, and cold pipeline agents (review, test,
document) leave no session log, so a task's review-loop cost never reached
Firstmate's records and could not be attributed after cleanup.

bin/fm-pipeline-spend.sh attributes a task's runs by the repository
no-mistakes resolves for the task copy, the task branch, and the branch's
creation (a relaunch mints a new spawn_gen while the branch keeps
validating), then sums every invocation, failed and cancelled included.
Token fields use no-mistakes' per-round deltas so resumed review rounds are
not counted twice, and unrecorded values stay unknown rather than zero.
show prints the record read-only; record appends it once per task
incarnation to data/pipeline-spend.jsonl. Teardown records it for every
ship task it cleans up, before deleting the branch and the task record.

fm_nm_state_db becomes the one owner of where no-mistakes' state database
lives, shared with the capped run-inventory reader.

* no-mistakes(review): Drop spend show command and timeout override

* style(bin): rewrap fm-pipeline-spend header comment

* Make pipeline spend recording opt-in

* feat(bin): record each task's no-mistakes pipeline spend at cleanup

no-mistakes keeps every pipeline agent invocation's token usage only in its
local agent_invocations records, and cold pipeline agents (review, test,
document) leave no session log, so a task's review-loop cost never reached
Firstmate's records and could not be attributed after cleanup.

bin/fm-pipeline-spend.sh attributes a task's runs by the repository
no-mistakes resolves for the task copy, the task branch, and the branch's
creation (a relaunch mints a new spawn_gen while the branch keeps
validating), then sums every invocation, failed and cancelled included.
Token fields use no-mistakes' per-round deltas so resumed review rounds are
not counted twice, and unrecorded values stay unknown rather than zero.
show prints the record read-only; record appends it once per task
incarnation to data/pipeline-spend.jsonl. Teardown records it for every
ship task it cleans up, before deleting the branch and the task record.

fm_nm_state_db becomes the one owner of where no-mistakes' state database
lives, shared with the capped run-inventory reader.

* no-mistakes(review): Drop spend show command and timeout override

* style(bin): rewrap fm-pipeline-spend header comment

* Make pipeline spend recording opt-in

* no-mistakes(document): Note pipeline-spend opt-in gate in teardown comments

* no-mistakes(ci): I fixed all three Greptile findings following your decision. Both test suites pass: tests/fm-teardown.test.sh (105 ok, exit 0) and tests/fm-pipeline-spend.test.sh (8 ok). I didn't run either new test against the old code, so the claim that they fail before the fix is from reading the code, not a run. Nothing was committed or pushed. **ci-1 / ci-2 (bin/fm-teardown.sh):** The rule is that every owned ship task in a home that has opted in gets a durable spend record before its task record is deleted. The `[ -d "$WT" ]` check skipped the recorder when the worktree folder was already gone, so no record was written. I removed that check and kept the other conditions: the task must be a ship task, teardown must own its worktree, and `config/pipeline-spend` must exist. `bin/fm-pipeline-spend.sh` already writes an unavailable-source line when the worktree is missing. The new test `test_teardown_records_unavailable_spend_for_a_gone_worktree` in tests/fm-teardown.test.sh sets up an owned ship task whose worktree is missing, with recording turned on. It checks that teardown succeeds, writes a line with `source=="unavailable"`, `total==null` and a reason saying the task copy is gone, and still removes the task record. Under the old check no ledger would have been written. **ci-3 (bin/fm-nm-run-lib.sh):** When `NM_HOME` and `HOME` were both unset, `fm_nm_state_db` fell back to `${HOME:-}/.no-mistakes`, which resolves to `/.no-mistakes`. It now uses `root=~/.no-mistakes`. Bash expands `~` to the account's home directory even when `HOME` is unset, which matches the previous Python `Path.home()` lookup. The new test `test_state_db_without_nm_home_or_home_uses_the_account_home` in tests/fm-pipeline-spend.test.sh runs the function with both variables unset and compares the result to the home directory from the system's password database. The old code would have returned `/.no-mistakes/state.sqlite`. shellcheck reports nothing new on the changed files. I added one `disable=SC2016` comment in the new test, where the single-quoted `$1` is meant to expand in the child shell
* feat(bin): start tasks from a named base branch

Spawns always reset a task's pooled copy to origin's default branch,
so work that belongs on a feature, integration, or release branch
started from the wrong code and opened its PR against the default.

fm-brief.sh --base-branch records the base in the brief, fm-spawn.sh
resets the copy to origin/<base> and records base_branch= in task
meta, and the worker targets its PR at that branch. Review diffs,
the cleanup content check, and scout promotion read the recorded
base. local-only and Gerrit deliveries refuse a named base.

* no-mistakes(review): Anchor brief base-branch parse on scaffold Setup line

* no-mistakes(review): Require explicit --base-branch on spawn, matching brief lines

* no-mistakes(document): Note base-branch reset in fm-spawn freshness header

* no-mistakes(ci): I fixed all four Greptile findings the way you specified. The affected suites pass locally: fm-brief, fm-dod-lib, fm-spawn-pool-base-freshen, fm-review-diff, fm-teardown and fm-task-delivery. shellcheck on the changed files reports only info-level notes, no warnings or errors. I didn't revert the code to watch each new test fail before the fix. - **ci-1 (brief text blocked or redirected spawns):** In bin/fm-dod-lib.sh, `fm_brief_base_branches` now only accepts a `Base branch: <value>` line that comes directly after the base-variant Setup sentence fm-brief.sh writes. Any other `Base branch:` line is ignored. `--base-branch` is still the only authority, and the existing checks in fm-spawn.sh are unchanged. With the flag, the spawn is refused unless a matching pair exists and no pair names a different branch. Without the flag, it is refused only if such a pair exists. The header comment in fm-spawn.sh is updated to match. - The `brief_with_base` test helper now writes the real two-line pair. - The decoy refusal case now uses a full decoy pair in the captain's intent. - New test `test_prose_base_branch_line_is_ignored`: a brief with no base whose intent quotes `Base branch: release/1.2` spawns normally with no `base_branch=` recorded. A spawn with `--base-branch feature/hub` whose intent names release/1.2 starts from `origin/feature/hub` and records it. - **ci-2 (scout base not checked against the forge):** fm-spawn.sh now looks up the project's registered forge with `bin/fm-project-mode.sh --forge`, like the ship path does, before `fm_base_branch_valid` runs. Previously it passed `none`. New test `test_scout_base_branch_refused_on_gerrit_forge`: a scout with a base on a forge=gerrit project is refused at spawn and no meta file is written. - **ci-3 (base not shell-quoted in worker commands):** `fm_dod_block` now quotes the base with `printf %q` in the `--base` and `--base-branch` arguments, and the branch name in the prose stays readable. The test in fm-brief.test.sh uses the base `release/$HOTFIX` and checks that both direct-PR and no-mistakes briefs render `release/\$HOTFIX` in the commands. - **ci-4 (ambiguous PR sentence):** The direct-PR sentence now reads "open a PR with `gh-axi` that is ready for review, not a draft, against the base branch `X` (`--base X`), not the repository default." A brief with no base renders exactly as before. The existing assertion in fm-brief.test.sh is updated

* no-mistakes(ci): Both real failures on PR 6442 (run 37064756768) come from CI tooling and a Pi version bump, not from the base-branch feature. Fixes for both already exist on upstream main, so I applied those two commits to the working tree with `git cherry-pick --no-commit`. Nothing is committed or pushed, and no feature file changed. - **Lint 2:** ShellCheck ran out of memory on `bin/fm-teardown.sh` (rss about 8 GB, exit 251, "shellcheck: out of memory"). The rule that broke is that the lint gate must finish on any valid shell root without exceeding the runner's memory ceiling. Upstream commit d719ef3 (kunchenguid#6443) fixes this in `bin/fm-lint.sh`: a root that hits the memory ceiling is retried without `--external-sources`. It also updates `tests/fm-lint.test.sh` and `docs/fm-test-portable-shards.md`. - **Behavior portable serial 6:** `tests/fm-calm-pi-extension.test.sh` failed with "grep disappeared from /export calm.html HTML while calm mode was on". Pi 1.0.1 renamed its HTML export renderer lookup, and upstream commit fede619 (kunchenguid#6530) updates the test to accept the new name. This change is test-only. Both commits applied cleanly. The four touched files are `bin/fm-lint.sh`, `docs/fm-test-portable-shards.md`, `tests/fm-lint.test.sh` and `tests/fm-calm-pi-extension.test.sh`. None of them is in the feature diff, so the feature contract is unchanged. **Local checks:** - `tests/fm-lint.test.sh` passes. - `bin/fm-lint.sh` on `bin/fm-teardown.sh`, `bin/fm-spawn.sh` and `bin/fm-dod-lib.sh` exits 0 with full ShellCheck analysis. - `tests/fm-calm-pi-extension.test.sh` exits 0 with 7 ok and 0 not ok. One case skipped because the Pi package isn't installed locally, so the Pi 1.0.1 export path that failed in CI couldn't be reproduced here. CI needs to confirm it. - I didn't reproduce the 8 GB OOM locally. The PR has to be updated through the pipeline with a normal push: no force push, no replacement PR, no merge
…ole (kunchenguid#6647)

* fix(bin): point project workers at the Firstmate skill file

The Skill tool cannot resolve a Firstmate skill from another project's worktree, so the launch role names the readable skill file instead.

* no-mistakes(review): fix(bin): name Firstmate skill file as fallback only
…ne (kunchenguid#6655)

* feat(bin): retire a contribution whose forge object is permanently gone

Add fm-contributions.sh retire <task> <url> <captain|fleet> <reason>,
which records actor, reason and time on the saved record and removes
that task/url pair from known, poll rotation and coverage even while a
backlog link remains. Repeating a retire keeps the first provenance;
an unrecorded pair, unknown actor, empty reason or unacknowledged
pending signal is refused.

* no-mistakes(review): Keep retirement per task when settling final owners

* no-mistakes(ci): Made the three Greptile fixes the captain chose. ci-1, retirement needs the captain's word: `retire` now accepts only the actor `captain`. Any other actor is refused, including `fleet`. I removed `fleet` from the usage line, the script header (`bin/fm-contributions.sh`), the check in `bin/fm-contributions.sh` and the retired-record validation in `bin/fm-contributions.jq`, which now requires `.actor == "captain"`. The script header now says a retirement always records the captain's word, because the script cannot verify who runs it and the authority to retire is the captain's. The Bearings skill line in `.agents/skills/bearings/SKILL.md` now says `retire` records the captain's word. In the tests, a `fleet` retire is now a refusal case and must leave the record unretired. Every other retire call in the tests uses `captain`. The PR description has not been changed yet: the ruling asks for the same captain's-word statement there, and that belongs to the PR phase. ci-2, blank reasons: the reason check is now `[ -n "${5//[[:space:]]/}" ]`, so a reason made only of spaces or tabs is refused. I added a refusal test that passes a space-tab-space reason. The header also lists a blank reason among the refusals. ci-3, the gone-object test: the test forge wrapper has a new `not-found` fault that returns HTTP 404 for every `api repos/o/r/...` read. The happy-path test `test_retire_ends_observation_of_a_gone_contribution` now uses it in place of the 502 `down` fault, so it models a permanently gone repository rather than an outage. Verification: `bash tests/fm-contributions.test.sh` passed with exit 0. The last lines of its output include the three retire tests passing and no failures. `shellcheck` flagged only SC2034 (`FM_WAKE_QUEUE` unused) and SC1091 (an unfollowed source of `bin/fm-path-lib.sh`), neither on a line this round changed
* test: give the remote-reply whole-log recapture a longer wait

* no-mistakes(review): Extend both recapture waits and simplify retry handling
…nguid#6654)

* fix(bin): reopen a pending-reply escalation after its resolve

A retry of an open decision still appends nothing. A later same-kind escalation after a resolved line for that key appends again, so the next loss is visible.

* no-mistakes(ci): Both Greptile findings are fixed in the working tree; the four directly affected test suites pass, and a wider run of related suites had not finished when I returned this result. Invariant: a line already in the status file is recorded, and only a caller with its own evidence of a new episode may append it again. It must hold for every caller of status_event_recorded: the continuity break and the other call in bin/fm-procevent-remote-reply.sh, fm_parent_channel_append_once, and the pending-reply escalation. Changes: - bin/fm-classify-lib.sh: status_event_recorded is back to the idempotent retry check. It returns at the first matching line, and a later resolved line no longer makes that line look new. This fixes ci-1 for every caller and restores the early exit for ci-2, with no separate scan optimization. - bin/fm-pending-reply-lib.sh: _fm_pending_reply_maybe_escalate_locked now owns the reopen. It appends a new blocked line when the record already has an escalated_epoch (it escalated before and was reset) and the keyed decision is no longer open. Otherwise it uses status_event_recorded, so a retry or a resend that leaves the decision open appends nothing. - Comments on status_event_recorded and the pending-reply header describe this scope. Tests: - tests/fm-pending-reply.test.sh: the regression for escalate, operator resolve, second escalate is unchanged and passes. - tests/fm-remote-reply.test.sh: new regression drives the real reader. After an operator resolve of remote-reply-continuity-ios, a repeated handle and ingest of the same break appends nothing and leaves the decision closed. I placed it beside the existing continuity-break test, not in the pending-reply file, because only that file has the reader harness. - tests/fm-classify-corr-token.test.sh and tests/fm-classify-decision-key.test.sh: the cases that expected a blocked line to append again after a resolve now assert it stays recorded. Verification: - fm-classify-decision-key, fm-classify-corr-token, fm-pending-reply and fm-remote-reply test suites all exit 0 with the fix. - With the two bin files reverted to the commit under review, the new continuity regression and the updated parent-publisher case both fail, so they reproduce ci-1. - shellcheck -x on the changed files reports nothing. - Not finished: a background run of every other suite that mentions the parent channel or pending replies had produced no output, pass or fail, when I returned. One known gap: the escalation writes escalated_epoch before phase. If the process dies between those two writes and an operator resolves the key before the next tick, that tick appends a blocked line for the same episode. I left it, because closing it needs new state. Issue 4755 and other escalation behavior are untouched. Nothing is committed
…ker (kunchenguid#6666)

* fix: refuse a confirming Enter on the Claude exit picker

The background-task picker still classifies as pending, so a retried Enter confirms "Exit and stop tasks".
Stop after the Enter that opened it, and raise the existing stale wake with the dialog name.

A non-paused secondmate still skips that wake, so a secondmate parked on the picker still looks idle.
Model-downgrade confirmation, MCP approval, and any other Claude exit confirmation are not covered, because there is no recorded screen for them.

* no-mistakes(review): fix: anchor exit picker match and wake second mates

* fix: refuse a typed submit while the Claude exit picker is open

A pane that already shows the picker must not receive the message or a confirming Enter.
The match requires the recorded heading and footer lines, and it is skipped when no dialog name is being recorded.

* no-mistakes(review): restore watcher to main behaviour, drop dialog wake

* no-mistakes(test): test: align Herdr picker fixtures with the preflight read

* no-mistakes(document): document exit refusal on a recognised dialog

* fix: remove the dialog file when exit runs in a subshell

do_exit is invoked from a command substitution, so the parent cleanup never saw the path it is supposed to delete.

* no-mistakes(review): fix: set the dialog file path after the control lock

* no-mistakes(document): document why the dialog file path follows the lock

* no-mistakes(ci): The exit cleanup in bin/fm-control.sh now deletes the dialog file first and releases the control lock second. The changes are in the worktree and are not committed. Invariant: only the process that holds the control lock for a task may write or delete that task's dialog file ($STATE/<id>.composer-dialog, the file that carries the picker name from the composer read to the Enter checks). The old cleanup released the lock and then deleted the file, so the delete ran outside the lock. Places where this invariant must hold: control_cleanup is the only place that deletes the file, and the single assignment after CONTROL_LOCK_HELD=1 is the only place that names it. do_exit only truncates the file, and it runs while the parent holds the lock. A process that loses the lock never sets the path, so it deletes nothing (earlier fix, unchanged). No sibling site needed a change. Fix: in control_cleanup, the rm of the dialog file moved above the fm_lock_release call, with a two-line comment that gives the reason. No dialog recognition or supervision code changed. Test: added test_exit_removes_the_dialog_file_before_releasing_the_lock in tests/fm-control-relaunch.test.sh. It runs one `fm-control exit` with a recording rm on PATH. The lock release removes paths at or under the control lock with rm, so the recording rm writes whether the dialog file exists at that moment. The test asserts the last record is "absent". It starts no second command. Verification, all run locally: - Before the fix, the new test failed with "the dialog file must be gone when the control lock is released, got: present". - After the fix, tests/fm-control-relaunch.test.sh exits 0 with 75 ok lines and no "not ok" line, including the new test and the existing test that the file is gone after exit and after relaunch. - tests/fm-control.test.sh exits 0 with 44 ok lines. - shellcheck -x on both changed files exits 0 with no output. One thing I noticed and did not change: tests/fm-control-relaunch.test.sh prints 615 "rm: cannot remove ... Permission denied" lines during its temp-directory teardown. The count is the same with my change reverted, so this change does not cause it, and the suite still exits 0. I could not read the Greptile check log (the check run was not found), so I worked from the finding text and the user instructions
…henguid#6028)

* fix: support stock macOS Bash in timeout watchdog

* no-mistakes(ci): Fixed ci-1 in tests/fm-timeout-lib.test.sh: both startup-owner failure paths now kill the bounded command group before killing the watchdog. Fault injection reproduced both leaks before the fix and confirmed cleanup afterward. Bash 3.2 suite, syntax check, ShellCheck, and diff checks passed; GNU timeout coverage skipped because the binary is unavailable. Production code unchanged
The merge took upstream's side of every textual conflict, which left
fork-only features half wired where both sides had edited the same lines.

Kept, re-attached to upstream's reworked code:
- the Jev checks (house-rules and already-exists steps in the brief and
  definition of done, PR risk line, routine-wake triage for declared
  waits, commit-check launch setting) and their configuration sections
- the project development-branch store for spawn, landing and cleanup
- the durable pool lease, now bounded with upstream's timeout helpers
- captain notes listed on every drain and `fm-inbox.sh pending`
- answer receipts for captain calls whose Done rows were pruned
- the launch-environment scrub and the failed-cleanup-step messages
- supervisor instructions for these in AGENTS.md and the skills upstream
  moved the old sections into

Dropped because upstream now provides it:
- per-task spawn --base and its base= record (upstream --base-branch)
- note-head status timestamps (upstream [at=<epoch>] stamp)
- contributions poll timeout handling and its triage class
- dispatch resolver small questions
- the bounded-run helper generalisation
- completion-gate inventory replacement and --repair-reason
- the CI hold on an old Pi version

Tests that covered dropped behaviour are removed, and upstream's new
fleet-ledger test gets a lease-capable fake pool.
…d tests

- the watcher's Jev triage and stale-bound code call upstream's
  away_record_present, the new name of the helper they used
- the base-branch test quotes its task ids for upstream's stricter lint
- the PR-risk test records its fixture head as pushed, which upstream's
  named-head gate now requires before a pull request is registered
- upstream's worker-account test retires the previous fixture task before
  each spawn, since the pool guard refuses a copy a live record names
Upstream launches a worker by sourcing a per-spawn script instead of
typing the whole command into the pane, so the case now shows that
script's text and names its per-spawn prompt record before comparing.
…d it with two small changes; the full `tests/fm-watch-triage.test.sh` run was still in progress when I had to report, so only the two affected tests are confirmed passing. **Failing test:** `tests/fm-watch-triage.test.sh` stopped at `not ok - could not arm a real PR merge poll for a delivered task`. This is a fork-only test of the fork-only "armed merge poll bounds stale churn" watcher feature. Two upstream changes broke it: 1. **Fixture could not arm.** Upstream's `bin/fm-pr-check.sh` now refuses to arm unless the delivered head is reachable outside the worker copy (`fm_dod_accept_ship_done`). The fixture's meta had no `worktree=`, so arming failed with `named head cannot be verified: worktree missing`. I gave the fixture in `make_merge_poll_home` a real git worktree whose HEAD sits under a remote-tracking ref. Upstream's gate is untouched. 2. **Watcher no longer absorbed the churn.** Upstream changed the poll loop's `last` from `last_status_line` to `status_declared_wait_line`, which is empty for a `done:` line. The fork's `merge_poll_stale_bound` took that variable as its status line, so it never saw `done` and the feature was silently dead after the merge. In `bin/fm-watch.sh` it now reads the task's last status line itself, and the status-line argument is dropped from it and from `terminal_stale_bound`. Upstream's `last=` line is unchanged. **Verification:** - Reproduced the CI failure locally with the exact arm command. - After the fixes, `test_armed_merge_poll_bounds_delivered_stale_churn` and `test_stale_churn_without_an_armed_merge_poll_still_alarms` both pass when run on their own. - `bash -n bin/fm-watch.sh` is clean. - Not confirmed: the tests that come after the previously failing one in that file. CI never reached them, and my full local run had not finished. The cancelled "Behavior portable serial 2" check was not part of this round and I did not touch it
@talktejas
talktejas merged commit 5e730b3 into main Oct 6, 2026
54 of 56 checks passed
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.