Skip to content

feat(bin): add agy crew harness adapter pinned to gemini-3.1-pro-high - #2366

Closed
BohnBawerick wants to merge 13 commits into
kunchenguid:mainfrom
BohnBawerick:fm/hz-agy-adapter
Closed

BohnBawerick wants to merge 13 commits into
kunchenguid:mainfrom
BohnBawerick:fm/hz-agy-adapter

Conversation

@BohnBawerick

Copy link
Copy Markdown

Intent

Verify agy as a firstmate crew harness and wire the adapter so crew dispatch can use Antigravity CLI, not only Grok.

Standing rules: agy is always gemini-3.1-pro-high, never a claude-* model it also offers (the arbiter reading agy output is Claude; running Claude inside agy collapses two voices). agy must not be dispatched until the adapter is verified; this work is evidence, not optimism. Adding agy must not put another harness at risk.

Verification required: launch a trivial supervised task through fm-spawn's raw-launch-command escape hatch; establish empirically the launch command, autonomy flag, model and effort flags, trust/permission dialog on a fresh worktree and what clears it, busy/idle signal, composer empty vs pending, interrupt key and exit command, whether a status-file append from inside agy wakes firstmate, and whether the plugin subcommand can carry a turn-end hook.

Wire only what was proved, in every owner the harness-adapters skill names: fm-spawn.sh, fm-busy-lib.sh, fm-composer-lib.sh (the one fleet-wide composer owner), fm-control-lib.sh, fm-harness.sh, and the skill itself. Be explicit about surfaces not verified (secondmate launch, primary turn-end guard, watcher-arm guard). Do not add agy to config/crew-dispatch.json; verification and routing are separate, and routing is the captain's.

Keep it test-driven and simple. If a surface fails verification, land what works and state plainly what does not.

Observed and accepted implementation choices: pin --model gemini-3.1-pro-high and omit --effort because pairing --effort with a -high model id made agy 1.1.12 fall back to Gemini 3.6 Flash High; keep --prompt-interactive last because placing it before --model consumed --model as the prompt; --dangerously-skip-permissions and --model are named options (agy --help) and so are order-independent relative to each other, and firstmate emits --dangerously-skip-permissions then --model then --prompt-interactive; refuse claude- models at spawn; treat agy as crewmate/scout only because secondmate, primary turn-end, and watcher-arm were not verified; leave semantic busy unknown because Stop does not fire on Escape interrupt; install a gated global Stop hook in ~/.gemini/config/hooks.json because project-local .agents/hooks.json loaded zero files; accept the workspace trust dialog with Enter when visible because --dangerously-skip-permissions does not suppress it; scan every workspacePaths entry for the turn-end pointer; share PATH binary resolution across muse/agy/kimi.

Captain-decided composer classifier narrowing, already applied, do not reopen as unanswered design:

  1. Geometry/VALID gate and only the verified greater-than glyph.
  2. On identity-capable backends, pi identity wins over the glyph shortcut; a live pi greater-than draft must never read empty.
  3. Refuse the shortcut while identity is still unfetched (has_identity=1 and identity empty) so the first classify emits need-identity.
  4. Refuse the shortcut when identity is unavailable (has_identity != 1), restoring unknown on zellij, cmux, and orca. agy is not verified on those three backends, so there is no working agy behaviour there to preserve.

