feat(bin): land upstream fleet subsystems and Codex max reasoning effort - #20
Open
BohnBawerick wants to merge 108 commits into
Open
BohnBawerick wants to merge 108 commits into
BohnBawerick wants to merge 108 commits into
Conversation
…enguid#2707) * fix(bearings): always show decision options and a close/drop control Freeform-only Captain's Call cards hid the option buttons the board was designed around, and there was no way to drop a stale hold without inventing an answer. Require selectable options, keep freeform as a supplement, and route the reserved __drop__ answer through decline so the hold leaves Captain's Call. * no-mistakes(review): Fix drop closure and decision-only option validation * no-mistakes(review): Preserve answerability for non-decision cards * no-mistakes(document): Clarify decision drop documentation
Signature-only PRs can hide skipped review, test, or document steps. Fail unless no-mistakes >= 1.46.0 attests those three steps completed.
…#2728) * feat(captain-hold): collapse the decisions concept into tasks held for the captain A decision is no longer a separate type: it is an ordinary backlog task held for the captain, identified by its task id. bin/fm-captain-hold.sh owns the surviving behaviors - guarded hold creation, the recorded-answer close (answer/answers with a release mode for captain-gated work), the source bindings, and the investigation completion gate - and bin/fm-decision-hold.sh becomes a one-release compatibility shim over it. The fleet snapshot now parses hold-until and computes captain_actionable as queued + captain-held + unblocked + due, independent of row kind, plus a presentation-only deferred_marker for prose-deferred rows. Bearings renders every due captain-held task in Captain's Call, date-deferred holds as dated Charted Next gates, suppresses prose-deferred rows from default views with an omitted disclosure, and excludes from Recently Landed anything that closed while still held for the captain. Legacy compatibility: pre-collapse <origin>-decision-<key> rows are already plain task ids and keep working; short keys in recorded metadata, concrete origin bindings, chat --resolve-key fallbacks, and old resolution records all resolve in place. * no-mistakes(review): Fix captain answer replay and body preservation * no-mistakes(review): Fix captain hold idempotency and legacy replay * no-mistakes(review): Validate card close modes and compatibility routing * no-mistakes(review): Enforce release replay mode matching * no-mistakes(review): Prevent duplicate decision cards and released replay mismatches * no-mistakes(review): Preserve answer columns and legacy resolve replays * no-mistakes(document): Document strict replay and legacy compatibility * no-mistakes(lint): Quote done literals to satisfy ShellCheck * no-mistakes: apply CI fixes * fix(rebase): keep collapsed captain hold board semantics
…id#2733) * fix(watch): announce recovery once per generation and keep successors supervising A lost Pi/OpenCode handling handshake re-announced the same recovery generation on every cycle and spent the successor's first ~55s blind, so a real crew event could be ignored and then dropped. Record the announcement in the durable marker, confirm the handshake before the follow-up without swallowing failure, and enter the poll loop immediately. * no-mistakes(review): Tighten recovery event timing regression * no-mistakes(document): Document recovery-loop supervision guarantees
* fix(bin): signal a captain call resolved in the log but still held A captain call has two records and closing one has never closed the other: a `resolved [key=...]` line closes the status-log fold, while the backlog task held for the captain closes only through `fm-captain-hold.sh answer`. Answering on the status side alone left no trace of the disagreement - the fold went quiet, the durable record kept saying the captain owed an answer, and nothing warned. The defect was never the separation; it was the silence. Add `fm-captain-hold.sh diverged`, a read-only report of that contradiction, and print it from `fm-wake-drain.sh` as a bounded RECORD DIVERGENCE section beside OPEN DECISIONS on every drain. It flags one condition: a task still open and still carrying the captain-hold annotations whose key was closed on the status side by the resolve verb, under the collapsed identity or the legacy derived one. It closes nothing, ever. A captain call closed wrongly leaves review entirely, which is worse than the noise, so both reconciliation directions stay human-owned and the printed hint names both - a resolution is not proof the captain ruled, since a call can dissolve on a false premise or turn out to have been a question of fact. Three states are deliberately not divergence: a `captain-held` close is the verified transfer `complete` writes, a still-open keyed decision belongs to the OPEN DECISIONS fold, and a captain call with no routed work item is legitimate rather than incomplete, so routed work is no part of the test. `fm-classify-lib.sh` gains `status_key_closing_verb`, which reports how the status side currently reads one key by replaying the existing `_fm_decision_fold_line` rule rather than re-deriving it, so the two closing verbs stay distinguishable in one place. The per-wake cost is one `tasks-axi list`, one key scan per status log, and the precise per-key fold only for a key that already names a still-open task; the call is hard-bounded so a slow backlog tool can never delay wake presentation. * fix(document): Correct divergence lifecycle documentation * fix(document): Neutralize divergence lifecycle prose
…escalation while a worktree is written (kunchenguid#2524) * fix(watch): re-arm supervision after an abandoned auto-arm claim A Claude auto-arm cycle that armed, delivered one rewake, and exited left its single-flight lock behind. Both Stop-event participants then deferred to that lock forever, because its recorded pid was still live: the turn-end guard read it as recovery under way and allowed the stop, and the next Stop firing treated it as another owner and declined to arm. On 2026-08-14 a home with two tasks in flight lost supervision for about 40 minutes with no watcher process and no watcher lock, its beacon frozen at the one delivery, and both crewmates' finished reports sat in the durable queue until an operator drained it by hand. Abandonment is now proven from the epoch ledger instead of inferred from pid liveness. A lock whose holder pid matches the ledger's own owner_pid while the recorded outcome is anything other than arming has already finished its decision, so that claim is reclaimed under the lock's steal mutex, stops counting as recovery ownership in the guard, and is cleared by the guard's terminal check rather than deferred to. A failed clear re-blocks instead of allowing a blind stop, and an arming entry stays in flight however old it is, because its owner foregrounds the arm for the whole watcher cycle. Issue kunchenguid#2251's PR kunchenguid#2263 does not cover this failure. It is closed and unmerged, lives entirely in bin/fm-watch-arm.sh, and retires the stalled watcher and matching stale watcher lock of an arm that is currently running. Here no arm and no watcher were running and no watcher lock existed, so it has nothing to retire and the home stays blind. tests/fm-claude-stop-autoarm.test.sh covers the reclaim, the still-arming and unnamed-owner cases that must keep the gate closed, and the failed clear. tests/fm-turnend-guard.test.sh covers the guard side of the same boundary. Both fail without this change. * fix(watch): defer a wedge escalation while the task worktree is written The wedge detector had two inputs, rendered pane quietness and the run step, and neither can see a crew that is writing source, then tests, then documentation behind a static pane. On 2026-08-14 one crewmate produced eight consecutive possible-wedge escalations in a single afternoon, three of them demanding deep inspection, while it was demonstrably working and then committed. Every one of them cost a supervision turn to disprove by hand. Add write activity inside the crew's own recorded worktree as a third liveness input. crew_worktree_written_since compares the worktree against the caller's existing idle-window timer file, so -newer needs no clock arithmetic, no temp file, and no portable mtime write. The probe runs only inside the branch that was about to escalate, which bounds it to one pruned, depth-bounded walk per window per FM_STALE_ESCALATE_SECS and leaves the per-poll stale sweep exactly as cheap as before. Positive evidence defers rather than cancels. The idle timer restarts so the next window probes again, the escalation counter is neither advanced nor reset so a later genuine wedge keeps the demand-deep-inspection history it earned, and a .writing-since marker ages the whole deferral chain so the pane still re-surfaces once per FM_PAUSE_RESURFACE_SECS, through the same throttle shape a declared pause already uses, labeled as a recheck rather than a wedge. This can only reduce false positives: every absence of evidence, including no recorded worktree, a torn-down worktree, a missing anchor, and a failed walk, falls through to the unchanged escalation schedule, so a crew that writes nothing still escalates on the existing timetable. What the signal cannot see, by design or by construction: - CPU burn with no writes, such as a long compaction, is invisible. That case keeps the old behavior exactly. - A commit-only phase writes only .git, which is pruned first so that firstmate's own read-only git commands against the worktree can never make the probe self-fulfilling. - Writes under the pruned generated trees, or deeper than FM_WORKTREE_WRITE_MAXDEPTH, do not count. - The probe cannot attribute a write to the crew, so a background build or another process touching the tree looks the same. The hourly re-surface is what bounds that, and a churny file cannot buy silence. - The away-mode daemon's own escalation path is deliberately untouched. tests/fm-watch-triage.test.sh covers the classifier including the .git prune, both halves of the live case on one fixture (quiet plus writing defers, quiet plus silent still escalates and counts), and the bounded re-surface. All three fail without this change. * no-mistakes(review): prove autoarm claims by identity; skip mate-home write probe * no-mistakes(document): document away-mode wedge boundary and probe filesystem limit * no-mistakes(document): qualify turn-end recovery condition for abandoned auto-arm claims * fix(watch): keep a write deferral scoped to its own idle window Two consistency gaps in the worktree write probe, both found while reviewing the wedge-deferral change on this branch. A write deferral is a bounded chain: its .writing-since marker ages the whole chain so a churning worktree still re-surfaces once per resurface window. That is only sound while the chain belongs to the current quiet stretch, so every path that restarts the idle-window timer has to drop it too. Two did not: the corrupt-timer repair in wedge_timer_check, and both first-sight branches for a captain-relevant status. A chain left over from an earlier quiet stretch made the first deferral of the new window re-surface immediately instead of after a full fresh window. FM_WORKTREE_WRITE_PRUNE is a skip list, so clearing it reads as "skip nothing" and is the obvious way to widen the probe to the whole depth-bounded tree. Instead an empty list reported no evidence at all, quietly costing the wedge detector its third liveness input on a home that meant to widen the walk. An empty list now widens the walk, and the header says so. Neither change alters when a stall that writes nothing escalates. Regressions in tests/fm-watch-triage.test.sh cover all three paths and each one fails on the pre-fix code. * no-mistakes(review): honor an empty write-prune, bound the probe, share window_key * no-mistakes(document): align probe knob count and guard regression-coverage ownership * no-mistakes(lint): silence deliberate single-quote SC2016 in write-prune env test
…lared pause (kunchenguid#2748) * fix(bin): give a captain hold the same bounded pause cadence as a declared pause Two supervisors read a finished task's last status line and disagreed about which declarations mean an idle endpoint is expected. bin/fm-inactive-reconcile.sh suppresses its inactive-outcome scan only on `captain-held`, while the away-mode daemon's wedge path gated deferral on `paused` alone. Both read the LAST line, so the two verbs are mutually exclusive and no finished task waiting on a person could satisfy both at once. Marking 11 such tasks `captain-held:` silenced the 900s outcome scan and immediately produced five possible-wedge escalations in one batch, because the 240s wedge detector no longer saw a pause verb. fm-classify-lib.sh's status_is_paused_or_captain_held already owns the combined question, and bin/fm-watch.sh's ordinary-crew wedge path already asked it. This extends that same answer to the paths still asking the narrower one: - bin/fm-supervise-daemon.sh, all six sites, which form one subsystem and have to move together. classify_stale returns the pause action, reconcile_pause_tracking and migrate_watcher_pause_markers record and migrate the marker, and housekeeping defers the wedge and then re-surfaces the recheck. Changing only the stale-persistence gate would defer the escalation while reconcile_pause_tracking recorded nothing, so the wedge marker would persist and the sweep would `continue` past it forever: quiet, but never re-surfacing. - bin/fm-watch.sh's secondmate stale gate, whose downstream owner pause_state_class already treats both declarations identically. - bin/fm-push-transition-lib.sh's absorb, where either declaration already names the human the transition would report and the wait is already durably recorded. Quieting alone would be half a fix, so the bounded re-surface had to reach a hold too. A hold has no current-state mapping, unlike `paused`, so authoritative crew state reports it as unknown and pause_state_class received `none`. An ordinary crew recovers pause classification from that state through confirmed agent death, which proves no live decision gate is being silenced. A secondmate's endpoint liveness is deliberately never read there, because an idle mate is healthy by design, so that confirmation is unavailable by construction and cannot be required: without recovering the classification for a mate, every caller silenced a held mate outright and its hold would rot invisibly. That promotion is bounded by the declared-wait guard at the top of the function, so it can only reclassify a task that already declared a wait and shows no positive working evidence. Two narrow `status_is_paused` calls are deliberately left alone. bin/fm-crew-state.sh's map_log_state is a current-state reporting contract, not a wedge path; reporting a hold as `paused` would erase the distinction status_key_closing_verb and fm-captain-hold.sh depend on, where a `captain-held` close is a verified durable transfer and a `resolved` close claims outright settlement. fm-classify-lib.sh's call inside status_is_captain_relevant needs no change because that function's own case list already returns non-relevant for `captain-held`. bin/fm-inactive-reconcile.sh keeps its `captain-held` suppression as it is. Its guard exists because a finished task's crew state still reports done from a higher-priority source than the log, and a declared pause needs no such guard: the scan only reports done or failed, and nothing else reaches its record path. Widening it would change a separate subsystem's reporting contract, which this defect does not require. Coverage extends the existing colocated patterns for these predicates and asserts both halves. tests/fm-daemon.test.sh covers the classification, the wedge marker converting to pause tracking with no escalation, the bounded re-surface with its window reset, and the boundary case where an answered hold stops claiming the cadence. tests/fm-watch-triage.test.sh covers a held secondmate re-surfacing on the same bounded cadence without being labeled a wedge. tests/fm-supervision-events.test.sh covers the absorbed push transition. Every one of these fails on the pre-fix code except the answered-hold boundary case, which is there to pin that the quieting was not widened too far. The `paused:` workaround appended to those 11 tasks is live supervision state and is untouched here. It can be retired once this lands. * no-mistakes(review): name the captain in a held task's bounded recheck * no-mistakes(document): extend declared-wait supervision docs to captain-held holds
…guid#2758) * fix(lint): name the installer when ShellCheck or actionlint is missing A missing actionlint exited 127 like a bare command-not-found. Fail with exit 1 and point at the pinned installer, matching the missing-ShellCheck path, without weakening the version pin. * test: isolate kimi and muse detection from inherited Cursor markers Harness detection checks CURSOR_AGENT before ancestry, so these markerless-adapter cases failed when the suite itself ran under Cursor. Clear the verified markers the same way the secondmate harness tests already do. * no-mistakes(document): Document Muse Cursor marker cleanup
…lled but inert (kunchenguid#2684) * feat(checks): report tool updates that are available or installed but inert Firstmate had no way to notice that tooling this home depends on needs an update, and no way at all to notice the worse case: an update that installed correctly and then did nothing. That second case is why this exists. A tool that self-installs into ~/.local/bin while a version manager keeps its own older copy earlier on PATH looks completely up to date to anything that asks only "is a newer version published". On 2026-08-20 a Herdr update landed at 0.8.2 while an older 0.8.0 copy stayed earlier on PATH, so every Herdr command failed on a protocol mismatch and firstmate could not read its own fleet. bin/fm-tool-update-check.sh reports the two conditions separately: <tool> update available a newer version exists at the update source. <tool> update not in effect a newer copy is installed on this host, but PATH still resolves an older one. PATH skew is measured, never inferred. Every executable copy of a watched command on PATH is asked for its own version and those answers are compared, so one lookup cannot hide the skew, and a directory name is never read as a version because a version manager's "latest" directory can hold an older build. A copy that will not report a version is a check failure, not a pass. The watched tools live in local, gitignored config/watched-tools.json, so adding a tool is a config edit rather than a code change, and the file is never propagated to another home. Update sources cover both shapes: a local clone's commit distance from its remote branch, and a command's own version and update announcement, including a tool like no-mistakes that prints its version on one command and announces a new release on another. The check prints one line when something needs attention and prints nothing otherwise, so it rides the existing watcher state-check contract with its trust binding instead of introducing a schedule of its own, and state/.tool-updates keeps the same pending update from being reported on every poll. The check only reports. It never installs, updates, reorders PATH, touches a version manager, or fetches into a watched repository; every git probe is read-only. Tests cover the skew case as a regression, and it was verified by mutation: removing the skew report, or stopping after the first PATH hit as a single lookup would, each make that test fail. * no-mistakes(review): fix tool update check probe reporting, budget, and shim write * no-mistakes(review): keep sweeps alive on broken patterns and oversized budgets * no-mistakes(review): roll back failed arm, widen budget clamp, bound repo probe * no-mistakes(review): guard git probes at the budget, record uncut findings * no-mistakes(document): fix stale watched-tool report-record wording in docs and header * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes The behavior shard's watch-triage suite failed on the new worktree-write wedge tests. Those five tests are the only ones in the file that do not use its standard waits. They give a fixed 3 second liveness budget to the one poll that now spawns the bounded worktree walk, and 4 seconds to an escalating watcher where every other test in the file gives 10. On a loaded runner that poll outlives the fixed budget, so the round is reaped before the deferral it asserts on is recorded, and the test reports a lost deferral instead of the deferral under test. Wait for a completed poll cycle through the file's own wait_poll_cycle, which is what its header documents this hazard for, and use the file's standard 100 tick exit budget. Verified against a load that reproduces the failure: 11 of 12 runs failed before, 8 of 8 pass after. Verified by mutation too, so the waits still prove the behavior: removing the write deferral, and keeping a finished deferral chain across an idle-timer repair, each still fail their test.
* fix: treat yolo as merge authority only, not ask-user finding authority Yolo on/off was documented as also deciding no-mistakes ask-user findings, which hid firstmate's duty to judge unambiguous-toward-design findings itself. Keep every safety boundary; this is a contract clarification, not a relaxation. * no-mistakes(document): Clarify yolo documentation ownership and merge posture
…sk record Reads a project's registered "+hardened" annotation and carries it to the worker's instructions and the task's durable record, so the quality loop that bin/fm-quality.sh will drive has a posture and a fixed base commit to work from. That script is not part of this change; it is referenced by name only. - bin/fm-project-mode.sh: --quality prints one word, standard or hardened. The two-word stdout its three callers parse is untouched, so it gets its own output path. The bracket grammar is now position-tolerant: a "+"-prefixed token is a flag and never a mode, so "[+hardened local-only]" resolves the mode behind it instead of reading the flag as an unknown mode. Unrecognized flags are still ignored rather than refused. - bin/fm-brief.sh: --quality standard|hardened, defaulting to standard and refused on scout, dreamer, and secondmate scaffolds. A hardened brief records the sibling "Quality contract: quality=hardened" line and one short quality gate section; a standard brief records neither and stays byte-identical to the pre-quality scaffold. - bin/fm-spawn.sh: the brief's quality line must agree with --quality, the same check the delivery line already gets, in both directions. quality= and base_sha= land in the task record; the base commit is captured once at spawn and read back on relaunch, never recaptured, because the loop commits each round and a later capture would narrow the gate while still reporting success. - AGENTS.md: one sentence placing quality resolution at intake. Tests execute the real interfaces. The load-bearing ones prove a project without "+hardened" and a brief scaffolded without --quality behave exactly as before: the two-word stdout is pinned across every annotation form, the two scaffolds are compared byte for byte, and the task record's key set is pinned so only quality= and base_sha= are additive.
… work over (kunchenguid#2767) * feat(voice): spoken round trip on Nova Sonic 2 with a measured relay cost Step one of the spoken interface: the laptop captures and plays audio, this desktop holds the model session, and no AWS credential leaves the desktop. Measured, amazon.nova-2-sonic-v1:0 in eu-north-1, end of speech to first byte of reply audio, 6 runs each, all answered, on a question that forces a records read: relay path 1.229 1.379 1.428 1.447 1.481 1.516 median 1.438 direct 1.147 1.179 1.203 1.237 1.244 1.317 median 1.220 The relay costs about 0.22s of the median. The direct figure reproduces the earlier survey, which is what makes it a usable control. Excluded: the captain's own ssh round trip, microphone capture, and speaker output. This desktop has no microphone and no speaker, so every run used audio files. Three pieces: bin/fm-voice-relay.py holds the conversation on this host bin/fm_voice_records.py what a spoken answer may read, and the handover bin/fm-voice-client.py the laptop end; audio devices UNVERIFIED bin/fm_voice_frame.py the wire format both machines share Real work is handed to the existing bin/fm-inbox.sh rather than a second queueing surface, and the agent says it is handing over rather than answering as firstmate. Read scope: Done history and free-form note bodies are never assembled at any scope, so the wide default cannot reach the places commercial detail accumulates. config/voice-read-scope narrows it to counts only, and config/voice-read-deny excludes a named item in one line. The boundary is an executable test that widening the reader fails. Push to talk is the default because it is cheaper and the choice is still open; --listen open-mic is the single flip. Two traps worth knowing: a clip with no trailing silence is never answered, and the end of a reply is contentEnd with stopReason END_TURN, not completionEnd. A second user turn in one session is treated as barge-in unconditionally, and an interrupted turn that calls a tool is lost, so the session reconnects per turn and gives up conversational memory. That is the concrete thing step three has to solve. * no-mistakes(review): fix voice relay credential reuse, frame validation and record parsing * no-mistakes(review): test uplink header guard, bound unknown expiry, align state dir * no-mistakes(review): decide deny per item, guard turn failures, bound ambient credentials * no-mistakes(review): read account config from home, harden deny and turn failures * no-mistakes(review): close status verb set, fix inbox help, pair data override * no-mistakes(review): keep profile-free relay alive, unblock loop, fix dead assertion * no-mistakes(review): hide finished pull requests, refuse open mic, keep suite offline * no-mistakes(review): survive reader failures, release devices, fix claims A failure while handling a model event, or while sending a tool result, left the reader task dead with ended and turn_done clear, and close() re-raised the stored failure on every await. One dropped stream became a relay that could never build another session. The reader now reports the session over in a finally whatever killed it, and close() absorbs the task the same way it already absorbed its sends. The laptop client releases what it already started when a later startup step refuses, SystemExit from the handshake wait included, and names a device refusal instead of leaking a raw PortAudio error. Whether it releases correctly against a real device is still unverified here. The records docstring claimed every reading was filtered to open ids. Only the pull request count and list are; the worker count and the state histogram cover every live runtime record, finished ids included, because a meta file still on disk still needs tearing down. The finished-work deny half of the suite asserted things that held with the deny list absent. It is replaced by a deny on an open title, which removes the row and says so while the count stays honest. * no-mistakes(review): name reader failures, split file and device refusals A failure inside the model reader released the waiting turn and told nobody. The session was not marked spent, no notice reached the client, and the client waits for a reply end or a notice, so the captain got their whole timeout of silence and then a record saying the turn went unanswered with nothing about why. Both ends of the relay now name a failed turn through one function, once per turn, and --self-test carries the cause in relay_error the way the client's own record does. Two things that are not failures stay that way. A stream that simply ends is the end of a session, which serve still reads on its own terms. A stream that goes away because close() asked it to is an ordinary renew, and announcing it would have put a failure notice in front of the captain on every turn. On the laptop end, the refusal that became a device error covered the file-backed playback and capture too, so a mistyped --in-file was reported as an audio device failure and the advice named the flag that had just failed. The file ends now report the path and the flag that chose it and stay an OSError; the device ends keep the device advice and name the flag for that end. The device paths remain unrun here, so only the file halves are covered by a test. * no-mistakes(test): survive model session end, order client turn frames * no-mistakes(document): sync voice relay docs with reviewed relay behavior * no-mistakes(document): re-measure relay latency and correct its cause * no-mistakes(document): correct measurement date and name the unmeasured SSH hop * no-mistakes(document): describe the unpublished control measurement, fix list formatting * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes
…unchenguid#2763) * fix: keep Relay public loops open until retire Delivering a promised-final reply was deleting the only record that tied a public thread to later work, so a follow-on ship silently owed no closing reply. Retain the registration after delivery, rechain follow-on work onto the same thread, and make retire --reason the only close. * no-mistakes(review): Propagate public follow-up registration removal failures * no-mistakes(review): Persist retire receipts and align parent resolution * no-mistakes(review): Make rechain resumable after partial obligation creation * no-mistakes(review): Repair follow-up state, briefs, and expiry escalation * no-mistakes(review): Serialize follow-up delivery stamps with retirement * no-mistakes(review): Serialize rechain claims and protect registration terminal states * no-mistakes(review): Avoid reporting retired delivery loops as open * no-mistakes(document): Refresh public-loop documentation and verification evidence * no-mistakes: apply CI fixes * no-mistakes(review): Preserve delivered follow-up bindings during registration replay * no-mistakes(review): Harden public follow-up retirement and rechain races * no-mistakes(review): Fail closed on unresolved secondmate retirement * no-mistakes(review): Bind secondmate cleanup to its recorded canonical home * no-mistakes(review): Fix rechain command output and expiry validation * no-mistakes(review): Validate brief keys and warn on remote promotion * no-mistakes(document): Document retained public follow-up loops * no-mistakes(lint): Remove unused bounded-wait loop variable
…ture and scripts inventory
The stage 0a pilot showed the receipt cannot express real findings and that a missing head_sha makes a drifted base report not-applicable and exit 0. This revises unpublished schema v1 in place: require head_sha, duration_ms, engine, threshold, and a stable finding id; replace survivors[] with per-phase findings[]; and make verify one envelope with phases[]. bounds.budget_minutes is the missing wall-clock bound.
…ath (kunchenguid#2779) * feat(bin): merge GitLab merge requests through the guarded PR merge path bin/fm-pr-lib.sh already parses a GitLab merge request URL for the watcher, but bin/fm-pr-merge.sh refused every non-github provider, so a merge request had to be merged by hand and got none of the recording, guards, or audit trail a pull request gets. The merge path now dispatches on the parsed provider. A GitHub URL keeps its exact previous behavior. A GitLab URL is addressed through glab by the project URL rebuilt from the parsed host and path, so a merge request on any instance resolves and no host is hardcoded, and no merge-method flag is added because the project's own merge method is what should apply. A GitLab merge happens only after one live read of the merge request confirms it is open, detailed_merge_status is mergeable, has_conflicts is false, blocking_discussions_resolved is true, and the head pipeline succeeded at the exact current head. Every failing condition is reported, not just the first. The verified head is bound to the merge with glab's --sha, so a push landing between the read and the merge fails the merge instead of landing commits nothing verified. Recorded metadata is never the authority for any of this: a rebase moves the head and leaves a recorded value stale, so a recorded head that disagrees with the live one is reported rather than trusted, and the recorded value is read before the recording step because that step drops a GitLab head it cannot resolve. * no-mistakes(review): reject bundled -R clusters and make tool-absence cases host-independent * no-mistakes(test): state authorised GitHub narrowing of bundled -R guard This branch NARROWS GitHub behaviour. The narrowing was authorised deliberately rather than slipping in by accident, and it applies to both providers, GitHub and GitLab alike, because a script that guards one provider and not the other is a trap for the next reader. What bin/fm-pr-merge.sh now refuses is extra merge arguments containing a bundled short-option cluster that includes R, for example "-dR other/repo". The forge CLIs expand such a cluster one character at a time, so it carries "--repo other/repo", and that later value wins over the repository the URL named. Before this change, "fm-pr-merge.sh <task> <github-url> -- -dR other/repo" reached "gh-axi pr merge 12 --repo example/repo --squash -dR other/repo" and exited 0 with pr= recorded and the merge poll armed. It now exits 1 with "extra merge arguments must not override the repository", records nothing, and invokes no forge merge command. Every other GitHub invocation is byte-identical to the base commit. Closing that hole honours the existing rule rather than departing from it. The file header already forbids --repo and -R because the repository must come only from the URL, so a bundled cluster carrying a repository override was never legitimate behaviour to preserve: it was that guard being evaded. Redirecting a merge to a repository the URL does not name is exactly what the guard exists to prevent. The refusal is already pinned on both paths by the existing case test_bundled_repo_override_args_refuse_before_recording in tests/fm-pr-merge.test.sh. On GitHub ("-dR wrong/repo") and on GitLab ("-yR https://other.example/g/p") it asserts exit 1, the refusal wording, no pr= in the task meta, no armed merge poll, and no forge merge command invoked, with a control case proving a cluster that carries no repository override still reaches the forge. No duplicate assertion was added. Both assertions were confirmed to have teeth by narrowing the guard back to a bare -R and watching each path fail. This commit carries no file change: the guard and its coverage landed in 614853d, and this message exists so the pull request description states the narrowing. * no-mistakes(document): fix README pointer for GitLab watch and merge doc * no-mistakes: apply CI fixes
A short herdr recent tail can drop Claude's opening rule and classify an idle composer unknown, so native-hosted away-mode never injected. Classify a glyph immediately under a closing rule as empty, read the visible viewport for composer capture, and let herdr native idle deliver when the composer is still unknown. Max-defer retries that path before alarming. The native-hosted daemon still injects into the captain pane. A dead shell has no idle agent registration and still defers.
… proven idle composers
…erdr composer read
A Claude Code background continuation runs in its own process tree, so the harness-ancestry walk answered a different question in a hook than in a tool shell. One session could hold the lock by the auto-arm's reckoning and not hold it by every mutating path's, which left supervision off while wake drains, gate answers, merges, teardowns and a promotion all proceeded. - bin/fm-session-lock-lib.sh becomes the single owner of the ownership verdict and its refusal. Identity resolves in three tiers: the vendor-declared CLAUDE_PID, then the conversation id recorded in state/.lock.session, then the ancestry walk for harnesses that declare neither. - fm_require_session_lock gates the eight fleet-mutation entry points before argument validation, so the read-only rule is enforced where mutation happens. It refuses only on a live foreign owner plus a caller that is itself in a harness session, leaving ssh, detached and CI callers alone. - bin/fm-lock.sh and bin/fm-session-start.sh state ownership in words; the digest carries an explicit HELM: line. - bin/fm-turnend-guard.sh tells a correct decline from an auto-arm failure, reports it once per holder, and then stops blocking. - tests/fm-session-lock-ownership.test.sh pins all three parts with real competing processes; tests/fm-session-identity-live-e2e.test.sh proves the two vendor-declared values against real Claude Code.
…tity, and turn-end notices
…unchenguid#3121) * fix(bin): explain a pooled slot's stale submodule refusal A pool slot whose submodule pin moved is refused with "is not clean; refusing to discard uncommitted work", while the operator's own `git status` in that slot reads clean. The message names no submodule, no pin, and no remedy, so the refusal is unreadable and the slot looks wedged for no reason. That is the failure that jammed three slots in a row when a submodule pin moved. The refusal itself was never the bug and is unchanged: the gate still refuses, and still touches nothing. It now distinguishes the one case it can prove and says what it found - the submodule, the pin the slot has, the pin the base records, and the command that clears it. The diagnosis is deliberately conservative, because ` M <path>` alone cannot tell a stale pin from real work. An entry is reported as stale only when every reported entry is a gitlink whose submodule is internally clean and whose recorded pin actually differs. A submodule holding uncommitted work, untracked files, or an unpushed commit therefore keeps the original uncommitted-work refusal, even when its pin is also stale - the remedy command would be wrong there, and the conservative refusal is the safe answer. Nothing is converged, synced, initialized, or deleted. There is no new failure path: a slot that launched before still launches, a slot that refused before still refuses, and projects that configure a submodule `ignore` are read exactly as before. Paths are read with core.quotePath=false so a non-ASCII submodule is named rather than falling back to the unreadable message. Tests keep the reproductions that prove the message is accurate: the stale-pin diagnosis (which fails against the previous refusal), work inside a submodule still refused as uncommitted work, and a stale pin carrying real work refused conservatively rather than called stale. Each asserts the slot is left untouched. * no-mistakes(review): require remote containment before calling a submodule pin stale * fix(bin): stop printing a remedy the containment check cannot stand behind The stale-pin diagnosis printed `git submodule update --checkout` as the command that clears the slot. The containment check behind it reads local refs only and never fetches, because this gate has to stay usable offline. A remote-tracking ref that has gone stale - its upstream branch deleted or force-pushed, and never pruned - still reads as containment, so a commit that is really unpushed can look contained and that command would move the submodule off it. Naming the submodule and both pins is the whole point of the diagnosis: it turns "is not clean", on a slot whose own `git status` reads clean, into a statement of which submodule drifted and where it drifted from. The operator can choose the remedy from that, seeing the whole picture. Printing an instruction that rests on a judgement which can be fooled is worse than printing none, so it is dropped. The limitation is now stated where it applies, in the script header and beside the check itself, rather than left for a reader to discover. No fetch is added: the gate stays offline-safe by design. Nothing else changes - the same conditions are refused, the slot is still never touched, and a submodule carrying real work or an unpushed commit still keeps the conservative uncommitted-work refusal. * no-mistakes(review): bound submodule containment probe to first commit
* fix(pi): type captain supervision outcomes so main relays them A captain-relevant branch outcome reached main as a bare user message with no marker of origin or required action, written in main's own captain-facing voice, landing in a tail that often already held several such notes. Pi keeps only a custom message's content when it builds the provider request, so customType and display never reach the model and content was the only place that identity could live. Main could not tell an incoming outcome from its own earlier answer and sometimes re-emitted that answer instead of relaying the outcome, losing it. Measured against real Pi 0.84.1 on openai-codex/gpt-5.6-sol: 6 failures in 24 turns, rising to 3 in 6 once one stale answer was already in the tail, which is how one captain conversation saw six identical messages in a row. The same scenario with the outcome typed failed 0 times in 14 turns. Wrap only the captain-verdict note in the branch-outcome operational kind owned by bin/fm-operational-input.sh. Delivery is otherwise unchanged: still display: false, still one triggerTurn follow-up, so the turn remains the single captain-visible outcome and no hidden note is ever shown twice. Routine notes stay plain because their renderer reads the glyph off the front of that same string. An outcome that cannot be encoded degrades to the same instruction as plain text rather than being lost, matching this file's stated failure direction. The existing assertions could not catch this: they pin the sendMessage options and never look at what main receives. Add a portable regression that classifies the delivered payload with the real protocol executable, and a live guard that runs the real Pi SDK's own convertToLlm to prove content is the entire model-visible payload. * no-mistakes(document): Document typed Pi captain outcomes
… or enriched wedge (kunchenguid#3155) * fix(bin): keep a busy pane from retiring a still-declared wait's window The away-mode daemon's pause re-surface recheck (housekeeping step 2b) read a busy pane as "the crew resumed" and dropped the declared-wait marker, without re-reading that the crew's own latest status line still declared the wait. That inference is not safe, because a declared wait can legitimately hold a pane busy: a worker sitting on a long foreground call keeps that call live for as long as the wait lasts. The marker is then cleared while the declaration still stands, and migrate_watcher_pause_markers recreates it with a fresh timestamp on the very next tick, so the window restarts forever and the wait never matures into its one bounded recheck. Away mode makes that terminal. Since the watcher half landed, a busy pane under a declared wait is handed to the daemon exactly once per declaration and never woken again while the declaration stands (bin/fm-watch.sh, busy_turn_bound_check), so this recheck is the only thing left that can re-surface the pane at all. Measured end to end on a throwaway state root, away mode active, a pi pane busy past FM_BUSY_TURN_MAX_SECS, status still `paused:`, over six PAUSE_RESURFACE_SECS windows: 0 captain-facing rechecks before this change, 6 after - one per window, with the marker reset each time. The fix drops only the busy arm of the 2b probe, leaving it an endpoint-readability check: exit code 2 still means the capture failed, so the endpoint is gone and the marker goes. The loop head above already drops the marker the moment the status line stops declaring the wait, so nothing else is needed to end the routing, and the reconcile path runs before the probe ever reads a pane. tests/fm-daemon.test.sh: test_housekeeping_paused_resumed_cleared pinned the old inference on purpose - its fixture's status line still read `paused:` while the pane was busy, and its comment read "A pause whose pane became busy again (the crew resumed)". Its fixture now resumes the way a crew actually resumes, by appending a non-declaring status line, and it asserts its own busy verdict first so it cannot silently decay into the idle-pane case that test_housekeeping_paused_unpaused_cleared already covers. What it pins is now the inverse guard: a busy pane must not GATE the clear either, so an over-correction that kept the marker alive whenever the pane is busy would fail it. test_housekeeping_busy_declared_wait_matures_its_window is the new regression, over both declaration forms. It asserts the busy verdict, then that ticks inside the window neither escalate nor let the marker be recreated with a fresh timestamp, then exactly one recheck past the window named for the right human and never a wedge, then silence on the next tick inside the reset window. It fails on unmodified main with "produced 0 escalations past its window, expected exactly one". Refs kunchenguid#3149 * fix(bin): let a declared wait outrank an enriched wedge escalation handle_wake classifies a stale wake through classify_stale, which returns a `pause` verdict for a crew whose latest status line declares an external wait or a verified captain-held transfer. It then threw that verdict away whenever the wake reason matched `idle *s, possible wedge, escalation *`, so the watcher's enriched wedge decoration outranked the crew's own declaration and a healthy declared wait was escalated once per FM_STALE_ESCALATE_SECS for as long as the wait lasted. The enriched reason earns its precedence over the daemon's cheaper status-log absorption honestly - it carries the watcher's escalation count and its explicit "do not re-absorb on the run-step/pane state alone" demand. A `pause` verdict is not run-step or pane state. It is the crew's own declaration that this pane waits by design, which is precisely the question the wedge timer cannot answer for itself, so it is the one verdict that decoration must not override. The two classifications genuinely disagree in steady state rather than only in a race: a crew that declares `paused:` while its no-mistakes run is still attributed to its code reads `working` to the watcher's pause_state_class, so the watcher takes the wedge timer while the daemon's classify_stale reads the status log and correctly returns `pause`. The wait stays bounded, not silenced. Absorbing to the pause action records the declared-wait marker and drops wedge aging, and housekeeping (2b) then owns the re-surface, so the pane still reaches the captain - once per PAUSE_RESURFACE_SECS as an explicit "recheck whether the wait still holds", instead of once per FM_STALE_ESCALATE_SECS as a possible wedge. Measured on a throwaway state root over five wedge cadences for one declared wait: 5 escalations climbing to demand-deep-inspection before this change, 0 after, with the one bounded recheck still delivered. tests/fm-daemon.test.sh: test_stale_diagnostic_wedge_survives_busy_housekeeping's `paused` case pinned the old precedence on purpose, asserting exactly one escalation carrying the demand-deep-inspection payload. That case now asserts the pause cadence instead - no escalation inside the window, pause tracking recorded - while the `working` and `prior-terminal` cases keep asserting the enriched wedge verbatim, so the override itself is still pinned everywhere it is correct. test_enriched_wedge_under_declared_wait_uses_pause_cadence is the new regression. It asserts the fixture's own classifier verdict is a pause first, so the case cannot go vacuous, then drives four consecutive wedge-cadence deliveries in both the plain and demand-deep-inspection forms through the real handle_wake and housekeeping pair, then matures the window for exactly one awaiting-external recheck, then lifts the declaration and requires the same enriched wedge to escalate again unchanged. It fails on unmodified main at the first delivery. Refs kunchenguid#3149 * no-mistakes(review): align afk skill recheck wording with still-declared contract * no-mistakes(document): daemon doc comments: pause window ages on declaration --------- Co-authored-by: Talon Stark <talonstark@gmail.com>
* fix(bin): make Claude auto-arm continuity self-heal past a hung claim On a Claude primary, a Stop-hook auto-arm process that hung mid-arm held the single-flight owner lock with its epoch ledger frozen at outcome=arming, and the abandonment proof read any live lock holder in arming as legitimately deciding forever. Every later Stop firing exited 0 at the lock, the turn-end guard kept deferring to the hung owner as recovery under way, and the watcher was never auto-re-armed again for the rest of the session - supervision survived only on manual arms and lapsed between them (the 2026-08-26 watcher flap). Corrections layered onto the lock-held-across-arm shape each reopened the same concurrency class one level down, so this replaces the claim machinery wholesale with a generation-based optimistic design: - The epoch ledger's monotonic sequence IS the claim generation; the two-line entry (classic epoch record plus the claimant's MANDATORY pid-identity) is the claim. Every firing defers to a live OPEN claim: outcome arming, owner alive, identity recomputes and matches, and not stuck (entry and watcher beacon both older than the guard grace). - A finished, dead, identity-mismatched, identityless, or stuck claim is superseded by simply taking the next generation - no signalling or revocation of a steady-state predecessor. - No mutex is held across arming or output; the owner lock survives only as a micro-mutex around individual ledger writes. A superseded owner goes completely silent: ownership is re-verified before every arm invocation, episode-state mutation, ledger write, and continuation. - The irrevocable commit point of a translation is the exit status (the harness delivers the collected stderr only on exit 2), so the owned terminal ledger write is the atomic commit: the winning generation exits 2 unconditionally after it, a refused one exits 0 silently even after printing, and the once-per-episode failure notice commits in the same owned critical section as the winning failed write. Two bounded residuals are documented accepted intent: an owner dying between its owned write and its own exit, and a hung old-build owner resuming during the one legacy upgrade window. - The pre-generation lock-holding claim shape keeps defer-or-reclaim behavior through a legacy shim: a live identity-verified stuck owner is retired via TERM (with a queued TERM sufficient when the owner is stopped) before its lock is removed, an unverified or identityless pid is never signalled but never blocks a proven-abandoned reclaim, and the lock's identity evidence is grafted into the ledger (mtime-preserving) so pid-reuse protection survives the lock. - The guard reads the same predicates for recovery ownership and its terminal fail-open (which re-checks for a live open claim under the held locks before committing the attended alarm), with ledger reads anchored to line 1 so the identity line can never confuse them. Behavioral regression coverage exercises all three edge classes through the real hook and guard - a live open claim defers with no lock held, a stuck claim is superseded and the home re-arms, and an end-to-end run with a genuinely hung owner shows a concurrent firing deferring promptly mid-arm, a later firing superseding the stuck owner, and the superseded owner exiting silently without a second translation - plus the identityless/reused-pid loopholes, the superseded-owner arm boundary, and the legacy TERM, SIGSTOP, and signal-free reclaim paths. * no-mistakes(review): Refuse auto-arm commits when notice marker creation fails * no-mistakes(review): Make episode reset atomic with generation ownership * no-mistakes(document): Update auto-arm generation and commit documentation
…n unproved merge (kunchenguid#3064) * fix(pr): verify GitHub merge outcome * no-mistakes(review): Captain, fixed forge-only merge verification, queue guidance, metadata propagation * no-mistakes(document): Correct forge-specific merge documentation * no-mistakes(review): Captain: forge-only queue fix, focused tests pass * no-mistakes(review): Captain: suppress closed-state guidance and prove parent regression * no-mistakes(review): Captain: remove history proof; retain executable regressions * no-mistakes(document): Clarify GitHub recording timing in architecture docs * no-mistakes(document): Clarify outcome-aware PR merge recording documentation * no-mistakes: apply CI fixes * Revert "no-mistakes: apply CI fixes" This reverts commit c326cfa. The automatic CI repair round removed the up-front `gh` prerequisite check while keeping the `gh` dependency: `bin/fm-pr-merge.sh` still calls `gh api graphql` for the outcome read and `gh api` for the branch-rules read. That left the same hard requirement without the clear named error, and review immediately raised a new finding for exactly the failure the check prevents - `gh-axi pr merge` landing the merge while the follow-up read fails, so the PR metadata is never recorded. The check is also symmetric with the GitLab arm directly above it, which already refuses up front when `glab` or `jq` is missing, on the stated principle that a missing tool should be a named prerequisite rather than a merge that is armed and then refused for an unexplained reason. The workflows this round was chasing sit at `action_required` because this is a fork pull request; no code change can turn them green. * fix(pr): keep PR bookkeeping when a merge outcome read fails On the GitHub path a merge call that returned success was followed by `github_read_outcome || exit 1`, so a transient API failure, rate limit, or network blip during the read dropped out of the script before `record_pr_metadata` ever ran. The merge could have landed while `pr=` went unrecorded and the merge poll was never armed - bookkeeping lost on a real merge. The failure path just above already recorded metadata before exiting, so the error path was more careful than the success one. Record the PR before that refusal. Recording arms the later merge poll and is not a success claim, which is the same reasoning that keeps `record_pr_metadata` on the gh-axi failure path. The refusal itself is unchanged: exit stays non-zero and the message still names the concrete observed state. Metadata is withheld only when the read succeeds and proves the pull request neither merged nor queued. Pin it with a case that stubs `gh api graphql` into failure after a successful `gh-axi pr merge`, asserting both the non-zero exit and the recorded metadata. * no-mistakes(review): Aggregate queue rules and report conflicts explicitly * fix(pr): keep the merge abstraction reachable and its bookkeeping intact Two holes remained in the outcome-verified GitHub merge path, both on installations where gh-axi is present but gh is not. The verification preflight refused before bin/fm-pr-merge.sh ever reached the configured gh-axi merge abstraction, so an installation without gh could no longer merge at all. gh-axi now performs the merge unconditionally and the queue-aware gh read became an optional enrichment: with gh on PATH its GraphQL view still separates merged from queued, and without gh the gh-axi view still proves a landed merge while every outcome it cannot prove refuses. The PR metadata recording sat behind the outcome read, so a merge that landed before that read failed lost pr= and its merge poll. Recording now happens once, before either forge call, which arms the poll without claiming a landed outcome and leaves teardown a PR identity to verify against no matter how the read ends. Rebasing onto main also restored the durable merge-outcome reporting and the GitLab landed-state confirmation that the conflict resolution dropped. Tests pin each fix through the executable interface: the merge abstraction is reached and verified with gh absent, a failed fallback read keeps its bookkeeping, and a mock that snapshots the task meta during the forge call proves pr= is recorded before the merge can land. * no-mistakes(review): fix(pr): de-dup queue methods, fall back on failed gh read, refresh contracts * no-mistakes(review): fix(pr): quote forge output and explain armed auto-merge on refusal * no-mistakes(review): fix(pr): claim auto-merge armed only when the forge accepted it * no-mistakes(review): fix(pr): tell the operator what each GitHub refusal could not observe * no-mistakes(review): fix(pr): gate every forge-acceptance claim on a successful merge * no-mistakes(document): align merge docs with verified GitHub outcome contract
* fix(pi): stop reporting one merge to the captain twice The supervision branch's captain-outcome note told main, unconditionally, that the note "is not your own earlier output" and to relay it now. When main had already reported the same event, that assertion was false and the order turned the correct response - saying nothing new - into a mechanical re-report, so the captain saw one merge reported twice in 16 seconds. Two independent changes, both needed: - The relay instruction is now conditional. It still names itself as a supervision outcome so main cannot mistake it for its own earlier answer (the silent loss that instruction exists to prevent), and it now lets main stay quiet about an outcome it has already given the captain. - The merge case is closed at its source rather than left to that judgment. One merge reaches a home on two independent paths by design - main's own permanently main-owned merge poll, and the branch's task-local status wake - and main's captain-facing text only reaches the branch's mirror at main's turn end, so the branch can escalate before it could possibly see the captain was already told. bin/fm-pr-merge-notified.sh answers that question from bin/fm-pr-lib.sh's canonical merge-notification marker, so the answer holds regardless of mirror timing. A captain outcome naming an already-published merge is delivered as the ordinary rendered note instead of opening a follow-up turn: still appended, still visible, still recorded with the verdict the branch decided, minus the wasted turn. Any error, timeout, or unreadable state relays the outcome. A duplicate announces itself; a lost outcome does not. Regression coverage drives the real delivery path in both directions: a new outcome must still reach the captain in exactly one follow-up turn even beside an unrelated published merge, and an already-published merge must open no second turn while a different PR in the same task still does. The merge path's real producer and this new consumer are exercised end to end in tests/fm-pr-merge.test.sh. Pi-only by construction: the delivery path lives in .pi/extensions, so no other harness loads it, and the new script only reads existing markers. * no-mistakes(review): Document accepted latest-marker suppression residual * no-mistakes(review): Recheck ownership before merge outcome delivery * no-mistakes(document): Document merge-outcome suppression exception * refactor(pi): drop the source-level merge suppression, keep the envelope fix The captain reviewed this branch and judged the source-level duplicate suppression overly complicated for the problem it solved, and asked for the change to be reduced to the envelope wording alone. Remove the mergeIntoMain downgrade path, bin/fm-pr-merge-notified.sh, and every test and document that existed only for it. What remains is the conditional captain-outcome instruction: main is told to stay quiet about an outcome it has already reported and to relay anything else, which covers the duplicate without a second mechanism. The silent-loss protection is untouched - the note is still typed, self-describing, and delivered as one invisible follow-up turn - and the behavioral tests still assert that, now requiring both halves of the conditional instruction. * no-mistakes(ci): Clarified in code comments and owned documentation that this is intentionally an M1-only, model-facing conditional relay fix—not source-level suppression—addressing Greptile’s mistaken scope expectation without changing runtime behavior. Net diff remains 3 files and 27 insertions. Verified with fm-pi-branch-extension tests, fm-lint, doc audience check, and git diff --check; all passed * no-mistakes(ci): Strengthened the runtime delivery test to verify the captain outcome retains its required self-description and outcome text. Verified with `bash tests/fm-pi-branch-extension.test.sh`, `bin/fm-lint.sh`, `bin/fm-doc-audience-check.sh`, and `git diff --check`; all passed. The outer pipeline can now commit and attest the new head
* fix(bin): bind the live pipeline-owned run instead of a superseded failed row fm-crew-state.sh bound a superseded FAILED no-mistakes run to a task instead of the LIVE replacement run: the live run's pipeline-owned lane head is not a git object in the task worktree, so head-equality attribution rejected it and the coarse runs-list fallback silently continued past the RUNNING row onto an older failed row whose head equalled the stale worktree HEAD. The home summary then flipped invalid and Bearings hid the home's live work (F10). Attribution precedence now follows the daemon's own identity: - An ACTIVE run for the task's branch binds without head equality while branch_sync.state is pipeline_owned (fm_nm_run_is_pipeline_owned_active); the pipeline owning the branch is itself the attribution. - A genuinely failed run with no later run on the branch still reports failed through the unchanged head-equality path - real failures are not hidden. - In the coarse runs scan, an unresolvable head is unknown attribution and stops the scan (fm_nm_head_resolvable) instead of falling through to an older row; a resolvable-but-mismatched head keeps the historical reused-branch skip. The exemption never applies to a terminal run and requires pipeline_owned specifically, both pinned by negative-control tests. Fixture shape verified against the live incident run's real axi status output. * no-mistakes(document): Updated run-attribution documentation ownership
…unchenguid#3211) * fix(pi): surface requested supervision outcomes * no-mistakes(review): Mirror in-flight captain requests before branch dispatch * no-mistakes(review): Exercise real branch ownership and main outcome access * no-mistakes(review): Preserve request tails and align verdict guidance * no-mistakes(review): Preserve complete current captain requests * no-mistakes(review): Require visible requested outcomes and realistic classification * no-mistakes(document): Align supervision outcome documentation * no-mistakes(ci): Fixed Greptile’s runtime-ordering finding. The extension now stages Pi’s authoritative `before_agent_start` prompt before SessionManager persistence and suppresses the later duplicate entry. Updated docs and behavioral regression to reproduce real Pi ordering and verify each prompt is mirrored exactly once. Passed branch-extension tests, supervision tests, strict Pi typecheck, full lint, and diff checks * no-mistakes(review): Use canonical operational input classification * no-mistakes(review): Filter legacy operational inputs canonically * no-mistakes(document): Clarify captain request mirroring boundary * no-mistakes(ci): Fixed the CI time-boundary failure in tests/fm-public-followup.test.sh by pinning its clock, including context-registry setup. This prevents follow-up fixtures from expiring based on wall time. Verified the full regression suite passes, project-owned lint passes, and git diff checks are clean * no-mistakes(document): Clarify captain-visible supervision outcome documentation
…#3210) * feat(bin): per-home remote transport lanes with cancellation, bounded send, and closed stdin All remote commands for every home on one host used to serialize through one single-job-at-a-time worker on one shared queue: a timed-out caller abandoned a staged job that kept running, retries convoyed behind it, fm-send's remote leg had no time bound, and staging captured the caller's stdin to EOF so any fm-on.sh caller with an open stdin wedged staging indefinitely. - The worker now serves one lane per staged home: same-home jobs run strictly FIFO in a new staging-sequence order while different homes run concurrently, each lane as its own top-level worker process (a backgrounded subshell does not reliably reap dead children, so a zombie group leader kept a finished command's process group signalable). Long-poll preemption is lane-scoped. - A caller that disconnects or times out cancels its job: the entrypoint marks the record on any post-staging exit and probes its parent so a dead ssh channel cancels without a signal; the worker skips cancelled queued jobs, terminates a running cancelled job's process group, and reaps the record. - fm-send's remote leg is bounded by FM_SEND_REMOTE_BUDGET (default 30s) and a bound hit exits through the existing unconfirmed-delivery contract, which stays idempotent because the remote enqueue deduplicates. - fm-on.sh defaults the remote command's stdin to /dev/null; the three payload callers pass the new --stdin flag. Abandoned .stage.* litter is age-reaped. - The job execution deadline no longer loses up to a second to clock truncation. * no-mistakes(review): Protect live stages and validate send budgets early * no-mistakes(review): Preserve sequence lock ownership during stale recovery * no-mistakes(review): Allocate job sequences at publication boundary * no-mistakes(review): Bound remote keys and extend stale lock recovery * no-mistakes(document): Document bounded remote transport behavior * no-mistakes(lint): Suppress intentional deferred-expansion lint warning * no-mistakes(ci): Fixed stale sequence-lock recovery by reconciling the counter against published job records before allocating the next sequence, preventing duplicate sequences and same-home FIFO violations. Added a behavioral regression test reproducing displacement after publication and verifying execution order. Passed fm-remote-transport-lanes.test.sh, fm-remote-job.test.sh, fm-lint.sh, and git diff --check * no-mistakes(review): Use atomic sequence claims and lossless lane keys * no-mistakes(review): Recover regressed sequence hints and rate-limit claim reaping * no-mistakes(review): Restrict worker heartbeats to serving loop * no-mistakes(review): Verify supervisor identity before lane recovery signals * no-mistakes(review): Verify tracked lane and claim owner identities * no-mistakes(document): Clarify remote lane and transport contracts * no-mistakes(ci): Fixed the CI time-boundary failure by pinning fm-public-followup tests to a deterministic clock, including context-registry setup. Verified tests/fm-public-followup.test.sh, tests/fm-remote-transport-lanes.test.sh, shellcheck, and git diff --check * no-mistakes(review): Preserve assigned lane ownership of queued jobs * no-mistakes(review): Reserve homes owned by foreign queued lanes * no-mistakes(review): Preserve completed results during crash recovery * no-mistakes(review): Harden claim cleanup, expiry, and cancellation races * no-mistakes(review): Verify process groups and reap abandoned results * no-mistakes(review): Stop leaderless groups and reap cancelled publications * no-mistakes(document): Correct remote transport lifecycle documentation * no-mistakes(lint): Quote done state comparisons for ShellCheck
* fix(tests): make the changed-file map select per script and stabilize a budget flake
The changed-file map's bin/ fallback resolved a direct test reference to that
test's whole FAMILY. bin/fm-push-transition-lib.sh is named by exactly one
real-Herdr E2E, so a one-line change to it selected all 12 real-herdr-gated
scripts, including a 341s presentation E2E with no dependency on it.
Resolve direct test references per script, and keep resolving consumer bin/
scripts through the curated map so recorded family-level coupling survives.
Also fix a load-sensitive flake: the tool-update budget deadline is whole-second
granular, so a test budget of 1 left headroom anywhere in (0, 1] seconds and the
first budget check could already read as exhausted.
* feat(bin): make suite wall clock a result and let a family's concurrency be proven
--max-wall-ms fails a run whose wall clock exceeds the caller's budget, after
reporting the per-script results. A suite that stays green while outgrowing its
caller's invocation budget is the regression that got an agent killed mid-run
and retried invisibly, so duration has to be a result rather than a log note.
--pool on the isolation-proof harness runs the same concurrent proof over a
whole family, so 'is this family safe to parallelize?' is answered by a command
instead of a guess. Measured watcher-wake-lock and refused it: 3 of 18 scripts
fail under concurrency on wall-clock assertions about reaching the next poll.
* perf(bin): schedule the changed suite concurrently, longest first
The watcher-wake-lock family is proven concurrent-safe (two clean runs, 18
candidates, 0 failures at 4 workers; docs/fm-test-isolation-proof.md), so
--changed now schedules its proven-concurrent scripts with bounded parallelism
and runs any unproven remainder serially afterwards, never beside them.
Concurrent runs are ordered longest-hint-first. Workers are handed scripts in
order, so alphabetical order started the 193s fm-watch-triage last and stranded
it running alone: 395s wall against a 205s balanced four-worker sum.
An explicit --jobs keeps its strict refusal, so every CI lane is unchanged.
* fix(bin): bound a hung test instead of letting it hang the suite
tests/fm-calm-pi-extension.test.sh was observed running 17+ minutes against a
464ms recorded hint, and the suite had no per-script bound to stop it. An
unbounded suite is precisely what silently outruns a caller's invocation budget,
and --max-wall-ms is evaluated after the run so it cannot end one that never
finishes.
--per-script-timeout-secs terminates a script that outruns it and records exit
124, so the run still completes, accounts for the script, and fails. The
auto-concurrent --changed path applies 900s, far above the slowest real script
(the 341s Herdr presentation E2E), so it only ever converts a hang.
* no-mistakes(review): Enforce safe concurrency and descendant timeouts
* no-mistakes(review): Validate empty runs and isolation proof pools
* no-mistakes(review): Measure selection time in wall budget
* no-mistakes(review): Reap interrupted workers and bound finalization
* no-mistakes(review): Contain shutdown descendants and watchdog finalization
* no-mistakes(review): Honor remaining budget and close launch races
* no-mistakes(review): Restore timeout helper and simplify runner cleanup
* no-mistakes(review): Record isolation pool admission metadata
* no-mistakes(review): Bound Chrome reap and scope proof admission
* no-mistakes(review): Align proof scheduling and preserve budget summaries
* no-mistakes(review): Remove unreliable finalization watchdog
* no-mistakes(review): Freeze budget duration and enforce admission caps
* no-mistakes(document): Refresh test runner concurrency documentation
* no-mistakes(lint): Fix ShellCheck findings in test runner scripts
* no-mistakes(ci): Fixed Greptile’s concurrency-consent finding. `--changed` now remains serial by default; `--changed --jobs auto` explicitly opts into bounded concurrency and the automatic hang timeout. Updated documentation and added behavioral coverage proving serial default behavior, explicit concurrent scheduling, and refusal of `--jobs auto` outside `--changed`. Verified with `bash tests/fm-test-run.test.sh`, `bin/fm-lint.sh`, and `git diff --check`
* no-mistakes(review): Restore automatic changed-suite concurrency and timeout
* no-mistakes(review): Correct changed-suite contributor guidance
* no-mistakes(review): Reject gate-skipped isolation proofs
* no-mistakes(review): Correct automatic concurrency evidence
* no-mistakes(review): Isolate nested runner process groups
* no-mistakes(review): Remove unreliable signal cleanup machinery
* no-mistakes(test): Narrow changed-suite selection to executable contract owners
* no-mistakes(document): Document isolation proof skip and artifact semantics
* no-mistakes(ci): Fixed Greptile’s concurrency-consent finding. `--changed` now remains serial by default; bounded concurrency requires explicit `--jobs auto`. Updated behavioral coverage, contributor guidance, and isolation-proof commands accordingly. Verified with `tests/fm-test-run.test.sh`, `bin/fm-doc-audience-check.sh`, `bin/fm-lint.sh`, Bash syntax checks, and `git diff --check`; all passed
* no-mistakes(review): Restore plain changed-suite automatic concurrency
* no-mistakes(review): Record resolved changed-suite worker count
* fix(bin): keep a runner change selecting its whole curated family
A pipeline fix round narrowed the curated changed-file map so bin/fm-test-run.sh
and bin/fm-test-isolation-proof.sh selected only their own two contract tests,
and the documentation surfaces only the audience test. That cut this branch's
own changed selection from 33 scripts to 5.
The runner executes every pure-contract-unit script, so its contract test
passing proves its logic is right, not that the suite it drives still runs.
Narrowing it also makes any wall-clock claim about the changed suite trivially
true by not running the work.
Only the unmapped bin/* grep fallback resolves per script; curated mappings keep
their recorded family coupling.
* perf(bin): admit the pure-contract-unit family to bounded concurrency
A runner-file change selects pure-contract-unit, so that family decides the
changed suite's wall clock. With only watcher-wake-lock admitted, 14 of its 33
selected scripts fell to the serial tail and the selection measured 327.3s
against a 300s budget: the concurrent group was 19 scripts totalling 273.4s
while the tail alone was 215.7s.
bin/fm-test-isolation-proof.sh --pool pure-contract-unit --jobs 4 passes twice,
32 candidates, 0 failures, so the family is admitted on recorded evidence.
Full 33-script plain --changed: 327.3s -> 181.8s / 178.5s / 172.7s, 0 failures,
inside a 300000ms budget. Also states the per-script guard's derivation.
* no-mistakes(review): Align contract-unit concurrency cap with recorded proof
* no-mistakes(document): Record final changed-suite performance evidence
* fix(bin): keep an empty changed selection clean on stock macOS Bash
Under set -u, bash 3.2 treats "${arr[@]}" on an EMPTY array as an
unbound-variable error, while bash 4.4+ makes it a harmless no-op. The
concurrency work removed the early exit for an empty selection, so execution
fell through to the unguarded existence loop: on stock /bin/bash 3.2.57 a
contributor who changes only documentation and runs --changed got
bin/fm-test-run.sh: line 1713: SCRIPTS[@]: unbound variable
with exit 1 and no summary, instead of a clean total=0 pass.
Restore the early exit, and guard every remaining array expansion reachable
with an empty selection. The reported duration is real elapsed invocation
time rather than a hardcoded zero, so a selection phase that outran
--max-wall-ms still fails.
Verified on this host with /bin/bash 3.2.57: exit 1 with the unbound-variable
error before, exit 0 with FM_TEST_SUMMARY total=0 after.
* no-mistakes(document): Document shell-bound changed-suite performance
---------
Co-authored-by: Kun Chen <kun-1@kunchenguid.com>
* feat(bin): publish per-home summary ledger * no-mistakes(review): Bound and schedule home summary publication * no-mistakes(review): Prove recurring watcher summary refresh cadence * no-mistakes(review): Bound refresh workers and publish durable spawns * no-mistakes(review): Fix atomic kill process-group coverage * no-mistakes(review): Bound state initialization within refresh timeout * no-mistakes(document): Document recurring bounded home-summary publication * no-mistakes(review): Bound and log all best-effort refresh failures * no-mistakes(review): Harden cadence and timeout regression coverage * no-mistakes(document): Document home-summary runtime tuning * no-mistakes(lint): Fix direct exit-code check in refresh test * no-mistakes(ci): Fixed remote secondmate retirement recreating the deleted home: teardown now skips side-band summary refresh when its overridden state directory was removed. Verified with remote lifecycle E2E, teardown tests, home-summary tests, ShellCheck, and git diff checks * no-mistakes(document): Clarify atomic home-summary publication guarantee
* fix(pi): gate first call on startup context * no-mistakes(document): Correct Pi startup prerequisite verification date * no-mistakes(review): Captain, fix startup process-group retirement after leader exit * no-mistakes(review): Captain, release reload exit listeners on shutdown * no-mistakes(review): Captain, complete startup exit lifecycle ownership * no-mistakes(review): Captain, release empty startup process-group ownership promptly * no-mistakes(review): Captain, supervise startup ownership and restore failure fallback * no-mistakes(review): Captain, restore live Pi supervisor execution * no-mistakes(document): docs: clarify Pi startup prerequisite delivery
* fix(pi): restore 0.84.4 adapter compatibility * no-mistakes(review): Restore Pi collapsed and expanded outcome parity * no-mistakes(review): Preserve Pi stock previews through capability probing * no-mistakes(document): Document Pi 0.84.4 renderer compatibility
Claude Code reordered two hook keys and added the ponytail plugin entry. Commit it as-is so the tree is clean before the upstream reconciliation, which refuses to run on a dirty checkout.
Stage 1 copied 41 files that exist upstream and not here. Stage 4 took the captain-hold cluster, replaced fm-decision-hold.sh with upstream's shim, and removed the two superseded decision-hold files. Also took the captain-hold aware files we never modified, which the cluster's own test depends on. Scratch commit on an isolated branch. Not for landing as-is.
Takes upstream's captain-hold as the hold owner, its durable steering inbox, GitLab merge support and merge-outcome reporting, the Pi supervision branch, worktree-write wedge deferral, and the stale-submodule spawn diagnosis. Keeps this fork's four protected areas: the curated memory system, the quality gate and its hardened posture, landing into this home's own local main, and agy support. Also keeps the session-lock gate on every fleet-mutation entry point, the ternary run-head identity beside upstream's pipeline-owned exemption, the provably-working wedge suppression beside upstream's write deferral, and the contained test lane. Upstream f66be0f, 60 commits past the merge base.
The merge kept both attribution designs and they disagreed on an unresolvable run head: ours bound it as `unverified`, upstream's refused unless the pipeline owned the branch. Two upstream regressions caught it. Decide it on the evidence the run actually carries, strongest first. On the coarse runs list there is no launch anchor at all, so only a verified match binds. On `axi status`, a pipeline-owned active run binds outright, a run whose branch_sync names any other state is positive evidence against attribution and refuses, and a run carrying no branch_sync block keeps the old `unverified` binding that can report progress but never a terminal verdict. Both suites pass: 73 checks green.
Codex CLI 0.150.1 accepts model_reasoning_effort=max. Spawn was dropping that flag, so a requested max silently ran at the Codex default. Verified 2026-08-29: an invalid-effort 400 lists none|minimal|low|medium| high|xhigh|max, and a max run with gpt-5.6-luna started real work.
BohnBawerick
force-pushed
the
fm/fm-codex-max-effort
branch
from
August 30, 2026 01:56
b62b515 to
f846341
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Rebase and land the Codex max-effort fix: make the launcher emit Codex max reasoning effort and update harness-adapters, verified against installed codex-cli 0.150.1. In the test step, stream test output script by script / shard by shard directly so progress heartbeats (FM_TEST_BEGIN and FM_TEST_END) appear continuously rather than running the full suite as one silent block.
What Changed
maininto the fork and brought over the new subsystems: the quality-gate loop (bin/fm-quality.sh,bin/fm-quality-receipt.sh,docs/quality-gate.md), durable steer inboxes and leases (bin/fm-inbox.sh,bin/fm-task-inbox-lib.sh,bin/fm-lease.sh), the Nova Sonic voice relay (bin/fm-voice-relay.py,bin/fm-voice-client.py), persistent Pi branch supervision (.pi/extensions/fm-branch-supervision.ts,bin/fm-branch-prompt.sh,bin/fm-branch-outcome.sh), and the tool-update, home-summary, and secondmate-reconcile scripts. The separate decision-hold concept collapses intobin/fm-captain-hold.sh, leavingbin/fm-decision-hold.shas a one-release compatibility shim.bin/fm-spawn.shnow emits-c 'model_reasoning_effort="max"'for the codex harness instead of dropping the flag,bin/fm-bootstrap.shcrew-dispatch validation acceptscodex:max, and.agents/skills/harness-adapters/SKILL.mdrecords the effort table as verified against codex-cli 0.150.1.bin/fm-test-run.shgains bounded automatic concurrency for--changed, family-level concurrency proofs with per-family job caps,--per-script-timeout-secsand--max-wall-msbounds, and longest-hint-first scheduling, with the effective per-script bound recorded in the timing artifact.Risk Assessment
✅ Low: The fix-round changes are small, each is pinned by a behaviour test that would fail against the code it replaced, and the intent-relevant Codex max-effort change is consistent across the launcher, the dispatch validator and the harness-adapters table, leaving only one cosmetic artifact-labelling nit.
Testing
Ran four test scripts one at a time so the FM_TEST_BEGIN/FM_TEST_END heartbeats streamed continuously rather than as one silent block: spawn dispatch profile, bootstrap, crew-state plus teardown, and the test-lane suite. On top of that I drove the launcher end to end through the suite's fake tmux and captured the literal command firstmate sends for codex at each effort, showed the pre-fix launcher dropping
max, showed bootstrap moving from rejecting to accepting a codex max dispatch config, and read the installed codex-cli 0.150.1 catalog to confirmmaxis a real reasoning level. Everything tied to the stated intent passed. The single failure, tests/fm-teardown.test.sh::test_exec_changed_process_is_still_reaped, reproduced 4/4 and traces to this WSL2 host returning a driftingps -o lstart=for a live pid; the reaping code and the test are unchanged from the base commit, so it is a host limitation rather than a regression from this branch.Evidence: codex launch command line before and after the fix (--effort max)
Source: codex launch command line before and after the fix (--effort max)
BEFORE (f846341^): codex --model 'gpt-5.6-sol' --dangerously-bypass-approvals-and-sandbox ... AFTER (a25825b): codex --model 'gpt-5.6-sol' -c 'model_reasoning_effort="max"' --dangerously-bypass-approvals-and-sandbox ...Evidence: codex launch lines for every supported effort (medium/high/xhigh/max) with the recorded meta profile
Source: codex launch lines for every supported effort (medium/high/xhigh/max) with the recorded meta profile
Evidence: session-start output for a crew-dispatch config requesting codex effort=max, before and after
Source: session-start output for a crew-dispatch config requesting codex effort=max, before and after
AFTER: BOOTSTRAP_INFO: crew dispatch rule: hardest work -> codex/gpt-5.6-luna/max BEFORE: CREW_DISPATCH: invalid config/crew-dispatch.json - invalid effort: codex:maxEvidence: installed codex-cli 0.150.1 advertised reasoning levels (codex debug models)
Source: installed codex-cli 0.150.1 advertised reasoning levels (codex debug models)
gpt-5.6-sol -> low,medium,high,xhigh,max,ultra gpt-5.6-luna -> low,medium,high,xhigh,max gpt-5.5 -> low,medium,high,xhighEvidence: streamed FM_TEST_BEGIN/FM_TEST_END heartbeat timeline across the test step
Source: streamed FM_TEST_BEGIN/FM_TEST_END heartbeat timeline across the test step
Evidence: teardown exec-changed failure diagnosis
Source: teardown exec-changed failure diagnosis
Evidence: host ps lstart instability probe (root cause of the teardown failure)
Source: host ps lstart instability probe (root cause of the teardown failure)
read 1: Sun Aug 30 15:06:16 2026 read 2: Sun Aug 30 15:06:15 2026 read 3: Sun Aug 30 15:06:15 2026 /proc starttime (stable ticks): 74920747Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-nm-run-lib.sh:135-fm_nm_head_matches_worktreecontains the identical statement[ "$(fm_nm_head_identity "$1" "$2")" = match ]twice in a row. The second line is added by this branch (the merge resolution in 35b2200), and the diff shows only the second copy as+, so it is a merge artifact rather than a deliberate double check. The first invocation's exit status is discarded and the function returns the second one, so the result is unchanged, but every call now runsfm_nm_head_identitytwice - which isgit rev-parse HEAD,git rev-parse --verify <head>^{commit}andgit merge-base --is-ancestorexecuted a second time for no effect. The single caller is bin/fm-teardown.sh:1501 on the pre-teardown run-abort path. A reader also cannot tell whether the repetition is intentional. Delete the duplicate line.bin/fm-nm-run-lib.sh:144-fm_nm_head_resolvableis added by this branch and has no caller anywhere in the repository (grep across bin/, tests/, .pi/, .opencode/ finds only its own definition). Its doc comment describes the caller it was written for - "A caller scanning run rows newest-first must stop on unknown attribution rather than surface an older, superseded run" - but the coarse walk in bin/fm-crew-state.sh:481 was changed by 1597a75 to decide that throughnm_coarse_head_identity/fm_nm_head_identityinstead, so the helper is dead on arrival. Either remove it or point the coarse walk at it, so the file does not carry two competing answers to the same question..claude/settings.json:55- Commit 97ea071 adds"enabledPlugins": {"ponytail@ponytail": true}to.claude/settings.json, which is the checked-in, project-shared Claude Code settings file (as opposed to.claude/settings.local.json). The commit message states the intent was to get a clean tree before the upstream reconciliation, not to enable a plugin repo-wide, andponytailappears nowhere else in the repository. The effect as committed is that every contributor and every agent session opened in this repo has that plugin enabled, which is unrelated to the Codex max-effort goal and to the test-streaming goal in the stated intent. The hook key reordering in the same commit is a genuine no-op and fine to keep. Confirm whether the plugin entry should be repo-wide; if not, move it to.claude/settings.local.json(already covered by the repo's ignore rules) and drop it from the tracked file.🔧 Fix: drop duplicate head check, dead helper, shared plugin toggle
1 info still open:
bin/fm-test-run.sh:2089- The clamp branch is entered whenever PER_SCRIPT_TIMEOUT_SECS is not strictly less than SCRIPT_TIMEOUT, so it also fires on equality where no clamp happened. Concrete case:bin/fm-test-run.sh --script-timeout 900 --per-script-timeout-secs 900 --json out.jsonwrites selection fieldsper-script-timeout=900s;per-script-timeout-requested=900s. The comment two lines above states the second field exists "so a clamp is visible rather than silent", so an artifact consumer reading that field as evidence of a clamp gets a false positive on a run where the requested and effective bounds are identical. The effective seconds recorded are still correct, so this is a labelling inaccuracy in the timing artifact rather than a behaviour defect. Emitting the requested field only when PER_SCRIPT_TIMEOUT_SECS is strictly greater than SCRIPT_TIMEOUT would make the field mean what its comment says.tests/fm-teardown.test.sh:2453- tests/fm-teardown.test.sh :: test_exec_changed_process_is_still_reaped fails on this host, not because of this branch. The test forces the non-/proc identity path (FM_PROC_ROOT_OVERRIDE points at a missing dir), so teardown identifies a process byps -p <pid> -o lstart=. On this WSL2 host that value drifts for a live pid (five consecutive reads of the same pid returned two different birth times), so the identity re-check never matches and the SIGTERM-ignoring process is never force-killed; teardown then REFUSES after 3 reap passes. bin/fm-teardown.sh's task_process_identity / task_process_identity_matches / reap_task_worktree_processes are byte-identical at base 672c6d0 and target a25825b, and the test body is unchanged, so this is a pre-existing host limitation rather than a regression. Real Linux CI runners read the stable /proc starttime instead. Reproduced 4/4 times; instrumented output and the ps drift probe are in the evidence artifacts.bin/fm-test-run.sh tests/fm-spawn-dispatch-profile.test.sh(32 assertions, includescodex receives model_reasoning_effort max)bin/fm-test-run.sh tests/fm-bootstrap.test.sh(includes the crew-dispatch validation table rowcodex max effort is accepted)bin/fm-test-run.sh tests/fm-crew-state.test.sh tests/fm-teardown.test.sh(run-head attribution and the pre-teardown run-abort ownership path that calls fm_nm_head_matches_worktree)bin/fm-test-run.sh tests/fm-test-run.test.sh(FM_TEST_BEGIN/END marker contract, per-script bound, leak reaping bounded and unbounded)Manual: drove the real fm-spawn launch-construction path through the suite's fake tmux for codex at effort medium/high/xhigh/max and captured the literal launch command lineManual before/after: same driver againstgit show f846341^:bin/fm-spawn.shto show max was silently dropped pre-fixManual before/after: ran bin/fm-bootstrap.sh with a crew-dispatch.json requesting codex effort=max, against both the current and the pre-fix bootstrapManual:codex --version(codex-cli 0.150.1) andcodex debug modelsto read the installed CLI's advertised reasoning levelsManual:git check-ignore -v .claude/settings.local.jsonand a python json parse of .claude/settings.json confirming no enabledPlugins keyManual diagnosis: instrumented bin/fm-teardown.sh's reap loop (restored afterwards, worktree left clean) plus a 5-readps -p <pid> -o lstart=stability probe✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.