Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion .agents/skills/harness-adapters/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: harness-adapters
description: Agent-only reference for firstmate harness operations. Use before spawning or recovering a crewmate or secondmate, handling a trust dialog, sending a harness-specific skill invocation, interrupting or exiting an agent, resuming an exited agent, or verifying a new harness adapter. Contains verified facts for claude, codex, opencode, pi, pi-signed, grok, and kimi.
description: Agent-only reference for firstmate harness operations. Use before spawning or recovering a crewmate or secondmate, handling a trust dialog, sending a harness-specific skill invocation, interrupting or exiting an agent, resuming an exited agent, or verifying a new harness adapter. Contains verified facts for claude, codex, opencode, pi, pi-signed, grok, kimi, and cursor.
user-invocable: false
metadata:
internal: true
Expand Down Expand Up @@ -127,6 +127,7 @@ The supported launch-profile flags below are verified locally; each row records
| pi / pi-signed | `--model <model>` | `--thinking <low\|medium\|high\|xhigh\|max>` | Verified 2026-07-27 on Pi and pi-signed 0.82.0. Both expose the same accepted thinking levels and completed the same model-qualified max-thinking smoke. |
| opencode | `--model <provider/model>` | none for firstmate's interactive launch | Verified on opencode 1.17.6. `opencode run` has `--variant`, but firstmate launches the interactive `opencode --prompt` path, which has no verified effort flag. |
| kimi | `--model <model>` | none | Verified 2026-07-25 on Kimi Code CLI 0.29.1. |
| cursor | `--model <model>` | none | Verified 2026-08-04 on cursor-agent 2026.07.23-e383d2b. `--model composer-2.5` selects Composer 2.5 non-fast (the `-fast` variant is a separate model id). Effort is encoded in the model id, not a separate flag, so firstmate emits none and records the requested effort in meta. |

The concrete `harness` field owns adapter identity independently of the model provider: `harness=pi` with `model=xai/grok-*` is Pi using xAI, not `harness=grok`, and does not require Grok CLI login; `harness=grok` remains the standalone Grok Build CLI adapter.
No script resolves that split for you: establish which credential store a tuple reads from the discovery surfaces below plus `quota-axi auth --json`'s per-provider sources, and show that reasoning rather than inferring it from a harness, model, or source name.
Expand All @@ -144,6 +145,7 @@ Use the discovery surface in the current authenticated environment because suppo
| pi / pi-signed | Run the selected executable as `<executable> --list-models [search]`; Pi's installed `docs/models.md` owns how built-in, extension-registered, and custom provider/model entries reach that list. |
| grok | Run `grok models`, which lists the models available to the current Grok installation and account. |
| kimi | Run `kimi provider list --json`, which lists the current provider and model configuration. |
| cursor | Run `cursor-agent --list-models` (or `cursor-agent models`), which lists the models available to the current Cursor account; `composer-2.5` and `composer-2.5-fast` are distinct entries. |

