docs: add GROK_BOT.md Grok Bot system prompt - #2590
Merged
Merged
Conversation
This was referenced Aug 18, 2026
kunchenguid
added a commit
that referenced
this pull request
Aug 18, 2026
* fix(bin): guarantee inactive-reconcile scan progress under second quantization The inactive-outcome scan computed its aggregate deadline in whole seconds, so a 1-second budget's effective value lands anywhere in (0,1]; a scan starting just before a wall-clock second boundary rounded its whole budget away mid-scan and exited having visited no child, while the durable cursor had already advanced past the never-examined child. This is the CI flake behind tests/fm-inactive-reconcile.test.sh's 'next bounded scan did not resume with the following child' (watcher-wake-lock family, portable serial 2, seen on the PR #2590 run). Every scan now visits at least its first due child with the per-child state-read bound floored at one second, so no invocation can be a zero-work no-op. The outer process-group kill moves to budget+1s: the scan's own deadline enforces the budget, and the kill is a backstop for a scan wedged in an unbounded wait instead of a racer that routinely preempts the clean bounded exit. The wake-lock-wait test bound tracks the backstop (3s -> 4s); the previously flaky assertion is unchanged. * no-mistakes(document): Document inactive-reconcile deadline backstop
kaan-sirin
pushed a commit
to kaan-sirin/firstmate
that referenced
this pull request
Aug 18, 2026
* docs: add GROK_BOT.md Grok Bot system prompt * docs: amend GROK_BOT.md with charter report-back and delegation marker * docs: classify GROK_BOT.md as public-product * docs: make GROK_BOT.md the plain Grok Bot system prompt
kaan-sirin
pushed a commit
to kaan-sirin/firstmate
that referenced
this pull request
Aug 18, 2026
…#2595) * fix(bin): guarantee inactive-reconcile scan progress under second quantization The inactive-outcome scan computed its aggregate deadline in whole seconds, so a 1-second budget's effective value lands anywhere in (0,1]; a scan starting just before a wall-clock second boundary rounded its whole budget away mid-scan and exited having visited no child, while the durable cursor had already advanced past the never-examined child. This is the CI flake behind tests/fm-inactive-reconcile.test.sh's 'next bounded scan did not resume with the following child' (watcher-wake-lock family, portable serial 2, seen on the PR kunchenguid#2590 run). Every scan now visits at least its first due child with the per-child state-read bound floored at one second, so no invocation can be a zero-work no-op. The outer process-group kill moves to budget+1s: the scan's own deadline enforces the budget, and the kill is a backstop for a scan wedged in an unbounded wait instead of a racer that routinely preempts the clean bounded exit. The wake-lock-wait test bound tracks the backstop (3s -> 4s); the previously flaky assertion is unchanged. * no-mistakes(document): Document inactive-reconcile deadline backstop
joliverMI
pushed a commit
to joliverMI/firstmate
that referenced
this pull request
Aug 19, 2026
* docs: add GROK_BOT.md Grok Bot system prompt * docs: amend GROK_BOT.md with charter report-back and delegation marker * docs: classify GROK_BOT.md as public-product * docs: make GROK_BOT.md the plain Grok Bot system prompt
joliverMI
pushed a commit
to joliverMI/firstmate
that referenced
this pull request
Aug 19, 2026
…#2595) * fix(bin): guarantee inactive-reconcile scan progress under second quantization The inactive-outcome scan computed its aggregate deadline in whole seconds, so a 1-second budget's effective value lands anywhere in (0,1]; a scan starting just before a wall-clock second boundary rounded its whole budget away mid-scan and exited having visited no child, while the durable cursor had already advanced past the never-examined child. This is the CI flake behind tests/fm-inactive-reconcile.test.sh's 'next bounded scan did not resume with the following child' (watcher-wake-lock family, portable serial 2, seen on the PR kunchenguid#2590 run). Every scan now visits at least its first due child with the per-child state-read bound floored at one second, so no invocation can be a zero-work no-op. The outer process-group kill moves to budget+1s: the scan's own deadline enforces the budget, and the kill is a backstop for a scan wedged in an unbounded wait instead of a racer that routinely preempts the clean bounded exit. The wake-lock-wait test bound tracks the backstop (3s -> 4s); the previously flaky assertion is unchanged. * no-mistakes(document): Document inactive-reconcile deadline backstop
quinnbot-ai
added a commit
to quinnbot-ai/firstmate
that referenced
this pull request
Aug 19, 2026
…sidebar (#135) * fix(decisions): close decision holds at answer time via one general keyed-answer path (kunchenguid#2490) * fix(decisions): close captain holds at answer time Firstmate had two "a decision is open" ledgers with asymmetric closing mechanics. The live status-log ledger closes atomically at answer time, because bin/fm-send.sh --resolve-key makes answering a decision be the act that closes it. The durable backlog hold ledger had no such coupling: answering and recording were two separate acts, and only the first was forced by the workflow. That asymmetry lost four real captain decisions. Their answers were captured durably to disk, keyed character for character by the hold decision keys, acknowledged, and even implemented and shipped, yet the holds stayed open for two days and the captain was asked to re-answer decisions already on his own disk. Give the hold ledger the same answer-time-closure property: - bin/fm-decision-hold.sh gains an `answer` subcommand, the hold ledger's counterpart to --resolve-key. It shares one unrouted close implementation with `decline`, so it carries every existing guard - the captain decision file, the active-hold requirement, retry identity, and the refusal to release still-routed work - and differs only in the resolution mode it records. `decline` keeps its stronger meaning that the answer routes no follow-up work at all. - bin/fm-procevent-lavish.sh wires the channel that actually carried the lost answers. `arm --decisions-origin` binds a deck to the origin whose holds it carries, `answers` reads the structured choices out of a captured poll result, `close-decisions` maps each key to its hold and closes it through the command above, and `autohandle` lets the runner apply that at capture time. Safety is preserved rather than traded away. Only rows tagged `choice` are read, so freeform captain prose cannot forge a decision key. Closure is confined to the one bound origin. The decision text is a pure function of the captured result, so a replayed capture is idempotent. A hold that is absent, already closed, or still blocking routed work is skipped and left for `resolve`, never forced. A deck armed without the binding touches no hold at all. And autohandle deliberately never reports full handling, because recording an answer is transcription while acting on it is firstmate's judgement - so the check wake still reaches the handler. fm-send --resolve-key is untouched. * no-mistakes(document): document state/lavish-decisions binding dir in AGENTS.md state inventory * refactor(decisions): make keyed-answer closure one general capability The previous pass gave holds answer-time closure but built it as bespoke Lavish wiring: the review adapter carried the source-to-origin binding, mapped keys to hold identities, wrote decision records, decided what to skip, and closed holds itself. That treated a review deck as a special decision source. It is not - it is an ephemeral discussion format that happens to carry answers. Collapse it into ONE general capability with one owner. bin/fm-decision-hold.sh now owns the whole of "a keyed answer closes its matching hold": - `answers <origin> --source <provenance>` is the channel-agnostic intake. It reads key/answer/label lines on stdin, maps each key to its hold, and closes it through the same `answer` path, so every guard applies identically whatever channel the answer came from. --source is provenance recorded in the decision, never a behavior switch; there is no per-channel branch and no knowledge of chat, decks, or transports. - `bind`/`unbind`/`binding` own the source-to-origin binding for any channel whose answers arrive detached from their origin. Every channel is now an ordinary caller that only turns what it received into keyed lines: - bin/fm-send.sh (chat) feeds the intake for a key that names an active hold. This also fixes a real gap: once `complete` transfers a decision to its hold it closes the live status copy, so --resolve-key alone could never answer a transferred decision. - bin/fm-procevent.sh feeds it generically. A bound source's captured result goes to `<adapter> answers <result-file>` and whatever that prints is piped into the intake. The runner names no adapter, parses no result, and carries no decision rule, so any future adapter with an `answers` command works with no change here. - bin/fm-procevent-lavish.sh keeps only `answers`, which reports the structured choices a review captured and stops. It maps nothing to a hold and closes nothing; it lost ~160 lines of decision logic. Feeding is independent of handling, so it never acknowledges a result and never suppresses a wake - recording an answer is transcription, acting on it stays firstmate's judgement. The regression that proves closure now drives a FIXTURE adapter that is not the review adapter, so what is proven is that any bound channel reaches the intake rather than that one channel is wired specially. A new regression drives the real fm-send over a stubbed transport for the chat side. Every prior guarantee still holds, and fm-send's status-log behavior is unchanged. * no-mistakes(review): test(decisions): drop source-content grep from hold-closure regression * fix(memory): emit a real @AGENTS.md pointer instead of a CLAUDE.md symlink (kunchenguid#2512) A Write aimed at CLAUDE.md followed the symlink and destroyed AGENTS.md. The installer now creates and migrates to a recoverable two-line pointer file. * fix(ci): keep CLAUDE.md pointer check valid (kunchenguid#2515) * ci: gate GitHub workflows with pinned actionlint (kunchenguid#2517) * fix(lint): catch malformed GitHub workflows before merge A self-broken ci.yml cannot report its own breakage, so parse every workflow in the local lint path that no-mistakes already runs. * fix(lint): pin actionlint instead of Ruby for workflow lint A self-broken ci.yml still has to fail in the local lint path, and the named tool for that gate is actionlint, not a new Ruby runtime. * no-mistakes(document): Clarify pinned workflow lint documentation * fix: install pinned lint tools across supported platforms (kunchenguid#2546) * fix: install pinned shellcheck and actionlint on macOS and linux arm64 The installers were hardcoded to linux amd64 and sha256sum, so a Mac dev could not satisfy the refuse-on-mismatch lint gate. Select the official per-platform archive and checksum, and fall back to shasum -a 256. * no-mistakes(document): Document cross-platform pinned lint installers * docs: reconcile test-evidence docs with store_in_repo: true (kunchenguid#2548) .no-mistakes.yaml has set test.evidence.store_in_repo: true since kunchenguid#2355, but CONTRIBUTING.md, docs/configuration.md, and docs/architecture.md still described the old policy of keeping evidence out of the repo in a temp directory. The current no-mistakes behavior for store_in_repo: true is to publish each run's test evidence to the orphan no-mistakes/evidence branch and link it from the PR body. That branch shares no history with code branches, so evidence never enters a pushed feature branch or the default branch, and CI's tracked personal fleet paths rule stays accurate. Docs only. No change to .no-mistakes.yaml or any workflow. * docs: clarify test evidence branch storage (kunchenguid#2549) * docs: correct test evidence storage comment in .no-mistakes.yaml * no-mistakes: apply CI fixes * docs: hint that live scouts may host their own Lavish review loop (kunchenguid#2563) Make that a first-class option in always-loaded instructions so firstmate does not default to mediating and tearing the scout down between iteration rounds. * fix(bin): report remote secondmate delivery and state truthfully (kunchenguid#2570) * fix(bin): report remote secondmate delivery and state truthfully A steer to a remote secondmate crosses fm-on.sh to a host-local fm-send leg whose unconfirmed submit read-back (verdict=pending, typically a busy mate whose harness queues the steer) was flattened into exit 1, so the parent printed "error: text not submitted" / "error: text not sent" and discarded the pending-reply expectation for a steer that had actually landed. fm-send now carries the verdict across the ssh boundary as a documented delivered-unconfirmed exit 3: the parent reports the steer as delivered with confirmation pending, exits 0, keeps the expectation armed (awaiting_report), and closes --resolve-key decisions, while transport loss (ssh 255) and real remote failures keep failing loudly with the remote leg's stderr attached. A local unconfirmed submit now also exits 3 with an honest non-error message and still never closes a decision key. fm-crew-state.sh and fm-peek.sh no longer read a remote mate's endpoint through local probes (which misreported a healthy mate as "worktree gone" / "can't find session: remote"): both now use the true remote source over fm-on.sh, and an unreachable or unreadable remote reads as unknown-remote, never as gone or dead. * no-mistakes(document): Document remote delivery and state truth * no-mistakes: apply CI fixes * feat: adopt spendPriority for quota dispatch (kunchenguid#2574) * Adopt quota-axi 0.1.29 spendPriority-primary array dispatch. quota-axi 0.1.29 publishes schema 5 with selection.spendPriority as the primary comparative signal and demotes derivation fields out of default --json. Rank comparable-fit candidates on that scalar, keep runway versus the completion horizon as a hard gate, and raise the compatibility floor so a pre-consolidation build cannot reach dispatch intake. * no-mistakes(review): Correct schema fixtures and remove prescriptive selection prompts * no-mistakes(document): Correct quota verification evidence chronology * Collapse quota-array-dispatch onto TOON-first spendPriority ranking. Decide from quota-axi's default TOON; keep --json as a rare defensive fallback. Rank by spendPriority after eligibility, reasoning-class, and runway-feasibility gates, and drop the hand-computed Pareto, pace, reserve, and window-id layers. * no-mistakes(review): Permit ambiguous JSON fallback and correct reset fixtures * no-mistakes(review): Correct runway semantics and escalate unresolved uncertainty * no-mistakes(document): Document TOON-first quota dispatch evidence * docs: add GROK_BOT.md Grok Bot system prompt (kunchenguid#2590) * docs: add GROK_BOT.md Grok Bot system prompt * docs: amend GROK_BOT.md with charter report-back and delegation marker * docs: classify GROK_BOT.md as public-product * docs: make GROK_BOT.md the plain Grok Bot system prompt * docs: update GROK_BOT.md nautical terms and self-improvement (kunchenguid#2592) * doc: Update language in GROK_BOT.md for clarity Refine language for clarity and consistency in instructions. * fix(bin): preserve inactive reconciliation scan progress (kunchenguid#2595) * fix(bin): guarantee inactive-reconcile scan progress under second quantization The inactive-outcome scan computed its aggregate deadline in whole seconds, so a 1-second budget's effective value lands anywhere in (0,1]; a scan starting just before a wall-clock second boundary rounded its whole budget away mid-scan and exited having visited no child, while the durable cursor had already advanced past the never-examined child. This is the CI flake behind tests/fm-inactive-reconcile.test.sh's 'next bounded scan did not resume with the following child' (watcher-wake-lock family, portable serial 2, seen on the PR kunchenguid#2590 run). Every scan now visits at least its first due child with the per-child state-read bound floored at one second, so no invocation can be a zero-work no-op. The outer process-group kill moves to budget+1s: the scan's own deadline enforces the budget, and the kill is a backstop for a scan wedged in an unbounded wait instead of a racer that routinely preempts the clean bounded exit. The wake-lock-wait test bound tracks the backstop (3s -> 4s); the previously flaky assertion is unchanged. * no-mistakes(document): Document inactive-reconcile deadline backstop * doc: Revise Firstmate delegation and communication guidelines Refactor the guidelines for Firstmate's role and delegation process, emphasizing the importance of crewmates and asynchronous work. * doc: Update work delegation and secret management instructions Clarified guidelines for handing off work to crewmates and managing secrets. * feat(herdr): make crewmates, scouts, and second mates legible in the sidebar Herdr's Spaces and Agents sidebar lists are drawn from configurable row templates fed by display-only pane and workspace metadata. Firstmate never reported any, so every spawned entry rendered identically and only the fm-<id> tab label distinguished one worker from another. Report, once per spawn, the facts only Firstmate knows: whether an entry is a persistent second mate, an ordinary crewmate, or a scout; the domain or project it belongs to; its task; and its owning home. The reported display name improves the sidebar under a completely unconfigured Herdr, while the fm_role, fm_glyph, fm_scope, fm_task, and fm_home tokens let a captain compose their own rows and colors in their own Herdr config, which is where presentation taste belongs. The surface is display-only and carries no authority. Reporting never renames a workspace or tab and never touches a value that placement, endpoint metadata, the presentation journal, husk replacement, list-live discovery, bare-selector resolution, or cleanup resolves a worker by; the task tab stays exactly fm-<id>. Every report is best-effort, and the display name and the tokens are reported separately so an older release that cannot store tokens still shows the readable name. Reporting happens at spawn time only, so no running worker is retitled and an existing fleet converges as its tasks turn over. * doc: point architecture at the Herdr sidebar legibility boundary * no-mistakes(review): derive herdr Space role from journal, stabilize home scope * no-mistakes(review): report Space role from spawn state, skip on relaunch * doc(herdr): state the true reason a relaunch re-badges its pane The relaunch arm adopts the recorded endpoint and creates no pane, so the previous rationale - that the replacement pane carries no metadata yet - was wrong. The adopted pane keeps its reported tokens; the re-report is a refresh for facts that can change while the endpoint stays the same. The concrete case is promotion: fm-promote.sh flips a task from scout to ship in place on the same endpoint without going through spawn, so the badge reads scout until a later relaunch re-reads that kind and re-badges it as crew. Promotion alone does not update the sidebar, and the refresh is not a promise that a badge is continuously current. Wording only; no behavior change. * no-mistakes(document): doc(herdr): fm_role covers projected one-task Spaces --------- Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com> Co-authored-by: QuinnBot <quinnbot@proton.me>
prajwal-395
added a commit
to prajwal-395/firstmate
that referenced
this pull request
Aug 19, 2026
…-ahead behaviour (#21) * fix(ci): fail hung Herdr behavior runs in 20 minutes (kunchenguid#2413) A wedged family-run step was occupying the runner until the 75-minute job cap; bound that step so cleanup and timing artifacts still upload. * fix: keep the public promise reachable when work is routed to a second mate (kunchenguid#2457) The lightweight Relay follow-up link lives in the answering home's own state/<task-id>.meta, so it can only bind work that home owns. When a Relay-linked request is routed to a second mate, the task record lives in the second mate's home, fm-x-link.sh failed with a bare "no such task ...meta", and nothing else picked the promise up: only the soft acknowledgement was ever posted. The typed promised-final path already supports --work-home secondmate:<id>; the playbook simply never chose it. - fmx-respond now states the routing rule crisply: a task in this home takes the lightweight link, and second-mate-routed work takes a promised-final commitment bound to that home, registered up front with the brief command carried into the routed worker's instructions. - fm-x-link.sh refuses a task with no local record by naming the registered second mate whose home actually holds it and printing the promised-final registration command, with the exact --work-home when the match is unambiguous. A home with no registered second mates keeps the plain error. - fm-backlog-handoff.sh reports, after a successful move, any moved key that still owes a public reply bound to main/<key>, since that binding no longer names the home owning the work. The move itself is never blocked. Docs and the secondmate handoff prose follow the same rule. Tests cover the refusal, its scoping, the unchanged local-link path, and both handoff outcomes at the script boundary. * docs(skills): add remote-secondmate recovery hint for false-negative verdicts (kunchenguid#2456) * fix(skills): hint that remote secondmate liveness verdicts false-negative fm-crew-state and fm-send routinely misreport a live remote secondmate as dead; confirm against the pane before relaunching, and relaunch only through fm-spawn.sh, never raw herdr pane surgery. * no-mistakes: apply CI fixes * fix(calm): keep Pi's export confirmation visible (kunchenguid#2461) Pi 0.83.0 added a status line to every tool-expansion change, and Pi updates the previous status line in place when two status messages arrive back to back. Calm's post-export redraw cycled tool expansion on the macrotask right after Pi printed "Session exported to: <path>", so both expansion status lines coalesced over that confirmation and the captain was left with no record of where their export landed. Calm now repaints only the tool rows it presents, by invalidating each row through the render context Pi hands its render slots, and requests the surrounding redraw through setStatus. Neither appends to the transcript. The repaint is still needed because Pi can re-render a row asynchronously - the built-in edit row invalidates itself once its diff is ready - and that re-render can land inside the window where /export forces stock rendering. The real-terminal /export case now asserts the confirmation is still on screen after the redraw has settled, and that the redraw restored every Calm-hidden row, instead of only racing the moment the confirmation first appeared. * feat(stow): add open-record persistence to /stow before reset (kunchenguid#2488) * feat(stow): persist the open records a session is holding /stow curated memory and captured session knowledge, but never touched record state, while AGENTS.md called it an "unfinished-work sweep" and the receipt declared the session "safe to reset" - wording that implied a record-correctness guarantee stow does not make. A shipped PR with no backlog item, a queued umbrella whose phases had merged, and four decision holds left open after their answers shipped all survived repeated stows. Add a bounded pass that files record state from the same volatile input the rest of stow already uses: the open threads in context, minutes before the reset destroys them. It creates a record for an unfiled thread and corrects one the session knows is wrong, through the owning path, and states its boundary as part of the contract - it never enumerates the backlog, lists holds, or queries a forge, because it cannot be a reconciliation and must not be read as one. Correct the wording in AGENTS.md and the completion receipt so reset-safe means what it actually guarantees: nothing this session knew was lost. * no-mistakes(review): correct stow decision-hold inspection to read hold via tasks-axi * no-mistakes(document): note /stow open-record persistence in README command catalog * refactor(stow): state open-record persistence as principle, not procedure The first version enumerated triggers, named commands, and prescribed an ordered procedure. That is too rigid for an agent skill: it invites literal execution of a checklist instead of judgment, and every enumerated example is a way for the guidance to go stale. Reduce it to the intent - before a reset, the important open work you are holding in context must end up durably recorded rather than dying with the session, filing what is unfiled and correcting what is stale - and let the agent judge importance, the record, and the owning write path. Keep the scope bound, since it is a decided contract and not a mechanic: this covers the open work the session is holding, never a reconciliation of durable records against repository or forge reality. The wording corrections in AGENTS.md and the completion receipt are unchanged. * fix(decisions): close decision holds at answer time via one general keyed-answer path (kunchenguid#2490) * fix(decisions): close captain holds at answer time Firstmate had two "a decision is open" ledgers with asymmetric closing mechanics. The live status-log ledger closes atomically at answer time, because bin/fm-send.sh --resolve-key makes answering a decision be the act that closes it. The durable backlog hold ledger had no such coupling: answering and recording were two separate acts, and only the first was forced by the workflow. That asymmetry lost four real captain decisions. Their answers were captured durably to disk, keyed character for character by the hold decision keys, acknowledged, and even implemented and shipped, yet the holds stayed open for two days and the captain was asked to re-answer decisions already on his own disk. Give the hold ledger the same answer-time-closure property: - bin/fm-decision-hold.sh gains an `answer` subcommand, the hold ledger's counterpart to --resolve-key. It shares one unrouted close implementation with `decline`, so it carries every existing guard - the captain decision file, the active-hold requirement, retry identity, and the refusal to release still-routed work - and differs only in the resolution mode it records. `decline` keeps its stronger meaning that the answer routes no follow-up work at all. - bin/fm-procevent-lavish.sh wires the channel that actually carried the lost answers. `arm --decisions-origin` binds a deck to the origin whose holds it carries, `answers` reads the structured choices out of a captured poll result, `close-decisions` maps each key to its hold and closes it through the command above, and `autohandle` lets the runner apply that at capture time. Safety is preserved rather than traded away. Only rows tagged `choice` are read, so freeform captain prose cannot forge a decision key. Closure is confined to the one bound origin. The decision text is a pure function of the captured result, so a replayed capture is idempotent. A hold that is absent, already closed, or still blocking routed work is skipped and left for `resolve`, never forced. A deck armed without the binding touches no hold at all. And autohandle deliberately never reports full handling, because recording an answer is transcription while acting on it is firstmate's judgement - so the check wake still reaches the handler. fm-send --resolve-key is untouched. * no-mistakes(document): document state/lavish-decisions binding dir in AGENTS.md state inventory * refactor(decisions): make keyed-answer closure one general capability The previous pass gave holds answer-time closure but built it as bespoke Lavish wiring: the review adapter carried the source-to-origin binding, mapped keys to hold identities, wrote decision records, decided what to skip, and closed holds itself. That treated a review deck as a special decision source. It is not - it is an ephemeral discussion format that happens to carry answers. Collapse it into ONE general capability with one owner. bin/fm-decision-hold.sh now owns the whole of "a keyed answer closes its matching hold": - `answers <origin> --source <provenance>` is the channel-agnostic intake. It reads key/answer/label lines on stdin, maps each key to its hold, and closes it through the same `answer` path, so every guard applies identically whatever channel the answer came from. --source is provenance recorded in the decision, never a behavior switch; there is no per-channel branch and no knowledge of chat, decks, or transports. - `bind`/`unbind`/`binding` own the source-to-origin binding for any channel whose answers arrive detached from their origin. Every channel is now an ordinary caller that only turns what it received into keyed lines: - bin/fm-send.sh (chat) feeds the intake for a key that names an active hold. This also fixes a real gap: once `complete` transfers a decision to its hold it closes the live status copy, so --resolve-key alone could never answer a transferred decision. - bin/fm-procevent.sh feeds it generically. A bound source's captured result goes to `<adapter> answers <result-file>` and whatever that prints is piped into the intake. The runner names no adapter, parses no result, and carries no decision rule, so any future adapter with an `answers` command works with no change here. - bin/fm-procevent-lavish.sh keeps only `answers`, which reports the structured choices a review captured and stops. It maps nothing to a hold and closes nothing; it lost ~160 lines of decision logic. Feeding is independent of handling, so it never acknowledges a result and never suppresses a wake - recording an answer is transcription, acting on it stays firstmate's judgement. The regression that proves closure now drives a FIXTURE adapter that is not the review adapter, so what is proven is that any bound channel reaches the intake rather than that one channel is wired specially. A new regression drives the real fm-send over a stubbed transport for the chat side. Every prior guarantee still holds, and fm-send's status-log behavior is unchanged. * no-mistakes(review): test(decisions): drop source-content grep from hold-closure regression * fix(memory): emit a real @AGENTS.md pointer instead of a CLAUDE.md symlink (kunchenguid#2512) A Write aimed at CLAUDE.md followed the symlink and destroyed AGENTS.md. The installer now creates and migrates to a recoverable two-line pointer file. * fix(ci): keep CLAUDE.md pointer check valid (kunchenguid#2515) * ci: gate GitHub workflows with pinned actionlint (kunchenguid#2517) * fix(lint): catch malformed GitHub workflows before merge A self-broken ci.yml cannot report its own breakage, so parse every workflow in the local lint path that no-mistakes already runs. * fix(lint): pin actionlint instead of Ruby for workflow lint A self-broken ci.yml still has to fail in the local lint path, and the named tool for that gate is actionlint, not a new Ruby runtime. * no-mistakes(document): Clarify pinned workflow lint documentation * fix: install pinned lint tools across supported platforms (kunchenguid#2546) * fix: install pinned shellcheck and actionlint on macOS and linux arm64 The installers were hardcoded to linux amd64 and sha256sum, so a Mac dev could not satisfy the refuse-on-mismatch lint gate. Select the official per-platform archive and checksum, and fall back to shasum -a 256. * no-mistakes(document): Document cross-platform pinned lint installers * docs: reconcile test-evidence docs with store_in_repo: true (kunchenguid#2548) .no-mistakes.yaml has set test.evidence.store_in_repo: true since kunchenguid#2355, but CONTRIBUTING.md, docs/configuration.md, and docs/architecture.md still described the old policy of keeping evidence out of the repo in a temp directory. The current no-mistakes behavior for store_in_repo: true is to publish each run's test evidence to the orphan no-mistakes/evidence branch and link it from the PR body. That branch shares no history with code branches, so evidence never enters a pushed feature branch or the default branch, and CI's tracked personal fleet paths rule stays accurate. Docs only. No change to .no-mistakes.yaml or any workflow. * docs: clarify test evidence branch storage (kunchenguid#2549) * docs: correct test evidence storage comment in .no-mistakes.yaml * no-mistakes: apply CI fixes * docs: hint that live scouts may host their own Lavish review loop (kunchenguid#2563) Make that a first-class option in always-loaded instructions so firstmate does not default to mediating and tearing the scout down between iteration rounds. * fix(bin): report remote secondmate delivery and state truthfully (kunchenguid#2570) * fix(bin): report remote secondmate delivery and state truthfully A steer to a remote secondmate crosses fm-on.sh to a host-local fm-send leg whose unconfirmed submit read-back (verdict=pending, typically a busy mate whose harness queues the steer) was flattened into exit 1, so the parent printed "error: text not submitted" / "error: text not sent" and discarded the pending-reply expectation for a steer that had actually landed. fm-send now carries the verdict across the ssh boundary as a documented delivered-unconfirmed exit 3: the parent reports the steer as delivered with confirmation pending, exits 0, keeps the expectation armed (awaiting_report), and closes --resolve-key decisions, while transport loss (ssh 255) and real remote failures keep failing loudly with the remote leg's stderr attached. A local unconfirmed submit now also exits 3 with an honest non-error message and still never closes a decision key. fm-crew-state.sh and fm-peek.sh no longer read a remote mate's endpoint through local probes (which misreported a healthy mate as "worktree gone" / "can't find session: remote"): both now use the true remote source over fm-on.sh, and an unreachable or unreadable remote reads as unknown-remote, never as gone or dead. * no-mistakes(document): Document remote delivery and state truth * no-mistakes: apply CI fixes * feat: adopt spendPriority for quota dispatch (kunchenguid#2574) * Adopt quota-axi 0.1.29 spendPriority-primary array dispatch. quota-axi 0.1.29 publishes schema 5 with selection.spendPriority as the primary comparative signal and demotes derivation fields out of default --json. Rank comparable-fit candidates on that scalar, keep runway versus the completion horizon as a hard gate, and raise the compatibility floor so a pre-consolidation build cannot reach dispatch intake. * no-mistakes(review): Correct schema fixtures and remove prescriptive selection prompts * no-mistakes(document): Correct quota verification evidence chronology * Collapse quota-array-dispatch onto TOON-first spendPriority ranking. Decide from quota-axi's default TOON; keep --json as a rare defensive fallback. Rank by spendPriority after eligibility, reasoning-class, and runway-feasibility gates, and drop the hand-computed Pareto, pace, reserve, and window-id layers. * no-mistakes(review): Permit ambiguous JSON fallback and correct reset fixtures * no-mistakes(review): Correct runway semantics and escalate unresolved uncertainty * no-mistakes(document): Document TOON-first quota dispatch evidence * docs: add GROK_BOT.md Grok Bot system prompt (kunchenguid#2590) * docs: add GROK_BOT.md Grok Bot system prompt * docs: amend GROK_BOT.md with charter report-back and delegation marker * docs: classify GROK_BOT.md as public-product * docs: make GROK_BOT.md the plain Grok Bot system prompt * docs: update GROK_BOT.md nautical terms and self-improvement (kunchenguid#2592) * doc: Update language in GROK_BOT.md for clarity Refine language for clarity and consistency in instructions. * fix(bin): preserve inactive reconciliation scan progress (kunchenguid#2595) * fix(bin): guarantee inactive-reconcile scan progress under second quantization The inactive-outcome scan computed its aggregate deadline in whole seconds, so a 1-second budget's effective value lands anywhere in (0,1]; a scan starting just before a wall-clock second boundary rounded its whole budget away mid-scan and exited having visited no child, while the durable cursor had already advanced past the never-examined child. This is the CI flake behind tests/fm-inactive-reconcile.test.sh's 'next bounded scan did not resume with the following child' (watcher-wake-lock family, portable serial 2, seen on the PR kunchenguid#2590 run). Every scan now visits at least its first due child with the per-child state-read bound floored at one second, so no invocation can be a zero-work no-op. The outer process-group kill moves to budget+1s: the scan's own deadline enforces the budget, and the kill is a backstop for a scan wedged in an unbounded wait instead of a racer that routinely preempts the clean bounded exit. The wake-lock-wait test bound tracks the backstop (3s -> 4s); the previously flaky assertion is unchanged. * no-mistakes(document): Document inactive-reconcile deadline backstop * doc: Revise Firstmate delegation and communication guidelines Refactor the guidelines for Firstmate's role and delegation process, emphasizing the importance of crewmates and asynchronous work. * doc: Update work delegation and secret management instructions Clarified guidelines for handing off work to crewmates and managing secrets. * test(procevent): make the process-event suite's detached-runner assertions deterministic (kunchenguid#2617) Three assertions in tests/fm-procevent.test.sh depended on a detached runner having finished work that the command starting it does not wait for. reconcile's replacement runner is started through detach_runner, which only forks: reconcile returns and counts the start before that runner has claimed its source or exec'd its child. Any assertion taken straight after reconcile therefore samples a race. - The publish-before-apply recovery section left its always-ready /bin/echo source registered across the recovery reconcile, so that reconcile launched a competing detached poll (observed: started=1) that then raced every later assertion for the source claim, the next capture sequence, and this home's applied record, and outlived the section holding a live claim. It is now retired before that reconcile - re-announcement is proven from the durable inbox alone and needs no registration - and started=0 is asserted so a competing poll cannot be reintroduced unnoticed. This is the same retire-before-reconcile discipline the self-announcing section already carries; that section acquired it after the identical race made its "not-autohandled: self-src" assertion read "already owned: self-src". - The crashed-leader replacement section snapshotted the replacement's claim file and execution log behind a fixed 0.5s settle window. On a loaded machine that window expires first, which is the CI flake behind "a replacement runner started without recording its own claim" and "reconcile did not start exactly one replacement source". Both effects are now waited for with the suite's bounded wait helpers; the exact one-replacement count is still asserted afterwards, unchanged. - The duplicate-start section slept 0.5s for reconcile's runner to record ownership before asserting that a second start loses to it. It now waits for that claim. Also tighten one assertion that could not fail as written: "autohandled: self-src" is a substring of "not-autohandled: self-src", so the applied path was accepted even when the runner reported the capture left for the handler. Evidence: on the unmodified suite, 128 full runs at 6-8x concurrency produced 6 failing runs, all in the crashed-leader section. On the fixed suite, 216 full runs under the same load produced none. Reverting the self-announcing section's retire-before-reconcile line reproduces "already owned: self-src" on the first iteration, confirming the shared mechanism. * fix: preserve pending replies and defer remote reposts (kunchenguid#2618) * fix(bin): keep pending-reply expectations honest on both send legs Two related asymmetries let the parent-owned secondmate reply guard drop or nag requests it should not have. Local delivered-unconfirmed dropped the expectation. A marked request whose submit read-back stayed unconfirmed (verdict=pending) is the same not-a-failure outcome the remote leg reports as delivered, but fm-send discarded the parent's pending-reply record for it, so a request that very likely landed stopped being tracked entirely. The record now stays armed on its unconfirmed-delivery marker: a correlated report still resolves it, and an unanswered one still surfaces through the library's own reconciliation. Exit 3 and the local rule that an unconfirmed answer never closes a decision key are unchanged. Remote replies were nagged for a repost they did not need. A remote mate's report reaches the parent's status log only through the asynchronous mirror in fm-procevent-remote-reply.sh, yet the guard read an absent correlated line as proof the mate never reported - even while the answer was still in flight, which is the common case because the mirror's poll window is comparable to the recovery grace. The mirror now publishes one caught-up watermark from a quiet window, and the guard admits a missing report as evidence only once that watermark passes the turn that should have produced it. A genuinely missed report still gets exactly one repost, and a channel that is behind, unarmed, or broken leaves the request durably open and un-nagged rather than nagging blind; the mirror escalates its own continuity failures as before. Tests: a local unconfirmed secondmate send keeps its expectation armed and resolvable; a mirrored correlated remote reply resolves with no repost; a stale or absent watermark withholds the repost while a fresh one still releases it; a quiet remote window publishes the watermark and retirement clears it. * no-mistakes(review): Distinguish preempted polls from quiet windows * no-mistakes(document): Clarify remote reply channel freshness * no-mistakes(lint): Annotate shared remote preemption exit constant * fix(bin): honor declared pauses in busy-pane wedge checks (kunchenguid#2619) * fix(watch): honor a declared pause on a busy pane's completed-turn bound A worker that declares an external wait (`paused:`) and then blocks in one long foreground call - a review-hosting scout parked in a single blocking `lavish-axi poll`, a bounded watch loop, a rate-limit sleep - keeps its pane BUSY, so the stale path that already honors declared pauses never ran for it. The busy-pane completed-turn bound instead routed it straight into wedge_timer_check, which re-escalated "possible wedge, escalation N" (and, past the threshold, demand-deep-inspection) every FM_STALE_ESCALATE_SECS for as long as the review stayed open. busy_turn_bound_check now owns which absorber takes a crossed bound: a crew whose own last status line declares an external wait or a verified captain-held transfer takes the bounded FM_PAUSE_RESURFACE_SECS recheck, and everything else keeps the unchanged wedge timer. The discriminator is the declaration together with liveness (the caller has already confirmed the pane is busy), never a blanket silencing - a crew that declared nothing, or whose pane is not live, escalates exactly as before, and a declared pause still re-surfaces once per long cadence so a forgotten wait cannot rot invisibly. Away mode is untouched: the daemon owns pause triage there and already reads the same vocabulary. The two call sites also no longer clear pause bookkeeping in the same poll the pause cadence recorded it, which would have erased the re-surface throttle and turned the long cadence back into a per-poll re-surface. Tests: a three-phase regression fixture pins the absorbed pause, its long-cadence recheck, and the restored wedge escalation once the declaration is lifted on the same busy over-age pane. Also de-flakes tests/fm-watch-triage.test.sh, which failed spuriously on a loaded machine: fixed liveness budgets were reaping watchers mid-startup, so assertions on post-poll state passed vacuously or failed spuriously. Waits that describe a poll's outcome now wait for a completed poll cycle via the liveness beacon, the heartbeat test waits for the heartbeat it asserts on, and every wait_for_exit budget is the uniform 10s already used elsewhere in the file. * no-mistakes(review): Fail poll-cycle waits on timeout * no-mistakes(review): Prevent poll timeout test hangs * no-mistakes(document): Clarify paused busy-pane supervision * fix(ci): rebalance portable serial shard weight hints from measured CI timings The old weight hints (from 2026-08-02, 69 scripts) left 52 tests at the 20000ms default. Many live-e2e tests actually run in <100ms while several new tests run over 100s, so the LPT algorithm's apparently balanced assignment was wildly skewed in practice: shard 1 took ~14.1 min vs 7.9-9.8 for the others, hitting the 15-min job timeout. Refresh all hints from CI run 32302063973 (2026-08-19, 119 scripts, ~39 min total). Three unmeasured new tests use 5000ms conservative estimates. The rebalanced shards each estimate ~589s (~9.8 min) with a 2ms imbalance. Also bump the CI timeout from 15 to 20 minutes (~2x margin on the ~10 min balanced target), and update docs/fm-test-portable-shards.md to match. --------- Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
prandelicious
added a commit
to prandelicious/firstmate
that referenced
this pull request
Aug 20, 2026
* ci: gate GitHub workflows with pinned actionlint (kunchenguid#2517) * fix(lint): catch malformed GitHub workflows before merge A self-broken ci.yml cannot report its own breakage, so parse every workflow in the local lint path that no-mistakes already runs. * fix(lint): pin actionlint instead of Ruby for workflow lint A self-broken ci.yml still has to fail in the local lint path, and the named tool for that gate is actionlint, not a new Ruby runtime. * no-mistakes(document): Clarify pinned workflow lint documentation * fix: install pinned lint tools across supported platforms (kunchenguid#2546) * fix: install pinned shellcheck and actionlint on macOS and linux arm64 The installers were hardcoded to linux amd64 and sha256sum, so a Mac dev could not satisfy the refuse-on-mismatch lint gate. Select the official per-platform archive and checksum, and fall back to shasum -a 256. * no-mistakes(document): Document cross-platform pinned lint installers * docs: reconcile test-evidence docs with store_in_repo: true (kunchenguid#2548) .no-mistakes.yaml has set test.evidence.store_in_repo: true since kunchenguid#2355, but CONTRIBUTING.md, docs/configuration.md, and docs/architecture.md still described the old policy of keeping evidence out of the repo in a temp directory. The current no-mistakes behavior for store_in_repo: true is to publish each run's test evidence to the orphan no-mistakes/evidence branch and link it from the PR body. That branch shares no history with code branches, so evidence never enters a pushed feature branch or the default branch, and CI's tracked personal fleet paths rule stays accurate. Docs only. No change to .no-mistakes.yaml or any workflow. * docs: clarify test evidence branch storage (kunchenguid#2549) * docs: correct test evidence storage comment in .no-mistakes.yaml * no-mistakes: apply CI fixes * docs: hint that live scouts may host their own Lavish review loop (kunchenguid#2563) Make that a first-class option in always-loaded instructions so firstmate does not default to mediating and tearing the scout down between iteration rounds. * fix(bin): report remote secondmate delivery and state truthfully (kunchenguid#2570) * fix(bin): report remote secondmate delivery and state truthfully A steer to a remote secondmate crosses fm-on.sh to a host-local fm-send leg whose unconfirmed submit read-back (verdict=pending, typically a busy mate whose harness queues the steer) was flattened into exit 1, so the parent printed "error: text not submitted" / "error: text not sent" and discarded the pending-reply expectation for a steer that had actually landed. fm-send now carries the verdict across the ssh boundary as a documented delivered-unconfirmed exit 3: the parent reports the steer as delivered with confirmation pending, exits 0, keeps the expectation armed (awaiting_report), and closes --resolve-key decisions, while transport loss (ssh 255) and real remote failures keep failing loudly with the remote leg's stderr attached. A local unconfirmed submit now also exits 3 with an honest non-error message and still never closes a decision key. fm-crew-state.sh and fm-peek.sh no longer read a remote mate's endpoint through local probes (which misreported a healthy mate as "worktree gone" / "can't find session: remote"): both now use the true remote source over fm-on.sh, and an unreachable or unreadable remote reads as unknown-remote, never as gone or dead. * no-mistakes(document): Document remote delivery and state truth * no-mistakes: apply CI fixes * feat: adopt spendPriority for quota dispatch (kunchenguid#2574) * Adopt quota-axi 0.1.29 spendPriority-primary array dispatch. quota-axi 0.1.29 publishes schema 5 with selection.spendPriority as the primary comparative signal and demotes derivation fields out of default --json. Rank comparable-fit candidates on that scalar, keep runway versus the completion horizon as a hard gate, and raise the compatibility floor so a pre-consolidation build cannot reach dispatch intake. * no-mistakes(review): Correct schema fixtures and remove prescriptive selection prompts * no-mistakes(document): Correct quota verification evidence chronology * Collapse quota-array-dispatch onto TOON-first spendPriority ranking. Decide from quota-axi's default TOON; keep --json as a rare defensive fallback. Rank by spendPriority after eligibility, reasoning-class, and runway-feasibility gates, and drop the hand-computed Pareto, pace, reserve, and window-id layers. * no-mistakes(review): Permit ambiguous JSON fallback and correct reset fixtures * no-mistakes(review): Correct runway semantics and escalate unresolved uncertainty * no-mistakes(document): Document TOON-first quota dispatch evidence * docs: add GROK_BOT.md Grok Bot system prompt (kunchenguid#2590) * docs: add GROK_BOT.md Grok Bot system prompt * docs: amend GROK_BOT.md with charter report-back and delegation marker * docs: classify GROK_BOT.md as public-product * docs: make GROK_BOT.md the plain Grok Bot system prompt * docs: update GROK_BOT.md nautical terms and self-improvement (kunchenguid#2592) * doc: Update language in GROK_BOT.md for clarity Refine language for clarity and consistency in instructions. * fix(bin): preserve inactive reconciliation scan progress (kunchenguid#2595) * fix(bin): guarantee inactive-reconcile scan progress under second quantization The inactive-outcome scan computed its aggregate deadline in whole seconds, so a 1-second budget's effective value lands anywhere in (0,1]; a scan starting just before a wall-clock second boundary rounded its whole budget away mid-scan and exited having visited no child, while the durable cursor had already advanced past the never-examined child. This is the CI flake behind tests/fm-inactive-reconcile.test.sh's 'next bounded scan did not resume with the following child' (watcher-wake-lock family, portable serial 2, seen on the PR kunchenguid#2590 run). Every scan now visits at least its first due child with the per-child state-read bound floored at one second, so no invocation can be a zero-work no-op. The outer process-group kill moves to budget+1s: the scan's own deadline enforces the budget, and the kill is a backstop for a scan wedged in an unbounded wait instead of a racer that routinely preempts the clean bounded exit. The wake-lock-wait test bound tracks the backstop (3s -> 4s); the previously flaky assertion is unchanged. * no-mistakes(document): Document inactive-reconcile deadline backstop * doc: Revise Firstmate delegation and communication guidelines Refactor the guidelines for Firstmate's role and delegation process, emphasizing the importance of crewmates and asynchronous work. * doc: Update work delegation and secret management instructions Clarified guidelines for handing off work to crewmates and managing secrets. * test(procevent): make the process-event suite's detached-runner assertions deterministic (kunchenguid#2617) Three assertions in tests/fm-procevent.test.sh depended on a detached runner having finished work that the command starting it does not wait for. reconcile's replacement runner is started through detach_runner, which only forks: reconcile returns and counts the start before that runner has claimed its source or exec'd its child. Any assertion taken straight after reconcile therefore samples a race. - The publish-before-apply recovery section left its always-ready /bin/echo source registered across the recovery reconcile, so that reconcile launched a competing detached poll (observed: started=1) that then raced every later assertion for the source claim, the next capture sequence, and this home's applied record, and outlived the section holding a live claim. It is now retired before that reconcile - re-announcement is proven from the durable inbox alone and needs no registration - and started=0 is asserted so a competing poll cannot be reintroduced unnoticed. This is the same retire-before-reconcile discipline the self-announcing section already carries; that section acquired it after the identical race made its "not-autohandled: self-src" assertion read "already owned: self-src". - The crashed-leader replacement section snapshotted the replacement's claim file and execution log behind a fixed 0.5s settle window. On a loaded machine that window expires first, which is the CI flake behind "a replacement runner started without recording its own claim" and "reconcile did not start exactly one replacement source". Both effects are now waited for with the suite's bounded wait helpers; the exact one-replacement count is still asserted afterwards, unchanged. - The duplicate-start section slept 0.5s for reconcile's runner to record ownership before asserting that a second start loses to it. It now waits for that claim. Also tighten one assertion that could not fail as written: "autohandled: self-src" is a substring of "not-autohandled: self-src", so the applied path was accepted even when the runner reported the capture left for the handler. Evidence: on the unmodified suite, 128 full runs at 6-8x concurrency produced 6 failing runs, all in the crashed-leader section. On the fixed suite, 216 full runs under the same load produced none. Reverting the self-announcing section's retire-before-reconcile line reproduces "already owned: self-src" on the first iteration, confirming the shared mechanism. * fix: preserve pending replies and defer remote reposts (kunchenguid#2618) * fix(bin): keep pending-reply expectations honest on both send legs Two related asymmetries let the parent-owned secondmate reply guard drop or nag requests it should not have. Local delivered-unconfirmed dropped the expectation. A marked request whose submit read-back stayed unconfirmed (verdict=pending) is the same not-a-failure outcome the remote leg reports as delivered, but fm-send discarded the parent's pending-reply record for it, so a request that very likely landed stopped being tracked entirely. The record now stays armed on its unconfirmed-delivery marker: a correlated report still resolves it, and an unanswered one still surfaces through the library's own reconciliation. Exit 3 and the local rule that an unconfirmed answer never closes a decision key are unchanged. Remote replies were nagged for a repost they did not need. A remote mate's report reaches the parent's status log only through the asynchronous mirror in fm-procevent-remote-reply.sh, yet the guard read an absent correlated line as proof the mate never reported - even while the answer was still in flight, which is the common case because the mirror's poll window is comparable to the recovery grace. The mirror now publishes one caught-up watermark from a quiet window, and the guard admits a missing report as evidence only once that watermark passes the turn that should have produced it. A genuinely missed report still gets exactly one repost, and a channel that is behind, unarmed, or broken leaves the request durably open and un-nagged rather than nagging blind; the mirror escalates its own continuity failures as before. Tests: a local unconfirmed secondmate send keeps its expectation armed and resolvable; a mirrored correlated remote reply resolves with no repost; a stale or absent watermark withholds the repost while a fresh one still releases it; a quiet remote window publishes the watermark and retirement clears it. * no-mistakes(review): Distinguish preempted polls from quiet windows * no-mistakes(document): Clarify remote reply channel freshness * no-mistakes(lint): Annotate shared remote preemption exit constant * fix(bin): honor declared pauses in busy-pane wedge checks (kunchenguid#2619) * fix(watch): honor a declared pause on a busy pane's completed-turn bound A worker that declares an external wait (`paused:`) and then blocks in one long foreground call - a review-hosting scout parked in a single blocking `lavish-axi poll`, a bounded watch loop, a rate-limit sleep - keeps its pane BUSY, so the stale path that already honors declared pauses never ran for it. The busy-pane completed-turn bound instead routed it straight into wedge_timer_check, which re-escalated "possible wedge, escalation N" (and, past the threshold, demand-deep-inspection) every FM_STALE_ESCALATE_SECS for as long as the review stayed open. busy_turn_bound_check now owns which absorber takes a crossed bound: a crew whose own last status line declares an external wait or a verified captain-held transfer takes the bounded FM_PAUSE_RESURFACE_SECS recheck, and everything else keeps the unchanged wedge timer. The discriminator is the declaration together with liveness (the caller has already confirmed the pane is busy), never a blanket silencing - a crew that declared nothing, or whose pane is not live, escalates exactly as before, and a declared pause still re-surfaces once per long cadence so a forgotten wait cannot rot invisibly. Away mode is untouched: the daemon owns pause triage there and already reads the same vocabulary. The two call sites also no longer clear pause bookkeeping in the same poll the pause cadence recorded it, which would have erased the re-surface throttle and turned the long cadence back into a per-poll re-surface. Tests: a three-phase regression fixture pins the absorbed pause, its long-cadence recheck, and the restored wedge escalation once the declaration is lifted on the same busy over-age pane. Also de-flakes tests/fm-watch-triage.test.sh, which failed spuriously on a loaded machine: fixed liveness budgets were reaping watchers mid-startup, so assertions on post-poll state passed vacuously or failed spuriously. Waits that describe a poll's outcome now wait for a completed poll cycle via the liveness beacon, the heartbeat test waits for the heartbeat it asserts on, and every wait_for_exit budget is the uniform 10s already used elsewhere in the file. * no-mistakes(review): Fail poll-cycle waits on timeout * no-mistakes(review): Prevent poll timeout test hangs * no-mistakes(document): Clarify paused busy-pane supervision * doc: Enhance communication guidelines for decision-making Added guidelines for decision communication to the captain. * doc: Update task delegation and communication guidelines Clarify communication protocols with crewmates regarding task delegation and reporting. * fix(bin): reliably confirm herdr steer submission (kunchenguid#2647) * fix(herdr): confirm local steers that native agent-state misses Herdr can leave agent_status idle for a landed Claude turn and can keep queued Enter text visible while busy, so fm-send was reporting false swallows. Confirm those cases through the shared queued-Enter verdict and a cleared composer, and keep a genuine idle pending composer as unconfirmed. * no-mistakes(review): Stop Herdr Enter retries on unreadable composers * no-mistakes(review): Reject queued delivery when all Herdr Enter sends fail * no-mistakes(review): Prevent confirmation after failed Herdr Enter * no-mistakes(review): Pace Herdr retries and clarify submit fallback * no-mistakes(review): Align Herdr submit docs with idle fallback * no-mistakes(document): Correct Herdr submit-confirmation documentation * feat(bearings): add interactive Lavish fleet board (kunchenguid#2659) * feat(bin): accept any-origin decision bindings with full-identity keys An aggregation surface (the bearings board) carries captain answers for holds across origins, but a binding was one-origin-per-source and the Lavish adapter capped question keys at 64 chars while real full hold identities measure 69-81. - fm-decision-hold.sh: bind <source-id> --any-origin records the (any) marker; binding prints it verbatim and answers accepts it, so the runner's feed seam carries an any-origin source with no runner change. In any-origin mode each key is a full hold identity <origin>-decision-<key>, split at its first -decision-; a key with no separator (merge/dispatch instructions) is skipped and feeds nothing, keeping non-decision answers out of the hold ledger by construction. Every existing close guard applies unchanged. - fm-procevent-lavish.sh: raise the question-key cap 64 -> 128 so a full hold identity fits; the slug-shape security property is unchanged. - tests: cross-origin closure through the real runner seam, an 81-char identity through the adapter, cap and shape refusals, routed-work skips, nonexistent-identity skips, and idempotent replay. * feat(bearings): add the /bearings lavish interactive fleet board /bearings lavish renders the bearings snapshot onto a shipped, reusable board template and arms it as a Lavish process-event source, so the captain answers Captain's Call items on the board and firstmate is woken by an ordinary check wake - no conversational turn ever blocks on a poll. - .agents/skills/bearings/assets/board-template.html: the shipped template (myfirstmate design system inlined, one fm-bearings-board.v1 JSON slot, fail-closed schema guard that renders an error card instead of an empty fleet). Per-invocation agent work is composing the payload only. - bin/fm-bearings-board.sh: build/refresh owner - fail-closed payload validation, slot injection with a round-trip check and \u003c escaping, stable board path, any-origin bind ALWAYS before arm, arm-if-absent. - bearings SKILL.md: the lavish invocation option, board composition rules, board-wake handling, and the captain-ruled merge-click authorization with its mandatory safeguards (PR resolved from the task's own meta record, wake-time green re-verification, never a red or changed PR, merges only through bin/fm-pr-merge.sh, chat echo with the full PR URL). - process-event-sources SKILL.md: one-line board-wake routing trigger. - tests: payload refusals, injection round-trip, bind-before-arm, idempotent re-arm, and template slot integrity. Fleet pickup: homes receive this after merge plus a firstmate self-update; landing timing is coordinated with the main firstmate. * no-mistakes(review): Harden bearings board validation and wake handling * no-mistakes(review): Require HTTPS for bearings board PR links * no-mistakes(review): Fail closed and bound bearings board answers * no-mistakes(review): Enforce UTF-8 byte limits for board answers * no-mistakes(review): Serve bearings board before arming and reject empty actions * no-mistakes(review): Prove bind-before-arm ordering through live answer consumption * no-mistakes(document): Document bearings board and cross-origin answers * no-mistakes(document): doc: document /bearings lavish fleet board mode in README --------- Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
digbycampbell
added a commit
to digbycampbell/firstmate
that referenced
this pull request
Aug 21, 2026
* fix: keep the public promise reachable when work is routed to a second mate (kunchenguid#2457) The lightweight Relay follow-up link lives in the answering home's own state/<task-id>.meta, so it can only bind work that home owns. When a Relay-linked request is routed to a second mate, the task record lives in the second mate's home, fm-x-link.sh failed with a bare "no such task ...meta", and nothing else picked the promise up: only the soft acknowledgement was ever posted. The typed promised-final path already supports --work-home secondmate:<id>; the playbook simply never chose it. - fmx-respond now states the routing rule crisply: a task in this home takes the lightweight link, and second-mate-routed work takes a promised-final commitment bound to that home, registered up front with the brief command carried into the routed worker's instructions. - fm-x-link.sh refuses a task with no local record by naming the registered second mate whose home actually holds it and printing the promised-final registration command, with the exact --work-home when the match is unambiguous. A home with no registered second mates keeps the plain error. - fm-backlog-handoff.sh reports, after a successful move, any moved key that still owes a public reply bound to main/<key>, since that binding no longer names the home owning the work. The move itself is never blocked. Docs and the secondmate handoff prose follow the same rule. Tests cover the refusal, its scoping, the unchanged local-link path, and both handoff outcomes at the script boundary. * docs(skills): add remote-secondmate recovery hint for false-negative verdicts (kunchenguid#2456) * fix(skills): hint that remote secondmate liveness verdicts false-negative fm-crew-state and fm-send routinely misreport a live remote secondmate as dead; confirm against the pane before relaunching, and relaunch only through fm-spawn.sh, never raw herdr pane surgery. * no-mistakes: apply CI fixes * fix(calm): keep Pi's export confirmation visible (kunchenguid#2461) Pi 0.83.0 added a status line to every tool-expansion change, and Pi updates the previous status line in place when two status messages arrive back to back. Calm's post-export redraw cycled tool expansion on the macrotask right after Pi printed "Session exported to: <path>", so both expansion status lines coalesced over that confirmation and the captain was left with no record of where their export landed. Calm now repaints only the tool rows it presents, by invalidating each row through the render context Pi hands its render slots, and requests the surrounding redraw through setStatus. Neither appends to the transcript. The repaint is still needed because Pi can re-render a row asynchronously - the built-in edit row invalidates itself once its diff is ready - and that re-render can land inside the window where /export forces stock rendering. The real-terminal /export case now asserts the confirmation is still on screen after the redraw has settled, and that the redraw restored every Calm-hidden row, instead of only racing the moment the confirmation first appeared. * feat(stow): add open-record persistence to /stow before reset (kunchenguid#2488) * feat(stow): persist the open records a session is holding /stow curated memory and captured session knowledge, but never touched record state, while AGENTS.md called it an "unfinished-work sweep" and the receipt declared the session "safe to reset" - wording that implied a record-correctness guarantee stow does not make. A shipped PR with no backlog item, a queued umbrella whose phases had merged, and four decision holds left open after their answers shipped all survived repeated stows. Add a bounded pass that files record state from the same volatile input the rest of stow already uses: the open threads in context, minutes before the reset destroys them. It creates a record for an unfiled thread and corrects one the session knows is wrong, through the owning path, and states its boundary as part of the contract - it never enumerates the backlog, lists holds, or queries a forge, because it cannot be a reconciliation and must not be read as one. Correct the wording in AGENTS.md and the completion receipt so reset-safe means what it actually guarantees: nothing this session knew was lost. * no-mistakes(review): correct stow decision-hold inspection to read hold via tasks-axi * no-mistakes(document): note /stow open-record persistence in README command catalog * refactor(stow): state open-record persistence as principle, not procedure The first version enumerated triggers, named commands, and prescribed an ordered procedure. That is too rigid for an agent skill: it invites literal execution of a checklist instead of judgment, and every enumerated example is a way for the guidance to go stale. Reduce it to the intent - before a reset, the important open work you are holding in context must end up durably recorded rather than dying with the session, filing what is unfiled and correcting what is stale - and let the agent judge importance, the record, and the owning write path. Keep the scope bound, since it is a decided contract and not a mechanic: this covers the open work the session is holding, never a reconciliation of durable records against repository or forge reality. The wording corrections in AGENTS.md and the completion receipt are unchanged. * fix(decisions): close decision holds at answer time via one general keyed-answer path (kunchenguid#2490) * fix(decisions): close captain holds at answer time Firstmate had two "a decision is open" ledgers with asymmetric closing mechanics. The live status-log ledger closes atomically at answer time, because bin/fm-send.sh --resolve-key makes answering a decision be the act that closes it. The durable backlog hold ledger had no such coupling: answering and recording were two separate acts, and only the first was forced by the workflow. That asymmetry lost four real captain decisions. Their answers were captured durably to disk, keyed character for character by the hold decision keys, acknowledged, and even implemented and shipped, yet the holds stayed open for two days and the captain was asked to re-answer decisions already on his own disk. Give the hold ledger the same answer-time-closure property: - bin/fm-decision-hold.sh gains an `answer` subcommand, the hold ledger's counterpart to --resolve-key. It shares one unrouted close implementation with `decline`, so it carries every existing guard - the captain decision file, the active-hold requirement, retry identity, and the refusal to release still-routed work - and differs only in the resolution mode it records. `decline` keeps its stronger meaning that the answer routes no follow-up work at all. - bin/fm-procevent-lavish.sh wires the channel that actually carried the lost answers. `arm --decisions-origin` binds a deck to the origin whose holds it carries, `answers` reads the structured choices out of a captured poll result, `close-decisions` maps each key to its hold and closes it through the command above, and `autohandle` lets the runner apply that at capture time. Safety is preserved rather than traded away. Only rows tagged `choice` are read, so freeform captain prose cannot forge a decision key. Closure is confined to the one bound origin. The decision text is a pure function of the captured result, so a replayed capture is idempotent. A hold that is absent, already closed, or still blocking routed work is skipped and left for `resolve`, never forced. A deck armed without the binding touches no hold at all. And autohandle deliberately never reports full handling, because recording an answer is transcription while acting on it is firstmate's judgement - so the check wake still reaches the handler. fm-send --resolve-key is untouched. * no-mistakes(document): document state/lavish-decisions binding dir in AGENTS.md state inventory * refactor(decisions): make keyed-answer closure one general capability The previous pass gave holds answer-time closure but built it as bespoke Lavish wiring: the review adapter carried the source-to-origin binding, mapped keys to hold identities, wrote decision records, decided what to skip, and closed holds itself. That treated a review deck as a special decision source. It is not - it is an ephemeral discussion format that happens to carry answers. Collapse it into ONE general capability with one owner. bin/fm-decision-hold.sh now owns the whole of "a keyed answer closes its matching hold": - `answers <origin> --source <provenance>` is the channel-agnostic intake. It reads key/answer/label lines on stdin, maps each key to its hold, and closes it through the same `answer` path, so every guard applies identically whatever channel the answer came from. --source is provenance recorded in the decision, never a behavior switch; there is no per-channel branch and no knowledge of chat, decks, or transports. - `bind`/`unbind`/`binding` own the source-to-origin binding for any channel whose answers arrive detached from their origin. Every channel is now an ordinary caller that only turns what it received into keyed lines: - bin/fm-send.sh (chat) feeds the intake for a key that names an active hold. This also fixes a real gap: once `complete` transfers a decision to its hold it closes the live status copy, so --resolve-key alone could never answer a transferred decision. - bin/fm-procevent.sh feeds it generically. A bound source's captured result goes to `<adapter> answers <result-file>` and whatever that prints is piped into the intake. The runner names no adapter, parses no result, and carries no decision rule, so any future adapter with an `answers` command works with no change here. - bin/fm-procevent-lavish.sh keeps only `answers`, which reports the structured choices a review captured and stops. It maps nothing to a hold and closes nothing; it lost ~160 lines of decision logic. Feeding is independent of handling, so it never acknowledges a result and never suppresses a wake - recording an answer is transcription, acting on it stays firstmate's judgement. The regression that proves closure now drives a FIXTURE adapter that is not the review adapter, so what is proven is that any bound channel reaches the intake rather than that one channel is wired specially. A new regression drives the real fm-send over a stubbed transport for the chat side. Every prior guarantee still holds, and fm-send's status-log behavior is unchanged. * no-mistakes(review): test(decisions): drop source-content grep from hold-closure regression * fix(memory): emit a real @AGENTS.md pointer instead of a CLAUDE.md symlink (kunchenguid#2512) A Write aimed at CLAUDE.md followed the symlink and destroyed AGENTS.md. The installer now creates and migrates to a recoverable two-line pointer file. * fix(ci): keep CLAUDE.md pointer check valid (kunchenguid#2515) * ci: gate GitHub workflows with pinned actionlint (kunchenguid#2517) * fix(lint): catch malformed GitHub workflows before merge A self-broken ci.yml cannot report its own breakage, so parse every workflow in the local lint path that no-mistakes already runs. * fix(lint): pin actionlint instead of Ruby for workflow lint A self-broken ci.yml still has to fail in the local lint path, and the named tool for that gate is actionlint, not a new Ruby runtime. * no-mistakes(document): Clarify pinned workflow lint documentation * fix: install pinned lint tools across supported platforms (kunchenguid#2546) * fix: install pinned shellcheck and actionlint on macOS and linux arm64 The installers were hardcoded to linux amd64 and sha256sum, so a Mac dev could not satisfy the refuse-on-mismatch lint gate. Select the official per-platform archive and checksum, and fall back to shasum -a 256. * no-mistakes(document): Document cross-platform pinned lint installers * docs: reconcile test-evidence docs with store_in_repo: true (kunchenguid#2548) .no-mistakes.yaml has set test.evidence.store_in_repo: true since kunchenguid#2355, but CONTRIBUTING.md, docs/configuration.md, and docs/architecture.md still described the old policy of keeping evidence out of the repo in a temp directory. The current no-mistakes behavior for store_in_repo: true is to publish each run's test evidence to the orphan no-mistakes/evidence branch and link it from the PR body. That branch shares no history with code branches, so evidence never enters a pushed feature branch or the default branch, and CI's tracked personal fleet paths rule stays accurate. Docs only. No change to .no-mistakes.yaml or any workflow. * docs: clarify test evidence branch storage (kunchenguid#2549) * docs: correct test evidence storage comment in .no-mistakes.yaml * no-mistakes: apply CI fixes * docs: hint that live scouts may host their own Lavish review loop (kunchenguid#2563) Make that a first-class option in always-loaded instructions so firstmate does not default to mediating and tearing the scout down between iteration rounds. * fix(bin): report remote secondmate delivery and state truthfully (kunchenguid#2570) * fix(bin): report remote secondmate delivery and state truthfully A steer to a remote secondmate crosses fm-on.sh to a host-local fm-send leg whose unconfirmed submit read-back (verdict=pending, typically a busy mate whose harness queues the steer) was flattened into exit 1, so the parent printed "error: text not submitted" / "error: text not sent" and discarded the pending-reply expectation for a steer that had actually landed. fm-send now carries the verdict across the ssh boundary as a documented delivered-unconfirmed exit 3: the parent reports the steer as delivered with confirmation pending, exits 0, keeps the expectation armed (awaiting_report), and closes --resolve-key decisions, while transport loss (ssh 255) and real remote failures keep failing loudly with the remote leg's stderr attached. A local unconfirmed submit now also exits 3 with an honest non-error message and still never closes a decision key. fm-crew-state.sh and fm-peek.sh no longer read a remote mate's endpoint through local probes (which misreported a healthy mate as "worktree gone" / "can't find session: remote"): both now use the true remote source over fm-on.sh, and an unreachable or unreadable remote reads as unknown-remote, never as gone or dead. * no-mistakes(document): Document remote delivery and state truth * no-mistakes: apply CI fixes * feat: adopt spendPriority for quota dispatch (kunchenguid#2574) * Adopt quota-axi 0.1.29 spendPriority-primary array dispatch. quota-axi 0.1.29 publishes schema 5 with selection.spendPriority as the primary comparative signal and demotes derivation fields out of default --json. Rank comparable-fit candidates on that scalar, keep runway versus the completion horizon as a hard gate, and raise the compatibility floor so a pre-consolidation build cannot reach dispatch intake. * no-mistakes(review): Correct schema fixtures and remove prescriptive selection prompts * no-mistakes(document): Correct quota verification evidence chronology * Collapse quota-array-dispatch onto TOON-first spendPriority ranking. Decide from quota-axi's default TOON; keep --json as a rare defensive fallback. Rank by spendPriority after eligibility, reasoning-class, and runway-feasibility gates, and drop the hand-computed Pareto, pace, reserve, and window-id layers. * no-mistakes(review): Permit ambiguous JSON fallback and correct reset fixtures * no-mistakes(review): Correct runway semantics and escalate unresolved uncertainty * no-mistakes(document): Document TOON-first quota dispatch evidence * docs: add GROK_BOT.md Grok Bot system prompt (kunchenguid#2590) * docs: add GROK_BOT.md Grok Bot system prompt * docs: amend GROK_BOT.md with charter report-back and delegation marker * docs: classify GROK_BOT.md as public-product * docs: make GROK_BOT.md the plain Grok Bot system prompt * docs: update GROK_BOT.md nautical terms and self-improvement (kunchenguid#2592) * doc: Update language in GROK_BOT.md for clarity Refine language for clarity and consistency in instructions. * fix(bin): preserve inactive reconciliation scan progress (kunchenguid#2595) * fix(bin): guarantee inactive-reconcile scan progress under second quantization The inactive-outcome scan computed its aggregate deadline in whole seconds, so a 1-second budget's effective value lands anywhere in (0,1]; a scan starting just before a wall-clock second boundary rounded its whole budget away mid-scan and exited having visited no child, while the durable cursor had already advanced past the never-examined child. This is the CI flake behind tests/fm-inactive-reconcile.test.sh's 'next bounded scan did not resume with the following child' (watcher-wake-lock family, portable serial 2, seen on the PR kunchenguid#2590 run). Every scan now visits at least its first due child with the per-child state-read bound floored at one second, so no invocation can be a zero-work no-op. The outer process-group kill moves to budget+1s: the scan's own deadline enforces the budget, and the kill is a backstop for a scan wedged in an unbounded wait instead of a racer that routinely preempts the clean bounded exit. The wake-lock-wait test bound tracks the backstop (3s -> 4s); the previously flaky assertion is unchanged. * no-mistakes(document): Document inactive-reconcile deadline backstop * doc: Revise Firstmate delegation and communication guidelines Refactor the guidelines for Firstmate's role and delegation process, emphasizing the importance of crewmates and asynchronous work. * doc: Update work delegation and secret management instructions Clarified guidelines for handing off work to crewmates and managing secrets. * test(procevent): make the process-event suite's detached-runner assertions deterministic (kunchenguid#2617) Three assertions in tests/fm-procevent.test.sh depended on a detached runner having finished work that the command starting it does not wait for. reconcile's replacement runner is started through detach_runner, which only forks: reconcile returns and counts the start before that runner has claimed its source or exec'd its child. Any assertion taken straight after reconcile therefore samples a race. - The publish-before-apply recovery section left its always-ready /bin/echo source registered across the recovery reconcile, so that reconcile launched a competing detached poll (observed: started=1) that then raced every later assertion for the source claim, the next capture sequence, and this home's applied record, and outlived the section holding a live claim. It is now retired before that reconcile - re-announcement is proven from the durable inbox alone and needs no registration - and started=0 is asserted so a competing poll cannot be reintroduced unnoticed. This is the same retire-before-reconcile discipline the self-announcing section already carries; that section acquired it after the identical race made its "not-autohandled: self-src" assertion read "already owned: self-src". - The crashed-leader replacement section snapshotted the replacement's claim file and execution log behind a fixed 0.5s settle window. On a loaded machine that window expires first, which is the CI flake behind "a replacement runner started without recording its own claim" and "reconcile did not start exactly one replacement source". Both effects are now waited for with the suite's bounded wait helpers; the exact one-replacement count is still asserted afterwards, unchanged. - The duplicate-start section slept 0.5s for reconcile's runner to record ownership before asserting that a second start loses to it. It now waits for that claim. Also tighten one assertion that could not fail as written: "autohandled: self-src" is a substring of "not-autohandled: self-src", so the applied path was accepted even when the runner reported the capture left for the handler. Evidence: on the unmodified suite, 128 full runs at 6-8x concurrency produced 6 failing runs, all in the crashed-leader section. On the fixed suite, 216 full runs under the same load produced none. Reverting the self-announcing section's retire-before-reconcile line reproduces "already owned: self-src" on the first iteration, confirming the shared mechanism. * fix: preserve pending replies and defer remote reposts (kunchenguid#2618) * fix(bin): keep pending-reply expectations honest on both send legs Two related asymmetries let the parent-owned secondmate reply guard drop or nag requests it should not have. Local delivered-unconfirmed dropped the expectation. A marked request whose submit read-back stayed unconfirmed (verdict=pending) is the same not-a-failure outcome the remote leg reports as delivered, but fm-send discarded the parent's pending-reply record for it, so a request that very likely landed stopped being tracked entirely. The record now stays armed on its unconfirmed-delivery marker: a correlated report still resolves it, and an unanswered one still surfaces through the library's own reconciliation. Exit 3 and the local rule that an unconfirmed answer never closes a decision key are unchanged. Remote replies were nagged for a repost they did not need. A remote mate's report reaches the parent's status log only through the asynchronous mirror in fm-procevent-remote-reply.sh, yet the guard read an absent correlated line as proof the mate never reported - even while the answer was still in flight, which is the common case because the mirror's poll window is comparable to the recovery grace. The mirror now publishes one caught-up watermark from a quiet window, and the guard admits a missing report as evidence only once that watermark passes the turn that should have produced it. A genuinely missed report still gets exactly one repost, and a channel that is behind, unarmed, or broken leaves the request durably open and un-nagged rather than nagging blind; the mirror escalates its own continuity failures as before. Tests: a local unconfirmed secondmate send keeps its expectation armed and resolvable; a mirrored correlated remote reply resolves with no repost; a stale or absent watermark withholds the repost while a fresh one still releases it; a quiet remote window publishes the watermark and retirement clears it. * no-mistakes(review): Distinguish preempted polls from quiet windows * no-mistakes(document): Clarify remote reply channel freshness * no-mistakes(lint): Annotate shared remote preemption exit constant * fix(bin): honor declared pauses in busy-pane wedge checks (kunchenguid#2619) * fix(watch): honor a declared pause on a busy pane's completed-turn bound A worker that declares an external wait (`paused:`) and then blocks in one long foreground call - a review-hosting scout parked in a single blocking `lavish-axi poll`, a bounded watch loop, a rate-limit sleep - keeps its pane BUSY, so the stale path that already honors declared pauses never ran for it. The busy-pane completed-turn bound instead routed it straight into wedge_timer_check, which re-escalated "possible wedge, escalation N" (and, past the threshold, demand-deep-inspection) every FM_STALE_ESCALATE_SECS for as long as the review stayed open. busy_turn_bound_check now owns which absorber takes a crossed bound: a crew whose own last status line declares an external wait or a verified captain-held transfer takes the bounded FM_PAUSE_RESURFACE_SECS recheck, and everything else keeps the unchanged wedge timer. The discriminator is the declaration together with liveness (the caller has already confirmed the pane is busy), never a blanket silencing - a crew that declared nothing, or whose pane is not live, escalates exactly as before, and a declared pause still re-surfaces once per long cadence so a forgotten wait cannot rot invisibly. Away mode is untouched: the daemon owns pause triage there and already reads the same vocabulary. The two call sites also no longer clear pause bookkeeping in the same poll the pause cadence recorded it, which would have erased the re-surface throttle and turned the long cadence back into a per-poll re-surface. Tests: a three-phase regression fixture pins the absorbed pause, its long-cadence recheck, and the restored wedge escalation once the declaration is lifted on the same busy over-age pane. Also de-flakes tests/fm-watch-triage.test.sh, which failed spuriously on a loaded machine: fixed liveness budgets were reaping watchers mid-startup, so assertions on post-poll state passed vacuously or failed spuriously. Waits that describe a poll's outcome now wait for a completed poll cycle via the liveness beacon, the heartbeat test waits for the heartbeat it asserts on, and every wait_for_exit budget is the uniform 10s already used elsewhere in the file. * no-mistakes(review): Fail poll-cycle waits on timeout * no-mistakes(review): Prevent poll timeout test hangs * no-mistakes(document): Clarify paused busy-pane supervision * doc: Enhance communication guidelines for decision-making Added guidelines for decision communication to the captain. * doc: Update task delegation and communication guidelines Clarify communication protocols with crewmates regarding task delegation and reporting. * fix(bin): reliably confirm herdr steer submission (kunchenguid#2647) * fix(herdr): confirm local steers that native agent-state misses Herdr can leave agent_status idle for a landed Claude turn and can keep queued Enter text visible while busy, so fm-send was reporting false swallows. Confirm those cases through the shared queued-Enter verdict and a cleared composer, and keep a genuine idle pending composer as unconfirmed. * no-mistakes(review): Stop Herdr Enter retries on unreadable composers * no-mistakes(review): Reject queued delivery when all Herdr Enter sends fail * no-mistakes(review): Prevent confirmation after failed Herdr Enter * no-mistakes(review): Pace Herdr retries and clarify submit fallback * no-mistakes(review): Align Herdr submit docs with idle fallback * no-mistakes(document): Correct Herdr submit-confirmation documentation * feat(bearings): add interactive Lavish fleet board (kunchenguid#2659) * feat(bin): accept any-origin decision bindings with full-identity keys An aggregation surface (the bearings board) carries captain answers for holds across origins, but a binding was one-origin-per-source and the Lavish adapter capped question keys at 64 chars while real full hold identities measure 69-81. - fm-decision-hold.sh: bind <source-id> --any-origin records the (any) marker; binding prints it verbatim and answers accepts it, so the runner's feed seam carries an any-origin source with no runner change. In any-origin mode each key is a full hold identity <origin>-decision-<key>, split at its first -decision-; a key with no separator (merge/dispatch instructions) is skipped and feeds nothing, keeping non-decision answers out of the hold ledger by construction. Every existing close guard applies unchanged. - fm-procevent-lavish.sh: raise the question-key cap 64 -> 128 so a full hold identity fits; the slug-shape security property is unchanged. - tests: cross-origin closure through the real runner seam, an 81-char identity through the adapter, cap and shape refusals, routed-work skips, nonexistent-identity skips, and idempotent replay. * feat(bearings): add the /bearings lavish interactive fleet board /bearings lavish renders the bearings snapshot onto a shipped, reusable board template and arms it as a Lavish process-event source, so the captain answers Captain's Call items on the board and firstmate is woken by an ordinary check wake - no conversational turn ever blocks on a poll. - .agents/skills/bearings/assets/board-template.html: the shipped template (myfirstmate design system inlined, one fm-bearings-board.v1 JSON slot, fail-closed schema guard that renders an error card instead of an empty fleet). Per-invocation agent work is composing the payload only. - bin/fm-bearings-board.sh: build/refresh owner - fail-closed payload validation, slot injection with a round-trip check and \u003c escaping, stable board path, any-origin bind ALWAYS before arm, arm-if-absent. - bearings SKILL.md: the lavish invocation option, board composition rules, board-wake handling, and the captain-ruled merge-click authorization with its mandatory safeguards (PR resolved from the task's own meta record, wake-time green re-verification, never a red or changed PR, merges only through bin/fm-pr-merge.sh, chat echo with the full PR URL). - process-event-sources SKILL.md: one-line board-wake routing trigger. - tests: payload refusals, injection round-trip, bind-before-arm, idempotent re-arm, and template slot integrity. Fleet pickup: homes receive this after merge plus a firstmate self-update; landing timing is coordinated with the main firstmate. * no-mistakes(review): Harden bearings board validation and wake handling * no-mistakes(review): Require HTTPS for bearings board PR links * no-mistakes(review): Fail closed and bound bearings board answers * no-mistakes(review): Enforce UTF-8 byte limits for board answers * no-mistakes(review): Serve bearings board before arming and reject empty actions * no-mistakes(review): Prove bind-before-arm ordering through live answer consumption * no-mistakes(document): Document bearings board and cross-origin answers --------- Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
prandelicious
added a commit
to prandelicious/firstmate
that referenced
this pull request
Aug 22, 2026
* ci: gate GitHub workflows with pinned actionlint (kunchenguid#2517) * fix(lint): catch malformed GitHub workflows before merge A self-broken ci.yml cannot report its own breakage, so parse every workflow in the local lint path that no-mistakes already runs. * fix(lint): pin actionlint instead of Ruby for workflow lint A self-broken ci.yml still has to fail in the local lint path, and the named tool for that gate is actionlint, not a new Ruby runtime. * no-mistakes(document): Clarify pinned workflow lint documentation * fix: install pinned lint tools across supported platforms (kunchenguid#2546) * fix: install pinned shellcheck and actionlint on macOS and linux arm64 The installers were hardcoded to linux amd64 and sha256sum, so a Mac dev could not satisfy the refuse-on-mismatch lint gate. Select the official per-platform archive and checksum, and fall back to shasum -a 256. * no-mistakes(document): Document cross-platform pinned lint installers * docs: reconcile test-evidence docs with store_in_repo: true (kunchenguid#2548) .no-mistakes.yaml has set test.evidence.store_in_repo: true since kunchenguid#2355, but CONTRIBUTING.md, docs/configuration.md, and docs/architecture.md still described the old policy of keeping evidence out of the repo in a temp directory. The current no-mistakes behavior for store_in_repo: true is to publish each run's test evidence to the orphan no-mistakes/evidence branch and link it from the PR body. That branch shares no history with code branches, so evidence never enters a pushed feature branch or the default branch, and CI's tracked personal fleet paths rule stays accurate. Docs only. No change to .no-mistakes.yaml or any workflow. * docs: clarify test evidence branch storage (kunchenguid#2549) * docs: correct test evidence storage comment in .no-mistakes.yaml * no-mistakes: apply CI fixes * docs: hint that live scouts may host their own Lavish review loop (kunchenguid#2563) Make that a first-class option in always-loaded instructions so firstmate does not default to mediating and tearing the scout down between iteration rounds. * fix(bin): report remote secondmate delivery and state truthfully (kunchenguid#2570) * fix(bin): report remote secondmate delivery and state truthfully A steer to a remote secondmate crosses fm-on.sh to a host-local fm-send leg whose unconfirmed submit read-back (verdict=pending, typically a busy mate whose harness queues the steer) was flattened into exit 1, so the parent printed "error: text not submitted" / "error: text not sent" and discarded the pending-reply expectation for a steer that had actually landed. fm-send now carries the verdict across the ssh boundary as a documented delivered-unconfirmed exit 3: the parent reports the steer as delivered with confirmation pending, exits 0, keeps the expectation armed (awaiting_report), and closes --resolve-key decisions, while transport loss (ssh 255) and real remote failures keep failing loudly with the remote leg's stderr attached. A local unconfirmed submit now also exits 3 with an honest non-error message and still never closes a decision key. fm-crew-state.sh and fm-peek.sh no longer read a remote mate's endpoint through local probes (which misreported a healthy mate as "worktree gone" / "can't find session: remote"): both now use the true remote source over fm-on.sh, and an unreachable or unreadable remote reads as unknown-remote, never as gone or dead. * no-mistakes(document): Document remote delivery and state truth * no-mistakes: apply CI fixes * feat: adopt spendPriority for quota dispatch (kunchenguid#2574) * Adopt quota-axi 0.1.29 spendPriority-primary array dispatch. quota-axi 0.1.29 publishes schema 5 with selection.spendPriority as the primary comparative signal and demotes derivation fields out of default --json. Rank comparable-fit candidates on that scalar, keep runway versus the completion horizon as a hard gate, and raise the compatibility floor so a pre-consolidation build cannot reach dispatch intake. * no-mistakes(review): Correct schema fixtures and remove prescriptive selection prompts * no-mistakes(document): Correct quota verification evidence chronology * Collapse quota-array-dispatch onto TOON-first spendPriority ranking. Decide from quota-axi's default TOON; keep --json as a rare defensive fallback. Rank by spendPriority after eligibility, reasoning-class, and runway-feasibility gates, and drop the hand-computed Pareto, pace, reserve, and window-id layers. * no-mistakes(review): Permit ambiguous JSON fallback and correct reset fixtures * no-mistakes(review): Correct runway semantics and escalate unresolved uncertainty * no-mistakes(document): Document TOON-first quota dispatch evidence * docs: add GROK_BOT.md Grok Bot system prompt (kunchenguid#2590) * docs: add GROK_BOT.md Grok Bot system prompt * docs: amend GROK_BOT.md with charter report-back and delegation marker * docs: classify GROK_BOT.md as public-product * docs: make GROK_BOT.md the plain Grok Bot system prompt * docs: update GROK_BOT.md nautical terms and self-improvement (kunchenguid#2592) * doc: Update language in GROK_BOT.md for clarity Refine language for clarity and consistency in instructions. * fix(bin): preserve inactive reconciliation scan progress (kunchenguid#2595) * fix(bin): guarantee inactive-reconcile scan progress under second quantization The inactive-outcome scan computed its aggregate deadline in whole seconds, so a 1-second budget's effective value lands anywhere in (0,1]; a scan starting just before a wall-clock second boundary rounded its whole budget away mid-scan and exited having visited no child, while the durable cursor had already advanced past the never-examined child. This is the CI flake behind tests/fm-inactive-reconcile.test.sh's 'next bounded scan did not resume with the following child' (watcher-wake-lock family, portable serial 2, seen on the PR kunchenguid#2590 run). Every scan now visits at least its first due child with the per-child state-read bound floored at one second, so no invocation can be a zero-work no-op. The outer process-group kill moves to budget+1s: the scan's own deadline enforces the budget, and the kill is a backstop for a scan wedged in an unbounded wait instead of a racer that routinely preempts the clean bounded exit. The wake-lock-wait test bound tracks the backstop (3s -> 4s); the previously flaky assertion is unchanged. * no-mistakes(document): Document inactive-reconcile deadline backstop * doc: Revise Firstmate delegation and communication guidelines Refactor the guidelines for Firstmate's role and delegation process, emphasizing the importance of crewmates and asynchronous work. * doc: Update work delegation and secret management instructions Clarified guidelines for handing off work to crewmates and managing secrets. * test(procevent): make the process-event suite's detached-runner assertions deterministic (kunchenguid#2617) Three assertions in tests/fm-procevent.test.sh depended on a detached runner having finished work that the command starting it does not wait for. reconcile's replacement runner is started through detach_runner, which only forks: reconcile returns and counts the start before that runner has claimed its source or exec'd its child. Any assertion taken straight after reconcile therefore samples a race. - The publish-before-apply recovery section left its always-ready /bin/echo source registered across the recovery reconcile, so that reconcile launched a competing detached poll (observed: started=1) that then raced every later assertion for the source claim, the next capture sequence, and this home's applied record, and outlived the section holding a live claim. It is now retired before that reconcile - re-announcement is proven from the durable inbox alone and needs no registration - and started=0 is asserted so a competing poll cannot be reintroduced unnoticed. This is the same retire-before-reconcile discipline the self-announcing section already carries; that section acquired it after the identical race made its "not-autohandled: self-src" assertion read "already owned: self-src". - The crashed-leader replacement section snapshotted the replacement's claim file and execution log behind a fixed 0.5s settle window. On a loaded machine that window expires first, which is the CI flake behind "a replacement runner started without recording its own claim" and "reconcile did not start exactly one replacement source". Both effects are now waited for with the suite's bounded wait helpers; the exact one-replacement count is still asserted afterwards, unchanged. - The duplicate-start section slept 0.5s for reconcile's runner to record ownership before asserting that a second start loses to it. It now waits for that claim. Also tighten one assertion that could not fail as written: "autohandled: self-src" is a substring of "not-autohandled: self-src", so the applied path was accepted even when the runner reported the capture left for the handler. Evidence: on the unmodified suite, 128 full runs at 6-8x concurrency produced 6 failing runs, all in the crashed-leader section. On the fixed suite, 216 full runs under the same load produced none. Reverting the self-announcing section's retire-before-reconcile line reproduces "already owned: self-src" on the first iteration, confirming the shared mechanism. * fix: preserve pending replies and defer remote reposts (kunchenguid#2618) * fix(bin): keep pending-reply expectations honest on both send legs Two related asymmetries let the parent-owned secondmate reply guard drop or nag requests it should not have. Local delivered-unconfirmed dropped the expectation. A marked request whose submit read-back stayed unconfirmed (verdict=pending) is the same not-a-failure outcome the remote leg reports as delivered, but fm-send discarded the parent's pending-reply record for it, so a request that very likely landed stopped being tracked entirely. The record now stays armed on its unconfirmed-delivery marker: a correlated report still resolves it, and an unanswered one still surfaces through the library's own reconciliation. Exit 3 and the local rule that an unconfirmed answer never closes a decision key are unchanged. Remote replies were nagged for a repost they did not need. A remote mate's report reaches the parent's status log only through the asynchronous mirror in fm-procevent-remote-reply.sh, yet the guard read an absent correlated line as proof the mate never reported - even while the answer was still in flight, which is the common case because the mirror's poll window is comparable to the recovery grace. The mirror now publishes one caught-up watermark from a quiet window, and the guard admits a missing report as evidence only once that watermark passes the turn that should have produced it. A genuinely missed report still gets exactly one repost, and a channel that is behind, unarmed, or broken leaves the request durably open and un-nagged rather than nagging blind; the mirror escalates its own continuity failures as before. Tests: a local unconfirmed secondmate send keeps its expectation armed and resolvable; a mirrored correlated remote reply resolves with no repost; a stale or absent watermark withholds the repost while a fresh one still releases it; a quiet remote window publishes the watermark and retirement clears it. * no-mistakes(review): Distinguish preempted polls from quiet windows * no-mistakes(document): Clarify remote reply channel freshness * no-mistakes(lint): Annotate shared remote preemption exit constant * fix(bin): honor declared pauses in busy-pane wedge checks (kunchenguid#2619) * fix(watch): honor a declared pause on a busy pane's completed-turn bound A worker that declares an external wait (`paused:`) and then blocks in one long foreground call - a review-hosting scout parked in a single blocking `lavish-axi poll`, a bounded watch loop, a rate-limit sleep - keeps its pane BUSY, so the stale path that already honors declared pauses never ran for it. The busy-pane completed-turn bound instead routed it straight into wedge_timer_check, which re-escalated "possible wedge, escalation N" (and, past the threshold, demand-deep-inspection) every FM_STALE_ESCALATE_SECS for as long as the review stayed open. busy_turn_bound_check now owns which absorber takes a crossed bound: a crew whose own last status line declares an external wait or a verified captain-held transfer takes the bounded FM_PAUSE_RESURFACE_SECS recheck, and everything else keeps the unchanged wedge timer. The discriminator is the declaration together with liveness (the caller has already confirmed the pane is busy), never a blanket silencing - a crew that declared nothing, or whose pane is not live, escalates exactly as before, and a declared pause still re-surfaces once per long cadence so a forgotten wait cannot rot invisibly. Away mode is untouched: the daemon owns pause triage there and already reads the same vocabulary. The two call sites also no longer clear pause bookkeeping in the same poll the pause cadence recorded it, which would have erased the re-surface throttle and turned the long cadence back into a per-poll re-surface. Tests: a three-phase regression fixture pins the absorbed pause, its long-cadence recheck, and the restored wedge escalation once the declaration is lifted on the same busy over-age pane. Also de-flakes tests/fm-watch-triage.test.sh, which failed spuriously on a loaded machine: fixed liveness budgets were reaping watchers mid-startup, so assertions on post-poll state passed vacuously or failed spuriously. Waits that describe a poll's outcome now wait for a completed poll cycle via the liveness beacon, the heartbeat test waits for the heartbeat it asserts on, and every wait_for_exit budget is the uniform 10s already used elsewhere in the file. * no-mistakes(review): Fail poll-cycle waits on timeout * no-mistakes(review): Prevent poll timeout test hangs * no-mistakes(document): Clarify paused busy-pane supervision * doc: Enhance communication guidelines for decision-making Added guidelines for decision communication to the captain. * doc: Update task delegation and communication guidelines Clarify communication protocols with crewmates regarding task delegation and reporting. * fix(bin): reliably confirm herdr steer submission (kunchenguid#2647) * fix(herdr): confirm local steers that native agent-state misses Herdr can leave agent_status idle for a landed Claude turn and can keep queued Enter text visible while busy, so fm-send was reporting false swallows. Confirm those cases through the shared queued-Enter verdict and a cleared composer, and keep a genuine idle pending composer as unconfirmed. * no-mistakes(review): Stop Herdr Enter retries on unreadable composers * no-mistakes(review): Reject queued delivery when all Herdr Enter sends fail * no-mistakes(review): Prevent confirmation after failed Herdr Enter * no-mistakes(review): Pace Herdr retries and clarify submit fallback * no-mistakes(review): Align Herdr submit docs with idle fallback * no-mistakes(document): Correct Herdr submit-confirmation documentation * feat(bearings): add interactive Lavish fleet board (kunchenguid#2659) * feat(bin): accept any-origin decision bindings with full-identity keys An aggregation surface (the bearings board) carries captain answers for holds across origins, but a binding was one-origin-per-source and the Lavish adapter capped question keys at 64 chars while real full hold identities measure 69-81. - fm-decision-hold.sh: bind <source-id> --any-origin records the (any) marker; binding prints it verbatim and answers accepts it, so the runner's feed seam carries an any-origin source with no runner change. In any-origin mode each key is a full hold identity <origin>-decision-<key>, split at its first -decision-; a key with no separator (merge/dispatch instructions) is skipped and feeds nothing, keeping non-decision answers out of the hold ledger by construction. Every existing close guard applies unchanged. - fm-procevent-lavish.sh: raise the question-key cap 64 -> 128 so a full hold identity fits; the slug-shape security property is unchanged. - tests: cross-origin closure through the real runner seam, an 81-char identity through the adapter, cap and shape refusals, routed-work skips, nonexistent-identity skips, and idempotent replay. * feat(bearings): add the /bearings lavish interactive fleet board /bearings lavish renders the bearings snapshot onto a shipped, reusable board template and arms it as a Lavish process-event source, so the captain answers Captain's Call items on the board and firstmate is woken by an ordinary check wake - no conversational turn ever blocks on a poll. - .agents/skills/bearings/assets/board-template.html: the shipped template (myfirstmate design system inlined, one fm-bearings-board.v1 JSON slot, fail-closed schema guard that renders an error card instead of an empty fleet). Per-invocation agent work is composing the payload only. - bin/fm-bearings-board.sh: build/refresh owner - fail-closed payload validation, slot injection with a round-trip check and \u003c escaping, stable board path, any-origin bind ALWAYS before arm, arm-if-absent. - bearings SKILL.md: the lavish invocation option, board composition rules, board-wake handling, and the captain-ruled merge-click authorization with its mandatory safeguards (PR resolved from the task's own meta record, wake-time green re-verification, never a red or changed PR, merges only through bin/fm-pr-merge.sh, chat echo with the full PR URL). - process-event-sources SKILL.md: one-line board-wake routing trigger. - tests: payload refusals, injection round-trip, bind-before-arm, idempotent re-arm, and template slot integrity. Fleet pickup: homes receive this after merge plus a firstmate self-update; landing timing is coordinated with the main firstmate. * no-mistakes(review): Harden bearings board validation and wake handling * no-mistakes(review): Require HTTPS for bearings board PR links * no-mistakes(review): Fail closed and bound bearings board answers * no-mistakes(review): Enforce UTF-8 byte limits for board answers * no-mistakes(review): Serve bearings board before arming and reject empty actions * no-mistakes(review): Prove bind-before-arm ordering through live answer consumption * no-mistakes(document): Document bearings board and cross-origin answers * fix(bearings): restore decision options and add close controls (kunchenguid#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 * ci: require no-mistakes pipeline step attestation (kunchenguid#2710) Signature-only PRs can hide skipped review, test, or document steps. Fail unless no-mistakes >= 1.46.0 attests those three steps completed. * no-mistakes(review): Bind pipeline attestations to current PR head * no-mistakes(review): Trust head statuses and privatize decision drops * no-mistakes(review): Validate published no-mistakes evidence commits * no-mistakes(document): Document authenticated no-mistakes evidence requirement * ci: allow manual workflow dispatch * no-mistakes(document): Refresh documentation audience verification counts * ci: run checks for edited pull requests * no-mistakes(document): Document current pull request validation triggers --------- Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
prandelicious
added a commit
to prandelicious/firstmate
that referenced
this pull request
Aug 22, 2026
* ci: gate GitHub workflows with pinned actionlint (#2517)
* fix(lint): catch malformed GitHub workflows before merge
A self-broken ci.yml cannot report its own breakage, so parse every
workflow in the local lint path that no-mistakes already runs.
* fix(lint): pin actionlint instead of Ruby for workflow lint
A self-broken ci.yml still has to fail in the local lint path, and the
named tool for that gate is actionlint, not a new Ruby runtime.
* no-mistakes(document): Clarify pinned workflow lint documentation
* fix: install pinned lint tools across supported platforms (#2546)
* fix: install pinned shellcheck and actionlint on macOS and linux arm64
The installers were hardcoded to linux amd64 and sha256sum, so a Mac
dev could not satisfy the refuse-on-mismatch lint gate. Select the
official per-platform archive and checksum, and fall back to shasum -a 256.
* no-mistakes(document): Document cross-platform pinned lint installers
* docs: reconcile test-evidence docs with store_in_repo: true (#2548)
.no-mistakes.yaml has set test.evidence.store_in_repo: true since #2355, but
CONTRIBUTING.md, docs/configuration.md, and docs/architecture.md still described
the old policy of keeping evidence out of the repo in a temp directory.
The current no-mistakes behavior for store_in_repo: true is to publish each run's
test evidence to the orphan no-mistakes/evidence branch and link it from the PR
body. That branch shares no history with code branches, so evidence never enters
a pushed feature branch or the default branch, and CI's tracked personal fleet
paths rule stays accurate.
Docs only. No change to .no-mistakes.yaml or any workflow.
* docs: clarify test evidence branch storage (#2549)
* docs: correct test evidence storage comment in .no-mistakes.yaml
* no-mistakes: apply CI fixes
* docs: hint that live scouts may host their own Lavish review loop (#2563)
Make that a first-class option in always-loaded instructions so firstmate does not default to mediating and tearing the scout down between iteration rounds.
* fix(bin): report remote secondmate delivery and state truthfully (#2570)
* fix(bin): report remote secondmate delivery and state truthfully
A steer to a remote secondmate crosses fm-on.sh to a host-local fm-send
leg whose unconfirmed submit read-back (verdict=pending, typically a busy
mate whose harness queues the steer) was flattened into exit 1, so the
parent printed "error: text not submitted" / "error: text not sent" and
discarded the pending-reply expectation for a steer that had actually
landed. fm-send now carries the verdict across the ssh boundary as a
documented delivered-unconfirmed exit 3: the parent reports the steer as
delivered with confirmation pending, exits 0, keeps the expectation armed
(awaiting_report), and closes --resolve-key decisions, while transport
loss (ssh 255) and real remote failures keep failing loudly with the
remote leg's stderr attached. A local unconfirmed submit now also exits 3
with an honest non-error message and still never closes a decision key.
fm-crew-state.sh and fm-peek.sh no longer read a remote mate's endpoint
through local probes (which misreported a healthy mate as "worktree gone"
/ "can't find session: remote"): both now use the true remote source over
fm-on.sh, and an unreachable or unreadable remote reads as unknown-remote,
never as gone or dead.
* no-mistakes(document): Document remote delivery and state truth
* no-mistakes: apply CI fixes
* feat: adopt spendPriority for quota dispatch (#2574)
* Adopt quota-axi 0.1.29 spendPriority-primary array dispatch.
quota-axi 0.1.29 publishes schema 5 with selection.spendPriority as the primary comparative signal and demotes derivation fields out of default --json. Rank comparable-fit candidates on that scalar, keep runway versus the completion horizon as a hard gate, and raise the compatibility floor so a pre-consolidation build cannot reach dispatch intake.
* no-mistakes(review): Correct schema fixtures and remove prescriptive selection prompts
* no-mistakes(document): Correct quota verification evidence chronology
* Collapse quota-array-dispatch onto TOON-first spendPriority ranking.
Decide from quota-axi's default TOON; keep --json as a rare defensive fallback.
Rank by spendPriority after eligibility, reasoning-class, and runway-feasibility gates, and drop the hand-computed Pareto, pace, reserve, and window-id layers.
* no-mistakes(review): Permit ambiguous JSON fallback and correct reset fixtures
* no-mistakes(review): Correct runway semantics and escalate unresolved uncertainty
* no-mistakes(document): Document TOON-first quota dispatch evidence
* docs: add GROK_BOT.md Grok Bot system prompt (#2590)
* docs: add GROK_BOT.md Grok Bot system prompt
* docs: amend GROK_BOT.md with charter report-back and delegation marker
* docs: classify GROK_BOT.md as public-product
* docs: make GROK_BOT.md the plain Grok Bot system prompt
* docs: update GROK_BOT.md nautical terms and self-improvement (#2592)
* doc: Update language in GROK_BOT.md for clarity
Refine language for clarity and consistency in instructions.
* fix(bin): preserve inactive reconciliation scan progress (#2595)
* fix(bin): guarantee inactive-reconcile scan progress under second quantization
The inactive-outcome scan computed its aggregate deadline in whole seconds,
so a 1-second budget's effective value lands anywhere in (0,1]; a scan
starting just before a wall-clock second boundary rounded its whole budget
away mid-scan and exited having visited no child, while the durable cursor
had already advanced past the never-examined child. This is the CI flake
behind tests/fm-inactive-reconcile.test.sh's 'next bounded scan did not
resume with the following child' (watcher-wake-lock family, portable
serial 2, seen on the PR #2590 run).
Every scan now visits at least its first due child with the per-child
state-read bound floored at one second, so no invocation can be a zero-work
no-op. The outer process-group kill moves to budget+1s: the scan's own
deadline enforces the budget, and the kill is a backstop for a scan wedged
in an unbounded wait instead of a racer that routinely preempts the clean
bounded exit. The wake-lock-wait test bound tracks the backstop (3s -> 4s);
the previously flaky assertion is unchanged.
* no-mistakes(document): Document inactive-reconcile deadline backstop
* doc: Revise Firstmate delegation and communication guidelines
Refactor the guidelines for Firstmate's role and delegation process, emphasizing the importance of crewmates and asynchronous work.
* doc: Update work delegation and secret management instructions
Clarified guidelines for handing off work to crewmates and managing secrets.
* test(procevent): make the process-event suite's detached-runner assertions deterministic (#2617)
Three assertions in tests/fm-procevent.test.sh depended on a detached runner
having finished work that the command starting it does not wait for.
reconcile's replacement runner is started through detach_runner, which only
forks: reconcile returns and counts the start before that runner has claimed
its source or exec'd its child. Any assertion taken straight after reconcile
therefore samples a race.
- The publish-before-apply recovery section left its always-ready /bin/echo
source registered across the recovery reconcile, so that reconcile launched
a competing detached poll (observed: started=1) that then raced every later
assertion for the source claim, the next capture sequence, and this home's
applied record, and outlived the section holding a live claim. It is now
retired before that reconcile - re-announcement is proven from the durable
inbox alone and needs no registration - and started=0 is asserted so a
competing poll cannot be reintroduced unnoticed. This is the same
retire-before-reconcile discipline the self-announcing section already
carries; that section acquired it after the identical race made its
"not-autohandled: self-src" assertion read "already owned: self-src".
- The crashed-leader replacement section snapshotted the replacement's claim
file and execution log behind a fixed 0.5s settle window. On a loaded
machine that window expires first, which is the CI flake behind "a
replacement runner started without recording its own claim" and "reconcile
did not start exactly one replacement source". Both effects are now waited
for with the suite's bounded wait helpers; the exact one-replacement count
is still asserted afterwards, unchanged.
- The duplicate-start section slept 0.5s for reconcile's runner to record
ownership before asserting that a second start loses to it. It now waits
for that claim.
Also tighten one assertion that could not fail as written: "autohandled:
self-src" is a substring of "not-autohandled: self-src", so the applied path
was accepted even when the runner reported the capture left for the handler.
Evidence: on the unmodified suite, 128 full runs at 6-8x concurrency produced
6 failing runs, all in the crashed-leader section. On the fixed suite, 216
full runs under the same load produced none. Reverting the self-announcing
section's retire-before-reconcile line reproduces "already owned: self-src"
on the first iteration, confirming the shared mechanism.
* fix: preserve pending replies and defer remote reposts (#2618)
* fix(bin): keep pending-reply expectations honest on both send legs
Two related asymmetries let the parent-owned secondmate reply guard drop or
nag requests it should not have.
Local delivered-unconfirmed dropped the expectation. A marked request whose
submit read-back stayed unconfirmed (verdict=pending) is the same
not-a-failure outcome the remote leg reports as delivered, but fm-send
discarded the parent's pending-reply record for it, so a request that very
likely landed stopped being tracked entirely. The record now stays armed on
its unconfirmed-delivery marker: a correlated report still resolves it, and
an unanswered one still surfaces through the library's own reconciliation.
Exit 3 and the local rule that an unconfirmed answer never closes a decision
key are unchanged.
Remote replies were nagged for a repost they did not need. A remote mate's
report reaches the parent's status log only through the asynchronous mirror
in fm-procevent-remote-reply.sh, yet the guard read an absent correlated
line as proof the mate never reported - even while the answer was still in
flight, which is the common case because the mirror's poll window is
comparable to the recovery grace. The mirror now publishes one caught-up
watermark from a quiet window, and the guard admits a missing report as
evidence only once that watermark passes the turn that should have produced
it. A genuinely missed report still gets exactly one repost, and a channel
that is behind, unarmed, or broken leaves the request durably open and
un-nagged rather than nagging blind; the mirror escalates its own continuity
failures as before.
Tests: a local unconfirmed secondmate send keeps its expectation armed and
resolvable; a mirrored correlated remote reply resolves with no repost; a
stale or absent watermark withholds the repost while a fresh one still
releases it; a quiet remote window publishes the watermark and retirement
clears it.
* no-mistakes(review): Distinguish preempted polls from quiet windows
* no-mistakes(document): Clarify remote reply channel freshness
* no-mistakes(lint): Annotate shared remote preemption exit constant
* fix(bin): honor declared pauses in busy-pane wedge checks (#2619)
* fix(watch): honor a declared pause on a busy pane's completed-turn bound
A worker that declares an external wait (`paused:`) and then blocks in one
long foreground call - a review-hosting scout parked in a single blocking
`lavish-axi poll`, a bounded watch loop, a rate-limit sleep - keeps its pane
BUSY, so the stale path that already honors declared pauses never ran for it.
The busy-pane completed-turn bound instead routed it straight into
wedge_timer_check, which re-escalated "possible wedge, escalation N" (and, past
the threshold, demand-deep-inspection) every FM_STALE_ESCALATE_SECS for as long
as the review stayed open.
busy_turn_bound_check now owns which absorber takes a crossed bound: a crew
whose own last status line declares an external wait or a verified captain-held
transfer takes the bounded FM_PAUSE_RESURFACE_SECS recheck, and everything else
keeps the unchanged wedge timer. The discriminator is the declaration together
with liveness (the caller has already confirmed the pane is busy), never a
blanket silencing - a crew that declared nothing, or whose pane is not live,
escalates exactly as before, and a declared pause still re-surfaces once per
long cadence so a forgotten wait cannot rot invisibly. Away mode is untouched:
the daemon owns pause triage there and already reads the same vocabulary.
The two call sites also no longer clear pause bookkeeping in the same poll the
pause cadence recorded it, which would have erased the re-surface throttle and
turned the long cadence back into a per-poll re-surface.
Tests: a three-phase regression fixture pins the absorbed pause, its long-cadence
recheck, and the restored wedge escalation once the declaration is lifted on the
same busy over-age pane.
Also de-flakes tests/fm-watch-triage.test.sh, which failed spuriously on a loaded
machine: fixed liveness budgets were reaping watchers mid-startup, so assertions
on post-poll state passed vacuously or failed spuriously. Waits that describe a
poll's outcome now wait for a completed poll cycle via the liveness beacon, the
heartbeat test waits for the heartbeat it asserts on, and every wait_for_exit
budget is the uniform 10s already used elsewhere in the file.
* no-mistakes(review): Fail poll-cycle waits on timeout
* no-mistakes(review): Prevent poll timeout test hangs
* no-mistakes(document): Clarify paused busy-pane supervision
* doc: Enhance communication guidelines for decision-making
Added guidelines for decision communication to the captain.
* doc: Update task delegation and communication guidelines
Clarify communication protocols with crewmates regarding task delegation and reporting.
* fix(bin): reliably confirm herdr steer submission (#2647)
* fix(herdr): confirm local steers that native agent-state misses
Herdr can leave agent_status idle for a landed Claude turn and can keep
queued Enter text visible while busy, so fm-send was reporting false
swallows. Confirm those cases through the shared queued-Enter verdict
and a cleared composer, and keep a genuine idle pending composer as
unconfirmed.
* no-mistakes(review): Stop Herdr Enter retries on unreadable composers
* no-mistakes(review): Reject queued delivery when all Herdr Enter sends fail
* no-mistakes(review): Prevent confirmation after failed Herdr Enter
* no-mistakes(review): Pace Herdr retries and clarify submit fallback
* no-mistakes(review): Align Herdr submit docs with idle fallback
* no-mistakes(document): Correct Herdr submit-confirmation documentation
* feat(bearings): add interactive Lavish fleet board (#2659)
* feat(bin): accept any-origin decision bindings with full-identity keys
An aggregation surface (the bearings board) carries captain answers for holds
across origins, but a binding was one-origin-per-source and the Lavish adapter
capped question keys at 64 chars while real full hold identities measure 69-81.
- fm-decision-hold.sh: bind <source-id> --any-origin records the (any) marker;
binding prints it verbatim and answers accepts it, so the runner's feed seam
carries an any-origin source with no runner change. In any-origin mode each
key is a full hold identity <origin>-decision-<key>, split at its first
-decision-; a key with no separator (merge/dispatch instructions) is skipped
and feeds nothing, keeping non-decision answers out of the hold ledger by
construction. Every existing close guard applies unchanged.
- fm-procevent-lavish.sh: raise the question-key cap 64 -> 128 so a full hold
identity fits; the slug-shape security property is unchanged.
- tests: cross-origin closure through the real runner seam, an 81-char
identity through the adapter, cap and shape refusals, routed-work skips,
nonexistent-identity skips, and idempotent replay.
* feat(bearings): add the /bearings lavish interactive fleet board
/bearings lavish renders the bearings snapshot onto a shipped, reusable board
template and arms it as a Lavish process-event source, so the captain answers
Captain's Call items on the board and firstmate is woken by an ordinary check
wake - no conversational turn ever blocks on a poll.
- .agents/skills/bearings/assets/board-template.html: the shipped template
(myfirstmate design system inlined, one fm-bearings-board.v1 JSON slot,
fail-closed schema guard that renders an error card instead of an empty
fleet). Per-invocation agent work is composing the payload only.
- bin/fm-bearings-board.sh: build/refresh owner - fail-closed payload
validation, slot injection with a round-trip check and \u003c escaping,
stable board path, any-origin bind ALWAYS before arm, arm-if-absent.
- bearings SKILL.md: the lavish invocation option, board composition rules,
board-wake handling, and the captain-ruled merge-click authorization with
its mandatory safeguards (PR resolved from the task's own meta record,
wake-time green re-verification, never a red or changed PR, merges only
through bin/fm-pr-merge.sh, chat echo with the full PR URL).
- process-event-sources SKILL.md: one-line board-wake routing trigger.
- tests: payload refusals, injection round-trip, bind-before-arm, idempotent
re-arm, and template slot integrity.
Fleet pickup: homes receive this after merge plus a firstmate self-update;
landing timing is coordinated with the main firstmate.
* no-mistakes(review): Harden bearings board validation and wake handling
* no-mistakes(review): Require HTTPS for bearings board PR links
* no-mistakes(review): Fail closed and bound bearings board answers
* no-mistakes(review): Enforce UTF-8 byte limits for board answers
* no-mistakes(review): Serve bearings board before arming and reject empty actions
* no-mistakes(review): Prove bind-before-arm ordering through live answer consumption
* no-mistakes(document): Document bearings board and cross-origin answers
* fix(bearings): restore decision options and add close controls (#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
* ci: require no-mistakes pipeline step attestation (#2710)
Signature-only PRs can hide skipped review, test, or document steps. Fail unless no-mistakes >= 1.46.0 attests those three steps completed.
* feat: collapse decisions into tasks held for the captain (#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
* fix: bound recovery announcements and preserve supervision (#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): surface captain-call record divergence (#2744)
* 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
* fix(bin): re-arm after an abandoned auto-arm claim and defer a wedge escalation while a worktree is written (#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 #2251's PR #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
* fix(bin): give a captain hold the same bounded pause cadence as a declared pause (#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
* fix(bin): make lint prerequisites and harness tests reliable (#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
* feat(bin): report watched tooling updates that are available or installed but inert (#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: decouple ask-user decisions from yolo (#2764)
* 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
* feat(bin): add a spoken interface that answers from records and hands work over (#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
* fix(bin): preserve Relay follow-up loops until explicit disposition (#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
* feat(bin): merge GitLab merge requests through the guarded PR merge path (#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
---------
Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
Co-authored-by: Mickaël Rémond <mremond@process-one.net>
Co-authored-by: Inthuson <iaminthuson@gmail.com>
Co-authored-by: Inthuson <inthuson@amazon.com>
Bre77
added a commit
to Bre77/firstmate
that referenced
this pull request
Aug 22, 2026
…s) (#70) * fix(ci): fail hung Herdr behavior runs in 20 minutes (#2413) A wedged family-run step was occupying the runner until the 75-minute job cap; bound that step so cleanup and timing artifacts still upload. * fix: keep the public promise reachable when work is routed to a second mate (#2457) The lightweight Relay follow-up link lives in the answering home's own state/<task-id>.meta, so it can only bind work that home owns. When a Relay-linked request is routed to a second mate, the task record lives in the second mate's home, fm-x-link.sh failed with a bare "no such task ...meta", and nothing else picked the promise up: only the soft acknowledgement was ever posted. The typed promised-final path already supports --work-home secondmate:<id>; the playbook simply never chose it. - fmx-respond now states the routing rule crisply: a task in this home takes the lightweight link, and second-mate-routed work takes a promised-final commitment bound to that home, registered up front with the brief command carried into the routed worker's instructions. - fm-x-link.sh refuses a task with no local record by naming the registered second mate whose home actually holds it and printing the promised-final registration command, with the exact --work-home when the match is unambiguous. A home with no registered second mates keeps the plain error. - fm-backlog-handoff.sh reports, after a successful move, any moved key that still owes a public reply bound to main/<key>, since that binding no longer names the home owning the work. The move itself is never blocked. Docs and the secondmate handoff prose follow the same rule. Tests cover the refusal, its scoping, the unchanged local-link path, and both handoff outcomes at the script boundary. * docs(skills): add remote-secondmate recovery hint for false-negative verdicts (#2456) * fix(skills): hint that remote secondmate liveness verdicts false-negative fm-crew-state and fm-send routinely misreport a live remote secondmate as dead; confirm against the pane before relaunching, and relaunch only through fm-spawn.sh, never raw herdr pane surgery. * no-mistakes: apply CI fixes * fix(calm): keep Pi's export confirmation visible (#2461) Pi 0.83.0 added a status line to every tool-expansion change, and Pi updates the previous status line in place when two status messages arrive back to back. Calm's post-export redraw cycled tool expansion on the macrotask right after Pi printed "Session exported to: <path>", so both expansion status lines coalesced over that confirmation and the captain was left with no record of where their export landed. Calm now repaints only the tool rows it presents, by invalidating each row through the render context Pi hands its render slots, and requests the surrounding redraw through setStatus. Neither appends to the transcript. The repaint is still needed because Pi can re-render a row asynchronously - the built-in edit row invalidates itself once its diff is ready - and that re-render can land inside the window where /export forces stock rendering. The real-terminal /export case now asserts the confirmation is still on screen after the redraw has settled, and that the redraw restored every Calm-hidden row, instead of only racing the moment the confirmation first appeared. * feat(stow): add open-record persistence to /stow before reset (#2488) * feat(stow): persist the open records a session is holding /stow curated memory and captured session knowledge, but never touched record state, while AGENTS.md called it an "unfinished-work sweep" and the receipt declared the session "safe to reset" - wording that implied a record-correctness guarantee stow does not make. A shipped PR with no backlog item, a queued umbrella whose phases had merged, and four decision holds left open after their answers shipped all survived repeated stows. Add a bounded pass that files record state from the same volatile input the rest of stow already uses: the open threads in context, minutes before the reset destroys them. It creates a record for an unfiled thread and corrects one the session knows is wrong, through the owning path, and states its boundary as part of the contract - it never enumerates the backlog, lists holds, or queries a forge, because it cannot be a reconciliation and must not be read as one. Correct the wording in AGENTS.md and the completion receipt so reset-safe means what it actually guarantees: nothing this session knew was lost. * no-mistakes(review): correct stow decision-hold inspection to read hold via tasks-axi * no-mistakes(document): note /stow open-record persistence in README command catalog * refactor(stow): state open-record persistence as principle, not procedure The first version enumerated triggers, named commands, and prescribed an ordered procedure. That is too rigid for an agent skill: it invites literal execution of a checklist instead of judgment, and every enumerated example is a way for the guidance to go stale. Reduce it to the intent - before a reset, the important open work you are holding in context must end up durably recorded rather than dying with the session, filing what is unfiled and correcting what is stale - and let the agent judge importance, the record, and the owning write path. Keep the scope bound, since it is a decided contract and not a mechanic: this covers the open work the session is holding, never a reconciliation of durable records against repository or forge reality. The wording corrections in AGENTS.md and the completion receipt are unchanged. * fix(decisions): close decision holds at answer time via one general keyed-answer path (#2490) * fix(decisions): close captain holds at answer time Firstmate had two "a decision is open" ledgers with asymmetric closing mechanics. The live status-log ledger closes atomically at answer time, because bin/fm-send.sh --resolve-key makes answering a decision be the act that closes it. The durable backlog hold ledger had no such coupling: answering and recording were two separate acts, and only the first was forced by the workflow. That asymmetry lost four real captain decisions. Their answers were captured durably to disk, keyed character for character by the hold decision keys, acknowledged, and even implemented and shipped, yet the holds stayed open for two days and the captain was asked to re-answer decisions already on his own disk. Give the hold ledger the same answer-time-closure property: - bin/fm-decision-hold.sh gains an `answer` subcommand, the hold ledger's counterpart to --resolve-key. It shares one unrouted close implementation with `decline`, so it carries every existing guard - the captain decision file, the active-hold requirement, retry identity, and the refusal to release still-routed work - and differs only in the resolution mode it records. `decline` keeps its stronger meaning that the answer routes no follow-up work at all. - bin/fm-procevent-lavish.sh wires the channel that actually carried the lost answers. `arm --decisions-origin` binds a deck to the origin whose holds it carries, `answers` reads the structured choices out of a captured poll result, `close-decisions` maps each key to its hold and closes it through the command above, and `autohandle` lets the runner apply that at capture time. Safety is preserved rather than traded away. Only rows tagged `choice` are read, so freeform captain prose cannot forge a decision key. Closure is confined to the one bound origin. The decision text is a pure function of the captured result, so a replayed capture is idempotent. A hold that is absent, already closed, or still blocking routed work is skipped and left for `resolve`, never forced. A deck armed without the binding touches no hold at all. And autohandle deliberately never reports full handling, because recording an answer is transcription while acting on it is firstmate's judgement - so the check wake still reaches the handler. fm-send --resolve-key is untouched. * no-mistakes(document): document state/lavish-decisions binding dir in AGENTS.md state inventory * refactor(decisions): make keyed-answer closure one general capability The previous pass gave holds answer-time closure but built it as bespoke Lavish wiring: the review adapter carried the source-to-origin binding, mapped keys to hold identities, wrote decision records, decided what to skip, and closed holds itself. That treated a review deck as a special decision source. It is not - it is an ephemeral discussion format that happens to carry answers. Collapse it into ONE general capability with one owner. bin/fm-decision-hold.sh now owns the whole of "a keyed answer closes its matching hold": - `answers <origin> --source <provenance>` is the channel-agnostic intake. It reads key/answer/label lines on stdin, maps each key to its hold, and closes it through the same `answer` path, so every guard applies identically whatever channel the answer came from. --source is provenance recorded in the decision, never a behavior switch; there is no per-channel branch and no knowledge of chat, decks, or transports. - `bind`/`unbind`/`binding` own the source-to-origin binding for any channel whose answers arrive detached from their origin. Every channel is now an ordinary caller that only turns what it received into keyed lines: - bin/fm-send.sh (chat) feeds the intake for a key that names an active hold. This also fixes a real gap: once `complete` transfers a decision to its hold it closes the live status copy, so --resolve-key alone could never answer a transferred decision. - bin/fm-procevent.sh feeds it generically. A bound source's captured result goes to `<adapter> answers <result-file>` and whatever that prints is piped into the intake. The runner names no adapter, parses no result, and carries no decision rule, so any future adapter with an `answers` command works with no change here. - bin/fm-procevent-lavish.sh keeps only `answers`, which reports the structured choices a review captured and stops. It maps nothing to a hold and closes nothing; it lost ~160 lines of decision logic. Feeding is independent of handling, so it never acknowledges a result and never suppresses a wake - recording an answer is transcription, acting on it stays firstmate's judgement. The regression that proves closure now drives a FIXTURE adapter that is not the review adapter, so what is proven is that any bound channel reaches the intake rather than that one channel is wired specially. A new regression drives the real fm-send over a stubbed transport for the chat side. Every prior guarantee still holds, and fm-send's status-log behavior is unchanged. * no-mistakes(review): test(decisions): drop source-content grep from hold-closure regression * fix(memory): emit a real @AGENTS.md pointer instead of a CLAUDE.md symlink (#2512) A Write aimed at CLAUDE.md followed the symlink and destroyed AGENTS.md. The installer now creates and migrates to a recoverable two-line pointer file. * fix(ci): keep CLAUDE.md pointer check valid (#2515) * ci: gate GitHub workflows with pinned actionlint (#2517) * fix(lint): catch malformed GitHub workflows before merge A self-broken ci.yml cannot report its own breakage, so parse every workflow in the local lint path that no-mistakes already runs. * fix(lint): pin actionlint instead of Ruby for workflow lint A self-broken ci.yml still has to fail in the local lint path, and the named tool for that gate is actionlint, not a new Ruby runtime. * no-mistakes(document): Clarify pinned workflow lint documentation * fix: install pinned lint tools across supported platforms (#2546) * fix: install pinned shellcheck and actionlint on macOS and linux arm64 The installers were hardcoded to linux amd64 and sha256sum, so a Mac dev could not satisfy the refuse-on-mismatch lint gate. Select the official per-platform archive and checksum, and fall back to shasum -a 256. * no-mistakes(document): Document cross-platform pinned lint installers * docs: reconcile test-evidence docs with store_in_repo: true (#2548) .no-mistakes.yaml has set test.evidence.store_in_repo: true since #2355, but CONTRIBUTING.md, docs/configuration.md, and docs/architecture.md still described the old policy of keeping evidence out of the repo in a temp directory. The current no-mistakes behavior for store_in_repo: true is to publish each run's test evidence to the orphan no-mistakes/evidence branch and link it from the PR body. That branch shares no history with code branches, so evidence never enters a pushed feature branch or the default branch, and CI's tracked personal fleet paths rule stays accurate. Docs only. No change to .no-mistakes.yaml or any workflow. * docs: clarify test evidence branch storage (#2549) * docs: correct test evidence storage comment in .no-mistakes.yaml * no-mistakes: apply CI fixes * docs: hint that live scouts may host their own Lavish review loop (#2563) Make that a first-class option in always-loaded instructions so firstmate does not default to mediating and tearing the scout down between iteration rounds. * fix(bin): report remote secondmate delivery and state truthfully (#2570) * fix(bin): report remote secondmate delivery and state truthfully A steer to a remote secondmate crosses fm-on.sh to a host-local fm-send leg whose unconfirmed submit read-back (verdict=pending, typically a busy mate whose harness queues the steer) was flattened into exit 1, so the parent printed "error: text not submitted" / "error: text not sent" and discarded the pending-reply expectation for a steer that had actually landed. fm-send now carries the verdict across the ssh boundary as a documented delivered-unconfirmed exit 3: the parent reports the steer as delivered with confirmation pending, exits 0, keeps the expectation armed (awaiting_report), and closes --resolve-key decisions, while transport loss (ssh 255) and real remote failures keep failing loudly with the remote leg's stderr attached. A local unconfirmed submit now also exits 3 with an honest non-error message and still never closes a decision key. fm-crew-state.sh and fm-peek.sh no longer read a remote mate's endpoint through local probes (which misreported a healthy mate as "worktree gone" / "can't find session: remote"): both now use the true remote source over fm-on.sh, and an unreachable or unreadable remote reads as unknown-remote, never as gone or dead. * no-mistakes(document): Document remote delivery and state truth * no-mistakes: apply CI fixes * feat: adopt spendPriority for quota dispatch (#2574) * Adopt quota-axi 0.1.29 spendPriority-primary array dispatch. quota-axi 0.1.29 publishes schema 5 with selection.spendPriority as the primary comparative signal and demotes derivation fields out of default --json. Rank comparable-fit candidates on that scalar, keep runway versus the completion horizon as a hard gate, and raise the compatibility floor so a pre-consolidation build cannot reach dispatch intake. * no-mistakes(review): Correct schema fixtures and remove prescriptive selection prompts * no-mistakes(document): Correct quota verification evidence chronology * Collapse quota-array-dispatch onto TOON-first spendPriority ranking. Decide from quota-axi's default TOON; keep --json as a rare defensive fallback. Rank by spendPriority after eligibility, reasoning-class, and runway-feasibility gates, and drop the hand-computed Pareto, pace, reserve, and window-id layers. * no-mistakes(review): Permit ambiguous JSON fallback and correct reset fixtures * no-mistakes(review): Correct runway semantics and escalate unresolved uncertainty * no-mistakes(document): Document TOON-first quota dispatch evidence * docs: add GROK_BOT.md Grok Bot system prompt (#2590) * docs: add GROK_BOT.md Grok Bot system prompt * docs: amend GROK_BOT.md with charter report-back and delegation marker * docs: classify GROK_BOT.md as public-product * docs: make GROK_BOT.md the plain Grok Bot system prompt * docs: update GROK_BOT.md nautical terms and self-improvement (#2592) * doc: Update language in GROK_BOT.md for clarity Refine language for clarity and consistency in instructions. * fix(bin): preserve inactive reconciliation scan progress (#2595) * fix(bin): guarantee inactive-reconcile scan progress under second quantization The inactive-outcome scan computed its aggregate deadline in whole seconds, so a 1-second budget's effective value lands anywhere in (0,1]; a scan starting just before a wall-clock second boundary rounded its whole budget away mid-scan and exited having visited no child, while the durable cursor had already advanced past the never-examined child. This is the CI flake behind tests/fm-inactive-reconcile.test.sh's 'next bounded scan did not resume with the following child' (watcher-wake-lock family, portable serial 2, seen on the PR #2590 run). Every scan now visits at least its first due child with the per-child state-read bound floored at one second, so no invocation can be a zero-work no-op. The outer process-group kill moves to budget+1s: the scan's own deadline enforces the budget, and the kill is a backstop for a scan wedged in an unbounded wait instead of a racer that routinely preempts the clean bounded exit. The wake-lock-wait test bound tracks the backstop (3s -> 4s); the previously flaky assertion is unchanged. * no-mistakes(document): Document inactive-reconcile deadline backstop * doc: Revise Firstmate delegation and communication guidelines Refactor the guidelines for Firstmate's role and delegation process, emphasizing the importance of crewmates and asynchronous work. * doc: Update work delegation and secret management instructions Clarified guidelines for handing off work to crewmates and managing secrets. * test(procevent): make the process-event suite's detached-runner assertions deterministic (#2617) Three assertions in tests/fm-procevent.test.sh depended on a detached runner having finished work that the command starting it does not wait for. reconcile's replacement runner is started through detach_runner, which only forks: reconcile returns and counts the start before that runner has claimed its source or exec'd its child. Any assertion taken straight after reconcile therefore samples a race. - The publish-before-apply recovery section left its always-ready /bin/echo source registered across the recovery reconcile, so that reconcile launched a competing detached poll (observed: started=1) that then raced every later assertion for the source claim, the next capture sequence, and this home's applied record, and outlived the section holding a live claim. It is now retired before that reconcile - re-announcement is proven from the durable inbox alone and needs no registration - and started=0 is asserted so a competing poll cannot be reintroduced unnoticed. This is the same retire-before-reconcile discipline the self-announcing section already carries; that section acquired it after the identical race made its "not-autohandled: self-src" assertion read "already owned: self-src". - The crashed-leader replacement section snapshotted the replacement's claim file and execution log behind a fixed 0.5s settle window. On a loaded machine that window expires first, which is the CI flake behind "a replacement runner started without recording its own claim" and "reconcile did not start exactly one replacement source". Both effects are now waited for with the suite's bounded wait helpers; the exact one-replacement count is still asserted afterwards, unchanged. - The duplicate-start section slept 0.5s for reconcile's runner to record ownership before asserting that a second start loses to it. It now waits for that claim. Also tighten one assertion that could not fail as written: "autohandled: self-src" is a substring of "not-autohandled: self-src", so the applied path was accepted even when the runner reported the capture left for the handler. Evidence: on the unmodified suite, 128 full runs at 6-8x concurrency produced 6 failing runs, all in the crashed-leader section. On the fixed suite, 216 full runs under the same load produced none. Reverting the self-announcing section's retire-before-reconcile line reproduces "already owned: self-src" on the first iteration, confirming the shared mechanism. * fix: preserve pending replies and defer remote reposts (#2618) * fix(bin): keep pending-reply expectations honest on both send legs Two related asymmetries let the parent-owned secondmate reply guard drop or nag requests it should not have. Local delivered-unconfirmed dropped the expectation. A marked request whose submit read-back stayed unconfirmed (verdict=pending) is the same not-a-failure outcome the remote leg reports as delivered, but fm-send discarded the parent's pending-reply record for it, so a request that very likely landed stopped being tracked entirely. The record now stays armed on its unconfirmed-delivery marker: a correlated report still resolves it, and an unanswered one still surfaces through the library's own reconciliation. Exit 3 and the local rule that an unconfirmed answer never closes a decision key are unchanged. Remote replies were nagged for a repost they did not need. A remote mate's report reaches the parent's status log only through the asynchronous mirror in fm-procevent-remote-reply.sh, yet the guard read an absent correlated line as proof the mate never reported - even while the answer was still in flight, which is the common case because the mirror's poll window is comparable to the recovery grace. The mirror now publishes one caught-up watermark from a quiet window, and the guard admits a missing report as evidence only once that watermark passes the turn that should have produced it. A genuinely missed report still gets exactly one repost, and a channel that is behind, unarmed, or broken leaves the request durably open and un-nagged rather than nagging blind; the mirror escalates its own continuity failures as before. Tests: a local unconfirmed secondmate send keeps its expectation armed and resolvable; a mirrored correlated remote reply resolves with no repost; a stale or absent watermark withholds the repost while a fresh one still releases it; a quiet remote window publishes the watermark and retirement clears it. * no-mistakes(review): Distinguish preempted polls from quiet windows * no-mistakes(document): Clarify remote reply channel freshness * no-mistakes(lint): Annotate shared remote preemption exit constant * fix(bin): honor declared pauses in busy-pane wedge checks (#2619) * fix(watch): honor a declared pause on a busy pane's completed-turn bound A worker that declares an external wait (`paused:`) and then blocks in one long foreground call - a review-hosting scout parked in a single blocking `lavish-axi poll`, a bounded watch loop, a rate-limit sleep - keeps its pane BUSY, so the stale path that already honors declared pauses never ran for it. The busy-pane completed-turn bound instead routed it straight into wedge_timer_check, which re-escalated "possible wedge, escalation N" (and, past the threshold, demand-deep-inspection) every FM_STALE_ESCALATE_SECS for as long as the review stayed open. busy_turn_bound_check now owns which absorber takes a crossed bound: a crew whose own last status line declares an external wait or a verified captain-held transfer takes the bounded FM_PAUSE_RESURFACE_SECS recheck, and everything else keeps the unchanged wedge timer. The discriminator is the declaration together with liveness (the caller has already confirmed the pane is busy), never a blanket silencing - a crew that declared nothing, or whose pane is not live, escalates exactly as before, and a declared pause still re-surfaces once per long cadence so a forgotten wait cannot rot invisibly. Away mode is untouched: the daemon owns pause triage there and already reads the same vocabulary. The two call sites also no longer clear pause bookkeeping in the same poll the pause cadence recorded it, which would have erased the re-surface throttle and turned the long cadence back into a per-poll re-surface. Tests: a three-phase regression fixture pins the absorbed pause, its long-cadence recheck, and the restored wedge escalation once the declaration is lifted on the same busy over-age pane. Also de-flakes tests/fm-watch-triage.test.sh, which failed spuriously on a loaded machine: fixed liveness budgets were reaping watchers mid-startup, so assertions on post-poll state passed vacuously or failed spuriously. Waits that describe a poll's outcome now wait for a completed poll cycle via the liveness beacon, the heartbeat test waits for the heartbeat it asserts on, and every wait_for_exit budget is the uniform 10s already used elsewhere in the file. * no-mistakes(review): Fail poll-cycle waits on timeout * no-mistakes(review): Prevent poll timeout test hangs * no-mistakes(document): Clarify paused busy-pane supervision * doc: Enhance communication guidelines for decision-making Added guidelines for decision communication to the captain. * doc: Update task delegation and communication guidelines Clarify communication protocols with crewmates regarding task delegation and reporting. * fix(bin): reliably confirm herdr steer submission (#2647) * fix(herdr): confirm local steers that native agent-state misses Herdr can leave agent_status idle for a landed Claude turn and can keep queued Enter text visible while busy, so fm-send was reporting false swallows. Confirm those cases through the shared queued-Enter verdict and a cleared composer, and keep a genuine idle pending composer as unconfirmed. * no-mistakes(review): Stop Herdr Enter retries on unreadable composers * no-mistakes(review): Reject queued delivery when all Herdr Enter sends fail * no-mistakes(review): Prevent confirmation after failed Herdr Enter * no-mistakes(review): Pace Herdr retries and clarify submit fallback * no-mistakes(review): Align Herdr submit docs with idle fallback * no-mistakes(document): Correct Herdr submit-confirmation documentation * feat(bearings): add interactive Lavish fleet board (#2659) * feat(bin): accept any-origin decision bindings with full-identity keys An aggregation surface (the bearings board) carries captain answers for holds across origins, but a binding was one-origin-per-source and the Lavish adapter capped question keys at 64 chars while real full hold identities measure 69-81. - fm-decision-hold.sh: bind <source-id> --any-origin records the (any) marker; binding prints it verbatim and answers accepts it, so the runner's feed seam carries an any-origin source with no runner change. In any-origin mode each key is a full hold identity <origin>-decision-<key>, split at its first -decision-; a key with no separator (merge/dispatch instructions) is skipped and feeds nothing, keeping non-decision answers out of the hold ledger by construction. Every existing close guard applies unchanged. - fm-procevent-lavish.sh: raise the question-key cap 64 -> 128 so a full hold identity fits; the slug-shape security property is unchanged. - tests: cross-origin closure through the real runner seam, an 81-char identity through the adapter, cap and shape refusals, routed-work skips, nonexistent-identity skips, and idempotent replay. * feat(bearings): add the /bearings lavish interactive fleet board /bearings lavish renders the bearings snapshot onto a shipped, reusable board template and arms it as a Lavish process-event source, so the captain answers Captain's Call items on the board and firstmate is woken by an ordinary check wake - no conversational turn ever blocks on a poll. - .agents/skills/bearings/assets/board-template.html: the shipped template (myfirstmate design system inlined, one fm-bearings-board.v1 JSON slot, fail-closed schema guard that renders an error card instead of an empty fleet). Per-invocation agent work is composing the payload only. - bin/fm-bearings-board.sh: build/refresh owner - fail-closed payload validation, slot injection with a round-trip check and \u003c escaping, stable board path, any-origin bind ALWAYS before arm, arm-if-absent. - bearings SKILL.md: the lavish invocation option, board composition rules, board-wake handling, and the captain-ruled merge-click authorization with its mandatory safeguards (PR resolved from the task's own meta record, wake-time green re-verification, never a red or changed PR, merges only through bin/fm-pr-merge.sh, chat echo with the full PR URL). - process-event-sources SKILL.md: one-line board-wake routing trigger. - tests: payload refusals, injection round-trip, bind-before-arm, idempotent re-arm, and template slot integrity. Fleet pickup: homes receive this after merge plus a firstmate self-update; landing timing is coordinated with the main firstmate. * no-mistakes(review): Harden bearings board validation and wake handling * no-mistakes(review): Require HTTPS for bearings board PR links * no-mistakes(review): Fail closed and bound bearings board answers * no-mistakes(review): Enforce UTF-8 byte limits for board answers * no-mistakes(review): Serve bearings board before arming and reject empty actions * no-mistakes(review): Prove bind-before-arm ordering through live answer consumption * no-mistakes(document): Document bearings board and cross-origin answers * fix(bearings): restore decision options and add close controls (#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 * ci: require no-mistakes pipeline step attestation (#2710) Signature-only PRs can hide skipped review, test, or document steps. Fail unless no-mistakes >= 1.46.0 attests those three steps completed. * feat: collapse decisions into tasks held for the captain (#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 * fix: bound recovery announcements and preserve supervision (#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): surface captain-call record divergence (#2744) * 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 * fix(bin): re-arm after an abandoned auto-arm claim and defer a wedge escalation while a worktree is written (#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 #2251's PR #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 * fix(bin): give a captain hold the same bounded pause cadence as a declared pause (#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 * fix(bin): make lint prerequisites and harness tests reliable (#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 * feat(bin): report watched tooling updates that are available or installed but inert (#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: decouple ask-user decisions from yolo (#2764) * 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 * feat(bin): add a spoken interface that answers from records and hands work over (#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 * fix(bin): preserve Relay follow-up loops until explicit disposition (#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 * feat(bin): merge GitLab merge requests through the guarded PR merge path (#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 * fix(bin): record a lost relay connection instead of an unanswered turn (#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 #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 7f98e797 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…
ironerumi
added a commit
to ironerumi/firstmate
that referenced
this pull request
Aug 24, 2026
* fix(bin): prevent false Pi watcher alarms during hand-offs (#2304)
* fix(guard): stop the false send-time watcher-down alarm on Pi primaries
On a Pi primary the watcher process is not the liveness signal. The Pi
extension tears the watcher down on every actionable wake and spawns the
replacement itself, so the singleton lock is legitimately unheld between
cycles: every one of the 799 cycles in a live primary's ledger ends with
lock_after=pid:none, and a live capture caught the guard verdict flipping to
no-watcher during one hand-off with the beacon 63s old.
bin/fm-guard.sh classified Pi as a persistent-watcher harness, which demands a
live identity-matched lock holder at all times, so any guarded command landing
in a hand-off painted the full WATCHER DOWN - SUPERVISION IS OFF banner and
told firstmate to repair a cycle the extension already owns and is restoring.
Add an extension supervision model for pi and pi-signed. A live
identity-matched watcher stays the ordinary healthy state; an unheld lock is
healthy only while the beacon is fresh within grace AND a live Pi session
provably owns continuity - both primary extensions recorded in their state
markers at their current on-disk builds by the process named in state/.lock,
with that process still alive. Without that proof the banner fires exactly as
before, so an unloaded, version-drifted, or exited Pi session is loud
immediately and a cycle the extension never restores is loud once the beacon
passes grace. The queued-wake warning, the PID-strict turn-end guard, and
every other primary's detection are untouched.
Fold session-start's duplicate Pi marker predicate into the shared library so
the ownership contract has one owner.
* no-mistakes(review): Restrict Pi hand-off tolerance to unheld watcher locks
* no-mistakes(document): Document Pi watcher hand-off supervision
* feat: support Cursor Agent CLI as a primary harness (#2305)
* feat(cursor): add Cursor Agent CLI primary hooks, park supervision, and session start
Register a tracked project-scope .cursor/hooks.json for Cursor's stop,
sessionStart, preCompact, and preToolUse steps.
bin/fm-turnend-guard-cursor.sh owns Cursor's turn boundary as a park: it
foregrounds the watcher arm, holds the boundary open until an actionable close,
and returns that wake as one follow-up. Exit 2 is a silent no-op on Cursor's
stop step, so the adapter never uses it. The follow-up loop is bounded twice,
by Cursor's own loop_limit and by the payload's loop_count.
bin/fm-sessionstart-cursor.sh delivers the digest as additional_context at
sessionStart, and stages it for the next turn boundary at preCompact, which
cannot inject context.
Cursor also loads the tracked Claude settings, so bin/fm-hook-host-lib.sh lets
each tracked Claude-shaped entrypoint stand down on a Cursor-delivered payload
rather than running every covered event twice.
bin/fm-tmux-lib.sh reclassifies a Cursor pane's composer cursorlessly, because
Cursor parks its terminal cursor outside the composer, which restores a genuine
composer-empty proof and unblocks away-mode escalation delivery.
* feat(cursor): make Cursor Agent CLI a verified primary harness
Resolve Cursor in the session-lock ancestry through bin/fm-cursor-lib.sh, which
a Cursor primary needs before it can hold its own home lock, and classify its
stop-hook park under the autoarm supervision model so the mid-turn pull guard
stops reporting a healthy between-turns watcher as down.
Read a Cursor pane's composer cursorlessly on tmux, gated on Cursor's own
structural process identity, which restores a genuine composer-empty proof and
lets away-mode escalations reach a Cursor primary with no daemon change.
Lift the secondmate refusals in bin/fm-spawn.sh and bin/fm-control-lib.sh now
that the supervision protocol exists and is recorded.
Cover the whole surface with a portable regression over real processes, an
opt-in live guard against the installed cursor-agent, and dated per-harness
evidence.
* docs(cursor): record Cursor as a verified primary across the owning surfaces
Update the turn-end guard, session-start, arm-seatbelt, cd-guard, watcher
continuity, architecture, configuration, README, and harness-adapters owners,
and add dated live evidence to the supervision and runtime-backend verification
records. Correct the recorded Cursor tmux composer verdict: the cursor-anchored
read is still blind, but the composite reader is no longer unknown.
Lift the remaining remote-secondmate refusal missed in the previous commit, and
add the new libs to the existing fixtures that copy a fixed dependency list.
* refactor(cursor): name the park's stand-down condition for both its causes
Also record that Cursor's preCompact firing itself is not yet live-verified,
while the static evidence that it cannot inject context, and the staging path
that follows from it, both are.
* test: give the pretool fixtures their new dependency and one lint owner
The cd-guard fixture copies a fixed dependency list and now needs the shared
hook-host predicate. Both pretool suites also asserted cleanliness with a bare
shellcheck call, a second and weaker copy of the lint definition that
bin/fm-lint.sh owns: it omits --external-sources, so it failed the moment these
checkers sourced a shared library. They now delegate to that owner.
* test: assert the cursor secondmate contract instead of its removed refusal
A cursor secondmate now launches, so the suite asserts what its park actually
needs: --trust so the home's project hooks load at all, its own home pinned as
the workspace, and the autoarm supervision model inherited across the launch.
* no-mistakes(review): Serialize Cursor wakes and bind staged context
* no-mistakes(review): Serialize Cursor context and nag state commits
* no-mistakes(review): Enforce Cursor ceiling before staged context delivery
* no-mistakes(review): Serialize Cursor claims and staged context
* no-mistakes(review): Serialize Cursor ownership and state commits
* no-mistakes(review): Protect Cursor context across session takeover
* no-mistakes(review): Preserve Cursor context across session takeover
* no-mistakes(review): Enforce owner-keyed Cursor staged context
* no-mistakes(review): Atomically claim Cursor follow-ups and staged context
* no-mistakes(review): Defer Cursor preCompact staging and simplify supersession
* no-mistakes(review): Serialize Cursor park commits and defer preCompact
* no-mistakes(review): Stop Cursor parks after session takeover
* no-mistakes(test): Route Cursor preCompact context through stop follow-up
* no-mistakes(document): Update Cursor primary documentation
* revert(cursor): cut preCompact staging from this change
Carrying a compaction digest across two concurrently running stop hooks kept
producing races that could deliver it twice or strand it indefinitely, and
closing them kept enlarging a critical section inside a hook Cursor awaits at
the turn boundary. Native preCompact firing was never observed either, so the
surface has no empirical basis yet.
Remove the adapter, its registration, its staged path in the park, and its
tests, and record the surface as deferred and uncovered alongside the Codex
interactive TUI. A regression now asserts preCompact stays unregistered so it
cannot return without its own design and evidence.
This change ships the proven core only: the turn-end follow-up park, the
run-tier session start, and away-mode delivery.
* no-mistakes(review): Correct Cursor park supersession documentation
* no-mistakes(document): Clarify Cursor run-tier verification ownership
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
---------
Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
* feat(bin): add decline and repair paths for decision holds (#2330)
* feat(bin): add unrouted close paths to the captain decision gate
A captain who declines a held decision leaves no follow-up work to route,
so `resolve` could not express that answer: it requires at least one
`--routed-to` task. The only way to close such a hold was a direct
`tasks-axi done`, which never writes the durable resolution record the
completion gate reads, so the originating investigation could no longer
pass `verify` and its cleanup stayed blocked.
Add two close paths that route no work:
- `decline` closes an actively held hold with a recorded captain decision
and no routed task. It refuses while any task is still blocked by the
hold, because releasing routed work without recording it is `resolve`'s
job.
- `repair` records the missing resolution block on a hold that was already
closed outside this script. It never reopens a hold and never clears a
dependency edge, and it refuses a hold that is still actively held.
Both require a non-empty captain decision file and share `resolve`'s
digest-based retry identity, so an exact retry is idempotent while a
changed decision is rejected. The recorded body now also names which path
closed the hold, and each routed entry regains its own line.
The gate itself is unchanged: an unanswered decision still fails
completion and blocks teardown, and neither new path can close a hold
without the captain's recorded word.
* fix(bin): require captain-hold provenance before repairing a decision
`repair` checked only that the backlog item was kind captain and Done, so
an ordinary captain-kind task that was never held for the captain could be
closed, repaired, and then pass the completion gate.
tasks-axi keeps `hold_kind` through a close, so it is the surviving proof
that an identity really was a captain hold. Require it before writing the
resolution record, and cover the case in the gate regression.
* no-mistakes(document): Correct decision-hold lifecycle documentation
* fix(bin): surface buried wake status lines once (#2331)
* fix(bin): surface buried status notes on wake drain
A note: answer immediately followed by a routine note was dropped because
annotations kept only the newest line and note: never enters OPEN DECISIONS.
Present every unread note and pending-reply resolution since the last drain
cursor, and annotate every unread line on a queued signal.
* no-mistakes(review): Fix unread status cursor races and overflow
* no-mistakes(review): Preserve cursors when status span reads fail
* no-mistakes(review): Make status presentation transactional under I/O failures
* no-mistakes(review): Simplify unread status cursor and presentation locking
* no-mistakes(review): Align cursor failure regressions with transactional presentation
* no-mistakes(review): Retire stale presentation cursors during task teardown
* no-mistakes(review): Preserve routine status until signal annotation
* no-mistakes(review): Correct unread status cap documentation
* no-mistakes(document): Document unread wake status presentation
* no-mistakes(lint): Fix wake surfacing ShellCheck warnings
* no-mistakes: apply CI fixes
* feat: add max Calm presentation level (#2334)
* feat(calm): add a max presentation level that hides mid-turn working notes
Calm's home-local preference becomes a three-state level instead of a
boolean: "off" is stock Pi, "on" is today's Calm, and "max" is Calm plus
hiding the assistant text of messages the model did not end its response
with. `/calm max` selects it from any state, a plain `/calm` steps max
back to ordinary Calm and otherwise keeps the existing on/off cycle, and
any other argument keeps that cycle too.
`config/calm` now persists "max" as its own literal value, so a session
start, resume, fork, or reload restores the stored level rather than
treating it as unrecognized and dropping to off.
The hide rule keys on Pi's intrinsic per-message stopReason: "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, because suppressing it would also stop a genuine reply from
streaming. The existing assistant layout adapter filters the blocks out
of the same shallow presentation copy it already uses for collapsed
thinking, so the message, model context, session storage, /export, and
delivery are untouched and a hidden mid-turn row collapses to zero
height. The new "assistant-working-note" class keeps that choice in the
visibility policy owner, where ordinary Calm keeps it visible.
* no-mistakes(document): Clarify Calm max persistence and taxonomy
* feat(calm): hide mid-turn working notes by default (#2339)
* feat(calm): make hiding mid-turn working notes the ordinary Calm state
Calm collapses back to the two-state on/off toggle it was before the max
presentation level, with max's hide rule promoted into ordinary Calm.
Calm on now hides mid-turn assistant working notes in addition to what it
already hid, and the /calm command parses no argument again.
The hide rule itself is unchanged: assistant text is removed from the
shallow presentation copy when the message's own stopReason is "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, so a genuine reply still streams. The message, model context,
session storage, /export, and delivery remain untouched.
config/calm persists only "on" and "off" again, but the reader still maps
a persisted "max" to on so a home upgraded from the removed level keeps
Calm on instead of dropping to off.
The mid-turn hide is now default behavior rather than an opt-in level, so
docs/calm.md documents it for users, docs/configuration.md records the
two written values plus the legacy max mapping, and the feasibility
taxonomy drops its level-scoped wording.
* no-mistakes(document): Document ordinary Calm working-note hiding
* chore: store no-mistakes test evidence in the repo (#2355)
* chore: ignore scratchpad/ at the repo root (#2359)
* fix(ci): fail hung Herdr behavior runs in 20 minutes (#2413)
A wedged family-run step was occupying the runner until the 75-minute
job cap; bound that step so cleanup and timing artifacts still upload.
* fix: keep the public promise reachable when work is routed to a second mate (#2457)
The lightweight Relay follow-up link lives in the answering home's own
state/<task-id>.meta, so it can only bind work that home owns. When a
Relay-linked request is routed to a second mate, the task record lives in the
second mate's home, fm-x-link.sh failed with a bare "no such task ...meta", and
nothing else picked the promise up: only the soft acknowledgement was ever
posted. The typed promised-final path already supports --work-home
secondmate:<id>; the playbook simply never chose it.
- fmx-respond now states the routing rule crisply: a task in this home takes the
lightweight link, and second-mate-routed work takes a promised-final
commitment bound to that home, registered up front with the brief command
carried into the routed worker's instructions.
- fm-x-link.sh refuses a task with no local record by naming the registered
second mate whose home actually holds it and printing the promised-final
registration command, with the exact --work-home when the match is
unambiguous. A home with no registered second mates keeps the plain error.
- fm-backlog-handoff.sh reports, after a successful move, any moved key that
still owes a public reply bound to main/<key>, since that binding no longer
names the home owning the work. The move itself is never blocked.
Docs and the secondmate handoff prose follow the same rule. Tests cover the
refusal, its scoping, the unchanged local-link path, and both handoff outcomes
at the script boundary.
* docs(skills): add remote-secondmate recovery hint for false-negative verdicts (#2456)
* fix(skills): hint that remote secondmate liveness verdicts false-negative
fm-crew-state and fm-send routinely misreport a live remote secondmate
as dead; confirm against the pane before relaunching, and relaunch only
through fm-spawn.sh, never raw herdr pane surgery.
* no-mistakes: apply CI fixes
* fix(calm): keep Pi's export confirmation visible (#2461)
Pi 0.83.0 added a status line to every tool-expansion change, and Pi
updates the previous status line in place when two status messages
arrive back to back. Calm's post-export redraw cycled tool expansion on
the macrotask right after Pi printed "Session exported to: <path>", so
both expansion status lines coalesced over that confirmation and the
captain was left with no record of where their export landed.
Calm now repaints only the tool rows it presents, by invalidating each
row through the render context Pi hands its render slots, and requests
the surrounding redraw through setStatus. Neither appends to the
transcript. The repaint is still needed because Pi can re-render a row
asynchronously - the built-in edit row invalidates itself once its diff
is ready - and that re-render can land inside the window where /export
forces stock rendering.
The real-terminal /export case now asserts the confirmation is still on
screen after the redraw has settled, and that the redraw restored every
Calm-hidden row, instead of only racing the moment the confirmation
first appeared.
* feat(stow): add open-record persistence to /stow before reset (#2488)
* feat(stow): persist the open records a session is holding
/stow curated memory and captured session knowledge, but never touched
record state, while AGENTS.md called it an "unfinished-work sweep" and the
receipt declared the session "safe to reset" - wording that implied a
record-correctness guarantee stow does not make. A shipped PR with no
backlog item, a queued umbrella whose phases had merged, and four decision
holds left open after their answers shipped all survived repeated stows.
Add a bounded pass that files record state from the same volatile input the
rest of stow already uses: the open threads in context, minutes before the
reset destroys them. It creates a record for an unfiled thread and corrects
one the session knows is wrong, through the owning path, and states its
boundary as part of the contract - it never enumerates the backlog, lists
holds, or queries a forge, because it cannot be a reconciliation and must
not be read as one.
Correct the wording in AGENTS.md and the completion receipt so reset-safe
means what it actually guarantees: nothing this session knew was lost.
* no-mistakes(review): correct stow decision-hold inspection to read hold via tasks-axi
* no-mistakes(document): note /stow open-record persistence in README command catalog
* refactor(stow): state open-record persistence as principle, not procedure
The first version enumerated triggers, named commands, and prescribed an
ordered procedure. That is too rigid for an agent skill: it invites literal
execution of a checklist instead of judgment, and every enumerated example
is a way for the guidance to go stale.
Reduce it to the intent - before a reset, the important open work you are
holding in context must end up durably recorded rather than dying with the
session, filing what is unfiled and correcting what is stale - and let the
agent judge importance, the record, and the owning write path.
Keep the scope bound, since it is a decided contract and not a mechanic:
this covers the open work the session is holding, never a reconciliation of
durable records against repository or forge reality. The wording
corrections in AGENTS.md and the completion receipt are unchanged.
* fix(decisions): close decision holds at answer time via one general keyed-answer path (#2490)
* fix(decisions): close captain holds at answer time
Firstmate had two "a decision is open" ledgers with asymmetric closing
mechanics. The live status-log ledger closes atomically at answer time,
because bin/fm-send.sh --resolve-key makes answering a decision be the
act that closes it. The durable backlog hold ledger had no such coupling:
answering and recording were two separate acts, and only the first was
forced by the workflow.
That asymmetry lost four real captain decisions. Their answers were
captured durably to disk, keyed character for character by the hold
decision keys, acknowledged, and even implemented and shipped, yet the
holds stayed open for two days and the captain was asked to re-answer
decisions already on his own disk.
Give the hold ledger the same answer-time-closure property:
- bin/fm-decision-hold.sh gains an `answer` subcommand, the hold ledger's
counterpart to --resolve-key. It shares one unrouted close
implementation with `decline`, so it carries every existing guard - the
captain decision file, the active-hold requirement, retry identity, and
the refusal to release still-routed work - and differs only in the
resolution mode it records. `decline` keeps its stronger meaning that
the answer routes no follow-up work at all.
- bin/fm-procevent-lavish.sh wires the channel that actually carried the
lost answers. `arm --decisions-origin` binds a deck to the origin whose
holds it carries, `answers` reads the structured choices out of a
captured poll result, `close-decisions` maps each key to its hold and
closes it through the command above, and `autohandle` lets the runner
apply that at capture time.
Safety is preserved rather than traded away. Only rows tagged `choice`
are read, so freeform captain prose cannot forge a decision key. Closure
is confined to the one bound origin. The decision text is a pure function
of the captured result, so a replayed capture is idempotent. A hold that
is absent, already closed, or still blocking routed work is skipped and
left for `resolve`, never forced. A deck armed without the binding
touches no hold at all. And autohandle deliberately never reports full
handling, because recording an answer is transcription while acting on it
is firstmate's judgement - so the check wake still reaches the handler.
fm-send --resolve-key is untouched.
* no-mistakes(document): document state/lavish-decisions binding dir in AGENTS.md state inventory
* refactor(decisions): make keyed-answer closure one general capability
The previous pass gave holds answer-time closure but built it as bespoke
Lavish wiring: the review adapter carried the source-to-origin binding,
mapped keys to hold identities, wrote decision records, decided what to
skip, and closed holds itself. That treated a review deck as a special
decision source. It is not - it is an ephemeral discussion format that
happens to carry answers.
Collapse it into ONE general capability with one owner.
bin/fm-decision-hold.sh now owns the whole of "a keyed answer closes its
matching hold":
- `answers <origin> --source <provenance>` is the channel-agnostic
intake. It reads key/answer/label lines on stdin, maps each key to its
hold, and closes it through the same `answer` path, so every guard
applies identically whatever channel the answer came from. --source is
provenance recorded in the decision, never a behavior switch; there is
no per-channel branch and no knowledge of chat, decks, or transports.
- `bind`/`unbind`/`binding` own the source-to-origin binding for any
channel whose answers arrive detached from their origin.
Every channel is now an ordinary caller that only turns what it received
into keyed lines:
- bin/fm-send.sh (chat) feeds the intake for a key that names an active
hold. This also fixes a real gap: once `complete` transfers a decision
to its hold it closes the live status copy, so --resolve-key alone
could never answer a transferred decision.
- bin/fm-procevent.sh feeds it generically. A bound source's captured
result goes to `<adapter> answers <result-file>` and whatever that
prints is piped into the intake. The runner names no adapter, parses
no result, and carries no decision rule, so any future adapter with an
`answers` command works with no change here.
- bin/fm-procevent-lavish.sh keeps only `answers`, which reports the
structured choices a review captured and stops. It maps nothing to a
hold and closes nothing; it lost ~160 lines of decision logic.
Feeding is independent of handling, so it never acknowledges a result
and never suppresses a wake - recording an answer is transcription,
acting on it stays firstmate's judgement.
The regression that proves closure now drives a FIXTURE adapter that is
not the review adapter, so what is proven is that any bound channel
reaches the intake rather than that one channel is wired specially. A
new regression drives the real fm-send over a stubbed transport for the
chat side. Every prior guarantee still holds, and fm-send's status-log
behavior is unchanged.
* no-mistakes(review): test(decisions): drop source-content grep from hold-closure regression
* fix(memory): emit a real @AGENTS.md pointer instead of a CLAUDE.md symlink (#2512)
A Write aimed at CLAUDE.md followed the symlink and destroyed AGENTS.md.
The installer now creates and migrates to a recoverable two-line pointer file.
* fix(ci): keep CLAUDE.md pointer check valid (#2515)
* ci: gate GitHub workflows with pinned actionlint (#2517)
* fix(lint): catch malformed GitHub workflows before merge
A self-broken ci.yml cannot report its own breakage, so parse every
workflow in the local lint path that no-mistakes already runs.
* fix(lint): pin actionlint instead of Ruby for workflow lint
A self-broken ci.yml still has to fail in the local lint path, and the
named tool for that gate is actionlint, not a new Ruby runtime.
* no-mistakes(document): Clarify pinned workflow lint documentation
* fix: install pinned lint tools across supported platforms (#2546)
* fix: install pinned shellcheck and actionlint on macOS and linux arm64
The installers were hardcoded to linux amd64 and sha256sum, so a Mac
dev could not satisfy the refuse-on-mismatch lint gate. Select the
official per-platform archive and checksum, and fall back to shasum -a 256.
* no-mistakes(document): Document cross-platform pinned lint installers
* docs: reconcile test-evidence docs with store_in_repo: true (#2548)
.no-mistakes.yaml has set test.evidence.store_in_repo: true since #2355, but
CONTRIBUTING.md, docs/configuration.md, and docs/architecture.md still described
the old policy of keeping evidence out of the repo in a temp directory.
The current no-mistakes behavior for store_in_repo: true is to publish each run's
test evidence to the orphan no-mistakes/evidence branch and link it from the PR
body. That branch shares no history with code branches, so evidence never enters
a pushed feature branch or the default branch, and CI's tracked personal fleet
paths rule stays accurate.
Docs only. No change to .no-mistakes.yaml or any workflow.
* docs: clarify test evidence branch storage (#2549)
* docs: correct test evidence storage comment in .no-mistakes.yaml
* no-mistakes: apply CI fixes
* docs: hint that live scouts may host their own Lavish review loop (#2563)
Make that a first-class option in always-loaded instructions so firstmate does not default to mediating and tearing the scout down between iteration rounds.
* fix(bin): report remote secondmate delivery and state truthfully (#2570)
* fix(bin): report remote secondmate delivery and state truthfully
A steer to a remote secondmate crosses fm-on.sh to a host-local fm-send
leg whose unconfirmed submit read-back (verdict=pending, typically a busy
mate whose harness queues the steer) was flattened into exit 1, so the
parent printed "error: text not submitted" / "error: text not sent" and
discarded the pending-reply expectation for a steer that had actually
landed. fm-send now carries the verdict across the ssh boundary as a
documented delivered-unconfirmed exit 3: the parent reports the steer as
delivered with confirmation pending, exits 0, keeps the expectation armed
(awaiting_report), and closes --resolve-key decisions, while transport
loss (ssh 255) and real remote failures keep failing loudly with the
remote leg's stderr attached. A local unconfirmed submit now also exits 3
with an honest non-error message and still never closes a decision key.
fm-crew-state.sh and fm-peek.sh no longer read a remote mate's endpoint
through local probes (which misreported a healthy mate as "worktree gone"
/ "can't find session: remote"): both now use the true remote source over
fm-on.sh, and an unreachable or unreadable remote reads as unknown-remote,
never as gone or dead.
* no-mistakes(document): Document remote delivery and state truth
* no-mistakes: apply CI fixes
* feat: adopt spendPriority for quota dispatch (#2574)
* Adopt quota-axi 0.1.29 spendPriority-primary array dispatch.
quota-axi 0.1.29 publishes schema 5 with selection.spendPriority as the primary comparative signal and demotes derivation fields out of default --json. Rank comparable-fit candidates on that scalar, keep runway versus the completion horizon as a hard gate, and raise the compatibility floor so a pre-consolidation build cannot reach dispatch intake.
* no-mistakes(review): Correct schema fixtures and remove prescriptive selection prompts
* no-mistakes(document): Correct quota verification evidence chronology
* Collapse quota-array-dispatch onto TOON-first spendPriority ranking.
Decide from quota-axi's default TOON; keep --json as a rare defensive fallback.
Rank by spendPriority after eligibility, reasoning-class, and runway-feasibility gates, and drop the hand-computed Pareto, pace, reserve, and window-id layers.
* no-mistakes(review): Permit ambiguous JSON fallback and correct reset fixtures
* no-mistakes(review): Correct runway semantics and escalate unresolved uncertainty
* no-mistakes(document): Document TOON-first quota dispatch evidence
* docs: add GROK_BOT.md Grok Bot system prompt (#2590)
* docs: add GROK_BOT.md Grok Bot system prompt
* docs: amend GROK_BOT.md with charter report-back and delegation marker
* docs: classify GROK_BOT.md as public-product
* docs: make GROK_BOT.md the plain Grok Bot system prompt
* docs: update GROK_BOT.md nautical terms and self-improvement (#2592)
* doc: Update language in GROK_BOT.md for clarity
Refine language for clarity and consistency in instructions.
* fix(bin): preserve inactive reconciliation scan progress (#2595)
* fix(bin): guarantee inactive-reconcile scan progress under second quantization
The inactive-outcome scan computed its aggregate deadline in whole seconds,
so a 1-second budget's effective value lands anywhere in (0,1]; a scan
starting just before a wall-clock second boundary rounded its whole budget
away mid-scan and exited having visited no child, while the durable cursor
had already advanced past the never-examined child. This is the CI flake
behind tests/fm-inactive-reconcile.test.sh's 'next bounded scan did not
resume with the following child' (watcher-wake-lock family, portable
serial 2, seen on the PR #2590 run).
Every scan now visits at least its first due child with the per-child
state-read bound floored at one second, so no invocation can be a zero-work
no-op. The outer process-group kill moves to budget+1s: the scan's own
deadline enforces the budget, and the kill is a backstop for a scan wedged
in an unbounded wait instead of a racer that routinely preempts the clean
bounded exit. The wake-lock-wait test bound tracks the backstop (3s -> 4s);
the previously flaky assertion is unchanged.
* no-mistakes(document): Document inactive-reconcile deadline backstop
* doc: Revise Firstmate delegation and communication guidelines
Refactor the guidelines for Firstmate's role and delegation process, emphasizing the importance of crewmates and asynchronous work.
* doc: Update work delegation and secret management instructions
Clarified guidelines for handing off work to crewmates and managing secrets.
* test(procevent): make the process-event suite's detached-runner assertions deterministic (#2617)
Three assertions in tests/fm-procevent.test.sh depended on a detached runner
having finished work that the command starting it does not wait for.
reconcile's replacement runner is started through detach_runner, which only
forks: reconcile returns and counts the start before that runner has claimed
its source or exec'd its child. Any assertion taken straight after reconcile
therefore samples a race.
- The publish-before-apply recovery section left its always-ready /bin/echo
source registered across the recovery reconcile, so that reconcile launched
a competing detached poll (observed: started=1) that then raced every later
assertion for the source claim, the next capture sequence, and this home's
applied record, and outlived the section holding a live claim. It is now
retired before that reconcile - re-announcement is proven from the durable
inbox alone and needs no registration - and started=0 is asserted so a
competing poll cannot be reintroduced unnoticed. This is the same
retire-before-reconcile discipline the self-announcing section already
carries; that section acquired it after the identical race made its
"not-autohandled: self-src" assertion read "already owned: self-src".
- The crashed-leader replacement section snapshotted the replacement's claim
file and execution log behind a fixed 0.5s settle window. On a loaded
machine that window expires first, which is the CI flake behind "a
replacement runner started without recording its own claim" and "reconcile
did not start exactly one replacement source". Both effects are now waited
for with the suite's bounded wait helpers; the exact one-replacement count
is still asserted afterwards, unchanged.
- The duplicate-start section slept 0.5s for reconcile's runner to record
ownership before asserting that a second start loses to it. It now waits
for that claim.
Also tighten one assertion that could not fail as written: "autohandled:
self-src" is a substring of "not-autohandled: self-src", so the applied path
was accepted even when the runner reported the capture left for the handler.
Evidence: on the unmodified suite, 128 full runs at 6-8x concurrency produced
6 failing runs, all in the crashed-leader section. On the fixed suite, 216
full runs under the same load produced none. Reverting the self-announcing
section's retire-before-reconcile line reproduces "already owned: self-src"
on the first iteration, confirming the shared mechanism.
* fix: preserve pending replies and defer remote reposts (#2618)
* fix(bin): keep pending-reply expectations honest on both send legs
Two related asymmetries let the parent-owned secondmate reply guard drop or
nag requests it should not have.
Local delivered-unconfirmed dropped the expectation. A marked request whose
submit read-back stayed unconfirmed (verdict=pending) is the same
not-a-failure outcome the remote leg reports as delivered, but fm-send
discarded the parent's pending-reply record for it, so a request that very
likely landed stopped being tracked entirely. The record now stays armed on
its unconfirmed-delivery marker: a correlated report still resolves it, and
an unanswered one still surfaces through the library's own reconciliation.
Exit 3 and the local rule that an unconfirmed answer never closes a decision
key are unchanged.
Remote replies were nagged for a repost they did not need. A remote mate's
report reaches the parent's status log only through the asynchronous mirror
in fm-procevent-remote-reply.sh, yet the guard read an absent correlated
line as proof the mate never reported - even while the answer was still in
flight, which is the common case because the mirror's poll window is
comparable to the recovery grace. The mirror now publishes one caught-up
watermark from a quiet window, and the guard admits a missing report as
evidence only once that watermark passes the turn that should have produced
it. A genuinely missed report still gets exactly one repost, and a channel
that is behind, unarmed, or broken leaves the request durably open and
un-nagged rather than nagging blind; the mirror escalates its own continuity
failures as before.
Tests: a local unconfirmed secondmate send keeps its expectation armed and
resolvable; a mirrored correlated remote reply resolves with no repost; a
stale or absent watermark withholds the repost while a fresh one still
releases it; a quiet remote window publishes the watermark and retirement
clears it.
* no-mistakes(review): Distinguish preempted polls from quiet windows
* no-mistakes(document): Clarify remote reply channel freshness
* no-mistakes(lint): Annotate shared remote preemption exit constant
* fix(bin): honor declared pauses in busy-pane wedge checks (#2619)
* fix(watch): honor a declared pause on a busy pane's completed-turn bound
A worker that declares an external wait (`paused:`) and then blocks in one
long foreground call - a review-hosting scout parked in a single blocking
`lavish-axi poll`, a bounded watch loop, a rate-limit sleep - keeps its pane
BUSY, so the stale path that already honors declared pauses never ran for it.
The busy-pane completed-turn bound instead routed it straight into
wedge_timer_check, which re-escalated "possible wedge, escalation N" (and, past
the threshold, demand-deep-inspection) every FM_STALE_ESCALATE_SECS for as long
as the review stayed open.
busy_turn_bound_check now owns which absorber takes a crossed bound: a crew
whose own last status line declares an external wait or a verified captain-held
transfer takes the bounded FM_PAUSE_RESURFACE_SECS recheck, and everything else
keeps the unchanged wedge timer. The discriminator is the declaration together
with liveness (the caller has already confirmed the pane is busy), never a
blanket silencing - a crew that declared nothing, or whose pane is not live,
escalates exactly as before, and a declared pause still re-surfaces once per
long cadence so a forgotten wait cannot rot invisibly. Away mode is untouched:
the daemon owns pause triage there and already reads the same vocabulary.
The two call sites also no longer clear pause bookkeeping in the same poll the
pause cadence recorded it, which would have erased the re-surface throttle and
turned the long cadence back into a per-poll re-surface.
Tests: a three-phase regression fixture pins the absorbed pause, its long-cadence
recheck, and the restored wedge escalation once the declaration is lifted on the
same busy over-age pane.
Also de-flakes tests/fm-watch-triage.test.sh, which failed spuriously on a loaded
machine: fixed liveness budgets were reaping watchers mid-startup, so assertions
on post-poll state passed vacuously or failed spuriously. Waits that describe a
poll's outcome now wait for a completed poll cycle via the liveness beacon, the
heartbeat test waits for the heartbeat it asserts on, and every wait_for_exit
budget is the uniform 10s already used elsewhere in the file.
* no-mistakes(review): Fail poll-cycle waits on timeout
* no-mistakes(review): Prevent poll timeout test hangs
* no-mistakes(document): Clarify paused busy-pane supervision
* doc: Enhance communication guidelines for decision-making
Added guidelines for decision communication to the captain.
* doc: Update task delegation and communication guidelines
Clarify communication protocols with crewmates regarding task delegation and reporting.
* fix(bin): reliably confirm herdr steer submission (#2647)
* fix(herdr): confirm local steers that native agent-state misses
Herdr can leave agent_status idle for a landed Claude turn and can keep
queued Enter text visible while busy, so fm-send was reporting false
swallows. Confirm those cases through the shared queued-Enter verdict
and a cleared composer, and keep a genuine idle pending composer as
unconfirmed.
* no-mistakes(review): Stop Herdr Enter retries on unreadable composers
* no-mistakes(review): Reject queued delivery when all Herdr Enter sends fail
* no-mistakes(review): Prevent confirmation after failed Herdr Enter
* no-mistakes(review): Pace Herdr retries and clarify submit fallback
* no-mistakes(review): Align Herdr submit docs with idle fallback
* no-mistakes(document): Correct Herdr submit-confirmation documentation
* feat(bearings): add interactive Lavish fleet board (#2659)
* feat(bin): accept any-origin decision bindings with full-identity keys
An aggregation surface (the bearings board) carries captain answers for holds
across origins, but a binding was one-origin-per-source and the Lavish adapter
capped question keys at 64 chars while real full hold identities measure 69-81.
- fm-decision-hold.sh: bind <source-id> --any-origin records the (any) marker;
binding prints it verbatim and answers accepts it, so the runner's feed seam
carries an any-origin source with no runner change. In any-origin mode each
key is a full hold identity <origin>-decision-<key>, split at its first
-decision-; a key with no separator (merge/dispatch instructions) is skipped
and feeds nothing, keeping non-decision answers out of the hold ledger by
construction. Every existing close guard applies unchanged.
- fm-procevent-lavish.sh: raise the question-key cap 64 -> 128 so a full hold
identity fits; the slug-shape security property is unchanged.
- tests: cross-origin closure through the real runner seam, an 81-char
identity through the adapter, cap and shape refusals, routed-work skips,
nonexistent-identity skips, and idempotent replay.
* feat(bearings): add the /bearings lavish interactive fleet board
/bearings lavish renders the bearings snapshot onto a shipped, reusable board
template and arms it as a Lavish process-event source, so the captain answers
Captain's Call items on the board and firstmate is woken by an ordinary check
wake - no conversational turn ever blocks on a poll.
- .agents/skills/bearings/assets/board-template.html: the shipped template
(myfirstmate design system inlined, one fm-bearings-board.v1 JSON slot,
fail-closed schema guard that renders an error card instead of an empty
fleet). Per-invocation agent work is composing the payload only.
- bin/fm-bearings-board.sh: build/refresh owner - fail-closed payload
validation, slot injection with a round-trip check and \u003c escaping,
stable board path, any-origin bind ALWAYS before arm, arm-if-absent.
- bearings SKILL.md: the lavish invocation option, board composition rules,
board-wake handling, and the captain-ruled merge-click authorization with
its mandatory safeguards (PR resolved from the task's own meta record,
wake-time green re-verification, never a red or changed PR, merges only
through bin/fm-pr-merge.sh, chat echo with the full PR URL).
- process-event-sources SKILL.md: one-line board-wake routing trigger.
- tests: payload refusals, injection round-trip, bind-before-arm, idempotent
re-arm, and template slot integrity.
Fleet pickup: homes receive this after merge plus a firstmate self-update;
landing timing is coordinated with the main firstmate.
* no-mistakes(review): Harden bearings board validation and wake handling
* no-mistakes(review): Require HTTPS for bearings board PR links
* no-mistakes(review): Fail closed and bound bearings board answers
* no-mistakes(review): Enforce UTF-8 byte limits for board answers
* no-mistakes(review): Serve bearings board before arming and reject empty actions
* no-mistakes(review): Prove bind-before-arm ordering through live answer consumption
* no-mistakes(document): Document bearings board and cross-origin answers
* fix(bearings): restore decision options and add close controls (#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
* ci: require no-mistakes pipeline step attestation (#2710)
Signature-only PRs can hide skipped review, test, or document steps. Fail unless no-mistakes >= 1.46.0 attests those three steps completed.
* feat: collapse decisions into tasks held for the captain (#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
* fix: bound recovery announcements and preserve supervision (#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): surface captain-call record divergence (#2744)
* 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
* fix(bin): re-arm after an abandoned auto-arm claim and defer a wedge escalation while a worktree is written (#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 #2251's PR #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
* fix(bin): give a captain hold the same bounded pause cadence as a declared pause (#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
* fix(bin): make lint prerequisites and harness tests reliable (#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
* feat(bin): report watched tooling updates that are available or installed but inert (#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: decouple ask-user decisions from yolo (#2764)
* 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
* feat(b…
prandelicious
added a commit
to prandelicious/firstmate
that referenced
this pull request
Aug 26, 2026
* ci: gate GitHub workflows with pinned actionlint (#2517)
* fix(lint): catch malformed GitHub workflows before merge
A self-broken ci.yml cannot report its own breakage, so parse every
workflow in the local lint path that no-mistakes already runs.
* fix(lint): pin actionlint instead of Ruby for workflow lint
A self-broken ci.yml still has to fail in the local lint path, and the
named tool for that gate is actionlint, not a new Ruby runtime.
* no-mistakes(document): Clarify pinned workflow lint documentation
* fix: install pinned lint tools across supported platforms (#2546)
* fix: install pinned shellcheck and actionlint on macOS and linux arm64
The installers were hardcoded to linux amd64 and sha256sum, so a Mac
dev could not satisfy the refuse-on-mismatch lint gate. Select the
official per-platform archive and checksum, and fall back to shasum -a 256.
* no-mistakes(document): Document cross-platform pinned lint installers
* docs: reconcile test-evidence docs with store_in_repo: true (#2548)
.no-mistakes.yaml has set test.evidence.store_in_repo: true since #2355, but
CONTRIBUTING.md, docs/configuration.md, and docs/architecture.md still described
the old policy of keeping evidence out of the repo in a temp directory.
The current no-mistakes behavior for store_in_repo: true is to publish each run's
test evidence to the orphan no-mistakes/evidence branch and link it from the PR
body. That branch shares no history with code branches, so evidence never enters
a pushed feature branch or the default branch, and CI's tracked personal fleet
paths rule stays accurate.
Docs only. No change to .no-mistakes.yaml or any workflow.
* docs: clarify test evidence branch storage (#2549)
* docs: correct test evidence storage comment in .no-mistakes.yaml
* no-mistakes: apply CI fixes
* docs: hint that live scouts may host their own Lavish review loop (#2563)
Make that a first-class option in always-loaded instructions so firstmate does not default to mediating and tearing the scout down between iteration rounds.
* fix(bin): report remote secondmate delivery and state truthfully (#2570)
* fix(bin): report remote secondmate delivery and state truthfully
A steer to a remote secondmate crosses fm-on.sh to a host-local fm-send
leg whose unconfirmed submit read-back (verdict=pending, typically a busy
mate whose harness queues the steer) was flattened into exit 1, so the
parent printed "error: text not submitted" / "error: text not sent" and
discarded the pending-reply expectation for a steer that had actually
landed. fm-send now carries the verdict across the ssh boundary as a
documented delivered-unconfirmed exit 3: the parent reports the steer as
delivered with confirmation pending, exits 0, keeps the expectation armed
(awaiting_report), and closes --resolve-key decisions, while transport
loss (ssh 255) and real remote failures keep failing loudly with the
remote leg's stderr attached. A local unconfirmed submit now also exits 3
with an honest non-error message and still never closes a decision key.
fm-crew-state.sh and fm-peek.sh no longer read a remote mate's endpoint
through local probes (which misreported a healthy mate as "worktree gone"
/ "can't find session: remote"): both now use the true remote source over
fm-on.sh, and an unreachable or unreadable remote reads as unknown-remote,
never as gone or dead.
* no-mistakes(document): Document remote delivery and state truth
* no-mistakes: apply CI fixes
* feat: adopt spendPriority for quota dispatch (#2574)
* Adopt quota-axi 0.1.29 spendPriority-primary array dispatch.
quota-axi 0.1.29 publishes schema 5 with selection.spendPriority as the primary comparative signal and demotes derivation fields out of default --json. Rank comparable-fit candidates on that scalar, keep runway versus the completion horizon as a hard gate, and raise the compatibility floor so a pre-consolidation build cannot reach dispatch intake.
* no-mistakes(review): Correct schema fixtures and remove prescriptive selection prompts
* no-mistakes(document): Correct quota verification evidence chronology
* Collapse quota-array-dispatch onto TOON-first spendPriority ranking.
Decide from quota-axi's default TOON; keep --json as a rare defensive fallback.
Rank by spendPriority after eligibility, reasoning-class, and runway-feasibility gates, and drop the hand-computed Pareto, pace, reserve, and window-id layers.
* no-mistakes(review): Permit ambiguous JSON fallback and correct reset fixtures
* no-mistakes(review): Correct runway semantics and escalate unresolved uncertainty
* no-mistakes(document): Document TOON-first quota dispatch evidence
* docs: add GROK_BOT.md Grok Bot system prompt (#2590)
* docs: add GROK_BOT.md Grok Bot system prompt
* docs: amend GROK_BOT.md with charter report-back and delegation marker
* docs: classify GROK_BOT.md as public-product
* docs: make GROK_BOT.md the plain Grok Bot system prompt
* docs: update GROK_BOT.md nautical terms and self-improvement (#2592)
* doc: Update language in GROK_BOT.md for clarity
Refine language for clarity and consistency in instructions.
* fix(bin): preserve inactive reconciliation scan progress (#2595)
* fix(bin): guarantee inactive-reconcile scan progress under second quantization
The inactive-outcome scan computed its aggregate deadline in whole seconds,
so a 1-second budget's effective value lands anywhere in (0,1]; a scan
starting just before a wall-clock second boundary rounded its whole budget
away mid-scan and exited having visited no child, while the durable cursor
had already advanced past the never-examined child. This is the CI flake
behind tests/fm-inactive-reconcile.test.sh's 'next bounded scan did not
resume with the following child' (watcher-wake-lock family, portable
serial 2, seen on the PR #2590 run).
Every scan now visits at least its first due child with the per-child
state-read bound floored at one second, so no invocation can be a zero-work
no-op. The outer process-group kill moves to budget+1s: the scan's own
deadline enforces the budget, and the kill is a backstop for a scan wedged
in an unbounded wait instead of a racer that routinely preempts the clean
bounded exit. The wake-lock-wait test bound tracks the backstop (3s -> 4s);
the previously flaky assertion is unchanged.
* no-mistakes(document): Document inactive-reconcile deadline backstop
* doc: Revise Firstmate delegation and communication guidelines
Refactor the guidelines for Firstmate's role and delegation process, emphasizing the importance of crewmates and asynchronous work.
* doc: Update work delegation and secret management instructions
Clarified guidelines for handing off work to crewmates and managing secrets.
* test(procevent): make the process-event suite's detached-runner assertions deterministic (#2617)
Three assertions in tests/fm-procevent.test.sh depended on a detached runner
having finished work that the command starting it does not wait for.
reconcile's replacement runner is started through detach_runner, which only
forks: reconcile returns and counts the start before that runner has claimed
its source or exec'd its child. Any assertion taken straight after reconcile
therefore samples a race.
- The publish-before-apply recovery section left its always-ready /bin/echo
source registered across the recovery reconcile, so that reconcile launched
a competing detached poll (observed: started=1) that then raced every later
assertion for the source claim, the next capture sequence, and this home's
applied record, and outlived the section holding a live claim. It is now
retired before that reconcile - re-announcement is proven from the durable
inbox alone and needs no registration - and started=0 is asserted so a
competing poll cannot be reintroduced unnoticed. This is the same
retire-before-reconcile discipline the self-announcing section already
carries; that section acquired it after the identical race made its
"not-autohandled: self-src" assertion read "already owned: self-src".
- The crashed-leader replacement section snapshotted the replacement's claim
file and execution log behind a fixed 0.5s settle window. On a loaded
machine that window expires first, which is the CI flake behind "a
replacement runner started without recording its own claim" and "reconcile
did not start exactly one replacement source". Both effects are now waited
for with the suite's bounded wait helpers; the exact one-replacement count
is still asserted afterwards, unchanged.
- The duplicate-start section slept 0.5s for reconcile's runner to record
ownership before asserting that a second start loses to it. It now waits
for that claim.
Also tighten one assertion that could not fail as written: "autohandled:
self-src" is a substring of "not-autohandled: self-src", so the applied path
was accepted even when the runner reported the capture left for the handler.
Evidence: on the unmodified suite, 128 full runs at 6-8x concurrency produced
6 failing runs, all in the crashed-leader section. On the fixed suite, 216
full runs under the same load produced none. Reverting the self-announcing
section's retire-before-reconcile line reproduces "already owned: self-src"
on the first iteration, confirming the shared mechanism.
* fix: preserve pending replies and defer remote reposts (#2618)
* fix(bin): keep pending-reply expectations honest on both send legs
Two related asymmetries let the parent-owned secondmate reply guard drop or
nag requests it should not have.
Local delivered-unconfirmed dropped the expectation. A marked request whose
submit read-back stayed unconfirmed (verdict=pending) is the same
not-a-failure outcome the remote leg reports as delivered, but fm-send
discarded the parent's pending-reply record for it, so a request that very
likely landed stopped being tracked entirely. The record now stays armed on
its unconfirmed-delivery marker: a correlated report still resolves it, and
an unanswered one still surfaces through the library's own reconciliation.
Exit 3 and the local rule that an unconfirmed answer never closes a decision
key are unchanged.
Remote replies were nagged for a repost they did not need. A remote mate's
report reaches the parent's status log only through the asynchronous mirror
in fm-procevent-remote-reply.sh, yet the guard read an absent correlated
line as proof the mate never reported - even while the answer was still in
flight, which is the common case because the mirror's poll window is
comparable to the recovery grace. The mirror now publishes one caught-up
watermark from a quiet window, and the guard admits a missing report as
evidence only once that watermark passes the turn that should have produced
it. A genuinely missed report still gets exactly one repost, and a channel
that is behind, unarmed, or broken leaves the request durably open and
un-nagged rather than nagging blind; the mirror escalates its own continuity
failures as before.
Tests: a local unconfirmed secondmate send keeps its expectation armed and
resolvable; a mirrored correlated remote reply resolves with no repost; a
stale or absent watermark withholds the repost while a fresh one still
releases it; a quiet remote window publishes the watermark and retirement
clears it.
* no-mistakes(review): Distinguish preempted polls from quiet windows
* no-mistakes(document): Clarify remote reply channel freshness
* no-mistakes(lint): Annotate shared remote preemption exit constant
* fix(bin): honor declared pauses in busy-pane wedge checks (#2619)
* fix(watch): honor a declared pause on a busy pane's completed-turn bound
A worker that declares an external wait (`paused:`) and then blocks in one
long foreground call - a review-hosting scout parked in a single blocking
`lavish-axi poll`, a bounded watch loop, a rate-limit sleep - keeps its pane
BUSY, so the stale path that already honors declared pauses never ran for it.
The busy-pane completed-turn bound instead routed it straight into
wedge_timer_check, which re-escalated "possible wedge, escalation N" (and, past
the threshold, demand-deep-inspection) every FM_STALE_ESCALATE_SECS for as long
as the review stayed open.
busy_turn_bound_check now owns which absorber takes a crossed bound: a crew
whose own last status line declares an external wait or a verified captain-held
transfer takes the bounded FM_PAUSE_RESURFACE_SECS recheck, and everything else
keeps the unchanged wedge timer. The discriminator is the declaration together
with liveness (the caller has already confirmed the pane is busy), never a
blanket silencing - a crew that declared nothing, or whose pane is not live,
escalates exactly as before, and a declared pause still re-surfaces once per
long cadence so a forgotten wait cannot rot invisibly. Away mode is untouched:
the daemon owns pause triage there and already reads the same vocabulary.
The two call sites also no longer clear pause bookkeeping in the same poll the
pause cadence recorded it, which would have erased the re-surface throttle and
turned the long cadence back into a per-poll re-surface.
Tests: a three-phase regression fixture pins the absorbed pause, its long-cadence
recheck, and the restored wedge escalation once the declaration is lifted on the
same busy over-age pane.
Also de-flakes tests/fm-watch-triage.test.sh, which failed spuriously on a loaded
machine: fixed liveness budgets were reaping watchers mid-startup, so assertions
on post-poll state passed vacuously or failed spuriously. Waits that describe a
poll's outcome now wait for a completed poll cycle via the liveness beacon, the
heartbeat test waits for the heartbeat it asserts on, and every wait_for_exit
budget is the uniform 10s already used elsewhere in the file.
* no-mistakes(review): Fail poll-cycle waits on timeout
* no-mistakes(review): Prevent poll timeout test hangs
* no-mistakes(document): Clarify paused busy-pane supervision
* doc: Enhance communication guidelines for decision-making
Added guidelines for decision communication to the captain.
* doc: Update task delegation and communication guidelines
Clarify communication protocols with crewmates regarding task delegation and reporting.
* fix(bin): reliably confirm herdr steer submission (#2647)
* fix(herdr): confirm local steers that native agent-state misses
Herdr can leave agent_status idle for a landed Claude turn and can keep
queued Enter text visible while busy, so fm-send was reporting false
swallows. Confirm those cases through the shared queued-Enter verdict
and a cleared composer, and keep a genuine idle pending composer as
unconfirmed.
* no-mistakes(review): Stop Herdr Enter retries on unreadable composers
* no-mistakes(review): Reject queued delivery when all Herdr Enter sends fail
* no-mistakes(review): Prevent confirmation after failed Herdr Enter
* no-mistakes(review): Pace Herdr retries and clarify submit fallback
* no-mistakes(review): Align Herdr submit docs with idle fallback
* no-mistakes(document): Correct Herdr submit-confirmation documentation
* feat(bearings): add interactive Lavish fleet board (#2659)
* feat(bin): accept any-origin decision bindings with full-identity keys
An aggregation surface (the bearings board) carries captain answers for holds
across origins, but a binding was one-origin-per-source and the Lavish adapter
capped question keys at 64 chars while real full hold identities measure 69-81.
- fm-decision-hold.sh: bind <source-id> --any-origin records the (any) marker;
binding prints it verbatim and answers accepts it, so the runner's feed seam
carries an any-origin source with no runner change. In any-origin mode each
key is a full hold identity <origin>-decision-<key>, split at its first
-decision-; a key with no separator (merge/dispatch instructions) is skipped
and feeds nothing, keeping non-decision answers out of the hold ledger by
construction. Every existing close guard applies unchanged.
- fm-procevent-lavish.sh: raise the question-key cap 64 -> 128 so a full hold
identity fits; the slug-shape security property is unchanged.
- tests: cross-origin closure through the real runner seam, an 81-char
identity through the adapter, cap and shape refusals, routed-work skips,
nonexistent-identity skips, and idempotent replay.
* feat(bearings): add the /bearings lavish interactive fleet board
/bearings lavish renders the bearings snapshot onto a shipped, reusable board
template and arms it as a Lavish process-event source, so the captain answers
Captain's Call items on the board and firstmate is woken by an ordinary check
wake - no conversational turn ever blocks on a poll.
- .agents/skills/bearings/assets/board-template.html: the shipped template
(myfirstmate design system inlined, one fm-bearings-board.v1 JSON slot,
fail-closed schema guard that renders an error card instead of an empty
fleet). Per-invocation agent work is composing the payload only.
- bin/fm-bearings-board.sh: build/refresh owner - fail-closed payload
validation, slot injection with a round-trip check and \u003c escaping,
stable board path, any-origin bind ALWAYS before arm, arm-if-absent.
- bearings SKILL.md: the lavish invocation option, board composition rules,
board-wake handling, and the captain-ruled merge-click authorization with
its mandatory safeguards (PR resolved from the task's own meta record,
wake-time green re-verification, never a red or changed PR, merges only
through bin/fm-pr-merge.sh, chat echo with the full PR URL).
- process-event-sources SKILL.md: one-line board-wake routing trigger.
- tests: payload refusals, injection round-trip, bind-before-arm, idempotent
re-arm, and template slot integrity.
Fleet pickup: homes receive this after merge plus a firstmate self-update;
landing timing is coordinated with the main firstmate.
* no-mistakes(review): Harden bearings board validation and wake handling
* no-mistakes(review): Require HTTPS for bearings board PR links
* no-mistakes(review): Fail closed and bound bearings board answers
* no-mistakes(review): Enforce UTF-8 byte limits for board answers
* no-mistakes(review): Serve bearings board before arming and reject empty actions
* no-mistakes(review): Prove bind-before-arm ordering through live answer consumption
* no-mistakes(document): Document bearings board and cross-origin answers
* fix(bearings): restore decision options and add close controls (#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
* ci: require no-mistakes pipeline step attestation (#2710)
Signature-only PRs can hide skipped review, test, or document steps. Fail unless no-mistakes >= 1.46.0 attests those three steps completed.
* feat: collapse decisions into tasks held for the captain (#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
* fix: bound recovery announcements and preserve supervision (#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): surface captain-call record divergence (#2744)
* 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
* fix(bin): re-arm after an abandoned auto-arm claim and defer a wedge escalation while a worktree is written (#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 #2251's PR #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
* fix(bin): give a captain hold the same bounded pause cadence as a declared pause (#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
* fix(bin): make lint prerequisites and harness tests reliable (#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
* feat(bin): report watched tooling updates that are available or installed but inert (#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: decouple ask-user decisions from yolo (#2764)
* 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
* feat(bin): add a spoken interface that answers from records and hands work over (#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
* fix(bin): preserve Relay follow-up loops until explicit disposition (#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
* feat(bin): merge GitLab merge requests through the guarded PR merge path (#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
* fix(bin): record a lost relay connection instead of an unanswered turn (#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 #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 7f98e797 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 #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
* fix(composer): stop a blocked pi pane from proving an empty composer (#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).
* fix(bin): require project clone roots during fleet sync (#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(bin): retry transient Lavish poll interruptions (#2846)
* 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
* fix(brief): stop the documented {TASK} fill from corrupting the Herdr gate (#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.
* fix(bin): resolve the busy-state lock mtime with the platform's own stat form (#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): add opt-in pass horizon for memory decay (#2850)
* 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
* test(watcher): stop fixture confirmation budgets racing real child startup (#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): deterministically order remote tool paths (#2870)
* 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…
zeeshaanahmad
added a commit
to zeeshaanahmad/firstmate
that referenced
this pull request
Aug 26, 2026
* feat: adopt spendPriority for quota dispatch (kunchenguid#2574) * Adopt quota-axi 0.1.29 spendPriority-primary array dispatch. quota-axi 0.1.29 publishes schema 5 with selection.spendPriority as the primary comparative signal and demotes derivation fields out of default --json. Rank comparable-fit candidates on that scalar, keep runway versus the completion horizon as a hard gate, and raise the compatibility floor so a pre-consolidation build cannot reach dispatch intake. * no-mistakes(review): Correct schema fixtures and remove prescriptive selection prompts * no-mistakes(document): Correct quota verification evidence chronology * Collapse quota-array-dispatch onto TOON-first spendPriority ranking. Decide from quota-axi's default TOON; keep --json as a rare defensive fallback. Rank by spendPriority after eligibility, reasoning-class, and runway-feasibility gates, and drop the hand-computed Pareto, pace, reserve, and window-id layers. * no-mistakes(review): Permit ambiguous JSON fallback and correct reset fixtures * no-mistakes(review): Correct runway semantics and escalate unresolved uncertainty * no-mistakes(document): Document TOON-first quota dispatch evidence * docs: add GROK_BOT.md Grok Bot system prompt (kunchenguid#2590) * docs: add GROK_BOT.md Grok Bot system prompt * docs: amend GROK_BOT.md with charter report-back and delegation marker * docs: classify GROK_BOT.md as public-product * docs: make GROK_BOT.md the plain Grok Bot system prompt * docs: update GROK_BOT.md nautical terms and self-improvement (kunchenguid#2592) * doc: Update language in GROK_BOT.md for clarity Refine language for clarity and consistency in instructions. * fix(bin): preserve inactive reconciliation scan progress (kunchenguid#2595) * fix(bin): guarantee inactive-reconcile scan progress under second quantization The inactive-outcome scan computed its aggregate deadline in whole seconds, so a 1-second budget's effective value lands anywhere in (0,1]; a scan starting just before a wall-clock second boundary rounded its whole budget away mid-scan and exited having visited no child, while the durable cursor had already advanced past the never-examined child. This is the CI flake behind tests/fm-inactive-reconcile.test.sh's 'next bounded scan did not resume with the following child' (watcher-wake-lock family, portable serial 2, seen on the PR kunchenguid#2590 run). Every scan now visits at least its first due child with the per-child state-read bound floored at one second, so no invocation can be a zero-work no-op. The outer process-group kill moves to budget+1s: the scan's own deadline enforces the budget, and the kill is a backstop for a scan wedged in an unbounded wait instead of a racer that routinely preempts the clean bounded exit. The wake-lock-wait test bound tracks the backstop (3s -> 4s); the previously flaky assertion is unchanged. * no-mistakes(document): Document inactive-reconcile deadline backstop * doc: Revise Firstmate delegation and communication guidelines Refactor the guidelines for Firstmate's role and delegation process, emphasizing the importance of crewmates and asynchronous work. * doc: Update work delegation and secret management instructions Clarified guidelines for handing off work to crewmates and managing secrets. * test(procevent): make the process-event suite's detached-runner assertions deterministic (kunchenguid#2617) Three assertions in tests/fm-procevent.test.sh depended on a detached runner having finished work that the command starting it does not wait for. reconcile's replacement runner is started through detach_runner, which only forks: reconcile returns and counts the start before that runner has claimed its source or exec'd its child. Any assertion taken straight after reconcile therefore samples a race. - The publish-before-apply recovery section left its always-ready /bin/echo source registered across the recovery reconcile, so that reconcile launched a competing detached poll (observed: started=1) that then raced every later assertion for the source claim, the next capture sequence, and this home's applied record, and outlived the section holding a live claim. It is now retired before that reconcile - re-announcement is proven from the durable inbox alone and needs no registration - and started=0 is asserted so a competing poll cannot be reintroduced unnoticed. This is the same retire-before-reconcile discipline the self-announcing section already carries; that section acquired it after the identical race made its "not-autohandled: self-src" assertion read "already owned: self-src". - The crashed-leader replacement section snapshotted the replacement's claim file and execution log behind a fixed 0.5s settle window. On a loaded machine that window expires first, which is the CI flake behind "a replacement runner started without recording its own claim" and "reconcile did not start exactly one replacement source". Both effects are now waited for with the suite's bounded wait helpers; the exact one-replacement count is still asserted afterwards, unchanged. - The duplicate-start section slept 0.5s for reconcile's runner to record ownership before asserting that a second start loses to it. It now waits for that claim. Also tighten one assertion that could not fail as written: "autohandled: self-src" is a substring of "not-autohandled: self-src", so the applied path was accepted even when the runner reported the capture left for the handler. Evidence: on the unmodified suite, 128 full runs at 6-8x concurrency produced 6 failing runs, all in the crashed-leader section. On the fixed suite, 216 full runs under the same load produced none. Reverting the self-announcing section's retire-before-reconcile line reproduces "already owned: self-src" on the first iteration, confirming the shared mechanism. * fix: preserve pending replies and defer remote reposts (kunchenguid#2618) * fix(bin): keep pending-reply expectations honest on both send legs Two related asymmetries let the parent-owned secondmate reply guard drop or nag requests it should not have. Local delivered-unconfirmed dropped the expectation. A marked request whose submit read-back stayed unconfirmed (verdict=pending) is the same not-a-failure outcome the remote leg reports as delivered, but fm-send discarded the parent's pending-reply record for it, so a request that very likely landed stopped being tracked entirely. The record now stays armed on its unconfirmed-delivery marker: a correlated report still resolves it, and an unanswered one still surfaces through the library's own reconciliation. Exit 3 and the local rule that an unconfirmed answer never closes a decision key are unchanged. Remote replies were nagged for a repost they did not need. A remote mate's report reaches the parent's status log only through the asynchronous mirror in fm-procevent-remote-reply.sh, yet the guard read an absent correlated line as proof the mate never reported - even while the answer was still in flight, which is the common case because the mirror's poll window is comparable to the recovery grace. The mirror now publishes one caught-up watermark from a quiet window, and the guard admits a missing report as evidence only once that watermark passes the turn that should have produced it. A genuinely missed report still gets exactly one repost, and a channel that is behind, unarmed, or broken leaves the request durably open and un-nagged rather than nagging blind; the mirror escalates its own continuity failures as before. Tests: a local unconfirmed secondmate send keeps its expectation armed and resolvable; a mirrored correlated remote reply resolves with no repost; a stale or absent watermark withholds the repost while a fresh one still releases it; a quiet remote window publishes the watermark and retirement clears it. * no-mistakes(review): Distinguish preempted polls from quiet windows * no-mistakes(document): Clarify remote reply channel freshness * no-mistakes(lint): Annotate shared remote preemption exit constant * no-mistakes(document): Fixed stale completion-aware wording and GROK_BOT.md sentence-per-line formatting * fix(docs): keep upstream-verbatim files unreformatted Restore GROK_BOT.md to byte-identical with upstream 7f5255a, and record why in the repo style rules. The document step reflowed GROK_BOT.md to one sentence per line, citing the style rule in firstmate-coding-guidelines. That reformat had already been considered and declined at the review gate: GROK_BOT.md is a new upstream file this fork does not author, and five of this batch's nine upstream commits touch it, so holding a reformatted copy would trade cosmetics for a conflict on every future sync of that exact file. The one-sentence-per-line rule exists to keep this repo's own diffs reviewable, and a file we carry verbatim is not our prose. The added style-rule line makes that exemption explicit so the same reformat is not proposed again on the remaining upstream batches. The document step's three other changes are kept: they correct genuinely stale "completion-aware" wording left behind by upstream's spendPriority rewrite. --------- Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
prandelicious
added a commit
to prandelicious/firstmate
that referenced
this pull request
Aug 26, 2026
* ci: gate GitHub workflows with pinned actionlint (#2517)
* fix(lint): catch malformed GitHub workflows before merge
A self-broken ci.yml cannot report its own breakage, so parse every
workflow in the local lint path that no-mistakes already runs.
* fix(lint): pin actionlint instead of Ruby for workflow lint
A self-broken ci.yml still has to fail in the local lint path, and the
named tool for that gate is actionlint, not a new Ruby runtime.
* no-mistakes(document): Clarify pinned workflow lint documentation
* fix: install pinned lint tools across supported platforms (#2546)
* fix: install pinned shellcheck and actionlint on macOS and linux arm64
The installers were hardcoded to linux amd64 and sha256sum, so a Mac
dev could not satisfy the refuse-on-mismatch lint gate. Select the
official per-platform archive and checksum, and fall back to shasum -a 256.
* no-mistakes(document): Document cross-platform pinned lint installers
* docs: reconcile test-evidence docs with store_in_repo: true (#2548)
.no-mistakes.yaml has set test.evidence.store_in_repo: true since #2355, but
CONTRIBUTING.md, docs/configuration.md, and docs/architecture.md still described
the old policy of keeping evidence out of the repo in a temp directory.
The current no-mistakes behavior for store_in_repo: true is to publish each run's
test evidence to the orphan no-mistakes/evidence branch and link it from the PR
body. That branch shares no history with code branches, so evidence never enters
a pushed feature branch or the default branch, and CI's tracked personal fleet
paths rule stays accurate.
Docs only. No change to .no-mistakes.yaml or any workflow.
* docs: clarify test evidence branch storage (#2549)
* docs: correct test evidence storage comment in .no-mistakes.yaml
* no-mistakes: apply CI fixes
* docs: hint that live scouts may host their own Lavish review loop (#2563)
Make that a first-class option in always-loaded instructions so firstmate does not default to mediating and tearing the scout down between iteration rounds.
* fix(bin): report remote secondmate delivery and state truthfully (#2570)
* fix(bin): report remote secondmate delivery and state truthfully
A steer to a remote secondmate crosses fm-on.sh to a host-local fm-send
leg whose unconfirmed submit read-back (verdict=pending, typically a busy
mate whose harness queues the steer) was flattened into exit 1, so the
parent printed "error: text not submitted" / "error: text not sent" and
discarded the pending-reply expectation for a steer that had actually
landed. fm-send now carries the verdict across the ssh boundary as a
documented delivered-unconfirmed exit 3: the parent reports the steer as
delivered with confirmation pending, exits 0, keeps the expectation armed
(awaiting_report), and closes --resolve-key decisions, while transport
loss (ssh 255) and real remote failures keep failing loudly with the
remote leg's stderr attached. A local unconfirmed submit now also exits 3
with an honest non-error message and still never closes a decision key.
fm-crew-state.sh and fm-peek.sh no longer read a remote mate's endpoint
through local probes (which misreported a healthy mate as "worktree gone"
/ "can't find session: remote"): both now use the true remote source over
fm-on.sh, and an unreachable or unreadable remote reads as unknown-remote,
never as gone or dead.
* no-mistakes(document): Document remote delivery and state truth
* no-mistakes: apply CI fixes
* feat: adopt spendPriority for quota dispatch (#2574)
* Adopt quota-axi 0.1.29 spendPriority-primary array dispatch.
quota-axi 0.1.29 publishes schema 5 with selection.spendPriority as the primary comparative signal and demotes derivation fields out of default --json. Rank comparable-fit candidates on that scalar, keep runway versus the completion horizon as a hard gate, and raise the compatibility floor so a pre-consolidation build cannot reach dispatch intake.
* no-mistakes(review): Correct schema fixtures and remove prescriptive selection prompts
* no-mistakes(document): Correct quota verification evidence chronology
* Collapse quota-array-dispatch onto TOON-first spendPriority ranking.
Decide from quota-axi's default TOON; keep --json as a rare defensive fallback.
Rank by spendPriority after eligibility, reasoning-class, and runway-feasibility gates, and drop the hand-computed Pareto, pace, reserve, and window-id layers.
* no-mistakes(review): Permit ambiguous JSON fallback and correct reset fixtures
* no-mistakes(review): Correct runway semantics and escalate unresolved uncertainty
* no-mistakes(document): Document TOON-first quota dispatch evidence
* docs: add GROK_BOT.md Grok Bot system prompt (#2590)
* docs: add GROK_BOT.md Grok Bot system prompt
* docs: amend GROK_BOT.md with charter report-back and delegation marker
* docs: classify GROK_BOT.md as public-product
* docs: make GROK_BOT.md the plain Grok Bot system prompt
* docs: update GROK_BOT.md nautical terms and self-improvement (#2592)
* doc: Update language in GROK_BOT.md for clarity
Refine language for clarity and consistency in instructions.
* fix(bin): preserve inactive reconciliation scan progress (#2595)
* fix(bin): guarantee inactive-reconcile scan progress under second quantization
The inactive-outcome scan computed its aggregate deadline in whole seconds,
so a 1-second budget's effective value lands anywhere in (0,1]; a scan
starting just before a wall-clock second boundary rounded its whole budget
away mid-scan and exited having visited no child, while the durable cursor
had already advanced past the never-examined child. This is the CI flake
behind tests/fm-inactive-reconcile.test.sh's 'next bounded scan did not
resume with the following child' (watcher-wake-lock family, portable
serial 2, seen on the PR #2590 run).
Every scan now visits at least its first due child with the per-child
state-read bound floored at one second, so no invocation can be a zero-work
no-op. The outer process-group kill moves to budget+1s: the scan's own
deadline enforces the budget, and the kill is a backstop for a scan wedged
in an unbounded wait instead of a racer that routinely preempts the clean
bounded exit. The wake-lock-wait test bound tracks the backstop (3s -> 4s);
the previously flaky assertion is unchanged.
* no-mistakes(document): Document inactive-reconcile deadline backstop
* doc: Revise Firstmate delegation and communication guidelines
Refactor the guidelines for Firstmate's role and delegation process, emphasizing the importance of crewmates and asynchronous work.
* doc: Update work delegation and secret management instructions
Clarified guidelines for handing off work to crewmates and managing secrets.
* test(procevent): make the process-event suite's detached-runner assertions deterministic (#2617)
Three assertions in tests/fm-procevent.test.sh depended on a detached runner
having finished work that the command starting it does not wait for.
reconcile's replacement runner is started through detach_runner, which only
forks: reconcile returns and counts the start before that runner has claimed
its source or exec'd its child. Any assertion taken straight after reconcile
therefore samples a race.
- The publish-before-apply recovery section left its always-ready /bin/echo
source registered across the recovery reconcile, so that reconcile launched
a competing detached poll (observed: started=1) that then raced every later
assertion for the source claim, the next capture sequence, and this home's
applied record, and outlived the section holding a live claim. It is now
retired before that reconcile - re-announcement is proven from the durable
inbox alone and needs no registration - and started=0 is asserted so a
competing poll cannot be reintroduced unnoticed. This is the same
retire-before-reconcile discipline the self-announcing section already
carries; that section acquired it after the identical race made its
"not-autohandled: self-src" assertion read "already owned: self-src".
- The crashed-leader replacement section snapshotted the replacement's claim
file and execution log behind a fixed 0.5s settle window. On a loaded
machine that window expires first, which is the CI flake behind "a
replacement runner started without recording its own claim" and "reconcile
did not start exactly one replacement source". Both effects are now waited
for with the suite's bounded wait helpers; the exact one-replacement count
is still asserted afterwards, unchanged.
- The duplicate-start section slept 0.5s for reconcile's runner to record
ownership before asserting that a second start loses to it. It now waits
for that claim.
Also tighten one assertion that could not fail as written: "autohandled:
self-src" is a substring of "not-autohandled: self-src", so the applied path
was accepted even when the runner reported the capture left for the handler.
Evidence: on the unmodified suite, 128 full runs at 6-8x concurrency produced
6 failing runs, all in the crashed-leader section. On the fixed suite, 216
full runs under the same load produced none. Reverting the self-announcing
section's retire-before-reconcile line reproduces "already owned: self-src"
on the first iteration, confirming the shared mechanism.
* fix: preserve pending replies and defer remote reposts (#2618)
* fix(bin): keep pending-reply expectations honest on both send legs
Two related asymmetries let the parent-owned secondmate reply guard drop or
nag requests it should not have.
Local delivered-unconfirmed dropped the expectation. A marked request whose
submit read-back stayed unconfirmed (verdict=pending) is the same
not-a-failure outcome the remote leg reports as delivered, but fm-send
discarded the parent's pending-reply record for it, so a request that very
likely landed stopped being tracked entirely. The record now stays armed on
its unconfirmed-delivery marker: a correlated report still resolves it, and
an unanswered one still surfaces through the library's own reconciliation.
Exit 3 and the local rule that an unconfirmed answer never closes a decision
key are unchanged.
Remote replies were nagged for a repost they did not need. A remote mate's
report reaches the parent's status log only through the asynchronous mirror
in fm-procevent-remote-reply.sh, yet the guard read an absent correlated
line as proof the mate never reported - even while the answer was still in
flight, which is the common case because the mirror's poll window is
comparable to the recovery grace. The mirror now publishes one caught-up
watermark from a quiet window, and the guard admits a missing report as
evidence only once that watermark passes the turn that should have produced
it. A genuinely missed report still gets exactly one repost, and a channel
that is behind, unarmed, or broken leaves the request durably open and
un-nagged rather than nagging blind; the mirror escalates its own continuity
failures as before.
Tests: a local unconfirmed secondmate send keeps its expectation armed and
resolvable; a mirrored correlated remote reply resolves with no repost; a
stale or absent watermark withholds the repost while a fresh one still
releases it; a quiet remote window publishes the watermark and retirement
clears it.
* no-mistakes(review): Distinguish preempted polls from quiet windows
* no-mistakes(document): Clarify remote reply channel freshness
* no-mistakes(lint): Annotate shared remote preemption exit constant
* fix(bin): honor declared pauses in busy-pane wedge checks (#2619)
* fix(watch): honor a declared pause on a busy pane's completed-turn bound
A worker that declares an external wait (`paused:`) and then blocks in one
long foreground call - a review-hosting scout parked in a single blocking
`lavish-axi poll`, a bounded watch loop, a rate-limit sleep - keeps its pane
BUSY, so the stale path that already honors declared pauses never ran for it.
The busy-pane completed-turn bound instead routed it straight into
wedge_timer_check, which re-escalated "possible wedge, escalation N" (and, past
the threshold, demand-deep-inspection) every FM_STALE_ESCALATE_SECS for as long
as the review stayed open.
busy_turn_bound_check now owns which absorber takes a crossed bound: a crew
whose own last status line declares an external wait or a verified captain-held
transfer takes the bounded FM_PAUSE_RESURFACE_SECS recheck, and everything else
keeps the unchanged wedge timer. The discriminator is the declaration together
with liveness (the caller has already confirmed the pane is busy), never a
blanket silencing - a crew that declared nothing, or whose pane is not live,
escalates exactly as before, and a declared pause still re-surfaces once per
long cadence so a forgotten wait cannot rot invisibly. Away mode is untouched:
the daemon owns pause triage there and already reads the same vocabulary.
The two call sites also no longer clear pause bookkeeping in the same poll the
pause cadence recorded it, which would have erased the re-surface throttle and
turned the long cadence back into a per-poll re-surface.
Tests: a three-phase regression fixture pins the absorbed pause, its long-cadence
recheck, and the restored wedge escalation once the declaration is lifted on the
same busy over-age pane.
Also de-flakes tests/fm-watch-triage.test.sh, which failed spuriously on a loaded
machine: fixed liveness budgets were reaping watchers mid-startup, so assertions
on post-poll state passed vacuously or failed spuriously. Waits that describe a
poll's outcome now wait for a completed poll cycle via the liveness beacon, the
heartbeat test waits for the heartbeat it asserts on, and every wait_for_exit
budget is the uniform 10s already used elsewhere in the file.
* no-mistakes(review): Fail poll-cycle waits on timeout
* no-mistakes(review): Prevent poll timeout test hangs
* no-mistakes(document): Clarify paused busy-pane supervision
* doc: Enhance communication guidelines for decision-making
Added guidelines for decision communication to the captain.
* doc: Update task delegation and communication guidelines
Clarify communication protocols with crewmates regarding task delegation and reporting.
* fix(bin): reliably confirm herdr steer submission (#2647)
* fix(herdr): confirm local steers that native agent-state misses
Herdr can leave agent_status idle for a landed Claude turn and can keep
queued Enter text visible while busy, so fm-send was reporting false
swallows. Confirm those cases through the shared queued-Enter verdict
and a cleared composer, and keep a genuine idle pending composer as
unconfirmed.
* no-mistakes(review): Stop Herdr Enter retries on unreadable composers
* no-mistakes(review): Reject queued delivery when all Herdr Enter sends fail
* no-mistakes(review): Prevent confirmation after failed Herdr Enter
* no-mistakes(review): Pace Herdr retries and clarify submit fallback
* no-mistakes(review): Align Herdr submit docs with idle fallback
* no-mistakes(document): Correct Herdr submit-confirmation documentation
* feat(bearings): add interactive Lavish fleet board (#2659)
* feat(bin): accept any-origin decision bindings with full-identity keys
An aggregation surface (the bearings board) carries captain answers for holds
across origins, but a binding was one-origin-per-source and the Lavish adapter
capped question keys at 64 chars while real full hold identities measure 69-81.
- fm-decision-hold.sh: bind <source-id> --any-origin records the (any) marker;
binding prints it verbatim and answers accepts it, so the runner's feed seam
carries an any-origin source with no runner change. In any-origin mode each
key is a full hold identity <origin>-decision-<key>, split at its first
-decision-; a key with no separator (merge/dispatch instructions) is skipped
and feeds nothing, keeping non-decision answers out of the hold ledger by
construction. Every existing close guard applies unchanged.
- fm-procevent-lavish.sh: raise the question-key cap 64 -> 128 so a full hold
identity fits; the slug-shape security property is unchanged.
- tests: cross-origin closure through the real runner seam, an 81-char
identity through the adapter, cap and shape refusals, routed-work skips,
nonexistent-identity skips, and idempotent replay.
* feat(bearings): add the /bearings lavish interactive fleet board
/bearings lavish renders the bearings snapshot onto a shipped, reusable board
template and arms it as a Lavish process-event source, so the captain answers
Captain's Call items on the board and firstmate is woken by an ordinary check
wake - no conversational turn ever blocks on a poll.
- .agents/skills/bearings/assets/board-template.html: the shipped template
(myfirstmate design system inlined, one fm-bearings-board.v1 JSON slot,
fail-closed schema guard that renders an error card instead of an empty
fleet). Per-invocation agent work is composing the payload only.
- bin/fm-bearings-board.sh: build/refresh owner - fail-closed payload
validation, slot injection with a round-trip check and \u003c escaping,
stable board path, any-origin bind ALWAYS before arm, arm-if-absent.
- bearings SKILL.md: the lavish invocation option, board composition rules,
board-wake handling, and the captain-ruled merge-click authorization with
its mandatory safeguards (PR resolved from the task's own meta record,
wake-time green re-verification, never a red or changed PR, merges only
through bin/fm-pr-merge.sh, chat echo with the full PR URL).
- process-event-sources SKILL.md: one-line board-wake routing trigger.
- tests: payload refusals, injection round-trip, bind-before-arm, idempotent
re-arm, and template slot integrity.
Fleet pickup: homes receive this after merge plus a firstmate self-update;
landing timing is coordinated with the main firstmate.
* no-mistakes(review): Harden bearings board validation and wake handling
* no-mistakes(review): Require HTTPS for bearings board PR links
* no-mistakes(review): Fail closed and bound bearings board answers
* no-mistakes(review): Enforce UTF-8 byte limits for board answers
* no-mistakes(review): Serve bearings board before arming and reject empty actions
* no-mistakes(review): Prove bind-before-arm ordering through live answer consumption
* no-mistakes(document): Document bearings board and cross-origin answers
* fix(bearings): restore decision options and add close controls (#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
* ci: require no-mistakes pipeline step attestation (#2710)
Signature-only PRs can hide skipped review, test, or document steps. Fail unless no-mistakes >= 1.46.0 attests those three steps completed.
* feat: collapse decisions into tasks held for the captain (#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
* fix: bound recovery announcements and preserve supervision (#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): surface captain-call record divergence (#2744)
* 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
* fix(bin): re-arm after an abandoned auto-arm claim and defer a wedge escalation while a worktree is written (#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 #2251's PR #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
* fix(bin): give a captain hold the same bounded pause cadence as a declared pause (#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
* fix(bin): make lint prerequisites and harness tests reliable (#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
* feat(bin): report watched tooling updates that are available or installed but inert (#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: decouple ask-user decisions from yolo (#2764)
* 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
* feat(bin): add a spoken interface that answers from records and hands work over (#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
* fix(bin): preserve Relay follow-up loops until explicit disposition (#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
* feat(bin): merge GitLab merge requests through the guarded PR merge path (#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
* fix(bin): record a lost relay connection instead of an unanswered turn (#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 #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 7f98e797 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 #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
* fix(composer): stop a blocked pi pane from proving an empty composer (#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).
* fix(bin): require project clone roots during fleet sync (#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(bin): retry transient Lavish poll interruptions (#2846)
* 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
* fix(brief): stop the documented {TASK} fill from corrupting the Herdr gate (#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.
* fix(bin): resolve the busy-state lock mtime with the platform's own stat form (#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): add opt-in pass horizon for memory decay (#2850)
* 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
* test(watcher): stop fixture confirmation budgets racing real child startup (#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): deterministically order remote tool paths (#2870)
* 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 porta…
Bre77
added a commit
to Bre77/firstmate
that referenced
this pull request
Sep 2, 2026
…ts) (#72) * fix(ci): fail hung Herdr behavior runs in 20 minutes (#2413) A wedged family-run step was occupying the runner until the 75-minute job cap; bound that step so cleanup and timing artifacts still upload. * fix: keep the public promise reachable when work is routed to a second mate (#2457) The lightweight Relay follow-up link lives in the answering home's own state/<task-id>.meta, so it can only bind work that home owns. When a Relay-linked request is routed to a second mate, the task record lives in the second mate's home, fm-x-link.sh failed with a bare "no such task ...meta", and nothing else picked the promise up: only the soft acknowledgement was ever posted. The typed promised-final path already supports --work-home secondmate:<id>; the playbook simply never chose it. - fmx-respond now states the routing rule crisply: a task in this home takes the lightweight link, and second-mate-routed work takes a promised-final commitment bound to that home, registered up front with the brief command carried into the routed worker's instructions. - fm-x-link.sh refuses a task with no local record by naming the registered second mate whose home actually holds it and printing the promised-final registration command, with the exact --work-home when the match is unambiguous. A home with no registered second mates keeps the plain error. - fm-backlog-handoff.sh reports, after a successful move, any moved key that still owes a public reply bound to main/<key>, since that binding no longer names the home owning the work. The move itself is never blocked. Docs and the secondmate handoff prose follow the same rule. Tests cover the refusal, its scoping, the unchanged local-link path, and both handoff outcomes at the script boundary. * docs(skills): add remote-secondmate recovery hint for false-negative verdicts (#2456) * fix(skills): hint that remote secondmate liveness verdicts false-negative fm-crew-state and fm-send routinely misreport a live remote secondmate as dead; confirm against the pane before relaunching, and relaunch only through fm-spawn.sh, never raw herdr pane surgery. * no-mistakes: apply CI fixes * fix(calm): keep Pi's export confirmation visible (#2461) Pi 0.83.0 added a status line to every tool-expansion change, and Pi updates the previous status line in place when two status messages arrive back to back. Calm's post-export redraw cycled tool expansion on the macrotask right after Pi printed "Session exported to: <path>", so both expansion status lines coalesced over that confirmation and the captain was left with no record of where their export landed. Calm now repaints only the tool rows it presents, by invalidating each row through the render context Pi hands its render slots, and requests the surrounding redraw through setStatus. Neither appends to the transcript. The repaint is still needed because Pi can re-render a row asynchronously - the built-in edit row invalidates itself once its diff is ready - and that re-render can land inside the window where /export forces stock rendering. The real-terminal /export case now asserts the confirmation is still on screen after the redraw has settled, and that the redraw restored every Calm-hidden row, instead of only racing the moment the confirmation first appeared. * feat(stow): add open-record persistence to /stow before reset (#2488) * feat(stow): persist the open records a session is holding /stow curated memory and captured session knowledge, but never touched record state, while AGENTS.md called it an "unfinished-work sweep" and the receipt declared the session "safe to reset" - wording that implied a record-correctness guarantee stow does not make. A shipped PR with no backlog item, a queued umbrella whose phases had merged, and four decision holds left open after their answers shipped all survived repeated stows. Add a bounded pass that files record state from the same volatile input the rest of stow already uses: the open threads in context, minutes before the reset destroys them. It creates a record for an unfiled thread and corrects one the session knows is wrong, through the owning path, and states its boundary as part of the contract - it never enumerates the backlog, lists holds, or queries a forge, because it cannot be a reconciliation and must not be read as one. Correct the wording in AGENTS.md and the completion receipt so reset-safe means what it actually guarantees: nothing this session knew was lost. * no-mistakes(review): correct stow decision-hold inspection to read hold via tasks-axi * no-mistakes(document): note /stow open-record persistence in README command catalog * refactor(stow): state open-record persistence as principle, not procedure The first version enumerated triggers, named commands, and prescribed an ordered procedure. That is too rigid for an agent skill: it invites literal execution of a checklist instead of judgment, and every enumerated example is a way for the guidance to go stale. Reduce it to the intent - before a reset, the important open work you are holding in context must end up durably recorded rather than dying with the session, filing what is unfiled and correcting what is stale - and let the agent judge importance, the record, and the owning write path. Keep the scope bound, since it is a decided contract and not a mechanic: this covers the open work the session is holding, never a reconciliation of durable records against repository or forge reality. The wording corrections in AGENTS.md and the completion receipt are unchanged. * fix(decisions): close decision holds at answer time via one general keyed-answer path (#2490) * fix(decisions): close captain holds at answer time Firstmate had two "a decision is open" ledgers with asymmetric closing mechanics. The live status-log ledger closes atomically at answer time, because bin/fm-send.sh --resolve-key makes answering a decision be the act that closes it. The durable backlog hold ledger had no such coupling: answering and recording were two separate acts, and only the first was forced by the workflow. That asymmetry lost four real captain decisions. Their answers were captured durably to disk, keyed character for character by the hold decision keys, acknowledged, and even implemented and shipped, yet the holds stayed open for two days and the captain was asked to re-answer decisions already on his own disk. Give the hold ledger the same answer-time-closure property: - bin/fm-decision-hold.sh gains an `answer` subcommand, the hold ledger's counterpart to --resolve-key. It shares one unrouted close implementation with `decline`, so it carries every existing guard - the captain decision file, the active-hold requirement, retry identity, and the refusal to release still-routed work - and differs only in the resolution mode it records. `decline` keeps its stronger meaning that the answer routes no follow-up work at all. - bin/fm-procevent-lavish.sh wires the channel that actually carried the lost answers. `arm --decisions-origin` binds a deck to the origin whose holds it carries, `answers` reads the structured choices out of a captured poll result, `close-decisions` maps each key to its hold and closes it through the command above, and `autohandle` lets the runner apply that at capture time. Safety is preserved rather than traded away. Only rows tagged `choice` are read, so freeform captain prose cannot forge a decision key. Closure is confined to the one bound origin. The decision text is a pure function of the captured result, so a replayed capture is idempotent. A hold that is absent, already closed, or still blocking routed work is skipped and left for `resolve`, never forced. A deck armed without the binding touches no hold at all. And autohandle deliberately never reports full handling, because recording an answer is transcription while acting on it is firstmate's judgement - so the check wake still reaches the handler. fm-send --resolve-key is untouched. * no-mistakes(document): document state/lavish-decisions binding dir in AGENTS.md state inventory * refactor(decisions): make keyed-answer closure one general capability The previous pass gave holds answer-time closure but built it as bespoke Lavish wiring: the review adapter carried the source-to-origin binding, mapped keys to hold identities, wrote decision records, decided what to skip, and closed holds itself. That treated a review deck as a special decision source. It is not - it is an ephemeral discussion format that happens to carry answers. Collapse it into ONE general capability with one owner. bin/fm-decision-hold.sh now owns the whole of "a keyed answer closes its matching hold": - `answers <origin> --source <provenance>` is the channel-agnostic intake. It reads key/answer/label lines on stdin, maps each key to its hold, and closes it through the same `answer` path, so every guard applies identically whatever channel the answer came from. --source is provenance recorded in the decision, never a behavior switch; there is no per-channel branch and no knowledge of chat, decks, or transports. - `bind`/`unbind`/`binding` own the source-to-origin binding for any channel whose answers arrive detached from their origin. Every channel is now an ordinary caller that only turns what it received into keyed lines: - bin/fm-send.sh (chat) feeds the intake for a key that names an active hold. This also fixes a real gap: once `complete` transfers a decision to its hold it closes the live status copy, so --resolve-key alone could never answer a transferred decision. - bin/fm-procevent.sh feeds it generically. A bound source's captured result goes to `<adapter> answers <result-file>` and whatever that prints is piped into the intake. The runner names no adapter, parses no result, and carries no decision rule, so any future adapter with an `answers` command works with no change here. - bin/fm-procevent-lavish.sh keeps only `answers`, which reports the structured choices a review captured and stops. It maps nothing to a hold and closes nothing; it lost ~160 lines of decision logic. Feeding is independent of handling, so it never acknowledges a result and never suppresses a wake - recording an answer is transcription, acting on it stays firstmate's judgement. The regression that proves closure now drives a FIXTURE adapter that is not the review adapter, so what is proven is that any bound channel reaches the intake rather than that one channel is wired specially. A new regression drives the real fm-send over a stubbed transport for the chat side. Every prior guarantee still holds, and fm-send's status-log behavior is unchanged. * no-mistakes(review): test(decisions): drop source-content grep from hold-closure regression * fix(memory): emit a real @AGENTS.md pointer instead of a CLAUDE.md symlink (#2512) A Write aimed at CLAUDE.md followed the symlink and destroyed AGENTS.md. The installer now creates and migrates to a recoverable two-line pointer file. * fix(ci): keep CLAUDE.md pointer check valid (#2515) * ci: gate GitHub workflows with pinned actionlint (#2517) * fix(lint): catch malformed GitHub workflows before merge A self-broken ci.yml cannot report its own breakage, so parse every workflow in the local lint path that no-mistakes already runs. * fix(lint): pin actionlint instead of Ruby for workflow lint A self-broken ci.yml still has to fail in the local lint path, and the named tool for that gate is actionlint, not a new Ruby runtime. * no-mistakes(document): Clarify pinned workflow lint documentation * fix: install pinned lint tools across supported platforms (#2546) * fix: install pinned shellcheck and actionlint on macOS and linux arm64 The installers were hardcoded to linux amd64 and sha256sum, so a Mac dev could not satisfy the refuse-on-mismatch lint gate. Select the official per-platform archive and checksum, and fall back to shasum -a 256. * no-mistakes(document): Document cross-platform pinned lint installers * docs: reconcile test-evidence docs with store_in_repo: true (#2548) .no-mistakes.yaml has set test.evidence.store_in_repo: true since #2355, but CONTRIBUTING.md, docs/configuration.md, and docs/architecture.md still described the old policy of keeping evidence out of the repo in a temp directory. The current no-mistakes behavior for store_in_repo: true is to publish each run's test evidence to the orphan no-mistakes/evidence branch and link it from the PR body. That branch shares no history with code branches, so evidence never enters a pushed feature branch or the default branch, and CI's tracked personal fleet paths rule stays accurate. Docs only. No change to .no-mistakes.yaml or any workflow. * docs: clarify test evidence branch storage (#2549) * docs: correct test evidence storage comment in .no-mistakes.yaml * no-mistakes: apply CI fixes * docs: hint that live scouts may host their own Lavish review loop (#2563) Make that a first-class option in always-loaded instructions so firstmate does not default to mediating and tearing the scout down between iteration rounds. * fix(bin): report remote secondmate delivery and state truthfully (#2570) * fix(bin): report remote secondmate delivery and state truthfully A steer to a remote secondmate crosses fm-on.sh to a host-local fm-send leg whose unconfirmed submit read-back (verdict=pending, typically a busy mate whose harness queues the steer) was flattened into exit 1, so the parent printed "error: text not submitted" / "error: text not sent" and discarded the pending-reply expectation for a steer that had actually landed. fm-send now carries the verdict across the ssh boundary as a documented delivered-unconfirmed exit 3: the parent reports the steer as delivered with confirmation pending, exits 0, keeps the expectation armed (awaiting_report), and closes --resolve-key decisions, while transport loss (ssh 255) and real remote failures keep failing loudly with the remote leg's stderr attached. A local unconfirmed submit now also exits 3 with an honest non-error message and still never closes a decision key. fm-crew-state.sh and fm-peek.sh no longer read a remote mate's endpoint through local probes (which misreported a healthy mate as "worktree gone" / "can't find session: remote"): both now use the true remote source over fm-on.sh, and an unreachable or unreadable remote reads as unknown-remote, never as gone or dead. * no-mistakes(document): Document remote delivery and state truth * no-mistakes: apply CI fixes * feat: adopt spendPriority for quota dispatch (#2574) * Adopt quota-axi 0.1.29 spendPriority-primary array dispatch. quota-axi 0.1.29 publishes schema 5 with selection.spendPriority as the primary comparative signal and demotes derivation fields out of default --json. Rank comparable-fit candidates on that scalar, keep runway versus the completion horizon as a hard gate, and raise the compatibility floor so a pre-consolidation build cannot reach dispatch intake. * no-mistakes(review): Correct schema fixtures and remove prescriptive selection prompts * no-mistakes(document): Correct quota verification evidence chronology * Collapse quota-array-dispatch onto TOON-first spendPriority ranking. Decide from quota-axi's default TOON; keep --json as a rare defensive fallback. Rank by spendPriority after eligibility, reasoning-class, and runway-feasibility gates, and drop the hand-computed Pareto, pace, reserve, and window-id layers. * no-mistakes(review): Permit ambiguous JSON fallback and correct reset fixtures * no-mistakes(review): Correct runway semantics and escalate unresolved uncertainty * no-mistakes(document): Document TOON-first quota dispatch evidence * docs: add GROK_BOT.md Grok Bot system prompt (#2590) * docs: add GROK_BOT.md Grok Bot system prompt * docs: amend GROK_BOT.md with charter report-back and delegation marker * docs: classify GROK_BOT.md as public-product * docs: make GROK_BOT.md the plain Grok Bot system prompt * docs: update GROK_BOT.md nautical terms and self-improvement (#2592) * doc: Update language in GROK_BOT.md for clarity Refine language for clarity and consistency in instructions. * fix(bin): preserve inactive reconciliation scan progress (#2595) * fix(bin): guarantee inactive-reconcile scan progress under second quantization The inactive-outcome scan computed its aggregate deadline in whole seconds, so a 1-second budget's effective value lands anywhere in (0,1]; a scan starting just before a wall-clock second boundary rounded its whole budget away mid-scan and exited having visited no child, while the durable cursor had already advanced past the never-examined child. This is the CI flake behind tests/fm-inactive-reconcile.test.sh's 'next bounded scan did not resume with the following child' (watcher-wake-lock family, portable serial 2, seen on the PR #2590 run). Every scan now visits at least its first due child with the per-child state-read bound floored at one second, so no invocation can be a zero-work no-op. The outer process-group kill moves to budget+1s: the scan's own deadline enforces the budget, and the kill is a backstop for a scan wedged in an unbounded wait instead of a racer that routinely preempts the clean bounded exit. The wake-lock-wait test bound tracks the backstop (3s -> 4s); the previously flaky assertion is unchanged. * no-mistakes(document): Document inactive-reconcile deadline backstop * doc: Revise Firstmate delegation and communication guidelines Refactor the guidelines for Firstmate's role and delegation process, emphasizing the importance of crewmates and asynchronous work. * doc: Update work delegation and secret management instructions Clarified guidelines for handing off work to crewmates and managing secrets. * test(procevent): make the process-event suite's detached-runner assertions deterministic (#2617) Three assertions in tests/fm-procevent.test.sh depended on a detached runner having finished work that the command starting it does not wait for. reconcile's replacement runner is started through detach_runner, which only forks: reconcile returns and counts the start before that runner has claimed its source or exec'd its child. Any assertion taken straight after reconcile therefore samples a race. - The publish-before-apply recovery section left its always-ready /bin/echo source registered across the recovery reconcile, so that reconcile launched a competing detached poll (observed: started=1) that then raced every later assertion for the source claim, the next capture sequence, and this home's applied record, and outlived the section holding a live claim. It is now retired before that reconcile - re-announcement is proven from the durable inbox alone and needs no registration - and started=0 is asserted so a competing poll cannot be reintroduced unnoticed. This is the same retire-before-reconcile discipline the self-announcing section already carries; that section acquired it after the identical race made its "not-autohandled: self-src" assertion read "already owned: self-src". - The crashed-leader replacement section snapshotted the replacement's claim file and execution log behind a fixed 0.5s settle window. On a loaded machine that window expires first, which is the CI flake behind "a replacement runner started without recording its own claim" and "reconcile did not start exactly one replacement source". Both effects are now waited for with the suite's bounded wait helpers; the exact one-replacement count is still asserted afterwards, unchanged. - The duplicate-start section slept 0.5s for reconcile's runner to record ownership before asserting that a second start loses to it. It now waits for that claim. Also tighten one assertion that could not fail as written: "autohandled: self-src" is a substring of "not-autohandled: self-src", so the applied path was accepted even when the runner reported the capture left for the handler. Evidence: on the unmodified suite, 128 full runs at 6-8x concurrency produced 6 failing runs, all in the crashed-leader section. On the fixed suite, 216 full runs under the same load produced none. Reverting the self-announcing section's retire-before-reconcile line reproduces "already owned: self-src" on the first iteration, confirming the shared mechanism. * fix: preserve pending replies and defer remote reposts (#2618) * fix(bin): keep pending-reply expectations honest on both send legs Two related asymmetries let the parent-owned secondmate reply guard drop or nag requests it should not have. Local delivered-unconfirmed dropped the expectation. A marked request whose submit read-back stayed unconfirmed (verdict=pending) is the same not-a-failure outcome the remote leg reports as delivered, but fm-send discarded the parent's pending-reply record for it, so a request that very likely landed stopped being tracked entirely. The record now stays armed on its unconfirmed-delivery marker: a correlated report still resolves it, and an unanswered one still surfaces through the library's own reconciliation. Exit 3 and the local rule that an unconfirmed answer never closes a decision key are unchanged. Remote replies were nagged for a repost they did not need. A remote mate's report reaches the parent's status log only through the asynchronous mirror in fm-procevent-remote-reply.sh, yet the guard read an absent correlated line as proof the mate never reported - even while the answer was still in flight, which is the common case because the mirror's poll window is comparable to the recovery grace. The mirror now publishes one caught-up watermark from a quiet window, and the guard admits a missing report as evidence only once that watermark passes the turn that should have produced it. A genuinely missed report still gets exactly one repost, and a channel that is behind, unarmed, or broken leaves the request durably open and un-nagged rather than nagging blind; the mirror escalates its own continuity failures as before. Tests: a local unconfirmed secondmate send keeps its expectation armed and resolvable; a mirrored correlated remote reply resolves with no repost; a stale or absent watermark withholds the repost while a fresh one still releases it; a quiet remote window publishes the watermark and retirement clears it. * no-mistakes(review): Distinguish preempted polls from quiet windows * no-mistakes(document): Clarify remote reply channel freshness * no-mistakes(lint): Annotate shared remote preemption exit constant * fix(bin): honor declared pauses in busy-pane wedge checks (#2619) * fix(watch): honor a declared pause on a busy pane's completed-turn bound A worker that declares an external wait (`paused:`) and then blocks in one long foreground call - a review-hosting scout parked in a single blocking `lavish-axi poll`, a bounded watch loop, a rate-limit sleep - keeps its pane BUSY, so the stale path that already honors declared pauses never ran for it. The busy-pane completed-turn bound instead routed it straight into wedge_timer_check, which re-escalated "possible wedge, escalation N" (and, past the threshold, demand-deep-inspection) every FM_STALE_ESCALATE_SECS for as long as the review stayed open. busy_turn_bound_check now owns which absorber takes a crossed bound: a crew whose own last status line declares an external wait or a verified captain-held transfer takes the bounded FM_PAUSE_RESURFACE_SECS recheck, and everything else keeps the unchanged wedge timer. The discriminator is the declaration together with liveness (the caller has already confirmed the pane is busy), never a blanket silencing - a crew that declared nothing, or whose pane is not live, escalates exactly as before, and a declared pause still re-surfaces once per long cadence so a forgotten wait cannot rot invisibly. Away mode is untouched: the daemon owns pause triage there and already reads the same vocabulary. The two call sites also no longer clear pause bookkeeping in the same poll the pause cadence recorded it, which would have erased the re-surface throttle and turned the long cadence back into a per-poll re-surface. Tests: a three-phase regression fixture pins the absorbed pause, its long-cadence recheck, and the restored wedge escalation once the declaration is lifted on the same busy over-age pane. Also de-flakes tests/fm-watch-triage.test.sh, which failed spuriously on a loaded machine: fixed liveness budgets were reaping watchers mid-startup, so assertions on post-poll state passed vacuously or failed spuriously. Waits that describe a poll's outcome now wait for a completed poll cycle via the liveness beacon, the heartbeat test waits for the heartbeat it asserts on, and every wait_for_exit budget is the uniform 10s already used elsewhere in the file. * no-mistakes(review): Fail poll-cycle waits on timeout * no-mistakes(review): Prevent poll timeout test hangs * no-mistakes(document): Clarify paused busy-pane supervision * doc: Enhance communication guidelines for decision-making Added guidelines for decision communication to the captain. * doc: Update task delegation and communication guidelines Clarify communication protocols with crewmates regarding task delegation and reporting. * fix(bin): reliably confirm herdr steer submission (#2647) * fix(herdr): confirm local steers that native agent-state misses Herdr can leave agent_status idle for a landed Claude turn and can keep queued Enter text visible while busy, so fm-send was reporting false swallows. Confirm those cases through the shared queued-Enter verdict and a cleared composer, and keep a genuine idle pending composer as unconfirmed. * no-mistakes(review): Stop Herdr Enter retries on unreadable composers * no-mistakes(review): Reject queued delivery when all Herdr Enter sends fail * no-mistakes(review): Prevent confirmation after failed Herdr Enter * no-mistakes(review): Pace Herdr retries and clarify submit fallback * no-mistakes(review): Align Herdr submit docs with idle fallback * no-mistakes(document): Correct Herdr submit-confirmation documentation * feat(bearings): add interactive Lavish fleet board (#2659) * feat(bin): accept any-origin decision bindings with full-identity keys An aggregation surface (the bearings board) carries captain answers for holds across origins, but a binding was one-origin-per-source and the Lavish adapter capped question keys at 64 chars while real full hold identities measure 69-81. - fm-decision-hold.sh: bind <source-id> --any-origin records the (any) marker; binding prints it verbatim and answers accepts it, so the runner's feed seam carries an any-origin source with no runner change. In any-origin mode each key is a full hold identity <origin>-decision-<key>, split at its first -decision-; a key with no separator (merge/dispatch instructions) is skipped and feeds nothing, keeping non-decision answers out of the hold ledger by construction. Every existing close guard applies unchanged. - fm-procevent-lavish.sh: raise the question-key cap 64 -> 128 so a full hold identity fits; the slug-shape security property is unchanged. - tests: cross-origin closure through the real runner seam, an 81-char identity through the adapter, cap and shape refusals, routed-work skips, nonexistent-identity skips, and idempotent replay. * feat(bearings): add the /bearings lavish interactive fleet board /bearings lavish renders the bearings snapshot onto a shipped, reusable board template and arms it as a Lavish process-event source, so the captain answers Captain's Call items on the board and firstmate is woken by an ordinary check wake - no conversational turn ever blocks on a poll. - .agents/skills/bearings/assets/board-template.html: the shipped template (myfirstmate design system inlined, one fm-bearings-board.v1 JSON slot, fail-closed schema guard that renders an error card instead of an empty fleet). Per-invocation agent work is composing the payload only. - bin/fm-bearings-board.sh: build/refresh owner - fail-closed payload validation, slot injection with a round-trip check and \u003c escaping, stable board path, any-origin bind ALWAYS before arm, arm-if-absent. - bearings SKILL.md: the lavish invocation option, board composition rules, board-wake handling, and the captain-ruled merge-click authorization with its mandatory safeguards (PR resolved from the task's own meta record, wake-time green re-verification, never a red or changed PR, merges only through bin/fm-pr-merge.sh, chat echo with the full PR URL). - process-event-sources SKILL.md: one-line board-wake routing trigger. - tests: payload refusals, injection round-trip, bind-before-arm, idempotent re-arm, and template slot integrity. Fleet pickup: homes receive this after merge plus a firstmate self-update; landing timing is coordinated with the main firstmate. * no-mistakes(review): Harden bearings board validation and wake handling * no-mistakes(review): Require HTTPS for bearings board PR links * no-mistakes(review): Fail closed and bound bearings board answers * no-mistakes(review): Enforce UTF-8 byte limits for board answers * no-mistakes(review): Serve bearings board before arming and reject empty actions * no-mistakes(review): Prove bind-before-arm ordering through live answer consumption * no-mistakes(document): Document bearings board and cross-origin answers * fix(bearings): restore decision options and add close controls (#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 * ci: require no-mistakes pipeline step attestation (#2710) Signature-only PRs can hide skipped review, test, or document steps. Fail unless no-mistakes >= 1.46.0 attests those three steps completed. * feat: collapse decisions into tasks held for the captain (#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 * fix: bound recovery announcements and preserve supervision (#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): surface captain-call record divergence (#2744) * 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 * fix(bin): re-arm after an abandoned auto-arm claim and defer a wedge escalation while a worktree is written (#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 #2251's PR #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 * fix(bin): give a captain hold the same bounded pause cadence as a declared pause (#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 * fix(bin): make lint prerequisites and harness tests reliable (#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 * feat(bin): report watched tooling updates that are available or installed but inert (#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: decouple ask-user decisions from yolo (#2764) * 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 * feat(bin): add a spoken interface that answers from records and hands work over (#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 * fix(bin): preserve Relay follow-up loops until explicit disposition (#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 * feat(bin): merge GitLab merge requests through the guarded PR merge path (#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 * fix(bin): record a lost relay connection instead of an unanswered turn (#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 #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 7f98e797 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 afte…
lytv
pushed a commit
to lytv/mymate
that referenced
this pull request
Sep 8, 2026
* docs: add GROK_BOT.md Grok Bot system prompt * docs: amend GROK_BOT.md with charter report-back and delegation marker * docs: classify GROK_BOT.md as public-product * docs: make GROK_BOT.md the plain Grok Bot system prompt
lytv
pushed a commit
to lytv/mymate
that referenced
this pull request
Sep 8, 2026
…#2595) * fix(bin): guarantee inactive-reconcile scan progress under second quantization The inactive-outcome scan computed its aggregate deadline in whole seconds, so a 1-second budget's effective value lands anywhere in (0,1]; a scan starting just before a wall-clock second boundary rounded its whole budget away mid-scan and exited having visited no child, while the durable cursor had already advanced past the never-examined child. This is the CI flake behind tests/fm-inactive-reconcile.test.sh's 'next bounded scan did not resume with the following child' (watcher-wake-lock family, portable serial 2, seen on the PR kunchenguid#2590 run). Every scan now visits at least its first due child with the per-child state-read bound floored at one second, so no invocation can be a zero-work no-op. The outer process-group kill moves to budget+1s: the scan's own deadline enforces the budget, and the kill is a backstop for a scan wedged in an unbounded wait instead of a racer that routinely preempts the clean bounded exit. The wake-lock-wait test bound tracks the backstop (3s -> 4s); the previously flaky assertion is unchanged. * no-mistakes(document): Document inactive-reconcile deadline backstop
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.
Summary
GROK_BOT.mdat the repo root: the Firstmate system prompt for the Grok Bot product.Notes
Test plan
GROK_BOT.mdmatches the approved spec byte-for-byte