Skip to content

feat(bin): add Cursor harness adapter support - #1726

Draft
vipentti wants to merge 72 commits into
kunchenguid:mainfrom
vipentti:fm/cursor-adapter-ship-4
Draft

feat(bin): add Cursor harness adapter support#1726
vipentti wants to merge 72 commits into
kunchenguid:mainfrom
vipentti:fm/cursor-adapter-ship-4

Conversation

@vipentti

@vipentti vipentti commented Aug 5, 2026

Copy link
Copy Markdown

Overview

This PR adds first-class Cursor agent harness support to firstmate. Cursor becomes a verified adapter for crewmate and secondmate work: firstmate detects trusted Cursor processes and resolves installed executables, spawns agents, participates in session locking, supports tmux and Herdr endpoints, checks remote readiness and liveness, and safely contains and reaps detached worker-server processes.

It also adds tracked Cursor stop and preToolUse hooks with turn-end supervision, extends composer, ghost-text, busy-queue, and submission handling for Cursor's TUI, and documents and tests the behavior. This matters because Cursor's process identity, composer, hook protocol, and detached worker-server differ from existing harnesses; explicit adapter support lets firstmate supervise and clean up Cursor workers with fail-closed lifecycle behavior across local and remote work.

The adapter scope covers crewmate and secondmate harnesses. Primary-mode Cursor integration is also present in this shipped head through .cursor/hooks.json, the stop/preToolUse guard, and hook-owned turn-end supervision; no broader primary-mode feature set is claimed.

What Changed

  • Add verified Cursor Agent support across harness detection, executable resolution, spawning, session locking, tmux and Herdr backends, remote readiness, liveness, and worker-server teardown. Detection is narrowed to trusted cursor-agent identity or a proven legacy alias; unrelated agent processes, bare interpreters, and unverified aliases fail closed.
  • Add tracked Cursor stop and preToolUse hooks with Cursor-formatted deny responses, hook-owned turn-end supervision, continuation re-arming, bounded wake chains, and safe follow-up handling.
  • Extend shared composer, ghost-text, busy-queue, and submission logic for Cursor's prompt, Add a follow-up idle state, ghost rendering, and Enter-while-busy queue behavior across tmux and Herdr.
  • Contain detached Cursor worker servers by recording PID and start-time identity at spawn, reaping only the recorded process on teardown, and covering nested and secondmate cleanup without touching recycled PIDs or unrelated workers.
  • Apply review hardening for bounded and fail-closed Cursor probes, narrowed argv/process identity detection, remote readiness, wake-chain and identity-free fallback scoping, hook validation, and cleanup containment.
  • Keep harness-pinned suites hermetic under ambient CURSOR_AGENT, deduplicate resolver and shared arrow/glyph predicates, parameterize wake-refire coverage, keep the Cursor shim contract beside its implementation, and build follow-up JSON with jq.
  • Update harness, backend, supervision, runtime-verification, remote-secondmate, and teardown documentation, with regression coverage for detection, spawning, session locks, liveness, hooks, composer/ghost behavior, busy submission, remote doctor readiness, teardown, and fixture hermeticity.

How it was validated

  • Focused suites passed for fm-composer-lib, fm-composer-ghost, fm-turnend-guard, fm-remote-doctor, fm-spawn-dispatch-profile, Cursor harness detection, session locking, tmux/Herdr behavior, liveness, session start, busy submission, remote secondmates, and teardown.
  • bin/fm-lint.sh passed.
  • bin/fm-doc-audience-check.sh passed.
  • No-mistakes finalization gates passed for intent, rebase, review, test, document, lint, and push; CI was skipped per standing policy and upstream CI is monitored separately.
  • Current PR head: 929c4f43bd8c621d5513116430f3a2a2e39f64a6.

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed
✅ **Rebase** - passed
✅ **Review** - completed
🔧 **Test** - 2 issues found → auto-fixed (2) ✅
✅ **Document** - passed
✅ **Lint** - passed
✅ **Push** - passed

