Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
6 changes: 4 additions & 2 deletions .agents/skills/harness-adapters/references/harness/claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ Busy hooks verified 2026-07-28 on Claude Code 2.1.220.
| Model | `--model <model>`; discover through the interactive `/model` picker, with alias or full-name shape documented by `claude --help`. |
| Effort | `--effort <low\|medium\|high\|xhigh\|max>`, verified on 2.1.196. |

Fresh-worktree or first-machine launch may show trust or bypass-permissions confirmation.
Inspect within about 20 seconds, accept the required choice with `FM_HOME=<active-home> ../../../bin/fm-send.sh <window> --key Enter` unless already bound, and verify instructions started.
Workspace trust confirms on every worktree path Claude has not seen, which is every task, and the launch flags do not change that: it measured identically under `--dangerously-skip-permissions` and under today's `--permission-mode auto`.
`../../../docs/verification/runtime-backends.md` owns that measurement under "Crewmate autonomy and the status-file write contract".
Inspect within about 20 seconds; the preselected row is `No, exit`, so a bare Enter DECLINES and quits Claude, while the accept `Yes, I trust this folder` sits on the row below.
Accept with `FM_HOME=<active-home> ../../../bin/fm-send.sh <window> --key Down` then `--key Enter` unless already bound, and verify instructions started.

## Composer ghost

Expand Down
3 changes: 3 additions & 0 deletions .agents/skills/harness-adapters/references/harness/codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Verified on 2026-06-11 with codex-cli 0.139.0 unless a fact gives a newer versio
| Exit command | `/quit`; its slash popup needs about one second between text and Enter, which the shared submit path used by the control plane handles. |
| Interrupt | Single Escape. |
| Skill invocation | `$<skill>`, for example `$no-mistakes`; `/<skill>` is Claude-only and Codex rejects it as "Unrecognized command". |
| Autonomy | `-s workspace-write -a never`: sandboxed to the worktree, with no approval path to escalate out of it; `../../../bin/fm-spawn.sh` owns the exact flags. |
| Sandboxed writes | `workspace-write` normally refuses writes outside the worktree, and `-a never` leaves the model no way to ask, so the paths the brief permits outside it are granted with `--add-dir`; `../../../docs/verification/runtime-backends.md` owns the measurement under "Crewmate autonomy and the status-file write contract". `workspace-write` grants `/tmp` and `$TMPDIR` by default, so a write measured there proves nothing. |
| Resume | `codex resume <session-id>`, using the id printed on quit. |
| Model flag | `--model <model>`. |
| Effort flag | `-c 'model_reasoning_effort="<low\|medium\|high\|xhigh>"'`, verified on codex-cli 0.142.1 whose installed schema contains `model_reasoning_effort`, active config uses it, and bundled catalog advertises only these four values while omitting `max`. |
Expand All @@ -18,6 +20,7 @@ Verified on 2026-06-11 with codex-cli 0.139.0 unless a fact gives a newer versio
A directory trust dialog appears on the first run for a repository root: "Do you trust the contents of this directory?"
Accept it with Enter and verify the instructions begin processing.
The decision persists for the repository, so later worktrees of the same project skip it.
A second gate, `Hooks need review`, was observed standing between that dialog and the composer whenever Codex hooks are configured, so a Codex spawn can need two answers before it reads its brief; `../../../docs/verification/runtime-backends.md` owns that measurement under "Crewmate autonomy and the status-file write contract".

## Skill popup

Expand Down
9 changes: 6 additions & 3 deletions .agents/skills/harness-adapters/references/harness/cursor.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ Cross-harness provider and credential identity is owned by `references/common/mo
| Fact | Value |
|---|---|
| Binary | `fm_cursor_resolve_binary` in `../../../bin/fm-cursor-lib.sh` resolves stable launcher `cursor-agent` or legacy `agent`, never `cursor`; both symlink into `~/.local/share/cursor-agent/versions/<version>/cursor-agent`, whose target auto-update replaces. |
| Launch | Positional instructions with `--trust`, `--yolo`, optional `--model <model>`, and `--workspace <absolute-task-worktree>`, after clearing foreign primary markers. |
| Launch | Positional instructions with `--trust`, the autonomy flags `../../../bin/fm-spawn.sh` owns, optional `--model <model>`, and `--workspace <absolute-task-worktree>`, after clearing foreign primary markers. |
| Models | Use current-account `cursor-agent --list-models` or legacy `agent --list-models`; the drifting observed list had only `cursor-grok-4.5-high` and `cursor-grok-4.5-high-fast` for Grok plus several `xhigh` ids, so choose a returned reasoning id and never assume low or medium Grok. |
| Busy state | `../../../bin/fm-busy-lib.sh` folds the per-conversation transcript as `cursor-transcript`: `role:user` opens and typed `turn_ended` closes success or abort, covering manual interrupt; nothing is armed or seeded, and this backend-agnostic source was identical on tmux and Herdr. |
| Exit command | `/exit`. |
| Interrupt | Single Escape returns the placeholder with no clear key; control makes no cancellation claim because an aborted transcript close appeared within seconds in some runs and not within twenty in others. |
| Skill invocation | `/<skill>`, for example `/no-mistakes`; Cursor discovers Firstmate's user skills. |
| Resume | No verified native pane resume; use deterministic relaunch. |
| Autonomy | `--yolo`, documented alias for `--force`; footer `Run Everything`. |
| Autonomy | `--auto-review --sandbox enabled`, so a crewmate runs under Cursor's own review and sandbox controls, plus the `--add-dir` writable roots below; `../../../bin/fm-spawn.sh` owns the exact flags. `--yolo`, the documented alias for `--force` whose footer reads `Run Everything`, switches both off and is not used. |
| Sandboxed writes | The sandbox normally refuses writes outside the workspace, and `--auto-review` then parks the turn on a manual approval prompt an unattended crewmate cannot clear, so the paths the brief permits outside the worktree are granted with `--add-dir`; `../../../docs/verification/runtime-backends.md` owns the measurement under "Crewmate autonomy and the status-file write contract". |
| Trust | `--trust` suppresses the dialog; `--yolo` does not, and every task has a fresh path. |
| Marker | `CURSOR_INVOKED_AS=cursor-agent` on agent and children, plus `CURSOR_AGENT=1` on child or tool processes; other `CURSOR_*` variables are not identity markers. |
| Effort | No verified flag; `references/common/model-and-effort.md` owns unsupported-value handling. |
Expand Down Expand Up @@ -61,7 +62,9 @@ Refresh with `FM_HARNESS_LIVENESS_DRIFT=1 ../../../bin/fm-test-run.sh ../../../t

Firstmate enters its acquired worktree and passes the same absolute path through `--workspace`.
Never pass Cursor `-w` or `--worktree`, which allocates a second copy under `~/.cursor/worktrees` and breaks isolation.
The CLI supports repeatable `--add-dir`, but the adapter adds none; positional instructions need no grant to their private directory.
The adapter also passes repeatable `--add-dir <path>` for the writable roots the sandbox posture needs; `../../../bin/fm-spawn.sh` owns which ones, and they are the only paths a worker may write outside its worktree.
The brief rides inline as the positional prompt, so the private brief directory needs no grant.
`--add-dir` adds writable roots only: Cursor still records the `--workspace` path as its project `workspacePath`, the exact identity `state/<id>.cursor-session` binds the busy fold to.
Example: `../../../bin/fm-spawn.sh <task-id> <project> --scout --harness cursor --model cursor-grok-4.5-high`.

## Primary integration
Expand Down
83 changes: 64 additions & 19 deletions bin/backends/herdr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1203,8 +1203,13 @@ fm_backend_herdr_pane_idle_shell_pid() { # <session> <pane-id>
}

