Skip to content

feat(bin): sync upstream fleet orchestration capabilities - #3

Merged
Profreshor merged 196 commits into
mainfrom
fm/fork-upstream-sync
Oct 16, 2026
Merged

Profreshor merged 196 commits into
mainfrom
fm/fork-upstream-sync

Conversation

@Profreshor

@Profreshor Profreshor commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Intent

Our fork of firstmate (Profreshor/firstmate) is well behind upstream (kunchenguid/firstmate, maintained by Chun Ken). Get the fork up to speed with upstream without losing our 'firstmate crew' feature: the split pane view (the per-home Herdr crew workspace of task panes) is valued and must keep working, and we want upstream's latest improvements.

Context: as of 2026-10-16 the fork's main is 5 commits ahead and 194 commits behind upstream main; the merge base is 977a81e (2026-09-24). Fork-only commits: 6b9f571 per-home Herdr crew workspace of task panes, b18be3b overflow a full crew workspace into another, b8d1cad crew pane docs (merged as fork PR #1), and 4c727ac guarded settings inspection tool, bin/fm-secrets.sh/.py (fork PR #2). The fork home depends on bin/fm-secrets.sh daily, so it must keep working too.

What Changed

  • Sync upstream fleet orchestration updates across worker supervision, recovery, remote secondmates, event handling, and harness backends.
  • Add an optional config/crew-memory-max safeguard that runs each worker launch in a bounded fm-crew.slice systemd scope.
  • Update Firstmate skills, documentation, CI configuration, and automated coverage for the integrated behavior.

Risk Assessment

🚨 High: The upstream sync preserves the required crew and settings-tool paths, but it also carries an unrequested, host-specific worker-memory feature that needs explicit approval before merge.

Testing

Focused live validation passed: the isolated Herdr lab created, overflowed, rebalanced, and cleaned up crew panes through real spawn/teardown, while fm-secrets exercised its real CLI with safe synthetic settings. CLI transcripts are the appropriate reviewer-visible evidence; no UI surface changed. The lab test completed its guarded teardown.

  • Live validation: ✅ go - 3 of 3 scenarios driven live against the product
Scenario Result Live Evidence
Crew workspace creates and overflows split-pane task views ✅ pass live Live Herdr transcript: herdr-crew-workspace-live-e2e.txt
Crew task cleanup removes only its pane and recreates empty workspaces safely ✅ pass live Live Herdr transcript: herdr-crew-workspace-live-e2e.txt
fm-secrets safely inspects settings and runs commands without exposing secret values ✅ pass live fm-secrets CLI behavior transcript: fm-secrets-cli-behavior.txt
Evidence: Live Herdr crew-workspace transcript

Source: Live Herdr crew-workspace transcript

# lab herdr server 0.9.1 protocol 22, client 0.9.1 protocol 22
ok - real herdr: the first crew task creates one labeled crew workspace right after the home, without moving focus
ok - real herdr: six crew panes fill an even 3x2 grid, top row left to right, then bottom row
ok - real herdr: a seventh crew task opens a second crew workspace right after the first, without moving focus
ok - real herdr: list-live discovers crew panes by their task labels
ok - real herdr: killing one crew pane closes only that pane and keeps focus
ok - real herdr: the next crew task refills the freed slot and restores the even grid
ok - real herdr: a same-labeled agent-free crew pane is replaced only after its successor exists
ok - real herdr: closing a whole crew column rebalances the remaining columns evenly
ok - real herdr: each crew workspace disappears with its last pane and the next spawn recreates one
ok - real herdr: real spawns on the crew setting become panes of one crew workspace
ok - real herdr: real teardown closes one exact crew pane and the workspace goes with the last one
Evidence: fm-secrets CLI behavior transcript

Source: fm-secrets CLI behavior transcript

ok - fm-secrets: names and has expose names and booleans only
ok - fm-secrets: run scrubs stdout and stderr and preserves child status
ok - fm-secrets: run excludes ambient secrets and scrubs short secret names
ok - fm-secrets: empty secret settings do not corrupt output
ok - fm-secrets: run scrubs short selected values
ok - fm-secrets: run scrubs stream-boundary values
ok - fm-secrets: run detaches terminal descriptors
ok - fm-secrets: run forwards termination to its child process group
ok - fm-secrets: run rejects trailing quoted data
ok - fm-secrets: run scrubs empty-username URL passwords
ok - fm-secrets: run scrubs decoded URL passwords
ok - fm-secrets: service has inspects a running process without exposing values
ok - fm-secrets: service has safely reads EnvironmentFile and Environment declarations
ok - fm-secrets: service has orders wildcard EnvironmentFiles
ok - fm-secrets: service has rejects invalid EnvironmentFile characters
ok - fm-secrets: service has only ignores missing optional EnvironmentFiles
ok - fm-secrets: service has marks unmodeled environment unknown
ok - fm-secrets: help documents the scrub boundary

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 warning
  • ⚠️ bin/fm-spawn.sh:737 - The required intent is to synchronize upstream while retaining the crew workspace and fm-secrets; it does not require a new systemd memory-management feature. This merge additionally introduces config/crew-memory-max, which wraps every worker in a MemoryMax scope and can refuse launches on unsupported hosts. Remove this extra component (including its wrapper, docs, and test) unless you explicitly want to maintain that new fleet-memory contract.
✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 3 of 3 scenarios driven live against the product
Scenario Result Live Evidence
Crew workspace creates and overflows split-pane task views ✅ pass live Live Herdr transcript: herdr-crew-workspace-live-e2e.txt
Crew task cleanup removes only its pane and recreates empty workspaces safely ✅ pass live Live Herdr transcript: herdr-crew-workspace-live-e2e.txt
fm-secrets safely inspects settings and runs commands without exposing secret values ✅ pass live fm-secrets CLI behavior transcript: fm-secrets-cli-behavior.txt
  • tests/fm-backend-herdr-crew-workspace-e2e.test.sh — guarded disposable fm-lab session; real Herdr pane layout, overflow, real spawn, and teardown
  • tests/fm-secrets.test.sh — public CLI behavior with synthetic settings, redaction, status propagation, and service inspection
  • git status --short && git diff --check — no test-phase worktree changes
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

kunchenguid and others added 30 commits September 24, 2026 09:05
…#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>
…henguid#5658)

Fixes kunchenguid#5295

Session start now reports a remote inheritance failure using the
push's own error line instead of the first unchanged item that
happened to print before it, and the shared captain preferences
header check now names the first required phrase it did not find,
on both the local and remote inheritance paths.
…yloads (kunchenguid#5657)

* fix(bin): stand down the Claude Stop auto-arm on pi-code-delivered payloads

pi-code loads the tracked Claude settings but has no asyncRewake, so it
awaits every Stop hook; without a stand-down the auto-arm runs
synchronously inside Pi's turn end and holds it open for the declared
multi-hour timeout. Stand down when the payload's transcript_path
contains a /.pi/ path component, the same discriminator the closed-but-
unmerged fix in kunchenguid#3352 used, with an explicit string-type check on the
jq filter.

Fixes kunchenguid#3343

* no-mistakes(document): document pi-code stand-down in harness integrations reference
…kunchenguid#5659)

* fix(bin): match whole multi-word project names in the registry lookup

bin/fm-project-mode.sh matched a registered project name against only the
first whitespace-delimited token of a registry row, so a name containing a
space never matched, silently defaulting the project to no-mistakes off
instead of its declared posture.

The lookup now matches the whole registered name against the raw line text,
so a name is compared literally (never as a regex) and a name that is a
leading prefix of another registered name still resolves to its own row.

* no-mistakes(document): docs already accurate for multiword registry name match

* chore: drop accidental empty err file

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>
…d#5546)

* fix(bin): classify the stdin program of `bash -s` with operands in the arm policy

With -s, sh/bash/zsh read the program from stdin even when operands follow;
the operands are only positional parameters. The arm policy treated the first
operand as a script path, so heredoc and here-string payloads were never
classified and a hidden bin/fm-watch.sh execution was allowed.

A protected path in the operand position still fails closed as before.

Fixes kunchenguid#1489

* no-mistakes(document): Clarify stdin shell operand documentation

* no-mistakes(ci): Captain, fixed `shellInvocation` so `bash -- -s` treats `-s` as a script name, and updated R21 to test the exact command. The targeted policy suite, lint, documentation check, and diff check pass. Both hosted workflows show `action_required` before any jobs ran; that external approval state remains unresolved

* fix(bin): keep main's handling of words after a leading `--`

Revert the pipeline CI-step change that made the first word after a leading
`--` always a script. It turned forms that main denies today into allow
(for example `bash -- -c 'bin/fm-watch.sh'`), which is outside kunchenguid#1489 and
loosens a fail-closed policy. `--` after `-s` still ends option parsing.
…nchenguid#5695)

* fix(bin): strip AI co-author trailers from fleet-launched commits

Cursor and other non-Claude runtimes append the trailer after the typed
message. A per-task commit-msg hook removes it and leaves human co-authors
and the author identity untouched.

* no-mistakes(review): Export pane hooksPath override and drop generated-with stripping

* no-mistakes(ci): This PR caused all three CI failures, and the fix is test-only: 4 test files change, no product code. **Cause.** `fm-spawn.sh` now installs the AI-trailer strip hooks for every spawn, secondmates included. The installer refuses a worktree that is not a git repository, and the PR deliberately keeps that fail-closed rule because real secondmate homes are firstmate clones. Four test fixtures still gave secondmates a plain directory as their home, so each spawn failed with "not a git worktree ... could not install the AI-trailer strip hooks": - serial 5: `tests/fm-backlog-atomicity.test.sh` ("secondmate spawn failed"). - serial 8: `tests/fm-secondmate-harness.test.sh` ("split: no meta written"). - Herdr: `tests/fm-backend-herdr-launcher-workspace-e2e.test.sh` and `tests/fm-backend-herdr-workspace-per-home-e2e.test.sh`. **Rule that must hold.** Every home a test spawns as a secondmate must be a git worktree. I checked the other places in the changed area: the only secondmate spawns in these tests are the ones listed. The earlier rounds already fixed the other fixtures (`fm-secondmate-liveness`, `fm-secondmate-safety`) the same way. **Fix.** - Each of those four secondmate homes now gets the same `.gitignore` plus `git init -q -b main` that the liveness and safety tests already use. - The two Herdr tests clean up with their own plain `rm -rf "$TMP_ROOT"`, not the shared `tests/lib.sh` helper. Because the installer leaves each `state/<id>.git-hooks` directory read-only, that cleanup printed "Permission denied" and left the directories behind. Both cleanups now restore the owner's write bit on every directory before removing (`find ... -exec chmod u+rwx`), which is what `fm_test_remove_tree` in `tests/lib.sh` does. **Verification.** - `tests/fm-secondmate-harness.test.sh` passes. - `tests/fm-backlog-atomicity.test.sh` passes (99 ok, exit 0). - shellcheck is clean on all four files. - I could not run the two real-Herdr tests locally: the Herdr lab on this host refuses to start because it needs exactly one running default session, and I did not change the host's Herdr state to get around that. Instead I checked their two changed steps directly: the installer succeeds on a home set up the new way, and the new cleanup removes the read-only hooks directory completely. Those two tests will only be proven on CI
…id#5683)

* fix(bin): treat Pi's dollar-first cost footer as furniture

An idle Pi status row opening with $0.000 was read as a dead-shell prompt, so exit and relaunch refused on an empty composer.

* test: wait for the draining holder to exec sleep before reading its identity

The procevent drain fixture read fm_pid_identity immediately after
backgrounding setsid sleep, racing the child's exec chain. Mid-exec the
cmdline can read empty, failing the fixture on a loaded CI runner.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
andrewli8 and others added 18 commits October 14, 2026 03:22
…er-strip hooks path (kunchenguid#6341)

* fix(bin): keep inherited Git credential helpers when installing the trailer hooks path

Problem:
Every fleet launch prefixed the worker command with `export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=...`.
That replaced any `GIT_CONFIG_*` entries the pane already carried, so a scoped HTTPS credential setup such as `credential.helper=` followed by `credential.helper=!gh auth git-credential` was silently dropped.
Git then fell back to the system or global helper (for example `osxkeychain` from the system gitconfig on macOS), so a worker meant to use a scoped token pushed with the user's personal credentials.

Reproduction on main:
GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=credential.helper GIT_CONFIG_VALUE_0= GIT_CONFIG_KEY_1=credential.helper GIT_CONFIG_VALUE_1='!gh auth git-credential' sh -c "<emitted launch>; git config --get-all credential.helper" prints none of the inherited entries, only the system helper (`osxkeychain` from Homebrew's system gitconfig on the reporting machine).

Fix:
The launch prefix now appends `core.hooksPath` at index `GIT_CONFIG_COUNT` and increments the count, resolved in the destination shell after any `config/launch-env-allowlist` filtering.
The inherited count is validated as decimal before shell arithmetic, leading zeros are normalized (Git accepts `02`), and a count that cannot be extended within Git's signed int refuses.
A refusal uses `return 1 2>/dev/null || exit 1`, so when the pane sources the launch file the error stays visible and the interactive shell survives instead of the pane dying with the message.

Tests:
`tests/fm-spawn-dispatch-profile.test.sh` gains a behavior test that runs the real emitted launch under sh, bash, and zsh with inherited credential-helper entries and counts `2` and `02`, then asks Git for `credential.helper` and `core.hooksPath`.
The same test checks that an invalid or overflowing count refuses, and that sourcing the launch file from an interactive shell keeps that shell alive with the error visible.
The exact-launch assertions in the dispatch-profile and Kimi suites now extract the emitted prefix through one `fm_test_launch_git_prefix` helper in `tests/lib.sh` instead of two hand-copied strings, whose content the behavior test covers.

Risk:
Top-level `return` outside a sourced file is unspecified by POSIX; verified on macOS sh (bash 3.2 in POSIX mode), bash, and zsh, and every path still ends nonzero before the worker command runs.
The hook wrapper still unsets only `GIT_CONFIG_COUNT` and index 0, which is sufficient because Git ignores indexed entries once the count is unset.
A launch that forwards `GIT_CONFIG_COUNT` without all of its indexed keys now fails in Git instead of being masked by the overwrite.

* no-mistakes(review): Keep inherited Git config in hooks and fish-safe launch

* no-mistakes(document): docs: describe appended hooksPath keeping inherited Git config

* test: run the fish pane launch cases in CI

Install fish in the portable serial lane and run the credential-append
and invalid-count refusal cases under fish. Under CI the spawn test
fails instead of falling back to the static syntax check.

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

* test: strip the Git config launch prefix before reading the Claude doorbell

The trust suite evaluated the whole launch command to find the final prompt
argument, so it now also sourced the per-spawn git-config env file that the
fake pane never writes. Drop that prefix the same way the dispatch suite does.

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

* test: share the launch command parser across the spawn suites

The trust suite failed in CI because its copy of the launch parser missed
the Git configuration prefix the dispatch suite's copies already stripped.
Move the prefix and export stripping into tests/lib.sh next to
fm_test_launch_git_prefix so the three parsers cannot drift again.

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

* fix(bin): canonicalize env-file hooks path and tighten launch-config tests

env-file now writes the canonical (pwd -P) spelling of the hooks directory, matching what install records, so a symlinked path no longer makes the hook wrapper miss its own entry and silently skip the repository's hook.
A new strip test passes a symlinked hooks path through env-file and fails without this change.

The launch-env-allowlist test now also asserts the appended core.hooksPath, so a regression that drops the strip-hook entry only on the env -i path is caught.

Without fish installed locally, the launch test now prints an explicit fish skip line and limits its pass message to sh, bash and zsh, instead of claiming every pane shell after only a static token check.

* no-mistakes(review): Drop CI fish install and CI-only fish refusal

* no-mistakes(document): Document GIT_CONFIG_COUNT overflow refusal in configuration docs

* test: prove a refused Git config count never starts the worker

The invalid-count refusal checks only asserted a nonzero status and the helper's own error text.
If the launch line kept going after the refusal, the worker probe's final `git config` also failed under the bad count, so the checks still passed.
The probe now prints a marker on stderr, and every refusal case in sh, bash, zsh and fish asserts the marker never appears.
Dropping the `|| return 1 2>/dev/null || exit 1` guard from the launch line now fails the test.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…available (kunchenguid#6810)

* fix(bin): name the failing read when a contribution observation is unavailable

An unavailable contribution observation recorded one constant error and
discarded the failing read's exit status and error output on cleanup, so a
network failure, a forge refusal and a structural check failure all looked
the same and left nothing to diagnose once the next good read cleared it.

The record's error and the unavailable line now name the first failing step:
the read's label, exit status, budget seconds left and the first line of its
error output, or the structural check that refused the observation. Budget
refusal stays silent and unmeasured as before.

* no-mistakes(review): Label closing head read's structural failure as its own cause

* no-mistakes(document): Note structural refusals record contribution errors too
…unchenguid#6365)

* fix(spawn): give Orca workers an opt-in scoped environment source

Fixes kunchenguid#6339

On the Orca backend, `config/launch-env-allowlist` expands each allowlisted name in the destination pane, and that pane is an Orca terminal whose environment comes from the Orca app and the user's shell startup files.
Allowlisted credentials therefore resolve to the user's personal values or to nothing, with no warning, while the same allowlist scopes tmux workers correctly.

Add an opt-in `config/launch-env-tmux-session` naming one tmux session.
On a fresh Orca spawn, before the worktree or terminal is allocated, fm-spawn reads each allowlisted name from that session's local environment with `tmux -u show-environment -t =<session> <name>`.
The worker launches with the existing `env -i` boundary and operational floor, the snapshotted values in place of destination expansion, and `GIT_CONFIG_NOSYSTEM=1` so a system credential helper cannot answer for the worker.
The source requires an allowlist, so tmux's automatic update-environment names such as `SSH_AUTH_SOCK` are never forwarded unless listed.
A listed name missing from the session, an unreadable session, or a malformed selector refuses before allocation; an explicit `-NAME` record stays a deliberate unset.
Values are shell-quoted only in the existing owner-only staged launch file, never terminal input, and the pane deletes that file as sourcing starts; a deletion failure refuses the worker launch and keeps the interactive shell.
`-u` is required because tmux's non-UTF-8 client output replaces newlines and non-ASCII bytes in values with underscores.

The source is opt-in and nothing changes for homes that do not create the file.
When `config/backend` is orca, the allowlist is nonempty, and no source is configured, fm-spawn now prints a stderr warning that allowlisted names resolve from the Orca terminal, so the mismatch is no longer silent.
It warns rather than refuses because refusing would stop every existing Orca home that uses an allowlist, which the triage asked not to assume.
Other backends, relaunch (already refused on Orca), and secondmate inheritance are unchanged; the selector is not in the inherited configuration list.

The scoped Git credential helper case also needs kunchenguid#6341, which stops fm-spawn's `core.hooksPath` entry from overwriting inherited `GIT_CONFIG_*` entries; this change does not depend on it.

Tests in `tests/fm-spawn-orca-worktree.test.sh`:

- A real-tmux regression on a private socket checks exact multiline UTF-8 values under absent and C locales through sh, Bash, and zsh, refusal of names that exist only server-globally or only inside another value, and exclusion of the automatic `SSH_AUTH_SOCK`.
- Fake-Orca spawns execute the real staged launch file under synthetic personal credentials and cover the configured source, an empty allowlist, refusal before allocation for each invalid setting, file privacy and deletion before the worker runs, and the deletion-failure path.
- The warning appears for a nonempty allowlist without a source and not for an empty allowlist or a configured source.

Risk: the source keeps tmux and the named session as dependencies of Orca spawns, and secret values sit in a 0600 file under a 0700 directory until the pane sources it.
It is not a filesystem sandbox; a worker can still read the user's credential files unless the harness restricts that.
The hardened path was verified with the regressions above, not with a live Orca spawn.

* no-mistakes(review): Harden Orca launch-file cleanup, floor unsets, and stat portability

* no-mistakes(review): Scope Orca abort cleanup to the staged launch file

* docs: scope Orca source to fresh spawns and qualify pane values

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

* docs: state that Orca relaunch is refused

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

* no-mistakes: apply CI fixes

* no-mistakes(review): Keep Orca launch file while delivered terminal stays open

* no-mistakes(document): Qualify fm-spawn help's destination-pane expansion claim for Orca

* fix(spawn): keep launch-send failures fatal and compare tmux bytes per version

The Orca launch-file cleanup wrapped both launch sends in a condition, which runs outside errexit.
A failed send on tmux and every other non-Orca backend then continued as success, so a post-publication relaunch failure no longer failed closed.
Each send failure now exits again, and Orca source mode removes its staged file first.

The real-tmux regression asserted that invalid UTF-8 bytes survive tmux itself, which holds on tmux 3.6a but not on the CI runner's tmux.
It now asserts that the snapshot reproduces exactly the bytes the installed tmux reports for that name.

* fix(spawn): reclaim an Orca launch file the pane never runs

In Orca source mode the pane unlinks the credential-bearing launch file as it starts sourcing it.
A pane that accepted the source command but never ran it left the file on disk until teardown, because the abort cleanup keeps the file for a delivered, still-open terminal.
After delivery, fm-spawn now waits up to 30 seconds for the pane to consume the file; if it has not, fm-spawn removes it and fails the spawn with a status line naming the unrun command.

The snapshot already reads and writes tmux output as latin1, so bytes that are not valid UTF-8 reach the launch file unchanged.
A deterministic fake-tmux regression now pins that, independent of how the installed tmux stores such values.

The live-terminal test now drives a slow pane and fails unless the spawn waits for it, and the controlled-source test reads the file the fake pane consumed.

* no-mistakes(review): Keep Orca terminal open when launch file is unconsumed

* no-mistakes(review): Refuse Orca worker when launch file already reclaimed

* no-mistakes(review): Make Orca spawn's deadline launch-file reclaim exclusive

* no-mistakes(document): Document Orca launch-file reclaim keeping the terminal open

* fix(spawn): reclaim an interrupted Orca launch file and keep errexit panes open

An HUP, INT or TERM after the source command reached the Orca terminal left
the credential-bearing launch file on disk: bash ran the EXIT trap with status
0 and abort cleanup kept a delivered file for the pane. Source mode now traps
those signals until the pane consumes the file, and abort cleanup reclaims it
with the same non-forced unlink arbitration as the consume deadline, refusing
the worker when the spawn wins and leaving the terminal open.

The delivered source line now keeps a pane shell with errexit set open when
the launch is refused, including when the file was already reclaimed.

* no-mistakes(review): Trap signals as soon as Orca launch file is published

* no-mistakes(document): Record pre-send interrupt coverage in Orca verification notes

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…re, including absolute paths (kunchenguid#7307)

* fix(composer): treat OpenCode >= 1.18 right-aligned cwd:branch as furniture

OpenCode 1.18.31 draws the worker pane's working directory and branch
right-aligned inside the left-bar composer rows above the Build footer,
wrapped over up to three rail-width fragments whose final fragment
continues onto the footer row's right end. The fragments are RGB 128
grey with luminance exactly on the ghost-luma ceiling, so they survive
ghost stripping, and the left-bar classifier read them as unsent typed
text: every idle 1.18.31 pane answered pending, which skipped the
steering doorbell, the watcher's re-ring ladder, and /exit typing.

_fm_composer_leftbar_cwd_start recognises the run structurally and
position-gated: contiguous single-token rows immediately above the
footer row whose gap after the bar exceeds half the row, whose
fragments plus the footer row's gap-separated right-hand tail
concatenate to a path, optionally followed by :<branch>. A path-shaped
string typed at the composer's left edge is still typed text, so a
wrong call can only defer, never inject. Extraction shares the same
rule so extracted composer content stays user-scoped.

* no-mistakes(review): drop dead footer-tail concat, add live short-cwd fixture

* no-mistakes(review): accept only ~/ cwd rail; lone slash stays pending

* no-mistakes(review): require footer row for leftbar cwd furniture walk

* no-mistakes(review): Require topmost cwd fragment to carry ~/ prefix

* no-mistakes(document): Correct cwd furniture recognition doc to topmost-fragment check

* fix(composer): gate cwd rail on region width; correct version story

Review round on the independent PR review: the position gate now measures
the gap against the region's width (the widest trimmed plain row across
first..last), so a short space-prefixed home path typed into an otherwise
empty composer no longer reads empty; the right-half lone-token residual
is pinned explicitly. The cwd:branch furniture is documented as present
since OpenCode 1.18.0 (upstream anomalyco/opencode#36457, merged
2026-07-11), not a 1.18.31 layout change. The 1.18.30 capture comment
notes it omits the directory footer, and a cropped-composer extraction
assertion scopes user content to the typed row above the rail.

* no-mistakes(document): correct OpenCode cwd-furniture version framing and doc typos

* fix(composer): accept an absolute cwd rail outside $HOME as OpenCode furniture

Refs kunchenguid#4632, builds on kunchenguid#5030.

* no-mistakes(review): Harden live abs-cwd check waits and qualify rail comment

* no-mistakes(review): Fix composer-lib relative path in OpenCode harness doc

* no-mistakes(test): Relax live check's DONE wait for wide OpenCode panes

* no-mistakes(document): Separate absolute-cwd verification output block from following prose

---------

Co-authored-by: Konrad Heimel <konrad.heimel@corp.ovh.com>
…mpt (kunchenguid#6682)

* fix: name the Claude exit picker on existing stale wakes

A pane parked on that picker still read as healthy quiet after Enter was refused. The existing stale and wedge reports now carry the dialog name, including through away-mode escalation.

Refs kunchenguid#4754

* no-mistakes(review): fix: report exit picker on terminal-status away stales

* no-mistakes(document): docs: describe exit-picker reporting in stale supervision

* no-mistakes(review): fix: drop away stale marker when second-mate picker closes

* fix: keep the exit-picker name off paths main does not already run

A second mate stays exempt from stale capture, and a finished worker in away mode no longer ages into a wedge. The name remains on the stale and wedge wakes a worker already raises.

* no-mistakes(review): name the exit picker on wedge deferral reasons

* no-mistakes(review): limit exit-picker name to plain stale and wedge escalation
…uid#7308)

* feat(afk): Spend-Obergrenze beim Eintritt aus der lebenden Worker-Zahl

Ohne ausdrückliches --spend nimmt der Eintritt in den Away-Modus die Zahl der
gerade im Home stehenden gewöhnlichen Worker als Obergrenze, damit der Modus
laufende Parallelität nicht einengt. Der bisherige Default bleibt Untergrenze,
0 bleibt ausgeschlossen, und ein ausdrückliches --spend gewinnt weiterhin.

Die Zählung bekommt mit `live-workers` genau eine Implementierung in
bin/fm-afk-contract.sh. bin/fm-spawn.sh liest sie dort statt mit einer eigenen
Schleife, sodass Eintritt und spätere Sperre nicht auseinanderlaufen können.

* no-mistakes(document): Remove misplaced afk spend-cap plan

---------

Co-authored-by: ladwein <ladwein@firstmate.bost8.thelad.loc>
…ion paths (kunchenguid#6965)

* fix(bin): guide supervision through completed-scout cleanup

* no-mistakes(document): Document completed-scout cleanup ownership and regression coverage

* no-mistakes(test): Allow confined lab overrides and verify live scout cleanup

* no-mistakes(test): Clarify per-task cleanup reporting and verify live outcomes
…o the job's timeout (kunchenguid#7240)

* fix: recheck a worker paused on its own background job

A worker that declares `paused:` on its own background job and then misses the job's completion stays idle until the long declared-wait recheck, because the first-sight alert spends the declaration's throttle and nothing else names when the job must have ended.
The worker pause instructions now say that `until` for such a job is its start plus its timeout, and the watcher already rechecks once when that time passes.
The recovery playbook now tells the supervisor to verify the condition a declared wait names before reporting the wait as healthy.

Fixes kunchenguid#7239

* test: give the background-job until test headroom over its quiet round

The first absorb round of the until test may take four poll cycles of up to 30 s each on a loaded runner.
With `until` only 15 s ahead, that time could pass during the round and fail the test although the watcher behaves correctly.
Set `until` 120 s ahead so the window covers those four cycles, as CONTRIBUTING asks of fixture windows.

* chore: retrigger CI

* chore: retrigger CI

* no-mistakes(review): Rebuild bg-job until test with passed until, no sleep

* no-mistakes(review): Restore bg-job absorb checks and assert due recheck marker

---------

Co-authored-by: pedromuller-del <254459182+pedromuller-del@users.noreply.github.com>
…dapter (kunchenguid#6679)

* feat: add Command Code crewmate and scout adapter

Add `commandcode` as a verified worker runtime for crewmate and scout work
only, so Firstmate can dispatch workers on Command Code models such as
deepseek/deepseek-v4.1-flash.

- Detection: the anchored `command-code` process title identifies the
  adapter in fm-harness.sh and the shared liveness classifier.
- Launch: fm-spawn passes --yolo, --trust, --skip-onboarding, the model, an
  effort only where the model's accepted set is verified, and the typed
  launch brief after --; taste-learning state is kept out of git.
- Busy state: a tracked mod, bin/fm-commandcode-mod.ts, is loaded per
  process with --mod and --mod-option, writing run_start/run_end/session
  end through fm-busy-event.sh; no user or project settings are edited
  (Command Code's --config persists globally, so it is never used).
- Control: one Escape interrupts; the idle double-Escape Rewind picker is a
  recognized hazard that exit refuses and interrupt dismisses; /quit exits.
- Composer: the bright truecolor placeholder is read with an
  identity-scoped ghost ceiling, and the tmux adapter supplies the identity
  for a cursor parked below the footer; Herdr reports it natively as `cmd`.
- Attribution: the fleet commit-msg strip removes the CommandCodeBot
  trailer.
- Docs, harness reference, verification record, portable regressions, and
  the FM_COMMANDCODE_SIGNALS_LIVE live guard.

* no-mistakes(review): Drop flash-fast effort pairs; correct identity-probe cost comments

* no-mistakes(ci): I fixed both Greptile findings the way you asked. Both new regression tests fail on the old code and pass now. The changes are not committed. **ci-1: Command Code on zellij, cmux and orca.** The rule that must hold is that a Command Code worker only runs on a backend that tells Firstmate the pane is Command Code. Without that, its idle placeholder never reads as an empty composer, so steering stalls. `bin/fm-spawn.sh` now refuses `--harness commandcode` unless the backend is tmux or herdr. The error reads: `commandcode is verified on the tmux and herdr backends only; backend=<b> cannot prove its composer empty. Select backend tmux or herdr.` - The check runs after both the harness and the backend are known, so it covers fresh spawns and `--relaunch`. - It runs before any pane, worktree, task record or busy state is created. - New test in `tests/fm-commandcode-harness.test.sh`: tries a scout spawn on each of zellij, cmux and orca (with a fake `orca` that reports ready). It checks for the refusal and that nothing was launched, no task record was written and busy state was not armed. It needs no real backend installed. - Documented in `docs/configuration.md`, the Command Code adapter reference and the `fm-spawn.sh` header. **ci-2: an old process signalling its replacement.** The rule is that the `turn-ended` notification may only be raised under the same generation check as the idle write. The mod used to touch the file itself after the writer returned, which left a gap for a relaunch to slip in. - `bin/fm-busy-event.sh apply` takes a new `--turn-ended` flag. It touches `state/<id>.turn-ended` while still holding the writer lock, after the generation check passes. A stale generation is refused with no touch, and the flag is rejected on any subcommand other than `apply`. - `bin/fm-commandcode-mod.ts` no longer touches files itself. On `run_end` it passes `--turn-ended` unless the turn was interrupted. - New test in `tests/fm-commandcode-harness.test.sh`: a fake writer accepts the idle write and then arms a replacement before the mod continues. It asserts no notification appears. - New test in `tests/fm-busy-state.test.sh`: the notification is raised for the current generation, not for a stale one, and not outside `apply`. **Checks run:** - Passing: `fm-commandcode-harness`, `fm-busy-state`, `fm-busy-adapter-wiring`, `fm-backend`, `fm-documentation-audiences`, and `bash -n` on the changed shell files. - `fm-bootstrap.test.sh` fails one case ("backend=herdr must fail closed on its own missing session CLI"). It fails the same way on an untouched copy of the branch, because herdr is installed at `/usr/bin/herdr` on this machine. - `bin/fm-lint.sh` could not run because ShellCheck 0.11.0 is not on PATH, so the changed shell files have not been linted. **Not changed:** Devin's hook does the same thing outside this PR: it runs `apply ... && touch` in `bin/fm-devin-config.sh`. That predates this PR and is not in the changed area, so I left it alone

* no-mistakes(ci): I fixed ci-sc2016. ShellCheck is not installed on this machine, so I could not confirm that the lint warning is gone. **The rule that was broken:** the repo lints with ShellCheck at its default severity, which also fails on info-level notes. Any single-quoted `$` that is meant to stay literal needs a scoped disable comment that says why. Only one place in the changed area needed this: the racing-writer `printf` in `tests/fm-commandcode-harness.test.sh`. Its format string writes `"$2" "$3"` on purpose, so the generated fake writer reads its own arguments when it runs. **The fix:** I added one line just above that `printf`: `# shellcheck disable=SC2016 # positional params expand in the generated writer.` The repo already does this the same way, for example in `tests/fm-afk-launch.test.sh` and `tests/fm-ask-user-authority.test.sh`. Nothing else changed, so the test behaves exactly as before. **Checks run:** - `bash -n tests/fm-commandcode-harness.test.sh` passes. - `bash tests/fm-commandcode-harness.test.sh` passes all six cases, including the busy-mod case with the racing writer. **Not run:** `bin/fm-lint.sh` stops with "ShellCheck not found" because ShellCheck 0.11.0 is not on PATH and there is no copy in the repo. I did not install it, since installs outside the worktree are off limits here. The upstream lint run will be the first ShellCheck pass on this change. The change is not committed

* no-mistakes(review): Hard-code Command Code ghost luma ceiling at 160

* no-mistakes(review): Test commandcode relaunch refusal off tmux/herdr before stop

* no-mistakes(document): Note Command Code fixed ghost luma ceiling in config docs
Claude spills a hook output over 10,000 characters and shows only the first
2,000 plus a saved-file path. The re-emit prints captain.md and learnings.md
last, so they never reached the session. Print a short cue first that names
the saved file and the memory files, and document the cap.

Refs kunchenguid#6303
…kunchenguid#7302)

* fix: retry transient contribution reads once

* fix: classify contribution read errors safely

Address review findings for kunchenguid#7262 by parsing only gh status forms, recognizing real DNS errors, refusing GraphQL not-found retries, clearing per-URL diagnostics, and restoring exit-137 handling.

* fix: recognize bare gh HTTP errors

Complete kunchenguid#7262 review fixes by accepting bodyless gh status lines and keeping all GraphQL errors out of transient network classification.

* no-mistakes(review): Classify gh network errors by Go error tokens

* no-mistakes(document): Note why contribution 404 reads are never retried

* no-mistakes(review): Retry message-less GraphQL HTTP 5xx on closing read

* fix: retry transient contribution reads once

* fix: classify contribution read errors safely

Address review findings for kunchenguid#7262 by parsing only gh status forms, recognizing real DNS errors, refusing GraphQL not-found retries, clearing per-URL diagnostics, and restoring exit-137 handling.

* fix: recognize bare gh HTTP errors

Complete kunchenguid#7262 review fixes by accepting bodyless gh status lines and keeping all GraphQL errors out of transient network classification.

* no-mistakes(review): Classify gh network errors by Go error tokens

* no-mistakes(document): Note why contribution 404 reads are never retried

* no-mistakes(review): Retry message-less GraphQL HTTP 5xx on closing read
…tion (kunchenguid#7338)

* fix(bin): let a backlog captain hold bound the watcher's wedge escalation

A stuck worker whose task was held for the captain through
bin/fm-captain-hold.sh kept wedge-escalating every STALE_ESCALATE_SECS,
because wedge_wait_evidence only read the status line. It now also reads
the open backlog captain call (task_captain_call_open) and defers to the
existing captain-facing long recheck, silent while the away record exists.

* no-mistakes(review): Publish no wait age for backlog captain holds

* no-mistakes(review): Let an elapsed paused-until line fall through to backlog hold

* no-mistakes(document): Document backlog captain hold in wedge wait-evidence contract
…guid#5404)

* fix(bin): parse Lavish list-form results and refuse unparsed blocks

Lavish emits its queued prompts in TOON list form instead of table form as
soon as the items stop being uniform, for example when some annotations carry
a nested target object or a message carries an attachments table. The adapter
only recognized the table header, so read reported a complete, empty result
and answers closed nothing.

Move the result parsing into one shared reader used by read, answers, and
reconciles that accepts both forms, keeps only column-zero headers, skips
nested structures only beneath the field that opened them, and counts
anything it cannot account for as malformed. An unrecognized header naming
the block is now malformed rather than an empty result, answers and
reconciles exit nonzero when the result is not fully accounted for, and the
queued-content check treats a list header as content.

Refs kunchenguid#4726

* no-mistakes(document): Document Lavish capture formats and incomplete-result handling

* test: expect captain_message_count for the open-board list-form capture
…uid#5835)

* fix(pi): let the supervision branch claim Orca stale wakes

The watcher keys Orca endpoints by terminal=, but the branch dispatcher
only indexed window=, so every Orca stale row vetoed the wake scan and
went to main. Index both endpoint fields.

Local hotfix ahead of upstream; refs kunchenguid#5623.

* test(pi): cover terminal-keyed Orca stale rows in branch dispatch

Pins the terminal= endpoint lookup: a terminal-keyed stale row queued with
another eligible row must resolve to its task in both routine and heartbeat
scans, instead of vetoing the wake to main.

* no-mistakes(document): Update supervision dispatch doc for Orca terminal-keyed stale rows

* fix(pi): reject conflicting wake endpoint ownership

Index task keys and window/terminal aliases through one ownership check.
Ambiguous records must leave the wake on MAIN instead of selecting the
last task and possibly bypassing another owner's open decision.

Cover duplicate terminals and collisions with window and task keys in
both record orders while preserving unique terminal routing.

* no-mistakes(document): Document conflicting alias ownership as scope corruption

* fix(pi): retain ownership claims from incomplete task records

Check endpoint and task alias ownership before project eligibility so a
projectless record cannot lose its open decision to another terminal owner.
Require project context before decision filtering can grant routing scope.

Cover missing and empty project fields in both record orders, and preserve
refusal for a uniquely mapped terminal without project context.

* no-mistakes(review): Fix canonical routing ownership and custom SDK event handling

* no-mistakes(review): Preserve dedicated custom processing run ownership

* no-mistakes(document): Clarify Pi event handling and fail-closed routing documentation
…anches (kunchenguid#7368)

* feat(bin): add a forge=external review rail that never pushes task branches

A project whose review tool uploads its own snapshot of the worker's local
commits can now be registered [direct-PR forge=external]. Workers on such a
project never need to push a branch to publish, and must never push a task
branch to the shared origin.

- fm-project-mode.sh: external joins the closed forge set; never detected,
  refused on local-only, and +yolo reports yolo=off with a refusal.
- fm-dod-lib.sh: external ships direct-PR only; its first rule forbids origin
  and task-prefixed pushes, its Definition of done renders the home's
  config/external-review.md adapter verbatim, and the ready-report gate
  accepts `done: review <url> uploaded for review` on the URL alone or, with a
  recorded backup remote, once HEAD is on the backup namespace.
- fm-git-push-guard.sh (new): per-task pre-push guard refusing pushes to
  origin or a URL matching origin's, task-branch and task-prefixed refs on any
  remote, and refs outside the backup prefix on the backup remote.
- fm-git-strip-ai-trailers.sh: install gains --push-guard and --no-strip.
- fm-spawn.sh: refuses yolo, a missing adapter, and an unusable backup
  remote before launch; installs the guard even under keep-ai-trailers; and
  records forge=external, backup_remote=, and backup_prefix=.
- fm-review-check.sh (new): records review= and review_head= with no merge
  watch, and publishes the child's ready line in a secondmate home.
- fm-promote.sh refuses promotion onto forge=external, because a scout
  launches without the guard.
- Docs: docs/external-review-forge.md (design), configuration.md's new
  "External review tool" section, scripts.md, and the ship-landing and
  project-management skills.

* no-mistakes(review): Drop string-grep hook assertion from external-review spawn test

* no-mistakes(document): Note forge=external contract token and direct-PR-only composition
…uid#7387)

Ein Lauf, den sendCustomMessage startet, traegt eine Custom-Nachricht und
keinen prompt. Das Lesen des prompt an dieser Stelle warf, womit jeder
solche Lauf mit einem Fehler auftauchte statt zu starten.

Die Verengung dagegen steht seit 8c9c67d im Zweig, ein Test, der sie
benennt, fehlte: die einzige Abdeckung lag beilaeufig in einem
Abbruch-Fall der Abwesenheits-Haltung und verschwindet mit ihm. Der neue
Fall erweitert den bestehenden Mirror-Test und prueft, dass ein solcher
Lauf weder wirft noch als Dialog gespiegelt wird.

Gegenprobe: ohne die Verengung wirft genau dieser Fall mit
"Cannot read properties of undefined (reading 'trim')".

Co-authored-by: thelad-dev <ladwein@firstmate.bost8.thelad.loc>
Brings the fork up to date with upstream main while keeping the per-home
Herdr crew workspace and the guarded settings inspection tool.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T14:46:48.999581Z fad9773 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

done < <(fm_backend_herdr_cli "$session" workspace list 2>/dev/null | jq -r --arg want "$(fm_backend_herdr_crew_workspace_label)" \
'.result.workspaces[]? | select(.label == $want) | .workspace_id' 2>/dev/null)

P2 Badge Restrict crew discovery to this home's recorded workspaces

When two FM_HOME instances share a Herdr session, their crew workspaces can have the same label (for example, separate primary homes both use firstmate-crew). This loop scans every workspace with that label rather than the IDs recorded in the current home's state, so fm_backend_herdr_list_live reports another home's panes as local recovery/orphan entries. Enumerate the IDs returned by fm_backend_herdr_crew_workspace_recorded instead.

AGENTS.md reference: AGENTS.md:L59-L60


firstmate/bin/fm-secrets.py

Lines 487 to 491 in fad9773

for value, replacement in scrubbers:
for split in range(1, len(value)):
if stdout.endswith(value[:split]) and stderr.startswith(value[split:]):
stdout = stdout[:-split] + replacement
stderr = stderr[len(value) - split :]

P2 Badge Scrub secrets split from stderr into stdout

When a child writes a credential prefix to stderr and its suffix to stdout, both fragments survive because this checks only the opposite stdout-prefix/stderr-suffix ordering. For example, splitting abcdef as stderr abc and stdout def exposes every credential byte across the returned streams despite run's scrubbing guarantee; add the symmetric boundary check and regression coverage.


stdout_bytes, stderr_bytes = child.communicate()

P2 Badge Bound child output retained before redaction

When run wraps a verbose or long-running command, communicate() retains complete stdout and stderr in memory and emits nothing until the child exits. Since the public interface accepts an arbitrary command, a build, test, or log-producing process can exhaust the wrapper's memory and lose the scrubbed output; stream through a redacting buffer or spool the streams with a bounded in-memory overlap.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Profreshor Profreshor closed this Oct 16, 2026
@Profreshor Profreshor reopened this Oct 16, 2026
Resolve AGENTS.md by keeping upstream's version, which moved the home
layout listing into the operational-home-layout skill, and carry the
config/crew-memory-max entry into that skill's listing instead.
Resolve bin/fm-spawn.sh by keeping both the upstream and fork header
and config-read blocks, and keeping only the systemd-run memory wrap
where upstream moved the launch-home-token block earlier.
@Profreshor

Copy link
Copy Markdown
Owner Author

Re Codex finding "Restrict crew discovery to this home's recorded workspaces" (bin/backends/herdr.sh): this crew discovery loop is pre-existing code on fork main, unchanged by this upstream sync, so it stays as is in this PR. It is tracked as a follow-up.

@Profreshor

Copy link
Copy Markdown
Owner Author

Re Codex finding "Scrub secrets split from stderr into stdout" (bin/fm-secrets.py): the run scrubbing code is pre-existing on fork main, unchanged by this upstream sync, so it stays as is in this PR. It is tracked as a follow-up.

@Profreshor

Copy link
Copy Markdown
Owner Author

Re Codex finding "Bound child output retained before redaction" (bin/fm-secrets.py): the run output buffering is pre-existing on fork main, unchanged by this upstream sync, so it stays as is in this PR. It is tracked as a follow-up.

@Profreshor
Profreshor merged commit 0f8cb48 into main Oct 16, 2026
19 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.