chore: snapshot upstream main for 2026-08-27 - #78
Merged
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
… work over (kunchenguid#2767) * feat(voice): spoken round trip on Nova Sonic 2 with a measured relay cost Step one of the spoken interface: the laptop captures and plays audio, this desktop holds the model session, and no AWS credential leaves the desktop. Measured, amazon.nova-2-sonic-v1:0 in eu-north-1, end of speech to first byte of reply audio, 6 runs each, all answered, on a question that forces a records read: relay path 1.229 1.379 1.428 1.447 1.481 1.516 median 1.438 direct 1.147 1.179 1.203 1.237 1.244 1.317 median 1.220 The relay costs about 0.22s of the median. The direct figure reproduces the earlier survey, which is what makes it a usable control. Excluded: the captain's own ssh round trip, microphone capture, and speaker output. This desktop has no microphone and no speaker, so every run used audio files. Three pieces: bin/fm-voice-relay.py holds the conversation on this host bin/fm_voice_records.py what a spoken answer may read, and the handover bin/fm-voice-client.py the laptop end; audio devices UNVERIFIED bin/fm_voice_frame.py the wire format both machines share Real work is handed to the existing bin/fm-inbox.sh rather than a second queueing surface, and the agent says it is handing over rather than answering as firstmate. Read scope: Done history and free-form note bodies are never assembled at any scope, so the wide default cannot reach the places commercial detail accumulates. config/voice-read-scope narrows it to counts only, and config/voice-read-deny excludes a named item in one line. The boundary is an executable test that widening the reader fails. Push to talk is the default because it is cheaper and the choice is still open; --listen open-mic is the single flip. Two traps worth knowing: a clip with no trailing silence is never answered, and the end of a reply is contentEnd with stopReason END_TURN, not completionEnd. A second user turn in one session is treated as barge-in unconditionally, and an interrupted turn that calls a tool is lost, so the session reconnects per turn and gives up conversational memory. That is the concrete thing step three has to solve. * no-mistakes(review): fix voice relay credential reuse, frame validation and record parsing * no-mistakes(review): test uplink header guard, bound unknown expiry, align state dir * no-mistakes(review): decide deny per item, guard turn failures, bound ambient credentials * no-mistakes(review): read account config from home, harden deny and turn failures * no-mistakes(review): close status verb set, fix inbox help, pair data override * no-mistakes(review): keep profile-free relay alive, unblock loop, fix dead assertion * no-mistakes(review): hide finished pull requests, refuse open mic, keep suite offline * no-mistakes(review): survive reader failures, release devices, fix claims A failure while handling a model event, or while sending a tool result, left the reader task dead with ended and turn_done clear, and close() re-raised the stored failure on every await. One dropped stream became a relay that could never build another session. The reader now reports the session over in a finally whatever killed it, and close() absorbs the task the same way it already absorbed its sends. The laptop client releases what it already started when a later startup step refuses, SystemExit from the handshake wait included, and names a device refusal instead of leaking a raw PortAudio error. Whether it releases correctly against a real device is still unverified here. The records docstring claimed every reading was filtered to open ids. Only the pull request count and list are; the worker count and the state histogram cover every live runtime record, finished ids included, because a meta file still on disk still needs tearing down. The finished-work deny half of the suite asserted things that held with the deny list absent. It is replaced by a deny on an open title, which removes the row and says so while the count stays honest. * no-mistakes(review): name reader failures, split file and device refusals A failure inside the model reader released the waiting turn and told nobody. The session was not marked spent, no notice reached the client, and the client waits for a reply end or a notice, so the captain got their whole timeout of silence and then a record saying the turn went unanswered with nothing about why. Both ends of the relay now name a failed turn through one function, once per turn, and --self-test carries the cause in relay_error the way the client's own record does. Two things that are not failures stay that way. A stream that simply ends is the end of a session, which serve still reads on its own terms. A stream that goes away because close() asked it to is an ordinary renew, and announcing it would have put a failure notice in front of the captain on every turn. On the laptop end, the refusal that became a device error covered the file-backed playback and capture too, so a mistyped --in-file was reported as an audio device failure and the advice named the flag that had just failed. The file ends now report the path and the flag that chose it and stay an OSError; the device ends keep the device advice and name the flag for that end. The device paths remain unrun here, so only the file halves are covered by a test. * no-mistakes(test): survive model session end, order client turn frames * no-mistakes(document): sync voice relay docs with reviewed relay behavior * no-mistakes(document): re-measure relay latency and correct its cause * no-mistakes(document): correct measurement date and name the unmeasured SSH hop * no-mistakes(document): describe the unpublished control measurement, fix list formatting * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes
…unchenguid#2763) * fix: keep Relay public loops open until retire Delivering a promised-final reply was deleting the only record that tied a public thread to later work, so a follow-on ship silently owed no closing reply. Retain the registration after delivery, rechain follow-on work onto the same thread, and make retire --reason the only close. * no-mistakes(review): Propagate public follow-up registration removal failures * no-mistakes(review): Persist retire receipts and align parent resolution * no-mistakes(review): Make rechain resumable after partial obligation creation * no-mistakes(review): Repair follow-up state, briefs, and expiry escalation * no-mistakes(review): Serialize follow-up delivery stamps with retirement * no-mistakes(review): Serialize rechain claims and protect registration terminal states * no-mistakes(review): Avoid reporting retired delivery loops as open * no-mistakes(document): Refresh public-loop documentation and verification evidence * no-mistakes: apply CI fixes * no-mistakes(review): Preserve delivered follow-up bindings during registration replay * no-mistakes(review): Harden public follow-up retirement and rechain races * no-mistakes(review): Fail closed on unresolved secondmate retirement * no-mistakes(review): Bind secondmate cleanup to its recorded canonical home * no-mistakes(review): Fix rechain command output and expiry validation * no-mistakes(review): Validate brief keys and warn on remote promotion * no-mistakes(document): Document retained public follow-up loops * no-mistakes(lint): Remove unused bounded-wait loop variable
…ath (kunchenguid#2779) * feat(bin): merge GitLab merge requests through the guarded PR merge path bin/fm-pr-lib.sh already parses a GitLab merge request URL for the watcher, but bin/fm-pr-merge.sh refused every non-github provider, so a merge request had to be merged by hand and got none of the recording, guards, or audit trail a pull request gets. The merge path now dispatches on the parsed provider. A GitHub URL keeps its exact previous behavior. A GitLab URL is addressed through glab by the project URL rebuilt from the parsed host and path, so a merge request on any instance resolves and no host is hardcoded, and no merge-method flag is added because the project's own merge method is what should apply. A GitLab merge happens only after one live read of the merge request confirms it is open, detailed_merge_status is mergeable, has_conflicts is false, blocking_discussions_resolved is true, and the head pipeline succeeded at the exact current head. Every failing condition is reported, not just the first. The verified head is bound to the merge with glab's --sha, so a push landing between the read and the merge fails the merge instead of landing commits nothing verified. Recorded metadata is never the authority for any of this: a rebase moves the head and leaves a recorded value stale, so a recorded head that disagrees with the live one is reported rather than trusted, and the recorded value is read before the recording step because that step drops a GitLab head it cannot resolve. * no-mistakes(review): reject bundled -R clusters and make tool-absence cases host-independent * no-mistakes(test): state authorised GitHub narrowing of bundled -R guard This branch NARROWS GitHub behaviour. The narrowing was authorised deliberately rather than slipping in by accident, and it applies to both providers, GitHub and GitLab alike, because a script that guards one provider and not the other is a trap for the next reader. What bin/fm-pr-merge.sh now refuses is extra merge arguments containing a bundled short-option cluster that includes R, for example "-dR other/repo". The forge CLIs expand such a cluster one character at a time, so it carries "--repo other/repo", and that later value wins over the repository the URL named. Before this change, "fm-pr-merge.sh <task> <github-url> -- -dR other/repo" reached "gh-axi pr merge 12 --repo example/repo --squash -dR other/repo" and exited 0 with pr= recorded and the merge poll armed. It now exits 1 with "extra merge arguments must not override the repository", records nothing, and invokes no forge merge command. Every other GitHub invocation is byte-identical to the base commit. Closing that hole honours the existing rule rather than departing from it. The file header already forbids --repo and -R because the repository must come only from the URL, so a bundled cluster carrying a repository override was never legitimate behaviour to preserve: it was that guard being evaded. Redirecting a merge to a repository the URL does not name is exactly what the guard exists to prevent. The refusal is already pinned on both paths by the existing case test_bundled_repo_override_args_refuse_before_recording in tests/fm-pr-merge.test.sh. On GitHub ("-dR wrong/repo") and on GitLab ("-yR https://other.example/g/p") it asserts exit 1, the refusal wording, no pr= in the task meta, no armed merge poll, and no forge merge command invoked, with a control case proving a cluster that carries no repository override still reaches the forge. No duplicate assertion was added. Both assertions were confirmed to have teeth by narrowing the guard back to a bare -R and watching each path fail. This commit carries no file change: the guard and its coverage landed in 614853d, and this message exists so the pull request description states the narrowing. * no-mistakes(document): fix README pointer for GitLab watch and merge doc * no-mistakes: apply CI fixes
kunchenguid#2788) * no-mistakes: apply CI fixes * fix(bin): drop a private record citation and narrow the review rule Three corrections to the spoken interface that landed in kunchenguid#2767, plus one fix carried over from that branch after its pull request had already been merged. The confidentiality fix. The module docstring of bin/fm-voice-relay.py cited a private, gitignored fleet record by exact path and section number. That widens what this public repository points at, and it cannot resolve for any reader here, because the path has never been in the repository. Both traps it pointed at are already described in full in the list immediately below it, and docs/voice-relay.md carries the same two for operators with no citation at all, so the pointer is removed and no claim is weakened by losing it. Two comments that referred to "the survey" as though it were something a reader could open are reworded the same way. Neither exposed a path, so that half is comprehensibility rather than confidentiality. The review rule. .greptile/rules.md is kept, because its conditions are right and deleting it would leave the next reviewer to re-litigate a decision already argued out. What was wrong with it is narrower than its existence: it read as settled repository policy, when whether VISION.md itself should be reconciled is an open question belonging to the captain. One sentence now says so, and says that the conditions listed below it are what the interpretation depends on. That narrows the claim rather than widening it. The carried-over fix. The first commit on this branch is 7f98e79 from fm/voice-relay-build-v4, taken verbatim rather than rewritten. It closes the window where a transport failure was recorded and then erased, so a run could be emitted as answered false with relay_error null. That matters more than it looks: relay_error is the field that keeps an infrastructure failure from being averaged into a latency figure, so the failure mode is a dead connection wearing the costume of a slow reply. It landed fifteen minutes after kunchenguid#2767 merged and so never reached the default branch. * no-mistakes(review): name a reason on every unanswered-turn close path * no-mistakes(review): guard the downlink body and pin frames to their turn * no-mistakes(review): attribute reply audio to its own turn and tell endings apart * no-mistakes(review): tell a cut-short reply from an unanswered turn * no-mistakes(review): discard reply audio arriving after the output closes * no-mistakes(review): count discarded reply audio on the speaker path too * no-mistakes(review): keep a reason off a turn already answered in full * no-mistakes(review): say a reset cut a reply short, not that none arrived * no-mistakes(review): read one turn's audio count once, and hush a tidy exit * no-mistakes(document): fix stale session-end relay_error claim in voice-relay guide
…unchenguid#2811) A pi worker parked on an interactive prompt - a permission dialog, a question menu, a trust dialog - reports agent_status=blocked, because it is waiting on a human keystroke. Pi draws that menu above its separator pair, so the composer region between the rules is blank and structure alone looks like a free composer. _fm_composer_pi_verdict admitted blocked alongside idle and done, so the shared classifier reported an affirmatively empty composer for exactly the pane where typing is unsafe. Every "is it safe to type here?" consumer reads that verdict and proceeds only on an affirmative empty, so both are told yes on a parked prompt: the away-mode injection guard in bin/fm-supervise-daemon.sh, and fm-send's pre-type refusal. The keys then answer the menu instead of composing a message - the highlighted default is selected, the text is discarded, and the record attributes a decision to a human who never made it. blocked now defers to unknown, which every consumer already treats as fail-closed. idle and done still prove an empty composer, so ordinary steering is unchanged, and Cursor is unaffected because its always-blocked panes never reach this pi-only branch. Regression coverage lands first at both levels: the verdict owner (a blocked pi defers) and the herdr adapter (a parked pi prompt is not an empty composer).
…2849) * fix(bin): require a clone root before fleet-sync touches a project Git repository discovery walks upward, so `git -C projects/<dir>` on a plain directory nested under projects/ resolves to the enclosing repository - in a firstmate home, the firstmate checkout itself. fm-fleet-sync.sh guarded its candidates with `rev-parse --is-inside-work-tree`, which such a directory passes, so every later git call read, pruned and fast-forwarded firstmate's own default branch and reported it under the project directory's label. A running session's AGENTS.md changed underneath it, and the report named a project that had nothing to do with the change. Require each candidate to be the root of its own work tree before any other git command: compare `rev-parse --show-toplevel` against the directory's own physical path. Both sides are physical, so a symlinked clone still compares equal. Anything else is skipped by name, naming the repository that would have been touched, and bootstrap relays that as a FLEET_SYNC line. Regression coverage reproduces the wrong-repo fast-forward against a home nested inside another repository, in both the whole-fleet and single-project forms, and pins that a symlinked clone dir still syncs. * no-mistakes(review): Keep enclosing fixture clean during clone-root regression
* fix(procevent): retry a transient Lavish poll interruption quietly
A live Lavish listener can be cut short by the server with exactly
error: Lavish Editor poll response was interrupted
code: SERVER_ERROR
while the session's marks remain available. Firstmate registered raw
`lavish-axi poll` output, so the generic process-event runner captured
that transient response as a result and woke the whole fleet over what is
really an internal retry.
The Lavish adapter now registers its own listener command, which reruns
the published blocking poll up to 12 times at 5 second intervals for that
one exact two-line response. The match is deliberately narrow: real
feedback, ended and missing sessions, any other SERVER_ERROR, and the same
interruption still standing once the bound is spent all pass straight
through and are captured and announced as before. The retry is a Lavish
fact, so the generic runner stays adapter-agnostic.
`FM_LAVISH_POLL_RETRY_DELAY` is a bounded 0 to 60 second override for the
interval only, refused rather than rounded when malformed, so a test can
exercise the real bound without waiting it out.
* no-mistakes(review): Harden Lavish retry matching, validation, and cleanup
* no-mistakes(review): Bound Lavish retry staging and stabilize regression
* no-mistakes(document): docs: explain Lavish retry adoption
* no-mistakes(lint): Restore Lavish trap ShellCheck suppression
… gate (kunchenguid#2838) The unguarded Herdr declaration quoted `{TASK}` in its own prose while the scaffold instructs firstmate to replace every `{TASK}` placeholder. The documented global replace therefore spliced the whole task body into the middle of the safety gate's sentence, silently destroying the one contract that exists precisely because the scaffold cannot inspect the task text. Reword the gate to refer to the task text filled in above, leaving the placeholder only at its genuine fill site. Rewording rather than renaming the token keeps the unfilled-charter guards in fm-home-seed.sh and fm-remote-home-seed.sh working unchanged. Add a regression test that performs the documented global fill on ship and scout scaffolds and asserts the body lands once and the gate survives.
…tat form (kunchenguid#2837) The writer lock's stale-lock branch read the lock's mtime with `stat -f %m ... || stat -c %Y ...`. On GNU coreutils `-f` is filesystem stat, so it consumed the format string as a path, complained on stderr, printed a partial filesystem dump (" File: ...") on stdout, and still exited 0. The GNU form in the fallback therefore never ran, and the following arithmetic evaluated the word `File`, aborting the writer under `set -u` with "File: unbound variable". fm-teardown.sh died there after returning the worktree, leaving state/<id>.meta, .status, .busy-gen, .busy-state, .busy-state.lock/ and .turn-ended behind. The surviving metadata kept the watcher monitoring an endpoint whose agent was gone, so a finished task produced stale wakes forever, and every re-run died identically because the abandoned lock was never broken. Detect the platform once and pick the right stat form, the pattern bin/fm-watch.sh already documents, and treat any non-numeric result as "just created" so a future portability surprise degrades to a lock-timeout refusal rather than killing teardown mid-way.
* fix(stow): give memory decay a per-pass horizon so the clock fires The tiered decay clocks were wall-clock only, while admission is per-pass: each /stow admits the findings that pass produced. In a home that stows daily those two rates diverge by the stow cadence, an entry the fleet keeps exercising never reaches 30 days unreinforced, and memory only grows while the pass reports decay evaluated. Give each dated marker an optional unreinforced-pass counter and make both tiers stale at whichever horizon comes first: 10 passes or 30 days for aging, 3 passes or 7 days for perishable. Reinforcement clears the counter and nothing else does, so the existing evidence-based restamp rule stays the only way an entry renews its lease. An absent /N means zero, so entries that stay exercised carry no extra marker bytes, and a rarely stowed home keeps its current behaviour through the unchanged date horizon. * no-mistakes(document): Align stow workflow with dual decay clocks * fix(stow): make the per-pass decay horizon opt-in The unreinforced-pass horizon shipped as a new default archival cadence, which is a product default rather than a restoration of the existing wall-clock contract. Keep the 30-day and 7-day horizons as the only default clock, and put the 10-pass and 3-pass horizons behind an explicit opt-in: config/stow-pass-horizon for the firstmate home, and the file's own header pointer for the public skill. With the opt-in absent no counter is written and no counter is read, so a home that does not ask for it decays exactly as it does today. * no-mistakes(review): Preserve frozen counters and correct archive provenance
…artup (kunchenguid#2876) tests/fm-watcher-lock.test.sh passed in isolation but failed intermittently under full-suite and ambient concurrent load. bin/fm-watch-arm.sh computes its confirmation deadline immediately after forking the real child watcher, so the child's entire fork, exec, lock acquisition and beacon publication has to land inside that wall clock. Two cases shrank that budget to one second, leaving a two-second window for work measured at 3.1-4.9s under CPU oversubscription, so the arm honestly reported "FAILED - no live watcher with a fresh beacon" and their premises collapsed. A third case ran on the production budget, but its child must also execute a registered check before exiting: measured at 1.9-2.3s idle and 9.1-13.1s under load, against an 11s budget. The two cases that must confirm a real child now hold the arm to production's own budget instead of a shrunken fixture one, the immediate-wake case gets an explicit budget with headroom over its measured loaded cost, and the two waits for the arm's typed failure are sized off the largest production default rather than a fixed eight seconds. No bin/ change and no default behavior change: the lock's fail-closed semantics, SIGSTOP handling, stale-heartbeat detection and the arm's typed failures are untouched. Verified 4/4 green at 3x CPU oversubscription (loadavg 75-80) after 3/3 red before the change, and CONTRIBUTING.md records the convention.
* fix(bin): order discovered tool installs by the shell's own expansion fm_remote_job_compose_operator_path built the asdf and mise install directories with `compgen -G`, which does not sort. Bash sorts glob matches in pathexp.c, on the shell's own pathname-expansion path only; `compgen -G` reaches the same glob_filename through pcomplete.c, which sorts nothing. On bash 3.2 (macOS /bin/bash) and every bash before 5.3 that handed the composition raw readdir order, so which install of a multi-version tool a remote job resolved was decided by directory order on disk rather than by this composition. Expand the globs at the call sites and let the function take the matches, so the composition and the documented portable-PATH contract are the same operation. Quoting the account home at the call site also stops a home whose name contains glob metacharacters from being reinterpreted. The colocated regression pins both the order and the mechanism: bash 5.3 moved sorting into the glob library, so an order-only assertion cannot see the defect there. * no-mistakes(review): Remove source-reading PATH regression guard
…2848) * fix: surface stalled secondmate queues and wake handoffs * no-mistakes(review): Make handoff wakes retryable and stall alerts crash-safe * no-mistakes(review): Prevent duplicate handoff wakes and cover remote delivery * no-mistakes(review): Serialize local handoffs and preserve pre-move wake intent * no-mistakes(review): Serialize teardown with handoffs and retain remote wake confirmation * no-mistakes(review): Reconcile correlated handoff wake delivery after crashes * no-mistakes(review): Keep failed wakes retryable and isolate stall receipts * no-mistakes(review): Reset known-undelivered wake attempts for durable retries * no-mistakes(review): Refuse duplicate sends for unresolved delivery attempts * no-mistakes(review): Atomically restore retryability after reconciled send failures * no-mistakes(review): Serialize delivery confirmation with reconciliation * no-mistakes(document): Document routed wake and stall supervision * no-mistakes(lint): Fix ShellCheck expansion and subshell warnings * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes(review): Retire stale wake state and defer pre-move wakes * no-mistakes(review): Secure markers, bind batches, and preserve teardown routes * no-mistakes(review): Preserve unresolved prepared wakes across unrelated handoffs * no-mistakes(review): Preserve prepared wakes before unrelated moving handoffs * no-mistakes(document): Document prepared wake batch ownership * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes(review): Make local wake retirement recoverable * no-mistakes(document): Clarify handoff recovery and teardown documentation
…guid#2856) * feat(bin): steer local tasks by durable inbox record plus constant doorbell Stage 1 (local steers) of the captain-adopted reframe in data/fm-send-reliability-reframe-s1/report.md: an ordinary fm-send text steer to a task recorded in this home is appended as a sequenced durable record under state/<id>.inbox/ and the terminal receives only one constant self-describing doorbell line, best-effort. The worker acknowledges by moving the record into handled/; the watcher re-rings an unacknowledged message on an idle pane and escalates once as an ordinary stale wake. --resolve-key closes decisions at enqueue time, because the durable enqueue IS delivery to the task's record. bin/fm-task-inbox-lib.sh owns the record format, doorbell line, and re-ring ladder. The typed plane remains for what must reach the terminal itself: lifecycle keys, harness-native slash and codex $-skill invocations, explicit backend targets, and the remote secondmate leg (unchanged until the remote inbox leg ships separately). The composer classifier is demoted from delivery proof to an advisory ring guard that skips only on a proven pending verdict. Verified live against claude, codex, opencode, pi, grok, and muse: each real worker read its record, acted, and acked with the mv (docs/verification/runtime-backends.md "Steering-inbox doorbell"). * docs(verification): flag the grok 1.0.5 composer-matrix staleness observed by the doorbell run * test(captain-hold): read the chat-channel answer from the durable inbox record * test: migrate fm-control's marker contrast to the inbox record and fix macOS wc padding in the tool-update suite * no-mistakes(review): Harden inbox locking, teardown races, and acknowledgements * no-mistakes(review): Serialize watcher actions with inbox acknowledgements * no-mistakes(review): Bound metadata locking and tighten acknowledgement rechecks * no-mistakes(review): Preserve exact inbox bytes and harden delivery recovery * no-mistakes(review): Harden watcher bookkeeping against concurrent inbox teardown * no-mistakes(document): Update inbox and typed-plane documentation * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * revert(pipeline): keep parser-native secondmate marking and the both-failed exit out of stage 1 The CI monitor's fix changed the secondmate marking contract for parser-native invocations (appending the marker after the text) and softened the both-commit-and-marker-failed branch to exit 0. The merge authority ruled the marking question out of scope for this stage-1 transport PR (follow-up: fm-send-secondmate-harness-invocation-r1) and ruled the both-failed case a loud nonzero local failure. Restore both, keeping the monitor's legitimate migrations and hardening. * no-mistakes(document): Document inbox and typed-plane boundaries * no-mistakes(document): Scope backend transport docs to typed plane * no-mistakes(document): Clarify inbox attempt-budget documentation * no-mistakes: apply CI fixes * fix(send): the durable record alone governs the inbox exit status Captain-refined ruling on the F2/Greptile finding: the durable inbox record is what delivers the steer, so pending-reply bookkeeping trouble after a successful enqueue never exits nonzero - a resend-inviting status would make automated callers enqueue the delivered instruction again under a new sequence. With the recovery marker stored the watcher reconciles silently; with the commit and marker both lost the send surfaces a distinct reply-tracking-degraded do-not-resend warning and still exits 0. Nonzero remains only where nothing was delivered (or a decision close needs its manual command). Regression: record durable + both bookkeeping writes lost -> exit 0, one record, no duplicate. * no-mistakes(review): Preserve inbox ordering with drain-all doorbells * no-mistakes(review): Surface unwritable inbox ladder bookkeeping * no-mistakes(review): Silence ladder failures after inbox acknowledgement * no-mistakes(document): Update steering inbox documentation * no-mistakes: apply CI fixes
* feat: add fast local lint mode * fix: preserve complete fm-lint help * fix: isolate fast lint mode * no-mistakes(document): Clarify lint mode documentation ownership * no-mistakes: apply CI fixes
…#2901) * feat(bin): deliver remote secondmate steers through durable task inboxes Stage 2 of the inbox+doorbell steer channel (stage 1: kunchenguid#2856). A remote secondmate steer now crosses fm-on.sh as a durable record written idempotently into the remote home's steering inbox plus a best-effort remote doorbell, and the last typed-payload steer transport is deleted: - fm-remote-secondmate-control.sh cmd_send writes the record via the new fm_task_inbox_write_idempotent and rings the doorbell; it no longer types the payload through an inner fm-send at an explicit pane target. - fm-send.sh routes every remote text steer (harness-native included, which marking already reduced to chat) onto the remote inbox leg, retries the identical leg once on ssh 255, closes --resolve-key decisions at enqueue for remote too, and preserves a marked request's reply expectation when completion stays unknown. The exit-3-as- delivered remap, the 255 do-not-resend trap, and the remote typed submit block are removed. - fm-task-inbox-lib.sh owns the idempotent enqueue: an exact-body re-run lands on the existing record, handled or not, so an ambiguous transport can always be safely re-run. - Tests pin the new contract end to end (record + doorbell + no typed payload across ssh, one-record idempotence under an ambiguous transport, enqueue-time decision close, loud real failures, and the deleted typed-payload behaviors gone), and AGENTS.md plus docs/remote-secondmates.md describe the remote leg's new semantics. * no-mistakes(review): Harden remote inbox delivery against lifecycle races * no-mistakes(review): Enable correlation-preserving remote steer resends * no-mistakes(review): Fail closed on stale correlation resends * no-mistakes(review): Include home context in remote resend commands * no-mistakes(review): Lock and revalidate remote parent routes * no-mistakes(document): Clarify remote steer retry documentation * no-mistakes: apply CI fixes
* wip: forked supervision on Pi (checkpoint before docs) * fix(pi-branch): harden mirror delivery, fallback encoding, and session replacement Peek-then-shift mirror flush so a failed append retries instead of dropping; durable mirror cursor commits only after delivery into the branch; the main fallback wake is operational-encoded like every watcher injection; session_shutdown quiesces the generation and session_start re-arms, so /new and /resume no longer kill the branch permanently. Registers the extension in the strict typecheck, adds the dispatch handshake test, the branch extension suite, the bash-level regression suite, the session-start replay test, and the opt-in real-SDK live guard. * test(fixtures): carry the branch-dispatch lib and lease lib into isolated fixtures The watcher extension now imports lib/fm-branch-dispatch.ts and fm-teardown sources fm-lease-lib.sh, so every fixture that copies or symlinks those files in isolation gains the new sibling. * no-mistakes(review): Prevent shutdown wake loss and serialize lease claims * no-mistakes(review): Durably hand off wakes and retain portable leases * no-mistakes(review): Require durable reports and clear disposed branch leases * no-mistakes(review): Enforce per-wake outcomes and quiescent lease cleanup * no-mistakes(review): Require wake acknowledgements and tighten branch lifecycle boundaries * no-mistakes(review): Require complete acknowledgements and replay cleanup failures * no-mistakes(review): Bind supervision to lock ownership and durable delivery * no-mistakes(review): Activate branch lazily after session lock acquisition * no-mistakes(review): Preserve undelivered mirror context across extension rebinds * no-mistakes(review): Acknowledge startup replay only after main delivery * no-mistakes(review): Isolate replay metadata from untrusted digest content * no-mistakes(review): Reject duplicate reports for active wake sequences * no-mistakes(review): Retain failed fallbacks and deduplicate outcome replay * no-mistakes(review): Deduplicate durable outcomes and cache delivery receipts * no-mistakes(review): Anchor wake sequence matching to outcome fields * no-mistakes(document): Clarify Pi supervision durability contracts * no-mistakes(lint): Fix ShellCheck issues in branch supervision scripts * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * refactor(pi-branch): collapse to confused-agent-grade guards per captain decision Captain decision A: the lease/actor guards target the CONFUSED-AGENT threat model bin/fm-gate-refuse-lib.sh already documents; adversarial-grade separation is impossible in the shared-process design and is filed as separate follow-up work. Rip out the machinery that chased it: the generation fence and shell-provenance markers, the wrapper-tagged ancestry walks, guard auto-claim with per-script release traps, the pending-wake files and ack-receipt correlation (the durable wake queue already re-presents anything unacknowledged), the delivery-receipt store with contiguous cursor advancement, the session-start replay-metadata channel, and the branch tool quiescence counters. Keep the behaviors the board requires, each on its simplest implementation: lazy per-action session-lock ownership (cold start activates after the lock lands; a secondary session stays inert), mirror durability across extension rebinds via the durable cursor, replay-exactly-once from the one read cursor, the awaited operational-encoded fallback, per-generation stray-lease cleanup, session-lock-bound lease liveness (a recycled pid or a non-Pi home never honors a leftover lease), the loud accidental-override guards (readonly actor prelude, cross-actor claim refusal), and the role-partition refinements (no forced teardown, no direct relaunch for the branch). Default-on-for-Pi is unchanged. * no-mistakes(review): Enforce lock ownership and serialize lease mutations * no-mistakes(review): Synchronize guard cleanup and bind leases to lock owner * no-mistakes(review): Report outcomes before acknowledging durable wakes * no-mistakes(review): Restrict leases to Pi and instruct main claims * no-mistakes(review): Reject malformed lease locks and torn outcome tails * no-mistakes(review): Validate complete outcome tails before appending * no-mistakes(review): Guard branch side effects across session replacements * no-mistakes(document): Update Pi supervision durability and lease documentation * no-mistakes(lint): Suppress intentional nested-shell expansion warning * no-mistakes: apply CI fixes * fix(pi-branch): authorize lease releases by caller * fix(lint): break redundant source-analysis path in fm-lease-lib.sh fm-lease-lib.sh's lazy fallback source of fm-wake-lib.sh gave ShellCheck's --external-sources traversal a second path into an already 1540-line file that fm-send.sh and fm-teardown.sh also source directly, blowing up the recursive analysis past CI's lint timeout. Mark it a source=/dev/null analysis boundary, matching the existing fm-task-inbox-lib.sh convention. Also restores bin/fm-lint.sh and tests/fm-lint.test.sh to the shared serial-lint definition (dropping an unrelated parallel-sharding change that was itself hanging and masked this root cause). * no-mistakes(document): Correct lease caller-authorization documentation * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes
* feat(bin): parallelize session-start remote secondmate network sweeps Run per-secondmate liveness and convergence probes concurrently and overlap clone refresh, while replaying each mate's fail-closed diagnostic in original order. Ignore scratchpad* so untracked scratch no longer blocks remote sync. Co-authored-by: Cursor <cursoragent@cursor.com> * no-mistakes(document): Document parallel startup network sweeps * no-mistakes(lint): Fix empty environment assignment lint warning * no-mistakes: apply CI fixes --------- Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(tests): count declared-pause wakes without crashing on an absent queue The exited-declared-pause case counts queued stale wakes by handing state/.wake-queue straight to awk. A watcher that queues nothing never creates that file, and awk aborts on a missing path before its END rule runs, so the count collapses to the empty string. The next comparison then fails as an integer-expression error and surfaces as a wake flood with no number, hiding the real contract breach the following grep names. Read the queue the way the drain-count assertion at the end of this file already does: silence awk's open error and default an absent queue to zero. Applied to all four counts in this case, including the live external-decision gate pair whose queue an acknowledged drain can also leave behind. An absent queue now reports "did not use the bounded paused recheck", while a genuine flood still fails with its real count. Fixes kunchenguid#2628 * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes
* fix(bin): bound remote worker supervisors * no-mistakes(review): release incumbent supervisor before starting its replacement * no-mistakes(review): wait out a healthy same-root supervisor instead of replacing it * no-mistakes(review): narrow remote worker change to restart accounting only * no-mistakes(document): clarify supervisor restart guard is a lifetime total
* feat(bin,pi): per-actor wake consume, silent success gating, merge-poll dedup Three related fixes to the shared wake-drain and Pi supervision-branch dispatch machinery so a routine success is never main-blocking and a mixed queue can safely split between actors. 1. Successful routine results no longer create main-blocking wake rows. fm-startup-network.sh only enqueues a check: startup-network wake when the deferred result is actionable (state is not "done", or the report carries a bootstrap-diagnostics actionable prefix); a clean success stays durable in the report file without ever waking the agent. 2. Per-actor wake-drain consume contract. bin/fm-wake-drain.sh now scopes presentation and --ack-through to the current actor (bin/fm-lease-lib.sh's fm_lease_actor): main keeps the original whole-queue cutoff behavior, unaffected. A branch actor (FM_SUPERVISION_ACTOR=branch, set only inside the Pi supervision branch's own bash tool calls) is scoped to an explicit eligible-row snapshot instead of a cutoff comparison, so it can never remove a row it was not granted - the fix for the swallow risk that used to force an all-or-nothing whole-queue fallback to main. .pi/extensions/lib/fm-branch-dispatch.ts's scopeForUnreadWake is the single owner of eligibility: a check-kind row (merge-confirmation polls, Relay mentions, credential/auth failures) is now excluded rather than vetoing the whole scan for a non-heartbeat wake, while a heartbeat review keeps its original all-or-nothing rule unchanged. writeEligibleRowsSnapshot publishes the exact eligible sequence numbers before every branch prompt; fm-primary-pi-watch.ts's offer still refuses a check-kind trigger outright so a main-only close is never itself routed to the branch. 3. A repeat identical merged-PR-poll result for an already-notified task is absorbed instead of enqueued again. A poll's own retirement state is scoped to one registration and cannot see a prior registration's outcome, so a task re-registered after its merge was already surfaced would otherwise wake main a second time for the same event. bin/fm-pr-lib.sh's new per-task pr-poll-merge-notified marker survives across re-registrations to catch that case; the first notification for a task still reaches main unchanged. Regression tests colocated in tests/fm-startup-network.test.sh, tests/fm-wake-queue.test.sh (including the mixed-queue no-swallow property), tests/fm-pi-branch-extension.test.sh, and tests/fm-pr-check-security.test.sh. docs/watcher-continuity.md and docs/pi-supervision-branch.md updated for the new contracts. * no-mistakes(review): Bind merge deduplication to canonical PR identity * no-mistakes(review): Serialize wake row ownership across main and branch * no-mistakes(review): Bind branch grants and deduplicate within actor claims * no-mistakes(review): Fallback main-owned wake claims to main delivery * no-mistakes(review): Clarify silent startup success guidance * no-mistakes(review): Release residual branch grants after settled prompts * no-mistakes(review): Reject truncated wake rows as corrupted * no-mistakes(document): Document per-actor routing and silent startup success * no-mistakes(lint): Fix ShellCheck findings in wake grant and startup test * no-mistakes: apply CI fixes
* Hide branch outcome tool in Pi Calm * no-mistakes(review): Preserve stock outcomes rendering and document tool audit * no-mistakes(review): Document branch read tool audit disposition * no-mistakes(review): Match stock outcomes output sanitization * no-mistakes(document): Document Calm custom-tool visibility
* fix: delegate no-mistakes PR gate to pinned action * no-mistakes(document): Document commit-bound no-mistakes attestations
…uid#3028) * feat(pi): let operators pin a cheaper supervision-branch model Supervision is an easier job than the captain's own conversation, so the Pi supervision branch does not need main's model. A new /supervision-model command opens Pi's own selector over Pi's own catalog of credentialed models, plus a "Follow main" entry, and persists the pick as one <provider>/<model-id> line in this home's gitignored config/supervision-branch-model. Firstmate keeps no model catalog of its own. The branch resolves the pin at every branch build - the first wake of a cold start and the reopen after /new, /resume, /fork, or reload - so the choice survives all of them, and picking also releases the live branch so the next wake reopens the same persistent branch conversation under the new model. An absent, unreadable, or unparseable file means no pin and keeps today's behavior byte for byte: no model option is passed and Pi picks the branch's model exactly as before. A pin naming a model Pi cannot hand back is never silently downgraded onto main's model: the branch refuses to build and the wake falls back to the captain-facing main path naming the unusable pin, which is the extension's existing failure direction. The choice is home-local and not part of secondmate inherited configuration, matching the Pi Calm preference precedent. docs/configuration.md owns the operator-facing schema. Portable regressions cover pin-present on create and reopen, pin-absent default, the command's persistence, cancellation, and live rebind, and both unusable and unparseable pins. The opt-in real-SDK guard proves the vendor surface the pin reads and that an explicit model wins over the model a reopened session recorded. * no-mistakes(review): Fix supervision model runtime and rebind races * no-mistakes(review): Restrict supervision picker to isolated runtime models * no-mistakes(document): Document supervision branch model selection * fix(pi): make the supervision model pin authoritative on every reopen Clearing the pin with "Follow main" removed the file but the next branch build reopened the persistent branch session with no explicit model override, so Pi restored the model that session had recorded - the old pinned model - while the command reported that the branch now follows main. The same gap meant an absent pin did not reliably mean same-model-as-main once a home had pinned once. The pin file's current state now decides the model on every branch build, create and reopen alike, overriding Pi's session-state restore. With a pin, that model. With no pin, main's own current model is applied explicitly, tracked from the contexts Pi already hands the extension plus its model_select event, since the branch is built at wake time with no context of its own. Only when main's model is unknown, or this home's stored credentials cannot run it in the isolated branch runtime, does a build fall back to passing no override at all, which is the behavior from before the pin existed; the branch is never refused over model choice. The command's notification now reports the model actually applied, and says plainly when clearing the pin could not apply main's model instead of claiming a change that did not take effect. No credential handling changes: the branch still relies entirely on the stored credentials its own runtime already holds, and the picker stays restricted to models that runtime can resolve. Colocated regressions cover pin present on create and reopen, clearing the pin returning a reopened branch to main's model and specifically not the old pinned one, an unparseable pin behaving as no pin, and the unknown-main-model fallback to no override. * no-mistakes(review): Make unpinned supervision follow main model changes * no-mistakes(document): Correct supervision model documentation
…nguid#3079) * feat(pi): let /supervision-model pick the branch's reasoning effort Supervision is an easier job than the captain's own conversation, so the Pi supervision branch does not need main's reasoning effort any more than it needs main's model. /supervision-model now settles both in one flow: the existing model picker, then a follow-up effort picker built from Pi's own supported thinking levels for the model just chosen. Firstmate keeps no effort catalog of its own; the menu, the clamp, and the vocabulary all come from Pi. The pick persists as one line in this home's gitignored config/supervision-branch-effort, independent of the model pin: a captain may pin a model, an effort, both, or neither. The effort pin's current state decides the branch effort on every branch build - the first wake of a cold start and the reopen after /new, /resume, /fork, or reload - and overrides Pi's restore of whatever level a reopened branch session recorded, which is what keeps "Follow main" honest. With no pin, main's own current effort is applied explicitly and followed live through Pi's thinking_level_select event, the same way an unpinned branch already follows main's model, and the two selections now share one build revision so either change invalidates an in-flight build. The branch is never refused over effort. Pi owns the clamp, so a pinned level the branch's model cannot run becomes that model's nearest supported level while the captain's raw pick is kept for a model that supports it, and the command reports the level the branch will really run at rather than the raw pin. A token Pi would not recognize at all is treated as no pin rather than passed to that clamp, which would otherwise collapse a typo into the model's lowest level. Only when main's effort cannot be read either does a build pass no effort override at all, which is the behavior from before this file existed. Pi's own effort vocabulary is pinned by a bidirectional type assertion against Pi's getThinkingLevel return type, so the tracked strict typecheck against the installed package fails the moment Pi adds or removes a level. docs/configuration.md owns the operator-facing schema for both pins. Portable regressions cover the pin on create and reopen, model-only and effort-only pins working independently, clearing a pin returning the branch to main's effort, live-follow of a mid-session change, the clamp, an unrecognized token, the unknown-main-effort fallback, and the command's two-step flow, persistence, cancellation, and honest reporting. The opt-in real-SDK guard proves the vendor surface all of that rests on, and also repairs a pre-existing gap that left it unable to load the extension at all. * no-mistakes(review): Resolve effective branch effort honestly * no-mistakes(document): Clarify Pi-owned effort picker behavior
…3093) * fix(supervision): silence empty board closes and decouple the heartbeat Two unrelated sources of noise put routine supervision events in the captain's chat. An empty Lavish board close - the captain reads a review surface, says nothing, and closes it - became a check wake whose entire content was that nothing happened. Suppress it at its source instead of routing it anywhere: the generic runner gains a `silent` adapter seam mirroring the existing `terminal` one, and the Lavish adapter answers it for exactly one positively-determined shape, an `ended` session carrying no queued content block. A silenced result is recorded durably handled so it does not return on a later reconcile. Everything else announces unchanged - a `Send & End` close carrying the captain's real answer, an `ended` result still carrying content, a waiting or missing session, an unreadable result, and every adapter that implements no `silent` command at all. The keyed-answer feed is untouched, so suppressing an announcement never suppresses the captain's own answer. A fleet heartbeat was deferred to main merely because some unrelated check row happened to be sitting unread, which put a routine fleet review in the chat for a reason that had nothing to do with the fleet. A check row is permanently main-owned, so it is now excluded from a heartbeat claim rather than vetoing the scan, exactly as in every other mode. What all-or-nothing guarantees is unchanged: the branch takes every branch-ownable unread row or none of them, and an unresolvable task-local row, an unknown row kind, or an unreadable queue still defers the whole review to main. Main is still woken for the check on that check's own triggering close, so nothing starves. Main-only classes are unchanged and now each covered by a test: Relay mentions, credential failures, merge confirmations, real board answers, and watcher-failure repair. The per-actor acknowledgement and no-cross-swallow properties are untouched. * no-mistakes(review): Fail closed on all Lavish content headers * no-mistakes(review): Suppress false unacknowledged status for silenced results
kunchenguid#1967) * fix(classify): read the decision fold through a correlation token status_line_verb stripped a trailing [key=...] from a status line's prefix but left everything else glued to the verb, so a line carrying the correlation token bin/fm-pending-reply-lib.sh embeds and a secondmate echoes back matched no arm of _fm_decision_fold_line. Such a line folded as ordinary status in both directions: a needs-decision or blocked opener never opened its key, and a resolved or captain-held closer never closed one. The same glued verb also hid correlated done and blocked lines from status_is_captain_relevant and status_is_terminal_verb, and let correlated working and resolved lines leak through the free-text fallback the nonterminal guard was meant to stop. The verb parse now walks whole words and drops only a token of the exact shape a firstmate library writes - corr=<16 hex>, plus the bracketed form bin/fm-secondmate-report.sh emits - before or after the key token, unkeyed, or doubled. An arbitrary name=value word is deliberately NOT skipped: skipping unknown tokens would let free text carrying an equals sign reduce to a bare verb and impersonate a transition, which is the takeover the strict parse and _fm_decision_key_transition_allowed exist to prevent. A prefix with no corr= substring is returned byte-for-byte as before, so every line without a token keeps its exact historical verb. FM_OPEN_DECISIONS_FOLD_VERSION goes to 3, because every cursor persisted under the previous reading carries an open set computed while correlated lines were invisible and must be rebuilt from byte 0. Measured over a real 383-line status log: 254 lines keep byte-identical captain-relevance, pause, terminal-verb and captain-held verdicts, and all 129 changed lines carry a valid token - 14 correlated done/blocked/ needs-decision lines become captain-visible, and 20 correlated working/resolved lines stop being escalated on prose alone. * fix(review): Captain, block token-first decision impersonation * fix(document): Clarify normalized status verb ownership * fix(classify): reconcile the correlation-token read with the tag-stop parser Rebasing onto main put this change beside kunchenguid#2280, which made verb parsing stop at the first "[name=value]" tag. Both edit status_line_verb with different intents, so the resolution keeps both rules rather than letting one overwrite the other: - kunchenguid#2280's tag stop is kept verbatim and now owns every BRACKETED tag, including the "[corr=...]" form fm-secondmate-report.sh writes. The bracket-unwrapping arm this branch had added to the token test is therefore removed as unreachable. - This branch's token walk is kept and narrowed to the UNBRACKETED token fm-pending-reply-lib.sh writes, which the tag stop does not reach. Two consequences of standing beside kunchenguid#2280 rather than before it: The fold version had collided at 4: kunchenguid#2280 spent it on the tag-stop parser and this branch had spent it on the token read. A cursor persisted under kunchenguid#2280's reading predates this one and must still be rebuilt, so the version moves to 5. A bracketed impostor is dropped from the malformed-token list. On main today "resolved [corr=deadbeef] [key=victim]:" already reads as the bare verb, as does "resolved [anything at all] [key=victim]:", because the tag stop ends the parse at the opening bracket regardless of content. That is kunchenguid#2280's reviewed contract; asserting otherwise here would narrow it. The unbracketed impostors it owns stay strict and still fold as prose. Adds a consumer test for the two verb-string case arms that postdate this branch: fm-supervise-daemon.sh's transient-stale arm and fm-crew-state.sh's map_log_state. * fix(review): Captain: Seed cursor migration fixture with version four * fix(document): Clarify voice status normalization ownership
…unchenguid#3115) * fix(bin): Cursor-Park unter Pi-Host stilllegen. pi-cursor-sdk lädt .cursor/hooks.json in die Pi-Sitzung und parkte einen zweiten Watcher; das erzeugte rearm-resurface und brach laufende Rückfragen ab. Bei PI_CODING_AGENT=true beendet der Park sofort, native cursor-agent Primaries bleiben unverändert. * fix(bin): Cursor-Park trotz PI-Leak nur ohne Cursor-Identität stilllegen. Stand-down gilt nur bei PI_CODING_AGENT=true ohne CURSOR_AGENT und ohne CURSOR_INVOKED_AS. Handgestartete cursor-agent Primaries mit geerbtem PI-Marker parken weiter. * no-mistakes(document): Document Cursor park Pi-host stand-down * fix(bin): no-mistakes-Mindestversion auf 1.46.0 anheben. Die PR-Attestierung verlangt ab 1.46.0 strukturierte Pipeline-Schritte; der Bootstrap-Floor blieb bei 1.31.2 und ließ zu alte Builds zu. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…chenguid#3099) * fix(pi): make /supervision-model's model list bounded and searchable Pi's generic extension selector renders every option at once with no search box, so a real eligible catalog ran off the top of the terminal. The model step now draws the same rows through Pi's own SelectList - the bounded scrolling primitive behind Pi's /model picker - with Pi's own Input and fuzzy filter above it for search, keeping 'Follow main' first, the branch-runtime eligibility filter intact, and the pick branch-only. Pi's ModelSelectorComponent is deliberately not reused: its selection handler writes the captain's default model through Pi's settings manager, which would move main's conversation as a side effect of pinning the branch. The effort step's menu is a handful of levels and stays on Pi's plain selector dialog. * no-mistakes(document): Clarify supervision picker documentation * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes * no-mistakes(document): Document searchable supervision model picker * no-mistakes: apply CI fixes
* fix: make a landed merge leave a durable outcome A merge was the one lifecycle event that left no record outside the merging agent's memory. bin/fm-pr-merge.sh ended at the forge call, and a home merging under standing authority never waits for the merge poll that would otherwise confirm it, so three real merges reached the captain as silence. bin/fm-merge-outcome-lib.sh is the single owner of that record. A secondmate home reports the landed PR upward on the same parent reply channel its terminal-outcome backstop already uses; a main home records it on the durable wake queue. The record is at most once per task and canonical PR identity, and only a merge that actually landed produces one. The merge poll feeds that same channel when it detects a merge this home did not perform, so the captain's own forge merge and a merge firstmate performed itself produce one consistent outcome instead of two reporting paths. No new state file and no second watch path. Two smaller gaps from the same failure: - A mate charter listed its report triggers without naming a landed merge. Under standing merge authority nothing is ever "ready for review", so the enumerated list silently omitted the case that matters. - A secondmate home seeded without its parent binding failed every terminal-outcome report for the same reason, and the diagnostic never named the binding. It does now. * no-mistakes(review): Harden durable merge outcome reporting * no-mistakes(review): Make merge race regression deterministic * no-mistakes(review): Make merge outcomes retry-idempotent and forge-confirmed * no-mistakes(review): Unify merge publication under canonical outcome marker * no-mistakes(review): Publish merge outcomes before committing dedup markers * no-mistakes(review): Document at-least-once merge outcome recovery * no-mistakes(review): Use supported GitHub confirmation and update recovery docs * no-mistakes(review): Preserve distinct merge wakes by PR identity * no-mistakes(document): Document durable merge outcome semantics * no-mistakes(test): Make merge outcome interleaving test deterministic * no-mistakes(document): Clarify merge outcome documentation ownership * fix(lint): keep the merge-outcome library an analysis boundary bin/fm-watch.sh followed the new merge-outcome library's source graph, which reaches the wake queue, PR identity, and secondmate parent libraries. Expanding that inside an already-large lint root pushed ShellCheck's external-source analysis past the bounded CI lint worker: the Lint job was killed with SIGTERM after five silent minutes, twice, having emitted no diagnostics at all. Make it an analysis boundary, exactly as the transition and inbox owners directly above and below it already are and for the same stated reason. Coverage is unchanged because the library is a canonical lint root in its own right and is still linted as one. Measured locally: the watcher goes from not terminating within 120s to 9s clean, and the library alone lints in 1s clean.
…chenguid#3129) * fix(bearings): keep an inventory-mismatch home readable, and mark warnings as repairs A backlog-vs-metadata inventory mismatch inside a secondmate home was being reported as "we cannot read that home", which discarded that home's open captain calls, queued work, landed work, and live workers from the whole Bearings digest. The main home already treats the identical mismatch as a harmless disclosure; this makes the secondmate path agree. - fm-fleet-snapshot.sh: the invalidity gate now passes orphan_in_flight, unowned_current, and terminal_in_flight through the partial-structured carve-out alongside child_current_unavailable, so those homes keep their decisions, holds, queued, landed, and live work and leave unreadable[]. missing_backlog and unstructured_current stay on the discard path, because there the backlog itself is untrustworthy. - fm-fleet-snapshot.sh: the same three kinds no longer collapse the home's own classification to "unknown"; the real captain_decision / active_child_work / externally_held classification survives and invalidity carries the warning. An unavailable child state still collapses it, including when a mismatch masks it under strict-invalidity precedence. - secondmate_landed.partial now keys on partial-structured trust rather than an unknown state, so an inventory-mismatch home is still disclosed as partial. Ask the home that owns the wrong books to fix them: - bin/fm-secondmate-reconcile.sh sends exactly one reconcile instruction per mismatch episode through the ordinary steering transport. A persistent mismatch keeps its episode identity and never re-nags; a changed mismatch earns one more ask; a repaired one is forgotten so a recurrence is asked about again. The parent never touches the mate's own files, and a failed send records nothing so the next run retries it. Give integrity warnings their own look on the board: - charted rows take an optional kind of "queued" (the default) or "warning". A warning badges "needs repair" instead of "waiting" and is excluded from the Charted Next count, so alarms stop reading as dispatchable queued work. No fifth board section, and every existing payload stays valid. Tests pin the new policy behaviorally: the retained surfaces and classification for all three mismatch kinds, the still-discarding unstructured_current and missing_backlog cases, the once-per-episode reconcile ask through real durable steering records, and the board rendering exercised through the shipped template under a minimal DOM shim. * no-mistakes(review): Make reconcile dedupe atomic and warnings non-dispatchable * no-mistakes(review): Preserve reconcile identity and reject stale snapshots * no-mistakes(review): Order snapshots uniquely and canonicalize episode identities * no-mistakes(review): Add fire-and-forget reconcile and separate warning overflow * no-mistakes(review): Exclude fire-and-forget from escalation and track reconcile background * no-mistakes(review): Run reconcile enqueue inline across all adapters * no-mistakes(review): Track reconcile clears across strict-invalidity homes * no-mistakes(review): Persist reconcile transitions atomically * no-mistakes(document): Document reconcile and fire-and-forget contracts * refactor(bearings): replace the reconcile episode dedupe with a 4-hour cooldown The reconcile ask needed to fire once per problem without nagging on every recap. The episode-precise record that tried to do that had to be correct in every direction at once - order two concurrent snapshots, tell a repair from a new problem, and never lose a clear - and each direction it got wrong either swallowed a nudge or sent a duplicate. A per-home cooldown removes the whole class. One durable timestamp per home, one nudge per four hours, and nothing to get stale, mis-order, or mis-classify: a home in mismatch is asked once, later recaps stay silent, and a mismatch still sitting there after the window earns one gentle re-nudge. - bin/fm-secondmate-reconcile.sh: state/<id>.reconcile-nudged holds the epoch second of the last ask; FM_RECONCILE_COOLDOWN_SECONDS names the window. The episode identity, ordering generation, pending/clear transitions, and delivery-identity reuse are all gone. A known-undelivered send starts no cooldown so the next run retries it; an unconfirmed one does, because a duplicate ask is worse than one the mate may already hold. - bin/fm-fleet-snapshot.sh, bin/fm-bearings-snapshot.sh: drop the snapshot `observation` monotonic identity, which existed only to order those records. - bin/fm-teardown.sh: retire the cooldown record with the endpoint's other runtime artifacts, so reseeding a retired id is not silenced by its predecessor's window. The inline durable fire-and-forget send is unchanged, and the projection fix and the warning surface are untouched. Tests follow the behavior: the cooldown suite now pins one ask per window, the re-nudge after it, the four-hour boundary, per-home independence, and that the ask stays out of a re-ring ladder that still rings an ordinary steer beside it. The obsolete observation-ordering test is deleted with the machinery it covered. * no-mistakes(review): Serialize reconcile cooldown commits with mate lifecycle * no-mistakes(review): Reject stale reconcile snapshots across mate reincarnations * no-mistakes(review): Start reconcile cooldown after delivery completes * no-mistakes(review): Keep reconcile sends nonblocking and remove pending residue * no-mistakes(document): Document reconcile skip and stale-endpoint behavior * no-mistakes(lint): Fix reconcile test subshell lint warning * no-mistakes: apply CI fixes
…#3140) * fix(bin): stop dropping reconcile nudges for markerless remote secondmates A persistent remote secondmate's parent-side state/<id>.meta never carries spawn_gen: bin/fm-spawn.sh's spawn_remote_secondmate() is its sole writer and never writes one, because that incarnation identity does not apply to a remote route. fm-secondmate-reconcile.sh's row filter required a non-empty spawn_gen matching an identifier regex, so every such row was silently dropped before the per-row loop ever saw it: no sent/stale/failed line, no cooldown record, nothing sent, and no trace of why. Give a legitimately markerless persistent remote secondmate a safe substitute identity - its recorded remote_host - instead of weakening the spawn_gen check for rows that do have a generation: - bin/fm-secondmate-reconcile.sh: carry host through the row projection for both fm-fleet-snapshot.v1 and fm-bearings.v1 documents, and admit an empty spawn_gen instead of filtering the row out. A new revalidate_identity() compares the sampled spawn_gen against current metadata when one was sampled (unchanged), or the sampled host against the metadata's remote_host when none was sampled and the metadata still carries no spawn_gen of its own. A row with neither a spawn_gen nor a host has no safe identity at all and fails loudly instead of vanishing, exactly the visibility the original bug lacked. - Rows now join on the ASCII unit separator rather than @TSV: bash's IFS-whitespace read collapses consecutive tabs, which would have silently dropped a legitimately empty field again. - bin/fm-bearings-snapshot.sh: thread host through the secondmate_reconcile projection so the fm-bearings.v1 path (the one bearings itself feeds to the reconcile hook) carries the same substitute identity. - tests/fm-secondmate-reconcile.test.sh: end-to-end coverage through the real remote transport (fm-on.sh + fm-remote-secondmate-control.sh against a genuinely seeded remote home) for a markerless mate nudged once per cooldown window, a stale/replaced remote route refused exactly like the existing local spawn_gen case, and a row with no identity at all failing loudly rather than being swallowed. * no-mistakes(review): Enforce markerless remote host identity during final delivery * no-mistakes(document): Document markerless remote reconciliation safety
…decorated (kunchenguid#3147) * fix(watch): hand a busy declared pause to the away-mode daemon undecorated While away mode is active the daemon owns triage and the watcher reverts to one-shot, handing over plain wake identities the daemon classifies itself. The busy-turn bound was the one stale path that did not: with afk active it ran the wedge timer, so the daemon received a wake already decorated as a possible wedge. That decoration outranks the daemon's own verdict. handle_wake escalates an enriched wedge reason before its pause classification can apply, so a crew that declared the wait itself - a `paused:` external wait or a verified captain-held transfer holding a live foreground call - was wedge-escalated once per FM_STALE_ESCALATE_SECS for as long as the wait lasted, the escalation count climbing into demand-deep-inspection on a pane nobody needed to inspect. Measured on the pre-fix tree, five consecutive re-arms produced five escalations. busy_turn_bound_check now reads the declaration before the afk branch: away mode hands off the plain window identity, one-shot per distinct stale hash, leaving normal-mode pause bookkeeping unwritten because the daemon owns it there. The daemon then classifies the wait itself and self-handles it on the long cadence. Normal-mode behavior is unchanged, and lifting the declaration still restores the busy-pane wedge escalation on the same pane. The regression covers all three: the undecorated handoff with no wedge timer or escalation counter, the one-shot on re-arm that the escalation ladder used to climb, and the restored wedge escalation once the declaration is lifted. * no-mistakes(review): key afk busy-pause handoff on declaration, clear wedge state * no-mistakes(document): docs: scope away-mode busy-bound handoff to declared waits * no-mistakes(document): docs: note afk busy-bound handoff in watcher header --------- Co-authored-by: Talon Stark <talonstark@gmail.com>
…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
A crew with a current `paused:` (or verified `captain-held`) last status line kept surfacing bare `stale:` wakes every few minutes instead of being absorbed on the long FM_PAUSE_RESURFACE_SECS cadence. pause_state_class read backend agent liveness on EVERY evaluation, and for an ordinary crew whose agent is still live - the normal state of a paused crew - it answered `none`. `none` is a caller's "no declared wait here" signal, so the changed-hash caller ran clear_pause_tracking and erased the cadence flag, its long-cadence throttle, and the per-hash stale suppressor. Any pane redraw (a ticking clock, a token counter, the crew's own status echo) therefore re-armed a first sighting, and the next stable hash surfaced through surface_nonterminal_stale all over again. The liveness gate was only ever meant to give a live agent ONE inspection per declared wait, in case it declared a wait while actually parked at a decision gate. `.paused-<key>` already records that the key is on the bounded cadence, so the gate now reads it and is spent once: a live agent surfaces once and then joins the cadence, and the declaration - not the pane hash - decides how long the cadence holds. It survives further declaring appends and watcher restarts, and is dropped only when the last status line stops declaring a wait or the crew is provably working again. A confidently dead agent still joins the cadence with no initial surface, and a secondmate's endpoint liveness is still never read. The safety boundary is unchanged and now pinned: needs-decision, blocked and done still wake immediately through a fully armed cadence, on the signal path when their append is unseen and on the stale path when it has been seen. Verified against a real fm-watch.sh over five restarts with a churning idle pane: five bare stale wakes before, one after.
* fix(remote): complete already-retired secondmate teardown * no-mistakes(review): accept inheritance lock and staging residue when retired * no-mistakes(review): accept unapplied inheritance generations and cover absent home
…replies (#69) * fix(teardown): preserve unrelated pending replies * no-mistakes(document): document remote retirement pending-reply isolation
* feat: add durable resource telemetry recorder * no-mistakes(review): fix(telemetry): real fsync, atomic lock, UTC dating, temp cleanup * no-mistakes(review): fix(telemetry): durable dir flush, argv tokens, guarded locks, arm probe * no-mistakes(review): fix(telemetry): bounded arm, owner-verified guard, timestamped diagnostics * no-mistakes(document): docs: list fm-telemetry.sh in bin toolbelt inventory * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes
…#71) * fix(lock): bound stale recovery mutex acquisition * test(tool-updates): compare line counts numerically * test(lock): tolerate loaded-host timing * no-mistakes(review): fix lock status typing, steal reclaim, and E2E cleanup * no-mistakes(review): serialize steal reclaim, retire nested-steal debris, fail closed * no-mistakes(review): bind reclaim takeover to inspected marker, reclaim legacy steal dirs * no-mistakes(review): fix coordinator argv ownership and marker takeover gap * no-mistakes(review): guard conditional-context lock waits, retire legacy steal debris * no-mistakes(review): harden legacy steal reclaim, marker self-recovery, owner residue * no-mistakes(review): preserve steal-dir owner record, prove descendant-free attempts * no-mistakes(review): restore self-abandoned steal-mutex reclaim in non-recursive helper * no-mistakes(document): document typed lock statuses and non-recursive steal reclaim * no-mistakes(lint): quote stale-beacon verdict literal to fix SC2100
…ed on missing backend adapters (#72) * fix: restore portable baseline paths * test(remote): compare symlink residue portably * no-mistakes(review): make tree_snapshot fail closed on missing directories * no-mistakes(document): document adapter precheck, portable seed loop, tree_snapshot
…#73) * feat: add fseventsd footprint early warning * no-mistakes(review): fix fseventsd pressure encoding, doubling floor, growth window * no-mistakes(review): retain fseventsd action evidence, drop dead pressure clause * no-mistakes(review): retain fseventsd emergency evidence, update scripts index * no-mistakes(review): label fseventsd pressure by level, guard reason separators * no-mistakes(review): report every fseventsd action reason that holds * no-mistakes(review): isolate watcher tests from live fseventsd, alert on write failure * no-mistakes(document): document fseventsd early-warning wake in architecture
…ns (#75) * fix(pi): clear regular TUI after transcript shrink * no-mistakes(review): settle Pi viewport and init awk blank-run counter * no-mistakes(review): gate live-e2e settle on Calm ship across ship tick * no-mistakes(review): re-anchor bare-executable guard after clear-on-shrink assignment * no-mistakes(review): drop inert hidden-thinking setting from live shrink test * no-mistakes(test): make Pi clear-on-shrink live e2e reproduce stale rows * no-mistakes(document): point README Pi launch at clear-on-shrink rationale
…tover (content unchanged, ours strategy)
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.
Upstream snapshot
Adopted base:
upstream/mainatd63b0e2.This cutover intentionally replaces the fork base with the upstream snapshot, then reapplies only the approved local keep set.
An
ours-strategy merge records currentorigin/mainas reconciled while leaving the cutover tree byte-identical to161a5ad.Keep-list verdicts
Validation
The full Herdr-isolated suite passed on exact head
161a5ad:FM_TEST_SUMMARY total=168 failed=0 skipped_gate=23 duration_ms=5274675fm-herdr-lab.shbin/fm-lint.shpassedTest-repair edits
BASHPIDfallback.Pipeline note
No-mistakes run
01M11E7KA730FQYFPMCZ5FY8NKis terminal after its rebase step incorrectly rebased this upstream cutover ontoorigin/main; the reviewer restored the requiredd63b0e2base and the custody guard then failed closed.Exact custody refusal:
The required no-mistakes check is therefore expected to remain red for this documented tool-defect path. Direct delivery was explicitly authorized after recovery, fresh-run, and terminal-abort paths all failed closed.
Do not merge automatically. Captain will squash-merge after review.