From ccac5e87ee37271e9492dff263754478fb37bb06 Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Wed, 3 Jun 2026 21:10:42 -0700 Subject: [PATCH 1/7] chore(porch): bugfix-985 init bugfix --- .../status.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 codev/projects/bugfix-985-consult-m-claude-bills-metered/status.yaml diff --git a/codev/projects/bugfix-985-consult-m-claude-bills-metered/status.yaml b/codev/projects/bugfix-985-consult-m-claude-bills-metered/status.yaml new file mode 100644 index 00000000..2c920403 --- /dev/null +++ b/codev/projects/bugfix-985-consult-m-claude-bills-metered/status.yaml @@ -0,0 +1,14 @@ +id: bugfix-985 +title: consult-m-claude-bills-metered +protocol: bugfix +phase: investigate +plan_phases: [] +current_plan_phase: null +gates: + pr: + status: pending +iteration: 1 +build_complete: false +history: [] +started_at: '2026-06-04T04:10:42.581Z' +updated_at: '2026-06-04T04:10:42.582Z' From 8298e2a3d24e596b12b53e0cbeb3cd4a41c32370 Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Wed, 3 Jun 2026 21:14:35 -0700 Subject: [PATCH 2/7] chore(porch): bugfix-985 fix phase-transition --- .../bugfix-985-consult-m-claude-bills-metered/status.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codev/projects/bugfix-985-consult-m-claude-bills-metered/status.yaml b/codev/projects/bugfix-985-consult-m-claude-bills-metered/status.yaml index 2c920403..f84cbf34 100644 --- a/codev/projects/bugfix-985-consult-m-claude-bills-metered/status.yaml +++ b/codev/projects/bugfix-985-consult-m-claude-bills-metered/status.yaml @@ -1,7 +1,7 @@ id: bugfix-985 title: consult-m-claude-bills-metered protocol: bugfix -phase: investigate +phase: fix plan_phases: [] current_plan_phase: null gates: @@ -11,4 +11,4 @@ iteration: 1 build_complete: false history: [] started_at: '2026-06-04T04:10:42.581Z' -updated_at: '2026-06-04T04:10:42.582Z' +updated_at: '2026-06-04T04:14:34.836Z' From 287f44d66392dfc13f3893db9094694bc3ea3fb4 Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Wed, 3 Jun 2026 21:17:10 -0700 Subject: [PATCH 3/7] [Bugfix #985] Fix: consult -m claude bills metered API instead of subscription Strip ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN from the Claude Agent SDK subprocess env when CLAUDE_CODE_OAUTH_TOKEN is present, so consult authenticates against the Claude subscription rather than the metered Opus API. The SDK prioritizes ANTHROPIC_API_KEY over the OAuth token, which silently routed CMAP/review traffic to the metered API. - Extract env-building into pure exported helper buildClaudeConsultEnv() - Deletion scoped to the local subprocess env copy, never process.env, so other callers (persona, dev:local) are unaffected - Preserve the API key when no OAuth token is set (CI / key-only envs) - Regression test covering both auth paths + no-mutation invariant - Doc note in consult.md incl. 2026-06-15 subscription-credit caveat --- codev/resources/commands/consult.md | 13 ++++ codev/state/bugfix-985_thread.md | 38 +++++++++++ .../consult/__tests__/claude-auth-env.test.ts | 65 +++++++++++++++++++ packages/codev/src/commands/consult/index.ts | 41 ++++++++++-- 4 files changed, 151 insertions(+), 6 deletions(-) create mode 100644 codev/state/bugfix-985_thread.md create mode 100644 packages/codev/src/commands/consult/__tests__/claude-auth-env.test.ts diff --git a/codev/resources/commands/consult.md b/codev/resources/commands/consult.md index 4ed8a800..76da6fb9 100644 --- a/codev/resources/commands/consult.md +++ b/codev/resources/commands/consult.md @@ -154,6 +154,19 @@ Configure API keys: - Codex: `OPENAI_API_KEY` - Gemini: `GOOGLE_API_KEY` or `GEMINI_API_KEY` +### Claude auth: subscription vs. metered API + +`consult -m claude` runs on the Claude Agent SDK. When `CLAUDE_CODE_OAUTH_TOKEN` +(a Claude subscription/OAuth token) is present, consult strips `ANTHROPIC_API_KEY` +and `ANTHROPIC_AUTH_TOKEN` from the SDK subprocess env so the consultation +authenticates against the **subscription** rather than the **metered Opus API**. +The Agent SDK otherwise prioritizes `ANTHROPIC_API_KEY`, which silently routes +CMAP/review traffic to the metered API (issue #985). When no OAuth token is set, +the API key is used as before so CI / key-only environments keep working. + +> **Caveat:** dedicated Agent-SDK subscription credit starts **2026-06-15**. +> Before that date, subscription auth draws from the interactive Max quota. + ## The Consultant Role The consultant role (`codev/roles/consultant.md`) defines behavior: diff --git a/codev/state/bugfix-985_thread.md b/codev/state/bugfix-985_thread.md new file mode 100644 index 00000000..046443cd --- /dev/null +++ b/codev/state/bugfix-985_thread.md @@ -0,0 +1,38 @@ +# bugfix-985 thread + +## Issue +`consult -m claude` bills the metered Opus API instead of the Claude subscription because it +forwards `ANTHROPIC_API_KEY`/`ANTHROPIC_AUTH_TOKEN` into the Agent SDK env, which shadows +`CLAUDE_CODE_OAUTH_TOKEN` in the SDK's auth priority. Reported by shannon:main (~$150/day). + +## Investigate (done) +Root cause confirmed in source on this branch: +- `packages/codev/src/commands/consult/index.ts`, `runClaudeConsultation()` lines 504-509 + copy ALL of `process.env` into the `env` object passed to `claudeQuery`. That includes + `ANTHROPIC_API_KEY`/`ANTHROPIC_AUTH_TOKEN` when present, which the Agent SDK prioritizes + over `CLAUDE_CODE_OAUTH_TOKEN`. +- No existing test exercises this env construction. Function is not exported. +- Established testability pattern in this file: internal helpers re-exported with `_` prefix + (see export block ~line 1627). Test dir: `src/commands/consult/__tests__/` (vitest). +- Adjacent (noted, not in scope): line 519 hardcodes `model: 'claude-opus-4-6'`. + +## Fix plan +- Extract env-building into a pure exported helper `buildClaudeConsultEnv(processEnv)` so it's + unit-testable. When `CLAUDE_CODE_OAUTH_TOKEN` is set, delete `ANTHROPIC_API_KEY` and + `ANTHROPIC_AUTH_TOKEN` from the LOCAL env object (not global process.env). When OAuth token + is absent, preserve the API key (CI/key-only envs keep working). +- Regression test in `__tests__/claude-auth-env.test.ts`. +- Doc note in consult docs re: subscription-credit caveat (2026-06-15). + +## Fix (done) +- `consult/index.ts`: extracted env-building into exported pure helper + `buildClaudeConsultEnv(processEnv)`. Strips `ANTHROPIC_API_KEY`/`ANTHROPIC_AUTH_TOKEN` + from the local copy only when `CLAUDE_CODE_OAUTH_TOKEN` is set; preserves the key + otherwise. `runClaudeConsultation` now calls the helper. +- Regression test: `__tests__/claude-auth-env.test.ts` (4 cases: strip-on-oauth, + preserve-without-oauth, no-mutation-of-source, drops-undefined). All pass. +- Doc note added to `codev/resources/commands/consult.md`. +- Verified: consult suite 79/79 pass; `tsc --noEmit` clean (after building codev-core + in the fresh worktree). Net diff well under 300 LOC. + +BUGFIX protocol phases: Investigate → Fix → Create PR. diff --git a/packages/codev/src/commands/consult/__tests__/claude-auth-env.test.ts b/packages/codev/src/commands/consult/__tests__/claude-auth-env.test.ts new file mode 100644 index 00000000..d697638f --- /dev/null +++ b/packages/codev/src/commands/consult/__tests__/claude-auth-env.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect } from 'vitest'; +import { buildClaudeConsultEnv } from '../index.js'; + +/** + * Regression test for issue #985. + * + * `consult -m claude` must authenticate the Agent SDK with the Claude + * subscription (`CLAUDE_CODE_OAUTH_TOKEN`) rather than the metered Opus API. + * The SDK prioritizes `ANTHROPIC_API_KEY` over `CLAUDE_CODE_OAUTH_TOKEN`, so + * when an OAuth token is present the API/auth tokens must be stripped from the + * subprocess env. When no OAuth token is set, the API key must be preserved so + * CI / key-only environments still authenticate. + */ +describe('buildClaudeConsultEnv (issue #985)', () => { + it('strips ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN when OAuth token is set', () => { + const env = buildClaudeConsultEnv({ + CLAUDE_CODE_OAUTH_TOKEN: 'oauth-sub-token', + ANTHROPIC_API_KEY: 'sk-metered-key', + ANTHROPIC_AUTH_TOKEN: 'auth-token', + PATH: '/usr/bin', + }); + + expect(env.ANTHROPIC_API_KEY).toBeUndefined(); + expect(env.ANTHROPIC_AUTH_TOKEN).toBeUndefined(); + // OAuth token and unrelated vars are preserved. + expect(env.CLAUDE_CODE_OAUTH_TOKEN).toBe('oauth-sub-token'); + expect(env.PATH).toBe('/usr/bin'); + }); + + it('preserves ANTHROPIC_API_KEY when no OAuth token is set (CI / key-only)', () => { + const env = buildClaudeConsultEnv({ + ANTHROPIC_API_KEY: 'sk-metered-key', + ANTHROPIC_AUTH_TOKEN: 'auth-token', + PATH: '/usr/bin', + }); + + expect(env.ANTHROPIC_API_KEY).toBe('sk-metered-key'); + expect(env.ANTHROPIC_AUTH_TOKEN).toBe('auth-token'); + expect(env.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined(); + }); + + it('does not mutate the source process.env object', () => { + const source: NodeJS.ProcessEnv = { + CLAUDE_CODE_OAUTH_TOKEN: 'oauth-sub-token', + ANTHROPIC_API_KEY: 'sk-metered-key', + ANTHROPIC_AUTH_TOKEN: 'auth-token', + }; + + buildClaudeConsultEnv(source); + + // The deletion must be scoped to the returned copy, never the global env. + expect(source.ANTHROPIC_API_KEY).toBe('sk-metered-key'); + expect(source.ANTHROPIC_AUTH_TOKEN).toBe('auth-token'); + }); + + it('drops undefined values while copying', () => { + const env = buildClaudeConsultEnv({ + DEFINED: 'yes', + UNDEFINED: undefined, + }); + + expect(env.DEFINED).toBe('yes'); + expect('UNDEFINED' in env).toBe(false); + }); +}); diff --git a/packages/codev/src/commands/consult/index.ts b/packages/codev/src/commands/consult/index.ts index 8e0c5208..c481b184 100644 --- a/packages/codev/src/commands/consult/index.ts +++ b/packages/codev/src/commands/consult/index.ts @@ -477,6 +477,40 @@ export async function runCodexConsultation( } } +/** + * Build the env passed to the Claude Agent SDK subprocess for a consultation. + * + * Copies the given environment, but when a Claude subscription/OAuth token + * (`CLAUDE_CODE_OAUTH_TOKEN`) is present, strips `ANTHROPIC_API_KEY` and + * `ANTHROPIC_AUTH_TOKEN` from the *copy* (never the global `process.env`). + * The Agent SDK prioritizes the API key over the OAuth token, so leaving the + * key in would silently route CMAP/review traffic to the metered Opus API + * instead of the Claude subscription (issue #985). + * + * When no OAuth token is set, the API key is preserved so CI / key-only + * environments continue to authenticate. + * + * The deletion is scoped to this subprocess env only — other callers that need + * the API key (persona, dev:local) are unaffected. + */ +export function buildClaudeConsultEnv( + processEnv: NodeJS.ProcessEnv, +): Record { + const env: Record = {}; + for (const [key, value] of Object.entries(processEnv)) { + if (value !== undefined) { + env[key] = value; + } + } + + if (env.CLAUDE_CODE_OAUTH_TOKEN) { + delete env.ANTHROPIC_API_KEY; + delete env.ANTHROPIC_AUTH_TOKEN; + } + + return env; +} + /** * Run Claude consultation via Agent SDK. * Uses the SDK's query() function instead of CLI subprocess. @@ -501,12 +535,7 @@ async function runClaudeConsultation( const savedClaudeCode = process.env.CLAUDECODE; delete process.env.CLAUDECODE; - const env: Record = {}; - for (const [key, value] of Object.entries(process.env)) { - if (value !== undefined) { - env[key] = value; - } - } + const env = buildClaudeConsultEnv(process.env); try { const session = claudeQuery({ From 404ca14dab28e898f1275da4fce5d98915a3d22c Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Wed, 3 Jun 2026 21:17:40 -0700 Subject: [PATCH 4/7] chore(porch): bugfix-985 pr phase-transition --- .../bugfix-985-consult-m-claude-bills-metered/status.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codev/projects/bugfix-985-consult-m-claude-bills-metered/status.yaml b/codev/projects/bugfix-985-consult-m-claude-bills-metered/status.yaml index f84cbf34..b36ca8d0 100644 --- a/codev/projects/bugfix-985-consult-m-claude-bills-metered/status.yaml +++ b/codev/projects/bugfix-985-consult-m-claude-bills-metered/status.yaml @@ -1,7 +1,7 @@ id: bugfix-985 title: consult-m-claude-bills-metered protocol: bugfix -phase: fix +phase: pr plan_phases: [] current_plan_phase: null gates: @@ -11,4 +11,4 @@ iteration: 1 build_complete: false history: [] started_at: '2026-06-04T04:10:42.581Z' -updated_at: '2026-06-04T04:14:34.836Z' +updated_at: '2026-06-04T04:17:40.304Z' From fc9d2af38f67e03c809714e937406964beaf1f2c Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Wed, 3 Jun 2026 21:22:42 -0700 Subject: [PATCH 5/7] [Bugfix #985] docs: record CMAP results; scrub external-adopter name from thread --- codev/state/bugfix-985_thread.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/codev/state/bugfix-985_thread.md b/codev/state/bugfix-985_thread.md index 046443cd..5ecea938 100644 --- a/codev/state/bugfix-985_thread.md +++ b/codev/state/bugfix-985_thread.md @@ -3,7 +3,8 @@ ## Issue `consult -m claude` bills the metered Opus API instead of the Claude subscription because it forwards `ANTHROPIC_API_KEY`/`ANTHROPIC_AUTH_TOKEN` into the Agent SDK env, which shadows -`CLAUDE_CODE_OAUTH_TOKEN` in the SDK's auth priority. Reported by shannon:main (~$150/day). +`CLAUDE_CODE_OAUTH_TOKEN` in the SDK's auth priority. Reported by an external adopter +(~$150/day on a heavy dev day). ## Investigate (done) Root cause confirmed in source on this branch: @@ -36,3 +37,16 @@ Root cause confirmed in source on this branch: in the fresh worktree). Net diff well under 300 LOC. BUGFIX protocol phases: Investigate → Fix → Create PR. + +## Create PR (done) +- PR #986 opened against main. "Fixes #985". +- CMAP (3-way) on the PR — all APPROVE, HIGH confidence, zero blocking issues: + - Codex: APPROVE/HIGH — correct, tightly scoped, solid regression coverage. + - Gemini: APPROVE/HIGH — correct root-cause fix, thorough branch coverage. + - Claude: APPROVE/HIGH — clean minimal fix; flagged (non-blocking) external-adopter + scrubbing convention + the `_`-prefix export style note (kept direct export — it's a + legitimate public utility, not a test-only helper). +- Applied scrub: removed external-adopter workspace name from PR body + this thread + (kept the ~$150/day signal). Issue body still names the adopter — upstream of the + builder; flagged to architect. +- Awaiting architect approval to merge (BUGFIX pr gate). From 710d02929d19f98d4573fe9371c5b54686638427 Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Wed, 3 Jun 2026 21:22:57 -0700 Subject: [PATCH 6/7] chore(porch): bugfix-985 pr gate-requested --- .../bugfix-985-consult-m-claude-bills-metered/status.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/codev/projects/bugfix-985-consult-m-claude-bills-metered/status.yaml b/codev/projects/bugfix-985-consult-m-claude-bills-metered/status.yaml index b36ca8d0..ee734e40 100644 --- a/codev/projects/bugfix-985-consult-m-claude-bills-metered/status.yaml +++ b/codev/projects/bugfix-985-consult-m-claude-bills-metered/status.yaml @@ -7,8 +7,10 @@ current_plan_phase: null gates: pr: status: pending + requested_at: '2026-06-04T04:22:57.960Z' iteration: 1 build_complete: false history: [] started_at: '2026-06-04T04:10:42.581Z' -updated_at: '2026-06-04T04:17:40.304Z' +updated_at: '2026-06-04T04:22:57.961Z' +pr_ready_for_human: true From 5cb8de8249277c4e316ae41c0e53e8022f7d0ff0 Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Wed, 3 Jun 2026 22:13:18 -0700 Subject: [PATCH 7/7] chore(porch): bugfix-985 pr gate-approved --- .../bugfix-985-consult-m-claude-bills-metered/status.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/codev/projects/bugfix-985-consult-m-claude-bills-metered/status.yaml b/codev/projects/bugfix-985-consult-m-claude-bills-metered/status.yaml index ee734e40..fe90bfe5 100644 --- a/codev/projects/bugfix-985-consult-m-claude-bills-metered/status.yaml +++ b/codev/projects/bugfix-985-consult-m-claude-bills-metered/status.yaml @@ -6,11 +6,12 @@ plan_phases: [] current_plan_phase: null gates: pr: - status: pending + status: approved requested_at: '2026-06-04T04:22:57.960Z' + approved_at: '2026-06-04T05:13:18.384Z' iteration: 1 build_complete: false history: [] started_at: '2026-06-04T04:10:42.581Z' -updated_at: '2026-06-04T04:22:57.961Z' -pr_ready_for_human: true +updated_at: '2026-06-04T05:13:18.385Z' +pr_ready_for_human: false