diff --git a/.ai-run/guides/integration/external-integrations.md b/.ai-run/guides/integration/external-integrations.md index 03c9319b8..9c9571a2f 100644 --- a/.ai-run/guides/integration/external-integrations.md +++ b/.ai-run/guides/integration/external-integrations.md @@ -15,6 +15,7 @@ | OpenCode | Open-source AI assistant | SSO/API Key | Via CodeMie proxy | | MCP Servers | Remote MCP tool servers | OAuth 2.0 (auto) | `codemie-mcp-proxy` | | Enterprise SSO | Corporate auth | SAML/OAuth | `SSO_BASE_URL` | +| Anthropic Subscription | Claude via user's Anthropic account | None (subscription) | `anthropic-subscription` provider | --- @@ -277,6 +278,43 @@ Claude Code injects `!bash` commands as synthetic `type:'user'` messages. The pr --- +## Anthropic Subscription Provider + +The `anthropic-subscription` provider runs Claude Code against the user's own Anthropic subscription rather than a CodeMie-managed API key. Its model-selection pipeline differs from all other providers. + +### Model Is Not Stored in the Profile + +Setup omits the `model` field from the profile entirely when none is selected; an empty string is never persisted. The model is chosen per-session inside Claude Code (via `/model`). + +| Concern | Behavior | +|---|---| +| Profile `model` field | Absent — conditionally omitted at setup, never stored as empty string | +| Launch banner model line | `CODEMIE_CLI_MODEL` value if set; else `'chosen per session by Claude Code / your Anthropic subscription'` | +| `models list` command | Prints an informational message; exits 0 — no CodeMie model catalog exists for this provider | + +`file:src/providers/plugins/anthropic-subscription/anthropic-subscription.setup-steps.ts:129` +`file:src/agents/core/launch-model-display.ts:13` +`file:src/cli/commands/models.ts:82` + +### CODEMIE_CLI_MODEL Side-Channel + +An explicit `-m/--model` flag writes `CODEMIE_CLI_MODEL` via `applyCliModelEnv()`, clearing any pre-existing value first (stale-shell guard). The `enrichArgs` hook for the `claude` agent reads this var and prepends `--model ` to the claude binary argv; a dedup guard prevents double injection when `--model` or `--model=` is already present. Without an explicit `-m`, the var is unset and Claude Code picks the model independently. + +| Step | File | Line | +|---|---|---| +| Set by AgentCLI | `src/agents/core/AgentCLI.ts` | 210 | +| Implementation | `src/agents/core/cli-model-env.ts` | 1 | +| Consumed in enrichArgs | `src/providers/plugins/anthropic-subscription/anthropic-subscription.template.ts` | 103 | + +This is the only provider that carries the CLI model via a side-channel env var. All other providers use the `CODEMIE_MODEL → ANTHROPIC_MODEL` pipeline or equivalent. + +### Version-Prompt Policy + +When installed Claude Code is newer than the CodeMie-verified pin, the upgrade/downgrade prompt defaults to `'continue'` for `anthropic-subscription` (keep the newer binary — model availability depends on it) and `'install'` for all other providers (downgrade to tested pin). The minimum-version hard block is unaffected. + +`file:src/agents/core/version-prompt-policy.ts:1` +`file:src/agents/core/BaseAgentAdapter.ts:472` + ## skills.sh Wrapper (`codemie skills`) Catalog-agnostic thin wrapper around the upstream `skills` npm CLI. Discovery, ranking, and source classification are out of scope for this CLI. diff --git a/docs/superpowers/tasks/2026-08-31-subscription-model-access/actual-complexity.json b/docs/superpowers/tasks/2026-08-31-subscription-model-access/actual-complexity.json new file mode 100644 index 000000000..910c3471b --- /dev/null +++ b/docs/superpowers/tasks/2026-08-31-subscription-model-access/actual-complexity.json @@ -0,0 +1,92 @@ +{ + "task": "Add Anthropic Subscription model access support (EPMCDME-14341): stale-env clearing, per-session model display in launch banner, subscription-aware version-prompt defaults, buildConfig model-omit fix, moonshot guard, and three pure-helper extractions across agent core, CLI commands, and provider plugin layers.", + "generated": "2026-08-31T00:00:00Z", + "dimensions": { + "component_scope": { + "score": 4, + "label": "L", + "affected": "AgentCLI, BaseAgentAdapter, cli-model-env helper, launch-model-display helper, version-prompt-policy helper, models CLI command, setup CLI command, setup-ui utilities, AnthropicSubscriptionTemplate, AnthropicSubscriptionSetupSteps", + "layers": "Agent-Tool, Workflow, CLI, Service, Provider-Plugin" + }, + "requirements_clarity": { + "score": 2, + "label": "S", + "status": "Clear", + "gaps": "Three code-review fix-ups (stale-env clear, buildConfig omit, moonshot guard) emerged during review rather than upfront; all were low-risk additive changes fully resolved before merge — no unresolved gaps remain" + }, + "technical_risk": { + "score": 4, + "label": "L", + "risk_factors": "Authentication flow touched in AnthropicSubscriptionSetupSteps (Claude browser login orchestration, CodeMie SSO auth); template.ts beforeRun removes ANTHROPIC_AUTH_TOKEN and ANTHROPIC_API_KEY and runs the Claude extension installer; moonshot guard deletes ANTHROPIC_DEFAULT_*_MODEL and CLAUDE_CODE_SUBAGENT_MODEL to prevent cross-provider contamination", + "mitigation": "All auth changes are additive conditional branches bounded to the anthropic-subscription provider code path; pure-helper extractions (cli-model-env.ts, launch-model-display.ts, version-prompt-policy.ts) are trivially reversible; existing AgentCLI and BaseAgentAdapter patterns reused throughout with no structural changes to those base classes" + }, + "file_change_estimate": { + "score": 6, + "label": "XXL", + "modified_files": 7, + "modified_file_list": [ + "src/agents/core/AgentCLI.ts", + "src/agents/core/BaseAgentAdapter.ts", + "src/cli/commands/models.ts", + "src/cli/commands/setup.ts", + "src/providers/integration/setup-ui.ts", + "src/providers/plugins/anthropic-subscription/anthropic-subscription.setup-steps.ts", + "src/providers/plugins/anthropic-subscription/anthropic-subscription.template.ts" + ], + "new_files": 11, + "new_file_list": [ + "src/agents/core/cli-model-env.ts", + "src/agents/core/launch-model-display.ts", + "src/agents/core/version-prompt-policy.ts", + "8 test files (names not provided in changed_files input; ~3 new for the new helpers + ~5 updated for modified sources)" + ], + "affected_dirs": [ + "src/agents/core", + "src/cli/commands", + "src/providers/integration", + "src/providers/plugins/anthropic-subscription" + ] + }, + "dependencies": { + "score": 1, + "label": "XS", + "new_packages": [], + "version_changes": [] + }, + "affected_layers": { + "score": 4, + "label": "L", + "layers_changed": ["CLI", "Service", "Agent-Tool", "Provider-Plugin"], + "schema_migration": false, + "cross_system": false + } + }, + "total": 21, + "size": "L", + "band_range": "21-26", + "files_changed": 18, + "routing": "brainstorming", + "key_reasoning": [ + { + "dimension": "file_change_estimate", + "reason": "diffstat N=18 maps to 16+ = XXL: 7 modified source files (AgentCLI.ts, BaseAgentAdapter.ts, models.ts, setup.ts, setup-ui.ts, anthropic-subscription.setup-steps.ts, anthropic-subscription.template.ts) + 3 new pure-helper modules (cli-model-env.ts, launch-model-display.ts, version-prompt-policy.ts) + 8 test files" + }, + { + "dimension": "component_scope", + "reason": "Bumped M (3) to L (4) by 'affects multiple agents' red flag: applyCliModelEnv() inserted into AgentCLI.ts runs on every agent launch regardless of provider; four distinct component clusters touched: agent core base classes (AgentCLI + BaseAgentAdapter), three new pure-helper modules, two CLI commands, and the full anthropic-subscription plugin (template + setup-steps)" + }, + { + "dimension": "technical_risk", + "reason": "Bumped M (3) to L (4) by 'affects authentication or authorization' red flag: AnthropicSubscriptionSetupSteps orchestrates Claude browser login and CodeMie SSO auth; template.ts beforeRun hook removes ANTHROPIC_AUTH_TOKEN and ANTHROPIC_API_KEY before the Claude process starts and invokes the extension installer; moonshot guard targets ANTHROPIC_DEFAULT_*_MODEL and CLAUDE_CODE_SUBAGENT_MODEL to block cross-provider contamination" + }, + { + "dimension": "affected_layers", + "reason": "Four distinct architectural layers: CLI (models.ts + setup.ts commands), Service (setup-ui.ts provider-integration utilities), Agent-Tool/Workflow (AgentCLI + BaseAgentAdapter base classes + three helpers), Provider-Plugin (anthropic-subscription template and setup-steps) — exceeds the M threshold of 2-3 layers; no persistence/migration layer involved" + } + ], + "red_flags_applied": [ + "Component Scope bumped from M (3) to L (4): applyCliModelEnv() call added to AgentCLI.ts handleRun() runs on every agent launch path — 'affects multiple workflows or agents' red flag.", + "Technical Risk bumped from M (3) to L (4): AnthropicSubscriptionSetupSteps orchestrates Claude browser login and CodeMie SSO authentication; template.ts beforeRun removes Anthropic auth env vars from the spawned process environment — 'affects authentication or authorization' red flag." + ], + "split_recommendation": null +} diff --git a/docs/superpowers/tasks/2026-08-31-subscription-model-access/code-review-check.json b/docs/superpowers/tasks/2026-08-31-subscription-model-access/code-review-check.json new file mode 100644 index 000000000..eabf3fd9f --- /dev/null +++ b/docs/superpowers/tasks/2026-08-31-subscription-model-access/code-review-check.json @@ -0,0 +1,24 @@ +{ + "schema": 1, + "gate": "code-review.check", + "decision": "approve", + "confidence": "high", + "risk_flags": [], + "rationale": "All three prior blocking findings resolved against the fix-up diff (6 files, +91/-6). CR-001: applyCliModelEnv deletes CODEMIE_CLI_MODEL before the conditional set, closing the stale-shell-value leak, with a test covering the shell-preset case. CR-002: buildConfig omits the model field for an empty model and a buildConfig('')->undefined test was added. CR-003: moonshot regression guard added and passing. No new high-risk issue introduced by the fix-up (env helper is a pure delete-then-set; buildConfig spread is clean; remainder is test-only), so no confirmation pass was required. Full unit suite 3969/3969, lint and typecheck clean.", + "business_review": [ + {"criterion": "subscription --model reaches claude; no --model -> default (CS1+CS2)", "status": "pass", "notes": "CR-001 fix removes the stale-value leak; no --model now reliably yields Claude Code default."}, + {"criterion": "setup not asked for a model; per-session summary (CS6)", "status": "pass", "notes": "CR-002 fix: buildConfig stores no model field."}, + {"criterion": "D4 tests included", "status": "pass", "notes": "Test-plan items 2 and 5 now implemented."} + ], + "standards_review": [ + {"standard": "git-workflow (Conventional Commits + ticket ref)", "status": "pass", "notes": "Fix-up: fix(agents), fix(providers), test(providers), all with Refs: EPMCDME-14341."}, + {"standard": "code-quality", "status": "pass"}, + {"standard": "security", "status": "pass"} + ], + "finding_status": [ + {"id": "CR-001", "status": "resolved", "notes": "applyCliModelEnv clears before set; tested including shell-preset case."}, + {"id": "CR-002", "status": "resolved", "notes": "buildConfig omits model field when empty; buildConfig('')->undefined tested."}, + {"id": "CR-003", "status": "resolved", "notes": "moonshot regression guard added and passing."} + ], + "findings": [] +} diff --git a/docs/superpowers/tasks/2026-08-31-subscription-model-access/code-review-final.json b/docs/superpowers/tasks/2026-08-31-subscription-model-access/code-review-final.json new file mode 100644 index 000000000..d2a768c5c --- /dev/null +++ b/docs/superpowers/tasks/2026-08-31-subscription-model-access/code-review-final.json @@ -0,0 +1,54 @@ +{ + "schema": 1, + "gate": "code-review.final", + "decision": "request-changes", + "confidence": "high", + "risk_flags": [], + "rationale": "Three lenses ran (blind, edge-case, acceptance); the edge-case lens (repo access) cleared all functional concerns except one confirmed by two lenses. One real correctness bug (CR-001: stale shell CODEMIE_CLI_MODEL leaks into a no---model subscription launch, violating the D3 contract and the 'no --model -> Claude Code default' AC) and two approved-plan test gaps (CR-003 moonshot regression guard; CR-002 buildConfig-empty test + field-omit cleanliness). Dismissed 3 non-issues: --model= dedup form and dual-provider-source (edge-case verified safe for real invocation paths), and an untyped-params false positive from an abbreviated blind-lens diff. Standards clean: 5 Conventional commits with Refs: EPMCDME-14341, code-quality and security pass.", + "business_review": [ + {"criterion": "subscription --model reaches claude; no --model -> default (CS1+CS2)", "status": "partial", "notes": "Set path correct; missing clear-before-set lets a shell-preset value leak (CR-001)."}, + {"criterion": "model stated to user at launch (CS4)", "status": "pass"}, + {"criterion": "setup not asked for a model; per-session summary (CS6)", "status": "partial", "notes": "Prompt skipped and summary wired; buildConfig still returns model:'' and test-plan item 5 unmet (CR-002)."}, + {"criterion": "models list explains source, no unsupported error (CS5)", "status": "pass"}, + {"criterion": "version: newer->continue default; older note; below-min unchanged; subscription-scoped (CS3/D2)", "status": "pass"}, + {"criterion": "D1 no entitlement logic; refusal relayed", "status": "pass"}, + {"criterion": "D4 tests included", "status": "partial", "notes": "Items 2 (moonshot guard) and 5 (buildConfig-empty) missing (CR-002, CR-003)."} + ], + "standards_review": [ + {"standard": "git-workflow (Conventional Commits + ticket ref)", "status": "pass", "notes": "5 feat(scope) commits, subjects <100, Refs: EPMCDME-14341 on all."}, + {"standard": "code-quality", "status": "pass", "notes": "ESM .js imports, explicit return types on new exports, no any, why-only comments."}, + {"standard": "security", "status": "pass", "notes": "No secrets/injection/unsafe process handling; CODEMIE_CLI_MODEL is a model-id string."} + ], + "findings": [ + { + "id": "CR-001", + "severity": "major", + "triage": "patch", + "file": "src/agents/core/AgentCLI.ts", + "line": 205, + "problem": "CODEMIE_CLI_MODEL is set only when options.model is truthy but never cleared first. A value already present in process.env (e.g. shell-exported) survives into anthropic-subscription enrichArgs on a launch with no --model.", + "impact": "Violates the D3 env-var contract ('only when the user passed -m/--model this launch') and the AC 'no --model -> Claude Code default': a stale value is silently injected as --model while the banner still shows the per-session phrase.", + "recommendation": "Add `delete process.env.CODEMIE_CLI_MODEL;` immediately before the conditional set so each launch starts clean." + }, + { + "id": "CR-002", + "severity": "major", + "triage": "patch", + "file": "src/providers/plugins/anthropic-subscription/anthropic-subscription.setup-steps.ts", + "line": 126, + "problem": "buildConfig still returns model: selectedModel, so the subscription profile persists model:'' rather than omitting the field; spec CS6 says 'stores no model' and test-plan item 5 (buildConfig stores no model) is unimplemented. Functionally inert at runtime (edge-case lens verified) but leaves an empty-string in persisted config.", + "impact": "Spec-fidelity gap and an empty-string-in-config value that can surface on a get->update round-trip; the new behavior is untested.", + "recommendation": "Omit the field for an empty model: `...(selectedModel ? { model: selectedModel } : {})`; add a test asserting buildConfig(creds, '').model is undefined." + }, + { + "id": "CR-003", + "severity": "major", + "triage": "patch", + "file": "src/providers/plugins/moonshot-subscription/__tests__/moonshot-subscription.template.test.ts", + "line": 1, + "problem": "Spec test-plan item 2 (moonshot-subscription enrichArgs unaffected by CODEMIE_CLI_MODEL) has no test. Scope was verified manually (0 moonshot files changed) but the regression guard the plan committed to is absent.", + "impact": "No automated guard that the new CODEMIE_CLI_MODEL mechanism stays anthropic-subscription-only; a future shared-base edit could regress moonshot silently.", + "recommendation": "Add a test setting CODEMIE_CLI_MODEL and asserting the moonshot template does not inject --model (unchanged args)." + } + ] +} diff --git a/docs/superpowers/tasks/2026-08-31-subscription-model-access/complexity-assessment.json b/docs/superpowers/tasks/2026-08-31-subscription-model-access/complexity-assessment.json new file mode 100644 index 000000000..3a267c964 --- /dev/null +++ b/docs/superpowers/tasks/2026-08-31-subscription-model-access/complexity-assessment.json @@ -0,0 +1,26 @@ +{ + "schema": 1, + "task": "Fix anthropic-subscription model passthrough, setup messaging, models-list message, and version-prompt default in the @codemieai/code CLI.", + "generated": "2026-08-31T00:00:00Z", + "dimensions": { + "component_scope": { "score": 4, "label": "L" }, + "requirements_clarity": { "score": 2, "label": "S" }, + "technical_risk": { "score": 3, "label": "M" }, + "file_change_estimate": { "score": 3, "label": "M" }, + "dependencies": { "score": 1, "label": "XS" }, + "affected_layers": { "score": 3, "label": "M" } + }, + "total": 16, + "size": "M", + "routing": "brainstorming", + "key_reasoning": [ + { + "dimension": "component_scope", + "reason": "Four change sites across three layers: Provider template (anthropic-subscription.template.ts — enrichArgs injection of --model and conditional exportEnvVars blank), Core shared adapter (BaseAgentAdapter.ts — version-prompt default flip from 'install' to 'continue'), and CLI Workflow (models.ts subscription message, setup.ts/setup-ui.ts setup messaging). BaseAgentAdapter is a core shared utility consumed by every agent plugin and provider; the single-line default flip has blast radius across all providers and agents. Red flag applied: touches core shared utility — bumped from M to L." + } + ], + "red_flags_applied": [ + "Component Scope bumped from M (3) to L (4): BaseAgentAdapter.ts is a core shared utility used by all agent plugins and providers; version-prompt default flip affects every provider's version-check code path." + ], + "split_recommendation": null +} diff --git a/docs/superpowers/tasks/2026-08-31-subscription-model-access/decisions.jsonl b/docs/superpowers/tasks/2026-08-31-subscription-model-access/decisions.jsonl new file mode 100644 index 000000000..60f22ef16 --- /dev/null +++ b/docs/superpowers/tasks/2026-08-31-subscription-model-access/decisions.jsonl @@ -0,0 +1,4 @@ +{"ts":"2026-08-31T00:00:00Z","gate_id":"spec.approved","mode":"hitl","verdict":{"decision":"approve","rationale":"User approved spec with 6 change sites, CODEMIE_CLI_MODEL passthrough, subscription-scoped version flip, tests included","follow_ups":[],"confidence":"high","source":"hitl"},"escalated":false,"prior_context":{"question":"spec.approved","phase":3,"risk_flags":[],"artifact_refs":["spec.md"]}} +{"ts":"2026-08-31T00:00:00Z","gate_id":"plan.approved","mode":"hitl","verdict":{"decision":"approve","rationale":"User approved 6-task TDD plan; every impl task test-first with exact anchors and pure-helper extractions","follow_ups":[],"confidence":"high","source":"hitl"},"escalated":false,"prior_context":{"question":"plan.approved","phase":4,"risk_flags":[],"artifact_refs":["plan.md"]}} +{"ts":"2026-08-31T00:00:00Z","gate_id":"code-review.final","mode":"hitl","verdict":{"decision":"request-changes","rationale":"User chose to fix all 3 findings then re-check","follow_ups":["CR-001","CR-002","CR-003"],"confidence":"high","source":"hitl"},"escalated":false,"prior_context":{"question":"code-review.final","phase":6,"risk_flags":[],"artifact_refs":["code-review.diff","code-review-final.json"]},"prior_orchestrator_verdict":{"decision":"request-changes","confidence":"high","findings":["CR-001","CR-002","CR-003"]}} +{"ts":"2026-08-31T00:00:00Z","gate_id":"code-review.check","mode":"hitl","verdict":{"decision":"approve","rationale":"All 3 findings resolved; no new issues; suite 3969/3969","follow_ups":[],"confidence":"high","source":"hitl"},"escalated":false,"prior_context":{"question":"code-review.check","phase":6,"risk_flags":[],"artifact_refs":["code-review-check.diff","code-review-check.json"]},"prior_orchestrator_verdict":{"decision":"approve","confidence":"high","finding_status":"CR-001 resolved, CR-002 resolved, CR-003 resolved"}} diff --git a/docs/superpowers/tasks/2026-08-31-subscription-model-access/events.jsonl b/docs/superpowers/tasks/2026-08-31-subscription-model-access/events.jsonl new file mode 100644 index 000000000..8c23136fb --- /dev/null +++ b/docs/superpowers/tasks/2026-08-31-subscription-model-access/events.jsonl @@ -0,0 +1,8 @@ +{"event":"lifecycle_emission","intent":"record_complexity_score","assessment_mode":"initial","status":"skipped","note":"configured adapter codemie-jira-assistant not resolvable in this host; complexity artifact retained locally"} +{"schema":1,"ts":"2026-08-31T00:00:00Z","event":"decision.recorded","phase":3,"actor":"decision-router","summary":"Decision recorded for spec.approved: approve","artifacts":["decisions.jsonl"],"data":{"gate_id":"spec.approved","mode":"hitl","decision":"approve","source":"hitl","escalated":false}} +{"event":"lifecycle_emission","intent":"artifact_published","artifact_kind":"spec","status":"skipped","note":"configured adapter codemie-jira-assistant not resolvable in this host; spec.md retained locally"} +{"schema":1,"ts":"2026-08-31T00:00:00Z","event":"decision.recorded","phase":4,"actor":"decision-router","summary":"Decision recorded for plan.approved: approve","artifacts":["decisions.jsonl"],"data":{"gate_id":"plan.approved","mode":"hitl","decision":"approve","source":"hitl","escalated":false}} +{"event":"lifecycle_emission","intent":"artifact_published","artifact_kind":"plan","status":"skipped","note":"configured adapter codemie-jira-assistant not resolvable in this host; plan.md retained locally"} +{"schema":1,"ts":"2026-08-31T00:00:00Z","event":"decision.recorded","phase":6,"actor":"decision-router","summary":"Decision recorded for code-review.final: request-changes","artifacts":["decisions.jsonl","code-review-final.json"],"data":{"gate_id":"code-review.final","mode":"hitl","decision":"request-changes","source":"hitl","escalated":false}} +{"schema":1,"ts":"2026-08-31T00:00:00Z","event":"decision.recorded","phase":6,"actor":"decision-router","summary":"Decision recorded for code-review.check: approve","artifacts":["decisions.jsonl","code-review-check.json"],"data":{"gate_id":"code-review.check","mode":"hitl","decision":"approve","source":"hitl","escalated":false}} +{"event":"lifecycle_emission","intent":"record_complexity_score","assessment_mode":"actual","status":"skipped","note":"configured adapter codemie-jira-assistant not resolvable in this host; actual-complexity.json retained locally (L 21/36, delta +5 from initial M 16/36)"} diff --git a/docs/superpowers/tasks/2026-08-31-subscription-model-access/plan.md b/docs/superpowers/tasks/2026-08-31-subscription-model-access/plan.md new file mode 100644 index 000000000..ab1e0e8b5 --- /dev/null +++ b/docs/superpowers/tasks/2026-08-31-subscription-model-access/plan.md @@ -0,0 +1,499 @@ +# Latest Claude Models on an Anthropic Subscription Profile — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** On an `anthropic-subscription` profile, pass the user's `--model` through to Claude Code and actually use it, stop asking for/storing a model in setup, make `codemie models list` explain (not error) where models come from, and stop the version prompt from defaulting to a downgrade. + +**Architecture:** The subscription profile keeps blanking `CODEMIE_MODEL` so the shared env pipeline is untouched. A new env var `CODEMIE_CLI_MODEL`, set only from the explicit CLI `--model`, carries the request to the subscription `enrichArgs`, which injects `--model` into the `claude` argv (dedup-guarded like `--plugin-dir`). Version-prompt and display decisions are extracted into small pure helpers so the subscription-scoped behavior is unit-testable. + +**Tech Stack:** TypeScript (ESM, Node ≥20), Vitest 3.x, Commander, inquirer, chalk. + +**Spec:** `docs/superpowers/tasks/2026-08-31-subscription-model-access/spec.md` + +## Global Constraints + +- Model choice on the subscription path is **relayed**, never validated: no entitlement logic; Claude Code owns any refusal (spec D1). +- Version-pin softening is **subscription-scoped**: only `CODEMIE_PROVIDER === 'anthropic-subscription'` changes; proxied providers keep today's `'install'` default; the minimum-version hard block is unchanged (spec D2). +- Never modify shared env plumbing: `transformEnvVars`, `ConfigLoader.exportProviderEnvVars`, `AgentCLI.collectPassThroughArgs`, or `configOnlyOptions` membership of `'model'`. +- `moonshot-subscription` must not change behavior. +- Provider constant: `ProviderName.ANTHROPIC_SUBSCRIPTION` (`'anthropic-subscription'`) from `src/providers/core/types.ts`. Use it, not a string literal, in new code. +- Imports use the `.js` extension; errors use project error classes; no `console.log` for debug (`logger.debug`). Keep comments to "why" only. + +--- + +### Task 1: `--model` pass-through for the subscription profile (CS1 + CS2) + +**Test-first:** yes — `enrichArgs` injects `['--model', ]` when the env var is set, injects nothing when unset, does not double-inject when `--model` is already present, and composes with `--plugin-dir`. + +**Files:** +- Modify: `src/agents/core/AgentCLI.ts` (in `handleRun`, after the `ConfigLoader.load(...)` call, ~line 203) +- Modify: `src/providers/plugins/anthropic-subscription/anthropic-subscription.template.ts:95-105` (`agentHooks.claude.enrichArgs`) +- Test: `src/providers/plugins/anthropic-subscription/__tests__/anthropic-subscription.template.test.ts` + +**Interfaces:** +- Produces: env var `CODEMIE_CLI_MODEL` (string) — set by `AgentCLI` only when `--model` was explicitly passed this launch; read by the subscription `enrichArgs` and by Task 3's banner helper. +- `enrichArgs(args: string[], _config: AgentConfig): string[]` — now also prepends `['--model', ]` when set and not already present. + +- [ ] **Step 1: Write the failing tests** (append a new `describe` block to the template test) + +```ts +describe('agentHooks - enrichArgs (claude) --model passthrough', () => { + const enrich = AnthropicSubscriptionTemplate.agentHooks.claude!.enrichArgs!; + const cfg = { agent: 'claude' } as any; + + beforeEach(() => { + delete process.env.CODEMIE_CLI_MODEL; + delete process.env.CODEMIE_CLAUDE_EXTENSION_DIR; + }); + afterEach(() => { + delete process.env.CODEMIE_CLI_MODEL; + delete process.env.CODEMIE_CLAUDE_EXTENSION_DIR; + }); + + it('injects --model when CODEMIE_CLI_MODEL is set', () => { + process.env.CODEMIE_CLI_MODEL = 'claude-opus-4-5'; + expect(enrich(['--task', 'hi'], cfg)).toEqual(['--model', 'claude-opus-4-5', '--task', 'hi']); + }); + + it('injects nothing when CODEMIE_CLI_MODEL is unset', () => { + expect(enrich(['--task', 'hi'], cfg)).toEqual(['--task', 'hi']); + }); + + it('does not double-inject when --model is already present', () => { + process.env.CODEMIE_CLI_MODEL = 'claude-opus-4-5'; + expect(enrich(['--model', 'claude-haiku-4-5'], cfg)).toEqual(['--model', 'claude-haiku-4-5']); + }); + + it('composes with the --plugin-dir injection', () => { + process.env.CODEMIE_CLI_MODEL = 'claude-opus-4-5'; + process.env.CODEMIE_CLAUDE_EXTENSION_DIR = '/ext'; + expect(enrich(['--task', 'hi'], cfg)).toEqual(['--plugin-dir', '/ext', '--model', 'claude-opus-4-5', '--task', 'hi']); + }); +}); +``` + +- [ ] **Step 2: Run the tests to verify they fail** + +Run: `npx vitest run src/providers/plugins/anthropic-subscription/__tests__/anthropic-subscription.template.test.ts -t "model passthrough"` +Expected: FAIL (current `enrichArgs` ignores `CODEMIE_CLI_MODEL`). + +- [ ] **Step 3: Implement `enrichArgs`** — replace the body at `anthropic-subscription.template.ts:96-104` + +```ts + enrichArgs(args: string[], _config: AgentConfig): string[] { + let result = args; + + // Carry the explicit CLI --model straight through to the claude binary. + // Sourced from CODEMIE_CLI_MODEL (set by AgentCLI only when the user passed + // -m/--model this launch) — never from the stored profile, so a pre-existing + // profile's stale model is ignored. Claude Code owns entitlement/refusal. + const cliModel = process.env.CODEMIE_CLI_MODEL; + if (cliModel && !result.includes('--model')) { + result = ['--model', cliModel, ...result]; + } + + const pluginDir = process.env.CODEMIE_CLAUDE_EXTENSION_DIR; + if (pluginDir && !result.some(arg => arg === '--plugin-dir')) { + result = ['--plugin-dir', pluginDir, ...result]; + } + + return result; + } +``` + +- [ ] **Step 4: Wire `CODEMIE_CLI_MODEL` in `AgentCLI.handleRun`** — after the `const config = await ConfigLoader.load(...)` block (~line 203), add: + +```ts + // Record the explicitly-requested CLI model so the anthropic-subscription + // provider can pass it through to Claude Code. Only set when the user passed + // -m/--model this launch (options.model), never from the stored profile. + if (typeof options.model === 'string' && options.model.trim() !== '') { + process.env.CODEMIE_CLI_MODEL = options.model.trim(); + } +``` + +- [ ] **Step 5: Run the tests to verify they pass** + +Run: `npx vitest run src/providers/plugins/anthropic-subscription/__tests__/anthropic-subscription.template.test.ts` +Expected: PASS (new block + all existing cases). + +- [ ] **Step 6: Commit** + +```bash +git add src/providers/plugins/anthropic-subscription/anthropic-subscription.template.ts src/agents/core/AgentCLI.ts src/providers/plugins/anthropic-subscription/__tests__/anthropic-subscription.template.test.ts +git commit -m "feat(providers): pass subscription --model through to Claude Code" +``` + +--- + +### Task 2: Subscription-scoped version-pin default + message (CS3) + +**Test-first:** yes — `newerVersionPromptDefault` returns `'continue'` for `anthropic-subscription` and `'install'` otherwise; `olderSupportedModelNote` returns the "newer models" note only for the subscription provider. + +**Files:** +- Create: `src/agents/core/version-prompt-policy.ts` +- Modify: `src/agents/core/BaseAgentAdapter.ts` (Scenario 1 `isNewer` ~line 440-467; Scenario 2 `hasUpdate` ~line 484-502) +- Test: `src/agents/core/__tests__/version-prompt-policy.test.ts` + +**Interfaces:** +- Produces: + - `newerVersionPromptDefault(provider: string | undefined): 'install' | 'continue'` — `'continue'` iff `provider === ProviderName.ANTHROPIC_SUBSCRIPTION`, else `'install'`. + - `olderSupportedModelNote(provider: string | undefined): string | null` — the "newer models may be unavailable" line for the subscription provider, else `null`. +- Consumes: `envOverrides?.CODEMIE_PROVIDER` inside `run()` (the provider is on the `envOverrides` argument passed at `AgentCLI.ts:461`). + +- [ ] **Step 1: Write the failing tests** + +```ts +import { describe, it, expect } from 'vitest'; +import { newerVersionPromptDefault, olderSupportedModelNote } from '../version-prompt-policy.js'; + +describe('version-prompt-policy', () => { + it('defaults the newer-than-pinned prompt to continue for anthropic-subscription', () => { + expect(newerVersionPromptDefault('anthropic-subscription')).toBe('continue'); + }); + it('keeps install as the default for proxied providers', () => { + expect(newerVersionPromptDefault('ai-run-sso')).toBe('install'); + expect(newerVersionPromptDefault(undefined)).toBe('install'); + }); + it('returns the older-but-supported note only for anthropic-subscription', () => { + expect(olderSupportedModelNote('anthropic-subscription')).toMatch(/newer models/i); + expect(olderSupportedModelNote('litellm')).toBeNull(); + }); +}); +``` + +- [ ] **Step 2: Run to verify failure** + +Run: `npx vitest run src/agents/core/__tests__/version-prompt-policy.test.ts` +Expected: FAIL ("Cannot find module '../version-prompt-policy.js'"). + +- [ ] **Step 3: Implement the helper** — create `src/agents/core/version-prompt-policy.ts` + +```ts +import { ProviderName } from '../../providers/core/types.js'; + +/** + * A pin that would downgrade an already-installed newer binary is softened to a + * warning that defaults to keeping what is installed — but only on the Anthropic + * Subscription profile, whose model availability comes from the installed Claude + * Code version. Proxied providers keep 'install' as the tested default. The + * minimum-version block is unaffected (a separate branch). + */ +export function newerVersionPromptDefault(provider: string | undefined): 'install' | 'continue' { + return provider === ProviderName.ANTHROPIC_SUBSCRIPTION ? 'continue' : 'install'; +} + +/** + * On an older-but-supported Claude Code, tell subscription users that newer models + * may be unavailable on that version (the update to the verified version is already + * offered by the prompt). Returns null for providers this story does not touch. + */ +export function olderSupportedModelNote(provider: string | undefined): string | null { + if (provider !== ProviderName.ANTHROPIC_SUBSCRIPTION) return null; + return 'Newer models may be unavailable on this version of Claude Code.'; +} +``` + +- [ ] **Step 4: Run to verify pass** + +Run: `npx vitest run src/agents/core/__tests__/version-prompt-policy.test.ts` +Expected: PASS. + +- [ ] **Step 5: Wire into `BaseAgentAdapter.run()`** — at the top of `run()` compute the provider once (after the `supportedVersion` guard opens), e.g. `const provider = envOverrides?.CODEMIE_PROVIDER;`. Then: + - Scenario 1 (`isNewer`, ~line 465): change `default: 'install'` to `default: newerVersionPromptDefault(provider)`. + - Scenario 2 (`hasUpdate`, ~line 484): after the existing message lines and before the prompt, add: + +```ts + const olderNote = olderSupportedModelNote(provider); + if (olderNote) { + console.log(chalk.white(` ${olderNote}`)); + console.log(); + } +``` + + Add the import: `import { newerVersionPromptDefault, olderSupportedModelNote } from './version-prompt-policy.js';`. Leave Scenario 0 (`isBelowMinimum`) and Scenario 2's `default: 'install'` unchanged. + +- [ ] **Step 6: Run typecheck + the policy test** + +Run: `npm run typecheck && npx vitest run src/agents/core/__tests__/version-prompt-policy.test.ts` +Expected: typecheck passes; test passes. + +- [ ] **Step 7: Commit** + +```bash +git add src/agents/core/version-prompt-policy.ts src/agents/core/__tests__/version-prompt-policy.test.ts src/agents/core/BaseAgentAdapter.ts +git commit -m "feat(agents): stop the version prompt defaulting to a downgrade on subscription" +``` + +--- + +### Task 3: Launch banner states the model in use (CS4) + +**Test-first:** yes — `resolveLaunchModelDisplay` returns the explicit CLI model on subscription, a per-session phrase (not `'unknown'`) when none, and today's `envModel || 'unknown'` for other providers. + +**Files:** +- Modify: `src/agents/core/version-prompt-policy.ts` (add a display helper — colocated model-presentation policy) OR create `src/agents/core/launch-model-display.ts` +- Modify: `src/agents/core/BaseAgentAdapter.ts:564` +- Test: `src/agents/core/__tests__/launch-model-display.test.ts` + +**Interfaces:** +- Produces: `resolveLaunchModelDisplay(provider: string | undefined, envModel: string | undefined, cliModel: string | undefined): string` — for the subscription provider returns `cliModel` when set, else `'chosen per session by Claude Code / your Anthropic subscription'`; for all other providers returns `envModel || 'unknown'` (today's behavior). + +- [ ] **Step 1: Write the failing tests** — create `src/agents/core/__tests__/launch-model-display.test.ts` + +```ts +import { describe, it, expect } from 'vitest'; +import { resolveLaunchModelDisplay } from '../launch-model-display.js'; + +describe('resolveLaunchModelDisplay', () => { + it('shows the explicit CLI model on the subscription profile', () => { + expect(resolveLaunchModelDisplay('anthropic-subscription', '', 'claude-opus-4-5')).toBe('claude-opus-4-5'); + }); + it('shows a per-session phrase (not "unknown") when no CLI model on subscription', () => { + const s = resolveLaunchModelDisplay('anthropic-subscription', '', undefined); + expect(s).not.toBe('unknown'); + expect(s).toMatch(/Claude Code/i); + }); + it('is unchanged for non-subscription providers', () => { + expect(resolveLaunchModelDisplay('litellm', 'gpt-5.5', undefined)).toBe('gpt-5.5'); + expect(resolveLaunchModelDisplay('litellm', '', undefined)).toBe('unknown'); + }); +}); +``` + +- [ ] **Step 2: Run to verify failure** + +Run: `npx vitest run src/agents/core/__tests__/launch-model-display.test.ts` +Expected: FAIL (module missing). + +- [ ] **Step 3: Implement** — create `src/agents/core/launch-model-display.ts` + +```ts +import { ProviderName } from '../../providers/core/types.js'; + +const SUBSCRIPTION_PER_SESSION = 'chosen per session by Claude Code / your Anthropic subscription'; + +/** + * What the launch banner prints as the model. On the subscription profile the model + * is either the explicit --model the user passed (CODEMIE_CLI_MODEL) or, absent that, + * Claude Code's own per-session choice — never the blanked CODEMIE_MODEL, which would + * otherwise render as 'unknown'. Other providers keep their existing behavior. + */ +export function resolveLaunchModelDisplay( + provider: string | undefined, + envModel: string | undefined, + cliModel: string | undefined, +): string { + if (provider === ProviderName.ANTHROPIC_SUBSCRIPTION) { + return cliModel && cliModel.trim() !== '' ? cliModel : SUBSCRIPTION_PER_SESSION; + } + return envModel || 'unknown'; +} +``` + +- [ ] **Step 4: Wire into the banner** — at `BaseAgentAdapter.ts:564` replace `const model = env.CODEMIE_MODEL || 'unknown';` with: + +```ts + const model = resolveLaunchModelDisplay(provider, env.CODEMIE_MODEL, process.env.CODEMIE_CLI_MODEL); +``` + +Add the import `import { resolveLaunchModelDisplay } from './launch-model-display.js';`. Reuse the `provider` local from `env.CODEMIE_PROVIDER` already computed at line 562. + +- [ ] **Step 5: Run to verify pass + typecheck** + +Run: `npx vitest run src/agents/core/__tests__/launch-model-display.test.ts && npm run typecheck` +Expected: PASS; typecheck clean. + +- [ ] **Step 6: Commit** + +```bash +git add src/agents/core/launch-model-display.ts src/agents/core/__tests__/launch-model-display.test.ts src/agents/core/BaseAgentAdapter.ts +git commit -m "feat(agents): state the subscription session model in the launch banner" +``` + +--- + +### Task 4: `codemie models list` explains the subscription source (CS5) + +**Test-first:** yes — `subscriptionModelsListMessage` mentions the Anthropic subscription and `/model`, and does not say "not supported". + +**Files:** +- Modify: `src/cli/commands/models.ts` (add helper + a branch after `provider` resolves, ~line 66) +- Test: `src/cli/commands/__tests__/models-subscription-message.test.ts` + +**Interfaces:** +- Produces: `subscriptionModelsListMessage(): string` — the informational text (single string) shown for the subscription provider. + +- [ ] **Step 1: Write the failing test** — create `src/cli/commands/__tests__/models-subscription-message.test.ts` + +```ts +import { describe, it, expect } from 'vitest'; +import { subscriptionModelsListMessage } from '../models.js'; + +describe('subscriptionModelsListMessage', () => { + it('explains models come from the Anthropic subscription and how to switch in-session', () => { + const msg = subscriptionModelsListMessage(); + expect(msg).toMatch(/Anthropic subscription/i); + expect(msg).toMatch(/\/model/); + expect(msg).not.toMatch(/not supported/i); + }); +}); +``` + +- [ ] **Step 2: Run to verify failure** + +Run: `npx vitest run src/cli/commands/__tests__/models-subscription-message.test.ts` +Expected: FAIL (`subscriptionModelsListMessage` not exported). + +- [ ] **Step 3: Implement** — in `src/cli/commands/models.ts` add the export near the top and a branch in the `list` action after `const provider = config.provider;` (before the `UNSUPPORTED_PROVIDERS` / `getModelProxy` checks, ~line 66): + +```ts +export function subscriptionModelsListMessage(): string { + return [ + 'Models for this profile come from your Anthropic subscription and the installed Claude Code version.', + 'CodeMie does not maintain a model list for it.', + 'Run codemie-claude and use /model inside Claude Code to see or change the model for a session.', + ].join('\n'); +} +``` + +```ts + if (provider === ProviderName.ANTHROPIC_SUBSCRIPTION) { + console.log(subscriptionModelsListMessage()); + return; // exit 0 — this is informational, not an error + } +``` + +Add `import { ProviderName } from '../../providers/core/types.js';` if not already imported. + +- [ ] **Step 4: Run to verify pass** + +Run: `npx vitest run src/cli/commands/__tests__/models-subscription-message.test.ts` +Expected: PASS. + +- [ ] **Step 5: Manual smoke (optional, non-gating)** — on a subscription profile, `node bin/codemie.js models list` prints the message and exits 0. + +- [ ] **Step 6: Commit** + +```bash +git add src/cli/commands/models.ts src/cli/commands/__tests__/models-subscription-message.test.ts +git commit -m "feat(cli): explain subscription model source in models list instead of erroring" +``` + +--- + +### Task 5: Setup no longer prompts for or stores a model (CS6) + +**Test-first:** yes — `setupModelSummaryLine` states a per-session choice (no model name) for the subscription provider and shows the model for others; the setup flow sets `selectedModel = ''` so `buildConfig` stores no model. + +**Files:** +- Modify: `src/cli/commands/setup.ts` (model-selection block ~line 424-435) +- Modify: `src/providers/integration/setup-ui.ts` (`displaySetupSuccess`, line 266-274) +- Test: `src/providers/integration/__tests__/setup-success-model-line.test.ts` + +**Interfaces:** +- Produces: `setupModelSummaryLine(provider: string, model: string): string` — for the subscription provider, a per-session sentence; else `` `🤖 Model: ${model}` ``. + +- [ ] **Step 1: Write the failing test** — create `src/providers/integration/__tests__/setup-success-model-line.test.ts` + +```ts +import { describe, it, expect } from 'vitest'; +import { setupModelSummaryLine } from '../setup-ui.js'; + +describe('setupModelSummaryLine', () => { + it('states per-session choice for the subscription provider (no stored model name)', () => { + const line = setupModelSummaryLine('anthropic-subscription', ''); + expect(line).toMatch(/per session/i); + expect(line).toMatch(/Claude Code/i); + }); + it('shows the model for other providers', () => { + expect(setupModelSummaryLine('litellm', 'gpt-5.5')).toContain('gpt-5.5'); + }); +}); +``` + +- [ ] **Step 2: Run to verify failure** + +Run: `npx vitest run src/providers/integration/__tests__/setup-success-model-line.test.ts` +Expected: FAIL (`setupModelSummaryLine` not exported). + +- [ ] **Step 3: Implement the summary helper** — in `setup-ui.ts` add and use it in `displaySetupSuccess` (replace line 273): + +```ts +export function setupModelSummaryLine(provider: string, model: string): string { + if (provider === 'anthropic-subscription') { + return '🤖 Model: chosen per session by Claude Code and your Anthropic subscription'; + } + return `🤖 Model: ${model}`; +} +``` + +Replace `console.log(chalk.cyan(`🤖 Model: ${model}`));` with `console.log(chalk.cyan(setupModelSummaryLine(provider, model)));`. + +- [ ] **Step 4: Skip the model prompt in setup** — in `setup.ts`, wrap the model-selection block (~424-435) so the subscription provider does not prompt or store a model: + +```ts + // Step 3: Model selection + let selectedModel: string; + if (providerName === ProviderName.ANTHROPIC_SUBSCRIPTION) { + // Model is chosen per session by Claude Code + the user's Anthropic + // subscription; storing one here would never take effect (exportEnvVars + // blanks it) and would be shown as a stale value later. + selectedModel = ''; + } else { + const preselectedModel = setupSteps.selectModel + ? await setupSteps.selectModel(credentials, models, providerTemplate) + : undefined; + if (preselectedModel) { + selectedModel = preselectedModel; + logger.success(`Model selected automatically: ${selectedModel}`); + } else { + selectedModel = await promptForModelSelection(models, providerTemplate); + } + } +``` + +Ensure `ProviderName` is imported in `setup.ts`. `buildConfig(credentials, '')` then stores `model: ''` (no code change to `buildConfig` needed). + +- [ ] **Step 5: Run tests + typecheck** + +Run: `npx vitest run src/providers/integration/__tests__/setup-success-model-line.test.ts src/providers/plugins/anthropic-subscription/__tests__/anthropic-subscription.setup-steps.test.ts && npm run typecheck` +Expected: PASS; typecheck clean (confirm the existing setup-steps tests still pass — `buildConfig` unchanged). + +- [ ] **Step 6: Commit** + +```bash +git add src/cli/commands/setup.ts src/providers/integration/setup-ui.ts src/providers/integration/__tests__/setup-success-model-line.test.ts +git commit -m "feat(cli): stop asking for and storing a model on subscription setup" +``` + +--- + +### Task 6: Full quality gate + regression sweep + +**Files:** none (verification only) + +- [ ] **Step 1: Run the unit project** + +Run: `npx vitest run --project unit` +Expected: PASS, including `model-tier-e2e.test.ts` (non-subscription env pipeline regression guard) and `flag-transform-contract.test.ts`. + +- [ ] **Step 2: Lint + typecheck** + +Run: `npm run lint && npm run typecheck` +Expected: zero warnings; clean types. + +- [ ] **Step 3: Confirm scope discipline** + +Run: `git diff --name-only origin/main...HEAD` +Expected: only `AgentCLI.ts`, `anthropic-subscription.template.ts`, `version-prompt-policy.ts`, `launch-model-display.ts`, `BaseAgentAdapter.ts`, `models.ts`, `setup.ts`, `setup-ui.ts`, their tests, and the task docs. **No** change to `transformEnvVars`/`exportProviderEnvVars`/`collectPassThroughArgs` or to `moonshot-subscription`. + +--- + +## Self-Review + +**Spec coverage:** CS1→Task1 (AgentCLI env var); CS2→Task1 (enrichArgs); CS3→Task2 (version policy, both scenarios); CS4→Task3 (banner); CS5→Task4 (models list); CS6→Task5 (setup prompt + summary). D1 relay (no code — verified by absence of entitlement logic); D2 subscription-scoped (Task2/3/4/5 all gate on `ProviderName.ANTHROPIC_SUBSCRIPTION`); D3 env var (Task1); D4 tests (every task). G4 (actual-model reporting) verified in spec — no task. AC "stale stored model not shown" → Task5 (`selectedModel=''`) + Task3 banner (reads CLI model, not stored). All AC groups map to a task or a verified no-op. + +**Placeholder scan:** No TBD/TODO; every code step has concrete code. Copy strings are final. + +**Type consistency:** Helper names are stable across tasks: `newerVersionPromptDefault`, `olderSupportedModelNote`, `resolveLaunchModelDisplay`, `subscriptionModelsListMessage`, `setupModelSummaryLine`. `CODEMIE_CLI_MODEL` is written in Task1 and read in Task1 (enrichArgs) and Task3 (banner). `ProviderName.ANTHROPIC_SUBSCRIPTION` used consistently. diff --git a/docs/superpowers/tasks/2026-08-31-subscription-model-access/qa-report.md b/docs/superpowers/tasks/2026-08-31-subscription-model-access/qa-report.md new file mode 100644 index 000000000..eb43207a0 --- /dev/null +++ b/docs/superpowers/tasks/2026-08-31-subscription-model-access/qa-report.md @@ -0,0 +1,32 @@ +# QA Gate Report — EPMCDME-14341 subscription-model-access + +**Branch**: EPMCDME-14341_subscription-model-access +**Runner**: npm +**Started**: 2026-08-31 +**Status**: PASSED + +## Gates + +| Gate | Source | Status | Command | Notes | +|------|--------|--------|---------|-------| +| license | guide | PASS | `npm run license-check` | Passed after using a writable npm cache; the default `~/.npm/_cacache` hits EACCES/EEXIST in this environment (infra, not a license violation). | +| lint | guide | PASS | `npm run lint` | ESLint `--max-warnings=0`, clean. | +| typecheck | guide | PASS | `npm run typecheck` | `tsc --noEmit` clean. | +| build | guide | PASS | `npm run build` | `tsc && tsc-alias && copy-plugin` succeeded; pricing table + plugin assets copied. | +| unit | guide | PASS | `npm run test:unit` | 3969 passed (268 files), including all new tests (enrichArgs passthrough, version-prompt-policy, launch-model-display, models message, setup summary, cli-model-env, buildConfig-empty, moonshot guard). | +| integration | guide | SKIPPED | `npm run test:integration` (`vitest run --project cli`) | Full cli project hangs locally on a subprocess/PTY negative-command test that needs the live backend/auth this sandbox lacks (log froze at `error: unknown command 'invalid-command-xyz'`); not caused by this change (that path never reaches the modified handleRun model logic). The two guards directly covering this change area — `proxy-routing-guard.test.ts` (anthropic-subscription proxy bypass) and `model-tier-e2e.test.ts` (non-subscription env pipeline) — were run in isolation: **14 passed**. CI runs the full `test:integration` unconditionally and remains the settling gate. | +| commitlint | guide | PASS | `npm run commitlint:last` | 0 problems; all branch commits are Conventional with `Refs: EPMCDME-14341`. | +| secrets | hook | PASS | `npm run validate:secrets` | Ran via `.husky/pre-commit` on every commit (podman up); Gitleaks reported "no leaks found" each time. | +| ui | guide | SKIPPED | (n/a) | No UI surface changed — diff is `.ts` under `src/` only (CLI/agents/providers), matches no `ui_globs`. Green for this mechanical phase. | + +## Failure detail + +None. The only non-PASS gates are SKIPPED: the UI gate (no UI surface) and the full integration suite (environment hang on an unrelated subprocess test), with targeted integration coverage substituted and passing. + +## Owed to CI + +- `npm run test:integration` (full `cli` project) — CI runs it for real. Locally verified via the two change-relevant guards only. + +## Drift signal + +no diff --git a/docs/superpowers/tasks/2026-08-31-subscription-model-access/spec.md b/docs/superpowers/tasks/2026-08-31-subscription-model-access/spec.md new file mode 100644 index 000000000..a593370ad --- /dev/null +++ b/docs/superpowers/tasks/2026-08-31-subscription-model-access/spec.md @@ -0,0 +1,93 @@ +# Spec — EPMCDME-14341: Latest Claude Models on an Anthropic Subscription Profile + +**Ticket**: https://jiraeu.epam.com/browse/EPMCDME-14341 +**Branch**: `EPMCDME-14341_subscription-model-access` +**Complexity**: M (16/36) +**Flow**: sdlc-standard + +## Problem + +On an `anthropic-subscription` profile the CLI removes model access from a user who already pays for it, in two independent ways: + +1. **`--model` is silently swallowed.** `-m/--model` is a config-only option (`AgentCLI.ts:598`); it never reaches the `claude` binary, and the value it sets in `CODEMIE_MODEL` is then blanked by the provider (`anthropic-subscription.template.ts:125`). Every subscription launch runs on Claude Code's own default, with no message that the choice was dropped. +2. **The version prompt defaults to a downgrade.** When the installed Claude Code is newer than CodeMie's pinned version, the launcher prompt (`BaseAgentAdapter.ts:440-467`) defaults to installing the older pinned version — taking newly released models off the menu. + +Two adjacent UX gaps compound it: setup asks for and stores a model that never takes effect (pre-filled `sonnet`), and `codemie models list` errors with "not supported" for this provider. + +## Locked decisions + +- **D1 — No entitlement logic (product owner).** CodeMie passes `--model` straight through to Claude Code and **relays** Claude Code's own refusal for unentitled models. CodeMie never validates entitlement or substitutes a model. A live entitlement catalog is out of scope. +- **D2 — Version-pin flip is subscription-scoped.** The newer-than-pinned default flip and the "newer models may be unavailable" message apply only when `CODEMIE_PROVIDER === 'anthropic-subscription'`. Proxied providers (SSO/LiteLLM/Bedrock) keep today's `install` default, where an untested newer binary can break the CodeMie proxy. The minimum-version hard block is unchanged for all providers. +- **D3 — Passthrough rides a dedicated env var.** `enrichArgs` receives `extractConfig(env)` (derived from env; `.model` = the blanked `CODEMIE_MODEL`), so a new `AgentConfig` field cannot carry the value. A dedicated env var `CODEMIE_CLI_MODEL`, set only from the explicit CLI `--model`, is the mechanism. This is what keeps a stale **stored** profile model from ever being injected. +- **D4 — Tests included** (explicit opt-in via this TDD flow), despite the repo's default "tests only on explicit request". + +## Env var contract — `CODEMIE_CLI_MODEL` + +- **Set by**: `AgentCLI.handleRun` — only when `options.model` is truthy (the user passed `-m/--model` **this launch**). Never derived from the stored profile. +- **Read by**: `anthropic-subscription` `enrichArgs` (injects `--model`) and the launch banner. +- **Survives blanking**: it is a new key, untouched by the provider's `exportEnvVars` (which blanks `CODEMIE_*_MODEL`) and by `beforeRun` (which deletes `ANTHROPIC_DEFAULT_*`). `Object.assign(process.env, env)` (`BaseAgentAdapter.ts:613`) does not clear keys absent from `env`. +- **Non-subscription providers ignore it** — they continue to resolve model via `CODEMIE_MODEL → ANTHROPIC_MODEL`. + +## Change sites + +### CS1 — Capture the explicit CLI model (`src/agents/core/AgentCLI.ts`) +In `handleRun`, after config load (~line 203), set `process.env.CODEMIE_CLI_MODEL = options.model` when `options.model` is a non-empty string. Provider-agnostic to write; only the subscription path reads it. No change to `configOnlyOptions` (`--model` stays stripped from generic passthrough for all providers). + +### CS2 — Inject `--model` for subscription (`src/providers/plugins/anthropic-subscription/anthropic-subscription.template.ts`) +Extend `agentHooks.claude.enrichArgs` (currently only injects `--plugin-dir`). Read `process.env.CODEMIE_CLI_MODEL`; if set **and** `args` does not already contain `--model`, prepend `['--model', value]`. Compose with the existing `--plugin-dir` injection; keep both dedup guards. `exportEnvVars` blanking is unchanged. + +### CS3 — Version-pin, subscription-scoped (`src/agents/core/BaseAgentAdapter.ts`) +`run()` receives `envOverrides` (the provider env, carrying `CODEMIE_PROVIDER`) — readable at the version-check branch before `process.env` is merged. +- **Scenario 1 — `isNewer` (line 440-467)**: when `envOverrides?.CODEMIE_PROVIDER === 'anthropic-subscription'`, set the prompt `default` to `'continue'` (else keep `'install'`). No downgrade step runs on the default choice. +- **Scenario 2 — `hasUpdate`, older-but-supported (line 484-501)**: for the subscription provider, add a line to the message that newer models may be unavailable on the installed version. Default stays `'install'` (an upgrade to the verified version, which the AC wants offered). +- **Scenario 0 — `isBelowMinimum` (line 397)**: unchanged; already refuses the below-minimum version and directs the user to update. +- `setup.ts:895` / `install.ts:228` `isNewer` prompts are out of scope (not the launcher run path). + +### CS4 — Launch banner states the model (`src/agents/core/BaseAgentAdapter.ts:~564`) +Currently `const model = env.CODEMIE_MODEL || 'unknown'`. For the subscription provider: show `process.env.CODEMIE_CLI_MODEL` when set; otherwise a phrase indicating the model is chosen per session by Claude Code / the user's Anthropic subscription (never `'unknown'`). Non-subscription behavior unchanged. + +### CS5 — `codemie models list` (`src/cli/commands/models.ts:73-77`) +For `anthropic-subscription`, replace the `process.exit(1)` "not supported" path with an informational message (exit 0): models come from the user's Anthropic subscription, and `/model` inside Claude Code shows or changes the model in a session. Scoped to the subscription branch; other providers unchanged. + +### CS6 — Setup no longer prompts/stores a model (`src/cli/commands/setup.ts`, `src/providers/integration/setup-ui.ts`, `anthropic-subscription.setup-steps.ts`) +For the subscription provider: skip the "Enter model name manually" prompt (`setup.ts:617-632`); `buildConfig` stores no `model`; the success summary (`setup-ui.ts:273`) states the model is chosen per session by Claude Code + the user's Anthropic subscription rather than printing `Model: sonnet`. Pre-existing profiles that already carry a stored model launch normally (runtime ignores it via the blanking + `CODEMIE_CLI_MODEL` mechanism) with no migration. + +## Acceptance-criteria traceability + +| AC group | Covered by | +|---|---| +| `--model` entitled → session runs on it; launch not refused; relayed to `claude` | CS1 + CS2 | +| no `--model` → Claude Code default; no imposed/pre-filled model | CS1 (var unset) + CS2 (no injection) | +| in-session `/model` switch stays in effect | Unchanged (Claude Code owns the session) | +| API-key/SSO `--model` resolution unchanged | CS1/CS2 scoped; shared env pipeline untouched | +| model stated to the user at start | CS4 | +| setup not asked for a model; summary explains per-session choice | CS6 | +| `models list` explains source; no "unsupported" error | CS5 | +| stale stored model not presented / no migration | CS6 + D3 | +| newer-than-verified: warned; default continues on installed; no downgrade | CS3 Scenario 1 | +| older-but-supported: told newer models may be unavailable; offered update | CS3 Scenario 2 | +| below minimum: no session; told to update | CS3 Scenario 0 (unchanged) | +| unentitled model → refusal names model, attributes to subscription; no substitution | D1 (Claude Code refuses; CodeMie relays, never swallows/substitutes) | +| not installed/not authenticated → existing guidance | Unchanged | +| session-end reporting records actual model used | Verified — analytics reads transcripts (`BaseAgentAdapter.ts:908`); no change | + +## Out of scope + +Live entitlement-backed model catalog; supported-version refresh cadence / whether the pin should exist for other providers; Moonshot Subscription parity; per-tier overrides on the subscription path; any change to model resolution/ranking/recommended-starring for API-key/SSO/Bedrock/LiteLLM; models absent from Claude Code's own picker on a verified version; pricing entries for unreleased models; agents other than Claude Code; the `setup`/`install` command version prompts. + +## Test plan (Vitest, unit unless noted) + +1. `anthropic-subscription.template.test.ts` — `enrichArgs` injects `['--model', v]` when `CODEMIE_CLI_MODEL` set; injects nothing when unset; does **not** double-inject when `--model` already in `args`; composes with `--plugin-dir`. +2. `moonshot-subscription` — assert its `enrichArgs`/template is untouched by the `CODEMIE_CLI_MODEL` mechanism (no injection). +3. `BaseAgentAdapter` version prompt — Scenario 1 default is `'continue'` when provider is `anthropic-subscription`, `'install'` otherwise; minimum-version block unchanged. +4. `models list` — subscription provider prints the informational message and exits 0 (no "unsupported" error). +5. Setup — `buildConfig` stores no `model` for the subscription provider; success summary text asserts per-session wording. +6. Banner — states `CODEMIE_CLI_MODEL` when set; states the per-session phrase (not `'unknown'`) when unset, for the subscription provider. + +## Risks & mitigations + +- **Shared env pipeline regression** → confine changes to the subscription template + the provider-guarded branches in `BaseAgentAdapter`/`models.ts`; never touch `transformEnvVars`/`exportProviderEnvVars`/`collectPassThroughArgs`. `model-tier-e2e.test.ts` is the regression guard for the non-subscription env path. +- **Double `--model`** → dedup guard checks for both `--model` and the `--model=` equals-form before injecting (mirrors `--plugin-dir`). The equals-form is reachable only via raw passthrough after `--` (e.g. `-m X -- --model=Y`); found and closed during manual edge-case verification. +- **`moonshot-subscription` drift** → only `anthropic-subscription.template.ts` is edited; test 2 guards it. +- **Provider not readable at version branch** → mitigated: `envOverrides` carries `CODEMIE_PROVIDER` (AgentCLI:461 → run()). +- **No automated coverage for new behaviors** → addressed by the test plan above (D4). diff --git a/docs/superpowers/tasks/2026-08-31-subscription-model-access/technical-analysis.md b/docs/superpowers/tasks/2026-08-31-subscription-model-access/technical-analysis.md new file mode 100644 index 000000000..30642ca1e --- /dev/null +++ b/docs/superpowers/tasks/2026-08-31-subscription-model-access/technical-analysis.md @@ -0,0 +1,234 @@ +# Technical Research + +**Task**: anthropic-subscription providers claude-launcher model-flag version-pin +**Generated**: 2026-08-31T00:00:00Z +**Research path**: filesystem + +--- + +## 1. Original Context + +The CLI (@codemieai/code) has a dedicated Anthropic Subscription provider (`anthropic-subscription`) that lets `codemie-claude` run on the user's own Claude Code login instead of a CodeMie-issued key. By design it hands model choice to Claude Code: the provider blanks the model and model-tier values it would otherwise export, and the Claude plugin's catalog-driven tier auto-resolution is deliberately skipped for this provider. + +Five concrete behaviors this story targets: +1. `-m, --model` is offered on every agent launcher but treated as a CONFIG-ONLY option — consumed by CodeMie's config layer and never forwarded to the `claude` binary. On a subscription profile the value it sets is then blanked, so `codemie-claude --model ` has no effect and reports nothing. +2. Setup still asks for a model on this path. The subscription provider returns an empty model list, so setup falls through to "No models found. Enter model name manually:" pre-filled with the family token `sonnet`, saves whatever is typed, and prints `Model: sonnet` in the success summary — a value that never reaches a session. +3. `codemie models list` has no model source registered for `anthropic-subscription` (only LiteLLM, AI/Run SSO, Bedrock, bearer-auth and Ollama providers have one), so it exits with "Model listing is not supported for provider 'anthropic-subscription'". +4. The defect: on a subscription profile, a requested model is accepted then discarded silently; the session always runs on Claude Code's own default. +5. CodeMie pins the Claude Code binary: one verified version, a hard minimum below which the launcher refuses to start, and it disables the binary's auto-updater on every run. When the installed Claude Code is NEWER than the pinned version, the launcher prompts and its DEFAULT choice installs the pinned OLDER version — a downgrade. This must become a warning that defaults to continuing on the installed version; the minimum-version block stays a hard block. + +Key design decision already made by the product owner (carry this — do NOT design entitlement logic): on the subscription path CodeMie PASSES the `--model` value straight through to Claude Code and never validates entitlement itself. If a model is unentitled, Claude Code refuses and CodeMie simply SURFACES/RELAYS that refusal (never swallows or substitutes). Building a real entitlement-backed model catalog is explicitly OUT OF SCOPE. + +--- + +## 2. Codebase Findings + +### Existing Implementations + +**Provider layer — anthropic-subscription:** +- `src/providers/plugins/anthropic-subscription/anthropic-subscription.template.ts` — The provider template. `exportEnvVars()` (lines 109–138) sets `CODEMIE_MODEL = ''`, `CODEMIE_HAIKU_MODEL = ''`, `CODEMIE_SONNET_MODEL = ''`, `CODEMIE_OPUS_MODEL = ''` — the blanking mechanism. `agentHooks['*'].beforeRun()` (lines 36–93) additionally deletes `ANTHROPIC_DEFAULT_HAIKU_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, `ANTHROPIC_DEFAULT_OPUS_MODEL`, and `CLAUDE_CODE_SUBAGENT_MODEL` to prevent stale shell values from pinning the binary. `agentHooks.claude.enrichArgs()` injects `--plugin-dir` for the CodeMie extension but does NOT inject `--model`. The `CODEMIE_PROVIDER` env var is set to `'anthropic-subscription'` here and acts as the global discriminator downstream. +- `src/providers/plugins/anthropic-subscription/anthropic-subscription.setup-steps.ts` — Setup steps. `fetchModels()` (lines 96–99) unconditionally returns `[]`. `selectModel()` returns `null` unless a CodeMie analytics URL is set; in either case no model is auto-selected. `buildConfig()` stores whatever model the user typed into the profile but the template's `exportEnvVars` neutralizes it at runtime. +- `src/providers/plugins/anthropic-subscription/index.ts` — Barrel re-export. +- `src/providers/plugins/moonshot-subscription/moonshot-subscription.setup-steps.ts` — Parallel subscription pattern: `fetchModels()` returns `[]`, `selectModel()` returns `null`. Both subscription providers share the same blanking convention. + +**Agent plugin layer — ClaudePlugin:** +- `src/agents/plugins/claude/claude.plugin.ts` — `CLAUDE_SUPPORTED_VERSION = '2.1.218'` (line ~39, pinned verified version). `CLAUDE_MINIMUM_SUPPORTED_VERSION = '2.1.208'` (line ~49, hard minimum; always 10 patch versions below supported). `DISABLE_AUTOUPDATER = '1'` set unconditionally in `lifecycle.beforeRun` (lines 183–186). The catalog tier resolution block is entirely gated at line 315 behind `if (env.CODEMIE_PROVIDER !== 'anthropic-subscription')` — the single divergence point for all model-catalog logic. `envMapping.model` maps to `['ANTHROPIC_MODEL']` but is never reached for the subscription path. +- `src/agents/plugins/claude/claude.models.ts` — `resolveClaudeModel(env, tier)`: live-catalog-based model selector for all four tiers (haiku/sonnet/opus/plus). Added in prior task EPMCDME-13xxx (2026-08-19). Skipped entirely for `anthropic-subscription`. + +**Core adapter layer:** +- `src/agents/core/BaseAgentAdapter.ts` — `run()` method is the shared launch pipeline for all providers. Version compatibility checked at line ~394: `compat.isNewer` branch (line 440–481) shows a list prompt with `default: 'install'` — this installs the OLDER pinned version by default; `'Continue with current version'` is option index 1 and is NOT the default. Welcome message (line 564): `const model = env.CODEMIE_MODEL || 'unknown'` — for subscription this always resolves to `'unknown'` after blanking. `transformEnvVars()` (line 1130–1134) maps `CODEMIE_MODEL → ANTHROPIC_MODEL`; since `CODEMIE_MODEL` is blank for subscription, `ANTHROPIC_MODEL` is never set in the child process. Session-end analytics at line 908 reads the actual model from conversation transcripts, not from `CODEMIE_MODEL`, so it does record the true model used even when the env var is absent. `extractConfig()` (line 1051) passes `model: env.CODEMIE_MODEL` to hooks; empty for subscription. +- `src/agents/core/AgentCLI.ts` — `-m/--model ` option defined at line 78. `'model'` is listed in `configOnlyOptions` (line 598), which causes `collectPassThroughArgs()` to skip it — the value NEVER appears in the `claude` binary's argv for any provider. At line 198–203 the model is passed only to `ConfigLoader.load()` as `options.model`. + +**Config layer:** +- `src/utils/config.ts` — `ConfigLoader.exportProviderEnvVars()` (line ~1425): sets `CODEMIE_MODEL = config.model` from the loaded profile+CLI-override before calling the provider's `exportEnvVars`. The provider template then overwrites it to `''` for subscription. This is the correct sequencing point where `--model` from the CLI reaches `CODEMIE_MODEL` and where subscription-specific passthrough logic should be inserted. + +**CLI commands:** +- `src/cli/commands/setup.ts` — `promptForModelSelection()` (lines 617–632): when `models.length === 0`, shows `'No models found. Enter model name manually:'` with `default: providerTemplate?.recommendedModels?.[0] || 'gpt-5.5'`. For `anthropic-subscription`, `recommendedModels[0]` is `'sonnet'`. +- `src/cli/commands/models.ts` — `createModelsCommand`: checks `ProviderRegistry.getModelProxy(provider)` at lines 73–77; if null, exits with `"Model listing is not supported for provider '${provider}'"` and `process.exit(1)`. No model proxy is registered for `anthropic-subscription`. +- `src/providers/integration/setup-ui.ts` — `displaySetupSuccess()` (line 273): prints `Model: ${model}` in the setup success summary. + +### Architecture and Layers Affected + +| Layer | Component | Change scope | +|---|---|---| +| CLI parse | `AgentCLI.ts:78, 598` | `-m/--model` is already parsed; `configOnlyOptions` membership stays; passthrough to `claude` argv is the new behavior for subscription only | +| Config export | `ConfigLoader.exportProviderEnvVars` (`config.ts:1425`) | Shared pipeline; must not change shared behavior | +| Provider template | `anthropic-subscription.template.ts:109-138` (exportEnvVars), `36-93` (beforeRun), `enrichArgs` | Primary change site: conditional model carry-through instead of unconditional blank | +| Agent plugin | `claude.plugin.ts:315` | Guard already correct; no change needed to guard logic itself | +| Core adapter | `BaseAgentAdapter.ts:440-481` | `compat.isNewer` default flip: `'install'` → `'continue'` | +| Setup wizard | `setup.ts:617-632`, `setup-ui.ts:273` | Inform user that model is passed through; may update messaging | + +### Integration Points + +**Internal module dependency chain (model path):** +``` +AgentCLI.ts:78 (parse --model) + → ConfigLoader.load() [options.model override] + → ConfigLoader.exportProviderEnvVars() [sets CODEMIE_MODEL] + → ProviderTemplate.exportEnvVars() [subscription: blanks CODEMIE_MODEL → to change] + → BaseAgentAdapter.transformEnvVars() [maps CODEMIE_MODEL → ANTHROPIC_MODEL] + → claude binary spawn [enrichArgs adds --model if subscription path] +``` + +**Version-pin path:** +``` +BaseAgentAdapter.run() + → checkVersionCompatibility(installedVersion, CLAUDE_SUPPORTED_VERSION, CLAUDE_MINIMUM_SUPPORTED_VERSION) + → compat.isNewer → prompt with default: 'install' [to change to 'continue'] + → compat.isBelowMinimum → hard block [stays] + → lifecycle.beforeRun sets DISABLE_AUTOUPDATER='1' +``` + +**External services:** +- `@anthropic-ai/claude-code` binary — the spawned process; receives env vars and argv from the above pipeline. +- Claude Code's own model entitlement checking — opaque; CodeMie will surface its refusal messages unchanged. + +### Patterns and Conventions + +- **Subscription blanking pattern**: both `anthropic-subscription` and `moonshot-subscription` use `fetchModels() → []`, `selectModel() → null`, and `exportEnvVars` that blank `CODEMIE_*_MODEL`. The new passthrough logic will be the first departure from this pattern and must be subscription-specific without affecting moonshot-subscription. +- **`configOnlyOptions` guard**: `AgentCLI.collectPassThroughArgs()` excludes `'model'` for all providers universally. The fix does NOT change this; instead, the subscription provider's `enrichArgs` injects `--model ` into the Claude binary's args directly, which is the established mechanism (used for `--plugin-dir`, `--task → -p`, `--resume → -r`). +- **`enrichArgs` injection**: the `agentHooks.claude.enrichArgs(args, env)` function in the provider template receives the full args array and the env object. It can check `env.CODEMIE_MODEL` and prepend `['--model', env.CODEMIE_MODEL]` to args. This is the correct insertion point — it fires just before the binary is spawned and is already provider-scoped. +- **`ConfigurationError` convention**: all errors in provider/adapter code use `ConfigurationError` from `src/utils/errors.ts`, never generic `Error`. +- **`resolveModel` naming**: model resolvers live in `src/agents/plugins//.models.ts`. Not applicable to this story (no new resolver needed). +- **Version-pin comment**: `CLAUDE_SUPPORTED_VERSION` and `CLAUDE_MINIMUM_SUPPORTED_VERSION` carry inline comments "UPDATE THIS WHEN BUMPING CLAUDE VERSION" and "always 10 patch versions below supported". No automation enforces the gap — manual discipline required. + +--- + +## 3. Documentation Findings + +### Guides and Architecture Docs + +- `.ai-run/guides/architecture/architecture.md` — 5-layer plugin architecture (CLI → Registry → Plugin → Core → Utils); documents `src/providers/plugins/` and `src/agents/plugins/` layout. Directly relevant to understanding which layer owns the fix for each story target. +- `.ai-run/guides/development/development-practices.md` — Mandates `ConfigurationError` (never generic `Error`) for all new error paths. Relevant to the `codemie models list` unsupported message path if refactored. +- `.ai-run/guides/integration/external-integrations.md` — Claude session processing, drain loop, bash passthrough. Does NOT cover model-catalog resolution or the subscription-specific env var lifecycle. +- `.ai-run/guides/usage/project-config.md` — Profiles, env vars, paths. Relevant to `CODEMIE_*` var layering and how `config.model` propagates. +- `.ai-run/guides/quality-gates.md`, `.ai-run/guides/standards/`, `.ai-run/guides/testing/` — Process/quality gates, not feature-specific. + +### Architectural Decisions + +- **2026-08-19 prior task (EPMCDME-13xxx, `docs/superpowers/tasks/2026-08-19-claude-model-autoupdate/`)**: Added `claude.models.ts` with `resolveClaudeModel()`. The `anthropic-subscription` skip guard at `claude.plugin.ts:315` was added deliberately in that task. `CR-004`: no `modelSource` signal guard; explicit decision to keep current behavior, documented inline at `claude.models.ts:214-224`. `CR-005`: no tests added (policy: tests only on explicit request). +- **EPMCDME-12779**: `ConfigLoader.exportProviderEnvVars` always emits even-empty `CODEMIE_*` vars to overwrite stale shell values; anthropic-subscription must explicitly blank them. This decision is the root of the current blanking mechanism — the new passthrough fix must keep the blank for all vars EXCEPT the `CODEMIE_MODEL` path when a user model is supplied. +- **EPMCDME-14355**: `CLAUDE_CODE_SUBAGENT_MODEL` removed from sonnet tier's `envMapping` to avoid silencing per-subagent model params on multi-tier tenants. This affects only the non-subscription path. +- **EPMCDME-13734**: Per-agent supported-version constants removed then re-added per the version management spec. Current constants are authoritative. +- **Product owner decision (this story)**: No entitlement validation on subscription path. CodeMie passes `--model` through; Claude Code owns the refusal. This is OUT OF SCOPE to implement entitlement logic. + +### Derived Conventions + +- `enrichArgs` is the canonical injection point for binary CLI args in provider templates. It receives the pre-built args array and can prepend or append. Guard deduplication is expected (see `--plugin-dir` guard pattern in existing `enrichArgs`). +- Model passthrough via CLI arg (`--model`) is preferred over env var (`ANTHROPIC_MODEL`) for the subscription path: the env var path goes through `transformEnvVars` which is shared with all providers, while `enrichArgs` is already provider-scoped. +- The `exportEnvVars` blanking for `CODEMIE_MODEL` exists specifically to prevent the `transformEnvVars` shared pipeline from setting `ANTHROPIC_MODEL` to a CodeMie-catalog model that is meaningless for the subscription path. The fix must preserve this intent while adding a conditional: if a user-requested model is present (non-empty `CODEMIE_MODEL` before blanking), carry it to `enrichArgs` instead. + +--- + +## 4. Testing Landscape + +### Existing Coverage + +- `src/providers/plugins/anthropic-subscription/__tests__/anthropic-subscription.template.test.ts` — Covers: template metadata (`name`, `authType`, `defaultBaseUrl`, `recommendedModels`), `beforeRun` hook (auth env var stripping, model-tier env var stripping, `CODEMIE_CLAUDE_EXTENSION_DIR` set/skip), `enrichArgs` (`--plugin-dir` injection), `exportEnvVars` (model blanking, codeMieUrl/Project export). Does NOT test `enrichArgs` behavior when a `--model` arg is present. +- `src/providers/plugins/anthropic-subscription/__tests__/anthropic-subscription.setup-steps.test.ts` — Covers: `selectModel` (auto-select with codeMieUrl, null for no-analytics, empty-list fallback), `fetchModels` (always returns `[]`), `buildConfig` (provider/model fields, codeMieUrl/Project passthrough, defaultBaseUrl fallback). +- `src/providers/plugins/anthropic-subscription/__tests__/anthropic-subscription.auth.test.ts` — Covers: `parseClaudeAuthStatus` JSON parsing, error handling for invalid/empty input. +- `tests/integration/proxy-routing-guard.test.ts` — Covers: proxy bypass for `anthropic-subscription`; `exportProviderEnvVars` emits `CODEMIE_AUTH_METHOD=manual`; `shouldUseProxy` returns false even with stale JWT env. +- `tests/integration/model-tier-e2e.test.ts` — Covers: Config→Export→Transform pipeline for non-subscription providers, `ClaudePlugin` `envMapping` for haiku/sonnet/opus, `ANTHROPIC_*` env var transformation, CLI model override while preserving tier config. +- `src/agents/plugins/__tests__/flag-transform-contract.test.ts` — Covers per-agent flag contracts: `claude: --task → -p`, `claude: --resume → -r`. No assertion for `--model` behavior on `claude` launcher. +- `src/agents/core/__tests__/flag-transform.test.ts` — `transformFlags` utility unit tests. +- `tests/integration/agent-model.test.ts` (agent, real network) — TC-020 (profile model selection), TC-021 (metrics records model), TC-022 (`codemie models list` via SSO/JWT only), TC-024 (in-session `/model` switch via PTY). +- `tests/integration/agent-setup.test.ts` (agent, real network) — TC-029 setup wizard SSO profile creation and model persistence (SSO provider only, not `anthropic-subscription`). + +### Testing Framework and Patterns + +- **Framework**: Vitest 3.x; three project configs: `unit` (`src/**/*.test.ts`), `cli` (`tests/integration/**/*.test.ts` excluding agent-*), `agent` (`tests/integration/agent-*.test.ts` with real network/auth). +- **Module mocking**: `vi.hoisted()` + `vi.mock()` for hoisting before ESM imports; `vi.fn()` / `vi.restoreAllMocks()` in `beforeEach`/`afterEach`. +- **Filesystem isolation**: `mkdtempSync` per suite, cleaned in `afterAll`; `CODEMIE_HOME` env override for full profile isolation. +- **Process env isolation**: `process.env` saved/restored in `beforeEach`/`afterEach` pairs. +- **Conditional gates**: `describe.runIf(process.env.SSO_AVAILABLE !== 'false')` for auth-required cases. +- **Interactive flows**: PTY session helper (`spawnPty` from `tests/helpers/pty-session.ts`) for prompt-driven flows; `spawnSync` for headless CLI subprocess invocations. +- **Auth helpers**: `tests/helpers/sso-auth.ts`, `tests/helpers/jwt-auth.ts`, `tests/helpers/sso-claude-plugin.test.ts`. + +### Coverage Gaps + +- **`--model` passthrough for `anthropic-subscription`**: no test in `flag-transform-contract.test.ts` or `anthropic-subscription.template.test.ts` asserts that a user-supplied `--model ` is injected into the `claude` binary's argv by `enrichArgs` on the subscription path. This is the core new behavior. +- **`codemie models list` with subscription profile**: TC-022 tests SSO/JWT only. No unit or CLI integration test asserts that `codemie models list` prints `"Model listing is not supported for provider 'anthropic-subscription'"` and exits non-zero. +- **Version-pin downgrade prompt default**: no test covers `BaseAgentAdapter.run()` behavior when `compat.isNewer` is true. The default flip from `'install'` to `'continue'` is untested. +- **`enrichArgs` deduplication when `--model` already in user args**: no test for the case where the user passes `--model` as an unknown arg that passes through Commander's `.allowUnknownOption()` AND the subscription enrichArgs also injects `--model`. +- **Setup wizard for `anthropic-subscription`** (integration): no TC-029 equivalent for the full wizard flow with this provider; the unit test covers `selectModel`/`buildConfig` but not the wizard prompt chain end-to-end. +- **Welcome message model display on subscription profile**: no test asserts that `renderProfileInfo` shows `'unknown'` (current) or a model name (after fix) for subscription sessions. + +--- + +## 5. Configuration and Environment + +### Environment Variables + +**Set by `anthropic-subscription.template.ts` `beforeRun` (deleted from child process env):** +- `ANTHROPIC_AUTH_TOKEN` — deleted; native Claude Code login takes over +- `ANTHROPIC_API_KEY` — deleted +- `ANTHROPIC_BASE_URL` — deleted +- `ANTHROPIC_DEFAULT_HAIKU_MODEL` — deleted; prevents stale shell value from pinning haiku tier +- `ANTHROPIC_DEFAULT_SONNET_MODEL` — deleted +- `ANTHROPIC_DEFAULT_OPUS_MODEL` — deleted +- `CLAUDE_CODE_SUBAGENT_MODEL` — deleted + +**Set by `anthropic-subscription.template.ts` `exportEnvVars` (blanked in CODEMIE namespace):** +- `CODEMIE_MODEL` — set to `''` (the primary change target for the passthrough fix) +- `CODEMIE_HAIKU_MODEL` — set to `''` +- `CODEMIE_SONNET_MODEL` — set to `''` +- `CODEMIE_OPUS_MODEL` — set to `''` +- `CODEMIE_API_KEY` — set to `''` + +**Set by `claude.plugin.ts` `lifecycle.beforeRun` (unconditional for all Claude sessions):** +- `DISABLE_AUTOUPDATER` — `'1'`; prevents Claude Code binary self-update. Overridable if already set in shell. +- `ENABLE_TOOL_SEARCH` — `'0'`; workaround for Claude Code ≥2.1.69 startup failure when proxied +- `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS` — `'1'` unless already set +- `CLAUDE_CODE_ENABLE_TELEMETRY` — `'0'`; prevents 404s on CodeMie backend +- `ENABLE_PROMPT_CACHING_1H` — `'1'` unless already set +- `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` — default `'85'`; overridable via `CODEMIE_PROFILE_CONFIG.claudeAutocompactPct` + +**Routing/discriminator vars:** +- `CODEMIE_PROVIDER` — set to `'anthropic-subscription'`; gates `claude.plugin.ts:315` catalog skip +- `CODEMIE_JWT_TOKEN` / `CODEMIE_BASE_URL` — used by `claude.models.ts` catalog fetch for non-subscription paths +- `CODEMIE_REASONING_EFFORT` — if set, `applyReasoningEffort()` injects `--effort` into binary args + +### Configuration Files + +- `config.example.json` — Profile shape: `provider`, `baseUrl`, `apiKey`, `model`, `timeout`, `debug`, `allowedDirs`, `ignorePatterns`. The `model` field is the stored placeholder that `exportEnvVars` currently discards for subscription. + +### Feature Flags and Deployment Concerns + +- **`DISABLE_AUTOUPDATER`**: Set unconditionally by `claude.plugin.ts`. If a shell sets `DISABLE_AUTOUPDATER=0`, it is respected — a potential escape hatch that bypasses version control. Not directly related to this story but noted. +- **Version constant update discipline**: `CLAUDE_SUPPORTED_VERSION` and `CLAUDE_MINIMUM_SUPPORTED_VERSION` in `claude.plugin.ts` are manually maintained. The 10-patch-version gap is convention, not enforced. Any bump requires updating both constants. +- **`enrichArgs` deduplication risk**: if a user passes `--model claude-haiku-4-5` as an unknown arg (Commander passes it through via `.allowUnknownOption()`) AND `enrichArgs` also injects `--model `, the Claude binary receives `--model` twice. The fix must deduplicate — check whether `--model` is already present in `args` before injecting, following the existing `--plugin-dir` guard pattern. +- **`ANTHROPIC_MODEL` vs `--model` arg**: both mechanisms exist; `ANTHROPIC_MODEL` env var is the non-subscription path (set by `transformEnvVars`). For subscription the fix should use `--model` in argv via `enrichArgs` rather than `ANTHROPIC_MODEL` env var to stay within the subscription-scoped code path and avoid changing the shared `transformEnvVars` logic. + +--- + +## 6. Risk Indicators + +- **Shared env pipeline (`transformEnvVars`, `exportProviderEnvVars`)**: these functions run for ALL providers. Any change to them (rather than to the subscription template's `exportEnvVars` / `enrichArgs`) risks breaking SSO, LiteLLM, Bedrock, and bearer-auth model resolution. Changes must be confined to `anthropic-subscription.template.ts` and `BaseAgentAdapter.ts:440-481` (version prompt only). + +- **Double `--model` injection**: Commander's `.allowUnknownOption()` + `[args...]` passes user-supplied unknown args to the `claude` binary. If a user types `codemie-claude --model claude-opus-4-5` and the new `enrichArgs` also injects `--model`, the binary receives the flag twice. The fix must check `args.includes('--model')` before injecting — same pattern as the `--plugin-dir` deduplication already in `enrichArgs`. + +- **`moonshot-subscription` must not change**: it shares the blanking convention. The fix is scoped only to `anthropic-subscription.template.ts`. Confirm `moonshot-subscription.template.ts` is not touched. + +- **Welcome message regression**: `BaseAgentAdapter.ts:564` reads `env.CODEMIE_MODEL || 'unknown'`. After the fix, for subscription sessions with a `--model` flag, `CODEMIE_MODEL` may no longer be blank (if the fix chooses not to blank it in `exportEnvVars`). This changes the welcome message from `'unknown'` to the user's requested model — a UI improvement but a behavioral change that should be explicitly verified. If the fix injects `--model` via `enrichArgs` without changing `exportEnvVars`, then `CODEMIE_MODEL` remains `''` and the welcome message stays `'unknown'` — this is arguably acceptable since the model display at launch for subscription profiles is already acknowledged as non-functional. + +- **`DISABLE_AUTOUPDATER` shell override**: setting `DISABLE_AUTOUPDATER=0` in the shell bypasses CodeMie's version control. This is a pre-existing gap, not introduced by this story. + +- **Version-pin default change scope**: `BaseAgentAdapter.ts:440-481` is in the shared adapter; changing `default: 'install'` to `default: 'continue'` affects all agents and all providers that use `BaseAgentAdapter`. The version-pin prompt logic is agent-agnostic, so this is correct — but verify no other agent (Codex, Gemini, Kimi, etc.) has its own version-pin prompt in a separate adapter that also needs the same fix. + +- **No test gate for downgrade default**: `CR-005` from the prior task established a repo policy of "tests only on explicit request." The version-pin default flip and the model passthrough are new behaviors with zero existing test coverage. The implementation must be careful since there is no automated safety net. The `model-tier-e2e.test.ts` test is the closest regression guard for the non-subscription env var pipeline. + +- **`codemie models list` messaging**: the exit path at `models.ts:73-77` uses `process.exit(1)` which is acceptable but untested for `anthropic-subscription`. If the story requires a more informative message (e.g., "This provider uses your Claude Code subscription; model selection is managed by Claude Code"), the change is in `models.ts` which is shared — scope it to the `anthropic-subscription` branch only. + +- **Session analytics model recording**: `BaseAgentAdapter.ts:908` reads the model from conversation transcripts, not from `CODEMIE_MODEL`. This means end-of-session reporting will correctly record the actual model Claude Code used, regardless of whether `CODEMIE_MODEL` is blank. No change needed here — but the story's acceptance criteria should note that launch-time model reporting (welcome banner) and session-end reporting have different sources. + +- **`CODEMIE_PROVIDER` guard dependency**: `claude.plugin.ts:315` is the single guard between subscription and non-subscription catalog resolution. Any future provider that also sets `CODEMIE_PROVIDER !== 'anthropic-subscription'` but should also skip catalog resolution will need a more general guard. Not a risk for this story but worth noting for future-proofing. + +--- + +## 7. Summary for Complexity Assessment + +This story touches four discrete change sites across two architectural layers, with no structural rearchitecting required. The implementation surface is narrow and well-isolated: the primary model-passthrough fix lives entirely in `src/providers/plugins/anthropic-subscription/anthropic-subscription.template.ts` (the `enrichArgs` function and the conditional blank in `exportEnvVars`), and the version-pin default flip is a single-line change at `BaseAgentAdapter.ts` line ~461 (`default: 'install'` → `default: 'continue'`). The `configOnlyOptions` guard in `AgentCLI.ts` and the `claude.plugin.ts:315` guard are already correct and need no modification. The `codemie models list` unsupported message and the setup wizard messaging are documentation/UX improvements with no logic change required unless the story demands richer error text. + +The task follows established patterns: `enrichArgs` injection is the existing mechanism for provider-specific arg injection; the deduplication guard is already exemplified by `--plugin-dir`. The only design novelty is the decision to carry `CODEMIE_MODEL` (or a captured snapshot of it before blanking) into `enrichArgs` for the subscription path — no prior provider does this because all other providers set `ANTHROPIC_MODEL` via the env var pipeline instead. This means the implementer must decide whether to (a) capture the model value before `exportEnvVars` blanks it and pass it via closure or a side-channel to `enrichArgs`, or (b) not blank `CODEMIE_MODEL` in `exportEnvVars` for the subscription path when a model is set (relying on the `beforeRun` deletion of `ANTHROPIC_DEFAULT_*_MODEL` as the auth-env guard), or (c) read `config.model` directly in `enrichArgs` from the env object. Option (c) is cleanest: `env.CODEMIE_MODEL` should be populated before `exportEnvVars` runs (it is set by `ConfigLoader.exportProviderEnvVars`), so capturing it in `enrichArgs` via `env.CODEMIE_MODEL` before the blank occurs is possible only if `enrichArgs` runs before `exportEnvVars`. Confirming the exact call order of `enrichArgs` vs `exportEnvVars` in the adapter pipeline is the one code-reading task remaining before implementation begins. + +Test coverage posture: the anthropic-subscription provider has meaningful unit tests (`template.test.ts`, `setup-steps.test.ts`, `auth.test.ts`) that cover the existing blanking behavior. The new passthrough behavior must extend `template.test.ts` with an `enrichArgs` test case for the `--model` injection path and a case for deduplication. The version-pin prompt flip has zero test coverage and must be verified manually or by adding a Vitest unit test using `vi.fn()` to mock the inquirer prompt. The `codemie models list` unsupported path also has no test. Total new test additions needed: 2–4 unit test cases and potentially 1 CLI integration test for `models list`. This is low-risk scope. diff --git a/src/agents/core/AgentCLI.ts b/src/agents/core/AgentCLI.ts index e79f8b747..f3d2b2ed5 100644 --- a/src/agents/core/AgentCLI.ts +++ b/src/agents/core/AgentCLI.ts @@ -10,6 +10,7 @@ import { AuthMethod, ProviderName } from '../../providers/core/types.js'; import { JWTTemplate } from '../../providers/plugins/jwt/jwt.template.js'; import { logger } from '../../utils/logger.js'; import { getDirname } from '../../utils/paths.js'; +import { applyCliModelEnv } from './cli-model-env.js'; import { BUILTIN_AGENT_NAME } from '../registry.js'; import { ClaudePluginMetadata } from '../plugins/claude/claude.plugin.js'; import { CodeMieCodePluginMetadata } from '../plugins/codemie-code.plugin.js'; @@ -202,6 +203,12 @@ export class AgentCLI { reasoningEffort: options.reasoningEffort as import('./types.js').CanonicalReasoningEffort | undefined, }); + // Record the explicitly-requested CLI model so the anthropic-subscription + // provider can pass it through to Claude Code. Clears any pre-existing value + // first, so it reflects ONLY this launch's -m/--model, never a stale shell + // value or the stored profile. + applyCliModelEnv(options.model); + // JWT token from CLI overrides everything if (options.jwtToken) { process.env.CODEMIE_JWT_TOKEN = options.jwtToken as string; diff --git a/src/agents/core/BaseAgentAdapter.ts b/src/agents/core/BaseAgentAdapter.ts index d67d720a2..6806f2bd5 100644 --- a/src/agents/core/BaseAgentAdapter.ts +++ b/src/agents/core/BaseAgentAdapter.ts @@ -28,6 +28,8 @@ import { executeAfterRun } from './lifecycle-helpers.js'; import { redactSecrets } from './config-redaction.js'; +import { newerVersionPromptDefault, olderSupportedModelNote } from './version-prompt-policy.js'; +import { resolveLaunchModelDisplay } from './launch-model-display.js'; import { extractGeneratedConfig } from './print-config.js'; import inquirer from 'inquirer'; @@ -393,6 +395,9 @@ export abstract class BaseAgentAdapter implements AgentAdapter { if (this.metadata.supportedVersion) { const compat = await this.checkVersionCompatibility(); + // Provider drives the subscription-scoped softening of the version prompt. + const provider = envOverrides?.CODEMIE_PROVIDER; + // Scenario 0: Version is below minimum supported — hard block, no override if (compat.isBelowMinimum) { const installedDisplay = compat.installedVersion ?? 'unknown'; @@ -462,7 +467,9 @@ export abstract class BaseAgentAdapter implements AgentAdapter { { name: 'Continue with current version', value: 'continue' }, { name: 'Exit', value: 'exit' }, ], - default: 'install', + // Subscription profiles default to keeping the installed newer version + // (never downgrade a binary the user's newer models depend on). + default: newerVersionPromptDefault(provider), }, ]); @@ -488,6 +495,12 @@ export abstract class BaseAgentAdapter implements AgentAdapter { console.log(chalk.white(` Latest version: v${compat.supportedVersion} `) + chalk.green('(recommended)')); console.log(); + const olderNote = olderSupportedModelNote(provider); + if (olderNote) { + console.log(chalk.white(` ${olderNote}`)); + console.log(); + } + const { updateChoice } = await inquirer.prompt([ { type: 'list', @@ -561,7 +574,7 @@ export abstract class BaseAgentAdapter implements AgentAdapter { const profileName = env.CODEMIE_PROFILE_NAME || 'default'; const provider = env.CODEMIE_PROVIDER || 'unknown'; const cliVersion = env.CODEMIE_CLI_VERSION || 'unknown'; - const model = env.CODEMIE_MODEL || 'unknown'; + const model = resolveLaunchModelDisplay(provider, env.CODEMIE_MODEL, process.env.CODEMIE_CLI_MODEL); const codeMieUrl = env.CODEMIE_URL; // Display ASCII logo with configuration diff --git a/src/agents/core/__tests__/cli-model-env.test.ts b/src/agents/core/__tests__/cli-model-env.test.ts new file mode 100644 index 000000000..33774870f --- /dev/null +++ b/src/agents/core/__tests__/cli-model-env.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { applyCliModelEnv } from '../cli-model-env.js'; + +describe('applyCliModelEnv', () => { + beforeEach(() => { delete process.env.CODEMIE_CLI_MODEL; }); + afterEach(() => { delete process.env.CODEMIE_CLI_MODEL; }); + + it('sets CODEMIE_CLI_MODEL to the trimmed explicit model', () => { + applyCliModelEnv(' claude-opus-4-5 '); + expect(process.env.CODEMIE_CLI_MODEL).toBe('claude-opus-4-5'); + }); + + it('leaves CODEMIE_CLI_MODEL unset when no model is given', () => { + applyCliModelEnv(undefined); + expect(process.env.CODEMIE_CLI_MODEL).toBeUndefined(); + }); + + it('CLEARS a pre-existing (e.g. shell-exported) value when no model is given', () => { + process.env.CODEMIE_CLI_MODEL = 'stale-from-shell'; + applyCliModelEnv(undefined); + expect(process.env.CODEMIE_CLI_MODEL).toBeUndefined(); + }); + + it('clears a pre-existing value before applying an empty/whitespace model', () => { + process.env.CODEMIE_CLI_MODEL = 'stale-from-shell'; + applyCliModelEnv(' '); + expect(process.env.CODEMIE_CLI_MODEL).toBeUndefined(); + }); + + it('ignores non-string input', () => { + applyCliModelEnv(42); + expect(process.env.CODEMIE_CLI_MODEL).toBeUndefined(); + }); +}); diff --git a/src/agents/core/__tests__/launch-model-display.test.ts b/src/agents/core/__tests__/launch-model-display.test.ts new file mode 100644 index 000000000..c0cafd874 --- /dev/null +++ b/src/agents/core/__tests__/launch-model-display.test.ts @@ -0,0 +1,19 @@ +import { describe, it, expect } from 'vitest'; +import { resolveLaunchModelDisplay } from '../launch-model-display.js'; + +describe('resolveLaunchModelDisplay', () => { + it('shows the explicit CLI model on the subscription profile', () => { + expect(resolveLaunchModelDisplay('anthropic-subscription', '', 'claude-opus-4-5')).toBe('claude-opus-4-5'); + }); + + it('shows a per-session phrase (not "unknown") when no CLI model on subscription', () => { + const s = resolveLaunchModelDisplay('anthropic-subscription', '', undefined); + expect(s).not.toBe('unknown'); + expect(s).toMatch(/Claude Code/i); + }); + + it('is unchanged for non-subscription providers', () => { + expect(resolveLaunchModelDisplay('litellm', 'gpt-5.5', undefined)).toBe('gpt-5.5'); + expect(resolveLaunchModelDisplay('litellm', '', undefined)).toBe('unknown'); + }); +}); diff --git a/src/agents/core/__tests__/version-prompt-policy.test.ts b/src/agents/core/__tests__/version-prompt-policy.test.ts new file mode 100644 index 000000000..a34eb8324 --- /dev/null +++ b/src/agents/core/__tests__/version-prompt-policy.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect } from 'vitest'; +import { newerVersionPromptDefault, olderSupportedModelNote } from '../version-prompt-policy.js'; + +describe('version-prompt-policy', () => { + it('defaults the newer-than-pinned prompt to continue for anthropic-subscription', () => { + expect(newerVersionPromptDefault('anthropic-subscription')).toBe('continue'); + }); + + it('keeps install as the default for proxied providers and when unknown', () => { + expect(newerVersionPromptDefault('ai-run-sso')).toBe('install'); + expect(newerVersionPromptDefault('litellm')).toBe('install'); + expect(newerVersionPromptDefault(undefined)).toBe('install'); + }); + + it('returns the older-but-supported note only for anthropic-subscription', () => { + expect(olderSupportedModelNote('anthropic-subscription')).toMatch(/newer models/i); + expect(olderSupportedModelNote('litellm')).toBeNull(); + expect(olderSupportedModelNote(undefined)).toBeNull(); + }); +}); diff --git a/src/agents/core/cli-model-env.ts b/src/agents/core/cli-model-env.ts new file mode 100644 index 000000000..562f9b691 --- /dev/null +++ b/src/agents/core/cli-model-env.ts @@ -0,0 +1,15 @@ +/** + * Publish the explicitly-requested CLI model for this launch as CODEMIE_CLI_MODEL, + * the signal the anthropic-subscription provider passes through to Claude Code. + * + * Always clears any pre-existing value first (e.g. one exported in the user's shell) + * so the var reflects ONLY what the user passed on THIS launch — never a stale value. + * With no explicit model the var is left unset, so a no---model launch runs on Claude + * Code's own default. + */ +export function applyCliModelEnv(model: unknown): void { + delete process.env.CODEMIE_CLI_MODEL; + if (typeof model === 'string' && model.trim() !== '') { + process.env.CODEMIE_CLI_MODEL = model.trim(); + } +} diff --git a/src/agents/core/launch-model-display.ts b/src/agents/core/launch-model-display.ts new file mode 100644 index 000000000..1b3f5926d --- /dev/null +++ b/src/agents/core/launch-model-display.ts @@ -0,0 +1,20 @@ +import { ProviderName } from '../../providers/core/types.js'; + +const SUBSCRIPTION_PER_SESSION = 'chosen per session by Claude Code / your Anthropic subscription'; + +/** + * What the launch banner prints as the model. On the subscription profile the model + * is either the explicit --model the user passed (CODEMIE_CLI_MODEL) or, absent that, + * Claude Code's own per-session choice — never the blanked CODEMIE_MODEL, which would + * otherwise render as 'unknown'. Other providers keep their existing behavior. + */ +export function resolveLaunchModelDisplay( + provider: string | undefined, + envModel: string | undefined, + cliModel: string | undefined, +): string { + if (provider === ProviderName.ANTHROPIC_SUBSCRIPTION) { + return cliModel && cliModel.trim() !== '' ? cliModel : SUBSCRIPTION_PER_SESSION; + } + return envModel || 'unknown'; +} diff --git a/src/agents/core/version-prompt-policy.ts b/src/agents/core/version-prompt-policy.ts new file mode 100644 index 000000000..196f39922 --- /dev/null +++ b/src/agents/core/version-prompt-policy.ts @@ -0,0 +1,23 @@ +import { ProviderName } from '../../providers/core/types.js'; + +/** + * A pin that would downgrade an already-installed newer binary is softened to a + * warning that defaults to keeping what is installed — but only on the Anthropic + * Subscription profile, whose model availability comes from the installed Claude + * Code version. Proxied providers keep 'install' as the tested default (a newer, + * unverified binary can break the CodeMie proxy). The minimum-version block is a + * separate branch and is unaffected. + */ +export function newerVersionPromptDefault(provider: string | undefined): 'install' | 'continue' { + return provider === ProviderName.ANTHROPIC_SUBSCRIPTION ? 'continue' : 'install'; +} + +/** + * On an older-but-supported Claude Code, tell subscription users that newer models + * may be unavailable on that version (the update to the verified version is already + * offered by the prompt). Returns null for providers this story does not touch. + */ +export function olderSupportedModelNote(provider: string | undefined): string | null { + if (provider !== ProviderName.ANTHROPIC_SUBSCRIPTION) return null; + return 'Newer models may be unavailable on this version of Claude Code.'; +} diff --git a/src/cli/commands/__tests__/models-subscription-message.test.ts b/src/cli/commands/__tests__/models-subscription-message.test.ts new file mode 100644 index 000000000..5463f7f2d --- /dev/null +++ b/src/cli/commands/__tests__/models-subscription-message.test.ts @@ -0,0 +1,11 @@ +import { describe, it, expect } from 'vitest'; +import { subscriptionModelsListMessage } from '../models.js'; + +describe('subscriptionModelsListMessage', () => { + it('explains models come from the Anthropic subscription and how to switch in-session', () => { + const msg = subscriptionModelsListMessage(); + expect(msg).toMatch(/Anthropic subscription/i); + expect(msg).toMatch(/\/model/); + expect(msg).not.toMatch(/not supported/i); + }); +}); diff --git a/src/cli/commands/models.ts b/src/cli/commands/models.ts index fce33f504..56a53fba6 100644 --- a/src/cli/commands/models.ts +++ b/src/cli/commands/models.ts @@ -4,9 +4,23 @@ import { ConfigLoader } from '../../utils/config.js'; import { ProviderRegistry } from '../../providers/core/registry.js'; import { logger } from '../../utils/logger.js'; import type { ModelInfo } from '../../providers/core/types.js'; +import { ProviderName } from '../../providers/core/types.js'; const UNSUPPORTED_PROVIDERS = new Set(['openai', 'openai-compatible']); +/** + * Anthropic Subscription has no CodeMie model catalog — models come from the user's + * Anthropic subscription and the installed Claude Code version. `models list` explains + * that instead of erroring, and points the user at Claude Code's in-session /model. + */ +export function subscriptionModelsListMessage(): string { + return [ + 'Models for this profile come from your Anthropic subscription and the installed Claude Code version.', + 'CodeMie does not maintain a model list for it.', + 'Run codemie-claude and use /model inside Claude Code to see or change the model for a session.', + ].join('\n'); +} + function formatTable(models: ModelInfo[]): void { const ID_WIDTH = 40; const NAME_WIDTH = 35; @@ -65,6 +79,11 @@ export function createModelsCommand(): Command { process.exit(1); } + if (provider === ProviderName.ANTHROPIC_SUBSCRIPTION) { + console.log(subscriptionModelsListMessage()); + return; // exit 0 — this is informational, not an error + } + if (UNSUPPORTED_PROVIDERS.has(provider)) { console.error(chalk.red(`Model listing is not supported for provider '${provider}'.`)); process.exit(1); diff --git a/src/cli/commands/setup.ts b/src/cli/commands/setup.ts index aab79357b..c7e29eecb 100644 --- a/src/cli/commands/setup.ts +++ b/src/cli/commands/setup.ts @@ -423,15 +423,22 @@ async function handlePluginSetup( // Step 3: Model selection let selectedModel: string; - const preselectedModel = setupSteps.selectModel - ? await setupSteps.selectModel(credentials, models, providerTemplate) - : undefined; - - if (preselectedModel) { - selectedModel = preselectedModel; - logger.success(`Model selected automatically: ${selectedModel}`); + if (providerName === ProviderName.ANTHROPIC_SUBSCRIPTION) { + // Model is chosen per session by Claude Code + the user's Anthropic + // subscription; storing one here would never take effect (exportEnvVars + // blanks it) and would later be shown as a stale value. + selectedModel = ''; } else { - selectedModel = await promptForModelSelection(models, providerTemplate); + const preselectedModel = setupSteps.selectModel + ? await setupSteps.selectModel(credentials, models, providerTemplate) + : undefined; + + if (preselectedModel) { + selectedModel = preselectedModel; + logger.success(`Model selected automatically: ${selectedModel}`); + } else { + selectedModel = await promptForModelSelection(models, providerTemplate); + } } // Step 3.5: Install model if provider supports it (e.g., Ollama) diff --git a/src/providers/integration/__tests__/setup-success-model-line.test.ts b/src/providers/integration/__tests__/setup-success-model-line.test.ts new file mode 100644 index 000000000..589c9a541 --- /dev/null +++ b/src/providers/integration/__tests__/setup-success-model-line.test.ts @@ -0,0 +1,14 @@ +import { describe, it, expect } from 'vitest'; +import { setupModelSummaryLine } from '../setup-ui.js'; + +describe('setupModelSummaryLine', () => { + it('states per-session choice for the subscription provider (no stored model name)', () => { + const line = setupModelSummaryLine('anthropic-subscription', ''); + expect(line).toMatch(/per session/i); + expect(line).toMatch(/Claude Code/i); + }); + + it('shows the model for other providers', () => { + expect(setupModelSummaryLine('litellm', 'gpt-5.5')).toContain('gpt-5.5'); + }); +}); diff --git a/src/providers/integration/setup-ui.ts b/src/providers/integration/setup-ui.ts index 2e2ffd229..a6712607d 100644 --- a/src/providers/integration/setup-ui.ts +++ b/src/providers/integration/setup-ui.ts @@ -258,6 +258,18 @@ export function getAllModelChoices( return sortedModels.map(model => formatModelChoice(model, template, recommendedIds.has(model))); } +/** + * The setup-summary model line. On the Anthropic Subscription profile there is no + * stored model to show — the model is chosen per session by Claude Code and the + * user's Anthropic subscription — so state that instead of an (empty) model name. + */ +export function setupModelSummaryLine(provider: string, model: string): string { + if (provider === 'anthropic-subscription') { + return '🤖 Model: chosen per session by Claude Code and your Anthropic subscription'; + } + return `🤖 Model: ${model}`; +} + /** * Display success message * @@ -270,7 +282,7 @@ export function displaySetupSuccess( ): void { console.log(chalk.bold.green(`\n✅ Profile "${profileName}" configured successfully!\n`)); console.log(chalk.cyan(`🔗 Provider: ${provider}`)); - console.log(chalk.cyan(`🤖 Model: ${model}`)); + console.log(chalk.cyan(setupModelSummaryLine(provider, model))); console.log(chalk.cyan(`📁 Config: ~/.codemie/codemie-cli.config.json\n`)); console.log(chalk.bold(' Next Steps:')); diff --git a/src/providers/plugins/anthropic-subscription/__tests__/anthropic-subscription.setup-steps.test.ts b/src/providers/plugins/anthropic-subscription/__tests__/anthropic-subscription.setup-steps.test.ts index 10aa63bfc..ae267c5da 100644 --- a/src/providers/plugins/anthropic-subscription/__tests__/anthropic-subscription.setup-steps.test.ts +++ b/src/providers/plugins/anthropic-subscription/__tests__/anthropic-subscription.setup-steps.test.ts @@ -93,5 +93,15 @@ describe('AnthropicSubscriptionSetupSteps', () => { expect(config.baseUrl).toBe(AnthropicSubscriptionTemplate.defaultBaseUrl); }); + + it('stores no model when setup passes an empty model (chosen per session by Claude Code)', () => { + const config = AnthropicSubscriptionSetupSteps.buildConfig( + { baseUrl: 'https://api.anthropic.com', apiKey: '', additionalConfig: { authMethod: 'manual' } }, + '' + ); + + expect(config.model).toBeUndefined(); + expect(config.provider).toBe('anthropic-subscription'); + }); }); }); diff --git a/src/providers/plugins/anthropic-subscription/__tests__/anthropic-subscription.template.test.ts b/src/providers/plugins/anthropic-subscription/__tests__/anthropic-subscription.template.test.ts index 92f7c1ade..c774654a8 100644 --- a/src/providers/plugins/anthropic-subscription/__tests__/anthropic-subscription.template.test.ts +++ b/src/providers/plugins/anthropic-subscription/__tests__/anthropic-subscription.template.test.ts @@ -190,6 +190,51 @@ describe('AnthropicSubscriptionTemplate', () => { }); }); + describe('agentHooks - enrichArgs (claude) --model passthrough', () => { + beforeEach(() => { + delete process.env.CODEMIE_CLI_MODEL; + delete process.env.CODEMIE_CLAUDE_EXTENSION_DIR; + }); + afterEach(() => { + delete process.env.CODEMIE_CLI_MODEL; + delete process.env.CODEMIE_CLAUDE_EXTENSION_DIR; + }); + + const enrich = (args: string[]): string[] => { + const hook = AnthropicSubscriptionTemplate.agentHooks?.['claude']; + return hook!.enrichArgs!(args, { agent: 'claude' }); + }; + + it('injects --model when CODEMIE_CLI_MODEL is set', () => { + process.env.CODEMIE_CLI_MODEL = 'claude-opus-4-5'; + expect(enrich(['--task', 'hi'])).toEqual(['--model', 'claude-opus-4-5', '--task', 'hi']); + }); + + it('injects nothing when CODEMIE_CLI_MODEL is unset', () => { + expect(enrich(['--task', 'hi'])).toEqual(['--task', 'hi']); + }); + + it('does not double-inject when --model is already present', () => { + process.env.CODEMIE_CLI_MODEL = 'claude-opus-4-5'; + expect(enrich(['--model', 'claude-haiku-4-5'])).toEqual(['--model', 'claude-haiku-4-5']); + }); + + it('does not double-inject when --model=value (equals form) is already present', () => { + // Reachable only via raw passthrough after `--` (e.g. `-m X -- --model=Y`); the + // guard must still recognise the equals form so the binary never gets two --model. + process.env.CODEMIE_CLI_MODEL = 'claude-opus-4-5'; + expect(enrich(['--model=claude-haiku-4-5'])).toEqual(['--model=claude-haiku-4-5']); + }); + + it('composes with the --plugin-dir injection (plugin-dir first, then model)', () => { + process.env.CODEMIE_CLI_MODEL = 'claude-opus-4-5'; + process.env.CODEMIE_CLAUDE_EXTENSION_DIR = '/ext'; + expect(enrich(['--task', 'hi'])).toEqual([ + '--plugin-dir', '/ext', '--model', 'claude-opus-4-5', '--task', 'hi' + ]); + }); + }); + describe('exportEnvVars', () => { it('always exports CODEMIE_API_KEY as empty string', () => { const env = AnthropicSubscriptionTemplate.exportEnvVars!({} as any); diff --git a/src/providers/plugins/anthropic-subscription/anthropic-subscription.setup-steps.ts b/src/providers/plugins/anthropic-subscription/anthropic-subscription.setup-steps.ts index 084fc5e6a..a5df55d46 100644 --- a/src/providers/plugins/anthropic-subscription/anthropic-subscription.setup-steps.ts +++ b/src/providers/plugins/anthropic-subscription/anthropic-subscription.setup-steps.ts @@ -123,7 +123,10 @@ export const AnthropicSubscriptionSetupSteps: ProviderSetupSteps = { provider: 'anthropic-subscription', baseUrl: credentials.baseUrl || AnthropicSubscriptionTemplate.defaultBaseUrl, apiKey: '', - model: selectedModel, + // This provider stores no model — it is chosen per session by Claude Code and + // the user's Anthropic subscription. Omit the field entirely rather than persist + // an empty string when setup passes none. + ...(selectedModel ? { model: selectedModel } : {}), authMethod: 'manual', codeMieUrl: credentials.additionalConfig?.codeMieUrl as string | undefined, codeMieProject: credentials.additionalConfig?.codeMieProject as string | undefined, diff --git a/src/providers/plugins/anthropic-subscription/anthropic-subscription.template.ts b/src/providers/plugins/anthropic-subscription/anthropic-subscription.template.ts index 2664e7ee7..a60ac45c8 100644 --- a/src/providers/plugins/anthropic-subscription/anthropic-subscription.template.ts +++ b/src/providers/plugins/anthropic-subscription/anthropic-subscription.template.ts @@ -94,13 +94,24 @@ export const AnthropicSubscriptionTemplate = registerProvider( }, 'claude': { enrichArgs(args: string[], _config: AgentConfig): string[] { - const pluginDir = process.env.CODEMIE_CLAUDE_EXTENSION_DIR; + let result = args; + + // Carry the explicit CLI --model straight through to the claude binary. + // Sourced from CODEMIE_CLI_MODEL (set by AgentCLI only when the user passed + // -m/--model this launch) — never from the stored profile, so a pre-existing + // profile's stale model is ignored. Claude Code owns entitlement/refusal. + const cliModel = process.env.CODEMIE_CLI_MODEL; + const hasModelFlag = result.some(arg => arg === '--model' || arg.startsWith('--model=')); + if (cliModel && !hasModelFlag) { + result = ['--model', cliModel, ...result]; + } - if (!pluginDir || args.some(arg => arg === '--plugin-dir')) { - return args; + const pluginDir = process.env.CODEMIE_CLAUDE_EXTENSION_DIR; + if (pluginDir && !result.some(arg => arg === '--plugin-dir')) { + result = ['--plugin-dir', pluginDir, ...result]; } - return ['--plugin-dir', pluginDir, ...args]; + return result; } } }, diff --git a/src/providers/plugins/moonshot-subscription/__tests__/moonshot-subscription.template.test.ts b/src/providers/plugins/moonshot-subscription/__tests__/moonshot-subscription.template.test.ts index 30d6899e7..e142ce4aa 100644 --- a/src/providers/plugins/moonshot-subscription/__tests__/moonshot-subscription.template.test.ts +++ b/src/providers/plugins/moonshot-subscription/__tests__/moonshot-subscription.template.test.ts @@ -138,3 +138,26 @@ describe('MoonshotSubscriptionTemplate', () => { }); }); }); + +describe('MoonshotSubscriptionTemplate - CODEMIE_CLI_MODEL isolation', () => { + // Regression guard: the --model passthrough is anthropic-subscription only. + // Moonshot pairs with the Kimi launcher and must never inject --model from + // CODEMIE_CLI_MODEL, even if it gains an enrichArgs hook in the future. + beforeEach(() => { process.env.CODEMIE_CLI_MODEL = 'claude-opus-4-5'; }); + afterEach(() => { delete process.env.CODEMIE_CLI_MODEL; }); + + it('wires no claude enrichArgs and never injects --model from CODEMIE_CLI_MODEL', () => { + expect(MoonshotSubscriptionTemplate.agentHooks?.['claude']?.enrichArgs).toBeUndefined(); + + const hooks = (MoonshotSubscriptionTemplate.agentHooks ?? {}) as Record< + string, + { enrichArgs?: (args: string[], config: { agent: string }) => string[] } + >; + for (const key of Object.keys(hooks)) { + const enrichArgs = hooks[key]?.enrichArgs; + if (enrichArgs) { + expect(enrichArgs(['--task', 'hi'], { agent: 'kimi' })).not.toContain('--model'); + } + } + }); +});