feat(cursor): add Cursor Agent as a verified crewmate and primary harness#400
Closed
vitalNohj wants to merge 14 commits into
Closed
feat(cursor): add Cursor Agent as a verified crewmate and primary harness#400vitalNohj wants to merge 14 commits into
vitalNohj wants to merge 14 commits into
Conversation
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>
…ermetic harness detect
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
force-pushed
the
fm/cursor-agent-bin-54
branch
from
July 10, 2026 02:17
0b0ef86 to
e6815db
Compare
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. |
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
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
bin/fm-harness.shdetects a Cursor primary session (keying oncursor-agent/CURSOR_AGENT),bin/fm-spawn.shandbin/fm-lock.shrecognizecursoras a crewmate/secondmate adapter, and supporting scripts (fm-bootstrap,fm-teardown,fm-watch,fm-supervision-instructions, tmux backend) plus docs andharness-adaptersgain Cursor entries.bin/fm-cursor-hook-lib.shwith a launch resolver (fm_cursor_launch_bin) that always preferscursor-agenton PATH, falls back to a bareagentonly when it is verified to be Cursor via a resolved-symlink path or--versionfingerprint, 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 bareagent. The lib also installs/teardowns a Cursor turn-end stop hook scoped to firstmate-owned.cursor/hooks.jsonentries.tests/fm-cursor-harness.test.sh(16 tests) covering the three resolver cases (prefercursor-agentover a colliding non-Cursoragent, verified-agent fallback, refusal of a non-Cursoragent) and an end-to-end spawn assertingcursor-agent --forcelaunches even with a non-Cursoragentearlier 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 iscursor-agent --force --workspaceeven with a non-Cursoragentshadowing 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
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'.Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
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 bareagenton 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 bareagentis actually Grok, the resolver returnsagentand 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 --secondmateflag 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 isnodejs(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 isnode), but it is a genuine regression versus prior behavior. Consider addingnodejsto 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 e2etest_cursor_spawn_uses_cursor_agent_not_bare_agentResolver CLI transcript: sourced bin/fm-cursor-hook-lib.sh and ranfm_cursor_launch_binwith (1) both a Grokagentandcursor-agenton PATH → resolvedcursor-agent, (2) only a--version-verified Cursoragent→ resolvedagent, (3) only a non-Cursor Grokagent→ refused with exit 1 and empty output; ambient CURSOR_INVOKED_AS/CURSOR_AGENT neutralized for hermeticityEnd-to-end spawn: ran the real bin/fm-spawn.sh via the test harness with a colliding non-Cursoragentearlier on PATH and captured the tmux send-keys payload → launch command wascursor-agent --force --workspace "$(pwd)", never the bare Grok agent✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.