@vipentti
vipentti force-pushed the fm/cursor-adapter-ship-4 branch from 3dbb21e to 02b08dd Compare August 5, 2026 06:50
@vipentti vipentti changed the title feat: add cursor-agent adapter as verified firstmate harness feat(bin): add cursor-agent as verified harness adapter Aug 5, 2026
@vipentti vipentti changed the title feat(bin): add cursor-agent as verified harness adapter feat(bin): add cursor-agent adapter as verified crewmate harness Aug 5, 2026
@vipentti
vipentti force-pushed the fm/cursor-adapter-ship-4 branch from 65dd134 to 3b84259 Compare August 5, 2026 07:37
@vipentti vipentti changed the title feat(bin): add cursor-agent adapter as verified crewmate harness feat(harness): add cursor-agent adapter as verified firstmate harness Aug 5, 2026
@vipentti vipentti changed the title feat(harness): add cursor-agent adapter as verified firstmate harness feat(harness): add verified cursor-agent adapter with spawn-time binary resolution Aug 5, 2026
@vipentti
vipentti force-pushed the fm/cursor-adapter-ship-4 branch from 406afee to 59d19df Compare August 5, 2026 10:31
@vipentti
vipentti marked this pull request as draft August 5, 2026 10:36
@vipentti
vipentti force-pushed the fm/cursor-adapter-ship-4 branch 2 times, most recently from 54afe2e to 660a09e Compare August 5, 2026 11:27
@vipentti vipentti changed the title feat(harness): add verified cursor-agent adapter with spawn-time binary resolution feat(bin): add cursor-agent adapter as verified crewmate harness Aug 5, 2026
@vipentti
vipentti marked this pull request as ready for review August 5, 2026 12:13
@vipentti vipentti changed the title feat(bin): add cursor-agent adapter as verified crewmate harness feat(bin): add cursor-agent harness adapter with alias resolution Aug 5, 2026
@vipentti
vipentti force-pushed the fm/cursor-adapter-ship-4 branch 2 times, most recently from 3c216d2 to 5b21865 Compare August 5, 2026 14:53
@vipentti vipentti changed the title feat(bin): add cursor-agent harness adapter with alias resolution feat(bin): add Cursor primary-harness support Aug 5, 2026
@vipentti
vipentti force-pushed the fm/cursor-adapter-ship-4 branch from 4341c86 to a45299f Compare August 5, 2026 18:59
@vipentti vipentti changed the title feat(bin): add Cursor primary-harness support feat(bin): add Cursor harness support and reap detached worker-server on teardown Aug 5, 2026
@vipentti
vipentti force-pushed the fm/cursor-adapter-ship-4 branch 4 times, most recently from 2398b85 to 77c909c Compare August 6, 2026 03:01
@vipentti vipentti changed the title feat(bin): add Cursor harness support and reap detached worker-server on teardown feat(harness): add Cursor agent harness support Aug 6, 2026
@kunchenguid

kunchenguid commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Automated reminder: thanks for the PR! This branch currently has a merge conflict with the base branch.

When you get a chance, please rebase onto (or merge) the latest base branch, resolve the conflict, and push. After that, checks will re-run and the PR will get looked at again.

Noted for firstmate#1726 at 77c909c0.

@vipentti vipentti changed the title feat(harness): add Cursor agent harness support feat(cursor): add Cursor adapter with stop-hook supervision Aug 6, 2026
@vipentti
vipentti force-pushed the fm/cursor-adapter-ship-4 branch from 58ae208 to fb4ac91 Compare August 6, 2026 18:40
@kunchenguid kunchenguid removed the wheelhouse:pending-contributor-action Managed by Wheelhouse label Aug 6, 2026
@kunchenguid

Copy link
Copy Markdown
Owner

Automated reminder: thanks for the PR! This branch currently has a merge conflict with the base branch.

When you get a chance, please rebase onto (or merge) the latest base branch, resolve the conflict, and push. After that, checks will re-run and the PR will get looked at again.

Noted for firstmate#1726 at fb4ac914.

vipentti added 15 commits August 7, 2026 08:23
…ases

Cursor identity was wide enough to misfire in several owners: any executable
named `agent`, and any path with an `agent/` directory component, classified as
Cursor across harness detection, session-lock ancestry, tmux liveness, and
worker-server discovery. The same generic name satisfied local spawn resolution
and remote readiness, so firstmate could launch an unrelated executable with
Cursor's flags or report a host ready with no Cursor installed.

Introduce bin/fm-cursor-lib.sh as the single owner of Cursor executable
resolution, the legacy-alias proof, Cursor process identity, and the shared
process-identity parse:

- R1 Clear the ambient CURSOR_AGENT marker on every Claude-pinned invocation in
  tests/fm-turnend-guard.test.sh, so the suite exercises Claude's repair wording
  when run from a Cursor primary. A regression case asserts the hermeticity
  rather than leaving it to convention.
- R2 State one launch-boundary rule: every template-built non-Cursor worker
  clears CURSOR_AGENT, muse included. The raw-command escape hatch keeps its
  byte-identical passthrough contract.
- R3 Record the worker-server identity through the same parse teardown
  re-checks, so a comm containing spaces no longer defeats the recycled-pid
  guard. Spawn previously read `$22` off the raw /proc line.
- R4 Accept an exact cursor-agent name; require Cursor-specific executable or
  install-path evidence for MainThread, a bare interpreter, or the legacy alias.
  Worker-server discovery now binds the exact resolved executable, not a
  basename.
- R5 Refuse a Cursor spawn on zellij, orca, and cmux before any endpoint, launch
  command, or task metadata exists: those backends expose no pane process tree,
  so the detached worker-server could never be recorded and would leak.
- R6 Accept the legacy `agent` alias only after it proves itself Cursor, either
  structurally through Cursor's versioned install tree or through a bounded,
  fail-closed --help identity probe. A bare zero exit is never proof. The remote
  doctor reproduces the resolver rather than sourcing it, so its hash-verified
  bootstrap never pulls in an unverified helper; a test pins the two to the same
  verdicts, and the pinned doctor digest is refreshed.

