feat(m3): GeminiCLIAdapter + CLISubscriptionAuthError base (M3 PR 16b)#16
Merged
Conversation
Replaces the PR 16 stub with the real GeminiCLIAdapter wrapping
`gemini -p -o stream-json` headless mode as a non-conversational
subprocess. Carries forward all the design pins from PR 16a:
Sandbox / approval:
- --approval-mode default (NOT yolo / auto_edit, which auto-approve
tool calls — gemini's analog of codex --full-auto)
- --skip-trust required for ephemeral scratch directories
(otherwise gemini blocks on stdin asking the user to trust the
workspace; fatal in headless mode)
§INV-3 belt-and-braces:
- _FORBIDDEN_FLAGS covers --yolo, -y, --raw-output,
--accept-raw-output-risk, plus hypothetical CodeAct/REPL/eval
- _FORBIDDEN_APPROVAL_MODES covers yolo + auto_edit (the values
we'd never pass via --approval-mode)
Spike fixture:
- tests/fixtures/gemini_stream_json_tool_call.jsonl is real gemini
0.39.1 output covering init / message (user) / tool_use /
tool_result / message (assistant deltas) / result
Typed auth error:
- GeminiCLIAuthError raised on declared phrases in result events,
assistant messages, OR stderr (mirrors codex stderr fallback
from PR 16a R2 #1)
Refactor: CLISubscriptionAuthError base class (PR 16a R2 follow-up #4):
- Defined in cli_subscription/base.py
- Both CodexCLIAuthError and GeminiCLIAuthError subclass it
- SubscriptionCLIBackend.generate_edit catches the BASE class via
isinstance, no longer the per-adapter class
- Reviewer's "premature today, defer to PR 16c" guidance was
contingent on a single concrete instance — PR 16b's GeminiCLIAdapter
is the second instance that motivates the abstraction
Stats: +325/-29 across 5 modified files (base, codex_cli, gemini_cli,
cli_subscription_backend, test_cli_subscription) plus 2 new files
(test_gemini_cli_adapter.py +377, gemini fixture +7). Total ≈ +709 LOC.
Authoritative count via `git diff --stat HEAD~1 HEAD | tail -1` after
push.
Test results: 2786 passed, 4 skipped (`pytest --ignore=tests/test_agents.py`).
The single pre-existing failure (test_create_agent_unknown_raises regex
case mismatch) traces to PR-16 commit c0906fa, unrelated to this PR.
PR 16b coverage:
- argv: -p prompt, -o stream-json, --approval-mode default,
--skip-trust
- §INV-3: forbidden flags + forbidden approval-mode values absent
- parser: spike fixture round-trip, assistant message aggregation
(with delta chunks combining), user echo excluded, tool_use →
tool_was_called, unknown event type → schema drift
- typed auth: result-event phrase, assistant-message phrase, stderr
fallback, non-auth errors not misclassified, phantom-free message
- hierarchy: GeminiCLIAuthError + CodexCLIAuthError both subclass
CLISubscriptionAuthError
- backend integration: parse_output raises → AgentResult.error_type=AUTH
via single isinstance-check on base class
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address 2 of 3 round-1 reviewer follow-ups (#1, #2). #3 (token cost reporting / API-vs-OAuth framing) is forward-pointing for a future docs/feature PR. #1 docstring/code mismatch (gemini_cli.py module docstring): Module docstring claimed `--include-directories` was passed to scope the blast radius. build_argv never emits it. Reality: the scope-limiting is the subprocess `cwd` (set to scratch by the backend's run_subprocess in base.py) — gemini operates only on cwd by default, no flag needed. Updated docstring to reflect actual mechanism. #2 verified auth-failure phrase: Captured real gemini 0.39.1 auth-failure stderr via HOME=tmp + empty GEMINI_API_KEY (saved as `tests/fixtures/gemini_auth_failure.stderr.txt`). The actual phrase emitted is "Please set an Auth method in your <path>/.gemini/ settings.json or specify one of the following environment variables before running: GEMINI_API_KEY, ...". Added "Please set an Auth method" as the first entry in _AUTH_FAILURE_PHRASES with inline comment marking it as VERIFIED (vs the rest which remain EXTRAPOLATED for forward-compat). New regression test test_gemini_auth_error_real_fixture_from_spike exercises the real fixture against the parser → raises GeminiCLIAuthError. Notable: gemini 0.39.1 exits 0 on auth failure (not 1) — the typed-exception pattern is what catches it, not exit-code heuristics. Stderr-fallback path (PR 16a R2 #1) is the one that fires here. Stats: +53/-15 across 3 files (gemini_cli.py +24/-15, new fixture +1, test_gemini_cli_adapter.py +28). Test results: 24 passed in test_gemini_cli_adapter.py (23 prior + 1 new fixture regression). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9 tasks
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.
Summary
GeminiCLIAdapterwith a real implementation wrappinggemini -p -o stream-jsonheadless mode.Stacked on PR 16a (#15)
This PR is stacked on
feat/m3-codex-cli-adapter. It adds the GeminiCLIAdapter on top of PR 16a's CodexCLIAdapter so the backend's auth-classification pathway can be unified into a singleCLISubscriptionAuthErrorbase catch.If PR 16a merges first, this PR's base will rebase cleanly onto main. If they merge in the opposite order, the only conflict surface is
cli_subscription_backend.py'stry/exceptclause (resolved by keeping the PR 16b version: catch the base class).CLISubscriptionAuthError base class (PR 16a R2 follow-up #4)
src/crucible/agents/cli_subscription/base.pyCodexCLIAuthErrorandGeminiCLIAuthErrorboth subclass itSubscriptionCLIBackend.generate_editcatches the BASE class via isinstance — single catch handles all current and future adapterstest_gemini_auth_error_subclasses_cli_subscription_auth_errorandtest_codex_auth_error_also_subclasses_cli_subscription_auth_errorenforce the hierarchyCarry-forward design pins
--sandbox workspace-write--approval-mode default+--skip-trust--full-auto,--dangerously-bypass-approvals-and-sandbox, hypothetical CodeAct/REPL/eval--yolo,-y,--raw-output,--accept-raw-output-risk+ hypothetical--ephemeral,--ignore-user-config,--ignore-rulescodex_exec_quota_exceeded.jsonlgemini_stream_json_tool_call.jsonl(init/message/tool_use/tool_result/result)CodexCLIAuthError(CLISubscriptionAuthError)GeminiCLIAuthError(CLISubscriptionAuthError)Stats
git diff --stat HEAD~1 HEAD | tail -1→ 768 insertions / 29 deletions, 7 files.Test results
2786 passed, 4 skipped (
pytest --ignore=tests/test_agents.py). Same pre-existing PR-16 regex case mismatch intest_create_agent_unknown_raisesexcluded; unrelated to this PR.Test plan
🤖 Generated with Claude Code