Skip to content

feat(bin): support safe record-only teardown for reassigned pool slots - #4

Open
ichts wants to merge 247 commits into
mainfrom
fm/fm-record-only-teardown-upstream
Open

ichts wants to merge 247 commits into
mainfrom
fm/fm-record-only-teardown-upstream

Conversation

@ichts

@ichts ichts commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Problem

Shared Treehouse pool slots are held by one task but can also be named by stale task records. bin/fm-teardown.sh runs its record-exclusivity scan before it checks the slot-owner claim, so an owner and its stale records refuse each other. No supported command can retire a finished record, and hand-editing state/<id>.meta is forbidden.

Fix

When the slot-owner claim for the recorded worktree positively names a different task, teardown of this record becomes record-only:

  • retire this record's backlog transition, status/meta/inbox/poll artifacts and backend endpoint;
  • skip every worktree and branch operation; no return, destroy, prune, or git inside the occupied slot;
  • keep the landed-work test for this record's own branch: unlanded commits still refuse.

Every existing refusal stays when ownership is ambiguous, unreadable, or names this task. The refusal message names the exact owner and the supported record-only path.

Tests

tests/fm-teardown-endpoint-safety.test.sh covers:

  • foreign owner claim plus landed branch: record-only success, neighbour worktree byte-identical;
  • foreign owner claim plus unlanded branch: refusal;
  • own claim: unchanged behaviour;
  • unreadable claim: refusal;
  • no read or git invocation inside the foreign-owned slot.

Context

Replaces upstream PR kunchenguid#7601, closed by request. The earlier upstream work in kunchenguid#7586 covered a wider scope and is not part of this change.

