Skip to content

feat(cursor): add Cursor Agent as a verified crewmate and primary harness#400

Closed
vitalNohj wants to merge 14 commits into
kunchenguid:mainfrom
vitalNohj:fm/cursor-agent-bin-54
Closed

feat(cursor): add Cursor Agent as a verified crewmate and primary harness#400
vitalNohj wants to merge 14 commits into
kunchenguid:mainfrom
vitalNohj:fm/cursor-agent-bin-54

Conversation

@vitalNohj

Copy link
Copy Markdown

Intent

Fix firstmate's Cursor crewmate launch so it never depends on the ambiguous bare 'agent' binary name. Cursor installs both 'agent' and 'cursor-agent' (often the same target), and Grok (or anything else) can also claim 'agent' on PATH, so today's bare 'agent --force' launch template in bin/fm-spawn.sh can silently invoke the wrong CLI and hijack Cursor spawns. This change makes launch match the robustness of primary detection (which already keys on cursor-agent / CURSOR_AGENT). Decisions/tradeoffs: (1) Added a resolver fm_cursor_launch_bin in bin/fm-cursor-hook-lib.sh that ALWAYS prefers cursor-agent when on PATH, falls back to bare 'agent' only when cursor-agent is absent AND agent is verified to be Cursor (CURSOR_INVOKED_AS env, a resolved cursor-agent symlink path, or a --version fingerprint), and returns non-zero when neither resolves so the spawn aborts rather than launching the wrong tool. The --version fingerprint runs only on the rare fallback path (cursor-agent absent). (2) bin/fm-spawn.sh's cursor launch template now uses a CURSORBIN placeholder resolved at spawn time; if resolution fails the spawn exits with a clear error. (3) Scope kept deliberately small: launch resolution + tests + docs/facts only; the primary Cursor followup_message stop-guard is intentionally NOT implemented here. (4) Updated colocated tests in tests/fm-cursor-harness.test.sh with three resolver unit tests (prefer cursor-agent over a colliding non-Cursor agent = the Grok collision, verified-agent fallback, and refusal of a non-Cursor agent) plus an end-to-end spawn test asserting cursor-agent is launched even when a non-Cursor agent is earlier on PATH; the resolver tests explicitly neutralize the ambient CURSOR_INVOKED_AS/CURSOR_AGENT env so they are hermetic. (5) Updated harness-adapters SKILL.md and docs/supervision-protocols/cursor.md to state cursor-agent --force as the preferred launch form. This work is based on origin/fm/cursor-primary-detect (the open Cursor adapter PR #384), not bare main, since main does not yet have the Cursor adapter.

What Changed

  • Add Cursor Agent as a verified harness across the fleet: bin/fm-harness.sh detects a Cursor primary session (keying on cursor-agent/CURSOR_AGENT), bin/fm-spawn.sh and bin/fm-lock.sh recognize cursor as a crewmate/secondmate adapter, and supporting scripts (fm-bootstrap, fm-teardown, fm-watch, fm-supervision-instructions, tmux backend) plus docs and harness-adapters gain Cursor entries.
  • Introduce bin/fm-cursor-hook-lib.sh with a launch resolver (fm_cursor_launch_bin) that always prefers cursor-agent on PATH, falls back to a bare agent only when it is verified to be Cursor via a resolved-symlink path or --version fingerprint, and returns non-zero otherwise; fm-spawn.sh's Cursor launch template now resolves a __CURSORBIN__ placeholder at spawn time and aborts on failure instead of invoking an ambiguous bare agent. The lib also installs/teardowns a Cursor turn-end stop hook scoped to firstmate-owned .cursor/hooks.json entries.
  • Add tests/fm-cursor-harness.test.sh (16 tests) covering the three resolver cases (prefer cursor-agent over a colliding non-Cursor agent, verified-agent fallback, refusal of a non-Cursor agent) and an end-to-end spawn asserting cursor-agent --force launches even with a non-Cursor agent earlier on PATH; extend the session-start, supervision-instructions, and gotmp tests and gitignore local Serena config.

Risk Assessment

✅ Low: The change is additive, tightly scoped to the new Cursor adapter, heavily covered by new tests, and the substantive concerns from earlier review rounds are already fixed in HEAD, leaving only a documented low-probability tradeoff.

Testing

