Skip to content
Open
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
4 changes: 2 additions & 2 deletions .agents/skills/harness-adapters/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Before inspecting or changing session-open behavior, read `docs/sessionstart-nud

At session start, `bin/fm-session-start.sh` prints exactly one watcher supervision block for the detected primary harness.
Do not substitute another harness's wait shape when resuming supervision.
Claude's Stop `asyncRewake` hook (`bin/fm-claude-stop-autoarm.sh`) owns tokenless re-arm around `bin/fm-watch-arm.sh`, and Grok uses tracked background-notify cycles around `bin/fm-watch-arm.sh`.
Claude's Stop `asyncRewake` hook (`bin/fm-claude-stop-autoarm.sh`) owns tokenless re-arm around `bin/fm-watch-arm.sh`, and Grok uses one tracked `bin/fm-watch-grok-longrun.sh` task that keeps routine declared-wait rechecks inside the task.
Codex uses bounded foreground checkpoints through `bin/fm-watch-checkpoint.sh` because Codex cannot reason while a foreground tool call is running.
OpenCode uses `.opencode/plugins/fm-primary-watch-arm.js`, which coordinates with the turn-end guard plugin and wakes the TUI with `client.session.promptAsync`.
Pi and pi-signed use the tracked `.pi/extensions/fm-primary-turnend-guard.ts` plus the tracked `.pi/extensions/fm-primary-pi-watch.ts`, both project-local extensions the Pi engine auto-discovers once trusted.
Expand Down Expand Up @@ -357,7 +357,7 @@ Grok 0.2.112 exposes native same-process Stop continuation in its running payloa
The exact adaptive and malformed-input contract is owned by `docs/turnend-guard.md`.
The tracked Claude hook entries whose event Grok already covers through its own `.grok/hooks/` registration skip themselves under `GROK_AGENT` or `GROK_HOOK_EVENT`, because Grok also loads Claude-compatible project settings and otherwise creates a second blocking path; the exact marker set and why `GROK_SESSION_ID` is excluded are owned by `docs/turnend-guard.md` "Harness integrations".
Project-local Grok hooks require folder trust, verified with launch-time `--trust`; if the primary firstmate checkout is not trusted for Grok hooks, this primary guard fails open and `fm-guard.sh` remains the next-command alarm.
Grok's primary watcher protocol remains background-notify around `bin/fm-watch-arm.sh`; native Stop continuation does not provide Pi-like extension ownership.
Grok's primary watcher protocol uses one tracked `bin/fm-watch-grok-longrun.sh` task that completes only for an actionable watcher result or failure; native Stop continuation does not provide Pi-like extension ownership.

## cursor (VERIFIED CREWMATE/SCOUT 2026-08-11 on tmux and 2026-08-12 on Herdr, and SECONDMATE/PRIMARY 2026-08-13, Cursor Agent CLI 2026.08.11-e8db854)

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Backend-specific setup is linked in [Documentation](#documentation).
### Recommended harnesses

**Claude Code, Grok, and Pi are equal co-primary recommendations** for running the primary firstmate session, with `pi-signed` supported as Pi's distinct signed-wrapper identity.
Claude Code uses a tracked Stop hook for tokenless watcher re-arm and rewake, Grok uses background-notify wake cycles, and Pi uses its tracked primary watcher extension.
Claude Code uses a tracked Stop hook for tokenless watcher re-arm and rewake, Grok uses a long-lived tracked watcher task that completes only for actionable work, and Pi uses its tracked primary watcher extension.
All three have verified turn-end guard paths when launched with their documented setup.
Pick whichever one matches your subscription and workflow.

Expand Down
5 changes: 3 additions & 2 deletions bin/fm-arm-command-policy.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env node
// Semantic policy for watcher arm and checkpoint shell commands.
// Semantic policy for watcher arm, checkpoint, and Grok long-runner shell commands.
//
// This parser is deliberately narrow.
// It recognizes executed command positions without evaluating, expanding,
Expand Down Expand Up @@ -44,7 +44,7 @@ function parseArguments(argv) {
}

function rawMentionsProtected(command) {
return /(?:^|[/\s'"`(])fm-watch(?:-(?:arm|checkpoint))?\.sh\b/.test(normalizeLineContinuations(command));
return /(?:^|[/\s'"`(])fm-watch(?:-(?:arm|checkpoint|grok-longrun))?\.sh\b/.test(normalizeLineContinuations(command));
}

function rawMentionsBroadKill(command) {
Expand Down Expand Up @@ -598,6 +598,7 @@ export function commandPosition(tokens) {
const PROTECTED_SCRIPTS = [
{ relative: "bin/fm-watch-arm.sh", kind: "arm" },
{ relative: "bin/fm-watch-checkpoint.sh", kind: "checkpoint" },
{ relative: "bin/fm-watch-grok-longrun.sh", kind: "grok-longrun" },
{ relative: "bin/fm-watch.sh", kind: "watch" },
];

Expand Down
6 changes: 3 additions & 3 deletions bin/fm-arm-pretool-check.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# Stable PreToolUse transport for the watcher-arm command policy.
# Stable PreToolUse transport for the watcher command policy.
#
# A firstmate primary must arm the watcher or run a Codex checkpoint as a
# standalone verified harness call.
# A firstmate primary must arm the watcher, run a Codex checkpoint, or run the
# Grok long-runner as a standalone verified harness call.
# bin/fm-arm-command-policy.mjs is the sole owner of shell classification,
# protected execution identity, the blessed setup tree, and deny reason codes.
# This wrapper only acquires the harness payload, discovers the active roots,
Expand Down
4 changes: 2 additions & 2 deletions bin/fm-supervision-instructions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ repair_line() {
printf '%s%s\n' "$prefix" 'repair missing watcher supervision by letting the OpenCode TUI plugin arm after idle; use bin/fm-watch-arm.sh only as a manual recovery probe if the plugin reports failure.'
;;
grok)
printf '%s%s\n' "$prefix" 'repair missing watcher supervision with bin/fm-watch-arm.sh as its own Grok tracked background task, never shell &.'
printf '%s%s\n' "$prefix" 'repair missing watcher supervision with bin/fm-watch-grok-longrun.sh as its own Grok tracked background task, never shell &.'
;;
cursor)
printf '%s%s\n' "$prefix" 'watcher supervision is owned by the stop-hook park; inspect the hook registration and watcher startup path before ending the turn.'
Expand All @@ -173,7 +173,7 @@ ordinary_wake_line() {
printf '%s\n' '- Ordinary wake: the OpenCode TUI plugin already owns watcher continuity; do not arm manually.'
;;
grok)
printf '%s\n' '- Ordinary wake: re-arm exactly one bin/fm-watch-arm.sh Grok tracked background task as directed below.'
printf '%s\n' '- Ordinary wake: re-arm exactly one bin/fm-watch-grok-longrun.sh Grok tracked background task as directed below.'
;;
cursor)
printf '%s\n' '- Ordinary wake: the stop-hook park (bin/fm-turnend-guard-cursor.sh) already owns watcher continuity; drain and handle the wake, and do not arm another cycle yourself.'
Expand Down
3 changes: 2 additions & 1 deletion bin/fm-test-run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ family_for_basename() {
fm-supervision-events.test.sh|fm-turnend-guard.test.sh|fm-wake-daemon-lifecycle-e2e.test.sh|\
fm-wake-drain-unread-status.test.sh|\
fm-tool-update-check.test.sh|\
fm-wake-queue.test.sh|fm-watch-arm.test.sh|fm-watch-checkpoint.test.sh|fm-watch-recovery-loop.test.sh|\
fm-wake-queue.test.sh|fm-watch-arm.test.sh|fm-watch-checkpoint.test.sh|fm-grok-watch-longrun.test.sh|fm-watch-recovery-loop.test.sh|\
fm-watch-triage.test.sh|fm-task-inbox.test.sh|\
fm-watcher-lock.test.sh|fm-inactive-reconcile.test.sh)
printf '%s\n' watcher-wake-lock
Expand Down Expand Up @@ -422,6 +422,7 @@ tests/fm-gate-refuse.test.sh 4071
tests/fm-gitignore-config.test.sh 63
tests/fm-gotmp.test.sh 762
tests/fm-grok-continuity-live-e2e.test.sh 19
tests/fm-grok-watch-longrun.test.sh 3000
tests/fm-grok-stop-live-e2e.test.sh 21
tests/fm-guard-stale-banner.test.sh 11280
tests/fm-harness-liveness-drift-live-e2e.test.sh 19
Expand Down
3 changes: 2 additions & 1 deletion bin/fm-watch-arm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
# daemon owns triage and the watcher exits on every wake for the daemon to
# classify. Reliability depends on arming through a mechanism that SURVIVES the
# call and NOTIFIES on exit, so firstmate must run this script as the harness's
# own tracked background task (e.g. run_in_background), or - for a Claude
# own tracked background task (e.g. run_in_background), through Grok's tracked
# bin/fm-watch-grok-longrun.sh wrapper, or - for a Claude
# primary - inside the Stop asyncRewake hook's foreground process tree
# (bin/fm-claude-stop-autoarm.sh), where the harness owns the process group and
# the hook's exit-2 rewake is the notification. Run it as its own standalone
Expand Down
56 changes: 56 additions & 0 deletions bin/fm-watch-grok-longrun.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Keep Grok's one tracked supervision task open across routine declared-wait
# rechecks. The task completes only for an actionable watcher result or a
# failure, so Grok does not inject billed task_completed prompts for quiet
# cycles.
set -u

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ARM="$SCRIPT_DIR/fm-watch-arm.sh"
STATE="${FM_STATE_OVERRIDE:-${FM_HOME:-${FM_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}}/state}"
mkdir -p "$STATE"
out=

cleanup() {
[ -n "$out" ] && rm -f "$out" 2>/dev/null || true
}
trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 143' TERM
trap 'exit 130' INT

routine_declared_wait() {
local out=$1 count reason
count=$(grep -Ec '^(signal:|stale:|check:|heartbeat($|:))' "$out" 2>/dev/null || true)
[ "$count" -eq 1 ] || return 1
reason=$(grep -E '^(signal:|stale:|check:|heartbeat($|:))' "$out" 2>/dev/null | head -1 || true)
case "$reason" in
stale:*'declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds)') return 0 ;;
stale:*'verified hold transfer, rechecked on a long cadence not a wedge; answer the held decision or release the hold)') return 0 ;;
*) return 1 ;;
esac
}

