feat(provider): gemini-cli adapter — third AgentProvider, spec pinned to 0.42.0 (grilled 2026-07-11) #126

Open
opened 2026-07-11 02:30:29 +02:00 by dominik.polakovics · 2 comments

Third AgentProvider: Gemini CLI, at the Codex bar (ADR-0036 DoD — Tier-1 conformance green + four Tier-2 live spikes evidenced by a committed internal/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)

  1. Scope: full parity in one landing — manual runs, AFK runs, auth, conformance. Same DoD as codex #87.
  2. Binary/version: nix-store gemini-cli 0.42.0 (/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 its agentPackages.gemini parameter wired so the bin lands on PATH (ADR-0033 baseline).
  3. Auth: Google OAuth (Code Assist free tier), seeded creds — maintainer completes the flow once / copies ~/.gemini/oauth_creds.json (codex auth.json precedent). AuthFlow kind oauth-code: NO_BROWSER=true prints an auth URL and takes a pasted authorization code on stdin → LoginStart scrapes the URL from the lab-login-gemini session, LoginSubmitCode delivers 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.
  4. Spawn argv: 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-id is lab-generated per run → deterministic transcript identity. InitialPrompt rides the trailing positional (0.42.0 help: positional query starts interactive mode).
  5. Transcript: ~/.gemini/tmp/<project-id>/chats/session-<sessionId>.jsonl. Verified in the 0.42.0 bundle: JSONL, born eagerly at ChatRecordingService.initialize() (metadata record before any turn — no codex-style lazy-birth deadlock, #96 class). Grammar: metadata, user, gemini (thoughts/tokens/model/embedded toolCalls), $set, $rewindTo; updates re-append with the same message idfold is last-writer-wins by id, not pure append of new items. LocateTranscript resolves via the spawned session-id (filename is session-<sessionId>.jsonl), no cwd-match heuristic.
  6. Liveness: telemetry-outfile edges. The session file has NO turn-lifecycle records (verified 0.42.0; true upstream on main too) — inferring idle from tool-status/tokens violates the #62 hard-evidence rule. 0.42.0 has GEMINI_TELEMETRY_ENABLED/TARGET/OUTFILE/LOG_PROMPTS/… env vars and emits gemini_cli.user_prompt, gemini_cli.tool_call, conversation-finished events. Plan: per-run outfile in the runtime dir; ReadChat folds transcript for content + telemetry events for working/ended edges (claude's #62 hook-edge pattern with a native feed).
  7. Context file: SeedMeta.ContextFileName = "GEMINI.local.md" (ADR-0035 .local rule), wired by a marker-guarded merge into user-level ~/.gemini/settings.json setting 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.)
  8. Skills: NativeSkillDiscovery: true, SkillsDir: ".gemini/skills" — 0.42.0 has native Agent-Skills support (gemini skills subcommand). First non-claude provider with native discovery; no generated index section.
  9. Models/Efforts: catalog pinned by live spike under the real account (candidates: CLI default/auto first, then explicit gemini-3-pro / gemini-3-flash if served on free tier). Default entry = omit -m. Efforts() = single sentinel (default) mapped to no flag (conformance requires non-empty catalogs; gemini has no effort knob).
  10. ScrubPatterns: defensive only — gemini CLI writes no commit attribution (no Co-authored-by machinery upstream; commits only happen if the model runs git commit itself). Patterns in the BRE∩RE2 dialect + fixture samples, codex §9 precedent.
  11. Retention: 0.42.0 session cleanup is opt-in (general.sessionRetention.enabled gates cleanupExpiredSessions; 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)

  • S1 transcript: --session-id flows into the chats/ filename (+ safeSessionId sanitization); <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 -p recording parity.
  • S2 telemetry edges: exact env combo for local-file-only telemetry (ENABLED + TARGET + OUTFILE; USE_COLLECTOR off) 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.
  • S3 reply/interrupt recipes: send-keys reply (paste + Enter pacing); mid-turn input queue semantics (Tab queue toggle?); interrupt = which key (Esc clears input, double-Esc opens rewind browser — hazard; Ctrl-C cancels request but quits when the input line is empty — hazard-check like codex's Ctrl-C ban); dialog inventory under --approval-mode yolo --skip-trust with seeded auth (expected: none → AnswerDialog = ErrDialogNotAnswerable).
  • S4 context-file discovery: settings key shape in 0.42.0 (nested context.fileName vs legacy flat contextFileName); list semantics (all matching files loaded vs first match); interplay with global ~/.gemini/GEMINI.md; .git memory boundary; /memory show as the verification probe.
  • S5 auth: NO_BROWSER=true paste-code flow live (scrape regexes for URL + code prompt, completion detection); AuthStatus surface — 0.42.0 has no auth/login subcommand, so status likely = creds-file presence/expiry (oauth_creds.json) + google_accounts.json email, force-refresh via a cheap probe; Logout = creds-file removal (codex escalation precedent); creds-refresh behavior when the seeded token expires.
  • S6 models + quota: /model inventory 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).
  • S7 attribution ground truth (incogni): confirm zero attribution markers in commits/PR bodies authored by a gemini run; collect fixture sample lines for the conformance scrub-markers subtest.

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 in internal/provider/conformance_test.go + provider.NewRegistry(...) in cmd/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's spawnEnv (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

  • Tier-1 providertest.Conformance green in CI for the gemini adapter.
  • All spikes above answered live on 0.42.0, posted as an amendment comment, distilled into internal/compat/gemini/compat.md (provenance-tagged like codex).
  • Manual run + AFK run + login/logout work end-to-end; picker shows Gemini with the spike-pinned model catalog.
  • ADR documenting the adapter (codex ADR-0037 precedent).
Third `AgentProvider`: Gemini CLI, at the Codex bar (ADR-0036 DoD — Tier-1 conformance green + four Tier-2 live spikes evidenced by a committed `internal/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) 1. **Scope**: full parity in one landing — manual runs, AFK runs, auth, conformance. Same DoD as codex #87. 2. **Binary/version**: nix-store **gemini-cli 0.42.0** (`/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 its `agentPackages.gemini` parameter wired so the bin lands on PATH (ADR-0033 baseline). 3. **Auth**: Google OAuth (Code Assist free tier), **seeded creds** — maintainer completes the flow once / copies `~/.gemini/oauth_creds.json` (codex auth.json precedent). `AuthFlow` kind **`oauth-code`**: `NO_BROWSER=true` prints an auth URL and takes a pasted authorization code on stdin → `LoginStart` scrapes the URL from the `lab-login-gemini` session, `LoginSubmitCode` delivers 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. 4. **Spawn argv**: `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-id` is lab-generated per run → deterministic transcript identity. InitialPrompt rides the trailing positional (0.42.0 help: positional `query` starts interactive mode). 5. **Transcript**: `~/.gemini/tmp/<project-id>/chats/session-<sessionId>.jsonl`. Verified in the 0.42.0 bundle: **JSONL, born eagerly** at `ChatRecordingService.initialize()` (metadata record before any turn — no codex-style lazy-birth deadlock, #96 class). Grammar: metadata, `user`, `gemini` (thoughts/tokens/model/embedded `toolCalls`), `$set`, `$rewindTo`; updates re-append with the same message `id` → **fold is last-writer-wins by id**, not pure append of new items. `LocateTranscript` resolves via the spawned session-id (filename is `session-<sessionId>.jsonl`), no cwd-match heuristic. 6. **Liveness**: **telemetry-outfile edges**. The session file has NO turn-lifecycle records (verified 0.42.0; true upstream on main too) — inferring idle from tool-status/tokens violates the #62 hard-evidence rule. 0.42.0 has `GEMINI_TELEMETRY_ENABLED/TARGET/OUTFILE/LOG_PROMPTS/…` env vars and emits `gemini_cli.user_prompt`, `gemini_cli.tool_call`, conversation-finished events. Plan: per-run outfile in the runtime dir; `ReadChat` folds transcript for content + telemetry events for working/ended edges (claude's #62 hook-edge pattern with a native feed). 7. **Context file**: `SeedMeta.ContextFileName = "GEMINI.local.md"` (ADR-0035 `.local` rule), wired by a **marker-guarded merge into user-level `~/.gemini/settings.json`** setting 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.) 8. **Skills**: `NativeSkillDiscovery: true`, `SkillsDir: ".gemini/skills"` — 0.42.0 has native Agent-Skills support (`gemini skills` subcommand). First non-claude provider with native discovery; no generated index section. 9. **Models/Efforts**: catalog pinned by live spike under the real account (candidates: CLI default/auto first, then explicit gemini-3-pro / gemini-3-flash if served on free tier). Default entry = omit `-m`. `Efforts()` = single sentinel (`default`) mapped to no flag (conformance requires non-empty catalogs; gemini has no effort knob). 10. **ScrubPatterns**: defensive only — gemini CLI writes no commit attribution (no Co-authored-by machinery upstream; commits only happen if the model runs `git commit` itself). Patterns in the BRE∩RE2 dialect + fixture samples, codex §9 precedent. 11. **Retention**: 0.42.0 session cleanup is **opt-in** (`general.sessionRetention.enabled` gates `cleanupExpiredSessions`; 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) - **S1 transcript**: `--session-id` flows into the chats/ filename (+ `safeSessionId` sanitization); `<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 `-p` recording parity. - **S2 telemetry edges**: exact env combo for local-file-only telemetry (`ENABLED` + `TARGET` + `OUTFILE`; `USE_COLLECTOR` off) 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. - **S3 reply/interrupt recipes**: send-keys reply (paste + Enter pacing); mid-turn input queue semantics (Tab queue toggle?); interrupt = which key (Esc clears input, double-Esc opens rewind browser — hazard; Ctrl-C cancels request but **quits when the input line is empty** — hazard-check like codex's Ctrl-C ban); dialog inventory under `--approval-mode yolo --skip-trust` with seeded auth (expected: none → `AnswerDialog` = `ErrDialogNotAnswerable`). - **S4 context-file discovery**: settings key shape in 0.42.0 (nested `context.fileName` vs legacy flat `contextFileName`); list semantics (all matching files loaded vs first match); interplay with global `~/.gemini/GEMINI.md`; `.git` memory boundary; `/memory show` as the verification probe. - **S5 auth**: `NO_BROWSER=true` paste-code flow live (scrape regexes for URL + code prompt, completion detection); `AuthStatus` surface — 0.42.0 has **no auth/login subcommand**, so status likely = creds-file presence/expiry (`oauth_creds.json`) + `google_accounts.json` email, force-refresh via a cheap probe; `Logout` = creds-file removal (codex escalation precedent); creds-refresh behavior when the seeded token expires. - **S6 models + quota**: `/model` inventory 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). - **S7 attribution ground truth (incogni)**: confirm zero attribution markers in commits/PR bodies authored by a gemini run; collect fixture sample lines for the conformance `scrub-markers` subtest. ## 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 in `internal/provider/conformance_test.go` + `provider.NewRegistry(...)` in `cmd/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's `spawnEnv` (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 - Tier-1 `providertest.Conformance` green in CI for the gemini adapter. - All spikes above answered live on 0.42.0, posted as an amendment comment, distilled into `internal/compat/gemini/compat.md` (provenance-tagged like codex). - Manual run + AFK run + login/logout work end-to-end; picker shows Gemini with the spike-pinned model catalog. - ADR documenting the adapter (codex ADR-0037 precedent).
Author
Owner

Amendment 1 — Tier-2 spike sweep (2026-07-11, gemini-cli 0.42.0)

Environment: nix-store gemini-cli 0.42.0 on the lab host (/nix/store/7aw1jsvlxmj5p6dv3yr5sz3sd868piny-gemini-cli-0.42.0/bin/gemini, --version0.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.json is NOT seeded on the host (decision 3 expected a maintainer copy; codex's auth.json is 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

  • Path shape: ~/.gemini/tmp/<project-id>/chats/session-<yyyy-MM-ddThh-mm>-<sid8>.jsonl where <sid8> = first 8 chars of sanitizeFilenamePart(sessionId) ([^a-zA-Z0-9_-]_). live + bundle (ChatRecordingService, SESSION_FILE_PREFIX).
  • <project-id> is NOT a hash: slugified basename(resolve(cwd)) (lowercase, non-alnum → -), deduped -1,-2,… via a registry in ~/.gemini/projects.json + a .project_root ownership marker inside the project tmp dir. The projectHash field inside the metadata record is sha256(cwd) — legacy-migration only, never the dirname. live + bundle.
  • Eager birth: confirmed live — the metadata header record ({sessionId, projectHash, startTime, lastUpdated, kind:"main"}) was on disk before any turn and before auth completed (TUI path). Headless -p records 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-id collision: 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 fresh randomUUID(), resetChat() starts a new file; the old file is left untouched (no $rewindTo, no truncation). bundle (clearCommand → resetNewSessionState). ⇒ epoch obligation: after /clear the lab-minted session id no longer names the live file; LocateTranscript resolves the project dir via projects.json (worktree path → slug) and prefers the exact <sid8> filename match, falling back to the newest session-*.jsonl in 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 $set is appended. bundle. Hazard recorded (chat-unsafe command, curated out).
  • Grammar (all record shapes the 0.42.0 writer emits): header metadata record (no wrapper key); message records {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 SAME id ⇒ fold is last-writer-wins by id; $rewindTo drops the matched id and everything after (the CLI's own loader does exactly this). bundle.

S2 telemetry edges

  • Env surface (9 vars, all argv ?? env ?? settings precedence): GEMINI_TELEMETRY_ENABLED, _TRACES_ENABLED, _TARGET (local|gcp, else FatalConfigError), _OTLP_ENDPOINT, _OTLP_PROTOCOL, _OUTFILE, _LOG_PROMPTS, _USE_COLLECTOR, _USE_CLI_AUTH. bundle.
  • Local-file-only combo: ENABLED=true + TARGET=local + OUTFILE=<path> (+ LOG_PROMPTS=false). OUTFILE forces File{Span,Log,Metric}Exporters; USE_COLLECTOR is irrelevant at target=local; zero OTEL network. BUT Clearcut usage-stats is a separate pipeline, ON by default (privacy.usageStatisticsEnabled default true, no env/flag) — the lab settings merge sets it false for true zero-export. bundle.
  • Outfile format: NOT JSONL — each OTEL record is pretty-printed 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 by SweepSpools. bundle.
  • Events + firing points: 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.skipNextSpeakerCheck defaults true), conversation_finished — single call site, fires only in the interactive TUI under --approval-mode yolo on 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: confirm conversation_finished lands in the outfile after every turn incl. tool-heavy ones.
  • LOG_PROMPTS=false strips prompt/response/function_args text from events. bundle.

S3 reply / interrupt / dialogs

  • Reply: bracketed paste + Enter. Enter while the model is busy queues natively and auto-flushes at idle; Tab is a dedicated queue key while generating. bundle + shipped docs.
  • Interrupt recipe: single Esc (cancels the in-flight request; when idle it only arms a self-clearing escape prompt). Double-Esc BANNED: within 500 ms it clears the input or submits /rewind (writes $rewindTo — epoch hazard). Ctrl-C BANNED: first press cancels + warns, second press within the window runs /quit regardless of input-buffer state (kills the session — stricter hazard than codex). bundle + docs; Ctrl-C-at-login-prompt no-exit also observed live.
  • Dialog inventory under --approval-mode yolo --skip-trust + seeded auth + lab settings: trust dialog suppressed (--skip-trustGEMINI_CLI_TRUST_WORKSPACE=true env, per-invocation, never persisted); tool/shell approvals suppressed (yolo policy); theme picker only on an invalid ui.theme (absent key does NOT open it); auth dialog only when security.auth.selectedType is absent (creds presence is irrelevant — the merge sets it); update nag default-off. Reachable residuals: ProQuotaDialog on daily quota exhaustion (see S6) and the auth dialog after a failed token refresh. AnswerDialog stays ErrDialogNotAnswerable in v1 (codex precedent). bundle; auth-choice dialog itself captured live pre-auth.

S4 context-file discovery

  • Key shape in 0.42.0: nested 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 name GEMINI.md.
  • List semantics: all matching names are loaded and concatenated per directory (wrapped in --- 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.
  • Scope: global ~/.gemini/<configured names> (the configured list applies there too — no hardcoded GEMINI.md fallback); upward walk stops one directory ABOVE the .git project root (quirk recorded — it reads dirname(projectRoot) too); downward BFS capped by context.discoveryMaxDirs (default 200). Workspace .gemini/settings.json overrides user settings when trusted (and --skip-trust makes it trusted) ⇒ a repo-committed settings file could override context.fileName — recorded as a hazard, accepted for v1. Unknown/invalid settings keys are warn-only, never fatal ⇒ the user-level merge is safe. bundle.
  • Merge target (decision 7 refined): marker-guarded idempotent merge into user ~/.gemini/settings.json setting context.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 touch general.sessionRetention (cleanup stays opt-in-disabled ⇒ durable transcripts, decision 11). residual-live: /memory show probe under a real turn.

S5 auth

  • No-browser triggers: NO_BROWSER=<truthy> env; also auto-suppressed when linux has no DISPLAY/WAYLAND_DISPLAY/MIR_SOCKET, or CI=true, or DEBIAN_FRONTEND=noninteractive, or ssh on non-linux. (Lab sandbox has no DISPLAY ⇒ suppressed regardless; the adapter still sets NO_BROWSER=true for determinism.) bundle.
  • Flow captured live: with selectedType set and no creds, a pre-TUI plain-stdio flow prints Please visit the following URL to authorize the application: + the Google URL (PKCE S256, state, redirect https://codeassist.google.com/authcode — the code is shown to the user on a Google page, no localhost listener), then prompts Enter 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, then FatalAuthenticationError. No success banner in this path ⇒ completion detection = oauth_creds.json birth (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.
  • LoginStart design: ensure the user-settings merge (so selectedType forces the pre-TUI path), spawn bare gemini with NO_BROWSER=true in the lab-login-gemini session, scrape the URL; LoginSubmitCode pastes the code (gemini, unlike codex, implements it — flow kind oauth-code); completion poller = creds-file watch.
  • Creds artifacts: ~/.gemini/oauth_creds.json (google-auth-library Credentials: access_token, refresh_token, scope, token_type, expiry_date ms-epoch, optional id_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_token inspection + email from google_accounts.json (0.42.0 has no auth/login/logout CLI subcommand — confirmed, top-level commands are only mcp|extensions|skills|hooks|gemma). Logout = remove oauth_creds.json + clear the active account (the /auth signout slash 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

  • Defaults: no -mauto-gemini-3 ("Auto (Gemini 3)"), resolved at request time to gemini-3-pro-preview/gemini-3-flash-preview, auto-downgrading to auto-gemini-2.5 when the account lacks preview access. /model list 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.
  • Lab catalog pin (bundle-provenance, to be refined by the live /model+one-turn sweep when creds land): Default = omit -m (labelled "Auto"); explicit gemini-3-pro-preview, gemini-3-flash-preview, gemini-2.5-pro, gemini-2.5-flash. Efforts() = single default sentinel → no flag (gemini has no effort knob; conformance needs a non-empty catalog).
  • Quota exhaustion (recorded, not worked around): transient 429/503 under auto-selection ⇒ silent fallback (activateFallbackMode) — observable as the gemini_cli.flash_fallback telemetry event + the model field on subsequent transcript records; hard daily exhaustion (TerminalQuotaError, the free-tier ceiling) ⇒ blocking ProQuotaDialog: "Usage limit reached for . Access resets at

S7 attribution ground truth

  • Bundle sweep: zero attribution machinery — no Co-authored-by/Generated with strings anywhere, no git commit templating of any kind (the CLI has no built-in commit tool; commits only happen if the model runs git in the shell tool). bundle. ⇒ ScrubPatterns ship 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

  • Spawn argv: 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 (isHeadlessMode stage-1/2 logic; tmux = TTY; lab must never set CI in spawn env) — 0.42.0 prints a startup notice about this default. cli + bundle. --approval-mode choices are default|auto_edit|yolo|plan.
  • Per-run telemetry env plumbing: the LiveSignals seam was extended so Setup can contribute spawn env (claude returns nil) — landed as commit 20f1c53 on afk/126 (coordinates with #124's SetupOpts; settings-file arming unchanged).
  • Retention: general.sessionRetention absent ⇒ cleanupExpiredSessions returns disabled — opt-in confirmed at the schema level (default: void 0); the lab merge never sets it. bundle.
  • Skills: native — gemini skills list|enable|disable|install|link|uninstall (cli); discovery at workspace .gemini/skills (trusted ⇒ --skip-trust suffices) + user ~/.gemini/skills (+ .agents/skills variants); skills.enabled defaults true. bundle. ⇒ NativeSkillDiscovery: true, SkillsDir: ".gemini/skills".

Creds-gated residual list (second amendment when ~/.gemini/oauth_creds.json is seeded)

  1. Live turn-fold fixtures (real gemini records incl. toolCalls/thoughts) and conversation_finished-in-outfile confirmation (S1/S2).
  2. /memory show context-discovery probe (S4).
  3. Completed login round-trip + refresh behavior (S5).
  4. /model inventory + one turn per candidate + real quota-exhaustion shape (S6).
  5. Incogni attribution ground truth from a real run (S7).
## Amendment 1 — Tier-2 spike sweep (2026-07-11, gemini-cli 0.42.0) Environment: nix-store `gemini-cli 0.42.0` on 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.json` is NOT seeded on the host (decision 3 expected a maintainer copy; codex's `auth.json` is 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 - Path shape: `~/.gemini/tmp/<project-id>/chats/session-<yyyy-MM-ddThh-mm>-<sid8>.jsonl` where `<sid8>` = first 8 chars of `sanitizeFilenamePart(sessionId)` (`[^a-zA-Z0-9_-]` → `_`). live + bundle (ChatRecordingService, SESSION_FILE_PREFIX). - `<project-id>` is NOT a hash: slugified `basename(resolve(cwd))` (lowercase, non-alnum → `-`), deduped `-1,-2,…` via a registry in `~/.gemini/projects.json` + a `.project_root` ownership marker inside the project tmp dir. The `projectHash` field inside the metadata record is `sha256(cwd)` — legacy-migration only, never the dirname. live + bundle. - Eager birth: confirmed **live** — the metadata header record (`{sessionId, projectHash, startTime, lastUpdated, kind:"main"}`) was on disk before any turn and before auth completed (TUI path). Headless `-p` records 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-id` collision: 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 fresh `randomUUID()`, `resetChat()` starts a new file; the old file is left untouched (no `$rewindTo`, no truncation). bundle (clearCommand → resetNewSessionState). ⇒ epoch obligation: after `/clear` the lab-minted session id no longer names the live file; `LocateTranscript` resolves the project dir via `projects.json` (worktree path → slug) and prefers the exact `<sid8>` filename match, falling back to the newest `session-*.jsonl` in 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 `$set` is appended. bundle. Hazard recorded (chat-unsafe command, curated out). - Grammar (all record shapes the 0.42.0 writer emits): header metadata record (no wrapper key); message records `{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 SAME `id` ⇒ fold is last-writer-wins by id; `$rewindTo` drops the matched id and everything after (the CLI's own loader does exactly this). bundle. ### S2 telemetry edges - Env surface (9 vars, all `argv ?? env ?? settings` precedence): `GEMINI_TELEMETRY_ENABLED`, `_TRACES_ENABLED`, `_TARGET` (`local|gcp`, else FatalConfigError), `_OTLP_ENDPOINT`, `_OTLP_PROTOCOL`, `_OUTFILE`, `_LOG_PROMPTS`, `_USE_COLLECTOR`, `_USE_CLI_AUTH`. bundle. - Local-file-only combo: `ENABLED=true` + `TARGET=local` + `OUTFILE=<path>` (+ `LOG_PROMPTS=false`). OUTFILE forces File{Span,Log,Metric}Exporters; `USE_COLLECTOR` is irrelevant at `target=local`; zero OTEL network. **BUT Clearcut usage-stats is a separate pipeline, ON by default** (`privacy.usageStatisticsEnabled` default `true`, no env/flag) — the lab settings merge sets it `false` for true zero-export. bundle. - Outfile format: NOT JSONL — each OTEL record is pretty-printed `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 by `SweepSpools`. bundle. - Events + firing points: `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.skipNextSpeakerCheck` defaults `true`), `conversation_finished` — single call site, fires **only in the interactive TUI under `--approval-mode yolo` on 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:** confirm `conversation_finished` lands in the outfile after every turn incl. tool-heavy ones. - `LOG_PROMPTS=false` strips prompt/response/function_args text from events. bundle. ### S3 reply / interrupt / dialogs - Reply: bracketed paste + Enter. Enter while the model is busy **queues natively** and auto-flushes at idle; Tab is a dedicated queue key while generating. bundle + shipped docs. - Interrupt recipe: **single Esc** (cancels the in-flight request; when idle it only arms a self-clearing escape prompt). **Double-Esc BANNED**: within 500 ms it clears the input or submits `/rewind` (writes `$rewindTo` — epoch hazard). **Ctrl-C BANNED**: first press cancels + warns, second press within the window runs `/quit` regardless of input-buffer state (kills the session — stricter hazard than codex). bundle + docs; Ctrl-C-at-login-prompt no-exit also observed live. - Dialog inventory under `--approval-mode yolo --skip-trust` + seeded auth + lab settings: trust dialog suppressed (`--skip-trust` ⇒ `GEMINI_CLI_TRUST_WORKSPACE=true` env, per-invocation, never persisted); tool/shell approvals suppressed (yolo policy); theme picker only on an *invalid* `ui.theme` (absent key does NOT open it); auth dialog only when `security.auth.selectedType` is absent (creds presence is irrelevant — the merge sets it); update nag default-off. **Reachable residuals:** `ProQuotaDialog` on daily quota exhaustion (see S6) and the auth dialog after a failed token refresh. `AnswerDialog` stays `ErrDialogNotAnswerable` in v1 (codex precedent). bundle; auth-choice dialog itself captured live pre-auth. ### S4 context-file discovery - Key shape in 0.42.0: nested `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 name `GEMINI.md`. - List semantics: **all matching names are loaded and concatenated** per directory (wrapped in `--- 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. - Scope: global `~/.gemini/<configured names>` (the configured list applies there too — no hardcoded GEMINI.md fallback); upward walk stops one directory ABOVE the `.git` project root (quirk recorded — it reads `dirname(projectRoot)` too); downward BFS capped by `context.discoveryMaxDirs` (default 200). Workspace `.gemini/settings.json` overrides user settings **when trusted** (and `--skip-trust` makes it trusted) ⇒ a repo-committed settings file could override `context.fileName` — recorded as a hazard, accepted for v1. Unknown/invalid settings keys are warn-only, never fatal ⇒ the user-level merge is safe. bundle. - Merge target (decision 7 refined): marker-guarded idempotent merge into **user** `~/.gemini/settings.json` setting `context.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 touch `general.sessionRetention` (cleanup stays opt-in-disabled ⇒ durable transcripts, decision 11). **residual-live:** `/memory show` probe under a real turn. ### S5 auth - No-browser triggers: `NO_BROWSER=<truthy>` env; also auto-suppressed when linux has no `DISPLAY`/`WAYLAND_DISPLAY`/`MIR_SOCKET`, or `CI=true`, or `DEBIAN_FRONTEND=noninteractive`, or ssh on non-linux. (Lab sandbox has no DISPLAY ⇒ suppressed regardless; the adapter still sets `NO_BROWSER=true` for determinism.) bundle. - Flow captured **live**: with `selectedType` set and no creds, a pre-TUI plain-stdio flow prints `Please visit the following URL to authorize the application:` + the Google URL (PKCE S256, `state`, redirect `https://codeassist.google.com/authcode` — the code is shown to the user on a Google page, no localhost listener), then prompts `Enter 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, then `FatalAuthenticationError`. **No success banner** in this path ⇒ completion detection = `oauth_creds.json` birth (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. - `LoginStart` design: ensure the user-settings merge (so `selectedType` forces the pre-TUI path), spawn bare `gemini` with `NO_BROWSER=true` in the `lab-login-gemini` session, scrape the URL; `LoginSubmitCode` pastes the code (gemini, unlike codex, implements it — flow kind `oauth-code`); completion poller = creds-file watch. - Creds artifacts: `~/.gemini/oauth_creds.json` (google-auth-library Credentials: `access_token`, `refresh_token`, `scope`, `token_type`, `expiry_date` ms-epoch, optional `id_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_token` inspection + email from `google_accounts.json` (0.42.0 has **no** auth/login/logout CLI subcommand — confirmed, top-level commands are only `mcp|extensions|skills|hooks|gemma`). `Logout` = remove `oauth_creds.json` + clear the active account (the `/auth signout` slash 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 - Defaults: no `-m` ⇒ `auto-gemini-3` ("Auto (Gemini 3)"), resolved at request time to `gemini-3-pro-preview`/`gemini-3-flash-preview`, auto-downgrading to `auto-gemini-2.5` when the account lacks preview access. `/model` list 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. - Lab catalog pin (bundle-provenance, to be refined by the live `/model`+one-turn sweep when creds land): Default = omit `-m` (labelled "Auto"); explicit `gemini-3-pro-preview`, `gemini-3-flash-preview`, `gemini-2.5-pro`, `gemini-2.5-flash`. `Efforts()` = single `default` sentinel → no flag (gemini has no effort knob; conformance needs a non-empty catalog). - Quota exhaustion (recorded, not worked around): transient 429/503 under auto-selection ⇒ **silent** fallback (`activateFallbackMode`) — observable as the `gemini_cli.flash_fallback` telemetry event + the `model` field on subsequent transcript records; hard daily exhaustion (`TerminalQuotaError`, the free-tier ceiling) ⇒ blocking `ProQuotaDialog`: "Usage limit reached for <model>. Access resets at <time>. …" — under yolo this is the one dialog a healthy run can still hit; state stays honest (transcript+telemetry), v1 does not auto-answer it. bundle. **residual-live:** the real 429 shape + which candidates the free tier actually serves. ### S7 attribution ground truth - Bundle sweep: **zero** attribution machinery — no `Co-authored-by`/`Generated with` strings anywhere, no `git commit` templating of any kind (the CLI has no built-in commit tool; commits only happen if the model runs git in the shell tool). bundle. ⇒ `ScrubPatterns` ship 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 - Spawn argv: `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 (`isHeadlessMode` stage-1/2 logic; tmux = TTY; lab must never set `CI` in spawn env) — 0.42.0 prints a startup notice about this default. cli + bundle. `--approval-mode` choices are `default|auto_edit|yolo|plan`. - Per-run telemetry env plumbing: the LiveSignals seam was extended so `Setup` can contribute spawn env (claude returns nil) — landed as commit `20f1c53` on `afk/126` (coordinates with #124's SetupOpts; settings-file arming unchanged). - Retention: `general.sessionRetention` absent ⇒ `cleanupExpiredSessions` returns disabled — opt-in confirmed at the schema level (`default: void 0`); the lab merge never sets it. bundle. - Skills: native — `gemini skills list|enable|disable|install|link|uninstall` (cli); discovery at workspace `.gemini/skills` (trusted ⇒ `--skip-trust` suffices) + user `~/.gemini/skills` (+ `.agents/skills` variants); `skills.enabled` defaults true. bundle. ⇒ `NativeSkillDiscovery: true`, `SkillsDir: ".gemini/skills"`. ### Creds-gated residual list (second amendment when `~/.gemini/oauth_creds.json` is seeded) 1. Live turn-fold fixtures (real `gemini` records incl. toolCalls/thoughts) and `conversation_finished`-in-outfile confirmation (S1/S2). 2. `/memory show` context-discovery probe (S4). 3. Completed login round-trip + refresh behavior (S5). 4. `/model` inventory + one turn per candidate + real quota-exhaustion shape (S6). 5. Incogni attribution ground truth from a real run (S7).
Author
Owner

This was generated by AI while landing a PR.

/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 touches agentPackages). After the conflict resolution below I re-ran the project's own gates locally on the resolved tree: go build ./... , full go test ./... , golangci-lint 2.12.2 0 issues . PR touches no web/ files, so the vitest surface is untouched.

Merge conflict — resolved and pushed to afk/126

The branch was behind main (#161/#158/#155/#153 landed since). Two resolutions, both pushed:

  1. cmd/lab/main.go — import union (8609bfe). main added internal/pull, the PR added internal/provider/gemini, adjacent lines. Deterministic, behaviour-preserving.
  2. internal/provider/gemini — semantic break git merged textually (45e3e48). #158 changed AgentProvider.Models() to return []provider.ModelOption (per-model efforts + reported default). The gemini adapter still implemented the pre-#156 []provider.Option signature, so the merged tree did not compile. Ported to claude-code's shape: gemini has no reasoning-effort knob at all (single default sentinel, no flag emitted in SpawnArgv), so every model carries the same shared efforts list and no DefaultEffort. Values, order and the spawn default unchanged; the compat §10 pin restated in the enriched shape.

🔴 Blocker — deterministic --session-id collides on every re-run of an issue

internal/provider/gemini/gemini.go:129 derives the session id as UUIDv5(ns, tmuxSessionName), and both SpawnArgv and LocateTranscript recompute 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: 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.
  • Manual: gitx.UniqueManualLabel de-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_ERROR when 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 leaves sessionRetention alone).

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_eagerBirthAndSessionIDCollision pins 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 BatchLogRecordProcessor unconfigured, so OTEL's default 5 s scheduledDelayMillis applies. The transcript's user record is written synchronously at submit, but gemini_cli.user_prompt only reaches the outfile at the next flush. In that window livenessState (livesignals.go:169) still sees the previous conversation_finished and reports StateNeedsInput while the agent is working — and internal/chat/notify.go:35 debounces for only 2 s, so a seeded run can fire a spurious "needs you" push seconds after spawn. Cheap fix: set OTEL_BLRP_SCHEDULE_DELAY / OTEL_BSP_SCHEDULE_DELAY in Setup'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 EnsureUserSettings implements a semantic-guard merge (re-enforces context.fileName to lab's list on every run). Defensible for JSON and documented in ADR-0041, but it silently overwrites an operator's custom context.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.

> *This was generated by AI while landing a PR.* ## /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 touches `agentPackages`). After the conflict resolution below I re-ran the project's own gates locally on the resolved tree: `go build ./...` ✅, full `go test ./...` ✅, `golangci-lint 2.12.2` 0 issues ✅. PR touches no `web/` files, so the vitest surface is untouched. ### Merge conflict — resolved and pushed to `afk/126` The branch was behind `main` (#161/#158/#155/#153 landed since). Two resolutions, both pushed: 1. **`cmd/lab/main.go` — import union** (`8609bfe`). `main` added `internal/pull`, the PR added `internal/provider/gemini`, adjacent lines. Deterministic, behaviour-preserving. 2. **`internal/provider/gemini` — semantic break git merged textually** (`45e3e48`). #158 changed `AgentProvider.Models()` to return `[]provider.ModelOption` (per-model efforts + reported default). The gemini adapter still implemented the pre-#156 `[]provider.Option` signature, so the merged tree **did not compile**. Ported to claude-code's shape: gemini has no reasoning-effort knob at all (single `default` sentinel, no flag emitted in `SpawnArgv`), so every model carries the same shared efforts list and no `DefaultEffort`. Values, order and the spawn default unchanged; the compat §10 pin restated in the enriched shape. ### 🔴 Blocker — deterministic `--session-id` collides on every re-run of an issue `internal/provider/gemini/gemini.go:129` derives the session id as `UUIDv5(ns, tmuxSessionName)`, and both `SpawnArgv` and `LocateTranscript` recompute 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:** `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. - **Manual:** `gitx.UniqueManualLabel` de-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_ERROR` when 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 leaves `sessionRetention` alone). **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_eagerBirthAndSessionIDCollision` pins 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 `BatchLogRecordProcessor` unconfigured, so OTEL's default 5 s `scheduledDelayMillis` applies. The transcript's `user` record is written synchronously at submit, but `gemini_cli.user_prompt` only reaches the outfile at the next flush. In that window `livenessState` (`livesignals.go:169`) still sees the previous `conversation_finished` and reports `StateNeedsInput` **while the agent is working** — and `internal/chat/notify.go:35` debounces for only 2 s, so a seeded run can fire a spurious "needs you" push seconds after spawn. Cheap fix: set `OTEL_BLRP_SCHEDULE_DELAY` / `OTEL_BSP_SCHEDULE_DELAY` in `Setup`'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 EnsureUserSettings` implements a **semantic**-guard merge (re-enforces `context.fileName` to lab's list on every run). Defensible for JSON and documented in ADR-0041, but it silently overwrites an operator's custom `context.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.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab#126
No description provided.