The pre-run baseline suite (all tests/*.test.sh) already passed; I re-ran the colocated tests/fm-cursor-harness.test.sh (16/16 pass) and then produced product-level CLI evidence beyond the assertions: a resolver transcript proving cursor-agent wins over a colliding Grok agent, that a --version-verified bare agent is an accepted fallback, and that a non-Cursor agent is refused so the spawn aborts; plus an end-to-end fm-spawn transcript showing the actual launched command is cursor-agent --force --workspace even with a non-Cursor agent shadowing it earlier on PATH. This is a CLI/launch-behavior change with no UI surface, so evidence is CLI transcripts rather than screenshots. Working tree left clean.

Evidence: Cursor launch resolver — three-scenario CLI transcript
==================================================================
 fm_cursor_launch_bin resolver — Cursor launch binary disambiguation
 (Grok / anything can claim bare 'agent' on PATH; cursor-agent must win)
==================================================================

## PATH contains BOTH a non-Cursor 'agent' (Grok) and 'cursor-agent':
$ agent --version   ->  grok agent 1.2.3
$ cursor-agent --version   ->  cursor-agent 2026.07.08-0c04a8a

------------------------------------------------------------------
Scenario 1 — collision: bare 'agent' (Grok) AND cursor-agent both present
  Expect: resolver prefers unambiguous cursor-agent
------------------------------------------------------------------
resolved launch binary : cursor-agent   (exit 0)
RESULT: PASS — Grok's 'agent' can never hijack the spawn

------------------------------------------------------------------
Scenario 2 — no cursor-agent; bare 'agent' IS verified Cursor (--version)
  Expect: resolver falls back to the verified bare 'agent'
------------------------------------------------------------------
resolved launch binary : agent   (exit 0)
RESULT: PASS — verified Cursor 'agent' accepted as fallback

------------------------------------------------------------------
Scenario 3 — no cursor-agent; only a non-Cursor 'agent' (Grok)
  Expect: resolver REFUSES (non-zero, prints nothing) so spawn aborts
------------------------------------------------------------------
resolved launch binary : ''   (exit 1)
RESULT: PASS — refuses to launch the wrong CLI

Done.
Evidence: End-to-end fm-spawn launch command under Grok PATH collision

Actual launch command sent to the crewmate window: cursor-agent --force --workspace "$(pwd)" VERDICT: launched via cursor-agent (correct), NOT the colliding bare Grok 'agent'.

PATH lookup order (bare 'agent' shadows first):
  which agent        -> resolves to a Grok stub (/var/folders/dw/l4d7vz_n1wz4lgm277d8g55m0000gn/T//cursor-evidence.WNjtcJ/spawn/fake/fakebin/agent)
  which cursor-agent -> /var/folders/dw/l4d7vz_n1wz4lgm277d8g55m0000gn/T//cursor-evidence.WNjtcJ/spawn/fake/fakebin/cursor-agent

spawn: SUCCEEDED

Actual launch command sent to the crewmate window (tmux send-keys payload):
-------------------------------------------------------------------------
cursor-agent --force --workspace "$(pwd)"
-------------------------------------------------------------------------

VERDICT: launched via cursor-agent (correct), NOT the colliding bare Grok 'agent'.

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ℹ️ bin/fm-cursor-hook-lib.sh:44 - In fm_cursor_agent_is_cursor (bin/fm-cursor-hook-lib.sh:44), the fallback trusts the ambient CURSOR_INVOKED_AS env marker to decide whether the candidate bare agent on PATH is Cursor. That marker describes the current process's ancestry, not the candidate binary. If fm-spawn ever runs with CURSOR_INVOKED_AS set (inherited/leaked) while cursor-agent is absent and the bare agent is actually Grok, the resolver returns agent and launches the wrong CLI - the exact failure the fix aims to prevent. Reachability is low (a live Cursor context normally puts cursor-agent on PATH, so the fallback isn't reached), and the path/version fingerprint checks that follow are binary-specific; consider dropping the env short-circuit and relying only on the resolved-path and --version checks, which actually interrogate the candidate binary.

🔧 Fix: drop ambient CURSOR_INVOKED_AS trust from cursor launch resolver
1 warning still open:

  • ⚠️ bin/fm-spawn.sh:284 - The secondmate positional-argument allow-list ''|claude|codex|opencode|pi|grok omits the newly verified 'cursor' adapter. A bare positional 'cursor' (e.g. fm-spawn.sh <id> cursor --secondmate) therefore falls through to the *) arm and is misinterpreted as a firstmate-home path (FIRSTMATE_HOME=cursor), causing the spawn to fail, whereas every other verified adapter is recognized as a harness override. The --harness cursor --secondmate flag form is unaffected. Add 'cursor' to the case list at line 284 to match the other verified adapters.

🔧 Fix: add cursor to secondmate positional harness allow-list
1 warning still open:

  • ⚠️ bin/fm-lock.sh:39 - The refactor of looks_like_harness narrowed the bare-interpreter match from the previous substring test (case "$comm" in node|python) to an exact base-name enumeration (node|node[0-9]|python|python[0-9]|deno|bun). This affects ALL harnesses' lock detection, not just cursor: when a harness CLI runs under an interpreter whose command name is nodejs (still the binary name on some Debian/Ubuntu-derived systems) the old code matched via node but the new list does not, so harness_pid/holder_alive would fail to recognize the lock holder. fm-harness.sh detect_own still uses the looser node*|python* glob (matches nodejs), so the two now disagree - fm-harness would identify the harness while fm-lock would not, potentially treating a live lock holder as not-a-harness. Reachability is low on macOS (comm is node), but it is a genuine regression versus prior behavior. Consider adding nodejs to the interpreter case to preserve parity with fm-harness.sh and the prior substring match.

🔧 Fix: restore nodejs interpreter match in fm-lock harness detection
1 info still open:

  • ℹ️ bin/fm-cursor-hook-lib.sh:152 - Install and teardown are asymmetric for an untracked, dev-local .cursor/hooks.json. On install, fm_cursor_install_turnend deliberately preserves a pre-existing untracked hooks.json by jq-merging firstmate's stop entry into it (keeping the dev's own hooks). On teardown, fm_cursor_teardown removes the entire untracked hooks.json rather than only un-merging firstmate's entry, so any dev-local hook config that was merged into is destroyed. The behavior is documented in the teardown comment, and the probability is very low in practice (crewmate worktrees are ephemeral treehouse pool checkouts that would rarely carry a pre-existing untracked hooks.json), but the merge-vs-remove asymmetry means the one path that bothers to preserve dev content on the way in silently discards it on the way out. Relatedly, if a pool worktree is reused without a clean teardown, the append-based jq merge is non-idempotent and would accumulate duplicate fm-turn-end stop entries (harmless, just repeated touches).
✅ **Test** - passed

✅ No issues found.

  • 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"
  • bash tests/fm-cursor-harness.test.sh — all 16 tests pass, including the three resolver unit tests (test_cursor_launch_bin_prefers_cursor_agent, ..._falls_back_to_verified_agent, ..._rejects_non_cursor_agent) and the e2e test_cursor_spawn_uses_cursor_agent_not_bare_agent
  • Resolver CLI transcript: sourced bin/fm-cursor-hook-lib.sh and ran fm_cursor_launch_bin with (1) both a Grok agent and cursor-agent on PATH → resolved cursor-agent, (2) only a --version-verified Cursor agent → resolved agent, (3) only a non-Cursor Grok agent → refused with exit 1 and empty output; ambient CURSOR_INVOKED_AS/CURSOR_AGENT neutralized for hermeticity
  • End-to-end spawn: ran the real bin/fm-spawn.sh via the test harness with a colliding non-Cursor agent earlier on PATH and captured the tmux send-keys payload → launch command was cursor-agent --force --workspace "$(pwd)", never the bare Grok agent
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

vitalNohj and others added 14 commits July 9, 2026 22:15
Detect CURSOR_AGENT=1 and cursor-agent process args so Cursor can hold the
fleet lock and get a Codex-shaped supervision protocol. Cursor is not yet a
verified crewmate launch adapter; set config/crew-harness for crew spawns.
Also stop calling basename(1) on ps comm names so macOS -zsh ancestry no
longer breaks harness detection.

Co-authored-by: Cursor <cursoragent@cursor.com>
Empirically verified against cursor-agent 2026.07.08: launch with
agent --force, project stop hook for turn-end, busy footer ctrl+c to stop,
Ctrl+C interrupt, /exit, and one-time workspace trust. Wire spawn, teardown,
busy detection, bootstrap verified list, and harness-adapters facts.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Cursor installs both agent and cursor-agent, and Grok (or anything else)
can claim the bare agent name on PATH, so bare 'agent --force' could
silently launch the wrong CLI. Resolve the launch binary through
fm_cursor_launch_bin: always prefer cursor-agent, fall back to bare
agent only when cursor-agent is absent and agent is verified Cursor
(CURSOR_INVOKED_AS, resolved cursor-agent path, or --version
fingerprint), and abort the spawn if neither resolves. Mirrors the
cursor-agent robustness of primary detection.
@vitalNohj
vitalNohj force-pushed the fm/cursor-agent-bin-54 branch from 0b0ef86 to e6815db Compare July 10, 2026 02:17
@vitalNohj

Copy link
Copy Markdown
Author

Withdrawing this contribution for now — opened against upstream by mistake while iterating on Cursor Agent support on our fork. Sorry for the noise; happy to reopen a cleaner PR later if useful.

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