The Orca runtime probe now runs after the harness/backend refusals, so an
impossible combination is reported as the configuration error it is.

Ghost-gap handling, wake-chain bounds, recorded worker-server reaping,
recycled-pid refusal, hooks validation, and the cursor-unverified busy posture
are unchanged.
…n tree

Three suites assemble their own bin/ directory for a real teardown or hook
subprocess and list each sibling the script sources. bin/fm-cursor-lib.sh is a
new one of those, so those fixtures failed at source time rather than on the
behavior under test.
@vipentti
vipentti force-pushed the fm/cursor-adapter-ship-4 branch from 9ee19a5 to 91c4f5e Compare August 7, 2026 05:34
@vipentti
vipentti marked this pull request as draft August 7, 2026 05:59
…eue rules

Behaviour-neutral cleanup of the Cursor adapter's duplicated logic.

- fm_composer_export_env owns the per-harness composer env contract
  (including Cursor's "Add a follow-up" idle default, which an explicit
  caller-supplied regex still overrides), replacing the copies in
  fm-send.sh and fm-supervise-daemon.sh.
- clamp_digits owns the leading-zero strip plus saturation shared by
  normalize_budget, normalize_counter, and fallback_total_from_loop_count
  in the Cursor turn-end shim. The length-before-value comparison, the
  exact ceilings, and each caller's own malformed-input answer are
  unchanged.
- fm_composer_queued_submit_verdict owns the exhausted-retry rule
  (verified queueing harness plus an affirmatively busy pane reports
  delivered, everything else stays pending) for both the tmux and herdr
  submit loops. Each backend keeps its own busy probe, and the probe runs
  only for a harness whose answer can change the verdict.
- Removes the fm_tmux_strip_ghost wrapper; callers and tests use
  fm_composer_strip_ghost directly.

Adds coverage for the two new shared composer helpers, including that the
busy exception never widens past opencode and cursor.
A1: fm-spawn.sh's kimi and muse resolvers duplicated the same
command-v-to-absolute-path block; one absolute_command_path helper now owns
it, and the single-line resolve_cursor_binary wrapper is gone in favour of
calling fm_cursor_resolve_binary directly. bin/fm-remote-doctor.sh keeps its
deliberate reproduction of the Cursor resolver: the remote entrypoint may run
the doctor before the job worker exists, so its only identity proof on a
git-less host is its pinned SHA-256, and its remaining absolute-path block
cannot use the reproduced canonicalizer without resolving symlinks for every
other harness. Unchanged, so the pinned digest still holds.

A4: bin/fm-composer-lib.sh gains fm_composer_cursor_arrow_ok (bordered
composer or cursor harness) and fm_composer_strip_prompt_glyph, and every
arrow verdict and glyph strip in the classifier routes through them. All
current verdicts preserved: an unscoped bare arrow stays unknown, a Cursor
arrow with real text stays pending, a verified Cursor idle placeholder reads
empty, and ghost-only unrelated bare rows stay unknown. The shared strip uses
literal glyph prefixes rather than character-count wildcards, which is the
only form correct outside a UTF-8 locale.

A7: the three near-twin cursor wake-refire suites collapse into one
parameterized case covering alternating static reasons, dynamic stale details,
and process-event sequence details. Canonicalization, the ceiling diagnostic,
state clearing, and the restarted chain all stay covered.

A8: bin/fm-turnend-guard-cursor.sh's header restated the whole chain
algorithm; it now states the contract and points at
docs/supervision-protocols/cursor.md, which owns it. Safety-critical comments
stay next to the implementation, and the doc's key list now names wake_reason.

A9: the followup body is built with jq rather than hand-rolled backslash and
quote escaping. The message text is unchanged, including its flattening to one
line.
…R_AGENT

The Cursor adapter makes CURSOR_AGENT win over other harness markers in
fm-harness.sh, so the kimi, muse, and watcher-lock fixtures that pin a
harness resolved to cursor whenever the suite ran from a Cursor session.
Unset CURSOR_AGENT on those pinned invocations, matching the pattern the
spawn and turn-end guard suites already use.
@vipentti vipentti changed the title feat(cursor): add Cursor adapter with stop-hook supervision feat(bin): add Cursor harness adapter support Aug 7, 2026
…dentity

Gate Cursor-only reverse-video gap drops and Herdr bare-arrow structural
admission on harness context, and classify Cursor processes from argv0 only so
a later cursor-agent argument token cannot impersonate the harness.
…ID identity

Round-2 review: keep established ❯/›/⟩ ghost-only composers empty like main,
activate Cursor ghost_gap buffering only under FM_COMPOSER_HARNESS=cursor, and
move generic process-reuse identity out of the Cursor lib.
…ety comment

Final cleanup: exercise ghost_gap buffering under FM_COMPOSER_HARNESS=cursor
for SGR27 and real-text-between-ghost preservation, and document that bare →
is Cursor-scoped rather than empty either way.
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.

2 participants