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
33 changes: 32 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, and grok.
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, grok, and agy.
user-invocable: false
metadata:
internal: true
Expand Down Expand Up @@ -58,6 +58,7 @@ The supported launch-profile flags below were verified locally on 2026-06-30 wit
| grok | `--model <model>` | `--reasoning-effort <low\|medium\|high\|xhigh>` | Verified on grok 0.2.73. `--effort` parses too, but firstmate's profile axis is reasoning effort. `--reasoning-effort max` is rejected, so `max` is omitted. |
| pi | `--model <model>` | `--thinking <low\|medium\|high\|xhigh>` | Verified on pi 0.80.2. `max` prints an invalid-thinking warning, so firstmate omits Pi effort when the requested effort is `max`. |
| 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. |
| agy | `--model <model>` | none for firstmate's interactive launch | Verified on agy 1.0.16. The installed CLI exposes `--model` only; no verified effort or reasoning knob on the interactive launch path. |

When a requested effort value is outside the harness-specific accepted set, `fm-spawn` records the requested `effort=` in meta but emits no effort flag for that harness.
This preserves launch success instead of passing a known-bad value.
Expand All @@ -72,6 +73,7 @@ Natural language is acceptable if uncertain.
- 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.
- agy: `/<skill>` (e.g. `/no-mistakes`), same as claude; use natural language if uncertain.

## claude (VERIFIED)

Expand Down Expand Up @@ -182,6 +184,35 @@ Pin `[hints] project_picker_disabled = true` in `~/.grok/config.toml` if a non-p

