Skip to content

feat(harness): add agy (Antigravity CLI) crewmate adapter - #2

Open
RooseveltAdvisors wants to merge 19 commits into
mainfrom
fm/agy-harness-feasibility
Open

RooseveltAdvisors wants to merge 19 commits into
mainfrom
fm/agy-harness-feasibility

Conversation

@RooseveltAdvisors

@RooseveltAdvisors RooseveltAdvisors commented Sep 4, 2026 •

Copy link
Copy Markdown
Owner

Intent

Make agy (Antigravity CLI) usable as a firstmate crewmate/scout worker runtime, so a worker can be spawned on agy, reach its brief without a human answering a workspace-trust dialog, and report turn end back to firstmate's supervision.

Requirements in their current accepted form:

  • Register agy across the adapter plane: harness detection must test ANTIGRAVITY_CONVERSATION_ID before CLAUDECODE, because agy does not clear an inherited marker; add the agy launch template, control verbs on the default backend, dispatch/bootstrap/quota registration, and the tmux liveness identity.
  • Workspace trust must be PRE-REGISTERED at spawn using upstream's fix(bin): pre-register claude workspace trust at spawn time kunchenguid/firstmate#3663 pattern (bin/fm-claude-trust.sh's block in fm-spawn.sh), with agy added as one more harness arm using that contract rather than a competing implementation. --dangerously-skip-permissions does not suppress agy's trust dialog and there is no environment bypass, so pre-registration is the only mechanism.
  • Trust must be retired while the path is still owned by the task, before it returns to the pool and after any rollback that could delete the record keyed on; withdraw only the spellings this task's spawn actually added, never an operator's own entry.
  • Turn-end wake rides agy's undocumented Stop hook and must gate on fullyIdle true, because a turn whose shell command outruns agy's wait emits two Stop events.
  • Design WITH the shared settings store: it cannot be relocated per task, so task isolation comes from the worktree, not a per-task settings root.
  • agy is crewmate/scout only; fm-spawn must refuse a --secondmate launch because no agy primary supervision protocol exists or has been verified.
  • Documented gaps must be stated rather than hidden: a declined tool call and an Escape interrupt both end a turn with no Stop event.
  • Tests pinned against the real binary; every behavioural claim probe-verified.
  • This branch also merges house main (8f7b79c) so the PR renders only the agy change; upstream owns the shared contract and no behaviour changes for any harness other than agy.