while :; do
out=$(mktemp "$STATE/.grok-watch-longrun.XXXXXX") || {
echo "watcher: FAILED - Grok long-runner could not allocate cycle output"
exit 1
}
status=0
"$ARM" >"$out" 2>&1 || status=$?
if [ "$status" -ne 0 ]; then
cat "$out"
rm -f "$out"
out=
exit "$status"
fi
if routine_declared_wait "$out"; then
rm -f "$out"
out=
continue
fi
cat "$out"
rm -f "$out"
out=
exit 0
done
7 changes: 7 additions & 0 deletions bin/fm-watch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,13 @@ resurface_after_downtime() {
fi
[ "$FM_RECOVERY_MARKER_ACTION" = recover ] || return 0
fi
# A recovery marker can exist with no durable wake and no open decision.
# Closing that empty cycle makes Grok inject a billed completion prompt even
# though there is nothing to handle. Keep this watcher live through the quiet
# episode after the recovery state is safely consumed.
[ -s "$FM_WAKE_QUEUE" ] && wake "check: rearm-resurface"
open=$(scan_open_decisions "$STATE") || wake "check: rearm-resurface"
[ -n "$open" ] || return 0
wake "check: rearm-resurface"
}

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ The default path remains local-only; live GitHub enrichment exists only behind t
Optional Relay integrates with the watcher only after explicit opt-in; [configuration.md](configuration.md#relay-env) owns its generated-artifact and dispatch mechanics.

At session start, `bin/fm-session-start.sh` emits exactly one primary-harness supervision block rendered by `bin/fm-supervision-instructions.sh` from `docs/supervision-protocols/`.
That block owns the live wait shape for the running primary harness: Claude's Stop `asyncRewake` hook owns tokenless re-arm cycles, Cursor's stop hook parks on the watcher, Grok uses background-notify cycles, Codex uses bounded foreground checkpoints, Pi and pi-signed use the same two tracked primary extensions, and OpenCode uses its TUI plugin.
That block owns the live wait shape for the running primary harness: Claude's Stop `asyncRewake` hook owns tokenless re-arm cycles, Cursor's stop hook parks on the watcher, Grok keeps one tracked task open across routine declared-wait rechecks, Codex uses bounded foreground checkpoints, Pi and pi-signed use the same two tracked primary extensions, and OpenCode uses its TUI plugin.
`bin/fm-watch-arm.sh` remains the verified arm wrapper for protocols that call it; it forks the watcher as a tracked child, verifies it is genuinely alive with a fresh liveness beacon, and prints an honest `started`, `attached`, or nonzero `FAILED` status.
[`watcher-continuity.md`](watcher-continuity.md#arm-layer-cycle-contract) owns the arm layer's successor, terminal-delivery, re-arm recovery, and typed clean-close failure contract.
The arm layer records one bounded lifecycle row per observed cycle in `state/.watch-cycle-exits.log`; `state/.watch-triage.log` remains exclusively the absorbed-wake debug log.
Expand Down
3 changes: 2 additions & 1 deletion docs/arm-pretool-check.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ A command word in executed position is a protected execution when its normalized
```text
bin/fm-watch-arm.sh (arm; blessed entry point)
bin/fm-watch-checkpoint.sh (checkpoint; blessed entry point)
bin/fm-watch-grok-longrun.sh (Grok long-runner; blessed entry point)
bin/fm-watch.sh (watch; protected but never blessed)
```

Expand Down Expand Up @@ -90,7 +91,7 @@ An actual protected command with a heredoc still has a redirection and is denied
## Blessed syntax tree

An allowed watcher program is one linear outer command list with zero or more approved setup nodes followed by exactly one direct protected node.
`bin/fm-watch-arm.sh` and `bin/fm-watch-checkpoint.sh` are the only blessed final nodes, including their expanded-path forms; a `bin/fm-watch.sh` final node is never blessed and denies with `watcher-direct`.
`bin/fm-watch-arm.sh`, `bin/fm-watch-checkpoint.sh`, and `bin/fm-watch-grok-longrun.sh` are the only blessed final nodes, including their expanded-path forms; a `bin/fm-watch.sh` final node is never blessed and denies with `watcher-direct`.

Approved setup nodes are:

Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ Pi-family launches adapt the regular-TUI safeguard to the installed CLI's capabi
Enabled primary-session turn-end guard integrations are tracked as repo-level hook files and documented in [`docs/turnend-guard.md`](turnend-guard.md).
Kimi remains outside the primary turn-end guard integrations; [`docs/turnend-guard.md`](turnend-guard.md#compatibility-limits) owns its separate captain-approved crew wake hook.
Primary-session watcher wake protocols are rendered at session start by [`bin/fm-supervision-instructions.sh`](../bin/fm-supervision-instructions.sh) from [`docs/supervision-protocols/`](supervision-protocols/).
Claude's Stop `asyncRewake` hook owns tokenless re-arm cycles, Cursor's stop hook parks on the watcher, Grok uses background-notify cycles, Codex uses bounded foreground checkpoints, Pi and pi-signed use the same two tracked primary extensions, and OpenCode uses its TUI plugin.
Claude's Stop `asyncRewake` hook owns tokenless re-arm cycles, Cursor's stop hook parks on the watcher, Grok keeps one tracked task open across routine declared-wait rechecks, Codex uses bounded foreground checkpoints, Pi and pi-signed use the same two tracked primary extensions, and OpenCode uses its TUI plugin.
`config/crew-harness` is a local, gitignored file containing one adapter name for crewmate and scout launches.
When pi-signed is selected, Firstmate preserves `FM_PI_HARNESS=pi-signed` and refuses the launch if the selected executable is unavailable rather than falling back to pi; [`fm-spawn.sh --help`](../bin/fm-spawn.sh) owns executable resolution and launch mechanics.
Plain Pi launches set `FM_PI_HARNESS=pi`, so a signed primary's environment cannot relabel a plain Pi worker.
Expand Down
1 change: 1 addition & 0 deletions docs/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ The shared no-mistakes gate refusal for fleet lifecycle entrypoints is summarize
| `fm-procevent-when.sh` | Fire a trust-bound deterministic action at most once when its registered condition holds, then wake with the outcome |
| `fm-gate-refuse-lib.sh` | Shared no-mistakes gate-context refusal for fleet lifecycle entrypoints |
| `fm-watch-arm.sh` | Verified home-scoped watcher arm wrapper with loud cycle endings and bounded lifecycle ledger |
| `fm-watch-grok-longrun.sh` | Grok tracked-task wrapper that absorbs routine declared-wait cycle closes and returns actionable results |
| `fm-watch-checkpoint.sh` | Run one bounded foreground watcher checkpoint for Codex-style supervision |
| `fm-watch.sh` | Singleton-safe watcher: absorb benign wakes, detect stalled local-secondmate wake queues, and exit on actionable ones |
| `fm-inactive-reconcile.sh` | Reconcile long-inactive direct crewmate terminal outcomes without forge access |
Expand Down
Loading