**Known gap, unfixed (found 2026-07-03, not yet in scope of any fix):** a freshly-dismissed, never-typed-into grok composer shows a placeholder ("Type a message...") styled with a dark 24-bit TRUECOLOR foreground, not the SGR-2 dim/faint attribute `fm_tmux_strip_ghost` detects, so it is NOT stripped and reads as real pending text - `FM_COMPOSER_IDLE_RE` is NOT already set to cover it. Worse, live-verified: in that exact pristine placeholder-only state, tmux's own `#{cursor_y}` points at the composer box's BOTTOM BORDER row, one row below the actual text row (the box appears to render one row lower before any real typing starts); once real text is typed the cursor correctly aligns with the text row again. A correct fix needs a row-window read near `cursor_y` (or a structural scan like the herdr adapter's composer-row finder, `bin/backends/herdr.sh`), not just a wider idle regex. In practice `fm-spawn` launches grok with the brief as its initial prompt, so a live task's composer is never observed in this pristine pre-typing state - but this is unverified for every path (e.g. a steer sent before grok's first real turn settles) and needs dedicated investigation before relying on it.

## agy (VERIFIED 2026-07-05, agy 1.0.16; spawn nudge 2026-07-05)

Antigravity CLI (`agy`), a Claude-Code-compatible TUI from Google Cloud Code.
Launch with a positional prompt: `agy --dangerously-skip-permissions "$(cat <brief>)"`.

| Fact | Value |
|---|---|
| Busy-pane signature | `esc to interrupt` (Claude-compatible footer; covered by the default `FM_BUSY_REGEX`) |
| Exit command | `/exit` (slash popup; `fm-send`'s 1.2s `/` settle applies) |
| Interrupt | single Escape |
| Skill invocation | `/<skill>` is rejected as unknown in agy 1.0.16; use natural language or shell `no-mistakes axi run` |
| Autonomy | `--dangerously-skip-permissions` |
| Resume | `agy --continue` or `agy --conversation <id>` |
| `fm-send` settle | 1.2s on all plain-text steers (agy often leaves text in the composer on the first Enter) |
| Post-spawn nudge | `fm-spawn` waits `FM_AGY_SPAWN_SETTLE` (default 10s), then sends a Begin-now steer with a second Enter backup |

**Positional brief does not auto-start.**
Verified: `agy ... "$(cat brief)"` opens the TUI with an empty composer; the brief is not processed until firstmate steers.
`fm-spawn` now sends the standard Begin-now nudge automatically after launch settle.
Without it, crewmates often search the filesystem instead of using the worktree cwd.

**no-mistakes headless path.**
Pipeline steps invoke `bin/no-mistakes-agy-shim` (or `~/.local/bin/no-mistakes-agy-shim`), which runs `agy -p` once and emits Claude stream-json.
Headless `agy -p` can hang indefinitely; the shim honors `FM_AGY_SHIM_TIMEOUT` (default 600s) when `timeout` is on PATH.

First launch on a machine may require Google sign-in.
First launch per directory shows a directory-trust dialog ("Do you trust the contents of this project?"); accept with Enter on "Yes, I trust this folder".
`fm-spawn` installs the same per-worktree `.claude/settings.local.json` Stop hook as claude so turn-end touches `state/<id>.turn-ended`.

Turn-end hook: grok fires a `Stop` hook at every turn boundary, giving firstmate a precise per-turn wake instead of only stale-pane detection.
grok loads PROJECT hooks (`<worktree>/.grok/hooks/`, `<worktree>/.claude/settings.local.json`) only after the folder is granted hook-trust in `~/.grok/trusted_folders.toml`, which is not automatic and which firstmate will not establish by editing grok's own managed trust store.
GLOBAL hooks in `~/.grok/hooks/` are always trusted and load on first launch.
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ If the captain expresses a standing dispatch preference such as "use grok for ne
Crewmates default to the same harness you are running on.
The captain may override the static default at any time, typically at bootstrap: record the choice in `config/crew-harness` (a single adapter name; absent or `default` means mirror your own harness).
Resolve `default` with `bin/fm-harness.sh`; resolve the active static crewmate harness with `bin/fm-harness.sh crew`.
Verified adapter names are `claude`, `codex`, `opencode`, `pi`, and `grok`.
Verified adapter names are `claude`, `codex`, `opencode`, `pi`, `grok`, and `agy`.

### Crew dispatch profiles

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Full detail on every feature lives in [docs/architecture.md](docs/architecture.m

## Quick Start

**Requirements:** a verified agent harness (claude, codex, opencode, pi, or grok), git with GitHub auth, and tmux for the reference session backend.
**Requirements:** a verified agent harness (claude, codex, opencode, pi, grok, or agy), git with GitHub auth, and tmux for the reference session backend.
The first mate detects and offers to install everything else.

```sh
Expand Down
4 changes: 2 additions & 2 deletions bin/fm-bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -332,13 +332,13 @@ crew_dispatch_validate() {
return 0
fi
err=$(jq -r '
def verified($h): ["claude","codex","opencode","pi","grok"] | index($h);
def verified($h): ["claude","codex","opencode","pi","grok","agy"] | index($h);
def effort_ok($h; $e):
if $e == null then true
elif ($e | type) != "string" then false
elif $h == "claude" then (["low","medium","high","xhigh","max"] | index($e))
elif ($h == "codex" or $h == "grok" or $h == "pi") then (["low","medium","high","xhigh"] | index($e))
elif $h == "opencode" then false
elif ($h == "opencode" or $h == "agy") then false
else true
end;
def bad_efforts:
Expand Down
4 changes: 3 additions & 1 deletion bin/fm-harness.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Detect the agent harness this process tree runs on.
# Usage: fm-harness.sh print own harness: claude|codex|opencode|pi|grok|unknown
# Usage: fm-harness.sh print own harness: claude|codex|opencode|pi|grok|agy|unknown
# fm-harness.sh crew print the effective CREWMATE harness
# (config/crew-harness; "default" resolves to own)
# fm-harness.sh secondmate print the harness the PRIMARY uses to launch
Expand Down Expand Up @@ -44,6 +44,7 @@ detect_own() {
*codex*) echo codex; return ;;
*opencode*) echo opencode; return ;;
*grok*) echo grok; return ;;
*agy*) echo agy; return ;;
pi) echo pi; return ;;
node*|python*)
# Bare interpreter: match the harness name in its script path.
Expand All @@ -53,6 +54,7 @@ detect_own() {
*codex*) echo codex; return ;;
*opencode*) echo opencode; return ;;
*grok*) echo grok; return ;;
*agy*) echo agy; return ;;
*" pi "*|*/pi) echo pi; return ;;
esac ;;
esac
Expand Down
2 changes: 1 addition & 1 deletion bin/fm-lock.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ LOCK="$STATE/.lock"
mkdir -p "$STATE"

# Known harness command names; extend when a new adapter is verified.
HARNESS_RE='claude|codex|opencode|grok|^pi$'
HARNESS_RE='(^|[^a-zA-Z0-9_-])(claude|codex|opencode|grok|agy|pi)([^a-zA-Z0-9_-]|$)'

harness_pid() {
local pid=$$ comm args
Expand Down
7 changes: 6 additions & 1 deletion bin/fm-send.sh
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,12 @@ else
\$*)
if [ "$TARGET_HARNESS" = codex ]; then settle=1.2; else settle=0.3; fi
;;
*) settle=0.3 ;;
*)
case "$TARGET_HARNESS" in
agy*) settle=1.2 ;;
*) settle=0.3 ;;
esac
;;
esac
retries=${FM_SEND_RETRIES:-3}
sleep_s=${FM_SEND_SLEEP:-0.4}
Expand Down
21 changes: 17 additions & 4 deletions bin/fm-spawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
# profile consultation. A --secondmate spawn is exempt and resolves the SECONDMATE
# harness (config/secondmate-harness -> config/crew-harness -> own), so the
# secondmate-vs-crewmate split is DURABLE across every respawn (recovery,
# /updatefirstmate, restart). A bare adapter name (claude|codex|opencode|pi|grok)
# /updatefirstmate, restart). A bare adapter name (claude|codex|opencode|pi|grok|agy)
# overrides it for this spawn (either kind). A non-flag string containing
# whitespace is treated as a RAW launch command - the escape hatch for verifying
# new adapters.
Expand Down Expand Up @@ -267,7 +267,7 @@ FIRSTMATE_HOME=

if [ "$KIND" = secondmate ]; then
case "${POS[1]:-}" in
''|claude|codex|opencode|pi|grok)
''|claude|codex|opencode|pi|grok|agy)
ARG3=${POS[1]:-}
;;
*' '*)
Expand Down Expand Up @@ -328,6 +328,8 @@ launch_template() {
# launch command - it is a Stop-event hook installed below (global hook +
# per-task pointer), so the template is identical for ship/scout/secondmate.
grok) printf '%s' 'grok --always-approve __MODELFLAG____EFFORTFLAG__"$(cat __BRIEF__)"' ;;
# agy (Antigravity CLI): Claude-Code-compatible TUI; verified 2026-07-05, agy 1.0.16.
agy) printf '%s' 'agy --dangerously-skip-permissions __MODELFLAG__"$(cat __BRIEF__)"' ;;
*) return 1 ;;
esac
}
Expand Down Expand Up @@ -415,7 +417,7 @@ model_flag_for_harness() {
local harness=$1 model=$2
[ -n "$model" ] && [ "$model" != default ] || return 0
case "$harness" in
claude|codex|opencode|pi|grok)
claude|codex|opencode|pi|grok|agy)
printf -- '--model %s ' "$(shell_quote "$model")"
;;
esac
Expand Down Expand Up @@ -828,7 +830,7 @@ exclude_path() {
}
if [ "$KIND" != secondmate ]; then
case "$HARNESS" in
claude*)
claude*|agy*)
mkdir -p "$WT/.claude"
cat > "$WT/.claude/settings.local.json" <<EOF
{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"touch '$TURNEND'"}]}]}}
Expand Down Expand Up @@ -998,4 +1000,15 @@ spawn_send_literal "$T" "$LAUNCH"
sleep 0.3
spawn_send_key "$T" Enter

# agy loads a positional brief into the session but does not auto-start the turn
# (verified 2026-07-05, agy 1.0.16). Nudge after trust-dialog/TUI settle.
if [[ "$HARNESS" == agy* ]]; then
sleep "${FM_AGY_SPAWN_SETTLE:-10}"
spawn_send_literal "$T" 'Begin now per brief: verify worktree isolation, create your task branch, and execute the full brief contract.'
sleep 1.2
spawn_send_key "$T" Enter
sleep 1.2
spawn_send_key "$T" Enter
fi

echo "spawned $ID harness=$HARNESS kind=$KIND mode=$MODE yolo=$YOLO window=$META_WINDOW worktree=$WT"
155 changes: 155 additions & 0 deletions bin/no-mistakes-agy-shim
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
#!/usr/bin/env bash
# Claude-compatible shim for no-mistakes: runs agy and emits stream-json events.
# Install to PATH (e.g. ~/.local/bin/no-mistakes-agy-shim) and point
# agent_path_override.claude at it in ~/.no-mistakes/config.yaml.
set -euo pipefail

AGY_BIN="${NO_MISTAKES_AGY_BIN:-agy}"
AGY_TIMEOUT="${FM_AGY_SHIM_TIMEOUT:-600}"

if ! command -v "$AGY_BIN" >/dev/null 2>&1; then
echo "no-mistakes-agy-shim: agy not found (set NO_MISTAKES_AGY_BIN)" >&2
exit 127
fi

prompt=""
schema=""
declare -a agy_args=()

args=("$@")
i=0
while [ "$i" -lt "${#args[@]}" ]; do
arg="${args[$i]}"
case "$arg" in
-p|--print|--prompt)
i=$((i + 1))
prompt="${args[$i]:-}"
;;
--json-schema)
i=$((i + 1))
schema="${args[$i]:-}"
;;
--verbose|--dangerously-skip-permissions) ;;
--output-format|--permission-mode)
i=$((i + 1))
;;
--output-format=*|--permission-mode=*) ;;
--model|--project|--sandbox|--add-dir)
agy_args+=("$arg")
i=$((i + 1))
agy_args+=("${args[$i]:-}")
;;
--model=*|--project=*|--sandbox=*|--add-dir=*)
agy_args+=("$arg")
;;
*)
if [ -z "$prompt" ] && [[ "$arg" != -* ]]; then
prompt="$arg"
fi
;;
esac
i=$((i + 1))
done

if [ -z "$prompt" ]; then
echo "no-mistakes-agy-shim: missing prompt" >&2
exit 2
fi

full_prompt="$prompt"
if [ -n "$schema" ]; then
full_prompt="${prompt}

Respond with one valid JSON object only (no markdown fences) matching this schema:
${schema}"
fi

stderr_file=$(mktemp)
output_file=$(mktemp)
cleanup() {
rm -f "$stderr_file" "$output_file"
}
trap cleanup EXIT

agy_ec=0
set +e
if command -v timeout >/dev/null 2>&1; then
timeout --signal=TERM "$AGY_TIMEOUT" \
"$AGY_BIN" --dangerously-skip-permissions "${agy_args[@]}" -p "$full_prompt" >"$output_file" 2>"$stderr_file"
else
"$AGY_BIN" --dangerously-skip-permissions "${agy_args[@]}" -p "$full_prompt" >"$output_file" 2>"$stderr_file"
fi
agy_ec=$?
set -e

if [ "$agy_ec" -ne 0 ]; then
err=$(tr '\n' ' ' <"$stderr_file" | sed 's/ */ /g')
printf '{"type":"result","subtype":"error","is_error":true}\n'
if [ "$agy_ec" -eq 124 ]; then
echo "agy timed out after ${AGY_TIMEOUT}s (set FM_AGY_SHIM_TIMEOUT to override)" >&2
else
echo "agy exited: ${err:-unknown error}" >&2
fi
exit "$agy_ec"
fi

SCHEMA_JSON="$schema" AGY_OUTPUT_FILE="$output_file" python3 - <<'PY'
import json
import os
import sys

output_file = os.environ.get("AGY_OUTPUT_FILE", "")
schema_raw = os.environ.get("SCHEMA_JSON", "")
structured = None

text = ""
if output_file and os.path.exists(output_file):
try:
with open(output_file, "r", encoding="utf-8", errors="replace") as f:
text = f.read()
except Exception as e:
sys.stderr.write(f"Error reading output file: {e}\n")

if schema_raw:
decoder = json.JSONDecoder()
for i in range(len(text)):
if text[i] in ('{', '['):
try:
obj, end_idx = decoder.raw_decode(text[i:])
structured = obj
break
except json.JSONDecodeError:
pass

assistant = {
"type": "assistant",
"message": {
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"cache_read_input_tokens": 0,
"cache_creation_input_tokens": 0,
},
"content": [{"type": "text", "text": text}],
},
}
print(json.dumps(assistant), flush=True)

if schema_raw and structured is None:
result = {
"type": "result",
"subtype": "error",
"is_error": True,
}
print(json.dumps(result), flush=True)
sys.exit(1)

result = {
"type": "result",
"subtype": "success",
"is_error": False,
}
if structured is not None:
result["structured_output"] = structured
print(json.dumps(result), flush=True)
PY
6 changes: 3 additions & 3 deletions docs/examples/crew-dispatch.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
},
{
"when": "The task is a big or ambiguous multi-file feature, a risky refactor, or work that requires holding many moving parts in mind.",
"use": { "harness": "codex", "model": "gpt-5.5", "effort": "high" },
"why": "Use a stronger coding profile for broad design and implementation work."
"use": { "harness": "agy", "model": "Gemini 3.5 Flash (High)" },
"why": "Use Agy for broad design and implementation work when Codex is unavailable."
}
],
"default": { "harness": "codex", "model": "gpt-5.5", "effort": "medium" }
"default": { "harness": "grok" }
}
Loading
Loading