What Changed

  • Add agy across the adapter plane: harness detection tests ANTIGRAVITY_CONVERSATION_ID before CLAUDECODE/GEMINI_CLI (with anchored agy ancestry fallback), a new -i launch template with foreign-marker clearing and a verified --model/--effort axis, control verbs on the default backend (interrupt, exit, wiring paths, turn-end registry), tmux/herdr liveness identity, and dispatch/bootstrap/quota registration. agy is crewmate/scout only — fm-spawn refuses a --secondmate launch since no primary supervision protocol exists.

  • Pre-register agy workspace trust at spawn via new bin/fm-agy-trust.sh (structural linked-worktree scope test writing trustedWorkspaces in ~/.gemini/antigravity-cli/settings.json), since --dangerously-skip-permissions does not suppress the trust dialog and no environment bypass exists. Trust is withdrawn exactly for the spellings this spawn added — at teardown while the task still owns the path, after rollback, and by spawn abort cleanup when no task record was published.

  • Add new bin/fm-agy-turnend-hook.sh installing a global Stop hook in ~/.gemini/config/hooks.json gated on fullyIdle true (a shell command outrunning agy's wait emits two Stop events), which wakes supervision through a gitignored .fm-agy-turnend pointer and a per-task token registry entry; document known gaps (a declined tool call or Escape interrupt ends a turn with no Stop event), add harness adapter reference docs and runtime-backend verification notes, and pin new/updated suites (fm-agy-harness, fm-agy-signals-live-e2e, teardown, herdr, tmux liveness) against the real binary.

Risk Assessment

⚠️ Medium: The change writes into operator-owned vendor config (settings.json trustedWorkspaces, global hooks.json) and rests on undocumented agy behaviors (Stop payload shape, fullyIdle semantics, trust-store location) that only the opt-in live guard can re-verify after agy self-updates, but every write is atomic, fingerprinted or refusal-guarded, trust retirement is keyed to what each spawn actually added, and the load-bearing paths are pinned behaviorally, so it is safe to merge with drift surveillance as the standing follow-up.

Testing

Completed 1 recorded test check.

  • Outcome: ⚠️ 1 error across 1 run (26m43s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • ℹ️ bin/fm-agy-turnend-hook.sh:208 - The hooks.json install rewrites the whole file via read-then-atomic-rename without the fingerprint-and-retry guard its sibling writer has: bin/fm-agy-trust.sh fingerprints settings.json specifically because agy sessions rewrite that store concurrently, while the hooks.json edit would silently drop an external write landing between readConfig and rename. This is acceptable today (agy exposes no hooks subcommand and plausibly never writes this file itself, and concurrent firstmate installs are race-tested), but if agy is ever observed writing hooks.json, this control should adopt the same moved-store refusal as the trust writer rather than clobbering operator hooks.
  • ℹ️ bin/fm-composer-lib.sh:315 - Adding agy's esc to cancel to FM_DELIVERY_BUSY_REGEX_DEFAULT widens the union used when no harness is recorded (herdr's rendered busy state without a harness argument): any unrecorded pane rendering that string now classifies busy. A recorded muse pane never matches (it falls into the never-borrow branch), so an unrecorded muse pane is now treated differently from a recorded one; the effect is conservative (a submit is refused, never falsely delivered), so no behavioral fix is requested, but the widening is a deliberate cross-harness surface change worth being aware of.
⚠️ **Test** - 1 error
  • 🚨 tests failed with exit code 1
  • bin/fm-test-run.sh --changed --exclude-family real-herdr-gated
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

RooseveltAdvisors and others added 19 commits September 3, 2026 19:42
Wires agy as a verified crewmate/scout worker runtime: detection, launch,
control mechanics, turn-end wake, and teardown.

The load-bearing piece is workspace trust. agy's
--dangerously-skip-permissions governs tool permissions only and does NOT
suppress its separate workspace-trust dialog, so every fresh task worktree
would park on a dialog that renders no status-bar text at all - a wedged
worker indistinguishable from an idle one. bin/fm-agy-trust.sh registers the
worktree before launch and refuses rather than degrades. Its scope test is
structural: only a linked git worktree of the named project is accepted, and
a primary checkout, a foreign project's worktree, a worktree subdirectory, a
plain directory, the home directory, and the settings directory are each
refused. It neutralises CDPATH and the git environment overrides that could
otherwise defeat those refusals, follows a store symlink only to a target
this user owns, requires node rather than degrading without it, and refuses a
store that changed underneath it rather than clobbering agy's own write.

bin/fm-agy-turnend-hook.sh owns one firstmate-turn-end key in agy's global
hooks.json, preserving every operator hook, plus a silent always-zero hook
script and a private per-task token registry. The hook fires only when the
Stop payload reports fullyIdle true: agy backgrounds a command that outruns
its own wait, yields the composer, and fires Stop with fullyIdle false while
that command is still running.

Two paths end an agy turn with no Stop event and are documented rather than
papered over: a declined tool call, and an Escape interrupt. A crewmate
launches with --dangerously-skip-permissions so it never reaches the first,
and firstmate initiates its own interrupts; the watcher's staleness check is
the backstop for both.

agy is crewmate/scout only. A secondmate is a firstmate instance and needs a
primary supervision protocol, which agy has no verified path for, so
fm-spawn refuses a --secondmate launch on it.

agy's rendered status bar is a delivery guard only, never a recorded worker
state: fm-busy-lib.sh forbids a second rendered-text classifier, so agy is
deliberately unarmed and its documented upgrade path is a semantic
PreInvocation/Stop pair.

tests/fm-agy-harness.test.sh proves the accepted path and every refusal with
no harness installed; FM_AGY_SIGNALS_LIVE_E2E=1
tests/fm-agy-signals-live-e2e.test.sh proves the trust dialog, both status
bars, typed submission, and the Stop hook against the real binary and fails
naming the harness and version, because agy self-updates.
Upstream owns the shared contract; agy is added alongside as one more
harness using it, never as a competing implementation.

- fm-spawn.sh: take upstream's `if [ "$KIND" != secondmate ]` trust
  pre-registration block from kunchenguid#3663 verbatim for the claude path and add
  `agy)` as a second arm, so agy uses that contract rather than its own
  parallel `if`. gemini's launch clear-set is split out of the shared arm
  so it stays byte-identical to upstream while the shared arm also drops
  an inherited agy marker.
- fm-harness.sh: keep both detection blocks. agy is ordered before gemini
  because agy is built on the same gemini_coder tree and whether it also
  exports GEMINI_CLI is unverified; testing agy's own marker first is
  correct under both possibilities and cannot change gemini's verdict.
- fm-control-lib.sh, docs, SKILL.md: union of both harness rosters.
- tests: upstream's spawn fixture now pins HOME to $home/user-home so a
  trust pre-registration cannot reach the developer's real store, and that
  pin beats an outer HOME= on the call. The agy spawn tests now assert
  against that sandboxed store, which is the same one claude registers in.

No behaviour changes for any harness other than agy: the claude trust arm
and gemini's launch clear-set were both diffed against 8f7b79c and are
identical.
@RooseveltAdvisors RooseveltAdvisors changed the title feat(bin): add agy (Antigravity CLI) as a crewmate/scout worker runtime feat(harness): add agy (Antigravity CLI) crewmate adapter Sep 4, 2026
RooseveltAdvisors added a commit that referenced this pull request Sep 5, 2026
… the CI failure is the fixture's `tasks-axi add` (beads backend) failing instantly on the runner, then hiding behind three diagnosability defects that this change fixes in tests/fm-stale-sweep.test.sh: (1) tasks-axi reports its errors only on stdout, which every fixture call discarded via >/dev/null, so the captured fixture log was empty exactly when a diagnosis was needed — the inner redirects are removed so both streams land in the log; (2) `bd init`'s exit status and output were silently ignored although bd init can exit non-zero while leaving a half-usable graph — it is now checked and its log captured; (3) make_fixture runs inside $(...), so its fail() only killed the subshell and the suite cascaded into the misleading 'backend is not beads' sweep against an empty FM_HOME — fx/bd-init failures now write a marker file that read_fixture (running in the real test shell) checks to abort the whole suite with the captured log attached. Verified locally: sabotaged-bd and failing-tasks-axi shims both abort loudly with the real error named; full suite passes (10/10, unchanged behavior); bin/fm-lint.sh clean. Note: the runner-side bd failure itself is not reproducible locally (identical OS/toolchain passes every simulation), so the next CI run will name the exact underlying error via the captured fixture log. Greptile Review: P1 #1 (captain-hold answer racing the sweep) was already fixed at 124a5e5 — fm-captain-hold now holds the per-task record lock across answer/hold, with a regression test in captain-hold-lifecycle; the failing Greptile run is stale against that head. P1 #2 (bare `tasks-axi done`/`hold` racing the second-proof→reopen window) needs no code change: verified locally that reopen leaves no detectable trace and tasks-axi offers no compare-and-swap, so no post-reopen guard is possible; the sweep already refuses when the record lock is held, refuses on a pending backlog-close replay, and re-proofs immediately before reopening — only an actor bypassing the documented locking protocol can hit the sub-second window, which is an author-response (comment resolution) matter, not a defect introduced by this change
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