diff --git a/.opencode/agent/designer.md b/.opencode/agent/designer.md index 6897b83..aaa4e61 100644 --- a/.opencode/agent/designer.md +++ b/.opencode/agent/designer.md @@ -1,8 +1,8 @@ --- description: UI designer/builder. Commits to a bold aesthetic direction and implements production-grade, non-generic interfaces. Embodies the distinctive-ui-design skill. Use for any UI work where "looks generic" is the failure to avoid. mode: all -model: openrouter/google/gemma-4-26b-a4b-it:free # multimodal (sees screenshots), fast MoE; 31b sibling is the quality upgrade when its :free endpoint isn't saturated -fallback: openrouter/nvidia/nemotron-3-nano-30b-a3b:free # used by tools/council.mjs retry (default; --no-retry disables); doctor.mjs validates it +model: openrouter/thinkingmachines/inkling-small:free # VISION (sees screenshots) + 1M ctx; gemma-4-26b's :free endpoint is chronically saturated +fallback: openrouter/minimax/minimax-m3:free # also VISION — a designer fallback must still see screenshots; used by tools/council.mjs retry temperature: 0.7 # higher than other agents — UI work needs creative range --- diff --git a/.opencode/agent/pr-reviewer.md b/.opencode/agent/pr-reviewer.md index a89a93c..4662298 100644 --- a/.opencode/agent/pr-reviewer.md +++ b/.opencode/agent/pr-reviewer.md @@ -1,8 +1,8 @@ --- description: Read-only staff-level reviewer for OTHER people's pull requests. Embodies reviewing-others-code — collaborative, severity-labeled, mentoring. Never edits the code under review. mode: all -model: openrouter/google/gemma-4-26b-a4b-it:free # strong free general reviewer, multimodal; 31b sibling = quality upgrade when its endpoint isn't saturated -fallback: openrouter/nvidia/nemotron-3-nano-30b-a3b:free # used by tools/council.mjs retry (default; --no-retry disables); doctor.mjs validates it +model: openrouter/minimax/minimax-m3:free # VISION + 1M ctx; MiniMax family, replaces chronically-saturated gemma-4-26b +fallback: openrouter/nvidia/nemotron-3.5-lightning:free # NVIDIA != MiniMax primary, 1M ctx; used by tools/council.mjs retry temperature: 0.2 permission: edit: deny # never modify someone else's branch; bash stays on to fetch diffs / run it diff --git a/.opencode/agent/proposer-b.md b/.opencode/agent/proposer-b.md index 1cf4bba..10dd142 100644 --- a/.opencode/agent/proposer-b.md +++ b/.opencode/agent/proposer-b.md @@ -2,7 +2,7 @@ description: Blind council proposer, family B (poolside Laguna). Produces ONE independent, self-contained proposal or analysis from a context packet — never sees other proposers' output. Used by /deep-design, /deep-debug, /council. mode: all # `all`, not `subagent`: headless `opencode run --agent` only accepts primaries — a subagent SILENTLY falls back to the default agent (zero decorrelation). `all` keeps it usable as an in-session subagent too. model: openrouter/poolside/laguna-s-2.1:free # family B (poolside) — ≠ NVIDIA (judge), ≠ Google (proposer-c) -fallback: openrouter/inclusionai/ling-3.0-tiny:free # family-diverse retry; used by tools/council.mjs retry (default; --no-retry disables); doctor.mjs validates it +fallback: openrouter/minimax/minimax-m3:free # family-diverse retry; replaces ling-3.0-tiny (slug no longer exists) temperature: 0.3 permission: edit: deny diff --git a/.opencode/agent/proposer-c.md b/.opencode/agent/proposer-c.md index 4d96cec..d158016 100644 --- a/.opencode/agent/proposer-c.md +++ b/.opencode/agent/proposer-c.md @@ -1,8 +1,8 @@ --- description: Blind council proposer, family C (Google Gemma). Produces ONE independent, self-contained proposal or analysis from a context packet — never sees other proposers' output. Used by /deep-design, /deep-debug, /council. mode: all # `all`, not `subagent`: headless `opencode run --agent` only accepts primaries — a subagent SILENTLY falls back to the default agent (zero decorrelation). `all` keeps it usable as an in-session subagent too. -model: openrouter/google/gemma-4-26b-a4b-it:free # family C — ≠ NVIDIA, ≠ inclusionAI -fallback: openrouter/google/gemma-4-31b-it:free # bigger sibling, same family; used by tools/council.mjs retry (default; --no-retry disables); doctor.mjs validates it +model: openrouter/thinkingmachines/inkling-small:free # family C (ThinkingMachines) != poolside proposer-b, != NVIDIA judge; 1M ctx. Replaces saturated gemma-4-26b +fallback: openrouter/minimax/minimax-m2.7:free # family-diverse retry; replaces gemma-4-31b (also saturated) temperature: 0.3 permission: edit: deny diff --git a/.opencode/agent/refuter.md b/.opencode/agent/refuter.md index f4576ee..24df7fe 100644 --- a/.opencode/agent/refuter.md +++ b/.opencode/agent/refuter.md @@ -1,8 +1,8 @@ --- description: Adversarial finding-killer for /deep-review. Attacks every merged review finding and tries to prove it wrong; only findings that survive reach the user. Family-decorrelated from both reviewers. mode: all # `all`, not `subagent`: headless `opencode run --agent` only accepts primaries — a subagent SILENTLY falls back to the default agent. `all` keeps it usable as an in-session subagent too. -model: openrouter/openai/gpt-oss-20b:free # ≠ reviewer (NVIDIA) and ≠ reviewer-cheap (Qwen) — correlated skepticism is a rubber stamp -fallback: openrouter/inclusionai/ling-3.0-tiny:free # used by tools/council.mjs retry (default; --no-retry disables); doctor.mjs validates it +model: openrouter/thinkingmachines/inkling-small:free # ThinkingMachines != reviewer (NVIDIA) and != reviewer-cheap (MiniMax). Replaces gpt-oss-20b, now paid-only +fallback: openrouter/nvidia/nemotron-3.5-lightning:free # at full fallback reviewer=Cohere / reviewer-cheap=MiniMax / this=NVIDIA -> still 3 families temperature: 0.1 permission: edit: deny diff --git a/.opencode/agent/reviewer-cheap.md b/.opencode/agent/reviewer-cheap.md index 28d1763..625bd96 100644 --- a/.opencode/agent/reviewer-cheap.md +++ b/.opencode/agent/reviewer-cheap.md @@ -1,8 +1,8 @@ --- description: Decorrelated second reviewer on a CHEAP, architecturally-different model. Different training distribution = different blind spots. Used in the /review consensus vote. mode: all # `all`, not `subagent`: headless `opencode run --agent` only accepts primaries — a subagent SILENTLY falls back to the default agent. `all` keeps it usable as an in-session subagent too. -model: ollama/qwen3.5:9b # local = $0 + no rate limits; Qwen family ≠ reviewer's NVIDIA -fallback: openrouter/google/gemma-4-26b-a4b-it:free # fires ONLY on the headless tools/council.mjs retry (default; --no-retry disables) — in-session /review and /deep-review always use the primary, so no-Ollama users must re-pin the PRIMARY to a :free slug; Google ≠ reviewer's NVIDIA/inclusionAI; doctor.mjs validates it +model: openrouter/minimax/minimax-m3:free # MiniMax != reviewer's NVIDIA and != poolside executor; 1M ctx. Replaces gemma-4-26b (saturated) +fallback: openrouter/minimax/minimax-m2.7:free # same-family sibling; at full fallback reviewer=Cohere / this=MiniMax / refuter=NVIDIA stay distinct temperature: 0.1 permission: edit: deny diff --git a/.opencode/agent/tester.md b/.opencode/agent/tester.md index 98b9bd9..65947be 100644 --- a/.opencode/agent/tester.md +++ b/.opencode/agent/tester.md @@ -2,7 +2,7 @@ description: Independent test author. Writes spec tests from the PLAN ONLY and must NOT read the implementation. Eliminates verification gaming — tests can't be reverse-engineered to pass buggy code. mode: all # `all`, not `subagent`: headless `opencode run --agent` only accepts primaries — a subagent SILENTLY falls back to the default agent. `all` keeps it usable as an in-session subagent too. model: openrouter/cohere/north-mini-code:free # agentic coding model, Cohere family ≠ poolside executor -fallback: ollama/qwen3.5:9b # local — used by tools/council.mjs retry (default; --no-retry disables); doctor.mjs validates it +fallback: openrouter/minimax/minimax-m3:free # MiniMax ≠ Cohere primary and ≠ poolside executor; 1M ctx — used by tools/council.mjs retry (default; --no-retry disables); doctor.mjs validates it temperature: 0.1 permission: bash: deny diff --git a/.opencode/command/a11y-review.md b/.opencode/command/a11y-review.md index 2c4f90f..63dbbb9 100644 --- a/.opencode/command/a11y-review.md +++ b/.opencode/command/a11y-review.md @@ -1,6 +1,6 @@ --- description: Audit a screen or diff for Section 508 / WCAG 2.1 AA conformance -model: openrouter/google/gemma-4-26b-a4b-it:free # criterion-by-criterion audit; multimodal helps with UI evidence +model: openrouter/thinkingmachines/inkling-small:free # VISION — a11y review reads screenshots; replaces saturated gemma-4-26b --- Audit the target below against the `accessibility-508` skill. Go criterion by criterion and diff --git a/.opencode/command/commit.md b/.opencode/command/commit.md index 3159e4e..72aa88c 100644 --- a/.opencode/command/commit.md +++ b/.opencode/command/commit.md @@ -1,6 +1,6 @@ --- description: Write a Conventional Commit message from the staged changes (does not commit) -model: ollama/qwen3.5:4b # local + instant — mechanical task, costs nothing +model: openrouter/poolside/laguna-xs-2.1:free # XS coding model, 262k — mechanical task, don't spend a big model on it (262k matters: a large staged diff is the input) --- Staged summary: diff --git a/.opencode/command/name.md b/.opencode/command/name.md index ea0b790..5c9785f 100644 --- a/.opencode/command/name.md +++ b/.opencode/command/name.md @@ -1,6 +1,6 @@ --- description: Generate distinctive product/app/company names (creative-naming) with rationale + availability -model: openrouter/google/gemma-4-26b-a4b-it:free # creative range across naming techniques +model: openrouter/minimax/minimax-m2.7:free # fast (~13s); replaces saturated gemma-4-26b --- Follow the `creative-naming` skill. From the concept below: extract the essence, generate 15–20 diff --git a/.opencode/command/setup.md b/.opencode/command/setup.md index 5ca124c..270f293 100644 --- a/.opencode/command/setup.md +++ b/.opencode/command/setup.md @@ -7,8 +7,10 @@ Set up opencode-amplifier for use. Walk through and report status at each step: 1. **Place the kit:** ensure `.opencode/` (agent, command, skills, plugins), `AGENTS.md`, `PATTERNS.md`, `patterns/`, `memory/` are present — copy into the project, or merge into `~/.config/opencode/` for global use across repos. -2. **Wire the providers:** in `opencode.jsonc`, confirm `provider.ollama.options.baseURL` and - `provider.openrouter`'s `OPENROUTER_API_KEY`. Run `opencode models` to list the real model IDs. +2. **Wire the provider:** in `opencode.jsonc`, confirm `provider.openrouter`'s + `OPENROUTER_API_KEY` resolves. Run `opencode models` to list the real model IDs. (If you added + the optional local tier, confirm `provider.ollama.options.baseURL` reaches a live server too — + a dead endpoint resolves silently and its agents fail with no useful error.) 3. **Confirm the shipped per-agent pins resolve:** every `model:`/`fallback:` in `.opencode/agent/*.md` (and the `model:` in the five pinned `.opencode/command/*.md`) appears in `opencode models`. Do NOT rewrite the decorrelated pins unless the user asks — they are diff --git a/ONBOARDING.md b/ONBOARDING.md index 457b9b2..6d566e6 100644 --- a/ONBOARDING.md +++ b/ONBOARDING.md @@ -4,8 +4,11 @@ For a teammate adopting the kit. Goal: from zero to a contract-governed, multi-m free, open-weights models, fast. ## 1. Prereqs -- OpenCode CLI installed. Ollama running locally (or on a LAN box — see `docs/local-models.md`). -- `export OPENROUTER_API_KEY=…` (free key from openrouter.ai/keys). +- OpenCode CLI installed. +- `export OPENROUTER_API_KEY=…` (free key from openrouter.ai/keys). That's the only requirement — + every shipped pin is an `openrouter/…:free` slug. +- *Optional:* Ollama locally or on a LAN box, to move the chatty roles off the OpenRouter pool — + see `docs/local-models.md`. ## 2. Get the kit - Clone this repo, OR merge its `.opencode/`, `AGENTS.md`, `PATTERNS.md`, `patterns/`, `memory/` diff --git a/README.md b/README.md index c9cacf2..5311666 100644 --- a/README.md +++ b/README.md @@ -98,21 +98,41 @@ Discipline lives in the `deep-analysis` skill + AGENTS.md rule 10. file. ## Model tiering (free tier, preserved quota) -Models are provider-prefixed (`ollama/…` local, `openrouter/…:free` hosted). Chatty roles run -local (no rate limits); bursty council roles spend the 1,000/day OpenRouter pool. Defaults wired -across 5+ families so the `/review` consensus vote has uncorrelated blind spots: +Every shipped pin is `openrouter/…:free` — one API key, no local model server required. Defaults +are wired across 5+ families so the `/review` consensus vote has uncorrelated blind spots. All of +it spends the same 1,000/day OpenRouter pool, so cheap roles get small models on purpose: | Tier | Models | Use for | |---|---|---| -| Reasoning | `openrouter/nvidia/nemotron-3-ultra-550b-a55b:free` (1M ctx) | architect, debugger, judge, mapper | -| Executor | `openrouter/poolside/laguna-s-2.1:free` (manual swap to `ollama/ornith:9b` when rate-limited) | the Build agent, step edits | -| Cross-family review | `openrouter/nvidia/nemotron-3-super-120b-a12b:free`, `openrouter/cohere/north-mini-code:free` | reviewer, tester | -| Decorrelated review | `ollama/qwen3.5:9b` (local, $0) | reviewer-cheap (different blind spots) | -| Council diversity | `openrouter/poolside/laguna-s-2.1:free`, `openrouter/google/gemma-4-26b-a4b-it:free`, `openrouter/openai/gpt-oss-20b:free` | proposers, refuter, designer | -| Mechanical | `ollama/qwen3.5:4b` | commit messages, boilerplate, renames | - -⚠️ The `:free` roster churns monthly. `node tools/doctor.mjs` (live, no `--offline`) catches -delisted slugs; swap in a live `:free` model or a pennies-tier one (see `opencode.jsonc` -comments). +| Reasoning | `nvidia/nemotron-3-ultra-550b-a55b:free` (1M ctx) | architect, debugger, judge, mapper, `/adr`, `/port-from-angular` | +| Executor | `poolside/laguna-s-2.1:free` | the Build agent, step edits | +| Cross-family review | `nvidia/nemotron-3-super-120b-a12b:free`, `cohere/north-mini-code:free` | reviewer, synthesizer, tester | +| Decorrelated review | `minimax/minimax-m3:free` (1M ctx, vision) | reviewer-cheap, pr-reviewer | +| Council diversity | `thinkingmachines/inkling-small:free`, `poolside/laguna-s-2.1:free`, `minimax/minimax-m2.7:free` | proposers, refuter, designer, `/a11y-review`, `/name` | +| Mechanical | `poolside/laguna-xs-2.1:free` | commit messages, boilerplate, renames | + +The `/review` consensus vote runs four different families — poolside (executor), NVIDIA +(reviewer), MiniMax (reviewer-cheap), ThinkingMachines (refuter) — and the fallbacks are chosen +so they stay distinct even if every primary is rate-limited at once. Vision roles (`designer`, +`pr-reviewer`, `/a11y-review`) are pinned to multimodal slugs on purpose. + +**Optional local tier ($0, unmetered).** Running Ollama lets you move the chatty roles +(`reviewer-cheap`, `/commit`) off the OpenRouter pool entirely. It is opt-in, not assumed — +see `docs/local-models.md` for the server and `examples/opencode.overlay.example.jsonc` for the +provider block and which pins to move. + +⚠️ **The `:free` roster churns monthly, and the catalog lies.** `opencode models` keeps listing +slugs OpenRouter has since moved to paid, so a static check passes and only a real call fails +with *"This model is unavailable for free."* On 2026-08-29 five of nine declared slugs were +broken this way — including a **primary**. Verify with a live round-trip, never the catalog: + +```bash +node tools/doctor.mjs # round-trips every primary +node tools/doctor.mjs --deep # ...and every fallback (2x the calls, catches the silent rot) +``` + +Fallbacks rot most — they only run when everything else broke, so nothing exercises them. Run +`--deep` after any provider change. Swap in a live `:free` model or a pennies-tier one (see +`opencode.jsonc` comments). ## Claude Code parity OpenCode is Claude-Code-compatible: it auto-discovers `.claude/skills/` and falls back to diff --git a/docs/local-models.md b/docs/local-models.md index 1de7815..8b1b609 100644 --- a/docs/local-models.md +++ b/docs/local-models.md @@ -1,6 +1,11 @@ -# Local model hosting (Ollama) +# Local model hosting (Ollama) — optional -How to stand up the `ollama/…` tier this kit pins — on your own machine or a LAN box. +How to stand up an **optional** `ollama/…` tier — on your own machine or a LAN box. + +> The kit ships with no local dependency: every pin is an `openrouter/…:free` slug and one API +> key runs the whole thing. Nothing here is required. Add this tier only if you want the chatty +> roles off your OpenRouter pool — and only if the server will actually be up, because a declared +> provider pointing at a dead host resolves silently and its agents fail with no useful error. ## 1. Why local @@ -147,9 +152,19 @@ ping -c 1 macmini.local curl -s http://macmini.local:11434/api/tags ``` -Client side is one line of config — copy `examples/opencode.overlay.example.jsonc` and change -`provider.ollama.options.baseURL` to `http://macmini.local:11434/v1`. Nothing else in the kit -changes; agent pins stay `ollama/qwen3.5:9b` etc. +Client side: copy the `provider.ollama` block from `examples/opencode.overlay.example.jsonc` into +your config and set `options.baseURL` to `http://macmini.local:11434/v1`. Then move the pins you +want local — the two the tier is designed for are: + +| File | Ships as | Local swap | +|---|---|---| +| `.opencode/agent/reviewer-cheap.md` | `openrouter/google/gemma-4-26b-a4b-it:free` | `ollama/qwen3.5:9b` | +| `.opencode/command/commit.md` | `openrouter/nvidia/nemotron-3-nano-30b-a3b:free` | `ollama/qwen3.5:4b` | + +Keep `reviewer-cheap` in a family that is neither the executor's (poolside) nor the primary +reviewer's (NVIDIA) — that decorrelation is the whole point of the `/review` consensus vote. +`node tools/doctor.mjs` round-trips any `ollama/*` primary over direct HTTP and will tell you if +the endpoint is dead. ## 5. 16GB tuning diff --git a/examples/opencode.overlay.example.jsonc b/examples/opencode.overlay.example.jsonc index ebdd081..2a503dc 100644 --- a/examples/opencode.overlay.example.jsonc +++ b/examples/opencode.overlay.example.jsonc @@ -8,7 +8,12 @@ // Usage: merge into the project's opencode.jsonc, or keep as ~/.config/opencode/opencode.jsonc. "$schema": "https://opencode.ai/config.json", - // Point the local tier at a LAN model server instead of localhost. + // ADD BACK the optional local tier. The kit itself declares NO ollama provider — every shipped + // pin is an openrouter/…:free slug, so it works with one API key and no model server. Paste + // this block only if you actually run Ollama; a provider pointing at a host that isn't up + // resolves silently and its agents fail with no useful error. + // + // Then move the pins you want local — see the table in docs/local-models.md §4. // Stable addressing: give the box a real hostname (mDNS → macmini.local) or a DHCP // reservation — never pin a dynamic IP. See docs/local-models.md for the server side. "provider": { diff --git a/opencode.jsonc b/opencode.jsonc index d494687..5cfcc94 100644 --- a/opencode.jsonc +++ b/opencode.jsonc @@ -1,40 +1,28 @@ { // opencode reads opencode.jsonc (comments allowed). Merge into your project's existing config - // if you have one. Runs entirely on free, open-weights models: - // - ollama: local models (http://localhost:11434 — or a LAN box, see examples/ overlay). - // This is the $0/unlimited tier: reviewer-cheap's primary and /commit run here. - // No Ollama? Re-pin those two to a :free slug (e.g. - // openrouter/google/gemma-4-26b-a4b-it:free) and you lose the unmetered tier. + // if you have one. Runs entirely on free, open-weights models, and needs exactly ONE thing: // - openrouter: big open-weights models on OpenRouter's :free tier (needs OPENROUTER_API_KEY). // Free-key tier = 50 req/day and an agentic session burns hundreds: do the - // one-time $10 credit purchase for 1,000/day. (Setting "model" below to - // ollama/ornith:9b only localizes the default executor session — every named - // agent keeps its own openrouter/* pin; going fully local means re-pinning - // each .opencode/agent/*.md too.) + // one-time $10 credit purchase for 1,000/day. + // + // No local model server is required. A local Ollama tier is OPTIONAL and costs $0/unmetered — + // see docs/local-models.md to stand one up and examples/opencode.overlay.example.jsonc for the + // provider block + which pins to move. It is opt-in on purpose: a declared provider pointing at + // a host that isn't running resolves silently to a dead endpoint, and agents pinned to it fail + // with no useful error. // // Auto-discovered from .opencode/ (no config needed): agent/, command/, skills/, plugins/. // OpenCode also auto-discovers .claude/skills/ — so your existing superpowers skills work too. "$schema": "https://opencode.ai/config.json", - // --- 1. Providers (both OpenAI-compatible) ------------------------------------------------- - // Agents reference models PROVIDER-PREFIXED (e.g. `model: ollama/qwen3.5:9b`). + // --- 1. Provider (OpenAI-compatible) ------------------------------------------------------- + // Agents reference models PROVIDER-PREFIXED (e.g. `model: openrouter/cohere/north-mini-code:free`). // ⚠️ The :free roster churns monthly. If a slug 404s: run `node tools/doctor.mjs` WITHOUT // --offline — only the live round-trip catches a dead slug; the static catalog check can't // (it only reflects what this file declares). Then swap in a live :free slug, // `openrouter/openrouter/free` (auto-router), or a pennies-tier model below. A new slug must // be ADDED to this provider's "models" map before any agent/command can pin it. "provider": { - "ollama": { - "npm": "@ai-sdk/openai-compatible", - "name": "Ollama (local)", - "options": { "baseURL": "http://localhost:11434/v1" }, // no API key needed - "models": { - "qwen3.5:9b": { "name": "Qwen3.5 9B — local reasoner/reviewer-cheap (262k)" }, - "ornith:9b": { "name": "Ornith 9B — agentic coder, local executor alternative (MIT)" }, - "qwen3.5:4b": { "name": "Qwen3.5 4B — mechanical (/commit), fast" }, - "gemma4:12b-it-qat": { "name": "Gemma 4 12B QAT — family-diverse local alt (256k)" } - } - }, "openrouter": { "npm": "@ai-sdk/openai-compatible", "name": "OpenRouter (free open-weights)", @@ -43,15 +31,31 @@ "apiKey": "{env:OPENROUTER_API_KEY}" // free account: openrouter.ai/keys }, "models": { - "nvidia/nemotron-3-ultra-550b-a55b:free": { "name": "Nemotron 3 Ultra 550B — top reasoner, only free 1M ctx" }, + // ── Round-tripped live 2026-08-29. Every PIN in this kit uses one of these. ────────── + "nvidia/nemotron-3-ultra-550b-a55b:free": { "name": "Nemotron 3 Ultra 550B — top reasoner, 1M ctx (~18s)" }, "nvidia/nemotron-3-super-120b-a12b:free": { "name": "Nemotron 3 Super 120B — reviewer/synthesizer (262k)" }, + "nvidia/nemotron-3.5-lightning:free": { "name": "Nemotron 3.5 Lightning — fast fallback (1M ctx)" }, "poolside/laguna-s-2.1:free": { "name": "Laguna-S 2.1 118B — coding agent, executor (262k)" }, + "poolside/laguna-xs-2.1:free": { "name": "Laguna-XS 2.1 — mechanical (/commit), fast (262k)" }, "cohere/north-mini-code:free": { "name": "North Mini Code 30B — tester (256k)" }, - "google/gemma-4-31b-it:free": { "name": "Gemma 4 31B — quality upgrade over 26B; :free endpoint often saturated (262k)" }, - "google/gemma-4-26b-a4b-it:free": { "name": "Gemma 4 26B MoE — designer/pr-review, multimodal, fast (262k)" }, - "inclusionai/ling-3.0-tiny:free": { "name": "Ling 3.0 Tiny — family-diverse fallback (262k)" }, - "nvidia/nemotron-3-nano-30b-a3b:free": { "name": "Nemotron 3 Nano 30B — fast fallback (256k)" }, - "openai/gpt-oss-20b:free": { "name": "gpt-oss 20B — refuter, Apache 2.0 (131k)" } + "minimax/minimax-m3:free": { "name": "MiniMax M3 — reviewer-cheap/pr-review, VISION, 1M ctx" }, + "minimax/minimax-m2.7:free": { "name": "MiniMax M2.7 — fast family-diverse fallback (~13s, 196k)" }, + "thinkingmachines/inkling-small:free": { "name": "Inkling Small — designer/refuter/proposer-c, VISION, 1M ctx (~12s)" }, + + // ── Declared but NOT pinned: reachable via `-m`, depended on by nothing. ──────────── + // Both Gemma :free endpoints timed out (>100s) on 2026-08-29 — the kit already described + // this endpoint as "often saturated". Fine for a manual one-off, wrong for an agent pin. + "google/gemma-4-31b-it:free": { "name": "Gemma 4 31B — SATURATED, unpinned (262k)" }, + "google/gemma-4-26b-a4b-it:free": { "name": "Gemma 4 26B MoE — SATURATED, unpinned, multimodal (262k)" } + + // ── REMOVED 2026-08-29 — verified broken by live round-trip. ──────────────────────── + // ⚠️ `opencode models` STILL LISTS the delisted ones, so the static catalog check PASSES + // and only a real call fails. Never re-add without a round-trip. + // "openai/gpt-oss-20b:free" -> "unavailable for free" (was refuter PRIMARY) + // "nvidia/nemotron-3-nano-30b-a3b:free" -> "unavailable for free" (was 3 fallbacks) + // "inclusionai/ling-3.0-tiny:free" -> ProviderModelNotFound, slug is gone entirely + // (was proposer-b + refuter fallback) + // // Pennies tier (paid but ~free) if a :free slug delists: // "qwen/qwen3.7-flash" ($0.03/M in, 1M ctx) // "z-ai/glm-4.7-flash" ($0.06/M in) @@ -59,7 +63,7 @@ } } }, - "model": "openrouter/poolside/laguna-s-2.1:free", // Build/executor default. No automatic fallback exists for this key — if rate-limited, swap to ollama/ornith:9b + "model": "openrouter/poolside/laguna-s-2.1:free", // Build/executor default. No automatic fallback exists for this key — if rate-limited, swap to another :free slug above (or a local one, if you added the optional Ollama tier) // --- 2. Always-on instructions (combined with AGENTS.md) ---------------------------------- // Globs and even remote URLs are supported. This loads the rails + durable memory every turn. diff --git a/tools/doctor.mjs b/tools/doctor.mjs index cf4f126..0d3d0ca 100644 --- a/tools/doctor.mjs +++ b/tools/doctor.mjs @@ -2,7 +2,7 @@ // doctor.mjs — first-run kit verifier. Run from the repo root BEFORE any real work on a new // machine, after a provider change, or after an OpenCode upgrade: // -// node tools/doctor.mjs [--offline] +// node tools/doctor.mjs [--offline] [--deep] // // Static checks always run (file/config sanity, agent mode policy). Live checks call the // configured provider — one tiny round-trip per unique pinned model, agent primaries AND @@ -19,9 +19,12 @@ import { existsSync, readFileSync } from "node:fs"; import { readAgents } from "./frontmatter.mjs"; const offline = process.argv.includes("--offline"); -const unknown = process.argv.slice(2).filter((a) => a !== "--offline"); +// --deep also round-trips every declared FALLBACK. Off by default because it doubles the live +// calls, but it is the only thing that catches a rotted fallback: the catalog check can't. +const deep = process.argv.includes("--deep"); +const unknown = process.argv.slice(2).filter((a) => a !== "--offline" && a !== "--deep"); if (unknown.length) { - console.error(`Unknown argument(s): ${unknown.join(" ")}. Usage: node tools/doctor.mjs [--offline]`); + console.error(`Unknown argument(s): ${unknown.join(" ")}. Usage: node tools/doctor.mjs [--offline] [--deep]`); process.exit(2); } @@ -127,11 +130,33 @@ if (offline) { const cmds = commandPins.filter((c) => c.model === m).map((c) => `command:${c.name}`); return cmds.length ? `${m} (${cmds.join(", ")})` : m; }; + // ⚠️ The catalog is NOT proof a :free slug is usable. `opencode models` keeps listing slugs + // OpenRouter has moved to paid ("This model is unavailable for free") — a static check passes + // and only a live call fails. That is how gpt-oss-20b (a PRIMARY) and three fallbacks rotted + // undetected. Treat catalog membership as necessary, never sufficient. for (const m of primaries) record(catalog.includes(m) ? "PASS" : "FAIL", `model in catalog: ${label(m)}`); for (const m of fallbackModels) { if (!primaries.includes(m)) record(catalog.includes(m) ? "PASS" : "WARN", `fallback in catalog: ${m}`, catalog.includes(m) ? "" : "declared fallback unavailable — retries will fail"); } + // One round-trip + one classifier for both primaries and (with --deep) fallbacks. The three + // failure shapes are worth distinguishing because they need different fixes: + // "unavailable for free" -> slug moved to paid; drop the :free suffix or re-pin + // ProviderModelNotFound -> slug deleted outright; OpenRouter suggests replacements + // timeout -> endpoint saturated; may recover, but don't PIN to it + const roundTrip = (m, sev, name) => { + const r = oc(["run", "-m", m], { input: "Reply with exactly: OK", timeout: 90_000 }); + const blob = `${r.stderr || ""}${r.stdout || ""}`; + if (r.status === 0 && /OK/.test(r.stdout || "")) return record("PASS", `round-trip: ${name}`); + if (/unavailable for free/i.test(blob)) return record(sev, `round-trip: ${name}`, "DELISTED — moved to paid; catalog still lists it"); + if (/ProviderModelNotFound/i.test(blob)) return record(sev, `round-trip: ${name}`, "GONE — slug no longer exists"); + if (r.status === null) return record(sev, `round-trip: ${name}`, "timeout — endpoint saturated; don't pin to it"); + return record(sev, `round-trip: ${name}`, blob.trim().slice(0, 200)); + }; + + // No pin ships as ollama/* any more — the local tier is OPT-IN (docs/local-models.md). This + // branch stays so doctor still validates it for anyone who adds it back, and is simply not + // taken otherwise. // Local thinking models (qwen3.5 etc.) get an UNBOUNDED token budget through `opencode run` // and can reason past any sane timeout on small hardware. For ollama/* we therefore prove the // endpoint serves a real, bounded completion via direct HTTP (cold load still allowed 300s); @@ -158,9 +183,17 @@ if (offline) { } continue; } - const r = oc(["run", "-m", m], { input: "Reply with exactly: OK", timeout: 90_000 }); - if (r.status === 0 && /OK/.test(r.stdout || "")) record("PASS", `round-trip: ${label(m)}`); - else record("FAIL", `round-trip: ${label(m)}`, r.status === null ? "timeout" : (r.stderr || r.stdout || "").trim().slice(0, 200)); + roundTrip(m, "FAIL", label(m)); + } + + // Fallbacks are the pins MOST likely to rot (they only run when everything else broke) and, + // until --deep, the ones with the WEAKEST check. Opt-in because it doubles the live calls. + if (deep) { + for (const m of fallbackModels) { + if (!primaries.includes(m)) roundTrip(m, "FAIL", `fallback ${m}`); + } + } else if (fallbackModels.some((m) => !primaries.includes(m))) { + record("WARN", "fallback round-trips", "skipped — re-run with --deep; a catalog hit does NOT prove a :free slug still serves"); } const probe = oc(["run", "--agent", "architect"], { input: "Reply with exactly: OK", timeout: 120_000 });