# fm_backend_herdr_pane_idle_shell_sample: one strict instantaneous
# observation for fm_backend_herdr_pane_idle_shell_pid, which owns the proof
# contract and the settle retry.
# observation of the idle-shell shape.
# fm_backend_herdr_pane_idle_shell_pid wraps it with the settle retry and owns
# the proof contract for the destructive close paths.
# fm_backend_herdr_pane_agent_state instead takes a single sample directly, on
# purpose: that read runs in poll loops, the retry budget would slow every
# healthy `live` answer, and its corroboration is positive-only, so a sample
# lost to a transient prompt helper simply keeps `live` until the next poll.
fm_backend_herdr_pane_idle_shell_sample() { # <session> <pane-id>
local session=$1 pane=$2 info shell_pid foreground_pgid count
local process_pid name argv0 shell_name rows stat ps_bin
Expand Down Expand Up @@ -1874,25 +1879,55 @@ fm_backend_herdr_explicit_close_pane_confirmed() { # <session> <pane_id>
# reaped it - verified empirically: killing a pane's shell pid
# on a live server makes herdr immediately drop both the pane
# and its tab from `pane get`/`tab list`).
# no-agent - `pane get` succeeds (the pane structurally exists) but `agent
# get` responds with error code agent_not_found: nothing is
# registered in it - exactly what a herdr session-layout restore
# produces (verified empirically: `session stop` + fresh `herdr
# server` restart leaves the pane alive, agent_status "unknown",
# agent get -> agent_not_found - docs/herdr-backend.md "ID
# stability across a server restart"), and what a future
# `resume_agents_on_restore = false` restore would produce too
# (a plain shell, never an agent).
# live - `agent get` succeeds and reports a real agent_status (working,
# idle, done, or blocked - any registered value). An idle or
# blocked agent is still a genuine, still-registered agent, not
# a restored husk, so it is never a close-and-replace candidate.
# no-agent - the pane structurally exists but confidently hosts no agent.
# Two independent positive grounds reach this verdict:
# (a) `agent get` responds with error code agent_not_found:
# nothing is registered in it - exactly what a herdr
# session-layout restore produces (verified empirically:
# `session stop` + fresh `herdr server` restart leaves the pane
# alive, agent_status "unknown", agent get -> agent_not_found -
# docs/herdr-backend.md "ID stability across a server restart"),
# and what a future `resume_agents_on_restore = false` restore
# would produce too (a plain shell, never an agent);
# (b) a registration IS reported, but the pane's own process
# inventory positively proves a lone bare idle shell, so the
# registration outlived the process it describes.
# live - `agent get` reports a real agent_status (working, idle, done,
# or blocked - any registered value) and the pane's process
# inventory does not contradict it. An idle or blocked agent is
# still a genuine, still-registered agent, not a restored husk,
# so it is never a close-and-replace candidate.
# unknown - anything else: an unparseable/unexpected response from either
# call, or a `pane get` success whose own echoed pane_id does not
# round-trip (guards against misreading a herdr response shape
# change as "the pane exists"). The caller must fail safe toward
# refusal here, never toward closing - this is the conservative
# backstop the husk check depends on.
#
# Why a reported registration is corroborated at all. Herdr's agent registry
# is written by whatever reports into it - each harness's herdr integration
# extension, or any other source - and a report is not withdrawn when the
# process that made it goes away. Verified empirically on herdr 0.8.2: a
# `pane report-agent` from a source herdr does not itself own stays registered
# on a pane running nothing but its shell, so `agent get` keeps answering
# `idle` with no agent alive anywhere. Trusting that alone made a task whose
# worker had already exited to its shell read `alive` forever: every lifecycle
# verb then typed the harness's exit command into a shell and refused to
# relaunch, with no state that could ever clear it. The tmux classifier never
# had this hole because it reads the foreground process group and lets a
# group that is nothing but shells settle the negative verdict; this is the
# same rule expressed through herdr's own `pane process-info` inventory.
#
# The corroboration is positive-only and one strict instantaneous sample, so
# it is the cheapest possible addition to a read that runs in poll loops and
# can only ever move a verdict from `live` toward `no-agent`, never the other
# way. Anything short of proof - a live harness process in the foreground, an
# extra foreground process, a shell with a child, an unreadable inventory, or
# a response about a different pane - leaves `live` exactly as before, so a
# genuinely live, ambiguous, unreadable, or contradicting endpoint is never
# reclassified as recoverable. An idle shell transiently hosting a prompt
# helper therefore keeps `live` for that sample; every caller that acts on the
# negative verdict polls, so the next sample settles it.
fm_backend_herdr_pane_agent_state() { # <session> <pane_id>
local session=$1 pane_id=$2 out code presence status
presence=$(fm_backend_herdr_pane_presence_state "$session" "$pane_id")
Expand All @@ -1911,7 +1946,13 @@ fm_backend_herdr_pane_agent_state() { # <session> <pane_id>
fi
status=$(printf '%s' "$out" | jq -r '.result.agent.agent_status // empty' 2>/dev/null)
case "$status" in
working|idle|done|blocked) printf 'live' ;;
working|idle|done|blocked)
if fm_backend_herdr_pane_idle_shell_sample "$session" "$pane_id" >/dev/null 2>&1; then
printf 'no-agent'
else
printf 'live'
fi
;;
*) printf 'unknown' ;;
esac
}
Expand All @@ -1920,7 +1961,9 @@ fm_backend_herdr_pane_agent_state() { # <session> <pane_id>
# states (dead, no-agent) fm_backend_herdr_pane_agent_state can positively
# confirm; live and unknown both refuse (1), so an inconclusive read never
# licenses closing anything. Restored-layout recovery depends on this
# fail-safe-toward-refusal behavior.
# fail-safe-toward-refusal behavior. A husk is defined by what the pane IS -
# gone, or a plain shell - so a pane whose registration outlived its agent is
# a husk on the same terms as a restored one; the classifier owns that proof.
fm_backend_herdr_tab_is_husk() { # <session> <pane_id>
case "$(fm_backend_herdr_pane_agent_state "$1" "$2")" in
dead|no-agent) return 0 ;;
Expand All @@ -1931,8 +1974,10 @@ fm_backend_herdr_tab_is_husk() { # <session> <pane_id>
# fm_backend_herdr_agent_state: recovery-grade state for the same session-start
# sweep as the tmux classifier. It reuses the husk classifier rather than
# creating a second Herdr state machine: a structurally gone pane is `missing`,
# a confirmed agent-less pane is `dead`, a registered agent is `alive`, and an
# unexpected or failed API read is `unreadable`.
# a confirmed agent-less pane is `dead` - including one whose reported
# registration its own process inventory proves stale - a registered agent
# that inventory does not contradict is `alive`, and an unexpected or failed
# API read is `unreadable`.
fm_backend_herdr_agent_state() { # <target>
local target=$1
fm_backend_herdr_parse_target "$target" || { printf 'unreadable'; return 0; }
Expand Down
Loading
Loading