feat: support Cursor as a primary harness - #28
Merged
Merged
Conversation
* 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
… attribution guard Brings in the six upstream-only commits since the 4930d2c merge base, with upstream SHAs preserved by this --no-ff merge rather than a rebase or squash: 96876db fix(bin): require quota-axi 0.1.25 (kunchenguid#2300) 85cefa9 fix(bin): prevent false Pi watcher alarms during hand-offs (kunchenguid#2304) 81f7020 feat: support Cursor Agent CLI as a primary harness (kunchenguid#2305) 5521323 feat(bin): add decline and repair paths for decision holds (kunchenguid#2330) db0280f fix(bin): surface buried wake status lines once (kunchenguid#2331) 88d0f2e feat: add max Calm presentation level (kunchenguid#2334) Two files conflicted. bin/fm-spawn.sh takes upstream's version outright. The only fork-side change to that file since the merge base was the Cursor attribution guard, which this fork is deliberately dropping, so upstream's file is the whole intended result: the Cursor --secondmate refusals are lifted and the per-task commit-msg hook install is gone. The guard's remaining wiring in teardown, tests, and documentation is removed in a follow-up commit on this branch. docs/architecture.md preserves both sides. The fork's Option-A arm-layer ownership stand-down contract stays verbatim, and upstream's Cursor clause is folded into the primary-harness list in the same paragraph. Every other dual-touched file was reviewed rather than trusted to auto-merge. The Option-A supervision policy survives intact: fm_wake_append_resilient and its callers and tests are unchanged, and bin/fm-watch.sh, bin/fm-watch-arm.sh, and bin/fm-push-transition-lib.sh are byte-identical to the fork's prior main. docs/watcher-continuity.md is the fork's file plus exactly upstream's three Cursor and unread-status additions. bin/fm-session-lock-lib.sh keeps the fork's cursor-agent identity match rather than a bare cursor match, and bin/fm-harness.sh is untouched, so the cursor-like-filename reject fix stands.
Quidge
force-pushed
the
fm/cursor-parity-sync
branch
from
August 14, 2026 00:13
c24953f to
4440103
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Reconcile the fork (Quidge/firstmate = origin/main) with upstream (kunchenguid/firstmate
= upstream/main): sync in the 6 upstream-only commits via a merge commit, adopt
Cursor-as-primary, and DROP the fork's Cursor attribution guard entirely. This is
firstmate's OWN shared tracked material, so firstmate-coding-guidelines applies.
STEP 1 - Sync merge, preserving upstream SHAs.
On branch fm/cursor-parity-sync,
git merge --no-ff upstream/mainbrings in exactly sixupstream-only commits: 96876db (quota floor), 85cefa9 (Pi watcher-handoff), 81f7020
(Cursor-primary), 5521323 (decision-hold decline/repair), db0280f (buried wake-status),
88d0f2e (max Calm). Upstream SHAs MUST be preserved - never rebase, squash, or
cherry-pick. The --no-ff merge commit is what preserves them and must stay intact through
the whole pipeline. If any pipeline mechanic would rewrite that merge history, stop rather
than proceeding.
STEP 2 - Resolve merge conflicts (two files).
divergence in this file since the merge base was the attribution guard (3 hunks: header
comment, hook creation, hook activation), so taking upstream drops only the guard.
The result is byte-identical to upstream/main:bin/fm-spawn.sh.
documentation (0b138d9) is kept verbatim, with upstream's Cursor-primary clause folded
into the same primary-harness list.
Every other dual-touched file was semantically reviewed, not trusted to auto-merge:
.agents/skills/harness-adapters/SKILL.md, AGENTS.md, README.md, bin/fm-session-lock-lib.sh,
bin/fm-teardown.sh, bin/fm-wake-lib.sh, docs/verification/runtime-backends.md,
docs/watcher-continuity.md.
STEP 3 - Fully remove the Cursor attribution guard.
Taking upstream's fm-spawn.sh removes the guard's install; the remaining wiring is removed
in a follow-up commit stacked after the merge:
to delete state/.cursor-git-hooks; the separate .cursor-session cleanup is untouched).
spawn fakebin helper.
remove the paragraphs describing the hook as current behavior, reconciled against
upstream's rewritten Cursor section. The fork-local probe-transcript pointer survives.
Acceptance: git grep for cursor-git-hooks, @cursor.com, @cursor.sh, core.hooksPath, and
remove_cursor_artifacts must all be empty, with no dangling reference to the removed hook
script or tests.
STEP 4 - KEEP untouched (do NOT revert or remove).
bin/fm-watch-arm.sh, bin/fm-push-transition-lib.sh, docs/architecture.md,
docs/watcher-continuity.md. Upstream's 85cefa9/db0280f must NOT revert it to upstream's
louder-failure verdict; the attach-only benign stand-down must survive.
cursor-agent filename matching even if an adjacent identity file conflicts.
all 8 general fork skills (lavish, aside, rebasing-adapted-skill, writing-for-agents,
tailscale-serve, etc.). These stay as intentional fork divergence; NOT upstreamed.
STEP 5 - Adopt Cursor-as-primary (arrives with 81f7020).
No extra work beyond a clean merge. These files must be present:
.cursor/hooks.json, bin/fm-turnend-guard-cursor.sh, bin/fm-sessionstart-cursor.sh,
bin/fm-hook-host-lib.sh, docs/supervision-protocols/cursor.md,
tests/fm-cursor-primary.test.sh, tests/fm-cursor-primary-live-e2e.test.sh.
The Cursor --secondmate refusals in bin/fm-spawn.sh, bin/fm-control-lib.sh, and
bin/fm-remote-secondmate-control.sh must be lifted (only muse remains refused).
IN-SCOPE CORRECTION beyond the guard.
tests/fm-cursor-harness.test.sh's header claimed Cursor "is a crewmate/scout adapter only
and refuses a secondmate launch". Upstream never updated that comment when kunchenguid#2305 lifted the
refusal, and it has no backing assertion, so it directly contradicts Step 5's acceptance
criterion. Corrected rather than carried in.
ACCEPTED CONSEQUENCE of dropping the guard.
Cursor-authored commits can again carry
Co-authored-by: Cursor <cursoragent@cursor.com>,which AGENTS.md forbids. Upstream implements no equivalent protection. This is the
captain's explicit decision, so the guard is removed cleanly rather than half-wired, and no
replacement policy documentation was invented. If this surfaces as an ask-user finding it
must be escalated to firstmate, not answered by the worker.
VALIDATION EXPECTATIONS.
The full portable regression set runs in the pipeline, including the new
tests/fm-cursor-primary.test.sh and the updated secondmate/session-lock/turn-end/pretool
tests. The opt-in live Cursor-primary smoke
(FM_CURSOR_PRIMARY_LIVE_E2E=1 bin/fm-test-run.sh tests/fm-cursor-primary-live-e2e.test.sh)
is run where feasible but is NOT a separate gating eval; the captain trusts upstream's
testing and does not want a throwaway-home evaluation gating the land.
LIVE CURSOR-PRIMARY SMOKE: RUN AND FULLY PASSED on this host (not a gating eval).
FM_CURSOR_PRIMARY_LIVE_E2E=1 bin/fm-test-run.sh tests/fm-cursor-primary-live-e2e.test.sh
against real cursor-agent 2026.08.11-e8db854: 7 of 7 cases passed, failed=0. Covered the
session lock taken by the Cursor process itself, run-tier session start completing every
stage, sessionStart additional_context reaching model context before the first turn, the
stop-hook park delivering a real watcher wake as one follow-up, the park owning exactly one
arm cycle with a live watcher beacon, the supersession baton standing an older park down
after the next stop claim, and away-mode escalation delivery being confirmed and processed.
PORTABLE SUITE RESULT: 147 scripts, 4 failed, 32 gate-skipped. One of those four
(tests/fm-tmux-agent-liveness.test.sh) exposed the real merge defect fixed above; the
remaining three are accounted for below.
tests/fm-watcher-lock.test.sh fails on "arm did not exit with HUP status (got 124)". This
file sits in the Option-A / upstream-watcher overlap zone, so it was NOT assumed
environmental; it was settled with the same differential used for Calm, run strictly
serially because concurrent load was the first hypothesis. Three runs per tree:
HEAD (merged) -> fails 3/3
origin/main -> fails 3/3 (fork main, no upstream merge)
upstream/main -> fails 3/3 (upstream alone, no fork content)
It fails identically and deterministically on BOTH unmerged parents, so it is not a merge
regression. Root cause is the same uutils-coreutils host issue: /usr/bin/timeout is uutils
coreutils 0.8.0, and 124 is timeout's own timed-out exit code, where the assertion expects a
signal-derived HUP status (GNU timeout reports 128+signal). The Option-A cases themselves -
the benign attach-only stand-down and the owning arm's typed failure - pass.
Correction to an earlier note: a single standalone run of this test reported failed=0, and
that observation did not reproduce; the controlled 9-run differential above supersedes it.
The regression question is answered by the differential, not by that one pass.
KNOWN ENVIRONMENTAL FAILURES ON THIS HOST - do NOT chase, do NOT "fix" here.
fake Cursor process". This host has no cc/gcc/clang. It is upstream's harness-dependent
structural-identity test and runs normally on CI's ubuntu-latest/macos-latest runners,
which ship compilers. Not a code defect and not a regression from this change.
a duplicate captain answer". Verified pre-existing and NOT merge-induced: exporting each
parent tree with
git archiveinto a clean temp dir and running the test there failsidentically on origin/main alone (no upstream merge) and on upstream/main alone (no fork
content). Upstream's own new max-Calm case passes here. The fork's only Calm divergence
since the merge base is a single mock stub (getMarkdownTransformers) in the test file.
host (tracked fork issue, unrelated to this change): /usr/bin/sleep is a symlink into
the uutils multicall binary, so any fixture symlink named cursor-agent exits immediately
with "coreutils: unknown program 'cursor-agent'" and its pane never renders. Unmerged
upstream/main fails on this host the same way, at its first symlink fixture. Do not
chase or "fix" that here.
second uutils manifestation: /usr/bin/timeout is uutils coreutils 0.8.0 and 124 is its own
timed-out exit code where the assertion expects a signal-derived HUP status (GNU timeout
reports 128+signal). Settled by the serial 3-runs-per-tree differential recorded above:
fails 3/3 on HEAD, 3/3 on origin/main, and 3/3 on upstream/main, so it is deterministic on
both unmerged parents and not a merge regression. The Option-A cases pass.
The suite in general may need TMPDIR on a large disk.
These four are proven environmental on this host, each by differential rather than
assumption. If the test gate flags any of them, they must NOT be "fixed" here and must NOT be
force-passed: present this evidence and escalate as a decision instead.
REAL MERGE DEFECT FOUND AND FIXED in tests/fm-tmux-agent-liveness.test.sh (NOT environmental).
Separately from the uutils issue above, the auto-merge put two fixture strategies on one
path. The fork's make_runner already creates $LAB/bin/cursor-agent as an
exec -awrapper;upstream's new Cursor composer cases then ran
ln -s "$SLEEP_BIN" "$LAB/bin/cursor-agent"over it. ln refuses an existing path on every platform, so upstream's symlink was never
created and its positive case silently asserted against the wrapper, which can never
classify as Cursor (exec -a leaves comm as the real binary's name and puts the fixture path
only in argv[0], while fm_tmux_pane_is_cursor reads comm, which only a symlink populates).
This reaches CI, where the symlink would otherwise work. Fixed by moving the symlink
fixtures to their own directory and pointing the three composer cases at them, leaving the
alive-set case on the wrapper it needs. This is exactly the "auto-merge is not
semantic-merge" review the task required, and it does not weaken 864dcda, whose change was
scoped to detect_own() in bin/fm-harness.sh and not to the tmux pane classifier.
DELIVERY.
One sync+reconciliation PR into Quidge/firstmate main via the no-mistakes pipeline. The
captain is the merge authority (yolo off); the worker never merges.
What Changed
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ **Rebase** - passed
✅ **Review** - completed
✅ **Document** - passed
✅ **Lint** - passed
✅ **Push** - passed