feat(provider): gemini-cli adapter — third AgentProvider, spec pinned to 0.42.0 (grilled 2026-07-11) #126
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#126
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Third
AgentProvider: Gemini CLI, at the Codex bar (ADR-0036 DoD — Tier-1 conformance green + four Tier-2 live spikes evidenced by a committedinternal/compat/gemini/compat.md). Spec fully grilled 2026-07-11 with the maintainer; decisions below are settled — don't re-litigate.Pinned decisions (grill 2026-07-11)
/nix/store/7aw1jsvlxmj5p6dv3yr5sz3sd868piny-gemini-cli-0.42.0/bin/gemini). compat.md pins to 0.42.0. Host wiring stays generic (-provider-bin gemini=…/LAB_PROVIDER_BIN_GEMINI, ADR-0034); the deploy module needs itsagentPackages.geminiparameter wired so the bin lands on PATH (ADR-0033 baseline).~/.gemini/oauth_creds.json(codex auth.json precedent).AuthFlowkindoauth-code:NO_BROWSER=trueprints an auth URL and takes a pasted authorization code on stdin →LoginStartscrapes the URL from thelab-login-geminisession,LoginSubmitCodedelivers the code (unlike codex, this provider implements it). Free tier accepted for v1 (~60 req/min, 1000/day, shared across runs); quota-exhaustion behavior must be recorded, not worked around.gemini --approval-mode yolo --skip-trust --session-id <run-uuid> [-m <model>] [prompt]. Never-ask posture (codex ADR-0037 parity);--skip-trust= per-session trust, zero global mutation;--session-idis lab-generated per run → deterministic transcript identity. InitialPrompt rides the trailing positional (0.42.0 help: positionalquerystarts interactive mode).~/.gemini/tmp/<project-id>/chats/session-<sessionId>.jsonl. Verified in the 0.42.0 bundle: JSONL, born eagerly atChatRecordingService.initialize()(metadata record before any turn — no codex-style lazy-birth deadlock, #96 class). Grammar: metadata,user,gemini(thoughts/tokens/model/embeddedtoolCalls),$set,$rewindTo; updates re-append with the same messageid→ fold is last-writer-wins by id, not pure append of new items.LocateTranscriptresolves via the spawned session-id (filename issession-<sessionId>.jsonl), no cwd-match heuristic.GEMINI_TELEMETRY_ENABLED/TARGET/OUTFILE/LOG_PROMPTS/…env vars and emitsgemini_cli.user_prompt,gemini_cli.tool_call, conversation-finished events. Plan: per-run outfile in the runtime dir;ReadChatfolds transcript for content + telemetry events for working/ended edges (claude's #62 hook-edge pattern with a native feed).SeedMeta.ContextFileName = "GEMINI.local.md"(ADR-0035.localrule), wired by a marker-guarded merge into user-level~/.gemini/settings.jsonsetting the context filename list to["GEMINI.local.md", "GEMINI.md", "AGENTS.md"]— lab context always read, repo-tracked files keep working, no per-worktree tracked-file collision. (Codex global-config-append precedent.)NativeSkillDiscovery: true,SkillsDir: ".gemini/skills"— 0.42.0 has native Agent-Skills support (gemini skillssubcommand). First non-claude provider with native discovery; no generated index section.-m.Efforts()= single sentinel (default) mapped to no flag (conformance requires non-empty catalogs; gemini has no effort knob).git commititself). Patterns in the BRE∩RE2 dialect + fixture samples, codex §9 precedent.general.sessionRetention.enabledgatescleanupExpiredSessions; disabled by default) — durable-transcript requirement satisfied; the settings merge must never enable it. Record in compat.md.Tier-2 spike briefs (answer live on 0.42.0, post as an amendment comment BEFORE adapter code — codex #87 Amendment-2 pattern)
--session-idflows into the chats/ filename (+safeSessionIdsanitization);<project-id>dirname scheme for a given worktree; eager birth confirmed live;/clear— does it rotate the file or$rewindTo? epoch/identity obligation on context-clear;/resume/session-browser hazards; interactive vs-precording parity.ENABLED+TARGET+OUTFILE;USE_COLLECTORoff) with no GCP export; event set actually written in 0.42.0; does a turn-end event fire reliably after every turn (incl. tool-heavy turns)? prompts not logged (LOG_PROMPTS=false); file growth/rotation behavior.--approval-mode yolo --skip-trustwith seeded auth (expected: none →AnswerDialog=ErrDialogNotAnswerable).context.fileNamevs legacy flatcontextFileName); list semantics (all matching files loaded vs first match); interplay with global~/.gemini/GEMINI.md;.gitmemory boundary;/memory showas the verification probe.NO_BROWSER=truepaste-code flow live (scrape regexes for URL + code prompt, completion detection);AuthStatussurface — 0.42.0 has no auth/login subcommand, so status likely = creds-file presence/expiry (oauth_creds.json) +google_accounts.jsonemail, force-refresh via a cheap probe;Logout= creds-file removal (codex escalation precedent); creds-refresh behavior when the seeded token expires./modelinventory under the free-tier account; one real turn per candidate; the quota-exhaustion failure mode (429 vs silent fallback model) and what it looks like in transcript + telemetry (State must stay honest).scrub-markerssubtest.Implementation checklist (docs/agents/provider-authoring.md, ADR-0036)
internal/provider/gemini/mirroring codex file-for-file → SeedMeta (context file, skills dir, excludes, seeded-path patterns, scrub patterns) → generic host config only (no provider-named config fields) → registration row ininternal/provider/conformance_test.go+provider.NewRegistry(...)incmd/lab/main.go→ spikes →internal/compat/gemini/compat.md→ neutrality tests stay green.Open implementation point (recommendation, implementing agent verifies)
Per-run telemetry env needs spawn-time env plumbing: 0.42.0 has no
--telemetry*CLI flags (bundle-verified), only env vars/settings.LiveSignals.Setup(runID, dir)already hands the provider per-run runtime wiring but returns extra args, and core'sspawnEnv(internal/instance/start.go) is core-owned. Recommended: extend the LiveSignals capability so Setup can also contribute env (claude returns none), or set the vars via tmux session environment before spawn. Coordinate with #124, which touches the same spawnEnv seam.DoD
providertest.Conformancegreen in CI for the gemini adapter.internal/compat/gemini/compat.md(provenance-tagged like codex).Amendment 1 — Tier-2 spike sweep (2026-07-11, gemini-cli 0.42.0)
Environment: nix-store
gemini-cli 0.42.0on the lab host (/nix/store/7aw1jsvlxmj5p6dv3yr5sz3sd868piny-gemini-cli-0.42.0/bin/gemini,--version→0.42.0).Provenance legend:
live= driven against the real binary in tmux on the lab host today;bundle= extracted from the shipped 0.42.0 JS bundle (esbuild output retains identifiers; every claim traced to its source chunk);cli=--help/subcommand extraction. Creds caveat:~/.gemini/oauth_creds.jsonis NOT seeded on the host (decision 3 expected a maintainer copy; codex'sauth.jsonis present, gemini's creds are not — maintainer push-notified 2026-07-11). Everything that needs an authenticated turn is answered from the bundle and flagged residual-live below; a second amendment upgrades provenance when creds land.S1 transcript
~/.gemini/tmp/<project-id>/chats/session-<yyyy-MM-ddThh-mm>-<sid8>.jsonlwhere<sid8>= first 8 chars ofsanitizeFilenamePart(sessionId)([^a-zA-Z0-9_-]→_). live + bundle (ChatRecordingService, SESSION_FILE_PREFIX).<project-id>is NOT a hash: slugifiedbasename(resolve(cwd))(lowercase, non-alnum →-), deduped-1,-2,…via a registry in~/.gemini/projects.json+ a.project_rootownership marker inside the project tmp dir. TheprojectHashfield inside the metadata record issha256(cwd)— legacy-migration only, never the dirname. live + bundle.{sessionId, projectHash, startTime, lastUpdated, kind:"main"}) was on disk before any turn and before auth completed (TUI path). Headless-precords identically (Config.initialize()→ recording init is unconditional; only ENOSPC disables it). bundle. One nuance (live): when auth runs on the pre-TUI stdin path (selectedType set, creds absent), the file is born only when the TUI starts after auth.--session-idcollision: relaunching with an already-used id hard-errors, exit 42 (FATAL_INPUT_ERROR):Error starting session: Session ID "…" already exists. Use --resume to resume it, or provide a different ID.live. Accepted charset^[a-zA-Z0-9-_]+$(yargs coerce; not UUID-validated). bundle. ⇒ lab mints a fresh id per spawn./clear(alias/new): rotates — CLI generates a freshrandomUUID(),resetChat()starts a new file; the old file is left untouched (no$rewindTo, no truncation). bundle (clearCommand → resetNewSessionState). ⇒ epoch obligation: after/clearthe lab-minted session id no longer names the live file;LocateTranscriptresolves the project dir viaprojects.json(worktree path → slug) and prefers the exact<sid8>filename match, falling back to the newestsession-*.jsonlin that dir (each run has its own worktree ⇒ its own project dir, so newest-in-dir is the run's current epoch)./resume/session-browser: resumes into the OLD file; the in-memory session id reverts to the old id and a$setis appended. bundle. Hazard recorded (chat-unsafe command, curated out).{id, timestamp, type: user|gemini|info|warning|error, content, displayContent?, thoughts?, tokens?, model?, toolCalls?};{$set:{…partial metadata}};{$rewindTo: messageId}. Streaming/tool-status updates re-append a full record with the SAMEid⇒ fold is last-writer-wins by id;$rewindTodrops the matched id and everything after (the CLI's own loader does exactly this). bundle.S2 telemetry edges
argv ?? env ?? settingsprecedence):GEMINI_TELEMETRY_ENABLED,_TRACES_ENABLED,_TARGET(local|gcp, else FatalConfigError),_OTLP_ENDPOINT,_OTLP_PROTOCOL,_OUTFILE,_LOG_PROMPTS,_USE_COLLECTOR,_USE_CLI_AUTH. bundle.ENABLED=true+TARGET=local+OUTFILE=<path>(+LOG_PROMPTS=false). OUTFILE forces File{Span,Log,Metric}Exporters;USE_COLLECTORis irrelevant attarget=local; zero OTEL network. BUT Clearcut usage-stats is a separate pipeline, ON by default (privacy.usageStatisticsEnableddefaulttrue, no env/flag) — the lab settings merge sets itfalsefor true zero-export. bundle.JSON.stringify(x, null, 2)+\n, appended (flags:"a", no rotation/truncate ever) ⇒ per-run outfile under the runtime dir, folded with a brace-balanced reader, GC'd bySweepSpools. bundle.gemini_cli.user_prompt(on submit, before send),tool_call(per completed call),api_request/api_response(per model round-trip — several per tool-heavy turn),flash_fallback(silent quota fallback),next_speaker_check(disabled by default:model.skipNextSpeakerCheckdefaultstrue),conversation_finished— single call site, fires only in the interactive TUI under--approval-mode yoloon the responding→idle transition with turnCount>0. That is exactly lab's spawn posture ⇒ working edge =user_prompt, idle edge =conversation_finished; no scraping, native feed (#62 hard-evidence rule satisfied). bundle. residual-live: confirmconversation_finishedlands in the outfile after every turn incl. tool-heavy ones.LOG_PROMPTS=falsestrips prompt/response/function_args text from events. bundle.S3 reply / interrupt / dialogs
/rewind(writes$rewindTo— epoch hazard). Ctrl-C BANNED: first press cancels + warns, second press within the window runs/quitregardless of input-buffer state (kills the session — stricter hazard than codex). bundle + docs; Ctrl-C-at-login-prompt no-exit also observed live.--approval-mode yolo --skip-trust+ seeded auth + lab settings: trust dialog suppressed (--skip-trust⇒GEMINI_CLI_TRUST_WORKSPACE=trueenv, per-invocation, never persisted); tool/shell approvals suppressed (yolo policy); theme picker only on an invalidui.theme(absent key does NOT open it); auth dialog only whensecurity.auth.selectedTypeis absent (creds presence is irrelevant — the merge sets it); update nag default-off. Reachable residuals:ProQuotaDialogon daily quota exhaustion (see S6) and the auth dialog after a failed token refresh.AnswerDialogstaysErrDialogNotAnswerablein v1 (codex precedent). bundle; auth-choice dialog itself captured live pre-auth.S4 context-file discovery
context.fileName(string OR array). The v2 nested settings shape confirmed live — 0.42.0 itself wrote{"security":{"auth":{"selectedType":"oauth-personal"}}}on auth selection. Default context nameGEMINI.md.--- Context from: <path> ---blocks) — not first-match-wins. ⇒["GEMINI.local.md","GEMINI.md","AGENTS.md"]gives lab context always + repo-tracked files keep working. bundle.~/.gemini/<configured names>(the configured list applies there too — no hardcoded GEMINI.md fallback); upward walk stops one directory ABOVE the.gitproject root (quirk recorded — it readsdirname(projectRoot)too); downward BFS capped bycontext.discoveryMaxDirs(default 200). Workspace.gemini/settings.jsonoverrides user settings when trusted (and--skip-trustmakes it trusted) ⇒ a repo-committed settings file could overridecontext.fileName— recorded as a hazard, accepted for v1. Unknown/invalid settings keys are warn-only, never fatal ⇒ the user-level merge is safe. bundle.~/.gemini/settings.jsonsettingcontext.fileName=["GEMINI.local.md","GEMINI.md","AGENTS.md"],security.auth.selectedType="oauth-personal"(auth dialog gates on this setting),privacy.usageStatisticsEnabled=false(Clearcut kill, S2). It must NEVER touchgeneral.sessionRetention(cleanup stays opt-in-disabled ⇒ durable transcripts, decision 11). residual-live:/memory showprobe under a real turn.S5 auth
NO_BROWSER=<truthy>env; also auto-suppressed when linux has noDISPLAY/WAYLAND_DISPLAY/MIR_SOCKET, orCI=true, orDEBIAN_FRONTEND=noninteractive, or ssh on non-linux. (Lab sandbox has no DISPLAY ⇒ suppressed regardless; the adapter still setsNO_BROWSER=truefor determinism.) bundle.selectedTypeset and no creds, a pre-TUI plain-stdio flow printsPlease visit the following URL to authorize the application:+ the Google URL (PKCE S256,state, redirecthttps://codeassist.google.com/authcode— the code is shown to the user on a Google page, no localhost listener), then promptsEnter the authorization code:(readline on stdin ⇒ tmux paste + Enter works). Invalid code (live):Failed to authenticate with authorization code:invalid_grant+Failed to authenticate with user code. Retrying...+ a fresh URL (new PKCE challenge/state) ⇒ the scraper must always take the LAST URL in the pane. Two retries, thenFatalAuthenticationError. No success banner in this path ⇒ completion detection =oauth_creds.jsonbirth (the completion poller watches the file) and/or the TUI rendering. Ctrl-C at the code prompt does not exit (live) ⇒ teardown = kill the login session.LoginStartdesign: ensure the user-settings merge (soselectedTypeforces the pre-TUI path), spawn baregeminiwithNO_BROWSER=truein thelab-login-geminisession, scrape the URL;LoginSubmitCodepastes the code (gemini, unlike codex, implements it — flow kindoauth-code); completion poller = creds-file watch.~/.gemini/oauth_creds.json(google-auth-library Credentials:access_token,refresh_token,scope,token_type,expiry_datems-epoch, optionalid_token; 0600; rewritten on every silent refresh) and~/.gemini/google_accounts.json({active: <email>, old: []}). Silent refresh: expired access token + valid refresh token refreshes with no interaction; a FAILED refresh only debug-logs and falls through to the login flow (TUI: auth dialog / "(OAuth expired)"). bundle.AuthStatus= creds-file presence +expiry_date/refresh_tokeninspection + email fromgoogle_accounts.json(0.42.0 has no auth/login/logout CLI subcommand — confirmed, top-level commands are onlymcp|extensions|skills|hooks|gemma).Logout= removeoauth_creds.json+ clear the active account (the/auth signoutslash command does exactly this internally; file removal is the codex rm-escalation precedent). bundle + cli. residual-live: completed code-paste round-trip and refresh-after-expiry (needs the maintainer's Google account once).S6 models + quota
-m⇒auto-gemini-3("Auto (Gemini 3)"), resolved at request time togemini-3-pro-preview/gemini-3-flash-preview, auto-downgrading toauto-gemini-2.5when the account lacks preview access./modellist is hardcoded constants gated by server-fetched entitlements:gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite,gemini-3-pro-preview,gemini-3.1-pro-preview,gemini-3.1-flash-lite-preview,gemini-3-flash-preview(+ experimental gemma). bundle./model+one-turn sweep when creds land): Default = omit-m(labelled "Auto"); explicitgemini-3-pro-preview,gemini-3-flash-preview,gemini-2.5-pro,gemini-2.5-flash.Efforts()= singledefaultsentinel → no flag (gemini has no effort knob; conformance needs a non-empty catalog).activateFallbackMode) — observable as thegemini_cli.flash_fallbacktelemetry event + themodelfield on subsequent transcript records; hard daily exhaustion (TerminalQuotaError, the free-tier ceiling) ⇒ blockingProQuotaDialog: "Usage limit reached for . Access resets atS7 attribution ground truth
Co-authored-by/Generated withstrings anywhere, nogit committemplating of any kind (the CLI has no built-in commit tool; commits only happen if the model runs git in the shell tool). bundle. ⇒ScrubPatternsship defensive-only (BRE∩RE2: co-authored-by-Gemini + Generated-with-Gemini shapes) with synthetic fixture samples, codex §9 precedent. residual-live (incogni): capture commits/PR bodies from a real gemini run before flipping the samples to captured ground truth.Cross-cutting implementation pins
gemini --approval-mode yolo --skip-trust --session-id <lab-uuid> [-m <model>] [prompt]. Positional query starts INTERACTIVE mode with the prompt as first turn when stdin is a TTY and not CI (isHeadlessModestage-1/2 logic; tmux = TTY; lab must never setCIin spawn env) — 0.42.0 prints a startup notice about this default. cli + bundle.--approval-modechoices aredefault|auto_edit|yolo|plan.Setupcan contribute spawn env (claude returns nil) — landed as commit20f1c53onafk/126(coordinates with #124's SetupOpts; settings-file arming unchanged).general.sessionRetentionabsent ⇒cleanupExpiredSessionsreturns disabled — opt-in confirmed at the schema level (default: void 0); the lab merge never sets it. bundle.gemini skills list|enable|disable|install|link|uninstall(cli); discovery at workspace.gemini/skills(trusted ⇒--skip-trustsuffices) + user~/.gemini/skills(+.agents/skillsvariants);skills.enableddefaults true. bundle. ⇒NativeSkillDiscovery: true,SkillsDir: ".gemini/skills".Creds-gated residual list (second amendment when
~/.gemini/oauth_creds.jsonis seeded)geminirecords incl. toolCalls/thoughts) andconversation_finished-in-outfile confirmation (S1/S2)./memory showcontext-discovery probe (S4)./modelinventory + one turn per candidate + real quota-exhaustion shape (S6).codex debug models+ per-model efforts seam #156/land-pr audit — PR #129: FAIL (blocking correctness bug)
Verification signal: Forgejo Actions CI. On the pre-merge head both gates were green (
ci / native,ci-nix / flake-check— the nix gate did exercise this diff, which matters since it touchesagentPackages). After the conflict resolution below I re-ran the project's own gates locally on the resolved tree:go build ./...✅, fullgo test ./...✅,golangci-lint 2.12.20 issues ✅. PR touches noweb/files, so the vitest surface is untouched.Merge conflict — resolved and pushed to
afk/126The branch was behind
main(#161/#158/#155/#153 landed since). Two resolutions, both pushed:cmd/lab/main.go— import union (8609bfe).mainaddedinternal/pull, the PR addedinternal/provider/gemini, adjacent lines. Deterministic, behaviour-preserving.internal/provider/gemini— semantic break git merged textually (45e3e48). #158 changedAgentProvider.Models()to return[]provider.ModelOption(per-model efforts + reported default). The gemini adapter still implemented the pre-#156[]provider.Optionsignature, so the merged tree did not compile. Ported to claude-code's shape: gemini has no reasoning-effort knob at all (singledefaultsentinel, no flag emitted inSpawnArgv), so every model carries the same shared efforts list and noDefaultEffort. Values, order and the spawn default unchanged; the compat §10 pin restated in the enriched shape.🔴 Blocker — deterministic
--session-idcollides on every re-run of an issueinternal/provider/gemini/gemini.go:129derives the session id asUUIDv5(ns, tmuxSessionName), and bothSpawnArgvandLocateTranscriptrecompute it. ADR-0041 justifies this ("Considered options" → Persisting a per-run random--session-id) with "the collision error is avoided because tmux session names are unique per run". That premise is false:afk.Label(n, auto)=afk-<N>(internal/afk/decide.go:179) → session<repo>~afk-<N>(internal/afk/launch.go:223), worktree<repo>-<N>(internal/instance/instance.go:212). Both are pure functions of the issue number and repeat verbatim on every re-claim/re-run of the same issue.gitx.UniqueManualLabelde-dupes only against live tmux sessions (internal/instance/start.go:80-83), so a stop→start of the same label reproduces the name.Gemini 0.42.0 hard-errors
exit 42 FATAL_INPUT_ERRORwhen the id already exists — and the store it checks is~/.gemini/tmp/<project-slug>/chats/(compat.md §185), keyed by the worktree path through~/.gemini/projects.json. That lives in HOME, not in the worktree, so it survives worktree deletion, and session files are never cleaned (decision 11 deliberately leavessessionRetentionalone).Consequence: the second AFK run on any given issue spawns gemini → exit 42 → tmux session dies instantly → dead run, no transcript. Re-running a failed issue is the normal path, so this fires in ordinary use. The PR's own
TestCompat_Live_eagerBirthAndSessionIDCollisionpins exactly this behaviour. Secondary symptom from the same root cause:LocateTranscript(chat.go:68-73) prefers the exact<sid8>match, so a re-run in a reused worktree would bind to the previous run's transcript.Fix direction: mint a per-run unique id (persist it on the run row, or fold the run id / a nonce into the UUIDv5 input) rather than deriving it from the session name alone.
🟠 Serious — telemetry liveness edges arrive up to ~5 s late
Gemini builds
BatchLogRecordProcessorunconfigured, so OTEL's default 5 sscheduledDelayMillisapplies. The transcript'suserrecord is written synchronously at submit, butgemini_cli.user_promptonly reaches the outfile at the next flush. In that windowlivenessState(livesignals.go:169) still sees the previousconversation_finishedand reportsStateNeedsInputwhile the agent is working — andinternal/chat/notify.go:35debounces for only 2 s, so a seeded run can fire a spurious "needs you" push seconds after spawn. Cheap fix: setOTEL_BLRP_SCHEDULE_DELAY/OTEL_BSP_SCHEDULE_DELAYinSetup's env (the bundled SDK honours both). At minimum, compat.md §telemetry should pin the batch delay — it currently reads as if the edges are immediate.🟡 Note — decision 7 divergence
Decision 7 asked for a marker-guarded merge into
~/.gemini/settings.json;seed.go:72 EnsureUserSettingsimplements a semantic-guard merge (re-enforcescontext.fileNameto lab's list on every run). Defensible for JSON and documented in ADR-0041, but it silently overwrites an operator's customcontext.fileName— worth a maintainer nod.Verified clean (explicitly refuted as findings)
LiveSignals.Setup's new env return leaves claude-code byte-identical (nil env, arming payload untouched, sole call site updated, no slice aliasing); the transcript fold (last-writer-wins by id +$rewindTo, subagent-file filtering, reverse-time sort) matches the CLI's own loader; the telemetry byte-scan can't be poisoned (no other 0.42.0 record carries the event literals,LOG_PROMPTS=false, Clearcut killed); no nil derefs, goroutine leaks, or resource leaks.Verdict: FAIL on the session-id collision. The adapter otherwise genuinely mirrors codex file-for-file and every fragile 0.42.0 coupling I spot-checked against the bundle holds. Not merged.