Skip to content

feat(harness): add verified Cursor worker adapter (Cursor CLI + Grok 4.5) - #6

Merged
Amplify-Logic merged 3 commits into
mainfrom
fm/firstmate-cursor-agent-adapter-verify-c2
Jul 19, 2026
Merged

feat(harness): add verified Cursor worker adapter (Cursor CLI + Grok 4.5)#6
Amplify-Logic merged 3 commits into
mainfrom
fm/firstmate-cursor-agent-adapter-verify-c2

Conversation

@Amplify-Logic

Copy link
Copy Markdown
Owner

Intent

Verify Cursor CLI (the agent binary, 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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.

  10. 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

  • Added a Cursor CLI worker harness adapter (bin/fm-spawn.sh, bin/fm-harness.sh, backend scripts): launch template with --workspace pinning (no -w worktree), effort folded into the model id via cursor_model_with_effort (xhigh/max cap at high), CURSOR_AGENT=1 detection ordered before the CLAUDECODE check, busy detection on the ctrl+c to stop footer hint, and narrow argv-based liveness inspection for Cursor's node wrapper (other bare node panes still report unknown, never dead). Worker-only by design — primary-orchestrator support is not claimed.
  • Fixed two shared composer-monitoring defects surfaced during verification: the idle regex is now also matched against the plain (non-ghost-stripped) row so Cursor's reverse-video cursor cell no longer classifies an idle composer as pending, and composer-row detection uses a structural scan (mirroring herdr) instead of trusting tmux #{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 in tests/fm-cursor-adapter.test.sh.
  • Documented the adapter in docs/cursor-harness.md (including known limitations: Cursor executes claude-format hooks, --model mutates 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.

  • Outcome: ⚠️ 1 error across 2 runs (50m22s)

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.

⚠️ **Test** - 1 error
  • 🚨 tests failed with exit code 1
  • 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:

  • 🚨 tests failed with exit code 1
  • 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.

…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.
@Amplify-Logic
Amplify-Logic merged commit e07b7cc into main Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant