Skip to content

feat: make the Gemini route work, as a Fabric seat and a first-class adapter - #652

Merged
mblauberg merged 4 commits into
mainfrom
feat/agy-fabric-seat-and-cli-route
Aug 5, 2026
Merged

feat: make the Gemini route work, as a Fabric seat and a first-class adapter#652
mblauberg merged 4 commits into
mainfrom
feat/agy-fabric-seat-and-cli-route

Conversation

@mblauberg

@mblauberg mblauberg commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Gemini agents were not failing. They were succeeding into a contract that could
not express their own failure, on the one provider lane with no adapter behind it.

The defect

agy in headless mode cannot prompt for tool permission, so it auto-denies. What
it reports when it does:

Signal Value on an auto-denied run
exit code 0
json status SUCCESS
json response ""
json error absent
stderr jetski: no output produced ... permission

Three of the four signals a wrapper would trust say success. Denial is also
all-or-nothing and retroactive: one denied call discards the whole turn,
including reads that already succeeded.

Mining the session transcripts found what that cost: 93 exit-0 runs with no
content
, four cases where the dispatching agent quietly wrote the review
itself, and three that correctly reported CROSS-FAMILY-NOT-RUN.

Why there was no adapter

Commit 8d5f06c ("fabric-first routing") deleted the skills/agy-headless skill
and its wrapper, removed the agy branch from cf_dispatch.sh, and added
test_removed_agy_direct_route_fails_closed to lock it shut. The intent was to
route Gemini through Fabric instead. The Fabric side was never built, and
agy's live registration pointed at a path that does not exist, so it had never
reached Fabric at all.

What changes

Fabric. Agy gets its own agy seat instead of sharing codex's. The seat is
the provider identity on every Fabric record, so under the old config a
cross-family Gemini finding was filed as OpenAI, which is exactly the
attribution the review ladder depends on. Cursor and Kiro keep the codex seat:
they are brokers with no family of their own to record.

Dispatcher. cf_dispatch.sh --tool agy exists, with ok requiring SUCCESS
and a non-empty response, a new permission_denied status from the stderr
match, and stdout/stderr never merged. Two further CLI facts are encoded because
guessing them fails silently:

  • the prompt is one argv value. --print needs one, and --print - takes the
    dash as the literal prompt, ignores stdin and answers it. Oversized prompts are
    refused rather than clipped, because a truncated brief gets reviewed as though
    whole. The ceiling is the per-string limit, not the total: Linux caps one argv
    element at 128 KiB while darwin does not, so the guard is 124 KiB on both.
  • model and effort are separate flags, and efforts are per model:
    gemini-3.1-pro offers only low and high, so flagship-at-medium was
    unsatisfiable and now fails closed as effort_unsupported.

Routing. agy_capabilities.py captures the runtime model list so a route
validates against the installed CLI instead of a dated catalogue.

The boundary guard keeps its intent (no parallel provider skill, no opt-in gate
making the only Gemini route second-class) and now also requires the adapter to
exist.

Verification

Against agy 1.1.10, not against the stub. The Luna lane that built the adapter
delivered 36 passing tests around a --print + stdin form the real CLI rejects
with exit 2; the stub had agreed with the code and both were wrong. Tests now
enforce the real contract.

  • 1260 tests + scripts/check-harness + npm run check + fabric MCP smoke: green
  • live dispatch: status=ok, certification_eligible=true
  • forced denial: permission_denied, exit 1, not certifiable
  • fabric_whoami from agy reports provider agy

Note for the reviewer

cf_dispatch.sh:400 hardcodes guarantee="enforced" for cursor, but
cursor-agent --help says -p "has access to all tools, including write and
shell", and --mode plan is the documented read-only mode. Same defect class,
left out of scope here.

https://claude.ai/code/session_01DsDFRG6NVWb2VBNNtVkQ6N

Agy shared the codex seat, so every Fabric record from a Gemini run was
attributed to OpenAI. That is the one attribution the cross-family review
ladder depends on being true: a distinct-family leg recorded under the
primary's family is worse than no leg at all, because it reads as satisfied.

Cursor and Kiro keep the codex seat. They are brokers that front whichever
model the operator selects, so they have no family of their own to record.
Agy is the harness's only route to Gemini and now holds the `agy` seat.

The seat is a free-form string in identity.ts, so this needs no runtime
change beyond the registration itself.

Claude-Session: https://claude.ai/code/session_01DsDFRG6NVWb2VBNNtVkQ6N
The catalogue's google aliases could not be dispatched. `agy --model
gemini-3.1-pro` exits 1 with "requires --effort", because agy was configured
as a model-id effort adapter while the aliases carry no effort suffix. The
fallback order also listed max and xhigh, which exist in no Gemini model, and
the catalogue offered flagship at medium when gemini-3.1-pro only has low and
high.

Effort now travels in its own --effort flag, which the CLI accepts and which
keeps the requested effort auditable. agy_capabilities.py captures the runtime
model list so the resolver validates against what the installed CLI offers
rather than a dated catalogue, and model_route trusts "agy models" as a
capability source.

Effortless ids such as claude-sonnet-4-6 stay out of the routable set: they
carry no selectable effort, and routing a non-Gemini model through this
adapter would defeat the family distinctness the route exists to provide.

Verified against agy 1.1.10: flagship at medium now fails closed as
effort_unsupported instead of dispatching at an unrequested effort.

Claude-Session: https://claude.ai/code/session_01DsDFRG6NVWb2VBNNtVkQ6N
Gemini had no working route. The direct one was removed by the fabric-first
refactor and locked shut by a test, the Fabric replacement was never built,
and what remained was a prose recipe in a subagent definition that could not
work: it omitted --add-dir while insisting --add-dir was mandatory, asked for
text output that cannot be classified, and merged stderr into the answer file.

The defect that made this invisible is that agy reports a denied tool as
success. Headless mode cannot prompt, so it auto-denies, exits 0, and prints
{"status":"SUCCESS","response":""} with the reason on stderr alone. Every
signal a wrapper would check says the review happened. Across the session
transcripts that produced 93 exit-0 runs with no content, four cases where the
dispatching agent wrote the review itself, and three that correctly reported
CROSS-FAMILY-NOT-RUN.

So ok now requires SUCCESS and a non-empty response, a stderr jetski match is
permission_denied, and stdout and stderr are never merged.

Two further CLI facts are encoded because guessing them fails silently. The
prompt is one argv value: --print needs one, and --print - takes the dash as
the literal prompt, ignores stdin and answers it. An oversized prompt is
refused rather than clipped by ARG_MAX, because a truncated brief gets
reviewed as though it were whole. Model and effort are separate flags, since a
bare family id exits 1 asking for --effort.

The boundary guard keeps its intent: no parallel provider skill and no opt-in
gate that would make the only route to the Gemini family second-class. It now
also requires the adapter to exist.

Verified against agy 1.1.10, not against the stub: a live dispatch returns
status=ok with certification_eligible=true, and a forced denial returns
permission_denied with exit 1.

Claude-Session: https://claude.ai/code/session_01DsDFRG6NVWb2VBNNtVkQ6N
Copilot AI lite review requested due to automatic review settings August 5, 2026 02:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

The ceiling was set from darwin's 1 MiB total ARG_MAX, but the binding limit is
per-string. Linux caps a single argv element at MAX_ARG_STRLEN, 32 pages =
128 KiB, and refuses the exec with E2BIG, so a brief between 128 KiB and the old
768 KiB guard dispatched on a developer's Mac and died on a Linux runner.

CI caught it through the guard's own test, which passed the oversized prompt as
an argument and so failed in the test's exec before cf_dispatch ran. The test
now supplies it through --prompt-file, which is what a caller with material that
large should use anyway.

Claude-Session: https://claude.ai/code/session_01DsDFRG6NVWb2VBNNtVkQ6N
@mblauberg
mblauberg merged commit 46b8036 into main Aug 5, 2026
7 checks passed
@mblauberg
mblauberg deleted the feat/agy-fabric-seat-and-cli-route branch August 5, 2026 08:11
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.

2 participants