diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b265fb5..5e063023 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Changed -- **The `opus` alias upgrades from Claude Opus 4.6 to Claude Opus 4.8.** `MODEL_MAP["opus"]` now resolves to `claude-opus-4-8` — the newest Opus tier, at the same price as 4.6 — so every Opus-routed role (PR reviewer, Auditor, Board, CEO, Intake, Secretary) rides it at next spawn. Pricing already covered the id via the `claude-opus-4` fragment; a new `test_opus_is_priced` guard keeps the alias from ever pointing at an unpriced model. +- **The `opus` alias upgrades from Claude Opus 4.6 to Claude Opus 5.** `MODEL_MAP["opus"]` now resolves to `claude-opus-5` — the newest Opus tier, released 2026-07-25 at the same $5/$25 price — so every Opus-routed role (PR reviewer, Auditor, Board, CEO, Intake, Secretary) rides it at next spawn. The pricing table gains a dedicated `claude-opus-5` fragment (the `claude-opus-4` substring doesn't cover it — without the row, Opus fleet usage would silently cost-track as $0), and a new `test_opus_is_priced` guard keeps the alias from ever pointing at an unpriced model. - **Panel dialogs converge on one shape: one disclosure primitive, DialogFooter everywhere, three dialog widths.** `collapsible-section` becomes the single sectioned-disclosure primitive — task dialogs' raw `Collapsible` and create-project's ad-hoc `showAdvanced` both move onto it; every hand-rolled dialog footer becomes `DialogFooter`; dialog widths collapse from ten ad-hoc classes to three named sizes, with deliberate outliers annotated. No behavioral change. ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index 0f6b0c9b..7618e8f9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,7 +79,7 @@ pnpm test | RAG Engine | in-house (asyncpg + pgvector, hybrid retrieval) | | Cache/Queue | Redis | | Container Runtime | Docker + Docker Compose | -| Cloud LLM | Claude API (claude-opus-4-8) + xAI Grok (official `grok` CLI, SuperGrok subscription) + OpenAI (official `codex` CLI, ChatGPT subscription) + Google Gemini (official `gemini` CLI, OAuth login) | +| Cloud LLM | Claude API (claude-opus-5) + xAI Grok (official `grok` CLI, SuperGrok subscription) + OpenAI (official `codex` CLI, ChatGPT subscription) + Google Gemini (official `gemini` CLI, OAuth login) | | Local LLM | Ollama (glm-5.2:cloud for RAG/hybrid retrieval) | | Embeddings | qwen3-embedding:0.6b (1024 dim) | | Frontend | Next.js 16 + TypeScript + Tailwind + Radix UI (in `panel/`) | diff --git a/docs/map/_complete_map.md b/docs/map/_complete_map.md index f9991c5c..b13aefe8 100644 --- a/docs/map/_complete_map.md +++ b/docs/map/_complete_map.md @@ -5134,7 +5134,7 @@ The Pydantic/dataclass domain surface of RoboCo — the typed contract the API, | `OrchestratorAgentState` | StrEnum | runtime.py:15 | offline/starting/active/waiting_short/waiting_long/idle/stopping | | `AgentInstance` | dataclass | runtime.py:70 | Running Claude Code container record + `usage_session_id` | | `SpawnGitContext` | dataclass | runtime.py:28 | Git context for spawn (project_slug, branch, `task_short_id` for worktree) | -| `MODEL_MAP` | dict | runtime.py:106 | short-name→full Claude id (opus→claude-opus-4-8, sonnet→claude-sonnet-5, haiku→…) | +| `MODEL_MAP` | dict | runtime.py:106 | short-name→full Claude id (opus→claude-opus-5, sonnet→claude-sonnet-5, haiku→…) | | `ROLE_MODEL_MAP` | dict | runtime.py:120 | per-role default tier: developer/qa/documenter/cell_pm/main_pm→sonnet, pr_reviewer/auditor/board/ceo/prompter/secretary→opus. **Haiku retired from every delivery-lifecycle role (#680, 2026-07-24)**: qa/documenter moved haiku→sonnet — haiku can't reliably emit the structured envelopes the lifecycle runs on (`pass_review`'s per-AC `criteria_verified`, `delegate`'s `covers_parent_criteria`, the findings ledger), so a haiku QA/PM claims, gets validation-rejected, idles, respawns, and loops without progress. A structured-verb capability floor (`_below_capability_floor`/`_floor_below_capability`, `roboco/services/llm.py`) additionally upgrades ANY below-floor Anthropic assignment to sonnet at resolution time — from a pin, a plain ROLE row, or a future map edit — so the floor can't be silently reintroduced. Non-Anthropic providers are untouched (an Anthropic-tier floor, not a provider policy). | | `ROLE_EFFORT_MAP` | dict | runtime.py | per-role `CLAUDE_CODE_EFFORT_LEVEL` override injected at spawn; **empty/inert by default** (opt-in per role after verifying the level moves usage) | | `MODEL_CATALOG` | tuple | llm_catalog.py:67 | Settings-dropdown source of truth; Anthropic entries derived from `MODEL_MAP` | diff --git a/docs/map/models.md b/docs/map/models.md index 068f2d40..8a588004 100644 --- a/docs/map/models.md +++ b/docs/map/models.md @@ -83,7 +83,7 @@ The Pydantic/dataclass domain surface of RoboCo — the typed contract the API, | `OrchestratorAgentState` | StrEnum | runtime.py:15 | offline/starting/active/waiting_short/waiting_long/idle/stopping | | `AgentInstance` | dataclass | runtime.py:70 | Running Claude Code container record + `usage_session_id` | | `SpawnGitContext` | dataclass | runtime.py:28 | Git context for spawn (project_slug, branch, `task_short_id` for worktree) | -| `MODEL_MAP` | dict | runtime.py:106 | short-name→full Claude id (opus→claude-opus-4-8, sonnet→claude-sonnet-5, haiku→…) | +| `MODEL_MAP` | dict | runtime.py:106 | short-name→full Claude id (opus→claude-opus-5, sonnet→claude-sonnet-5, haiku→…) | | `ROLE_MODEL_MAP` | dict | runtime.py:120 | per-role default tier: developer/qa/documenter/cell_pm/main_pm→sonnet, pr_reviewer/auditor/board/ceo/prompter/secretary→opus. **Haiku retired from every delivery-lifecycle role (#680, 2026-07-24)**: qa/documenter moved haiku→sonnet — haiku can't reliably emit the structured envelopes the lifecycle runs on (`pass_review`'s per-AC `criteria_verified`, `delegate`'s `covers_parent_criteria`, the findings ledger), so a haiku QA/PM claims, gets validation-rejected, idles, respawns, and loops without progress. A structured-verb capability floor (`_below_capability_floor`/`_floor_below_capability`, `roboco/services/llm.py`) additionally upgrades ANY below-floor Anthropic assignment to sonnet at resolution time — from a pin, a plain ROLE row, or a future map edit — so the floor can't be silently reintroduced. Non-Anthropic providers are untouched (an Anthropic-tier floor, not a provider policy). | | `ROLE_EFFORT_MAP` | dict | runtime.py | per-role `CLAUDE_CODE_EFFORT_LEVEL` override injected at spawn; **empty/inert by default** (opt-in per role after verifying the level moves usage) | | `MODEL_CATALOG` | tuple | llm_catalog.py:67 | Settings-dropdown source of truth; Anthropic entries derived from `MODEL_MAP` | diff --git a/panel/src/components/settings/__tests__/ai-routing-card.test.tsx b/panel/src/components/settings/__tests__/ai-routing-card.test.tsx index e8c8919d..4dc5a254 100644 --- a/panel/src/components/settings/__tests__/ai-routing-card.test.tsx +++ b/panel/src/components/settings/__tests__/ai-routing-card.test.tsx @@ -33,9 +33,9 @@ const { } = vi.hoisted(() => ({ catalog: vi.fn(async () => [ { - model_name: "claude-opus-4-8", + model_name: "claude-opus-5", provider_type: "anthropic", - display_name: "Claude Opus 4.8", + display_name: "Claude Opus 5", }, { model_name: "grok-build-0.1", @@ -719,14 +719,14 @@ describe("AIRoutingCard", () => { const devLow = screen.getByTestId("complexity-select-developer-low"); fireEvent.click( - await within(devLow).findByRole("option", { name: "Claude Opus 4.8" }), + await within(devLow).findByRole("option", { name: "Claude Opus 5" }), ); await waitFor(() => expect(setComplexityOverride).toHaveBeenCalledWith({ role: "developer", complexity: "low", - model_name: "claude-opus-4-8", + model_name: "claude-opus-5", }), ); }); @@ -764,7 +764,7 @@ describe("AIRoutingCard", () => { const devLow = screen.getByTestId("complexity-select-developer-low"); fireEvent.click( - await within(devLow).findByRole("option", { name: "Claude Opus 4.8" }), + await within(devLow).findByRole("option", { name: "Claude Opus 5" }), ); await waitFor(() => expect(setComplexityOverride).toHaveBeenCalled()); diff --git a/panel/src/components/tg/tg-metrics-demo.ts b/panel/src/components/tg/tg-metrics-demo.ts index d6bb07c7..18aa4dbc 100644 --- a/panel/src/components/tg/tg-metrics-demo.ts +++ b/panel/src/components/tg/tg-metrics-demo.ts @@ -105,7 +105,7 @@ export const DEMO_TEAM_USAGE: TeamUsageRow[] = ( export const DEMO_MODEL_USAGE: ModelUsageSlice[] = ( [ - ["claude-opus-4-8", 42.1], + ["claude-opus-5", 42.1], ["glm-5.2:cloud", 16.8], ["grok-build", 7.64], ] as Array<[string, number]> diff --git a/roboco/billing/pricing.py b/roboco/billing/pricing.py index eeb63616..cd20e02c 100644 --- a/roboco/billing/pricing.py +++ b/roboco/billing/pricing.py @@ -57,11 +57,14 @@ _ANTHROPIC_FRAGMENTS = ("claude", "opus", "sonnet", "haiku") # Cache read is charged at ~10 % of the input price. # Cache write is charged at ~25 % of the input price. # -# Match on *substring* of model name so "claude-opus-4-8" and "opus" both +# Match on *substring* of model name so "claude-opus-5" and "opus" both # resolve to the same tier. # --------------------------------------------------------------------------- _PRICING: list[tuple[str, float, float, float, float]] = [ # (fragment, input/1M, output/1M, cache_read/1M, cache_write/1M) + # Opus 5 — same sticker as the Opus 4 family; needs its own fragment + # because "claude-opus-4" doesn't substring-match it. + ("claude-opus-5", 5.00, 25.00, 0.50, 6.25), # Opus 4 family ("claude-opus-4", 5.00, 25.00, 0.50, 6.25), # Sonnet 4 / 3.7 / 3.5 family diff --git a/roboco/models/llm_catalog.py b/roboco/models/llm_catalog.py index 4f251530..f7a8ca9c 100644 --- a/roboco/models/llm_catalog.py +++ b/roboco/models/llm_catalog.py @@ -7,7 +7,7 @@ the router maps back to the correct pre-seeded provider row. **Anthropic entries derive from `runtime.MODEL_MAP`** — that's the single source of truth for which Claude versions are supported. Bumping a model -version there (e.g. `claude-opus-4-8` → a newer id) updates the +version there (e.g. `claude-opus-5` → a newer id) updates the catalog automatically without a second edit here. Ollama Cloud entries are hand-maintained because Ollama's cloud tags @@ -46,7 +46,7 @@ def _build_anthropic_entries() -> tuple[CatalogEntry, ...]: Keeps the UI honest: a user picking "Claude Opus" sees exactly which underlying Claude Code model id will be used at spawn - (e.g. "Claude Opus · claude-opus-4-8"), so version bumps in + (e.g. "Claude Opus · claude-opus-5"), so version bumps in `runtime.MODEL_MAP` are immediately visible. """ entries: list[CatalogEntry] = [] diff --git a/roboco/models/runtime.py b/roboco/models/runtime.py index fe27bfde..9c776a15 100644 --- a/roboco/models/runtime.py +++ b/roboco/models/runtime.py @@ -110,7 +110,7 @@ class WaitingRecord: # Model mapping for cost optimization MODEL_MAP: dict[str, str] = { - "opus": "claude-opus-4-8", + "opus": "claude-opus-5", "sonnet": "claude-sonnet-5", "haiku": "claude-haiku-4-5-20251001", } diff --git a/tests/unit/billing/test_pricing.py b/tests/unit/billing/test_pricing.py index dc51c3df..9dab4e54 100644 --- a/tests/unit/billing/test_pricing.py +++ b/tests/unit/billing/test_pricing.py @@ -565,6 +565,7 @@ class TestProviderAwareness: for name in ( "claude-opus-4-6", "claude-opus-4-8", + "claude-opus-5", "claude-fable-5", "opus", "sonnet", diff --git a/tests/unit/models/test_misc_models.py b/tests/unit/models/test_misc_models.py index 6c97d602..b5b2a041 100644 --- a/tests/unit/models/test_misc_models.py +++ b/tests/unit/models/test_misc_models.py @@ -106,7 +106,7 @@ def test_build_anthropic_entries_skips_missing_models() -> None: # Patch MODEL_MAP to be missing one entry → loop hits the `continue` branch. with patch( "roboco.models.llm_catalog.MODEL_MAP", - {"opus": "claude-opus-4-8"}, # sonnet+haiku missing + {"opus": "claude-opus-5"}, # sonnet+haiku missing ): entries = _build_anthropic_entries() # Only one entry survives — opus. diff --git a/tests/unit/models/test_model_map.py b/tests/unit/models/test_model_map.py index d22aafc8..7e5fbb08 100644 --- a/tests/unit/models/test_model_map.py +++ b/tests/unit/models/test_model_map.py @@ -12,10 +12,9 @@ def test_sonnet_alias_resolves_to_sonnet_5() -> None: assert MODEL_MAP["sonnet"] == "claude-sonnet-5" -def test_opus_alias_resolves_to_opus_4_8() -> None: - # CEO upgrade to the newest Opus tier ("Opus 5" does not exist — the - # Claude 5 family is Fable/Mythos/Sonnet; Opus tops out at 4.8). - assert MODEL_MAP["opus"] == "claude-opus-4-8" +def test_opus_alias_resolves_to_opus_5() -> None: + # CEO upgrade to the newest Opus tier (Opus 5, released 2026-07-25). + assert MODEL_MAP["opus"] == "claude-opus-5" def test_sonnet_5_is_priced() -> None: diff --git a/tests/unit/runtime/test_intake_spawn.py b/tests/unit/runtime/test_intake_spawn.py index 41f7102f..a0ded54d 100644 --- a/tests/unit/runtime/test_intake_spawn.py +++ b/tests/unit/runtime/test_intake_spawn.py @@ -56,7 +56,7 @@ def _spec(**overrides: Any) -> _IntakeRunSpec: }, "session_id": "sess-abc", "cwd": "/data/workspaces/roboco/board/intake-1", - "cli_model": "claude-opus-4-8", + "cli_model": "claude-opus-5", "api_url": "http://roboco-orchestrator:8000", "provider_base_url": None, "provider_auth_token": None, @@ -108,7 +108,7 @@ class TestBuildIntakeRunCmd: assert "ROBOCO_WORKSPACE=/data/workspaces/roboco/board/intake-1" in cmd assert "ROBOCO_API_URL=http://roboco-orchestrator:8000" in cmd assert "ROBOCO_AGENT_ID=intake-1" in cmd - assert "CLAUDE_CODE_SUBAGENT_MODEL=claude-opus-4-8" in cmd + assert "CLAUDE_CODE_SUBAGENT_MODEL=claude-opus-5" in cmd def test_mounts_prompt_and_workspaces(self) -> None: cmd = AgentOrchestrator._build_intake_run_cmd(_spec()) diff --git a/tests/unit/runtime/test_orchestrator_manifest.py b/tests/unit/runtime/test_orchestrator_manifest.py index 353e841a..17e08d98 100644 --- a/tests/unit/runtime/test_orchestrator_manifest.py +++ b/tests/unit/runtime/test_orchestrator_manifest.py @@ -99,7 +99,7 @@ class TestBuildManifestForAgent: mock_settings.manifest_host_dir = str(tmp_path) mock_settings.workspaces_root = str(tmp_path / "workspaces") - result = _build_manifest_for_agent("be-dev-1", "claude-opus-4-8") + result = _build_manifest_for_agent("be-dev-1", "claude-opus-5") assert result is not None data = json.loads(result.read_text()) diff --git a/tests/unit/runtime/test_spawn_manifest.py b/tests/unit/runtime/test_spawn_manifest.py index bb55b2f9..8bdb1777 100644 --- a/tests/unit/runtime/test_spawn_manifest.py +++ b/tests/unit/runtime/test_spawn_manifest.py @@ -87,7 +87,7 @@ class TestBuildForRole: role=role, team="board", workspace_path=Path("/tmp/x"), - agent_model="claude-opus-4-8", + agent_model="claude-opus-5", ) ) assert "read_messages" in m.do_tools diff --git a/tests/unit/services/test_audit_agent_id.py b/tests/unit/services/test_audit_agent_id.py index c544f64f..61050f6b 100644 --- a/tests/unit/services/test_audit_agent_id.py +++ b/tests/unit/services/test_audit_agent_id.py @@ -182,7 +182,7 @@ async def test_log_agent_event_resolves_slug_to_uuid( event_type="agent.spawned", agent_slug=slug, task_id=task_id, - details={"container_id": "abc123def456", "model": "claude-opus-4-8"}, + details={"container_id": "abc123def456", "model": "claude-opus-5"}, ) # Read back the row. diff --git a/tests/unit/services/test_usage.py b/tests/unit/services/test_usage.py index d61cc245..db32b9cc 100644 --- a/tests/unit/services/test_usage.py +++ b/tests/unit/services/test_usage.py @@ -945,7 +945,7 @@ class TestGetRecentSessions: row = MagicMock() row.id = sid row.agent_slug = "product-owner" - row.model = "claude-opus-4-8" + row.model = "claude-opus-5" row.started_at = datetime.datetime(2026, 6, 11, 20, 41, tzinfo=datetime.UTC) row.ended_at = datetime.datetime(2026, 6, 11, 20, 42, tzinfo=datetime.UTC) row.tokens_input = exp_in @@ -966,7 +966,7 @@ class TestGetRecentSessions: s = out[0] assert s["id"] == str(sid) assert s["agent_slug"] == "product-owner" - assert s["model"] == "claude-opus-4-8" + assert s["model"] == "claude-opus-5" assert s["tokens_input"] == exp_in assert s["tokens_output"] == exp_out assert s["tokens_cache"] == exp_cr + exp_cw