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
19 changes: 13 additions & 6 deletions .agents/skills/harness-adapters/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ Natural language is acceptable if uncertain.
- codex: `$<skill>`, for example `$no-mistakes`; `/<skill>` is claude-only and codex rejects it as "Unrecognized command".
- opencode: no separate verified skill invocation beyond normal slash-command behavior; use natural language if the exact skill command is uncertain.
- pi: 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) already handles this correctly by reading the cursor row; 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.
- 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`. Typing `/<skill>` opens grok's slash-autocomplete. Grok 0.2.111 submits the selected no-argument `/no-mistakes` entry with one Enter; commands whose autocomplete selection expands to an argument-hint placeholder still require a second Enter. `fm_tmux_submit_core`'s composer-state retry (used by `fm-send` on the tmux backend) safely handles both shapes; 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 popup-close content changes.

## claude (VERIFIED)

Expand Down Expand Up @@ -260,22 +260,29 @@ The model arms through `fm_watch_arm_pi`, never a foreground bash arm; the watch
`bin/fm-session-start.sh` reports when the live Pi session has not loaded both the turn-end guard and watcher extensions, and points at plain `pi` after project trust as the fix, with `-e` as a trust-free fallback.
When a secondmate is launched on Pi, `fm-spawn.sh --secondmate` launches Pi with both `-e .pi/extensions/fm-primary-turnend-guard.ts` and `-e .pi/extensions/fm-primary-pi-watch.ts`, both already present in the secondmate home's git worktree.

## grok (VERIFIED 2026-06-29, grok 0.2.73; slash-submit re-verified 2026-07-03 on 0.2.82; reasoning-effort ceiling re-verified 2026-07-13 on 0.2.99; exit paths re-verified 2026-07-19 on grok 0.2.103)
## grok (VERIFIED 2026-07-24, grok 0.2.111)

Grok Build TUI (`grok`), a Claude-Code-compatible CLI from xAI.
Launch with a positional prompt: `grok --always-approve "$(cat <brief>)"`.
For Grok's supported reasoning-effort values and omission behavior, see the [launch-profile-axes table](#launch-profile-axes).

| Fact | Value |
|---|---|
| Busy-pane signature | `Ctrl+c:cancel` (the mid-turn cancel hint in grok's keybind bar, shown iff a turn is running; the spinner line is a braille glyph + `<status>… N.Ns` + `[stop]`, e.g. `⠹ Thinking… 1.1s … [stop]`). Idle keybind bar shows only `Shift+Tab:mode │ Ctrl+.:shortcuts`. The ASCII `Ctrl+c:cancel` is the busy regex (avoids locale fragility of matching braille). |
| Exit command | `/exit` typed into the composer exits the TUI cleanly and prints `Resume this session with: grok --resume <session-id>`; `Ctrl+Q` double-press within 1000ms remains a fallback; `Ctrl+D` is the quit key in VS Code family terminals; `Ctrl+C` is the interrupt, not the exit. |
| Interrupt | single `Ctrl+C` (cancels the current turn; the footer shows `Ctrl+c:cancel` mid-turn). `Esc` only moves focus to the scrollback, it does NOT interrupt. |
| Skill invocation | `/<skill>` (e.g. `/no-mistakes`), same as claude. Opens a slash-autocomplete popup, so a too-fast Enter selects the popup entry instead of sending. For an argument-taking command that first Enter does not submit at all - it expands the selection into an argument-hint placeholder in the composer (e.g. `/compact` -> `/compact compaction instructions`, live-verified), leaving real text still sitting there unsubmitted; a genuine second Enter is required. `fm-send`'s retried Enter lands it on BOTH backends, but only because each backend's own submit-verification correctly recognizes that placeholder-filled text as still-pending - see the incident below. |
| Busy-pane signature | `Esc:cancel` (the mid-turn cancel hint in grok's keybind bar, shown iff a turn is running; the spinner line remains a braille glyph + `<status>… N.Ns` + `[stop]`). Idle keybind bar shows only `Shift+Tab:mode │ Ctrl+x:shortcuts`. Firstmate's shared ASCII busy regex accepts both 0.2.111's `Esc:cancel` and the legacy 0.2.73 `Ctrl+c:cancel` footer, avoiding locale-fragile matching of the braille spinner. |
| Exit command | `/exit` typed into the composer exits the TUI cleanly and prints `Resume this session with: grok --resume <session-id>`; `Ctrl+Q` double-press within 1000ms remains a fallback; `Ctrl+D` is the quit key in VS Code family terminals; Escape is the interrupt, not the exit. |
| Interrupt | single Escape cancels the current turn; the footer shows `Esc:cancel` mid-turn and the pane reports `Turn cancelled by user`. |
| Skill invocation | `/<skill>` (e.g. `/no-mistakes`), same as claude. Opens a slash-autocomplete popup. On 0.2.111 one Enter submitted the selected no-argument `/no-mistakes` entry, which discovered the user-level skill and began its repository checks. An argument-hint placeholder left in the composer remains pending and receives the adapter's retry Enter. |
| Autonomy | `--always-approve` (footer shows `· always-approve`); auto-approves every tool execution, verified to run fully unattended. `--permission-mode bypassPermissions` is the stronger equivalent. |
| Env marker | `GROK_AGENT=1`, set for child/tool processes. grok does NOT set `CLAUDECODE` despite Claude compatibility, so the marker is unambiguous. |
| Resume | `grok --resume <session-id>` (id printed on exit) or `grok -c` / `--continue` (most recent for the cwd); `--fork-session` branches a new session id. |

**2026-07-24 terminal re-verification (grok 0.2.111).**
In a disposable git directory inside an isolated firstmate worktree, `grok --always-approve '<prompt>'` immediately submitted the positional prompt, displayed `Grok 4.5 (high) · always-approve`, and ran an unattended shell tool whose child recorded `GROK_AGENT=1` and the disposable working directory.
During both thinking and a 30-second shell tool the footer displayed `Esc:cancel`; one Escape produced `Turn cancelled by user`, after which the idle footer no longer contained the cancel hint.
`/exit` cleanly ended the TUI, and `grok --continue --always-approve` in the same directory restored the prior conversation.
The project was a git repository and displayed no trust, login, update, or permission dialog.
Typing `/no-mistakes` displayed the discovered skill in slash autocomplete; one Enter submitted it and Grok began the skill's repository checks.

**Incident (2026-07-03, herdr backend only, grok 0.2.82):** two grok/herdr crewmates were sent `/no-mistakes` via `fm-send`; both left it fully typed but unsubmitted in the composer for minutes (footer still `Enter:send`), and `fm-send` exited 0 with no error.
Reproduced live: the herdr adapter's submit-verification at the time treated ANY pane-content change after Enter as "submitted", and the popup-close-with-placeholder-fill described above IS a visible content change even though nothing was actually sent.
The tmux backend was never affected - `fm_tmux_composer_state` reads the actual cursor row, correctly sees the placeholder text as still-pending, and its retry loop already sends the needed second Enter.
Expand Down
2 changes: 1 addition & 1 deletion bin/fm-harness.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ detect_own() {
# Layer 1: environment markers for verified harnesses.
[ "${CLAUDECODE:-}" = "1" ] && { echo claude; return; }
[ "${PI_CODING_AGENT:-}" = "true" ] && { echo pi; return; }
# grok sets GROK_AGENT=1 for its child/tool processes (verified, grok 0.2.73).
# grok sets GROK_AGENT=1 for its child/tool processes (re-verified, grok 0.2.111).
# It does NOT set CLAUDECODE despite being Claude-Code-compatible, so this marker
# is unambiguous when firstmate runs natively on grok.
[ "${GROK_AGENT:-}" = "1" ] && { echo grok; return; }
Expand Down
3 changes: 2 additions & 1 deletion bin/fm-supervise-daemon.sh
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@
# (default 300)
# FM_HOUSEKEEPING_TICK seconds between housekeeping passes while
# the watcher is mid-cycle (default 15)
# FM_BUSY_REGEX OR-ed busy signatures (mirrors fm-watch.sh)
# FM_BUSY_REGEX OR-ed busy signatures overriding the shared
# fm-tmux-lib.sh default
# FM_COMPOSER_IDLE_RE empty-composer regex applied after dim-ghost
# and structural border stripping (default:
# bare prompt glyphs plus busy footers)
Expand Down
13 changes: 7 additions & 6 deletions bin/fm-tmux-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@
# tmux adapter does not paper over a herdr-specific shape.
#
# Per-harness override: FM_COMPOSER_IDLE_RE matches an empty composer after
# ghost and structural border stripping. FM_BUSY_REGEX overrides the busy
# footer set (mirrors fm-watch.sh / the daemon).
# ghost and structural border stripping. FM_BUSY_REGEX overrides the shared busy
# footer set consumed by fm-watch.sh and the daemon.
#
# All functions are `set -u` and `set -e` safe (guarded tmux calls, explicit
# returns) so they can be sourced into either context.
Expand All @@ -60,10 +60,11 @@
# shellcheck source=bin/fm-composer-lib.sh
. "$(dirname -- "${BASH_SOURCE[0]}")/fm-composer-lib.sh"

# Busy footers per harness (mirror fm-watch.sh). claude/codex: "esc to
# interrupt"; opencode: "esc interrupt"; pi: "Working..."; grok: "Ctrl+c:cancel"
# (grok's mid-turn cancel hint, shown iff a turn is running - verified grok 0.2.73).
FM_TMUX_BUSY_REGEX_DEFAULT='esc (to )?interrupt|Working\.\.\.|Ctrl\+c:cancel'
# Shared busy footers consumed by fm-watch.sh and the daemon. claude/codex:
# "esc to interrupt"; opencode: "esc interrupt"; pi: "Working..."; grok:
# "Ctrl+c:cancel" or "Esc:cancel" (mid-turn cancel hints, shown iff a turn is
# running - verified grok 0.2.73 and 0.2.111).
FM_TMUX_BUSY_REGEX_DEFAULT='esc (to )?interrupt|Working\.\.\.|(Ctrl\+c|Esc):cancel'

# fm_tmux_strip_ghost: thin adapter over the shared, fleet-wide ghost extractor
# fm_composer_strip_ghost (bin/fm-composer-lib.sh). It drops de-emphasised
Expand Down
12 changes: 7 additions & 5 deletions bin/fm-watch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,14 @@ CHECK_TIMEOUT=${FM_CHECK_TIMEOUT:-30} # seconds allowed per *.check.sh
SIGNAL_GRACE=${FM_SIGNAL_GRACE:-30} # seconds to linger after a signal so trailing
# signals (a status write, then the same turn's
# turn-end hook) coalesce into one wake
# Busy signatures per harness, OR-ed. Extend via env when new adapters are verified.
# Shared busy signatures from fm-tmux-lib.sh, OR-ed across harnesses.
# Extend via env when new adapters are verified.
# claude/codex: "esc to interrupt"; opencode: "esc interrupt"; pi: "Working...";
# grok: "Ctrl+c:cancel" (the mid-turn cancel hint in grok's keybind bar, shown iff a
# turn is running; absent when idle - verified grok 0.2.73, ASCII to avoid the
# locale fragility of matching grok's braille spinner glyph directly).
BUSY_REGEX=${FM_BUSY_REGEX:-'esc (to )?interrupt|Working\.\.\.|Ctrl\+c:cancel'}
# grok: "Ctrl+c:cancel" or "Esc:cancel" (mid-turn cancel hints in grok's keybind
# bar, shown iff a turn is running; absent when idle - verified grok 0.2.73 and
# 0.2.111, ASCII to avoid the locale fragility of matching grok's braille spinner
# glyph directly).
BUSY_REGEX=${FM_BUSY_REGEX:-$FM_TMUX_BUSY_REGEX_DEFAULT}
# Always-on wake triage: most wakes during a long crew validation are benign (a
# working: note or turn-end while a pipeline runs, a no-change heartbeat). Rather
# than wake firstmate's LLM for each, this watcher classifies every wake in bash
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ FM_STALE_WORKTREE_LOCK_RETRY_WAIT_SECS= # legacy alias for FM_TREEHOUSE_RETURN
FM_FLEET_SYNC_PACKED_REFS_LOCK_RETRIES=3 # fetch retries after fm-fleet-sync.sh hits the orphaned .git/packed-refs.lock signature
FM_FLEET_SYNC_PACKED_REFS_LOCK_RETRY_WAIT_SECS=1 # seconds fm-fleet-sync.sh waits before each of those retries
FM_FLEET_SYNC_PACKED_REFS_LOCK_AGE_SECS=30 # min mtime age before fm-fleet-sync.sh treats a leftover packed-refs.lock as provably stale
FM_BUSY_REGEX='esc (to )?interrupt|Working\.\.\.|Ctrl\+c:cancel' # busy-pane signatures, shared by watcher, fm-crew-state pane fallback, and tmux helper
FM_BUSY_REGEX='esc (to )?interrupt|Working\.\.\.|(Ctrl\+c|Esc):cancel' # busy-pane signatures, shared by watcher, fm-crew-state pane fallback, and tmux helper
FM_COMPOSER_IDLE_RE= # optional empty-composer regex, applied after ghost and border stripping
FM_COMPOSER_GHOST_LUMA_MAX=128 # fleet-wide: max perceived luminance (0.299R+0.587G+0.114B, 0-255) for a TRUECOLOR foreground to count as de-emphasised ghost/placeholder text and be stripped; dim/faint (SGR 2) is stripped regardless. Assumes a dark terminal theme (bin/fm-composer-lib.sh's fm_composer_strip_ghost, shared by the tmux and herdr composer readers)
GROK_HOME= # optional Grok config home for firstmate's global grok turn-end hook; defaults to ~/.grok
Expand Down
37 changes: 36 additions & 1 deletion tests/fm-grok-harness.test.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Behavior tests for Grok-harness hook authentication, teardown cleanup, and session-lock holder detection.
# Behavior tests for Grok-harness busy detection, hook authentication, teardown cleanup, and session-lock holder detection.
set -u

# shellcheck source=tests/lib.sh
Expand All @@ -9,6 +9,40 @@ SPAWN="$ROOT/bin/fm-spawn.sh"
TEARDOWN="$ROOT/bin/fm-teardown.sh"
TMP_ROOT=$(fm_test_tmproot fm-grok-harness)

test_grok_busy_signatures_match_verified_footers() {
local regex
# shellcheck source=bin/fm-tmux-lib.sh
. "$ROOT/bin/fm-tmux-lib.sh"
regex=$FM_TMUX_BUSY_REGEX_DEFAULT
printf '%s\n' 'Shift+Tab:mode │ Ctrl+c:cancel │ Ctrl+x:shortcuts' \
| grep -qiE "$regex" \
|| fail "grok 0.2.73 busy footer did not match the shared busy regex"
printf '%s\n' 'Shift+Tab:mode │ Esc:cancel │ Ctrl+x:shortcuts' \
| grep -qiE "$regex" \
|| fail "grok 0.2.111 busy footer did not match the shared busy regex"
if printf '%s\n' 'Shift+Tab:mode │ Ctrl+x:shortcuts' | grep -qiE "$regex"; then
fail "grok 0.2.111 idle footer matched the shared busy regex"
fi
assert_grep "BUSY_REGEX=\${FM_BUSY_REGEX:-\$FM_TMUX_BUSY_REGEX_DEFAULT}" \
"$ROOT/bin/fm-watch.sh" \
"watcher busy regex drifted from the shared busy regex"
(
tmux() { printf '%s\n' 'Shift+Tab:mode │ Ctrl+c:cancel │ Ctrl+x:shortcuts'; }
fm_pane_is_busy grok-pane
) || fail "shared tmux pane classifier did not recognize grok 0.2.73 as busy"
(
tmux() { printf '%s\n' 'Shift+Tab:mode │ Esc:cancel │ Ctrl+x:shortcuts'; }
fm_pane_is_busy grok-pane
) || fail "shared tmux pane classifier did not recognize grok 0.2.111 as busy"
if (
tmux() { printf '%s\n' 'Shift+Tab:mode │ Ctrl+x:shortcuts'; }
fm_pane_is_busy grok-pane
); then
fail "shared tmux pane classifier treated grok 0.2.111 idle footer as busy"
fi
pass "verified grok busy footers distinguish busy from idle"
}

make_spawn_fakebin() {
local dir=$1 fakebin
fakebin=$(fm_fakebin "$dir")
Expand Down Expand Up @@ -137,6 +171,7 @@ SH
pass "fm-lock recognizes grok harness processes"
}

test_grok_busy_signatures_match_verified_footers
test_grok_hook_requires_registered_token
test_grok_teardown_removes_pointer_and_token
test_fm_lock_recognizes_grok_holder
2 changes: 1 addition & 1 deletion tests/fm-lint.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ test_source_graph_boundaries_keep_every_owner() {
grep -q '^[[:space:]]*# shellcheck source=bin/' "$file" || continue
production_context_tests="${production_context_tests}$(basename "$file")|"
done
[ "$production_context_tests" = 'fm-backend-herdr.test.sh|fm-daemon.test.sh|fm-pending-reply.test.sh|fm-secondmate-sync.test.sh|' ] \
[ "$production_context_tests" = 'fm-backend-herdr.test.sh|fm-daemon.test.sh|fm-grok-harness.test.sh|fm-pending-reply.test.sh|fm-secondmate-sync.test.sh|' ] \
|| fail "only callback/variable interop tests may retain production source context: $production_context_tests"
pass "dispatcher, adapters, production owner, and tests have explicit lint boundaries"
}
Expand Down
Loading