sync: merge upstream kunchenguid/firstmate main (16 commits) - #27
Conversation
* fix(session-start): refresh drifted instructions on stale rebuilds * test(session-start): prove Pi instruction refresh end to end * no-mistakes(review): Fix stale instruction refresh and baseline integrity * no-mistakes(review): Preserve true-start baselines across Pi continuations * no-mistakes(review): Correct Pi continuation classification and live expectation * no-mistakes(review): Correct Pi continuation coverage documentation * no-mistakes(review): Fix read-only refresh and exact Pi session restores * no-mistakes(review): Classify Pi create-if-missing sessions correctly * no-mistakes(review): Classify named Pi sessions using immutable headers * no-mistakes(review): Correct Codex interactive coverage diagnostic * no-mistakes(document): Document immutable Pi compaction instruction refresh * no-mistakes(document): Correct Pi refresh documentation and validation claims
* feat(bin): add deterministic condition->action watch adapter on the process-event channel Register a (condition, action) pair once with bin/fm-procevent-when.sh and the existing process-to-event runner polls the condition tokenlessly, fires the action at most once on a stable true, and wakes firstmate exactly once with the captured outcome - instead of burning an agent turn per re-check. The pair is stored privately under state/when/ and hash-bound by a trust record the same way fm-check-register.sh binds a custom check, so a mutated spec is refused without executing anything. A durable exclusive fired marker claimed before the action makes restarts and re-polls unable to double-fire; every failure path (mutated spec, condition error past budget, expired deadline, failed action, uncaptured earlier fire) ends in a terminal captured outcome that wakes firstmate rather than a silent retry. Eligibility stays a firstmate judgment: only exact, safe, reversible actions may be bound, and judgment- needing or destructive actions keep the wake-and-decide flow. * no-mistakes(review): Harden when watcher concurrency, deadlines, timeouts, and output * no-mistakes(test): Bind watcher actions to registered executable bytes * no-mistakes(document): Correct condition-action watcher documentation * no-mistakes(document): Clarify outcome wake re-announcement * no-mistakes: apply CI fixes
…id#2202) The open-decisions fold only recognized a [key=<slug>] token between the verb and the colon (needs-decision [key=x]: note). The common worker shape with the colon first (needs-decision: [key=x] note) silently folded its stated key into the shared "default" bucket, so two open decisions could collapse into one record and fm-send --resolve-key <x> refused to close the decision it plainly named. A complete token at the head of the note is now an equivalent stated-key position for every keyed verb, shared by the whole-file and incremental folds through the one _fm_decision_key owner. The documented before-colon position wins when both are present, a token deeper in the note stays prose, a bare keyless line still folds to "default", and a stated-but-malformed slug is rejected rather than rewritten to "default". A consumed note-head token is stripped from the note so both positions yield identical records, and the incremental fold version is bumped so persisted cursors folded under the old interpretation are rebuilt from the authoritative log. Fixes kunchenguid#2109
…uid#2212) * fix(bin): keep a recovery acknowledgement valid across republication A watcher cycle that opened and closed while the model handled its drained wakes minted a fresh recovery generation, which invalidated the exact acknowledgement the drain had just printed. That acknowledgement then consumed nothing, so the marker stayed pending and every later arm spent its whole cycle re-announcing the same recovery instead of supervising - a livelock the home could not leave on its own. A downtime publication now reuses the generation of an outstanding handling episode, so a close during the handling window cannot orphan the printed acknowledgement. The acknowledgement itself separates its two facts: queue-row consumption is bound to the monotonic --ack-through sequence and always happens, while only retiring the episode is bound to --recovery-generation. A generation that moved on is a non-fatal result that names its own remedy instead of a refusal that consumes nothing. * no-mistakes(review): Preserve recovery generations and consume stale acknowledgements safely * no-mistakes(document): Document sequence-bound recovery acknowledgements
* feat(fmx-respond): consume in_reply_to_chain conversation context The relay's poll payload can carry in_reply_to_chain, an oldest-first transcript of the surrounding conversation, but the mention-handling procedure only ever read the immediate in_reply_to parent, so referents like "this" in a standalone mention stayed unresolvable even when context was delivered. Teach fmx-respond to read the chain when present (optional and backward-compatible: often absent today, kind label not required), resolve referents against the whole transcript, and extend the untrusted-content framing to every chain entry including the upcoming kind=history entries. Document the field's wire shape in docs/configuration.md as the firstmate-side owner. * no-mistakes(document): Document Relay chain context ownership
* fix(bin): strip every bracket tag, not just [key=...], from a status verb
status_line_verb only stripped a leading "[key=...]" token before the
colon, so a remote secondmate reply's leading "[corr=...]" correlation
tag stayed glued onto the returned verb word ("needs-decision
[corr=...]" instead of "needs-decision"). The open-decisions fold's
verb match then silently failed to recognize the line at all, so
fm-send --resolve-key refused to close a decision that was plainly
open on the status line.
Generalize the parser to strip every "[name=value]" tag before the
colon, in any order and count, so local and remote replies fold
identically.
* no-mistakes(review): Invalidate stale decision cursors after parser fix
* no-mistakes(document): Clarify status metadata verb parsing
* fix: collapse duplicate supervision wakes without losing legitimate updates One remote-secondmate note produced two handling turns (a procevent check wake published before autohandle, then a signal wake for the same mirrored bytes), already-ingested replays such as a cursor-loss whole-log recapture still woke with nothing to do, this home's own bookkeeping closes (fm-send --resolve-key, the pending-reply escalation close, the captain-held transfer) re-woke the session that wrote them, and turn-ended-only wakes were annotated with already-announced status lines that looked like fresh progress. Dedup rules, each at its layer's one owner: - fm-procevent.sh: an adapter may declare 'self-announcing'; the runner then applies first and publishes a check wake only for what remains unhandled. fm-procevent-remote-reply.sh declares it: the mirrored status append is the single announcement, so a fully applied capture publishes nothing and a byte-identical replay stays completely quiet. All other adapters keep strict publish-before-apply. - fm-wake-lib.sh: fm_wake_signal_sig/seen_path/seen_current now own the watcher's signal signature and .seen-* marker format, plus fm_wake_status_append_self_announced, the guarded bookkeeping append that advances the marker only over exactly its own bytes and fails toward waking on any pending or interleaved foreign write. - fm-send.sh, fm-pending-reply-lib.sh, fm-decision-hold.sh: bookkeeping closes go through that guarded append; escalation opens stay plain appends because a new blocker must wake. - fm-wake-lib.sh annotations: a historical (turn-ended-only) row skips its status annotation only when the file's signature provably matches the seen marker; anything unannounced keeps annotating. - fm-classify-lib.sh: a kind=secondmate task's status signal is never absorbed as provably-working, because that stream is the routed-reply channel the parent must read. Also fixes a pre-existing exit-path deadlock the regression run reproduced: a TERM inside a recovery-marker critical section left fm_lock_try_acquire spinning against this same process's abandoned hold; a self-held lock is now reclaimed (a subshell still waits on its parent's live hold). Regression tests drive the real wake functions and executables in both directions: each duplicate case collapses, while a new remote reply, new decision, new blocker, merge result, failure, first status change, and a later different note on the same task all still wake. * no-mistakes(document): Document wake deduplication contracts
* feat(harness): add Cursor Agent CLI adapter # Conflicts: # bin/fm-spawn.sh * fix(composer): read cursor-agent's reverse-video placeholder as idle cursor-agent renders its idle composer placeholder dim (SGR 2) but paints the cell under the terminal cursor in reverse video (SGR 0;7). Reverse video is neither dim nor a dark truecolor foreground, so the shared ghost stripper keeps that one character and an idle composer reduces to a lone `P`. Judged on its own, that remnant reads `pending` on a genuinely idle pane, which defers away-mode escalation indefinitely on the styled cursorless backends. Teach the ONE fleet-wide classifier the shape instead of adding an adapter-local copy: register `→` as an agent prompt glyph so the composer row is structurally findable at all (without it the bottom-most shape is a stale shell prompt echo in the scrollback), add both verified placeholders to the idle set, and consult the styling-independent plain row when the styled row is only a remnant. The plain-row branch demands the remnant be a proper, strictly shorter substring of a plain row matching a fully anchored placeholder. Real typed text is uniformly bright, so stripping leaves it equal to the plain row and it stays `pending` - verified live against a pane where the typed text was exactly the placeholder string. Verified live on cursor-agent 2026.08.11-e8db854; the regression pins the real captured bytes and asserts the remnant survives stripping, so the case cannot go vacuous if the stripper later learns SGR 7. Co-authored-by: Amplify Logic AI <lars@sockinator.co> * feat(cursor): narrow cursor identity and order its marker before CLAUDECODE Cursor ships two executable names - `cursor-agent` and the legacy alias `agent` - and runs as a bundled node script, so tmux reports the pane command as a bare `node`. Neither `agent` nor `node` can be trusted by name, so identity gets one owner in bin/fm-cursor-lib.sh that demands cursor's own name or install tree in the path or argv[0], from the structural signal only. Probing an arbitrary pid's executable during a liveness poll would execute a stranger's binary, which is the hazard that rule exists to close. Two consequences wired up: Detection. cursor-agent does NOT clear an inherited CLAUDECODE, so a cursor worker launched under a claude primary carries both markers and whichever is tested first wins. The cursor markers are ordered ahead of the CLAUDECODE check; fm-spawn additionally clears foreign markers at the launch boundary. Both are kept deliberately - launch sanitization only covers sessions fm-spawn started, while the ordering also covers a cursor session started by hand. Verified live that CURSOR_INVOKED_AS is set on the agent process and CURSOR_AGENT=1 on the child/tool processes fm-harness.sh actually runs as. Pane liveness. A cursor pane now classifies `agent`. An unrelated node or agent stays `other`, which the liveness callers already fold into `ambiguous` rather than `dead`, so a stranger's node pane is never reported agent-free. Resolution prints the STABLE launcher rather than the canonical target: identity is proven through canonicalization, but cursor's canonical path carries a version its own auto-update replaces, and pinning that would strand a task on a version that can vanish. The regression drives the two identity signals apart - a cursor-named executable outside any cursor tree, and a non-cursor-named alias inside one - and asserts each carries a verdict alone, so no single vendor string is load-bearing. Its negative controls are real spawned processes, not fixtures. Verified live on cursor-agent 2026.08.11-e8db854. Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com> * feat(cursor): classify cursor busy state from its own turn transcript Cursor shipped as "unknown cursor-unverified" on the premise that it exposes no semantic turn lifecycle, only a rendered "Working" footer. That premise is wrong: cursor-agent persists an append-only JSONL transcript per conversation and brackets every submitted turn with a role:user open and a typed turn_ended close. Verified live on 2026.08.11-e8db854, including the interrupt path, where Escape closes the turn with status "aborted" - so this source covers manual interruption, which Claude's Stop hook does not. That makes it a genuine pull source in the muse mould rather than the rendered text the redesign forbids: no writer, no arm, no gen, nothing seeded that could never be cleared. Cursor's `ctrl+c to stop` footer stays out of the verdict, and herdr's narrower native streaming state cannot stand in for it either. Binding deliberately does not reconstruct cursor's workspace-slug directory name. That slug collapses path separators, so rebuilding it would be a guess that could bind the wrong pane; cursor records the exact absolute workspace path in each project's .workspace-trusted, and the binding matches on that. A conversation recorded as prior at spawn is excluded, so a relaunch in a reused worktree folds its own turn rather than its predecessor's. Requiring a unique remaining conversation keeps zero and several both unknown, because neither proves anything about the current turn. The regression pins the fold with real transcript files and asserts the dangerous direction stays closed: an unresolvable binding, a record-free file, an unclaimed workspace, and a workspace-path PREFIX all read unknown, never idle. The prefix case uses an opaque fixture slug so a slug-rebuilding implementation cannot pass it. Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com> * feat(cursor): make the cursor launch runnable and give it lifecycle control Five gaps that together kept a cursor crewmate from being drivable end to end. Launch. The template invoked `cursor agent`, but `cursor` is not the CLI - the installed names are `cursor-agent` and the legacy alias `agent` - so the command could not run at all on a machine with a normal cursor install. It now resolves through the verified owner, which also refuses a spawn loudly instead of leaving a pane that dies with command-not-found and reads as a wedged worker. Session binding. fm-spawn writes state/<id>.cursor-session so the busy fold can find this pane's transcript, and teardown removes it. Lifecycle control. No cursor PR touched fm-control-lib.sh, so `fm-control <id> interrupt|exit|relaunch` could not drive a cursor worker at all. Verified live: interrupt is a single Escape, exit is /exit, and cursor does NOT repollute its composer with the cancelled prompt, so unlike muse it needs no clear key. Secondmate is refused, matching the spawn refusal. Submit acknowledgement. cursor parks its terminal cursor outside its composer, so the composer verdict on tmux is always `unknown` and a submit could never be acknowledged from the composer alone. The submit core's existing idle-to-busy transition covers that case, but only if the pane's busy footer is recognised, so cursor's `ctrl+c to stop` joins the harness-less default union the submit cores read. The TOKEN is matched rather than the spinner verb: the same version rendered both `Working` and `Running` in consecutive turns. Bootstrap. A configured cursor crew harness with no cursor executable is now a loud MISSING diagnostic rather than a first-spawn failure, and it accepts either installed name. Interrupt cancellation is deliberately left unconfirmed. The transcript does type an aborted close, but its post-interrupt write latency measured as variable - sometimes seconds, sometimes not within twenty - so a claim built on it would be unreliable. Normal turn completion is prompt, which is what the busy fold actually depends on. Two inherited tests are corrected rather than deleted: the busy test asserted cursor could have no semantic source, and the launch test pinned the literal `cursor agent` string. Both now pin the verified behaviour, including that the launch never allocates a second worktree. Co-authored-by: ABHISHAKE KUMAR BOJJA <abojja@uvic.ca> Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com> * docs(cursor): record the verified crewmate facts and extend the drift guard The inherited cursor entry was written against 2026.08.04-aaa8809 and several of its claims no longer hold: it named `cursor agent` as the binary (not the CLI name), listed six Grok model ids of which the live catalog now returns two, and recorded busy state, exit, interrupt, and skill invocation as unverified. Replaced with what was measured against 2026.08.11-e8db854, including the two facts most likely to be rediscovered painfully: cursor runs as a bundled node script so its pane title is a bare `node`, and it parks its terminal cursor outside its composer, which makes the tmux composer verdict permanently `unknown` by design rather than a defect to chase. Model ids now route to `--list-models` for the account instead of a fixed list, since that list is exactly what drifted. The live drift guard covers cursor, resolving it through the same verified owner fm-spawn uses and passing --trust so the probe cannot hang on the workspace prompt. Run against every installed harness: 8 checked, all alive, with cursor reporting title='node' foreground=[.../cursor-agent] - the drift shape this guard exists to catch. Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com> * docs(agents): record the cursor session-binding state file The state/ layout section is the inventory every session reads; a busy-source binding that fm-spawn writes and teardown removes belongs in it alongside muse's. * no-mistakes(review): Sanitize ambient Cursor marker in harness tests * no-mistakes(review): Validate Cursor models against live catalog * no-mistakes(review): Reject unsupported secondmates before binary preflight * no-mistakes(review): Narrow Cursor ancestry detection to structured process identity * no-mistakes(review): Parse Cursor transcripts and sanitize inherited markers * no-mistakes(review): Handle malformed Cursor transcript records safely * no-mistakes(review): Validate malformed Cursor closes in fallback parser * no-mistakes(review): Retire stale Cursor bindings during relaunch * no-mistakes(review): Fix Cursor drift guard command variable * no-mistakes(review): Narrow Cursor identity to versioned install trees * no-mistakes(document): Document Cursor harness boundaries * refactor(composer): move the delivery busy footers to the shared owner The per-harness rendered busy footers lived in bin/fm-tmux-lib.sh under FM_TMUX_* names, so cursor's `ctrl+c to stop` signature - and every other harness's - was reachable only from tmux. That placement was wrong on its own terms: herdr, zellij, cmux, and orca run the same harnesses and face the same question these footers answer, which is whether a submitted Enter actually landed. Nothing about the signature is tmux-specific. Moved verbatim into bin/fm-composer-lib.sh, the shared composer/delivery owner every backend already sources, and renamed to FM_DELIVERY_* so the names stop claiming a scope they never had. All five adapters now reach cursor's signature; verified per adapter rather than assumed. The boundary the move must not blur is stated where it now lives: this is a DELIVERY guard, never a worker-state source. Confirming a keystroke landed is a different question from asking what a worker is doing, and bin/fm-busy-lib.sh remains the semantic owner that forbids classifying a harness from rendered text. Cursor still classifies only from its transcript fold, which is already backend-agnostic because it folds a file rather than reading a pane - the same verdict on all six backends. The old FM_TMUX_* aliases are dropped rather than kept as dead shims: nothing outside the moved block referenced them except fm-busy-lib.sh's grok fallback, which now reads the new name. The documented operator override, FM_BUSY_REGEX, is untouched. Also removes a dead duplicate CURSOR_INVOKED_AS check in bin/fm-harness.sh, unreachable behind the marker check above it. * no-mistakes(review): Correct shared delivery guard ownership references * no-mistakes(document): Document shared delivery guards and Cursor backend limits * no-mistakes: apply CI fixes * fix(composer): bound a bare composer's wrap region at a half-block rule A live cursor crewmate on herdr classified its IDLE composer as `pending`, and fm-send consequently exited 1 with "delivery unconfirmed" on a message that had actually landed. The cause is not cursor-specific. Herdr draws a composer's top and bottom rules with the half-block glyphs U+2584 and U+2580 rather than the box-drawing family. fm_composer_row_has_edge knew only the box-drawing set, so no box was detected; the composer was found as a BARE row, and its wrap region - which extends while rows are non-blank and carry no structural edge - walked straight through the composer's own closing rule and swallowed the model and path footer below it. That footer is real text, so the region classified pending on a genuinely idle pane. Teaching the shared edge detector the half-block glyphs bounds the region at the closing rule. Measured on the captured bytes of a real herdr cursor pane: the same capture that read `pending` now reads `empty`. This is a shared shape-path change, so it is deliberately narrow - it adds glyphs to the edge vocabulary and changes no verdict logic - and the whole composer and backend suite is green, including the other harnesses' herdr fixtures. The regression pins the real captured shape and asserts the footer content is genuinely present, so the case cannot pass vacuously if the region were ever bounded for some unrelated reason. * fix(herdr): confirm a cursor submit from the rendered-footer transition Herdr's composer-shape fix made an idle cursor pane classify `empty`, but `fm-send` still exited 1 with "delivery unconfirmed" on messages that had actually landed. Live measurement found the second, independent cause. Herdr reports a cursor pane `agent_status=blocked` in EVERY state - idle, mid-turn, and after - so the submit path's idle-baseline native confirmation is structurally unreachable for cursor and every send falls into the composer branch. That branch reads cursor's mid-turn composer row, which renders its own `Add a follow-up` placeholder beside a right-aligned `ctrl+c to stop`. That token is composer content, so the verdict is `pending` on a composer holding no user text at all, and the Enter-retry budget then reports pending. The escape is the same semantic signal the native path uses, read from the pane's verified busy footer instead of native agent-state, and it is the rendered-footer twin of the tmux submit core's turn-started confirmation: an idle-to-busy transition ACROSS our Enter proves the harness accepted the submission. The baseline is taken before the first Enter and only when the native baseline was not legibly idle, so the idle-baseline path still never reads pane content and a pane already mid-turn before we typed keeps reporting `pending` rather than borrowing another turn as proof of this delivery. The composer verdict is deliberately NOT relaxed. A right-aligned status token on the composer row stays content for every other caller, including the away-mode pre-injection guard, and the shared cursorless submit core is left untouched so zellij, cmux, and Orca keep the behavior their own follow-up owns. Verified live on herdr 0.8.0 and cursor-agent 2026.08.11-e8db854 in an isolated lab session: `fm-send` now exits 0 and the steer executes, interrupt cancels a running turn, `/exit` stops the agent, and teardown clears the record. All seven panes of the running default session classify identically before and after the shape fix, so no other harness regressed. * no-mistakes(review): Prevent working Herdr baselines from falsely confirming delivery * no-mistakes(document): Correct Cursor harness and backend documentation --------- Co-authored-by: ABHISHAKE KUMAR BOJJA <abojja@uvic.ca> Co-authored-by: Amplify Logic AI <lars@sockinator.co> Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>
* fix: raise quota-axi floor to 0.1.25 for Cursor CLI quota awareness Homes on latest main need quota-axi kunchenguid#87 so Desktop-absent CLI machines report a fresh Cursor quota instead of a false sign-in-required. * no-mistakes(document): Update quota floor documentation pointer
…id#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(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>
…id#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 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(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): 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
Absorbs the 16 commits the fork was behind (merge base 07450b9) while preserving every fork-side change. Eight files conflicted; each resolution below is backed by the per-file evidence commands from the /firstmate-sync skill, not by preference. ## Absorbed upstream commits - 1238402 chore: store no-mistakes test evidence in the repo (kunchenguid#2355) - 9823ff8 feat(calm): hide mid-turn working notes by default (kunchenguid#2339) - 88d0f2e feat: add max Calm presentation level (kunchenguid#2334) - db0280f fix(bin): surface buried wake status lines once (kunchenguid#2331) - 5521323 feat(bin): add decline and repair paths for decision holds (kunchenguid#2330) - 81f7020 feat: support Cursor Agent CLI as a primary harness (kunchenguid#2305) - 85cefa9 fix(bin): prevent false Pi watcher alarms during hand-offs (kunchenguid#2304) - 96876db fix(bin): require quota-axi 0.1.25 (kunchenguid#2300) - 4930d2c feat: add Cursor CLI crew harness (kunchenguid#2238) - b0ad61e fix(bin): collapse duplicate supervision wakes (kunchenguid#2287) - b91016f fix: parse decision verbs before status metadata tags (kunchenguid#2280) - b5d430d feat(fmx-respond): consume Relay conversation chains (kunchenguid#2206) - c42cfe0 fix(bin): prevent watcher recovery acknowledgement livelock (kunchenguid#2212) - 614fae6 fix(bin): honor a decision key stated after the verb colon (kunchenguid#2202) - 81ce6dc feat: add deterministic condition-to-action watcher (kunchenguid#2200) - e8c7645 fix: refresh stale Pi instructions after compaction (kunchenguid#2163) The Cursor primary and crew harness, and the `when` condition->action watcher, are new surfaces that auto-merged clean and are absorbed wholesale. ## Conflict resolutions ### atelier vs lavish (fork PR #25, captain's decision 2026-07-13) The fork renamed `lavish-axi` to `atelier-axi` everywhere; upstream still uses the old name. The fork naming is kept through every conflict. Upstream's new prose reintroduced the old name in `.agents/skills/process-event-sources/`, so that skill takes upstream's content (the `when` watcher sections and the condition->action eligibility boundary, kept in full) with `Lavish` -> `Atelier`, `bin/fm-procevent-lavish.sh` -> `bin/fm-procevent-atelier.sh`, and `lavish-axi` -> `atelier-axi` restored. `git grep -i lavish` over the whole tree returns nothing, and `bin/fm-procevent-atelier.sh` is the only adapter script for that surface. `docs/verification/process-event-sources.md` takes upstream's Scope paragraph, which drops the "Lavish is the first adapter" sentence entirely in favour of generic adapter prose plus the `when` adapter's registration-publisher note, so no rename was needed there. The fork's re-verification evidence for `atelier-axi` 0.3.3 elsewhere in that file is untouched. ### AGENTS.md state-layout list - union Keeps upstream's new `.cursor-park-owner` line, keeps `.wedge-verified-*` in the watcher-internals line (fork feature upstream lacks, see below), and drops the fork's `.drain-cursor-<id>` / `.drain-retry-*` line together with `.wake-annotation.lock` because their machinery is superseded (see below). AGENTS.md now diverges from upstream only by the three intentional fork changes: the per-secondmate harness pin, `.wedge-verified-*`, and the atelier naming. ### bin/fm-classify-lib.sh - upstream supersedes fork #21 Fork PR #21 taught `_fm_decision_key` to accept a `[key=<slug>]` token leading the note. Upstream kunchenguid#2202/kunchenguid#2280 implement the same behavior as a strict superset: `_fm_key_at_note_head` accepts the same position, a token deeper in the note stays prose, an invalid slug is rejected rather than folded to `default`, and upstream additionally strips a consumed note-head token from the note and ends verb parsing at the first `[name=value]` tag instead of special-casing `[key=`. Upstream is adopted for both hunks. Verified rather than assumed: the fork's own three regression tests for #21 (`test_key_after_the_colon_files_under_its_real_key`, `test_distinct_after_colon_decisions_do_not_collapse`, `test_malformed_key_token_opens_no_decision`) survive in `tests/fm-wake-drain-open-decisions.test.sh` and all pass against upstream's parser. Fork #21 is superseded, not lost. The fork's `wedge_escalation_deferred` and `FM_WEDGE_WORKING_ESCALATE_SECS_DEFAULT` (PRs #14/#18) sit outside the conflicted hunks and are preserved unchanged. ### bin/fm-wake-lib.sh and bin/fm-wake-drain.sh - upstream supersedes fork #9 This was the incompatible-direction risk, and the evidence resolves it as supersession rather than feature loss. Fork PR #9 added a home-private per-status-file byte cursor (`state/.drain-cursor-<id>`), bounded backlog output with `[truncated]` and "N annotations omitted" markers, durable retry markers (`.drain-retry-{direct,historical}-<id>`), a `.wake-annotation.lock`, and a `fm_wake_print_annotations ''` call on the empty-queue drain path, so that a wake coalescing several status appends could not hide an earlier event. Upstream kunchenguid#2331 solves the same problem with a fleet-wide presentation cursor manifest (`state/.status-presentation-cursor`) owned by fm-classify-lib.sh and a snapshot/acknowledge/commit protocol (`print_status_presentation`). It is strictly stronger on every axis the fork's version covered: - it prints every still-unread line rather than a capped-and-deferred subset, so there is no remainder left to retry (the byte caps the fork bounded were upstream's own pre-existing code, which kunchenguid#2331 deliberately removed); - its cursor is committed transactionally against the presented snapshot and bound to file identity, so rotation and a reused task id are handled; - `print_unread_status_section` runs on every drain including the empty-queue fast path, which is what the fork's extra empty-queue call existed to do; - teardown retirement is integrated through `status_retire_presentation_task`. Keeping both would leave two competing sources of truth for "already presented", which is exactly the one-owner violation the repo's coding guidelines forbid, and the two cursors would disagree because the fork's advances at print time while upstream's commits on acknowledgement. Upstream is therefore taken wholesale for both files, and the orphaned fork artifacts are removed with it. Each file's only fork divergence was #9, confirmed with `git log <merge-base>..main -- <file>`. Upstream's own tests assert the preserved behavior directly: `test_signal_annotation_surfaces_every_unread_note_not_only_the_newest` and `test_unread_output_over_cap_remains_recoverable` in `tests/fm-wake-drain-unread-status.test.sh`. `tests/fm-wake-queue.test.sh` likewise takes upstream, dropping the six fork tests that exercised the removed cursor/retry internals; their behavior is covered by the suites above. ### bin/fm-teardown.sh Resolves to upstream, which is now byte-identical. Upstream's `status_retire_presentation_task` removes both `<id>.status` and `.<id>.open-decisions-cursor`, superseding the fork's explicit cleanup of the latter, and adds `<id>.cursor-session` for the new Cursor harness. The fork's `.drain-cursor-*` / `.drain-retry-*` cleanup is removed because that state no longer exists. ## Fork features confirmed still present - `.wedge-verified-*` stale-wedge deferral (PRs #14/#18): live in `bin/fm-watch.sh`, `bin/fm-supervise-daemon.sh`, and `wedge_escalation_deferred` in `bin/fm-classify-lib.sh`; covered by `tests/fm-watch-triage.test.sh` and `tests/fm-daemon.test.sh`, both green. - atelier-axi naming (PR #25), including the `ATELIER_AXI_MIN` 0.3.3 floor in `bin/fm-bootstrap.sh`. - per-secondmate harness and effort pin (PR #20). - Superseded rather than lost, with the evidence above: fork #21 (decision-key near miss) and fork #9 (coalesced drain backlog). ## Verification - `bin/fm-lint.sh`: clean, ShellCheck 0.11.0 (pinned 0.11.0). - `bin/fm-doc-audience-check.sh`: ok, surfaces=70 local_links=259. - 18 suites run green, covering every conflicted subsystem plus the new upstream surfaces: fm-wake-drain-open-decisions, fm-classify-decision-key, fm-wake-drain-unread-status, fm-wake-drain-open-decisions-cursor, fm-wake-queue, fm-cursor-harness, fm-cursor-primary, fm-procevent-when, fm-procevent, fm-guard-stale-banner, fm-tmux-agent-liveness, fm-decision-hold-lifecycle, fm-send-resolve-key, fm-watch-triage, fm-daemon, fm-teardown, fm-teardown-endpoint-safety. - `tests/fm-session-start.test.sh` fails one case here ("MISSING diagnostic did not appear at all"). This is not a merge regression: a pristine checkout of upstream/main fails the identical case with the identical message in this same environment. The tools the fixture expects to resolve (tasks-axi, quota-axi) live outside the test's isolated BASE_PATH on this machine. Left for CI rather than worked around.
|
CI result: 12 of 13 checks pass (lint, repo invariants, test coverage guard, all behavior suites portable + Herdr, macOS stock-bash snapshot, timing aggregate). The one failure is Note for whoever lands this: merging with a merge commit rather than a squash keeps the upstream ancestry that the next sync's merge-base calculation depends on. |
sync: merge upstream kunchenguid/firstmate main (16 commits)
Absorbs the 16 commits the fork was behind (merge base 07450b9) while
preserving every fork-side change. Eight files conflicted; each resolution
below is backed by the per-file evidence commands from the /firstmate-sync
skill, not by preference.
Absorbed upstream commits
The Cursor primary and crew harness, and the
whencondition->action watcher,are new surfaces that auto-merged clean and are absorbed wholesale.
Conflict resolutions
atelier vs lavish (fork PR #25, captain's decision 2026-07-13)
The fork renamed
lavish-axitoatelier-axieverywhere; upstream still usesthe old name. The fork naming is kept through every conflict. Upstream's new
prose reintroduced the old name in
.agents/skills/process-event-sources/, sothat skill takes upstream's content (the
whenwatcher sections and thecondition->action eligibility boundary, kept in full) with
Lavish->Atelier,bin/fm-procevent-lavish.sh->bin/fm-procevent-atelier.sh, andlavish-axi->atelier-axirestored.git grep -i lavishover the wholetree returns nothing, and
bin/fm-procevent-atelier.shis the only adapterscript for that surface.
docs/verification/process-event-sources.mdtakes upstream's Scope paragraph,which drops the "Lavish is the first adapter" sentence entirely in favour of
generic adapter prose plus the
whenadapter's registration-publisher note, sono rename was needed there. The fork's re-verification evidence for
atelier-axi0.3.3 elsewhere in that file is untouched.AGENTS.md state-layout list - union
Keeps upstream's new
.cursor-park-ownerline, keeps.wedge-verified-*inthe watcher-internals line (fork feature upstream lacks, see below), and drops
the fork's
.drain-cursor-<id>/.drain-retry-*line together with.wake-annotation.lockbecause their machinery is superseded (see below).AGENTS.md now diverges from upstream only by the three intentional fork
changes: the per-secondmate harness pin,
.wedge-verified-*, and the ateliernaming.
bin/fm-classify-lib.sh - upstream supersedes fork #21
Fork PR #21 taught
_fm_decision_keyto accept a[key=<slug>]token leadingthe note. Upstream kunchenguid#2202/kunchenguid#2280 implement the same behavior as a strict
superset:
_fm_key_at_note_headaccepts the same position, a token deeper inthe note stays prose, an invalid slug is rejected rather than folded to
default, and upstream additionally strips a consumed note-head token from thenote and ends verb parsing at the first
[name=value]tag instead ofspecial-casing
[key=. Upstream is adopted for both hunks.Verified rather than assumed: the fork's own three regression tests for #21
(
test_key_after_the_colon_files_under_its_real_key,test_distinct_after_colon_decisions_do_not_collapse,test_malformed_key_token_opens_no_decision) survive intests/fm-wake-drain-open-decisions.test.shand all pass against upstream'sparser. Fork #21 is superseded, not lost.
The fork's
wedge_escalation_deferredandFM_WEDGE_WORKING_ESCALATE_SECS_DEFAULT(PRs #14/#18) sit outside theconflicted hunks and are preserved unchanged.
bin/fm-wake-lib.sh and bin/fm-wake-drain.sh - upstream supersedes fork #9
This was the incompatible-direction risk, and the evidence resolves it as
supersession rather than feature loss.
Fork PR #9 added a home-private per-status-file byte cursor
(
state/.drain-cursor-<id>), bounded backlog output with[truncated]and"N annotations omitted" markers, durable retry markers
(
.drain-retry-{direct,historical}-<id>), a.wake-annotation.lock, and afm_wake_print_annotations ''call on the empty-queue drain path, so that awake coalescing several status appends could not hide an earlier event.
Upstream kunchenguid#2331 solves the same problem with a fleet-wide presentation cursor
manifest (
state/.status-presentation-cursor) owned by fm-classify-lib.sh anda snapshot/acknowledge/commit protocol (
print_status_presentation). It isstrictly stronger on every axis the fork's version covered:
so there is no remainder left to retry (the byte caps the fork bounded were
upstream's own pre-existing code, which fix(bin): surface buried wake status lines once kunchenguid/firstmate#2331 deliberately removed);
bound to file identity, so rotation and a reused task id are handled;
print_unread_status_sectionruns on every drain including the empty-queuefast path, which is what the fork's extra empty-queue call existed to do;
status_retire_presentation_task.Keeping both would leave two competing sources of truth for "already
presented", which is exactly the one-owner violation the repo's coding
guidelines forbid, and the two cursors would disagree because the fork's
advances at print time while upstream's commits on acknowledgement. Upstream is
therefore taken wholesale for both files, and the orphaned fork artifacts are
removed with it. Each file's only fork divergence was #9, confirmed with
git log <merge-base>..main -- <file>.Upstream's own tests assert the preserved behavior directly:
test_signal_annotation_surfaces_every_unread_note_not_only_the_newestandtest_unread_output_over_cap_remains_recoverableintests/fm-wake-drain-unread-status.test.sh.tests/fm-wake-queue.test.shlikewise takes upstream, dropping the six fork tests that exercised the removed
cursor/retry internals; their behavior is covered by the suites above.
bin/fm-teardown.sh
Resolves to upstream, which is now byte-identical. Upstream's
status_retire_presentation_taskremoves both<id>.statusand.<id>.open-decisions-cursor, superseding the fork's explicit cleanup of thelatter, and adds
<id>.cursor-sessionfor the new Cursor harness. The fork's.drain-cursor-*/.drain-retry-*cleanup is removed because that state nolonger exists.
Fork features confirmed still present
.wedge-verified-*stale-wedge deferral (PRs fix: defer stale wedge alarms for active crews #14/fix(supervision): defer stale wedge alarms for actively working crews #18): live inbin/fm-watch.sh,bin/fm-supervise-daemon.sh, andwedge_escalation_deferredinbin/fm-classify-lib.sh; covered bytests/fm-watch-triage.test.shandtests/fm-daemon.test.sh, both green.ATELIER_AXI_MIN0.3.3 floor inbin/fm-bootstrap.sh.near miss) and fork fix(wake): surface every unseen status event in a coalesced drain #9 (coalesced drain backlog).
Verification
bin/fm-lint.sh: clean, ShellCheck 0.11.0 (pinned 0.11.0).bin/fm-doc-audience-check.sh: ok, surfaces=70 local_links=259.upstream surfaces: fm-wake-drain-open-decisions, fm-classify-decision-key,
fm-wake-drain-unread-status, fm-wake-drain-open-decisions-cursor,
fm-wake-queue, fm-cursor-harness, fm-cursor-primary, fm-procevent-when,
fm-procevent, fm-guard-stale-banner, fm-tmux-agent-liveness,
fm-decision-hold-lifecycle, fm-send-resolve-key, fm-watch-triage, fm-daemon,
fm-teardown, fm-teardown-endpoint-safety.
tests/fm-session-start.test.shfails one case here("MISSING diagnostic did not appear at all"). This is not a merge
regression: a pristine checkout of upstream/main fails the identical case
with the identical message in this same environment. The tools the fixture
expects to resolve (tasks-axi, quota-axi) live outside the test's isolated
BASE_PATH on this machine. Left for CI rather than worked around.