For an unfamiliar harness or model namespace, establish support and provider identity from that harness's authoritative CLI help, model listing, or current documentation rather than guessing from a name or prefix.
A listing that reaches the account and does not contain the model is concrete evidence the model is unsupported: block that candidate and quote the result.
Expand All @@ -163,6 +165,7 @@ Natural language is acceptable if uncertain.
- pi and pi-signed: no separate verified skill invocation beyond normal command behavior; use natural language if the exact skill command is uncertain.
- grok: `/<skill>`, for example `/no-mistakes` (same form as claude). Verified end to end: grok discovers the user-level `no-mistakes` skill, `/no-mistakes` invokes it, and grok drives a real `no-mistakes axi run`. Like codex's `$`/`/` popups, typing `/<skill>` opens grok's slash-autocomplete, so a too-fast Enter selects the popup entry instead of sending, and for an argument-taking command (like `/no-mistakes`'s optional task-first argument) that first Enter only expands the popup selection into an argument-hint placeholder rather than submitting - a genuine second Enter is required (see the grok section below for the 2026-07-03 incident and fix). `fm_tmux_submit_core`'s retried Enter (used by `fm-send` on the tmux backend) handles this through the structural composer reader; the herdr backend needed a dedicated fix (`fm_backend_herdr_composer_state`, docs/herdr-backend.md) because its prior delta-based verification false-positived on that same popup-close content change.
- kimi: `/<skill>`, for example `/no-mistakes`.
- cursor: `/<skill>`, for example `/no-mistakes`; cursor's own slash menu is built-ins only (`/model`, `/ask`, `/plan`, ...), so `/no-mistakes` is a firstmate skill invoked by natural language unless a cursor skill is registered. Use natural language if the exact form is uncertain.

## Submission acknowledgement hazards

Expand Down Expand Up @@ -397,3 +400,33 @@ The delivery-only spinner match covers the full moon-phase glyph set rather than
Each Kimi crew worktree receives a gitignored `.fm-kimi-turnend` token pointer, and the global hook touches that task's `state/<id>.turn-ended` only when the Stop payload's `cwd`, pointer, and registry entry all agree.
A guarded silent hook cannot be verified from absence of effect, so prove invocation with an unguarded probe before concluding that the hook did not fire.
The guarded turn-end signal remains a wake notification; standalone Kimi has no busy-state source until one is live-verified.

## cursor (VERIFIED 2026-08-04, cursor-agent 2026.07.23-e383d2b, model composer-2.5 non-fast)

Cursor Composer's CLI (`cursor-agent`), a multi-model gateway driven directly like grok/kimi (not ACP).
Launch with a positional prompt: `cursor-agent --force --trust --model composer-2.5 "$(<brief>)"`.

| Fact | Value |
|---|---|
| Busy state | Native user hooks `beforeSubmitPrompt` (busy) and `stop` (idle), live-verified to bracket a real interactive turn; a real semantic source like claude's, not a rendered-tail fallback. `bin/fm-busy-lib.sh` owns the `cursor-hook` source and its `fm_busy_cursor_verified` gate. |
| Exit command | `/exit` (or `/quit`); both fire `sessionEnd` and terminate the process. |
| Interrupt | single `Esc` (aborts the running turn and returns the draft to the composer). A single `Ctrl-C` at idle is a harmless no-op. |
| Skill invocation | `/<skill>` via natural language; cursor's slash menu is built-ins only, so `/no-mistakes` is invoked in prose. |
| Autonomy | `--force` (footer shows `Run Everything`); auto-approves every tool execution, verified fully unattended. `--yolo` is an alias; `--auto-review` is NOT fully unattended and must not be used for a crewmate. |
| Trust | `--trust` suppresses the workspace-trust dialog at launch in a fresh worktree, so no post-launch keystroke is needed. |
| Env marker | None verified as exported to child/tool processes; detection is process ancestry on the `cursor-agent` command name only. |
| Resume | `cursor-agent --resume [chatId]`, `--continue`, or the `resume`/`ls` subcommands. |
| Composer | Bordered box; the prompt glyph is `→` (U+2192) rendered dim (SGR 2), and the idle placeholder is `Add a follow-up`, also dim. |

Turn-end and busy hook: cursor loads USER hooks from `~/.cursor/hooks.json` (schema version 1, entries are `{"command":...,"type":"command"}` directly in each event's array), which run from `~/.cursor` - NOT the worktree.
`fm-spawn` installs one firstmate-owned entry each for `beforeSubmitPrompt` and `stop` through `bin/fm-cursor-turnend-hook.sh`, plus a static guarded hook script and a private token registry under `~/.cursor/fm-turn-end.d/`, and it preserves the captain's own cursor hooks and other `hooks.json` keys.
Because user hooks run from `~/.cursor`, the guard keys on a `.fm-cursor-turnend` pointer found among the payload's `workspace_roots[]` (NOT cwd), so it is a silent no-op for every non-firstmate cursor session and for a worktree without a matching registry token.
An interrupted turn fires `stop` twice for one generation (`aborted` then `error`), so the hook dedupes `stop` by `generation_id` to enqueue exactly one wake; `beforeSubmitPrompt` and a completed `stop` fire once each.

Launch-template rule: the launch command MUST let the shell exec `cursor-agent` as its sole/last command with no trailing `;` command.
The launcher execs node under `exec -a "$0"`, so with a clean exec the pane's `#{pane_current_command}` reports `cursor-agent` (which `bin/backends/tmux.sh`'s alive-set matches); any trailing shell command leaves the pane leader as `bash` and breaks liveness.

Attribution (hard gate): cursor defaults `attribution.attributeCommitsToAgent` and `attributePRsToAgent` to `true` in the global `~/.cursor/cli-config.json`, producing `Co-authored-by: Cursor <cursoragent@cursor.com>`, which violates `AGENTS.md`.
`fm-spawn` neutralizes this per worktree with a gitignored `.cursor/cli.json` project config (`{"attribution":{"attributeCommitsToAgent":false,"attributePRsToAgent":false}}`) that cursor reads by default and merges over the global (its `--disable-project-configs` flag documents that default read), so no destructive or racy edit of the shared global config is needed and the override is removed with the worktree.
The `→` composer glyph is handled by `bin/fm-composer-lib.sh` (a `→` agent-glyph arm) and `bin/fm-tmux-lib.sh` (the empty-box geometry proof), so a dim, idle cursor composer classifies empty rather than as pending input.
`docs/verification/runtime-backends.md` owns the dated live-verification evidence, and `data/cursor-verify/report.md` holds the raw probe transcript.
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ A silent bootstrap section needs no action; for any printed actionable diagnosti
## 4. Harness and runtime dispatch

Load `harness-adapters` before every spawn or recovery and before trust handling, skill invocation, interrupt, exit, resume, or adapter verification.
The verified harnesses are `claude`, `codex`, `opencode`, `pi`, `pi-signed`, `grok`, and `kimi`; never dispatch on an unverified adapter.
The verified harnesses are `claude`, `codex`, `opencode`, `pi`, `pi-signed`, `grok`, `kimi`, and `cursor`; never dispatch on an unverified adapter.
If static `config/crew-harness` or `config/secondmate-harness` names an unverified adapter, report it and fall back only to a verified adapter rather than launching it.

`docs/configuration.md` owns dispatch-profile and runtime-backend schemas, `bin/fm-harness.sh` owns static resolution, and `bin/fm-spawn.sh` owns launch flags and fail-closed validation.
Expand Down
2 changes: 1 addition & 1 deletion bin/backends/tmux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ fm_backend_tmux_classify_process_name() { # <path> [argv0] -> agent|shell|other
base=${path##*/}
base=${base#-}
case "$base" in
*claude*|*codex*|*opencode*|*grok*|*kimi*|pi|pi-signed|pi-launcher|Pi) printf 'agent' ;;
*claude*|*codex*|*opencode*|*grok*|*kimi*|*cursor*|pi|pi-signed|pi-launcher|Pi) printf 'agent' ;;
zsh|bash|sh|dash|ash|ksh|mksh|tcsh|csh|fish) printf 'shell' ;;
*)
if fm_harness_path_name "$path" >/dev/null || fm_harness_path_name "$argv0" >/dev/null; then
Expand Down
19 changes: 19 additions & 0 deletions bin/fm-busy-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
# codex-hook, codex-appserver reserved: Codex, gated by
# fm_busy_codex_semantic_source
# kimi-wire, kimi-hook reserved: standalone Kimi, gated by fm_busy_kimi_verified
# cursor-hook cursor beforeSubmitPrompt (busy) and stop (idle) user
# hooks, gated by fm_busy_cursor_verified
# Firstmate-owned sources accepted for every converted adapter:
# fm-spawn the launch-brief turn seeded at spawn
# fm-interrupt a firstmate-controlled interruption of the worker
Expand Down Expand Up @@ -96,6 +98,19 @@ fm_busy_kimi_verified() {
[ -n "$FM_BUSY_KIMI_VERIFIED_VERSIONS" ]
}

# Cursor (cursor-agent / Composer) verification gate. Unlike Kimi and Codex this
# gate is OPEN: cursor's native beforeSubmitPrompt (busy) and stop (idle) user
# hooks were live-verified to bracket a real interactive turn on a
# firstmate-launched worker, including the interrupt path, on cursor-agent
# 2026.07.23-e383d2b (data/cursor-verify/report.md; the interrupt fires stop
# twice, aborted then error, for the same generation_id, so the installed hook
# dedupes stop by generation_id). fm-spawn arms and wires the cursor busy
# contract behind this gate, and the installed user hook writes cursor-hook
# busy/idle events through fm-busy-event.sh with the task's armed gen.
fm_busy_cursor_verified() {
return 0
}

# fm_busy_codex_appserver_observable: capability/version negotiation for the
# Codex app-server turn lifecycle. Returns 0 only when a pane worker's turns
# are observable through the app-server protocol on the installed binary.
Expand Down Expand Up @@ -177,6 +192,10 @@ fm_busy_sources_for_harness() { # <harness>
fm_busy_kimi_verified || { printf ''; return 0; }
adapter='kimi-wire kimi-hook'
;;
cursor*)
fm_busy_cursor_verified || { printf ''; return 0; }
adapter=cursor-hook
;;
*) printf ''; return 0 ;;
esac
printf '%s fm-spawn fm-interrupt fm-recovery' "$adapter"
Expand Down
11 changes: 7 additions & 4 deletions bin/fm-composer-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -185,14 +185,17 @@ fm_composer_classify_content() { # <bordered> <content> [idle_re] [idle_case] [
plain_content=${5:-$content}
if [ "$bordered" != 1 ] && [ -z "$content" ] && [ -n "$plain_content" ]; then
case "$plain_content" in
'❯'|'›') printf 'empty'; return 0 ;;
'❯'|'›'|'→') printf 'empty'; return 0 ;;
*) printf 'unknown'; return 0 ;;
esac
fi
# A bare prompt glyph on its own row.
case "$content" in
'❯'|'›')
'❯'|'›'|'→')
# Agent prompt glyph: a genuine empty agent composer, bordered or bare.
# `→` (U+2192) is cursor-agent's composer glyph, rendered dim (SGR 2) so
# fm_composer_strip_ghost normally removes it before this point; the arm
# keeps a surviving bare `→` from reading as a dead shell.
printf 'empty'; return 0 ;;
'>'|'$'|'%'|'#')
# Shell prompt glyph: empty ONLY inside a composer box (the harness's own
Expand All @@ -208,8 +211,8 @@ fm_composer_classify_content() { # <bordered> <content> [idle_re] [idle_case] [
fi
# Strip a leading prompt glyph, then re-judge the remainder.
case "$content" in
'❯ '*|'› '*|'> '*|'$ '*|'% '*|'# '*) content=${content#??} ;;
'❯'*|'›'*|'>'*|'$'*|'%'*|'#'*) content=${content#?} ;;
'❯ '*|'› '*|'→ '*|'> '*|'$ '*|'% '*|'# '*) content=${content#??} ;;
'❯'*|'›'*|'→'*|'>'*|'$'*|'%'*|'#'*) content=${content#?} ;;
esac
content="${content#"${content%%[![:space:]]*}"}"
content="${content%"${content##*[![:space:]]}"}"
Expand Down
Loading
Loading