Courtneyezra and others added 30 commits September 24, 2026 03:50
…t-in, and delivery (kunchenguid#5506)

Attestation MATCH; contract-class restore; CI/NM green. Squash-merged by Kun's firstmate.
…kunchenguid#5528)

* fix(bin): bound the startup-network worker's lock waits by its budget

Fixes kunchenguid#5377

The deferred startup network worker bounded its sweeps with a stage budget but
took the publish lock and the fleet-lock lease with an unbounded wait, so a live
holder of that lock kept the detached worker alive for hours past its timeout
with its output discarded at the end. Every wait now goes through the bounded
acquire and shares the remaining stage or delivery budget; a lock a live process
still holds at the deadline ends the worker with a failed record naming the
holder and the rerun command, and a wake so the result surfaces.

* no-mistakes(review): propagate publish exit code from cmd_run terminal paths
…dth (kunchenguid#5517)

* fix(bin): keep the ps fallback identity independent of terminal width

fm_pid_identity's portable fallback read the command column at the
ambient COLUMNS width, so an identity recorded from a wide shell never
matched the one recomputed inside a narrow hook and the continuity guard
denied every fleet command. Pass -ww so the column is never cut.

Fixes kunchenguid#799

* no-mistakes(ci): Fixed CI failure in Behavior portable serial 4. Root cause: the -ww flag added in commit ac7ab5d to fm_pid_identity (bin/fm-wake-lib.sh) shifted the ps argv so $1 became -ww instead of -p, breaking the positional fake-ps fixtures in tests/fm-procevent.test.sh (lines 2736, 3571) which then fell through to real ps and failed the fm-procevent test. Fix (already applied in the worktree, matching the authoritative user instruction exactly): replaced -ww with a COLUMNS=10000 environment pin so the call is COLUMNS=10000 LC_ALL=C ps -p "$pid" -o lstart= -o command=, mirroring fm_pending_reply_pid_identity in bin/fm-pending-reply-lib.sh:982. argv is back to -p PID -o lstart= -o command=, so the fixtures match again with no fixture edits. Comments above the call in bin/fm-wake-lib.sh and in test_pid_identity_is_terminal_width_invariant (tests/fm-watcher-lock.test.sh) now describe the COLUMNS pin instead of -ww; the regression test still asserts narrow-vs-wide byte equality and the full command. Verified: the terminal-width-invariant regression test passes. The only local not-ok results were flaky, run-varying timing tests (procevent launch/claim confirmation, listener reparenting) that differ each run and are unrelated to the ps argv change
…thorized intent (kunchenguid#5526)

Fixes kunchenguid#3608

When a scout is promoted to a ship, the captain's authorized intent is
extracted from a legacy `# Task` body by matching `Captain:` and `[captain]`
lines anywhere in the body, including inside fenced code blocks and indented
examples, while the heading reader already tracks fences. A fenced `Captain:`
example therefore passed the provenance gate and became the ship contract's
intent while the real ask was dropped.

Make the captain-words extractor fence-aware like the heading reader: a line
inside a ``` or ~~~ fenced block, or indented four spaces or a tab, is never a
marked line. The promotion and spawn callers need no change. The regression
test covers both the extractor and the promotion provenance gate refusing a
brief whose only Captain lines are fenced or indented examples.
…riefs (kunchenguid#2868)

* fix(bin): forbid administering the shared worktree pool in crewmate briefs

A crewmate ran a `git worktree remove` loop over the treehouse pool its own
worktree came from, destroying five worktrees - four belonging to tasks that
were running mid-pipeline. The generated brief's rule 2, "stay inside this
worktree; modify nothing outside it", is a rule about files: removing a
worktree is administration of shared state, not an edit outside a directory,
so the sentence never reached the act. The worker satisfied its brief
completely.

Rule 7 already named one piece of shared infrastructure - the no-mistakes
daemon, one instance serving every lane - with the reason stated plainly. The
worktree pool is the same class of thing and was unnamed.

Fold the pool into that existing rule rather than adding a second warning:
state the constraint around the act (create, remove, return, prune, move,
reassign a worktree or pool slot; write into a sibling slot), keep concrete
commands as examples rather than as the definition so no single provider is
pinned, and give the prohibition a real exit through `blocked:`.

The rule is emitted from one shared string interpolated into both crewmate
scaffolds, so the ship and scout copies cannot drift apart. The secondmate
charter deliberately omits it: that home runs its own fleet and legitimately
allocates and returns slots for its own crewmates.

Contract text only; no runtime enforcement layer.

* no-mistakes(document): Distill pool-safety comment rationale

* no-mistakes(review): align pool-rule test grep patterns with emitted [at=<epoch>] text
… a dispatch record (kunchenguid#5524)

* fix(bin): refuse tasks-axi add --start so In flight always has a dispatch record

Fixes kunchenguid#4753

Dispatch (bin/fm-spawn.sh) is the only path that moves a backlog row to
In flight, because it creates the task record, status file, and inbox
that go with the row. A row hand-placed there through the wrapper's
`add --start` had none of those, and nothing later noticed, so the
live-task count included work nobody was doing. The wrapper now refuses
`add --start` (exit 2) and names the dispatch path; plain `add` and
`start <id>` pass through unchanged, and the lifecycle transitions
address tasks-axi directly so dispatch is unaffected.

The issue's other half, a reconcile sweep in bin/fm-inactive-reconcile.sh
that notices an In flight row with no task record, is left as is; this
change closes the only path that creates such a row.

* no-mistakes(review): refuse create --start alias, not just add --start

* no-mistakes(review): reword add --start guard docs to drop only-path overclaim

* no-mistakes(review): scope add/create --start guard docs, drop universal claim
…#5503)

* feat(bin): run the supervision host beside the other non-Pi primaries while away

Cursor's stop-hook park, the OpenCode plugin, the omp watch extension, Grok's
model-owned background arm, and Codex's foreground checkpoint now run
bin/fm-supervision-host.sh in the watcher arm's place when the home opted in
with config/supervision-host, so the host's Claude engine takes away-posture
wakes beside those primaries exactly as it does beside Claude. Without the
file nothing changes.

- The host streams its first cycle's status line, accepts --restart and the
  owner's predecessor arm for its first cycle, and prints each exit in one
  write, so owners that wait for arm readiness and restart their own
  successor (OpenCode, omp) keep their handling handoff.
- Codex's checkpoint passes its bound to the host as the park boundary,
  raises it to FM_CODEX_WATCH_CHECKPOINT_AWAY (3600 s) while the away record
  exists, and lets an engine turn that starts before the bound finish after
  it (FM_SUPERVISION_HOST_PARK_LIMIT).
- /afk launches no away daemon on an opted-in home of those harnesses and
  says so at entry when the file selects no engine for that primary.
- Session start renders the host protocol for each arm owner, and Grok's
  arm command becomes the host.

* fix(bin): keep the watcher-down banner away from the supervision branch actor

A supervision host's engine turn runs guarded commands after its successor
watcher cycle may already have closed on a newer wake, so the guard showed it
the watcher-down banner with the primary's repair line. Under a Codex primary
pin that line is the checkpoint, and a live Codex lab run showed the away
session running it mid-turn (the nested host stood down on its ownership
check). The branch actor never owns watcher continuity, so the banner, its
reminder, and the episode state now leave that actor out, as the queued-wake
warning already does.

The lint telemetry fixture counts bin/fm-afk-launch.sh's source directives,
which the host engine note raised from four to five.

* fix(bin): queue away-session outcomes recorded after the return for main

A Cursor park superseded by the captain's return stops its host as the
engine turn ends, so the host's own handoff of that turn's outcomes was
never printed and the outcomes never reached main. The report surface now
queues every outcome it records after the away record is gone as a durable
check wake; the return owner archives the record before it reads the store,
so each outcome is in the return brief, queued, or both. A host stopped
mid-turn also removes its turn's result and error files.

The stream test now acknowledges its first close and accepts a restarted
cycle that closes on its resurface before the arm confirms it.

* fix(bin): clear a hard-killed host's turn at the next activation

A Cursor park superseded mid-turn can kill its host outright, which runs no
cleanup, so the turn's result, error, and descendant files stayed behind and
any tool process the engine started was left running. The next host's
activation now reaps the descendants that turn recorded and removes its
files. The host suite also registers its homes in a file, because
make_home runs in a command substitution, so its cleanup now stops every
host a case leaves running.

* fix(bin): leave rows that arrive after main's drain unclaimed at its acknowledgement

Main's acknowledgement re-claimed every unreserved queued row, including one
that arrived after the drain above the acknowledged cutoff. That row stayed
main's without ever being shown to it, so while away the supervision host
refused every later wake that included it and handed each back to main until
main drained again. The acknowledgement now claims only unreserved rows at or
below its cutoff.

* docs: name the killed turn's engine and files in the host's failure direction

* docs: record live supervision host runs on the non-Pi primaries

* no-mistakes(review): Replay host-only supervision boundaries across omp session replacement

* no-mistakes(review): Deliver omp supervision-host wakes only at the host's close

* no-mistakes(document): Correct supervision host documentation for non-Pi primaries

* no-mistakes(ci): Fixed the CI failure by naming FM_CODEX_WATCH_CHECKPOINT_AWAY in the rendered Codex host instructions. The focused instruction and checkpoint suites pass
)

* feat(bin): auto-relaunch dead persistent secondmates during ordinary supervision

A persistent secondmate whose primary agent exits mid-session previously
stayed down until the next session-start liveness sweep. Extract the
sweep's probe/classify/relaunch mechanics into a shared library and drive
the same contract from a cadence-gated watcher tick, so a positively dead
or missing endpoint is relaunched through the guarded spawn path within a
poll cycle instead of an hour later.

Only the recovery-grade `dead` and `missing` verdicts authorize relaunch;
ambiguous, unreadable, unverified, and unreachable-remote reads stay
fail-closed and a remote route is never replaced by a local endpoint.
Each relaunch emits exactly one `check` wake and appends to a durable
per-mate ledger; a mate exceeding the bounded attempt budget is parked
behind a marker until a live probe rearms it. A per-mate liveness lock
serializes the tick against a concurrent session-start sweep.

* no-mistakes(review): Fail closed on relaunch ledger errors; clear state on remote teardown

* no-mistakes(review): Share ledger read guard; retire relaunch state under liveness lock

* no-mistakes(review): Lazy-load wake lib; live rearm restores full relaunch budget

* no-mistakes(review): Finish liveness tick for every mate before waking once

* no-mistakes(review): Keep liveness tick scanning past per-mate errors, then wake

* no-mistakes(review): Wake only on queued rows; teardown holds liveness lock

* no-mistakes(review): Queue liveness outcome wake before releasing mate lock

* no-mistakes(document): Update secondmate liveness documentation for mid-session recovery

* no-mistakes(lint): Fix empty assignments flagged by ShellCheck

* no-mistakes(ci): Added ShellCheck analysis boundaries for the shared liveness library in both callers and marked its result globals as intentional library outputs. Changed-file lint passed; full CI partitions were not run locally

* no-mistakes(ci): Fixed Lint 2 by removing an unused test variable in tests/fm-wake-queue.test.sh. ShellCheck, bash syntax, and the full wake-queue test script pass

* no-mistakes(ci): Fixed the CI wake-queue fixture: stall-only watcher legs now seed the liveness cadence marker, preventing the new endpoint probe from interfering with their assertions. The full wake-queue test, ShellCheck, and diff checks pass locally
…cycle ends (kunchenguid#5550)

* fix(bin): start a successor when the Claude Stop-hook arm's attached cycle ends

Fixes kunchenguid#2381

When the Claude Stop hook's foreground arm attached to a peer watcher cycle
and that cycle ended, the arm reported the delivered wake and the hook exited
2 without starting a successor, so the handling turn ran with no watcher.
Pi, omp, and OpenCode start the next arm before delivering the wake and pass
the closed arm's pid as FM_WATCH_PREDECESSOR_ARM_PID; the Claude hook never
passed that predecessor identity.

The hook now runs its arm as a tracked child it waits on, so it holds that
arm's pid, and after any actionable close starts one handling-successor
bin/fm-watch-arm.sh with the closed arm's pid as FM_WATCH_PREDECESSOR_ARM_PID.
The successor is launched the one way a process outlives a Claude hook's
exit-2 rewake (nohup, detached stdio, own process group, the shape
bin/fm-startup-network.sh already uses); the hook waits for its status line
and adds one banner line when no live watcher was confirmed, never withholding
the wake. The supervision-host path is unchanged, as is the arm wrapper.

The regression test drives the real hook against an arm fixture whose attached
peer cycle ends: it fails on the previous tip because no successor starts, and
now asserts the successor names the closed arm as its predecessor and outlives
the rewake. A second case pins the unconfirmed-successor banner line.
docs/watcher-continuity.md no longer records the Claude asymmetry.

* no-mistakes(ci): Serial-4 failure was a real regression: tests/fm-session-lock-ancestry.test.sh asserts exact cumulative arm-invocation counts while driving the real fm-claude-stop-autoarm.sh hook against a stubbed fm-watch-arm.sh. This PR makes the hook start a handling successor after an actionable close, so every owned actionable phase now records TWO arm invocations (foreground arm + successor) instead of one, breaking "healthy chain: expected 1 arm(s), got 2". Fixed by updating the cumulative expectations to match the new behavior: owned phases 1/2/6 -> 2/4/6, foreign carry phases 3/4/5 -> 4, plus a comment explaining the +2-per-owned-phase model. Verified: phase-1 (the CI failure point) now passes on every run, syntax checks clean, and sibling arm-count tests (fm-claude-stop-autoarm.test.sh, fm-cursor-primary, fm-turnend-guard) pass unchanged. The only remaining local not-ok is a WSL-only environmental artifact (orphan reparents to a subreaper, not PID 1) that passes on the CI runner. Parallel-1 failure is an unrelated flake: its 11 tests (fm-lint, fm-pr-merge, fm-test-run, fm-cd-pretool-check, fm-pi-primary-types, fm-grok-harness, fm-composer-lib, fm-review-diff, fm-tmux-submit-busy, fm-composer-ghost, fm-brief) do not include fm-session-lock-ancestry and none reads any file this PR touches; all pass locally. It should clear on CI re-run. Made the smallest root-cause fix (one test file, 6 count updates + a clarifying comment). Validation of the branch continues through the no-mistakes pipeline, which owns re-running CI
…ning (kunchenguid#5566)

* fix(bin): report a Lavish source armed only after its listener is running

Registration alone was treated as ready, so arm could succeed before anything was collecting from the board.

* no-mistakes(review): Guard Lavish arm launches, keep retire refusals, report live prior listener

* no-mistakes(review): Keep polling through window before reporting a still-live prior listener

* test: wait for a capture's claim to drop before the next arm

The result is stored before the runner exits, so a re-arm in that gap was meeting a live claim.

* no-mistakes(document): Record Lavish arm readiness evidence in verification doc

* no-mistakes(ci): Both failures were caused by this PR, and both are fixed with test-only edits. Lint 2 (ShellCheck SC2034): this branch removed the only use of `reply_id` (a `start "$reply_id"` call) from tests/fm-procevent.test.sh, which left the assignment at line 1450 unused. I deleted that assignment. It was the only `reply_id` in the file. ShellCheck is now clean on both test files. Behavior portable serial 4: the failing test was tests/fm-bearings-board.test.sh, in the check "registration consumed its answer before the any-origin binding existed". I reproduced it locally: the hold was still `state: queued` when the test checked it. - What must hold: the test's check that the hold is closed must run after the listener has captured the answer. - Why it broke: the test used a stand-in adapter that ran `fm-procevent.sh start` in the foreground after `arm`, so capture finished before build returned. On this branch, `arm` starts the listener itself in the background, so the real listener captures the answer and closes the hold a moment after build returns. - Fix: removed the now-redundant stand-in adapter, the copied runtime directory, and its extra environment variables. The test now runs the real build through the existing `run_board` helper and waits up to about 10s for the hold to reach `state: done`. The checks that follow are unchanged: `Resolution mode: answered` and the any-origin binding. - Other tests: this was the only test in the file that stood in for the adapter this way. The shard's other pure-contract-unit test (tests/fm-trace-context-lib.test.sh) passed unchanged. Verification: - tests/fm-bearings-board.test.sh passed 3 times in a row via bin/fm-test-run.sh, all 18 checks, about 53s per run. - tests/fm-procevent.test.sh was not rerun, because the lint fix only removed an unused assignment
…elivered (kunchenguid#5599)

* fix(bin): acknowledge a delivered unknown-wake escalation

The same unrecognized wake was escalated again after it had already been handled, because delivery never recorded that identity.

* no-mistakes(review): Scope unknown-wake acknowledgements to one away session

* no-mistakes(review): Clear delivered digest when unknown-wake ack write fails

* no-mistakes(review): Limit unknown-wake suppression to acknowledged lines

* no-mistakes(document): List unknown-wake ack file among away-session artifacts
…ess wait (kunchenguid#5587)

* fix(bin): keep a stated default retraction from cancelling a keyless wait

A resolved line that names the shared default decision bucket was closing the keyless live wait that only prints as that same key. Keyless self-retraction still closes the keyless wait.

* no-mistakes(review): Keep declared waits standing past foreign-key resolved lines

* no-mistakes(review): Bound declared-wait read and share one decision-key parser

* no-mistakes(document): Document supervisors' key-aware declared-wait read
…unchenguid#5544)

* fix(bin): terminate a remote job worker that lost ownership when it receives TERM

A serving worker whose lock directory is gone can no longer quarantine shutdown, and resuming service publishes a false ready heartbeat. Exit after stopping only that worker's own command tree, without removing a replacement owner's lock.

* no-mistakes(review): Check worker lock ownership before publishing shutdown quarantine

* no-mistakes(document): Correct worker shutdown comment on replacement-owned lock

* fix(bin): keep an ousted remote job worker off the replacement quarantine

Shutdown can lose the lock after the first ownership check and before it
writes or clears quarantine. Bind both operations to the directory object
this process still owns so a replacement's quarantine stays untouched.

* no-mistakes(review): Make ousted-worker shutdown test reliably reach quarantine clear

* no-mistakes(document): Reattach worker_shutdown doc comment to its function

* no-mistakes(ci): Fixed the failing check (Behavior portable serial 7) with a test-only change to the stall test in tests/fm-remote-job.test.sh. Product code is unchanged; no other test changed. Cause: after the decoy dies, both workers run the same check-exists, read, delete sequence on the job records. On the CI runner the replacement deleted a record between the ousted worker's check and its read. The ousted worker exited 125, and because the file runs under set -e the unguarded `wait` ended the test with 125. The exit trap then killed the replacement, which produced the "Killed" line. Reproduction: a temporary 0.3 s delay between the check and the read, applied to the ousted worker only, made the committed test fail exactly as in CI (exit 125 and the "Killed" line). The new test passed with the same delay. The delay is reverted, along with a similar debug hook that the timed-out attempt had left in bin/fm-remote-job-worker.sh. Test changes: - The replacement is frozen (and confirmed stopped) before the decoy is killed and resumed only after the ousted worker exits, so only one worker touches the job records at a time. - The ousted worker is stopped only once its quarantine exists and its lane is reaped, which places it inside its stop loop. - Every fixed poll loop is now a wait on a named condition with a 30 s deadline and an explicit failure message. Exit detection also handles zombies. - The exit trap kills and waits for the decoy and both workers on every path. - A non-zero exit from the ousted worker now fails with its exit code and stderr instead of silently ending the file. The test still proves that the resumed ousted worker exits 0 and leaves the replacement's lock, quarantine contents and quarantine inode unchanged. Verification: the full test file passed four times on its own and three times under nice -n 10 with four busy-loop CPU hogs; bin/fm-lint.sh passes. Changes are not committed

* no-mistakes(ci): I fixed the failing check (Behavior portable serial 7) by changing only the stall test in tests/fm-remote-job.test.sh. Product code is unchanged. **What failed:** "an ousted worker in shutdown leaves the replacement quarantine untouched" failed on CI with the ousted worker exiting 125 ("could not stop the active command tree"). **Why:** during shutdown, the worker retries the still-running decoy command group a fixed 100 times, 0.01 s apart, then gives up and exits 125. The test tried to freeze the worker partway through those retries by sending SIGSTOP from outside. On a slow runner the retries ran out before the stop arrived, so the worker had already given up. The invariant is that the test must hold the ousted worker inside that retry loop until the replacement owns the lock. That was the only place the test depended on timing. The other waits already watch for a named state change with a 30 s deadline. **Fix:** - The ousted worker now starts with a small `sleep` wrapper at the front of its PATH, and the SIGSTOP race is gone. - The wrapper only holds a `sleep` called directly by that worker's own process (it checks its parent pid against a hold file) while its quarantine file exists. - The only such `sleep` is the first retry in the shutdown stop loop, so the worker waits there as long as needed. - The wrapper writes a marker when it starts holding. The test waits for that marker, then hands the lock to the replacement, freezes the replacement, and kills the decoy. - The test releases the worker by deleting the hold file. Deleting the whole temp directory also releases it, so a failed run cannot leave the wrapper looping. - A process leak: the test overwrites the job's command-group record with the decoy, so no worker ever stopped the job's real command. `fm-hold-job.sh` and its `sleep 30` stayed running for up to 30 s after the test. The test now records that group before overwriting it and kills it at the end of the test and in the exit cleanup. - The test still asserts the same things: the ousted worker exits 0, and the replacement's lock, quarantine contents and quarantine inode are unchanged. **Verification:** - The full file passed twice on its own, twice under `nice -n 10` with six busy-loop CPU hogs, and twice more after the leak fix. - `pgrep` found no leftover processes afterwards. - With the worker from just before the fix commit (cf45cb6^), the test still fails with "the ousted worker wrote or cleared the replacement quarantine during shutdown", so it still proves the fix. - `bin/fm-lint.sh` passes. - I did not reproduce the CI failure locally. The cause comes from the fixed retry limit and the CI error message. The changes are not committed

* no-mistakes(ci): I changed only the stall test ("an ousted worker in shutdown leaves the replacement quarantine untouched") in tests/fm-remote-job.test.sh. Product code is unchanged, and so is every other test. **Invariant:** the pid written to the job's group record must be a process-group leader whose group dies when that one process is killed. Otherwise the worker's bounded stop loop never sees the group die, gives up, and exits 125 ("could not stop the active command tree") before it reaches the lost-ownership exit. The decoy is the only place in this test that depends on this. **Fix:** - The decoy used to be `set -m; sleep 30 &`. It now starts as `perl -MPOSIX=setsid -e 'setsid() >= 0 or exit 1; exec @argv' sleep 30 &`, which gets its own session and group without shell job control. tests/fm-procevent.test.sh already uses the same idiom. - The test now waits, with the file's usual 30 s deadline and a named failure, until `ps -o pgid=` of the decoy equals its pid before writing it into the group record. This way the worker can never read the record before `setsid` has run. - The existing steps are unchanged: the test kills the decoy, reaps it with `wait` before releasing the hold file, and the exit trap still kills and reaps the decoy and both workers. - The assertions are unchanged: the ousted worker exits 0, and the replacement's lock pid, quarantine text and quarantine inode stay the same. **Cleanup:** I reverted a debug `printf` hook that the timed-out previous attempt had left in bin/fm-remote-job-worker.sh, and deleted its untracked `.tmp-repro/` directory. Neither was committed. **Verification:** - The full tests/fm-remote-job.test.sh passed twice normally and once under `setsid -w` with stdin from /dev/null (no controlling terminal). - `bin/fm-lint.sh` passes. - No leftover `sleep 30` processes afterwards. **Not reproduced:** I could not reproduce the CI failure locally. On this host `set -m` made the decoy its own group leader even without a controlling terminal, so the cause on the runner is not confirmed. The change removes the test's reliance on shell job control, as the user asked. Changes are not committed

* no-mistakes(ci): I changed only the stall test ("an ousted worker in shutdown leaves the replacement quarantine untouched") in tests/fm-remote-job.test.sh. Product code is unchanged, and so is every other test. **Invariant:** the group record the ousted worker checks in its stop loop must stay the job's own command group, and the test must stop that group before it releases the hold. Otherwise the bounded retry keeps seeing a live group, gives up, and exits 125 ("could not stop the active command tree") before it reaches the lost-ownership exit. The test overwrote this record in one place (the decoy) and stopped the group in one place (killing the decoy); both are changed. **Fix:** - I removed the setsid decoy and the overwrite of `.claim/group`. The record keeps the job's real command group, which the test still saves as `STALL_JOB_GROUP`. - The two-line `group_start` stays. It is still needed: without it the worker kills the real group on its first pass, before the replacement takes over, so the hold would never matter. - The `sleep` wrapper that holds the worker at its first stop-loop retry is unchanged. - After the replacement owns the lock, its quarantine is planted and it is frozen, the test runs `kill -KILL -- -$STALL_JOB_GROUP`. It then waits, with the file's usual 30 s deadline and a named failure, until `kill -0` on the group fails. Only then does it remove the hold file. The worker therefore always sees its own command already stopped and never races its retry budget. - The exit trap still kills the saved command group if the test fails. It can't `wait` on that group because the group is not a child of the test shell. The decoy variable and its cleanup entry are gone. - The assertions are unchanged: the ousted worker exits 0, and the replacement's lock pid, quarantine text and quarantine inode stay the same. **Verification:** - The full tests/fm-remote-job.test.sh passed twice normally. - It passed once under `setsid -w` with stdin from /dev/null (no controlling terminal). - It passed once under `nice -n 10` with six busy-loop CPU hogs. - With the worker from before the fix (cf45cb6^), the test still fails with "the ousted worker wrote or cleared the replacement quarantine during shutdown", so it still proves the fix. - No `fm-hold-job` or `sleep 30` processes were left afterwards. - `bin/fm-lint.sh` passes. **Not reproduced:** I couldn't reproduce the CI failure locally; the decoy version also passed on this host. So I can't confirm why the decoy group stayed alive on the runner. The new wait turns any leftover live group into a clear named failure instead of an exit 125. The changes are not committed

* fix(bin): keep a dead command group dead on bash 5.2

A bare return inside the liveness check drops the failing kill status when the check runs in a conditional, so shutdown keeps treating a stopped group as alive and exits 125.

* no-mistakes(review): Use bash 3.2 fd syntax and fix trap return comments
…henguid#5589)

* docs: make configuration settings easier to find and understand

* no-mistakes(review): Restore dropped qualifiers and fix misplaced config doc labels

* no-mistakes(review): Restore three dropped qualifiers in configuration reference
)

* fix: bound worker edits of project AGENTS.md/CLAUDE.md to factual corrections

These files are loaded into every agent session of a project, so additions
should be a deliberate human choice rather than automated task output. The
ship brief's project-memory section and AGENTS.md section 6 previously invited
workers to record durable knowledge, which let project AGENTS.md files accrete
detail the codebase or README already carries. Workers now edit only to fix
factually wrong content - including content their own change made wrong - and
fm-ensure-agents-md.sh runs only alongside such a correction. Stow no longer
routes project-memory additions through ship tasks, and the generated skeleton
no longer invites discovery-driven additions.

* no-mistakes(review): Stop running fm-ensure-agents-md.sh on memory-file corrections

* no-mistakes(document): Clarify manual project-memory initialization and remove duplicate guidance
…enguid#5635)

* fix(bin): let gate agents drive lifecycle against marked lab homes

Part 2 of the kunchenguid#5615 split. A no-mistakes gate agent runs inside a
checkout carrying the fleet-captain identity, so fm-gate-refuse-lib
refuses fleet mutation on the gate signal. That refusal was absolute,
which kept gate validation from ever exercising the real lifecycle.

Stamp a disposable lab FM_HOME with a .fm-lab-home marker file that
only bin/fm-lab-home.sh writes, and only onto a fresh empty dir, so
no call path can mark a populated real home. fm_refuse_if_gate_agent
then permits lifecycle only when FM_HOME carries the marker and is
driven through its stock layout - any FM_*_OVERRIDE relocation stays
refused so part of the "lab" cannot be split back onto the real fleet.
The threat model is a confused agent touching the real fleet, not
deliberate forgery, so the marker is a plain token file rather than a
bound record. FM_GATE_REFUSE_BYPASS is unchanged: it still serves the
test harness, which cannot mark hundreds of temp homes.

Teardown's slot-ownership scan compared state-dir paths textually
while fm_firstmate_root_home canonicalizes, so a lab home under a
symlinked TMPDIR scanned its own record twice and self-collided;
compare file identity (-ef) instead.

* no-mistakes(review): Refuse unlistable lab homes and hardlinked slot records

* no-mistakes(review): Mint lab markers only on verified-empty fresh dirs

* no-mistakes(document): Clarify lab-home gate documentation and comment contracts

* no-mistakes(document): Clarify lab-home gate documentation and remove stale claims

* no-mistakes(document): Clarify gate lab-home documentation and boundary wording
…ad of refusing every re-arm (kunchenguid#5594)

* fix(bin): replace a watcher whose beacon stalls past a hard bound instead of refusing every re-arm

A fleet watcher that is alive but whose liveness beacon has gone stale could
never be replaced: every re-arm was refused because the lock holder was a live
pid, and the holder was never evicted because it was not dead. Add
FM_WATCHER_STALL_BOUND (default 3x the stale grace): below it the refusal is
unchanged; at or past it the arm re-verifies the holder against the lock's
recorded identity, sends TERM, waits boundedly, and takes the lock the normal
way, ledgering a stalled-holder-replaced row. A holder that survives TERM keeps
the old refusal.

Fixes kunchenguid#4400

* no-mistakes(test): poll for replacement message to fix watcher-lock test flake

* no-mistakes(document): document FM_WATCHER_STALL_BOUND in config inventory
…n can keep them (kunchenguid#5563)

* fix(pi): hide queued Firstmate notifications under Calm only when the session can keep them

Calm now keeps authenticated Firstmate operational inputs out of Pi's queued-message
listing, but only after proving the live session exposes every member needed to keep
them across Escape. A session missing any of them keeps stock rows and Escape and shows
one generic warning. Escape and the dequeue key return only captain-authored messages to
the editor and re-queue hidden notifications in order; after an abort that kept any in
Pi's agent queue, the adapter starts the delivery turn itself because Pi 0.87.1 does not
continue an aborted run. Compaction-held notifications stay with Pi's compaction flush and
never start or announce a turn.

Fixes kunchenguid#1588

* docs(calm): record Pi 0.87.1 queued-row retention verification

* no-mistakes(review): Deliver kept Calm notifications after tree-navigation aborts too

* no-mistakes(review): Defer Calm notification turn until tree navigation finishes

* no-mistakes(lint): Silence SC2016 for literal JavaScript in queue-retention e2e test
…uid#5548)

* fix(bin): refuse teardown when a required source disappears

A missing sibling was sourced after cleanup had started, so Bash 3.2
exited 0 from the EXIT trap and Bash 5 continued and reported success.

* no-mistakes(review): Remove unused FM_TEST_ONLY hook from teardown tests

* no-mistakes(review): Check task backend sources before any teardown cleanup

* test(gotmp): give teardown fixtures every tmux adapter sibling

Teardown now refuses when a sibling the recorded backend's adapter sources
is missing, so the fake bin must carry fm-session-lock-lib.sh,
fm-agent-process-lib.sh and fm-gemini-lib.sh.
Restructure the supervision host doc's prose into shorter sections, lists,
and tables without changing documented behavior. Every original heading,
anchor, identifier, number, quoted string, and link target is preserved.
* docs: make herdr-backend easier to read

Restructure the Herdr backend doc's prose into shorter sections, lists, numbered procedures, and tables without changing documented behavior. Every original heading, anchor, fenced code block, link target, and documented fact is kept.

* no-mistakes(document): Restore composer-proof reason and complete Herdr topic table
Restructure the prose into sections, lists, and tables without changing
documented behavior. Every original heading and anchor, inline-code span,
link target, number, and quoted string is kept, and each sentence sits on
its own line. Adds a topic navigation table and short subsections under
the existing headings.
* docs: make watcher-continuity easier to read

Restructure the prose into sections, lists, and tables without changing documented behavior. Every original heading, anchor, identifier, link target, and number is kept.

* no-mistakes(review): Fix actor and supervision-host scope in watcher-continuity doc

* no-mistakes(review): Make readiness TERM and retry conditional on unready successor
* docs: make sessionstart-nudge easier to read

Restructure the prose into sections, lists, and tables without changing documented behavior. Every original heading, inline-code span, link target, number, and fact is preserved, and a harness-to-tier table now sits near the top.

* no-mistakes(review): Drop helm glossary line and dedupe exit-code lead-in
* docs: make captain-hold-lifecycle easier to read

Restructure the captain-hold lifecycle prose into sections, lists, and tables without changing documented behavior. Every original heading, anchor, identifier, number, quoted string, and link target is kept.

* no-mistakes(review): Fix verification record subjects and grouping headings

* no-mistakes(review): Clarify task-body read-back cases belong to the suite
* docs: make remote-secondmates easier to read

Restructure the remote second mates prose into sections, lists, numbered procedures, and tables without changing documented behavior. Every original heading, anchor, fenced code block, identifier, link target, and qualifier is preserved.

* no-mistakes(review): Merge remote-home table cell into one sentence

* no-mistakes(review): Tighten readiness lead-in, restore causal link, fix dangling reference
…nguid#5554)

* fix(bin): bound the away digest and log why a delivery failed

The away daemon joined every buffered escalation into one unbounded
digest. A start-up catch-all span can exceed what one transport argument
carries (tmux rejects the send-keys command; Linux refuses to exec any
argument above 131,071 bytes, which is how herdr receives it), so the
initial send failed on every housekeeping pass and was logged as an
unconfirmed Enter with text possibly in the composer.

escalate_flush now builds the injected digest under a fixed byte budget:
each event is cut at a UTF-8 boundary with an omitted-bytes marker, the
joined events stop with a "+K more event(s)" tail, and a bounded digest
names a state/.subsuper-digests/ file that keeps every buffered event
verbatim. The buffer itself is untouched, so the return catch-up stays
complete.

The tmux submit core and the herdr literal send now replay the
transport's stderr on failure, and inject_msg logs the failing stage
(initial send versus Enter confirmation) with the byte count and that
stderr. The wedge alarm line and marker carry the last failure reason.

Fixes kunchenguid#4382

* no-mistakes(review): Drop digest pruning; label send-failed as send-or-Enter stage

* no-mistakes(review): Keep digest full text once submit ran; reuse on retry

* no-mistakes(lint): Count digest files with find instead of ls

---------

Co-authored-by: firstmate-oss <firstmate@kunchenguid.local>
…henguid#5638)

* feat(tests): add FM_TEST_SEAM launch seam and gate lab-primary recipe

Part 1 of the kunchenguid#5615 split: the pieces that let the no-mistakes pipeline
live-validate firstmate changes, without the gate-refusal rescoping.

- bin/fm-afk-launch.sh: FM_TEST_HARNESS pins the detected harness only
  alongside the FM_TEST_SEAM=1 marker test suites set, so a leaked variable
  in a real primary's environment stays inert and unknown tokens fall
  through to real detection.
- tests/lib.sh: export FM_TEST_SEAM=1 for every suite.
- .no-mistakes.yaml: per-harness recipe for running a real fixture primary
  from a gate run - a plain mktemp lab FM_HOME on a private tmux socket,
  with FM_GATE_REFUSE_BYPASS=1 scoped to it and NO_MISTAKES_GATE scrubbed.
- tests/fm-wake-queue.test.sh: stop the owned watcher fixture with KILL and
  clear its lifecycle state so the next leg starts clean; TERM could leave
  bash waiting in a child on some runners.
- tests/fm-remote-secondmate-lifecycle-e2e.test.sh: wait for the liveness
  lock holder's post-acquire marker instead of the lock dir, which is
  published before the claim finishes.

* no-mistakes(review): Scrub lab home overrides and require FM_TEST_SEAM separately

* no-mistakes(document): Clarify test seam and disposable lab bypass documentation

* no-mistakes(document): Clarify lab isolation and test-seam documentation

* no-mistakes(ci): Fixed the CI failure: test cleanup killed the remote worker child but left its supervisor able to restart it during fixture removal. Cleanup now stops the worker tree. The lifecycle test passed locally; ShellCheck and diff checks passed
… home is gone (kunchenguid#5552)

* fix(bin): refuse watchers from disposable checkouts and exit when the home is gone

Fixes kunchenguid#321
Fixes kunchenguid#4760

A watcher armed from a disposable no-mistakes validation checkout under
.no-mistakes/worktrees/ outlived the validation step and kept writing the
real home's state, and a running watcher never noticed when its home,
state directory, or code root disappeared. The arm now refuses from such
a checkout with the typed failure line, the watcher checks once per poll
that its home, state directory (or its own lock holder record), and bin
directory still exist and exits with a logged reason scoped to itself,
and the shared test helpers reap every watcher a suite armed for a
temporary home through the home-scoped stop.

* no-mistakes(lint): fix SC1007 by assigning empty string in watch-arm test

* no-mistakes(ci): Found and fixed a genuine, reproducible hang introduced by this branch's test-watcher reaper, which is what killed both CI checks (serial-2 cancelled at the 30-min cap; Lint 2 exit 143 = the suite's own TERM-trap code). Root cause: test_drain_asserts_watcher_liveness (tests/fm-wake-queue.test.sh) fabricates a .watch.lock whose pid is the test runner's own $$ with the runner's real identity, to make the drain believe a live watcher exists. The new make_case tracking registers that state dir for reaping, so at fm_test_cleanup the new fm_test_reap_watchers drives fm-watch-arm.sh --stop; its identity check matches (the fixture recorded the runner's identity) and it kill -TERMs the test runner. tests/lib.sh:231 is `trap 'fm_test_cleanup; exit 143' TERM`, so the TERM re-enters cleanup -> reap -> kills $$ again -> infinite loop until the runner cap. I reproduced this locally: the suite ran all tests then looped forever in cleanup spawning fm-watch-arm.sh --stop against a lock naming its own PID. Fix (tests/lib.sh, +5 lines): in fm_test_reap_watchers, skip any tracked lock whose pid equals our own $$ before driving --stop. This is the single shared reap boundary; seven $$-self-lock fixtures across four test files are all covered by the one guard, and real armed watchers (pid != $$) are still reaped. Invariant: the test reaper must only signal real armed watcher processes, never the test runner itself. Verified locally: tests/fm-wake-queue.test.sh -> EXIT 0 (63 ok, no hang); tests/fm-watch-arm.test.sh -> EXIT 0 (21 ok, including test_reaper_stops_a_tracked_watcher, confirming the guard does not over-skip). Lint 2's exit 143 was the same shard/cap signature; a fresh CI run on this new commit will re-evaluate it

---------

Co-authored-by: firstmate-oss <firstmate@kunchenguid.local>
…m as silence (kunchenguid#5588)

* fix(bin): surface an unrecognized status prefix instead of dropping it

A parked or holding declaration, and a verb whose correlation token did not parse, never became an event, so the supervisor still saw the earlier line.

* no-mistakes(review): Require verb-shaped unrecognized status prefixes, add continuation tests

* no-mistakes(document): Document unrecognized status prefix escalation in afk skill

* no-mistakes(ci): I reproduced the "Behavior portable serial 6" failure locally and fixed it by changing the test data in one test. No product code changed. **What failed:** `tests/fm-session-start.test.sh`, in `test_orphan_status_logs_are_printed`, with "matched status log was printed 2 times". **Why:** the test writes status lines with made-up prefixes, `matched: surfaced once` and `orphan: step N`. The test only uses them as placeholder text. It checks that the session-start digest prints each task's status tail exactly once. This PR (kunchenguid#4763) deliberately makes an unrecognized one-word lowercase prefix a status event. So those lines now surface as captain-relevant events, and the wake queue's STATUS OUTCOME BACKSTOP section prints them a second time. The code under review is behaving as the issue asks. Only the test's placeholder data had become meaningful. **Rule the test depends on:** its status lines must not be captain-relevant, so the digest is the only place they are printed. Both lines in this test broke that rule. The orphan line would have failed the same count check right after the matched line did. **Fix:** in that test only, I switched both lines to the recognized, non-captain verb `working:`: `working: surfaced once` and `working: orphan step 1..6`. I updated the matching assertions and counts to use the new text. What the test checks is unchanged: orphan logs are labelled, the tail is bounded, the log path is printed, and each tail appears once. **Verification:** before the fix, the test failed locally the same way as in CI. After it, `bash tests/fm-session-start.test.sh` reports "all assertions passed

* no-mistakes(review): Detect unrecognized prefixes on unstamped lines; share verb list

---------

Co-authored-by: Kun's firstmate <kunchenguid+firstmate@users.noreply.github.com>
tiago-peixoto and others added 30 commits October 17, 2026 11:38
…ne (kunchenguid#7450)

Port the reported-check fallback and regressions from df60538. Preserve required-only reporting when required checks exist and refuse failed fallback reads.
* fix(tests): clean read-only relaunch fixtures

Use the shared fixture cleanup so generated read-only Git hook directories do not leave permission errors and temporary trees behind.

* fix(accounts): support durable per-secondmate account pins

Resolve optional primary-home secondmate account files through the worker pin parser and checks. Preserve existing defaults when no override exists and re-read the selected pin on spawn, relaunch, and recovery.

* no-mistakes(review): Name the rejected pin file in account refusals

* no-mistakes(document): Document per-secondmate account pins in relaunch and layout docs
…unchenguid#6958)

* feat(watch): consult opt-in operator guards before a wedge escalation

Add config/wedge-evidence-guards, an opt-in list of operator-owned guard
executables that the watcher's at-threshold wedge branch consults after the
wait evidence, worktree-write probe and dead-endpoint probe. A CRITICAL
answer defers one window at a time, capped by FM_WEDGE_GUARD_DEFER_MAX
consecutive deferrals recorded beside the other wedge-window records, and
the escalation that ends a capped run names the guard and its reason.
Timeouts, failures, malformed output, UNKNOWN and WARNING fail open, and an
untrusted config or guard is never run. With the config absent the ladder is
unchanged.

Refs kunchenguid#6919

* no-mistakes(document): Document wedge-evidence-guards in home layout index

* no-mistakes(document): Align jev-guards CRITICAL verdict with bounded deferral
…guid#7460)

* fix(bin): make the brief's inbox listing succeed on an empty inbox

The worker brief told crewmates to list the inbox with a bare *.msg glob.
On an empty inbox that glob fails: bash ls exits 2 and zsh aborts with
'no matches found'. Use find piped to sort -V so the listing is silent on an
empty or absent inbox and keeps numeric order past 999.

Refs kunchenguid#7244

* no-mistakes(review): Make relaunch note inbox listing safe when empty
…unchenguid#7394)

* fix(bin): fit remote second-mate convergence inside the startup network bound

The deferred startup network stage (120s) timed out once four second mates
were remote: the mates already converge concurrently, but each mate's
inheritance push sent its fifteen allowlisted items one SSH round trip after
another, so a single mate's convergence took 64-76s after a ~16s liveness
pass.

- fm-remote-inherit-push.sh validates and snapshots every item locally first,
  so a local refusal now sends nothing, then sends the items up to
  FM_REMOTE_INHERIT_PUSH_JOBS (default 4) at a time and replays their output in
  allowlist order. A failed item still fails the push with its own error, and
  no further item is started after it. The receiver is unchanged: each item is
  its own file and generation record, and the host's job worker still runs one
  home's commands one at a time.
- fm-bootstrap.sh gives each remote mate's convergence its own budget,
  FM_SECONDMATE_CONVERGENCE_TIMEOUT (default 75s). A mate whose sync or push
  hits it is reported as that mate's SECONDMATE_SYNC skip and keeps its retry
  marker, instead of holding the whole stage past its bound.

* fix(bin): nudge a remote second mate to re-read only when something changed

Each remote convergence attempt records its retry marker before it starts,
so a sweep cut off by the startup network bound or failing before any change
left a marker that made the next session start nudge every remote mate even
though its instructions and inherited files were unchanged. The marker now
records whether a re-read is owed, set only once an attempt actually changed
the home, and a tracked-file sync that moved nothing under AGENTS.md, bin/,
or .agents/skills/ no longer counts as a change.

* fix(bin): keep a remote mate's re-read owed when its convergence is cut short

A convergence step stopped by the per-mate budget loses its output, so a
push that had already applied an item reported nothing and left the retry
marker with no re-read owed; the next start then saw every item unchanged
and never told the mate to re-read. A step cut off by the budget now marks
the re-read owed, and so does a failed push whose output shows items it had
already pushed or removed.

The lifecycle e2e suite runs every remote leg through the real entrypoint
and job worker on the test machine, so it now gives convergence ample time;
the budget's own behavior is pinned in fm-bootstrap-network-parallel, whose
budget and failure cases are also made independent of machine load.

* no-mistakes(review): Keep re-read owed on partial or unknown completion

* no-mistakes(review): Persist owed re-read when killed mid-mutation

* no-mistakes(document): Fix stale remote re-read owed docs

* test: send inherited items one at a time in the remote lifecycle e2e fixture

Behavior portable serial 5 failed on the watcher auto-relaunch leg of
tests/fm-remote-secondmate-lifecycle-e2e.test.sh after the inheritance push
started overlapping its sends. Across 14 recent main CI runs that suite's
remote-spawn leg took 30.8-35.6s and the watcher leg 27.3-34.5s, already close
to its 30s wait; on the PR run remote spawn took 42.1s, well outside that range,
while legs that push nothing were only 5-10% slower. The watcher relaunch is a
remote spawn, and the only changed code on that path is the inheritance push.

This fixture's transport has no network latency and its job worker runs one
home's commands one at a time, so overlapping sends cannot hide any round trip
there and only add submission overhead to every remote spawn. The suite now
sends one item at a time, as it did before; the overlapped scheduler stays
covered by tests/fm-bootstrap-network-parallel.test.sh, and the production
default of four is unchanged. The watcher leg's failure now also prints the
watcher's output.

* no-mistakes(document): Clarify remote re-read obligations and consolidate documentation

* fix(bin): record a remote mate's re-read as owed before convergence can change its home

A live run against a real remote second-mate home showed the owed re-read
still lost when the startup network bound stopped the stage. That bound
stops it the way `timeout -k 1` does: TERM to the stage's process group, then
KILL one second later. Bash defers a TERM trap while the convergence worker
waits in a command substitution, and the remote call runs in its own timed
process group, so the KILL landed before the trap could record the re-read.
The remote home had already applied the change, the next start saw every item
unchanged, sent nothing, and deleted the marker, so the mate was never told
to re-read.

Bootstrap convergence and config-push now write the record owed before any
remote step can change the home, and write it back only after the attempt has
reported: removed after confirmed unchanged convergence or a successful send,
and back to owed=0 when a failed attempt's outcome proves nothing changed. A
kill at any point therefore leaves the re-read owed. The TERM/INT traps are
gone. The kill regression now sends TERM and then KILL one second later, as
the startup bound does; it fails on the previous head and passes here.

* no-mistakes(document): Clarify convergence timeout scope and consolidate reread documentation
…d#6987)

* fix(bin): refuse spawn into a project with an unborn HEAD

Spawn into a repository with no commit waited 60s for treehouse and then reported that the worktree was the spawning project itself, never naming the cause. Refuse before the wait and name the unborn HEAD and the remedy. The project-management skill now states that a new local repository needs a first commit.

* docs(skills): tell the captain a new project needs a first commit

The Create a project step no longer reads as firstmate committing into the project. It states that spawn refuses an unborn HEAD and that the captain is told the project needs a first commit.

* test: give spawn fixtures a first commit for the unborn-HEAD refusal

* no-mistakes(review): Commit foreign consent fixture so spawn guard passes
…log items for GitLab MRs (kunchenguid#7462)

* feat(bin): add an attended GitLab --allow-no-pipeline merge waiver

A GitLab repository that deliberately runs no merge request pipeline could
never be merged through fm-pr-merge.sh: the guard requires a successful head
pipeline at the current head, and GitLab has no per-check waiver.

--allow-no-pipeline waives only the pipeline condition, and only when GitLab
reports head_pipeline as present and null. A failed, running, canceled, or
stale pipeline still refuses, every other condition (open, mergeable
detailed_merge_status, no conflicts, discussions resolved) still holds, and
the verified head is still passed to glab as --sha. The flag is attended-only
(refused while the away-posture record exists), GitLab-only, and takes no
value; without it the default refusal is unchanged.

* fix(bin): close a GitLab merge request task's backlog item with a note

tasks-axi accepts a --pr link only as a GitHub or Forgejo pull request, so
after a GitLab merge request landed, teardown could never complete the
backlog Done transition and left the close pending forever.

Extend the existing Gerrit carve-out in fm_backlog_done and
fm_backlog_retain, and the captain-hold answer replay that shares it, to
record a GitLab merge request URL as a "GitLab merge request <url>" note,
until tasks-axi accepts GitLab merge request URLs. GitHub and Forgejo pull
requests are still recorded as pr links.

* no-mistakes(document): Note GitLab --allow-no-pipeline waiver in merge-watch record
…henguid#7451)

* fix(tests): tolerate slow orphan-storm source launches

* no-mistakes(document): docs: pin orphan-storm row to pacing, not lease expiry
* fix: respect gh-resolved base in publication target selection

* no-mistakes(document): Document gh-resolved publication target selection
…4728)

* feat(bin): add Paseo as an experimental session backend

Modeled on the cmux adapter, with Herdr's one-workspace-per-task UX:
terminal NAME is the routing authority, workspace title is decoration
only. tmux stays the default; Paseo is opt-in via config/backend,
FM_BACKEND=paseo, or PASEO_AGENT_ID/bundle-id auto-detection.

* no-mistakes(document): docs(paseo): register Paseo across backend inventories and lists

* no-mistakes(document): docs(paseo): list paseo in AGENTS.md config/backend entry

* fix(bin): give Paseo one shared workspace per project with a tab per task

Paseo's sidebar is project > workspace > terminal tab. The adapter used
to create one workspace per task, so a fleet showed up as a pile of
sidebar entries. It now mirrors Herdr's container shape: one shared
`firstmate` (or `2ndmate-<id>`) workspace per project, adopted by cwd
and title or created once, with each task as a terminal tab inside it.
Kill closes only the task's tab; the shared workspace is never archived
and the adapter never runs a `paseo project` command, so Paseo reuses
the project registered for the path instead of multiplying projects.

Live findings folded in: the CLI prints an Electron warning on stderr
when firstmate itself runs inside a Paseo agent, so parsed `--json`
calls no longer merge stderr into stdout; `workspace ls` reports the
cwd normalized but not symlink-resolved, so adoption matches the raw,
logical, and physical path; a bare Escape stays pending in zsh's emacs
keymap and fuses with the next key, so the smoke test verifies labels
with Enter and sends Escape last.

The smoke test now owns a throwaway project directory and deletes the
project it registered, so a captain's real sidebar is never touched.
Both paseo suites get a portable-serial duration hint and a `paseo`
family so the coverage guard stays under its unhinted cap.

* test(bin): teach the backend-list suites about paseo

CI serial shard 5 failed because tests/fm-bootstrap.test.sh still
expected the known-backend list to end at cmux. Add paseo to that
expectation and to the sibling lists in the control, trace-context, and
teardown-safety suites (with a valid paseo endpoint record), and scrub
PASEO_AGENT_ID and __CFBundleIdentifier in the bootstrap suite's hermetic
unset so running the tests from inside a Paseo agent cannot flip a
default-backend case onto paseo the way $TMUX or HERDR_ENV would.

* no-mistakes(review): Fix paseo control keys, secondmate fallback, docs, and tests

* no-mistakes(review): Send paseo C-u as raw byte, refuse untokenized keys

* no-mistakes(document): Sync Paseo docs for key tokens, secondmate fallback

* no-mistakes(lint): Use prefix-form PATH in paseo autodetect tests

* fix(bin): make paseo explicit-only and adopt the tab's own workspace

Paseo stamps its markers into every descendant process: an agent exports
PASEO_AGENT_ID, a terminal tab exports PASEO_TERMINAL_ID and
PASEO_WORKSPACE_ID, and both carry __CFBundleIdentifier=sh.paseo.desktop.
A tmux server started from a Paseo tab inherits all of them, so their
presence never proves the captain chose Paseo (the same finding recorded
on kunchenguid#4484 and kunchenguid#3884). fm_backend_detect now ignores every Paseo marker;
paseo is reachable only through config/backend, FM_BACKEND=paseo, or
--backend paseo, exactly like zellij and orca. That also retires the
auto-detected-paseo secondmate fallback to tmux, which only existed
because of auto-detection: an explicit paseo still refuses --secondmate,
and an unconfigured home inside Paseo stays on tmux silently.

Once Paseo has been selected explicitly, PASEO_WORKSPACE_ID is still
useful: a firstmate running inside a Paseo tab adopts that tab's own
workspace as the project's shared workspace when its cwd is the project,
before falling back to the firstmate-title lookup (herdr's
launcher-identity rule). Verified live that CLI-created tabs export
PASEO_WORKSPACE_ID and no PASEO_AGENT_ID.

Docs also note that `paseo run --cwd` is ignored when PASEO_AGENT_ID is
set; the adapter never uses `paseo run`.

* no-mistakes(test): Drop stale Paseo auto-detect mention from fm-spawn help

* no-mistakes(document): Document Paseo own-workspace adoption order

* no-mistakes(ci): Fixed ci-3 with a test-only change. The unconfigured-spawn case in test_secondmate_spawn_refuses_explicit_paseo_only failed on CI with "error: no launch template for harness unknown". CI runners have no agent process above the test, so harness detection returned unknown before the spawn reached the home check. The fix pins the harness with `printf claude > "$dir/config/secondmate-harness"` just before that spawn call, with a two-line comment explaining why (tests/fm-backend-paseo.test.sh). I verified locally that tests/fm-backend-paseo.test.sh passes, including the secondmate test, and that shellcheck is clean on the file. I did not rerun it detached from the agent process chain; your earlier report of it failing without the pin and passing with it is the evidence for that part. As you instructed, I made no change for ci-1 or ci-2 (the fork approval gate) or for the Lint 2 runner shutdown, which was infrastructure

* fix(bin): let the adapter precheck load paseo

Main now checks that a backend's adapter and its sibling libraries are
readable before sourcing it, and refuses any backend it has no sibling
list for. Paseo had no entry, so every paseo call failed to load the
adapter and fell back to 'unknown'. Paseo sources the same siblings as
cmux, so it shares that entry and sources through the checked path.

* no-mistakes(document): Make fm_backend_source adapter-count comment count-free

* test(bin): prove the Pi branch claims paseo stale wakes

The watcher keys a non-Orca stale wake by the task's window= value, and
the Pi supervision branch resolves a stale row to its task only through
window= (Orca's terminal= key is not indexed, which is how Orca stale
wakes ended up waking main). Paseo records
window=<terminal-uuid>:<workspace-uuid>, so pin that a stale row keyed
by that value is claimed by the branch for the right task.

* no-mistakes(document): Note Paseo adapter in composer capture-window config doc

* fix(bin): refuse a paseo spawn when its inventory is unreadable

Paseo allows duplicate workspaces and terminal names, so the adapter
looks up the project's shared workspace and checks the task's terminal
name before creating either. Both lookups treated a failed or unreadable
listing as empty, so a transient daemon error during spawn created a
second workspace for the project or a duplicate task terminal. The spawn
now refuses instead, and creates nothing, when either inventory cannot be
read.

* docs(paseo): note why capture reads the whole scrollback

Paseo caps terminal scrollback at 1024 lines, so reading all of it and
trimming locally stays bounded however long a task runs. A bounded
`--start -<n>` read is not a drop-in replacement: it counts the blank
screen rows below the prompt as the tail, so a quiet terminal captures
as empty.

* no-mistakes(document): Document Paseo spawn refusal on unreadable inventory

* no-mistakes(lint): Rewrite paseo workspace inventory check as explicit if-block

* feat(bin): keep paseo tasks from cluttering the sidebar

Every project a firstmate worked in got its own "firstmate" workspace in
Paseo, and those workspaces and their projects stayed in the sidebar
after the work was done.

When firstmate itself runs inside Paseo, task tabs now go into its own
workspace, whatever the project. A terminal tab names that workspace
through PASEO_WORKSPACE_ID; an agent session only has PASEO_AGENT_ID, so
its workspace is the one live workspace whose folder is the firstmate
home.

Otherwise the per-project shared workspace is still used, and closing
its last task tab archives it and deletes its project when the folder is
one of the home's own clones. Archiving a workspace also archives its
agents and leaves the project listed, so cleanup skips firstmate's own
workspace, workspaces titled by someone else, workspaces with an agent
in their folder, and projects outside the home, and does nothing when an
inventory cannot be read.

* no-mistakes(review): Retire paseo workspace when tab already gone; rename spawn vars

* no-mistakes(document): Align Paseo doc workspace wording with own-workspace placement

* fix(bin): serialize paseo spawns with workspace cleanup

Cleanup checked that a per-project workspace had no tabs, made more
Paseo calls, and then archived it. A spawn could open a tab in that
workspace in between, and the archive would then take a live task's tab
with it.

Spawning (choosing or creating the workspace and opening the tab) and
cleanup now share one home-wide lock, the stale-owner lock from
fm-wake-lib.sh that herdr already uses. A spawn that cannot get it
refuses and cleanup skips. Cleanup now only archives a workspace for one
of the home's own clones, which only this home spawns into, so a
per-home lock covers every spawn that could use it.

* no-mistakes(document): Scope Paseo workspace retirement to home clones in architecture

* test(paseo): fix shellcheck SC2031 lint warning in fm-backend-paseo test

The workspace-lock holder helper read $! after backgrounding the holder,
which ShellCheck's dataflow analysis flags as SC2031 because the helper
only runs inside command substitution. The holder now records its own
PID ($$, the same process $! named) before signalling readiness, and the
helper returns that. No behavior change.

* test(paseo): keep the smoke test from archiving a workspace it did not create

Run from a Paseo tab or agent, PASEO_WORKSPACE_ID/PASEO_AGENT_ID steered
create_task into the caller's own workspace, which cleanup then archived.
The smoke test now unsets both before sourcing the adapter, archives the
workspace create_task returned only while its cwd is the test's own temp
dir, and runs cleanup once. Test-only; assertions are unchanged.

* no-mistakes(document): List paseo in FM_BACKEND and README backend lists

* no-mistakes(document): Add paseo to backend dependency docs and lists

* no-mistakes(document): docs: list paseo among Command Code refused backends

* no-mistakes(document): Note Paseo as unstyled in opencode footer record
…g budget (kunchenguid#7545) (kunchenguid#7558)

Claude Code lists skill descriptions within a budget of 1% of the context
window and drops the least-invoked skills' descriptions first, which are
the rarely-loaded recovery skills whose descriptions AGENTS.md section 13
uses as the trigger index.

- Remove the decision-hold-lifecycle redirect stub; nothing on main
  points briefs at it any more (captain-hold-lifecycle owns the policy and
  docs/captain-hold-lifecycle.md still documents the fm-decision-hold.sh
  shim).
- Tighten the longest skill descriptions to their load triggers, keeping
  every trigger. Total description size drops from 11312 to 6142
  characters across the listing.
- Document /doctor, /skill-doctor, skillListingBudgetFraction, and
  skillOverrides "name-only" in the Claude harness adapter notes as
  opt-in, home-local vendor settings.

No session-start budget warning or other default-on surface is added.
…enguid#7563)

* fix(extension): launch lifecycle helpers through verified Bash

The extension host spawned bin/fm-procevent.sh directly with
shell:false, which native Windows Node rejects with spawn EFTYPE because
it cannot run a script through its shebang. Resolve an absolute Bash
from $BASH or PATH, verify it with a short probe (on Windows it must
report an MSYS or Cygwin OSTYPE, so WSL bash.exe is skipped), and pass
the script as its first argument. The child keeps shell:false, the
sanitized environment, literal argv, and the existing failure mapping.

Fixes kunchenguid#7559

* no-mistakes(review): Harden lifecycle Bash interpreter verification

* no-mistakes(document): Document lifecycle Bash verification contract

* fix(extension): try exported BASH only on Windows for lifecycle helpers
…nchenguid#7348)

Sites that run one no-mistakes daemon per isolated workspace keep each
task's runs in a per-workspace NM_HOME that cannot be derived from the
shared gate remote, so crew state, pipeline spend, and teardown read the
wrong home.

When NM_HOME is unset and config/nm-home-resolver names a site-provided
executable, fm-nm-run-lib.sh now calls it with the task copy path and
exports the printed home for that one call. Only host reads (axi status,
the axi overview, axi logs, runs, daemon status, the state database) and
axi abort --run use it. An explicit NM_HOME always wins, and with no file
behaviour is unchanged.

Fixes kunchenguid#7309
…ds-decision line (kunchenguid#7361)

* feat(bin): let a worker's own open blocked: or needs-decision: line defer wedge escalation behind config/wedge-defer-worker-wait

Opt-in only: absent the flag the wedge ladder is unchanged. Counts only the worker's own newest line (reserved keys excluded via status_own_open_decisions), defers onto the existing bounded recheck with a verb-specific clearing action and no escalation-count increase, and ends on any newer status line or a newer acknowledged inbox record. The away daemon honors the same evidence before escalating an enriched wedge.

Refs kunchenguid#6899

* no-mistakes(document): Document worker-wait deferral in wait-evidence lists
* feat(bin): pin per-mate secondmate runtime overrides in config/secondmate-harness

A single default pin line gives every secondmate one runtime, so raising the
default pin silently migrates a mate the captain ruled onto its own runtime.
config/secondmate-harness now accepts optional
"mate <secondmate-id>: <harness> [<model>] [<effort>]" override lines, and
fm-harness.sh secondmate, secondmate-model, and secondmate-effort take the
secondmate id as an optional second argument that selects that line.

Spawn, the control-plane relaunch, and the secondmate restart each pass the
mate id they already know, so all three resolve one decision. A malformed pin
file is refused with its line number and prints no token instead of resolving
around it to the default runtime.

* no-mistakes(review): Bypass unused pins during explicit secondmate relaunch

* no-mistakes(document): Correct and consolidate per-mate runtime pin documentation

* fix: preserve legacy secondmate pin parsing without overrides

* fix(bin): keep legacy secondmate pin files opt-in

config/secondmate-harness is documented as "the first non-empty, non-comment
line is parsed". The per-mate override grammar made the whole file strict, so
a second default line or a fourth token started refusing every secondmate
spawn, relaunch, and restart - even in a home that never wrote a mate line.
That changed how an existing, already-valid file is read, so the feature was
not opt-in.

Scope the strict whole-file parse to files that declare at least one
"mate <secondmate-id>:" line. A file with no mate line parses exactly as
before: first non-empty, non-comment line, the rest ignored, a fourth token
ignored, nothing refused. A file that does declare a mate line keeps the
strict parsing, so a duplicate override, a malformed entry, or an extra token
still refuses loudly with its line number.

Coverage: a legacy file with a second default line and a fourth token now
spawns, relaunches, and restarts a mate onto its first line; a declaring file
that is malformed refuses each of those paths with the offending line; and an
explicit --harness/--model/--effort relaunch succeeds whatever the pin file
holds, because the config read stays behind the explicit-harness guard.

* no-mistakes(document): Document per-mate secondmate runtime pins

* no-mistakes(ci): Fixed `tests/fm-backend-herdr-presentation-e2e.test.sh` teardown to restore write permission on fixture directories before removal. This addresses the read-only git-hook cleanup errors in the failed Herdr check. The real-Herdr presentation E2E test passed locally (exit 0)
…unchenguid#6973)

Adds bin/fm-capacity.sh, a read-only, network-free report of open ship/scout lanes against an optional config/lane-capacity target plus host load, memory and disk headroom. Without config/lane-capacity it reports counts only and gives no dispatch-hold advice. Docs, operational-home-layout skill entry, a gated AGENTS.md pointer, and tests/fm-capacity.test.sh.
… panes (kunchenguid#7437)

* fix: bound unknown-live watcher alarms

* no-mistakes(review): Gate unknown-alive stale bound on progress; fix arm CONFIG

* no-mistakes(review): Keep watch.env loading only in the watcher arm

* no-mistakes(review): Seed worktree progress before first unknown-alive alarm in test

* no-mistakes(document): List config/watch.env in operational home layout

* no-mistakes(review): Narrow watch.env docs to arm-launched watchers

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
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.