Push target is the fork; open the PR against upstream kunchenguid/firstmate (same route as hz-kimi-trust). The PR body MUST include these two notes:

  1. Reconsider the glyph-stands-in-for-identity design. Three same-theme findings rewrote it so identity is required and the glyph is only a post-identity shape check. The original "classifies without identity" claim is false. A fourth finding of this theme should stop patching the shortcut.
  2. Later review rounds dropped these two ask-user findings without a decision; they must not be silently lost: agy-trust-wait-no-midturn-ready-signal (on identityless backends the trust wait burns the full poll budget because composer is unknown and mid-turn footer is esc to cancel), and agy-claude-model-refusal-is-post-stop-on-relaunch (agy's claude-* model refusal is spawn-side only, so a mistyped relaunch --model claude-* stops a healthy crewmate before spawn refuses).

What Changed

  • Wires agy (Antigravity CLI) as a crewmate/scout-only adapter across every harness owner: fm-spawn.sh gains an agy launch template (--dangerously-skip-permissions --model <model> --prompt-interactive <brief> with --prompt-interactive last), a PATH binary resolver, a --model gemini-3.1-pro-high default pin, a hard refusal of claude-* models, no --effort flag, a bounded workspace-trust dialog wait that presses Enter at most once, and refusals for both local and remote secondmate launches; fm-harness.sh detects it by exact agy process ancestry, fm-control-lib.sh registers Escape interrupt / /exit / wiring paths, fm-busy-lib.sh documents the absent armed busy writer, fm-session-lock-lib.sh and bin/backends/tmux.sh classify agy as an agent process, and fm-bootstrap.sh accepts it as a verified harness with no effort axis.
  • Adds bin/fm-agy-turnend-hook.sh, the sole owner of one surgically named Firstmate key in the global ~/.gemini/config/hooks.json, installing a silent always-zero Stop hook that scans every workspacePaths entry for a .fm-agy-turnend pointer resolving through a private token registry; install refuses on symlinked, non-regular, malformed, or foreign config/script/registry state, and remove de-registers the key before the file guards run.
  • Narrows the composer classifier so agy's separated > shape is recognised only behind identity: a new FM_COMPOSER_SEPARATED_PROMPT_GLYPHS set holds > alone, the shortcut requires the PAIR_VALID geometry gate plus has_identity=1, and it defers to _fm_composer_pi_verdict when identity is unfetched or names pi. Teardown replaces the per-adapter grok/kimi cleanup functions with remove_turnend_auth and remove_turnend_worktree_pointers, both driven by the new fm_control_turnend_global_harnesses list, and resolve_muse_binary/resolve_kimi_binary/resolve_agy_binary now share resolve_path_binary. New tests/fm-agy-harness.test.sh (19 cases) plus additions to the composer, bootstrap, control, and tmux-liveness suites cover launch shape, model refusal, secondmate refusal, marker clearing, raw-launch, hook install/remove gating, payload framings, trust-dialog success and failure, and teardown. config/crew-dispatch.json is deliberately untouched.

Notes for review

  1. Reconsider the glyph-stands-in-for-identity design. Three same-theme findings rewrote it so identity is required and the glyph is only a post-identity shape check; the original "classifies without identity" claim is false. A fourth finding of this theme should stop patching the shortcut rather than narrowing it again.
  2. Two ask-user findings were dropped in later review rounds without a decision and must not be silently lost: agy-trust-wait-no-midturn-ready-signal (on identityless backends the trust wait burns the full poll budget because composer state is unknown and the mid-turn footer reads esc to cancel), and agy-claude-model-refusal-is-post-stop-on-relaunch (the claude-* model refusal is spawn-side only, so a mistyped relaunch with --model claude-* stops a healthy crewmate before spawn refuses).

Risk Assessment

⚠️ Medium: All three prior findings are genuinely fixed with bounded, behavior-tested changes and the intent's acceptance criteria are met, but the change still reaches fleet-shared owners beyond agy - the composer classifier's empty verdict for a separated > pair and teardown's pointer removal for every harness - so it is safe to merge with the two informational items as follow-ups rather than being purely well-bounded.

Testing

Ran the new agy harness suite plus the fleet-wide suites the change touches (composer lib and ghost, control plane and relaunch, real-tmux liveness, teardown, busy adapter wiring, zellij/cmux/orca) - all pass. On top of that I drove a full manual end-to-end: a real tmux server, the real fm-spawn.sh/fm-peek.sh/fm-teardown.sh, and a stubbed agy binary (sandboxed HOME, no Google quota spent), capturing the captain-visible spawn line, the exact argv agy would receive, the rendered crewmate pane, the classifier verdict on that live pane, the gated Stop-hook wake, both spawn refusals, and full teardown cleanup; the launch flags and the pinned/forbidden model ids were cross-checked against the really installed agy 1.1.13. Visual evidence is the captured TUI pane text from fm-peek.sh, which is the actual end-user surface for this terminal-only change - there is no graphical surface to screenshot. One pre-existing, environment-caused failure in tests/fm-bootstrap.test.sh is unrelated to this change and reproduces identically at the base commit.

Evidence: agy crew adapter end-to-end transcript (real tmux, real fm-spawn/fm-peek/fm-teardown)

===== 1. captain-visible spawn result (bin/fm-spawn.sh <id> <project> agy) ===== spawned agy-e2e-x1 harness=agy kind=ship mode=no-mistakes yolo=off window=firstmate:fm-agy-e2e-x1 worktree=/tmp/fm-agy-e2e/wt exit=0 ===== 2. argv the real agy binary would have received ===== --dangerously-skip-permissions --model gemini-3.1-pro-high --prompt-interactive FIRSTMATE_OP: v1 launch-brief: Read docs/architecture.md and report the top-level layout in two sentences. ===== 3. recorded task metadata ===== harness=agy kind=ship model=gemini-3.1-pro-high ===== 4. turn-end wiring minted by the spawn ===== worktree pointer $SANDBOX/wt/.fm-agy-turnend -> token=fm.aWMC9Jt1z7EM registry entry ~/.gemini/config/fm-agy-turn-end.d/fm.aWMC9Jt1z7EM -> $SANDBOX/home/state/agy-e2e-x1.turn-ended hooks.json Stop key: {"fm-agy-turn-end": {"Stop": [{"type": "command", "command": "$SANDBOX/home/.gemini/config/fm-agy-turn-end.sh", "timeout": 10}]}} ===== 5. the live agy crewmate pane, as the captain sees it (bin/fm-peek.sh) ===== Antigravity CLI ──────────────────────────────────────────── > ──────────────────────────────────────────── ? for shortcuts Gemini 3.1 Pro · high ===== 6. composer verdict on that live pane (shared classifier) ===== composer-state: empty agent-liveness: alive ===== 6b. agy's Stop event wakes firstmate for THIS task only ===== Stop in the task worktree -> wake marker written: $SANDBOX/home/state/agy-e2e-x1.turn-ended Stop in an unrelated workspace -> no wake marker (correctly ignored) ===== 7. the same spawn refused for a claude-* model (standing pin) ===== error: agy must not run a claude-* model; the captain's standing pin is gemini-3.1-pro-high ===== 8. the same spawn refused as a secondmate (crewmate/scout only) ===== error: agy is a verified crewmate/scout adapter only and cannot run a secondmate; no primary supervision protocol has been verified for Antigravity CLI ===== 9. teardown retires every agy pointer (bin/fm-teardown.sh) ===== teardown agy-e2e-x1 complete (window firstmate:fm-agy-e2e-x1, worktree $SANDBOX/wt) retired: $SANDBOX/wt/.fm-agy-turnend retired: $SANDBOX/home/state/agy-e2e-x1.agy-turnend-token retired: $SANDBOX/home/.gemini/config/fm-agy-turn-end.d/fm.aWMC9Jt1z7EM


===== 1. captain-visible spawn result (bin/fm-spawn.sh <id> <project> agy) =====
warning: /tmp/fm-agy-e2e/home/data/agy-e2e-x1/brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode no-mistakes - confirm its definition of done matches
spawned agy-e2e-x1 harness=agy kind=ship mode=no-mistakes yolo=off window=firstmate:fm-agy-e2e-x1 worktree=/tmp/fm-agy-e2e/wt
exit=0

===== 2. argv the real agy binary would have received =====
$SANDBOX/bin/agy --dangerously-skip-permissions --model gemini-3.1-pro-high --prompt-interactive ⁣FIRSTMATE_OP: v1 launch-brief: Read docs/architecture.md and report the top-level layout in two sentences.
--- one argument per line ---
--dangerously-skip-permissions
--model
gemini-3.1-pro-high
--prompt-interactive
⁣FIRSTMATE_OP: v1 launch-brief: Read docs/architecture.md and report the top-level layout in two sentences.

===== 3. recorded task metadata =====
harness=agy
kind=ship
model=gemini-3.1-pro-high

===== 4. turn-end wiring minted by the spawn =====
worktree pointer  /tmp/fm-agy-e2e/wt/.fm-agy-turnend -> token=fm.aWMC9Jt1z7EM
state token       fm.aWMC9Jt1z7EM
registry entry    ~/.gemini/config/fm-agy-turn-end.d/fm.aWMC9Jt1z7EM -> $SANDBOX/home/state/agy-e2e-x1.turn-ended
hooks.json Stop key:
{
  "fm-agy-turn-end": {
    "Stop": [
      {
        "type": "command",
        "command": "$SANDBOX/home/.gemini/config/fm-agy-turn-end.sh",
        "timeout": 10
      }
    ]
  }
}

===== 5. the live agy crewmate pane, as the captain sees it (bin/fm-peek.sh) =====
Antigravity CLI
────────────────────────────────────────────
>
────────────────────────────────────────────
? for shortcuts                          Gemini 3.1 Pro · high




































===== 6. composer verdict on that live pane (shared classifier) =====
composer-state: empty
agent-liveness: alive

===== 6b. agy's Stop event wakes firstmate for THIS task only =====
Stop in the task worktree      -> wake marker written: $SANDBOX/home/state/agy-e2e-x1.turn-ended
Stop in an unrelated workspace -> no wake marker (correctly ignored)

===== 7. the same spawn refused for a claude-* model (standing pin) =====
error: agy must not run a claude-* model; the captain's standing pin is gemini-3.1-pro-high
exit=1

===== 8. the same spawn refused as a secondmate (crewmate/scout only) =====
error: agy is a verified crewmate/scout adapter only and cannot run a secondmate; no primary supervision protocol has been verified for Antigravity CLI
exit=1

===== 9. teardown retires every agy pointer (bin/fm-teardown.sh) =====
teardown: reaping leaked worktree process(es) for agy-e2e-x1: 88879 96655
teardown: force-killing leaked worktree process(es) for agy-e2e-x1: 88879
$SANDBOX/project: already current
teardown agy-e2e-x1 complete (window firstmate:fm-agy-e2e-x1, worktree $SANDBOX/wt)
Backlog: agy-e2e-x1 just finished. Run tasks-axi done agy-e2e-x1 --pr PR_URL, then run tasks-axi ready for dependency-cleared candidates, check date gates, and dispatch only work whose blockers are gone and date is due.
retired:       $SANDBOX/wt/.fm-agy-turnend
retired:       $SANDBOX/home/state/agy-e2e-x1.agy-turnend-token
retired:       $SANDBOX/home/.gemini/config/fm-agy-turn-end.d/fm.aWMC9Jt1z7EM
Evidence: real Antigravity CLI 1.1.13 model list (pin exists; claude-* ids the adapter refuses)

gemini-3.1-pro-high Gemini 3.1 Pro (High) gemini-3.1-pro-low Gemini 3.1 Pro (Low) claude-sonnet-4-6 Claude Sonnet 4.6 (Thinking) claude-opus-4-6-thinking Claude Opus 4.6 (Thinking) gpt-oss-120b-medium GPT-OSS 120B (Medium)

Fetching available models...
gemini-3.7-flash-high	Gemini 3.7 Flash (High)
gemini-3.7-flash-medium	Gemini 3.7 Flash (Medium)
gemini-3.7-flash-low	Gemini 3.7 Flash (Low)
gemini-3.6-flash-high	Gemini 3.6 Flash (High)
gemini-3.6-flash-medium	Gemini 3.6 Flash (Medium)
gemini-3.6-flash-low	Gemini 3.6 Flash (Low)
gemini-3.5-flash-high	Gemini 3.5 Flash (High)
gemini-3.5-flash-medium	Gemini 3.5 Flash (Medium)
gemini-3.5-flash-low	Gemini 3.5 Flash (Low)
gemini-3.1-pro-high	Gemini 3.1 Pro (High)
gemini-3.1-pro-low	Gemini 3.1 Pro (Low)
claude-sonnet-4-6	Claude Sonnet 4.6 (Thinking)
claude-opus-4-6-thinking	Claude Opus 4.6 (Thinking)
gpt-oss-120b-medium	GPT-OSS 120B (Medium)
Evidence: real agy --help: the three launch flags are named options

--dangerously-skip-permissions Auto-approve all tool permission requests without prompting --effort Reasoning effort for the current CLI session (low|medium|high) --model Model for the current CLI session --prompt-interactive Run an initial prompt interactively and continue the session

Usage of agy:
  --add-dir                       Add a directory to the workspace (repeatable) (default [])
  --agent                         Agent for the current CLI session
  -c                              Short alias for --continue
  --continue                      Continue the most recent conversation
  --conversation                  Resume a previous conversation by ID
  --dangerously-skip-permissions  Auto-approve all tool permission requests without prompting
  --disable-slash-commands        Disable slash command and skill expansion in print mode
  --effort                        Reasoning effort for the current CLI session (low|medium|high)
  -i                              Short alias for --prompt-interactive
  --json-schema                   Optional JSON schema string or path to a schema file to enforce structured output (for stream-json, only applicable to the final result)
  --log-file                      Override CLI log file path
  --mode                          Set the agent execution mode for this session (accept-edits, plan)
  --model                         Model for the current CLI session
  --new-project                   Create a new project for this session
  --output-format                 Output format for print mode (text, json, stream-json) (default text)
  -p                              Short alias for --print
  --print                         Run a single prompt non-interactively and print the response
  --print-timeout                 Timeout for print mode wait (default 5m0s)
  --project                       Project ID for the current CLI session
  --prompt                        Alias for --print
  --prompt-interactive            Run an initial prompt interactively and continue the session
  --sandbox                       Run in a sandbox with terminal restrictions enabled

Available subcommands:
  agent           List available agents
  agents          List available agents
  changelog       Show changelog and release notes
  help            Show help for subcommands
  install         Configure environment paths and shell settings
  models          List available models
  plugin          Manage plugins (install, uninstall, list, enable, disable)
  plugins         Alias for plugin
  update          Update CLI
Evidence: reproduction script for the end-to-end verification
#!/usr/bin/env bash
# End-to-end manual verification of the agy crew adapter against a REAL tmux
# server. The `agy` binary is stubbed so no Google AI quota is spent and no
# real ~/.gemini state is touched (HOME is redirected into the sandbox), but
# everything else is the real product path: bin/fm-spawn.sh -> tmux backend ->
# a live pane -> the shared composer classifier -> bin/fm-teardown.sh.
set -u
ROOT=$1
SANDBOX=$2
SOCKET=fm-agy-e2e

rm -rf "$SANDBOX"
mkdir -p "$SANDBOX/bin" "$SANDBOX/home/state" "$SANDBOX/home/config" \
         "$SANDBOX/home/projects" "$SANDBOX/home/data"

# --- stub agy: records its argv, then renders agy 1.1.12's real screens ------
cat > "$SANDBOX/bin/agy" <<'SH'
#!/usr/bin/env bash
set -u
printf '%s\n' "$0 $*" > "$AGY_ARGV_LOG"
for a in "$@"; do printf '%s\n' "$a" >> "$AGY_ARGV_LOG.args"; done
clear
# agy's workspace trust dialog: --dangerously-skip-permissions does not
# suppress it on an untrusted path; Yes is preselected and Enter accepts.
cat <<'EOF'
  Do you trust the contents of this project?

  > Yes, I trust this workspace
    No, exit
EOF
read -r _key
clear
# The verified idle composer: two solid rules around a shell `>` prompt.
printf 'Antigravity CLI\n'
printf '%s\n' '────────────────────────────────────────────'
printf '> \n'
printf '%s\n' '────────────────────────────────────────────'
printf '? for shortcuts                          Gemini 3.1 Pro · high\n'
printf '\033[3;3H'
exec sleep 900
SH
chmod +x "$SANDBOX/bin/agy"

for t in treehouse gh-axi gh; do
  printf '#!/usr/bin/env bash\nexit 0\n' > "$SANDBOX/bin/$t"
  chmod +x "$SANDBOX/bin/$t"
done

# --- project + task worktree ------------------------------------------------
ID=agy-e2e-x1
PROJ="$SANDBOX/project"
WT="$SANDBOX/wt"
mkdir -p "$PROJ" "$SANDBOX/home/data/$ID"
git init -q "$PROJ"
printf '# e2e project\n' > "$PROJ/README.md"
git -C "$PROJ" add README.md
git -C "$PROJ" -c user.email=e2e@local -c user.name=e2e commit -qm initial
git clone --quiet --bare "$PROJ" "$PROJ.origin.git"
git -C "$PROJ" remote add origin "file://$(cd "$PROJ.origin.git" && pwd)"
git -C "$PROJ" worktree add --quiet -b "fm/$ID" "$WT"

# The crew pane's `treehouse get` has to actually ENTER the task worktree, so
# it is a shell function (the real treehouse cd's the caller's shell too).
cat > "$SANDBOX/home/.bashrc" <<SH
treehouse() { [ "\${1:-}" = get ] && cd "$WT" || return 0; }
SH
printf '. "$HOME/.bashrc"\n' > "$SANDBOX/home/.bash_profile"
printf 'Read docs/architecture.md and report the top-level layout in two sentences.\n' \
  > "$SANDBOX/home/data/$ID/brief.md"
touch "$SANDBOX/home/state/.last-watcher-beat"

export AGY_ARGV_LOG="$SANDBOX/agy-argv.txt"

tmux -L "$SOCKET" kill-server 2>/dev/null
tmux -L "$SOCKET" new-session -d -s firstmate -x 120 -y 40
# Panes on this private socket inherit the sandbox, so nothing the crewmate
# window does can reach the captain's real ~/.gemini or ~/.local/bin/agy.
tmux -L "$SOCKET" set-environment -g HOME "$SANDBOX/home"
tmux -L "$SOCKET" set-environment -g PATH "$SANDBOX/bin:$PATH"
tmux -L "$SOCKET" set-environment -g AGY_ARGV_LOG "$SANDBOX/agy-argv.txt"
tmux -L "$SOCKET" set-environment -g FM_GATE_REFUSE_BYPASS 1
sleep 0.5

# Run the real spawn from INSIDE the tmux session, so the tmux backend talks to
# this server exactly as it does for the captain.
tmux -L "$SOCKET" new-window -t firstmate -n spawn -- bash -lc "
  export PATH='$SANDBOX/bin':\$PATH
  export HOME='$SANDBOX/home'
  export AGY_ARGV_LOG='$SANDBOX/agy-argv.txt'
  export FM_ROOT_OVERRIDE=''
  export FM_HOME='$SANDBOX/home'
  export FM_STATE_OVERRIDE='$SANDBOX/home/state'
  export FM_DATA_OVERRIDE='$SANDBOX/home/data'
  export FM_PROJECTS_OVERRIDE='$SANDBOX/home/projects'
  export FM_CONFIG_OVERRIDE='$SANDBOX/home/config'
  export FM_SPAWN_NO_GUARD=1
  export FM_GATE_REFUSE_BYPASS=1
  '$ROOT/bin/fm-spawn.sh' '$ID' '$PROJ' agy --mode no-mistakes --yolo off \
    > '$SANDBOX/spawn.out' 2>&1
  echo \$? > '$SANDBOX/spawn.status'
"

# Wait for the spawn to finish.
i=0
while [ ! -s "$SANDBOX/spawn.status" ] && [ "$i" -lt 400 ]; do sleep 0.25; i=$((i+1)); done

section() { printf '\n===== %s =====\n' "$1"; }

section "1. captain-visible spawn result (bin/fm-spawn.sh <id> <project> agy)"
cat "$SANDBOX/spawn.out"
printf 'exit=%s\n' "$(cat "$SANDBOX/spawn.status")"

section "2. argv the real agy binary would have received"
sed "s#$SANDBOX#\$SANDBOX#g" "$SANDBOX/agy-argv.txt"
printf -- '--- one argument per line ---\n'
sed "s#$SANDBOX#\$SANDBOX#g" "$SANDBOX/agy-argv.txt.args"

section "3. recorded task metadata"
grep -E 'harness=|model=|kind=' "$SANDBOX/home/state/$ID.meta"

section "4. turn-end wiring minted by the spawn"
printf 'worktree pointer  %s -> %s\n' "$WT/.fm-agy-turnend" "$(cat "$WT/.fm-agy-turnend")"
TOKEN=$(cat "$SANDBOX/home/state/$ID.agy-turnend-token")
printf 'state token       %s\n' "$TOKEN"
printf 'registry entry    ~/.gemini/config/fm-agy-turn-end.d/%s -> %s\n' \
  "$TOKEN" "$(sed "s#$SANDBOX#\$SANDBOX#g" "$SANDBOX/home/.gemini/config/fm-agy-turn-end.d/$TOKEN")"
printf 'hooks.json Stop key:\n'
python3 -c "import json,sys;d=json.load(open(sys.argv[1]));print(json.dumps({k:v for k,v in d.items()},indent=2))" \
  "$SANDBOX/home/.gemini/config/hooks.json" | sed "s#$SANDBOX#\$SANDBOX#g"

section "5. the live agy crewmate pane, as the captain sees it (bin/fm-peek.sh)"
FM_ROOT_OVERRIDE='' FM_HOME="$SANDBOX/home" FM_STATE_OVERRIDE="$SANDBOX/home/state" \
  TMUX="$(tmux -L "$SOCKET" display-message -p '#{socket_path},0,0')" \
  "$ROOT/bin/fm-peek.sh" "$ID" 12 2>&1 | sed "s#$SANDBOX#\$SANDBOX#g"

section "6. composer verdict on that live pane (shared classifier)"
TARGET=$(sed -n 's/^window=//p' "$SANDBOX/home/state/$ID.meta")
FM_ROOT_OVERRIDE="$ROOT" bash -c "
  TMUX='$(tmux -L "$SOCKET" display-message -p '#{socket_path}'),0,0'
  export TMUX
  . '$ROOT/bin/fm-backend.sh'
  printf 'composer-state: %s\n' \"\$(fm_backend_composer_state tmux '$TARGET' 2>&1)\"
  if fm_backend_agent_alive tmux '$TARGET' >/dev/null 2>&1; then
    printf 'agent-liveness: alive\n'
  else
    printf 'agent-liveness: not-alive\n'
  fi
"

section "6b. agy's Stop event wakes firstmate for THIS task only"
MARKER="$SANDBOX/home/state/$ID.turn-ended"
rm -f "$MARKER"
printf '{"workspacePaths":["%s"]}\n' "$WT" \
  | HOME="$SANDBOX/home" "$SANDBOX/home/.gemini/config/fm-agy-turn-end.sh"
[ -e "$MARKER" ] && printf 'Stop in the task worktree      -> wake marker written: %s\n' \
  "${MARKER/$SANDBOX/\$SANDBOX}" || printf 'Stop in the task worktree      -> NO wake marker\n'
rm -f "$MARKER"
printf '{"workspacePaths":["/tmp/some-other-checkout"]}\n' \
  | HOME="$SANDBOX/home" "$SANDBOX/home/.gemini/config/fm-agy-turn-end.sh"
[ -e "$MARKER" ] && printf 'Stop in an unrelated workspace -> wake marker WRONGLY written\n' \
  || printf 'Stop in an unrelated workspace -> no wake marker (correctly ignored)\n'

section "7. the same spawn refused for a claude-* model (standing pin)"
PATH="$SANDBOX/bin:$PATH" HOME="$SANDBOX/home" FM_ROOT_OVERRIDE='' FM_HOME="$SANDBOX/home" \
  FM_STATE_OVERRIDE="$SANDBOX/home/state" FM_DATA_OVERRIDE="$SANDBOX/home/data" \
  FM_PROJECTS_OVERRIDE="$SANDBOX/home/projects" FM_CONFIG_OVERRIDE="$SANDBOX/home/config" \
  FM_SPAWN_NO_GUARD=1 FM_GATE_REFUSE_BYPASS=1 \
  "$ROOT/bin/fm-spawn.sh" agy-claude-x1 "$PROJ" agy --mode no-mistakes --yolo off \
    --model claude-sonnet-4-6 2>&1
printf 'exit=%s\n' "$?"

section "8. the same spawn refused as a secondmate (crewmate/scout only)"
PATH="$SANDBOX/bin:$PATH" HOME="$SANDBOX/home" FM_ROOT_OVERRIDE='' FM_HOME="$SANDBOX/home" \
  FM_STATE_OVERRIDE="$SANDBOX/home/state" FM_DATA_OVERRIDE="$SANDBOX/home/data" \
  FM_PROJECTS_OVERRIDE="$SANDBOX/home/projects" FM_CONFIG_OVERRIDE="$SANDBOX/home/config" \
  FM_SPAWN_NO_GUARD=1 FM_GATE_REFUSE_BYPASS=1 \
  "$ROOT/bin/fm-spawn.sh" agy-second-x1 agy --secondmate 2>&1
printf 'exit=%s\n' "$?"

section "9. teardown retires every agy pointer (bin/fm-teardown.sh)"
HOME="$SANDBOX/home" PATH="$SANDBOX/bin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$SANDBOX/home" \
  FM_STATE_OVERRIDE="$SANDBOX/home/state" FM_DATA_OVERRIDE="$SANDBOX/home/data" \
  FM_PROJECTS_OVERRIDE="$SANDBOX/home/projects" FM_CONFIG_OVERRIDE="$SANDBOX/home/config" \
  FM_SPAWN_NO_GUARD=1 FM_GATE_REFUSE_BYPASS=1 \
  TMUX="$(tmux -L "$SOCKET" display-message -p '#{socket_path}'),0,0" \
  "$ROOT/bin/fm-teardown.sh" "$ID" --force 2>&1 | sed "s#$SANDBOX#\$SANDBOX#g"
for p in "$WT/.fm-agy-turnend" "$SANDBOX/home/state/$ID.agy-turnend-token" \
         "$SANDBOX/home/.gemini/config/fm-agy-turn-end.d/$TOKEN"; do
  if [ -e "$p" ]; then printf 'STILL PRESENT: %s\n' "${p/$SANDBOX/\$SANDBOX}"
  else printf 'retired:       %s\n' "${p/$SANDBOX/\$SANDBOX}"; fi
done

tmux -L "$SOCKET" kill-server 2>/dev/null
- Outcome: ⚠️ 1 info across 1 run (24m25s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • ⚠️ bin/fm-spawn.sh:2673 - The agy turn-end mint runs off $HARNESS, but the registry directory it writes into is only created by the installer in the *__AGYBIN__* template block (bin/fm-spawn.sh:1505). A raw launch command whose first word is agy sets HARNESS=agy (bin/fm-spawn.sh:1209) without ever expanding a template, so mktemp &#34;$AGY_AUTH_DIR/fm.XXXXXXXXXXXX&#34; fails and, under the file-level set -eu, kills fm-spawn after the backend window and worktree already exist - leaving a half-spawned task and a bare mktemp error. grok's arm avoids exactly this with mkdir -p &#34;$GROK_AUTH_DIR&#34; at bin/fm-spawn.sh:2571. tests/fm-agy-harness.test.sh:244 works around it with its own mkdir and the test comment accepts the residual as "identical for kimi", so this is a deliberate choice - but the raw-launch hatch is the intent's own adapter-verification path, and one mkdir -p &#34;$AGY_AUTH_DIR&#34; before the mktemp would close it for agy without touching kimi.
  • ⚠️ bin/fm-spawn.sh:2233 - agy_maybe_accept_trust states its contract as returning non-zero "only when the dialog is STILL up at the end of the budget", but when the dialog first becomes visible on the final poll (i == max-1) the accept Enter is sent and the loop exits immediately with dialog=1, so the accept is never given a single re-check. The spawn then records failed: agy is still showing its workspace trust dialog, so the brief never reached the agent and exits 1 while a live agy pane is about to receive the brief - a false failure plus an orphaned working agent. The comment itself notes the ~30s budget exists because "a cold agy start reaches the dialog well after the launch keystroke", which is precisely the case that lands the dialog on a late poll. Re-poll once after sending the accept before declaring failure (for example, do not break out of the budget on the same iteration that set accepted=1).
  • ℹ️ bin/fm-teardown.sh:670 - remove_turnend_worktree_pointers hardcodes .fm-grok-turnend .fm-kimi-turnend .fm-agy-turnend, while the commit's stated point was to declare that adapter set exactly once (fm_control_turnend_global_harnesses) so "a new adapter of that shape cannot be wired without teardown retiring both its registry entry and the private state token". fm_control_harness_wiring_paths already owns each of those pointer paths per harness, so a fourth global-hook adapter still needs two edits and the hardcoded triple can silently drift from the declared set. Deriving the pointers from fm_control_turnend_global_harnesses + fm_control_harness_wiring_paths removes the second list.

🔧 Fix: fix agy trust re-poll, raw-launch registry dir, derive teardown pointers
2 infos still open:

  • ℹ️ bin/fm-spawn.sh:2680 - The new mkdir -p &#34;$AGY_AUTH_DIR&#34; runs before the umask 077 window on the next line, so on the one path where it actually creates the directory (raw launch, where the installer never runs) the private token registry is created at the ambient umask - typically 0755 - while bin/fm-agy-turnend-hook.sh:183 deliberately creates and re-enforces it at 0700 (private_dir(REGISTRY, ..., True)). The same mkdir -p also silently follows a symlinked ~/.gemini/config, which the installer refuses outright (bin/fm-agy-turnend-hook.sh:119). Exposure is narrow - token file contents stay 0600, the hook is not installed on the raw path, and the next templated agy spawn chmods the directory back to 0700 - but moving the mkdir -p below umask 077 closes the mode half for free and keeps spawn from contradicting its own installer's stated posture. grok's arm has the same shape, so this is a divergence from the agy installer rather than from the fleet.
  • ℹ️ AGENTS.md:93 - AGENTS.md's state/ contract table enumerates every per-task firstmate-owned state file, including &lt;id&gt;.grok-turnend-token, &lt;id&gt;.kimi-turnend-token, &lt;id&gt;.muse-session and &lt;id&gt;.cursor-session, but the new &lt;id&gt;.agy-turnend-token written by bin/fm-spawn.sh:2686 and retired by bin/fm-teardown.sh:663 was not added. AGENTS.md is edited by this change (section 4's verified-harness list) and is loaded on every turn, so the omission leaves the always-on state-directory contract incomplete for the adapter this change wires. One row after line 93 mirroring the kimi wording closes it.
⚠️ **Test** - 1 info
  • ℹ️ tests/fm-bootstrap.test.sh:906 - tests/fm-bootstrap.test.sh fails on this machine at test_network_phase_partitions_the_run: the fixture removes node from its fake bin dir but the machine has a real /usr/bin/node inside the test's BASE_PATH (/usr/bin:/bin:/usr/sbin:/sbin), so the expected 'MISSING: node (install:' diagnostic never appears. It reproduces identically at base commit 6789876 and is untouched by this change (bin/fm-bootstrap.sh's only edit here is the crew-dispatch harness list). Because the harness's fail() exits immediately, this abort also masks the new agy crew-dispatch validation rows in a default local run; with a node-free FM_TEST_BASE_PATH the whole script passes, including those rows. Left unfixed as pre-existing and out of this change's scope.
  • bin/fm-test-run.sh tests/fm-agy-harness.test.sh (19 cases: detection, launch shape and model pin, effort omission, claude-* refusal, secondmate refusal, marker clearing, raw-launch hatch, hook install/remove/fire, trust dialog, teardown)
  • bin/fm-test-run.sh tests/fm-composer-lib.test.sh tests/fm-control.test.sh
  • bin/fm-test-run.sh tests/fm-tmux-agent-liveness.test.sh (real tmux: agy pane idle=empty, typed=pending, identical screen over live pi never empty)
  • bin/fm-test-run.sh tests/fm-composer-ghost.test.sh tests/fm-busy-adapter-wiring.test.sh tests/fm-backend-zellij.test.sh tests/fm-backend-cmux.test.sh tests/fm-backend-orca.test.sh tests/fm-teardown.test.sh tests/fm-control-relaunch.test.sh
  • FM_TEST_BASE_PATH=/tmp/fm-nodefree-bin bash tests/fm-bootstrap.test.sh (exit 0; confirms the agy crew-dispatch validation rows pass once the environment's /usr/bin/node no longer defeats the fixture)
  • bash /tmp/no-mistakes-evidence/01KZZQRZVPPK1ZXF1EJGMAD7EM/agy-e2e-verify.sh &lt;repo&gt; /tmp/fm-agy-e2e - real-tmux end-to-end: fm-spawn.sh -> live agy pane -> fm-peek.sh -> fm_backend_composer_state -> Stop-hook wake -> claude-* and --secondmate refusals -> fm-teardown.sh
  • agy --help and agy models against the really installed Antigravity CLI 1.1.13
  • git archive 6789876 | tar -x -C /tmp/fm-base-check &amp;&amp; bash tests/fm-bootstrap.test.sh (proves the one failure is pre-existing at the base commit)
⚠️ **Document** - 2 infos
  • ℹ️ docs/verification/runtime-backends.md:203 - tests/fm-composer-matrix-live-e2e.test.sh now iterates agy, but the recorded output in docs/verification/runtime-backends.md is the 2026-08-10 run and carries no agy line. I documented the gap and the regressions that pin agy meanwhile (portable captures plus the real-process identity round trip), but the evidence itself can only be refreshed by rerunning FM_COMPOSER_MATRIX_LIVE=1 on a machine with agy installed. Left as a follow-up rather than fabricating a result.
  • ℹ️ docs/agent-control.md:51 - docs/agent-control.md's "resume is not a verb" rationale enumerates codex/grok (session id at exit) and claude/pi/pi-signed/kimi (no contract), omitting cursor, muse, and now agy - which per the harness-adapters skill prints agy --conversation=&lt;uuid&gt; at exit, the same shape as codex and grok. The conclusion is unchanged and the omission predates this change by two adapters, so completing that list is an out-of-scope consolidation worth a separate pass rather than an edit here.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Verify Antigravity CLI as a crewmate/scout adapter. Pin the launch to
gemini-3.1-pro-high, omit --effort (it conflicts with *-high model ids),
accept the workspace trust dialog, and install a gated global Stop hook.
Secondmate, primary turn-end, and watcher-arm remain unverified.
A separated greater-than pair is unknown on zellij, cmux, and orca
because agy is not verified there. Identity-capable backends keep
the existing probe-then-shortcut path.
@devin-ai-integration

Copy link
Copy Markdown

Closed as superseded — this work already landed on main via #4200.

— Kun's Firstmate

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant