feat: make the Gemini route work, as a Fabric seat and a first-class adapter - #652
Merged
Conversation
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
agyin headless mode cannot prompt for tool permission, so it auto-denies. Whatit reports when it does:
statusSUCCESSresponse""errorjetski: no output produced ... permissionThree 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-headlessskilland its wrapper, removed the
agybranch fromcf_dispatch.sh, and addedtest_removed_agy_direct_route_fails_closedto lock it shut. The intent was toroute 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
agyseat instead of sharing codex's. The seat isthe 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 agyexists, withokrequiringSUCCESSand a non-empty response, a new
permission_deniedstatus from the stderrmatch, and stdout/stderr never merged. Two further CLI facts are encoded because
guessing them fails silently:
--printneeds one, and--print -takes thedash 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.
gemini-3.1-prooffers only low and high, so flagship-at-medium wasunsatisfiable and now fails closed as
effort_unsupported.Routing.
agy_capabilities.pycaptures the runtime model list so a routevalidates 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 rejectswith exit 2; the stub had agreed with the code and both were wrong. Tests now
enforce the real contract.
scripts/check-harness+npm run check+ fabric MCP smoke: greenstatus=ok,certification_eligible=truepermission_denied, exit 1, not certifiablefabric_whoamifrom agy reports provideragyNote for the reviewer
cf_dispatch.sh:400hardcodesguarantee="enforced"for cursor, butcursor-agent --helpsays-p"has access to all tools, including write andshell", and
--mode planis the documented read-only mode. Same defect class,left out of scope here.
https://claude.ai/code/session_01DsDFRG6NVWb2VBNNtVkQ6N