feat(harness): add verified Cursor worker adapter (Cursor CLI + Grok 4.5) - #6
Merged
Merged
Conversation
…4.5)
Adds `cursor` (Cursor CLI `agent` 2026.07.16-899851b) as a verified WORKER
adapter, after empirical verification on 2026-07-19. Worker only: primary
support was not verified and is not claimed, the mirror of the Kimi
primary-only boundary. Kimi stays primary-only; no Kimi worker support here.
Verification found two defects in shared composer monitoring that would have
made an adapter monitoring cannot trust, both fixed in the shared owners:
- Reverse-video cursor cell. cursor draws the terminal cursor as SGR 7 over the
idle placeholder's FIRST character. Reverse video is neither dim/faint nor a
dark foreground, so fm_composer_strip_ghost kept it and an idle composer
reduced to a lone "A" -> `pending`, deferring every away-mode escalation
forever. A plain FM_COMPOSER_IDLE_RE does not fix this, because the idle regex
was only matched against ghost-stripped content; it is now matched against the
plain row too, which is styling-independent.
- #{cursor_y} does not point at cursor's composer (it parks the cursor in the
bottom status area, observed 5 rows off). Reading that row found an empty
status line, so a composer holding real unsubmitted text classified as
`empty` - a FALSE-EMPTY, the dangerous direction, since the away-mode injector
picks targets by emptiness and would type over pending input. The tmux path
now locates the composer structurally, matching herdr's existing scan.
Other integration surfaces:
- fm-spawn: launch template (--yolo autonomy, --workspace pinned to the task
worktree, and deliberately no -w so cursor never allocates a second worktree),
plus a per-task gitignored .cursor/hooks.json `stop` hook verified to fire
once per turn with no extra hook-trust grant.
- Effort axis: cursor has no effort flag - effort is a SUFFIX on the model id -
so cursor_model_with_effort folds it in. xhigh/max cap at high, an explicit
tiered model is never retiered, and -fast variants are never implicit.
- fm-harness: CURSOR_AGENT=1 detection, tested BEFORE CLAUDECODE because cursor
does not clear an inherited CLAUDECODE=1 and a cursor worker under a
claude-hosted firstmate would otherwise be steered with claude's vocabulary.
- Busy signature `ctrl+c to stop` in both owners. The spinner VERB is
deliberately not matched: it flips Working -> Running mid-turn, so matching it
reads a tool-executing pane as idle.
- tmux liveness: cursor's wrapper execs node, but the versioned cursor-agent
bundle path survives exec -a in argv, so a `node` COMM resolves to `alive`.
Any other bare node stays `unknown`, never dead; pi's gap is unchanged.
Backends reviewed rather than assumed: tmux verified; herdr composer-safe by
construction (structural scan + shared idle default) with liveness unverified;
zellij/orca/cmux not exercised with cursor. Recorded in docs/cursor-harness.md.
Also documents that cursor executes claude-format hooks (.claude/settings*.json,
Stop -> stop), so cursor must never be launched from the firstmate primary
checkout, and that --model mutates the account-global default.
A live fm-spawn dispatch was NOT run: it allocates a real pooled worktree and
writes live fleet state outside the task worktree. Every underlying behaviour
was verified by raw launches and the integrated path (launch, turn-end hook,
composer, liveness) was confirmed together on a real pane; the first supervised
fm-spawn --harness cursor run remains firstmate's gate.
Evidence: docs/cursor-harness.md. Regressions: tests/fm-cursor-adapter.test.sh
(20 assertions). bin/fm-lint.sh clean; composer/tmux/herdr/orca/cmux suites pass.
…itecture, and indexes
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
Verify Cursor CLI (the
agentbinary, 2026.07.16-899851b) with Cursor Grok 4.5 as a fully supervised Firstmate WORKER harness adapter, then implement the adapter only where every required behavior was empirically proven. The captain authorized this so Cursor/Grok can become the primary non-Codex volume pool; Codex is nearly exhausted, so no Codex model or Pi/Codex helper was used.Deliberate decisions a reviewer reading only the diff would not know:
WORKER ONLY, intentionally. Primary-orchestrator support (session-start nudge, turn-end guard, PreToolUse seatbelt, watcher supervision protocol) was NOT verified and is deliberately NOT claimed. This mirrors the existing Kimi primary-only boundary in reverse. Kimi deliberately stays primary-only; this change does not add Kimi worker support.
Two defects in SHARED composer monitoring were found during verification and fixed in the shared owners rather than worked around per-harness. (a) Cursor draws the terminal cursor as a reverse-video cell (SGR 7) over the idle placeholder's first character; reverse video is neither dim/faint nor dark-foreground, so fm_composer_strip_ghost kept it and an idle composer reduced to a lone 'A' and classified as 'pending', which would defer every away-mode escalation forever. The obvious fix (an FM_COMPOSER_IDLE_RE override) does NOT work, because the idle regex was only matched against ghost-stripped content; it is now also matched against the plain row, which is styling-independent. (b) tmux's #{cursor_y} does not point at Cursor's composer (Cursor parks the cursor in its bottom status area, observed 5 rows off), so a composer holding real unsubmitted text classified as 'empty' - a FALSE-EMPTY, the dangerous direction, because the away-mode injector picks injection targets by emptiness and would type over pending input. Fixed with a structural composer-row scan mirroring herdr's existing approach, which is why herdr was never affected. Both are regression-covered against verbatim captured pane rows.
Cursor has NO effort flag - reasoning effort is a SUFFIX on the model id - so the effort axis folds into the model via cursor_model_with_effort instead of emitting a flag. xhigh/max cap at high per the harness-adapters effort-fallback rule rather than being dropped silently; an already-tiered or bracket-parameterized model id is never retiered; -fast variants are a deliberate separate cost/speed choice and are never selected implicitly.
The launch template deliberately omits -w/--worktree. Cursor worktree allocation is opt-in, and Firstmate already owns the isolated task worktree, so passing it would create a second one. --workspace pins the root instead. Verified: ~/.cursor/worktrees gained no entry across every launch.
Busy signature is the footer hint 'ctrl+c to stop', and the spinner VERB is deliberately NOT matched: it flips 'Working' to 'Running' mid-turn, so matching the verb reads a tool-executing pane as idle and trips premature stale detection.
CURSOR_AGENT=1 detection is deliberately ordered BEFORE the existing CLAUDECODE check, because Cursor does not clear an inherited CLAUDECODE=1; without that ordering a cursor worker spawned from a claude-hosted firstmate misreports as claude and gets steered with claude's interrupt/exit/resume vocabulary.
tmux liveness: Cursor's wrapper execs node so pane_current_command is 'node'. The existing code deliberately declined argv introspection for pi because a bare node is unattributable. Cursor is different - the versioned cursor-agent bundle path survives exec -a in argv - so argv inspection is applied narrowly to the node COMM only. Any other bare node still returns 'unknown', never 'dead', preserving the rule that a wrong 'dead' must never respawn over a live agent. pi's behavior is unchanged.
Backends were reviewed per the compatibility rule rather than assumed: tmux verified; herdr composer-safe by construction (structural scan + shared idle default) with its liveness path unverified for cursor; zellij/orca/cmux not exercised with cursor at all. Only tmux is claimed as verified.
Documented but not fixed: Cursor executes claude-format hooks (.claude/settings*.json, mapping Stop to stop), so cursor must never be launched from the firstmate primary checkout; and --model mutates the account-global default in ~/.cursor/cli-config.json.
Known incomplete acceptance criterion, deliberate: a live end-to-end fm-spawn dispatch was NOT run. fm-spawn calls 'treehouse get', which allocates a real pooled worktree and writes live fleet state outside the task worktree - something this crewmate must not cause. Every underlying behavior was verified by raw launches, and the integrated path (launch with folded model id, turn-end hook firing, composer empty, liveness alive) was confirmed together on a real pane. This is recorded as limitation 2 in docs/cursor-harness.md and left as firstmate's gate.
config/crew-dispatch.json is captain-private and was deliberately NOT edited; the proven profile values are reported in docs/cursor-harness.md for firstmate to apply after merge.
What Changed
bin/fm-spawn.sh,bin/fm-harness.sh, backend scripts): launch template with--workspacepinning (no-wworktree), effort folded into the model id viacursor_model_with_effort(xhigh/max cap at high),CURSOR_AGENT=1detection ordered before theCLAUDECODEcheck, busy detection on thectrl+c to stopfooter hint, and narrow argv-based liveness inspection for Cursor'snodewrapper (other barenodepanes still reportunknown, neverdead). Worker-only by design — primary-orchestrator support is not claimed.#{cursor_y}, eliminating a false-empty that could let the away-mode injector type over pending input. The structural→scan is scoped to identified cursor panes per review feedback. Both fixes are regression-covered against captured pane rows intests/fm-cursor-adapter.test.sh.docs/cursor-harness.md(including known limitations: Cursor executes claude-format hooks,--modelmutates the account-global default, and the live end-to-end fm-spawn dispatch is deliberately left as firstmate's gate), plus updates to configuration, architecture, tmux-backend docs, and the harness-adapters skill. Only the tmux backend is claimed as verified.Note: the pipeline's Test stage reported a failure attributed on re-check to a watcher-lock timeout flake; no code changes resulted.
Risk Assessment
✅ Low: The confirmed cross-harness composer-hijack regression from round 1 is now fixed with positive cursor-pane identification that fails safe toward the pre-existing cursor_y behavior, is regression-covered for both the classification and fm-send consequences, and the rest of the branch was already verified intent-conformant with the one remaining tradeoff (generic busy phrase) explicitly accepted by the user.
Testing
Completed 1 recorded test check.
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 3 issues found → auto-fixed ✅
bin/fm-tmux-lib.sh:127- fm_tmux_cursor_composer_row is applied unconditionally to every tmux pane: fm_tmux_composer_state has no harness scoping, so on a non-cursor pane (claude/codex/grok/pi) any visible line matching '^[[:space:]]*→ ' — e.g. an arrow-prefixed bullet in model output or tool output — redirects composer classification from #{cursor_y} to that output line. The redirected row then reads as pending/unknown, which (a) defers away-mode escalations indefinitely (the same false-pending wedge class this change fixes for cursor) and (b) makes fm_tmux_submit_enter_core retry Enter and return a false 'pending' swallow verdict to fm-send, which can cause duplicated steers. The cited herdr precedent is narrower: it only promotes bordered rows and ❯/› prompt-glyph rows, which don't appear in ordinary output text. Consider scoping the scan by harness (thread an expected-label like the other backends) or constraining the → row to sit directly above cursor's status rows. Flagged ask-user because the unconditional design appears deliberate ('Harnesses that draw no "→ " composer are untouched' assumes → never appears in other harnesses' output).bin/fm-watch.sh:121- The new busy alternation 'ctrl+c to stop' is a generic phrase common in dev-server/tool output ("Press CTRL+C to stop"); fm_pane_is_busy and fm-watch match it case-insensitively over the last 6 non-blank lines, so tool output lingering near the pane bottom after a turn ends can read an idle pane as busy and delay stale/turn-end handling. Same accepted-risk class as the existing 'Working...' alternation; noting the tradeoff only.bin/fm-harness.sh:3- The usage comment at the top of bin/fm-harness.sh still enumerates the own-harness outputs as claude|codex|opencode|pi|grok|kimi|unknown, but detect_own now also returns 'cursor'. Mechanical comment-accuracy fix.🔧 Fix: scope cursor composer-row scan to identified cursor panes
✅ Re-checked - no issues remain.
command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"🔧 Fix: confirm watcher-lock timeout flake; tests pass, no changes
1 error still open:
command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.