Skip to content

[#1018] Add Claude Opus 5 as a pinned selectable model#1019

Merged
realproject7 merged 1 commit into
mainfrom
task/1018-claude-opus-5
Jul 25, 2026
Merged

[#1018] Add Claude Opus 5 as a pinned selectable model#1019
realproject7 merged 1 commit into
mainfrom
task/1018-claude-opus-5

Conversation

@realproject7

Copy link
Copy Markdown
Owner

Closes #1018.

Adds claude-opus-5 to the shared Claude model catalog as an explicit pin, so operators who need a reproducible version can select it instead of relying on the moving opus alias. Pure data addition + tests — no logic, spawn, or UI changes.

EPIC Alignment

Changes (3 files, +31/−0)

  • src/lib/agentModels.ts — one row, { value: "claude-opus-5", label: "claude-opus-5" }, placed immediately after claude-fable-5 and before the 4.x pins per the ticket's deterministic-placement requirement (newest pin first, never first).
  • server/agentModels.test.js — catalog membership; effectiveModel/sanitizeModel pin preservation; a placement block pinning the position and that opus stays index 0; and a normalizeButler({command:"claude", model:"claude-opus-5"}) regression.
  • server/agentArgs.gemini.test.js — a claude_opus5_agent fixture asserting --model claude-opus-5 plus retention of --dangerously-skip-permissions and --mcp-config <path>.

No production logic changed: buildAgentArgs()'s Claude branch already forwards any non-empty slug as --model <slug>, and PUT /api/project/:projectId/agent-models/:agentId stores model as a trimmed string with no allowlist (only reasoning_effort is validated). Both selection surfaces are catalog-driven — AgentModelsWidget.tsx via optionsForBackend, SettingsPage.tsx via modelsForBackend — so the dashboard modal, the per-agent Settings dropdown, and Butler all pick the row up with no UI work.

Self-Verification

  • npm test64 passed, 0 failed, 2 skipped (the two skips are the pre-existing Jest-style files, out of Unref routes.js module-load pollers + add cross-platform 'npm test' (fix full-suite hang) #836 scope).
  • npm run buildsucceeds (TypeScript clean, 9/9 static pages generated).
  • Targeted: node server/agentModels.test.js → 35 passed / 0 failed; node server/agentArgs.gemini.test.js → 12 passed / 0 failed.
  • Acceptance criteria walked individually:
    • Both surfaces offer the pin — catalog-driven, asserted via optionsForBackend("claude").
    • Persistence — route stores the value verbatim; sanitizeModel only rewrites values absent from the catalog, so adding a row strictly widens the accepted set and no saved model is rewritten by loading or saving unrelated settings.
    • Launch — asserted --model claude-opus-5 with permission + MCP args intact.
    • opus (latest) still present and still first concrete option — asserted (and the pre-existing guard at server/agentModels.test.js remains).
    • Butler normalization preserves the pin — asserted via the normalizeButler mirror.
    • Codex/Gemini untouched — no diff outside the Claude array; existing codex/gemini assertions still pass.
  • Baseline: branched from main @ caf94e2 (the ticket's corrected baseline).

Review notes

Reviewed as a ticket first: @re1, @re2, and @dev all approved #1018's spec at caf94e2, and @Head amended the ticket with the four review nits (placement, corrected baseline, the normalizeButler case, the permission/MCP assertion) — all four are implemented above. Butler deserves a second look: its surface is React-only, so the normalizeButler mirror in server/agentModels.test.js is the sole regression guard for that acceptance criterion.

🤖 Generated with Claude Code

Append `claude-opus-5` to MODEL_OPTIONS.claude immediately after
claude-fable-5 and before the 4.x pins (newest pin first, never first),
so modelsForBackend("claude")[0] stays "opus" — the #931 default/heal
target. Pure data addition: buildAgentArgs already forwards any concrete
Claude slug as `--model <slug>`, and the PUT agent-models route stores
the value without an allowlist, so no logic changes.

Coverage: catalog membership, effectiveModel/sanitizeModel pin
preservation, deterministic placement, the #935 normalizeButler mirror
(Butler's only regression guard, since that surface is React-only), and
a launch case asserting `--model claude-opus-5` alongside the permission
and --mcp-config arguments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@project7-interns project7-interns left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE

Epic Alignment: PASS

Standalone #1018 is aligned with its contract: one shared Claude-catalog row, opus remains the first concrete default/heal target, and saved valid models remain valid.

Checked (evidence)

  • Structural gate: live PR body includes filled ## EPIC Alignment and ## Self-Verification; linked amended ticket #1018 has standalone ## EPIC Context and its deterministic placement/test contracts.
  • Catalog and placement: src/lib/agentModels.ts:35-41 inserts the pin immediately after Fable 5, before 4.x pins, preserving opus at :29.
  • Persistence and Butler regression: server/agentModels.test.js:36,52,59-61,70,89 covers catalog exposure, valid display/save, position/default, and Butler preservation.
  • Riskiest part: Claude launch arguments retain permission and MCP wiring; server/agentArgs.gemini.test.js:91-99 verifies the exact pin plus both arguments.
  • Kill-list: scanned all items — clean; 3 changed files (+31/-0), no runtime logic change or scope creep.
  • CI: gh pr checks 1019test pass 1m7s (live).

Findings

  • None.

Decision

The live diff fully implements #1018 without altering the default alias, other catalogs, or launch logic. Approval applies to ef8715c2b22793ff6af28965e62c6a97ed4618f8.

@realproject7

Copy link
Copy Markdown
Owner Author

@re2 APPROVE at ef8715c2b22793ff6af28965e62c6a97ed4618f8

3 files, +31/−0, base main, MERGEABLE, test pass 1m7s. Implements the amended #1018 exactly: one catalog row plus the coverage the ticket requires, no production logic touched.

@dev asked me to check the no-silent-migration reasoning independently rather than take it on trust. I did — empirically, not just by reading sanitizeModel. At this SHA every previously-saved Claude value round-trips unchanged:

opus→opus  sonnet→sonnet  claude-fable-5→claude-fable-5
claude-opus-4-8→claude-opus-4-8  claude-opus-4-7→claude-opus-4-7  claude-opus-4-6→claude-opus-4-6
claude-sonnet-4-6→claude-sonnet-4-6  claude-haiku-4-5-20251001→claude-haiku-4-5-20251001
""→""   undefined→""

The reasoning holds and is stronger than stated: sanitizeModel heals only values absent from the catalog (src/lib/agentModels.ts:78-85), so adding a row strictly widens the accepted set — it can never newly rewrite a saved value, only stop rewriting one. Cross-backend healing is untouched (sanitizeModel("codex","sonnet")gpt-5.4, sanitizeModel("gemini","opus")gemini-2.5-pro).

On Butler: agreed that normalizeButler is the only regression guard, and the added case is the right shape. Worth recording that the guard is a mirror of the React save path, not the path itself — it pins the contract, not the component. That's the correct trade for this ticket (the alternative is a component test harness this repo doesn't have), but it means a future refactor of SettingsPage's butler save could drift from the mirror without failing. Not a change request here.

Checked (evidence)

Placement + ordering (the #931 invariant). Ran modelsForBackend("claude") at this SHA → opus, sonnet, claude-fable-5, claude-opus-5, claude-opus-4-8, claude-opus-4-7, claude-opus-4-6, claude-sonnet-4-6, claude-haiku-4-5-20251001. The pin sits immediately after claude-fable-5 and before the 4.x pins, exactly as the amended ticket specifies, and [0] is still opus. codex[0]gpt-5.4 and gemini[0]gemini-2.5-pro unchanged. Row is label == value, no duplicate slugs (src/lib/agentModels.ts:36).

Launch path. server/agentArgs.gemini.test.js:91-100 — new claude_opus5_agent fixture asserts --model claude-opus-5 plus retention of --dangerously-skip-permissions and --mcp-config <path>.json. Verified this needs no production change: the Claude branch of buildAgentArgs() (server/index.js:559-562) forwards any non-empty string.

Both selection surfaces are catalog-driven, so no UI work was needed or done. src/components/AgentModelsWidget.tsx:248 renders optionsForBackend(row.backend); src/components/SettingsPage.tsx:1254 (per-agent) and :1092 (Butler) render modelsForBackend(...). Confirmed no positional consumer exists — grepped src/ + server/ for indexed access into either helper and found only [0] inside agentModels.ts itself and comments.

Tests at this SHA, in a clean worktree: server/agentModels.test.js → 35 passed, 0 failed; server/agentArgs.gemini.test.js → 12 passed, 0 failed; full node server/run-tests.js → 64 passed, 0 failed, 2 skipped (the pre-existing #836 Jest skips, unchanged); npx tsc --noEmit → clean; npm run build → succeeds, 9/9 pages.

All four ticket-phase nits are present: deterministic placement (pinned by an explicit assertion block, server/agentModels.test.js:55-62), the normalizeButler regression (:89), the permission + MCP assertion on the launch case, and the corrected caf94e2 baseline.

No concerns. Head unmoved at ef8715c2 and CI green as of this comment.

@realproject7
realproject7 merged commit 7c4830e into main Jul 25, 2026
1 check passed
@realproject7 realproject7 mentioned this pull request Jul 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Claude Opus 5 as a pinned selectable model

2 participants