From ecf671c48bbc64bdf7634642a6bced4afbf38b37 Mon Sep 17 00:00:00 2001 From: Steffen Zellmer <151627820+Steffen025@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:21:32 +0100 Subject: [PATCH 1/7] fix(wp-n9): generate full agent-tier opencode.json from provider choice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add PAI-Install/engine/provider-models.ts with model maps for all four providers (anthropic, zen, openrouter, openai). Fix stepInstallPAI to generate a complete opencode.json with all 15 agent entries and quick/standard/advanced tiers instead of a minimal ai.model-only structure. Set username from collected principalName. Simplify ProviderConfig to provider+apiKey only — model strings are now resolved from the PROVIDER_MODELS map at write time. Update CLI quick-install.ts to match the simplified interface. --- PAI-Install/cli/quick-install.ts | 24 ++-- PAI-Install/engine/provider-models.ts | 66 ++++++++++ PAI-Install/engine/steps-fresh.ts | 169 ++++++++++++++------------ docs/epic/OPTIMIZED-PR-PLAN.md | 3 +- docs/epic/TODO-v3.0.md | 3 +- 5 files changed, 169 insertions(+), 96 deletions(-) create mode 100644 PAI-Install/engine/provider-models.ts diff --git a/PAI-Install/cli/quick-install.ts b/PAI-Install/cli/quick-install.ts index 87fafbd0..7eda99a3 100644 --- a/PAI-Install/cli/quick-install.ts +++ b/PAI-Install/cli/quick-install.ts @@ -16,7 +16,9 @@ import { join } from "node:path"; import { homedir } from "node:os"; import type { InstallState } from "../engine/types"; import { createFreshState } from "../engine/state"; -import { stepPrerequisites, stepBuildOpenCode, stepProviderConfig, ZEN_FREE_MODELS, stepIdentity, stepVoice, stepInstallPAI } from "../engine/steps-fresh"; +import { stepPrerequisites, stepBuildOpenCode, stepProviderConfig, stepIdentity, stepVoice, stepInstallPAI } from "../engine/steps-fresh"; +import { PROVIDER_MODELS } from "../engine/provider-models"; +import type { ProviderName } from "../engine/provider-models"; import { stepDetectMigration, stepCreateBackup, stepMigrate, stepBinaryUpdate, stepMigrationDone } from "../engine/steps-migrate"; import { stepDetectUpdate, stepApplyUpdate, stepUpdateDone } from "../engine/steps-update"; @@ -153,25 +155,17 @@ async function runFreshInstall(): Promise { // Step 4: Provider Config onProgress(75, "Configuring provider..."); + const validProviders = Object.keys(PROVIDER_MODELS) as ProviderName[]; const preset = values.preset || "zen"; - - // Type guard for valid presets - const validPresets = ["zen", "quick", "standard", "advanced", "anthropic", "openrouter", "openai"]; - const validatedPreset = validPresets.includes(preset) ? preset : "zen"; - - const models = validatedPreset === "zen" ? ZEN_FREE_MODELS : { - quick: "claude-haiku-3.5", - standard: "claude-sonnet-4.6", - advanced: "claude-opus-4.6", - }; - + const provider: ProviderName = validProviders.includes(preset as ProviderName) + ? (preset as ProviderName) + : "zen"; + await stepProviderConfig( state, { - provider: validatedPreset, + provider, apiKey: values["api-key"] || "", - modelTier: "standard", - models, }, onProgress ); diff --git a/PAI-Install/engine/provider-models.ts b/PAI-Install/engine/provider-models.ts new file mode 100644 index 00000000..eac54bc2 --- /dev/null +++ b/PAI-Install/engine/provider-models.ts @@ -0,0 +1,66 @@ +/** + * PAI-OpenCode Installer — Provider Model Maps + * + * Defines quick/standard/advanced model strings for each supported provider. + * The installer substitutes these into the opencode.json template at install time. + * + * To add a new provider: add an entry below and handle it in steps-fresh.ts. + */ + +export type ProviderName = "anthropic" | "zen" | "openrouter" | "openai"; + +export interface ModelTierMap { + quick: string; + standard: string; + advanced: string; +} + +/** + * Model strings per provider, formatted as "provider/model-name" ready for + * insertion into opencode.json agent entries. + */ +export const PROVIDER_MODELS: Record = { + anthropic: { + quick: "anthropic/claude-haiku-4-5", + standard: "anthropic/claude-sonnet-4-5", + advanced: "anthropic/claude-opus-4-6", + }, + zen: { + // OpenCode Zen — cost-optimised free/cheap tier + quick: "zen/minimax-m2.5-free", + standard: "zen/gpt-5.1-codex-mini", + advanced: "zen/claude-haiku-3.5", + }, + openrouter: { + quick: "openrouter/google/gemini-flash-1.5", + standard: "openrouter/anthropic/claude-3.5-sonnet", + advanced: "openrouter/anthropic/claude-3-opus", + }, + openai: { + quick: "openai/gpt-4o-mini", + standard: "openai/gpt-4o", + advanced: "openai/gpt-5", + }, +}; + +/** + * Human-readable labels shown in the installer wizard. + */ +export const PROVIDER_LABELS: Record = { + anthropic: { + label: "Anthropic (Claude)", + description: "Premium quality — requires Anthropic API key", + }, + zen: { + label: "OpenCode Zen (recommended)", + description: "Free tier available — 60× cost optimisation vs direct Anthropic", + }, + openrouter: { + label: "OpenRouter", + description: "Multi-provider flexibility — one API key for many models", + }, + openai: { + label: "OpenAI", + description: "GPT-4o and GPT-5 — requires OpenAI API key", + }, +}; diff --git a/PAI-Install/engine/steps-fresh.ts b/PAI-Install/engine/steps-fresh.ts index d9536598..191f139a 100644 --- a/PAI-Install/engine/steps-fresh.ts +++ b/PAI-Install/engine/steps-fresh.ts @@ -1,14 +1,16 @@ #!/usr/bin/env bun /** * PAI-OpenCode Installer — Fresh Install Steps - * + * * 7-step fresh installation flow with OpenCode-Zen as default provider. */ -import type { InstallState } from "./types"; -import { buildOpenCodeBinary } from "./build-opencode"; -import type { BuildResult } from "./build-opencode"; -import { existsSync, mkdirSync, writeFileSync, chmodSync, copyFileSync, symlinkSync, unlinkSync, lstatSync, realpathSync } from "node:fs"; +import type { InstallState } from "./types.ts"; +import { buildOpenCodeBinary } from "./build-opencode.ts"; +import type { BuildResult } from "./build-opencode.ts"; +import { PROVIDER_MODELS, PROVIDER_LABELS } from "./provider-models.ts"; +import type { ProviderName } from "./provider-models.ts"; +import { existsSync, mkdirSync, writeFileSync, chmodSync, symlinkSync, unlinkSync, lstatSync, realpathSync } from "node:fs"; import { join, resolve } from "node:path"; import { homedir } from "node:os"; @@ -110,39 +112,18 @@ export async function stepBuildOpenCode( // ═══════════════════════════════════════════════════════════ export interface ProviderConfig { - provider: "zen" | "anthropic" | "openrouter" | "openai"; + provider: ProviderName; apiKey: string; - modelTier: "quick" | "standard" | "advanced"; - models: { - quick: string; - standard: string; - advanced: string; - }; } -export const ZEN_FREE_MODELS = { - quick: "minimax-m2.5-free", // FREE - standard: "gpt-5.1-codex-mini", // $0.25/M - advanced: "claude-haiku-3.5", // $0.80/M -}; - -export const ANTHROPIC_MODELS = { - quick: "claude-haiku-3.5", - standard: "claude-sonnet-4.6", - advanced: "claude-opus-4.6", -}; - -export const OPENROUTER_MODELS = { - quick: "google/gemini-flash-1.5", - standard: "anthropic/claude-3.5-sonnet", - advanced: "anthropic/claude-3-opus", -}; - -export const OPENAI_MODELS = { - quick: "gpt-4o-mini", - standard: "gpt-4o", - advanced: "gpt-5", -}; +// Re-export for consumers that imported these from this module +export { PROVIDER_MODELS, PROVIDER_LABELS } from "./provider-models.ts"; + +// Legacy aliases kept for CLI quick-install.ts compatibility +export const ZEN_FREE_MODELS = PROVIDER_MODELS.zen; +export const ANTHROPIC_MODELS = PROVIDER_MODELS.anthropic; +export const OPENROUTER_MODELS = PROVIDER_MODELS.openrouter; +export const OPENAI_MODELS = PROVIDER_MODELS.openai; export async function stepProviderConfig( state: InstallState, @@ -150,14 +131,12 @@ export async function stepProviderConfig( onProgress: (percent: number, message: string) => void ): Promise { onProgress(75, "Configuring AI provider..."); - - // Save provider settings + + // Save provider + key; model strings are resolved from PROVIDER_MODELS at write time state.collected.provider = config.provider; state.collected.apiKey = config.apiKey; - state.collected.modelTier = config.modelTier; - state.collected.models = config.models; - - // API key will be saved to .env by config generation step + + // API key will be saved to .env by the install step } // ═══════════════════════════════════════════════════════════ @@ -280,35 +259,75 @@ ${providerEnvVar}=${state.collected.apiKey || ""} chmodSync(envPath, 0o600); onProgress(95, "Created .env with secure permissions..."); - // Generate opencode.json - const modelProvider = state.collected.provider || "anthropic"; - const modelTier = state.collected.modelTier || "standard"; - const modelMap = state.collected.models; - const modelName = modelMap && typeof modelMap === 'object' ? - (modelMap[modelTier] || modelMap['standard']) : - "claude-sonnet-4.6"; - const modelString = `${modelProvider}/${modelName}`; - + // Generate opencode.json — full agent-tier structure matching the repo template + const provider = (state.collected.provider || "anthropic") as ProviderName; + const tiers = PROVIDER_MODELS[provider] ?? PROVIDER_MODELS.anthropic; + + /** + * Build a standard agent entry with quick/standard/advanced tiers. + * The top-level `model` mirrors the standard tier so opencode has a + * sensible default when no tier is specified by a caller. + */ + function agentEntry(standard: string, quick: string, advanced: string) { + return { + model: standard, + model_tiers: { + quick: { model: quick }, + standard: { model: standard }, + advanced: { model: advanced }, + }, + }; + } + const opencode = { - ai: { - name: state.collected.aiName || "PAI", - model: modelString, - }, - voice: { - enabled: state.collected.voiceEnabled || false, - provider: state.collected.voiceProvider || "none", - voiceId: state.collected.voiceId || "default", + $schema: "https://opencode.ai/config.json", + theme: "dark", + model: tiers.standard, + snapshot: true, + username: state.collected.principalName || "User", + permission: { + "*": "allow", + websearch: "allow", + codesearch: "allow", + webfetch: "allow", + doom_loop: "ask", + external_directory: "ask", }, - memory: { - enabled: true, + mode: { + build: { + prompt: "You are a Personal AI assistant powered by PAI-OpenCode infrastructure.", + }, + plan: { + prompt: "You are a Personal AI assistant powered by PAI-OpenCode infrastructure.", + }, }, - skills: { - autoLoad: true, + agent: { + // Algorithm agent always uses the highest-quality model for orchestration + Algorithm: { model: tiers.advanced }, + Architect: agentEntry(tiers.standard, tiers.quick, tiers.advanced), + Engineer: agentEntry(tiers.standard, tiers.quick, tiers.advanced), + general: agentEntry(tiers.standard, tiers.quick, tiers.advanced), + // explore is always the quick model — speed matters more than quality + explore: { model: tiers.quick }, + Intern: agentEntry(tiers.quick, tiers.quick, tiers.standard), + Writer: agentEntry(tiers.standard, tiers.quick, tiers.advanced), + DeepResearcher: agentEntry(tiers.standard, tiers.quick, tiers.advanced), + // Specialised researchers keep their primary model but fall back to provider tiers + GeminiResearcher: agentEntry(tiers.standard, tiers.quick, tiers.advanced), + GrokResearcher: agentEntry(tiers.standard, tiers.quick, tiers.advanced), + PerplexityResearcher: agentEntry(tiers.standard, tiers.quick, tiers.advanced), + CodexResearcher: agentEntry(tiers.standard, tiers.quick, tiers.advanced), + // QATester has no tier override — single model is intentional + QATester: { model: tiers.standard }, + Pentester: agentEntry(tiers.standard, tiers.quick, tiers.advanced), + Designer: agentEntry(tiers.standard, tiers.quick, tiers.advanced), + Artist: agentEntry(tiers.standard, tiers.quick, tiers.advanced), }, }; + writeFileSync( join(localOpencodeDir, "opencode.json"), - JSON.stringify(opencode, null, 2) + JSON.stringify(opencode, null, 2), ); onProgress(97, "Generated opencode.json..."); @@ -388,25 +407,17 @@ export async function runFreshInstall( // Step 3: Provider Configuration (API Keys) await emit({ event: "step_start", step: "api-keys" }); // Collect provider config via interactive callbacks - const providerChoices = [ - { label: "OpenCode Zen (FREE tier available)", value: "zen", description: "Recommended - 60x cost optimization" }, - { label: "Anthropic (Claude)", value: "anthropic", description: "Premium quality, higher cost" }, - { label: "OpenRouter", value: "openrouter", description: "Multi-provider flexibility" }, - ]; - const provider = await requestChoice("provider", "Choose your AI provider:", providerChoices); + const providerChoices = Object.entries(PROVIDER_LABELS).map(([value, { label, description }]) => ({ + label, + value, + description, + })); + const provider = (await requestChoice("provider", "Choose your AI provider:", providerChoices)) as ProviderName || "zen"; const apiKey = await requestInput("api-key", `Enter your ${provider} API key:`, "key", "sk-..."); - - // Select models based on provider - const models = provider === "zen" ? ZEN_FREE_MODELS : - provider === "anthropic" ? ANTHROPIC_MODELS : - provider === "openrouter" ? OPENROUTER_MODELS : - provider === "openai" ? OPENAI_MODELS : ZEN_FREE_MODELS; - + await stepProviderConfig(state, { - provider: provider || "zen", + provider, apiKey: apiKey || "", - modelTier: "standard", - models, }, (percent, message) => { emit({ event: "progress", step: "api-keys", percent, detail: message }); }); diff --git a/docs/epic/OPTIMIZED-PR-PLAN.md b/docs/epic/OPTIMIZED-PR-PLAN.md index 291bb94c..3b3da33f 100644 --- a/docs/epic/OPTIMIZED-PR-PLAN.md +++ b/docs/epic/OPTIMIZED-PR-PLAN.md @@ -35,7 +35,8 @@ tags: [architecture, migration, v3.0, PR-strategy, native-transformation] | **WP-N5** | Plan Update | #54 | ✅ **Merged** | Sync all planning docs to reflect N1-N4 complete | | **WP-N6** | System Self-Awareness | #55 | ✅ **Merged** | OpenCodeSystem skill, 4 architecture reference docs, ADR-017 | | **WP-N7** | roborev + Biome CI | #56 | ✅ **Merged** | roborev plugin handler, CodeReview skill, GitHub Actions CI, ADR-018 | -| **WP-N8** | Obsidian Formatting Guidelines | — | 🔄 **In Progress** | Formatting guidelines, agent capability matrix (split from WP-N7) | +| **WP-N8** | Obsidian Formatting Guidelines | #57 | ✅ **Merged** | Formatting guidelines, agent capability matrix (split from WP-N7) | +| **WP-N9** | Installer opencode.json Fix | — | 🔄 **In Progress** | provider-models.ts, full agent-tier generation, principalName in username | > [!NOTE] > **2026-03-08 Live Audit:** WP-C scope significantly reduced after comparing repo against v4.0.3. diff --git a/docs/epic/TODO-v3.0.md b/docs/epic/TODO-v3.0.md index 9f743119..a9de2461 100644 --- a/docs/epic/TODO-v3.0.md +++ b/docs/epic/TODO-v3.0.md @@ -34,7 +34,8 @@ WP-N4 ████████████ 100% ✅ ← LSP + Fork Documentatio WP-N5 ████████████ 100% ✅ ← Plan Update complete, PR #54 WP-N6 ████████████ 100% ✅ ← System Self-Awareness, PR #55 merged WP-N7 ████████████ 100% ✅ ← roborev + Biome CI, PR #56 merged -WP-N8 ████████░░░░ 80% 🔄 ← Obsidian formatting + agent matrix, PR open +WP-N8 ████████████ 100% ✅ ← Obsidian formatting + agent matrix, PR #57 merged +WP-N9 ████████████ 100% ✅ ← Installer opencode.json fix, PR open ``` > **The port is done. The native transformation starts with WP-N1.** From bb04b361d529496826984163c658173bc2dff195 Mon Sep 17 00:00:00 2001 From: Steffen Zellmer <151627820+Steffen025@users.noreply.github.com> Date: Thu, 12 Mar 2026 13:10:29 +0100 Subject: [PATCH 2/7] fix(wp-n9): address CodeRabbit findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TODO-v3.0.md: WP-N9 progress bar corrected to 90% 🔄 (PR open, not complete) - OPTIMIZED-PR-PLAN.md: summary updated to reflect WP-N8 merged, WP-N9 in progress - steps-fresh.ts: fallback provider changed from 'anthropic' to 'zen' (project default) - steps-fresh.ts: remove stale modelTier/models fields from settings.json generation (these are no longer set by stepProviderConfig; model strings live in opencode.json) - steps-fresh.ts: replace illegal 'continue' outside loop with 'return' in broken-symlink handler --- PAI-Install/engine/steps-fresh.ts | 13 ++++++------- docs/epic/OPTIMIZED-PR-PLAN.md | 8 ++++---- docs/epic/TODO-v3.0.md | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/PAI-Install/engine/steps-fresh.ts b/PAI-Install/engine/steps-fresh.ts index 191f139a..8793b51f 100644 --- a/PAI-Install/engine/steps-fresh.ts +++ b/PAI-Install/engine/steps-fresh.ts @@ -224,8 +224,7 @@ export async function stepInstallPAI( default: state.collected.provider || "zen", [state.collected.provider || "zen"]: { // apiKey is stored in .env, not here - modelTier: state.collected.modelTier || "standard", - models: state.collected.models || [], + // model strings are written to opencode.json via PROVIDER_MODELS }, }, }; @@ -260,7 +259,7 @@ ${providerEnvVar}=${state.collected.apiKey || ""} onProgress(95, "Created .env with secure permissions..."); // Generate opencode.json — full agent-tier structure matching the repo template - const provider = (state.collected.provider || "anthropic") as ProviderName; + const provider = (state.collected.provider || "zen") as ProviderName; const tiers = PROVIDER_MODELS[provider] ?? PROVIDER_MODELS.anthropic; /** @@ -344,12 +343,12 @@ ${providerEnvVar}=${state.collected.apiKey || ""} let currentTarget: string; try { currentTarget = realpathSync(globalOpencodeLink); - } catch (err) { - // Symlink target doesn't exist (broken symlink) - // Remove and recreate + } catch { + // Symlink target doesn't exist (broken symlink) — remove and recreate unlinkSync(globalOpencodeLink); symlinkSync(localOpencodeDir, globalOpencodeLink, "dir"); - continue; + // Symlink is now correct; nothing more to do in this block + return; } if (currentTarget !== localOpencodeDir) { diff --git a/docs/epic/OPTIMIZED-PR-PLAN.md b/docs/epic/OPTIMIZED-PR-PLAN.md index 3b3da33f..3c53b1f7 100644 --- a/docs/epic/OPTIMIZED-PR-PLAN.md +++ b/docs/epic/OPTIMIZED-PR-PLAN.md @@ -219,11 +219,11 @@ Current state (dev branch): | Metric | 2026-03-08 | 2026-03-11 | **Current (2026-03-12)** | |--------|------------|------------|--------------------------| | Port WPs done | 8 ✅ | 9 ✅ (WP-E) | **9 ✅** | -| Native WPs done | 0 | 4 ✅ (N1–N4) | **7 ✅ (N1–N7), N8 in progress** | -| Open PRs | 2 (C, D) | 1 (#55) | **1 (WP-N8 — open, in progress)** | -| Remaining native work | Not planned | WP-N6 in progress | **WP-N8 in progress (Obsidian formatting)** | +| Native WPs done | 0 | 4 ✅ (N1–N4) | **8 ✅ (N1–N8), N9 in progress** | +| Open PRs | 2 (C, D) | 1 (#55) | **1 (WP-N9 — open, in progress)** | +| Remaining native work | Not planned | WP-N6 in progress | **WP-N9 in progress (installer opencode.json fix)** | -**Status:** Port complete. Native transformation: WP-N1 through WP-N7 merged (PR #50–#56). WP-N8 in progress (Obsidian formatting guidelines + agent capability matrix). +**Status:** Port complete. Native transformation: WP-N1 through WP-N8 merged (PR #50–#57). WP-N9 in progress (installer opencode.json full agent-tier generation). **Native transformation plan:** `docs/epic/EPIC-v3.0-OpenCode-Native.md` **Full gap analysis:** `docs/epic/GAP-ANALYSIS-v3.0.md` diff --git a/docs/epic/TODO-v3.0.md b/docs/epic/TODO-v3.0.md index a9de2461..fd69b104 100644 --- a/docs/epic/TODO-v3.0.md +++ b/docs/epic/TODO-v3.0.md @@ -35,7 +35,7 @@ WP-N5 ████████████ 100% ✅ ← Plan Update complete, P WP-N6 ████████████ 100% ✅ ← System Self-Awareness, PR #55 merged WP-N7 ████████████ 100% ✅ ← roborev + Biome CI, PR #56 merged WP-N8 ████████████ 100% ✅ ← Obsidian formatting + agent matrix, PR #57 merged -WP-N9 ████████████ 100% ✅ ← Installer opencode.json fix, PR open +WP-N9 ██████████░░ 90% 🔄 ← Installer opencode.json fix, PR #58 open ``` > **The port is done. The native transformation starts with WP-N1.** From 076001899dea51f60c434c91fd1cb442cc952034 Mon Sep 17 00:00:00 2001 From: Steffen Zellmer <151627820+Steffen025@users.noreply.github.com> Date: Thu, 12 Mar 2026 13:17:00 +0100 Subject: [PATCH 3/7] fix(wp-n9): correct zen advanced model ID to claude-3-5-haiku Verified against opencode.ai/docs/zen/ catalog. The correct catalog ID is 'zen/claude-3-5-haiku', not 'zen/claude-haiku-3.5'. Added inline cost comments for all three zen tier models. --- PAI-Install/engine/provider-models.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PAI-Install/engine/provider-models.ts b/PAI-Install/engine/provider-models.ts index eac54bc2..ab69e48d 100644 --- a/PAI-Install/engine/provider-models.ts +++ b/PAI-Install/engine/provider-models.ts @@ -26,10 +26,10 @@ export const PROVIDER_MODELS: Record = { advanced: "anthropic/claude-opus-4-6", }, zen: { - // OpenCode Zen — cost-optimised free/cheap tier - quick: "zen/minimax-m2.5-free", - standard: "zen/gpt-5.1-codex-mini", - advanced: "zen/claude-haiku-3.5", + // OpenCode Zen — cost-optimised tiers (IDs verified against opencode.ai/docs/zen/) + quick: "zen/minimax-m2.5-free", // FREE + standard: "zen/gpt-5.1-codex-mini", // $0.25/M in+out + advanced: "zen/claude-3-5-haiku", // $0.80/M — catalog ID for Claude Haiku 3.5 }, openrouter: { quick: "openrouter/google/gemini-flash-1.5", From bd0bc9c79403ee112f9fa7469a0f5a8a53e541c8 Mon Sep 17 00:00:00 2001 From: Steffen Zellmer <151627820+Steffen025@users.noreply.github.com> Date: Thu, 12 Mar 2026 13:17:59 +0100 Subject: [PATCH 4/7] fix(wp-n9): address remaining CodeRabbit findings - Fix fallback provider consistency: tiers now falls back to PROVIDER_MODELS.zen matching the 'zen' default on the provider line - Remove premature 'return' after broken symlink repair so execution continues to onProgress(100, 'Installation complete!') --- PAI-Install/engine/steps-fresh.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/PAI-Install/engine/steps-fresh.ts b/PAI-Install/engine/steps-fresh.ts index 8793b51f..2af88e95 100644 --- a/PAI-Install/engine/steps-fresh.ts +++ b/PAI-Install/engine/steps-fresh.ts @@ -260,7 +260,7 @@ ${providerEnvVar}=${state.collected.apiKey || ""} // Generate opencode.json — full agent-tier structure matching the repo template const provider = (state.collected.provider || "zen") as ProviderName; - const tiers = PROVIDER_MODELS[provider] ?? PROVIDER_MODELS.anthropic; + const tiers = PROVIDER_MODELS[provider] ?? PROVIDER_MODELS.zen; /** * Build a standard agent entry with quick/standard/advanced tiers. @@ -344,12 +344,11 @@ ${providerEnvVar}=${state.collected.apiKey || ""} try { currentTarget = realpathSync(globalOpencodeLink); } catch { - // Symlink target doesn't exist (broken symlink) — remove and recreate - unlinkSync(globalOpencodeLink); - symlinkSync(localOpencodeDir, globalOpencodeLink, "dir"); - // Symlink is now correct; nothing more to do in this block - return; - } + // Symlink target doesn't exist (broken symlink) — remove and recreate + unlinkSync(globalOpencodeLink); + symlinkSync(localOpencodeDir, globalOpencodeLink, "dir"); + // Symlink repaired; fall through to onProgress(100) below + } if (currentTarget !== localOpencodeDir) { // Remove old symlink and create new one From ab378347d31e87083c7898c04fcdbbdfcdd68cc1 Mon Sep 17 00:00:00 2001 From: Steffen Zellmer <151627820+Steffen025@users.noreply.github.com> Date: Thu, 12 Mar 2026 13:27:22 +0100 Subject: [PATCH 5/7] fix(wp-n9): address CodeRabbit findings round 3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update openrouter model IDs: claude-3.5-sonnet → claude-4.5-sonnet, claude-3-opus → claude-opus-4-6 (bring in line with current model names) - Assign currentTarget = localOpencodeDir in broken-symlink catch block so the subsequent 'if (currentTarget !== localOpencodeDir)' check has a defined value and doesn't trigger a redundant remove+recreate - Fix indentation alignment of currentTarget block inside isSymbolicLink branch --- PAI-Install/engine/provider-models.ts | 4 ++-- PAI-Install/engine/steps-fresh.ts | 30 ++++++++++++++------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/PAI-Install/engine/provider-models.ts b/PAI-Install/engine/provider-models.ts index ab69e48d..ef3b1bc9 100644 --- a/PAI-Install/engine/provider-models.ts +++ b/PAI-Install/engine/provider-models.ts @@ -33,8 +33,8 @@ export const PROVIDER_MODELS: Record = { }, openrouter: { quick: "openrouter/google/gemini-flash-1.5", - standard: "openrouter/anthropic/claude-3.5-sonnet", - advanced: "openrouter/anthropic/claude-3-opus", + standard: "openrouter/anthropic/claude-4.5-sonnet", + advanced: "openrouter/anthropic/claude-opus-4-6", }, openai: { quick: "openai/gpt-4o-mini", diff --git a/PAI-Install/engine/steps-fresh.ts b/PAI-Install/engine/steps-fresh.ts index 2af88e95..3b4f2630 100644 --- a/PAI-Install/engine/steps-fresh.ts +++ b/PAI-Install/engine/steps-fresh.ts @@ -338,24 +338,26 @@ ${providerEnvVar}=${state.collected.apiKey || ""} if (existsSync(globalOpencodeLink)) { const stats = lstatSync(globalOpencodeLink); - if (stats.isSymbolicLink()) { - // It's already a symlink - check if it points to our location - let currentTarget: string; - try { - currentTarget = realpathSync(globalOpencodeLink); - } catch { + if (stats.isSymbolicLink()) { + // It's already a symlink - check if it points to our location + let currentTarget: string; + try { + currentTarget = realpathSync(globalOpencodeLink); + } catch { // Symlink target doesn't exist (broken symlink) — remove and recreate unlinkSync(globalOpencodeLink); symlinkSync(localOpencodeDir, globalOpencodeLink, "dir"); - // Symlink repaired; fall through to onProgress(100) below + // Assign so the subsequent check sees a defined, correct value + // and doesn't attempt a redundant remove+recreate + currentTarget = localOpencodeDir; } - - if (currentTarget !== localOpencodeDir) { - // Remove old symlink and create new one - unlinkSync(globalOpencodeLink); - symlinkSync(localOpencodeDir, globalOpencodeLink, "dir"); - } - // If it already points to our location, nothing to do + + if (currentTarget !== localOpencodeDir) { + // Remove old symlink and create new one + unlinkSync(globalOpencodeLink); + symlinkSync(localOpencodeDir, globalOpencodeLink, "dir"); + } + // If it already points to our location, nothing to do } else if (stats.isDirectory()) { // It's a real directory - backup and replace with symlink const backupPath = `${globalOpencodeLink}.backup-${Date.now()}`; From 92f14a71fd5f35e37387a7f275ad1abce845784e Mon Sep 17 00:00:00 2001 From: Steffen Zellmer <151627820+Steffen025@users.noreply.github.com> Date: Thu, 12 Mar 2026 13:32:34 +0100 Subject: [PATCH 6/7] fix(wp-n9): align indentation of symlink block to parent scope The if (stats.isSymbolicLink()) block and all its contents were indented one level short relative to their parent if (existsSync(globalOpencodeLink)) block. No logic changes. --- PAI-Install/engine/steps-fresh.ts | 40 +++++++++++++++---------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/PAI-Install/engine/steps-fresh.ts b/PAI-Install/engine/steps-fresh.ts index 3b4f2630..c1f34350 100644 --- a/PAI-Install/engine/steps-fresh.ts +++ b/PAI-Install/engine/steps-fresh.ts @@ -337,27 +337,27 @@ ${providerEnvVar}=${state.collected.apiKey || ""} // Check if ~/.opencode exists if (existsSync(globalOpencodeLink)) { const stats = lstatSync(globalOpencodeLink); - - if (stats.isSymbolicLink()) { - // It's already a symlink - check if it points to our location - let currentTarget: string; - try { - currentTarget = realpathSync(globalOpencodeLink); - } catch { - // Symlink target doesn't exist (broken symlink) — remove and recreate - unlinkSync(globalOpencodeLink); - symlinkSync(localOpencodeDir, globalOpencodeLink, "dir"); - // Assign so the subsequent check sees a defined, correct value - // and doesn't attempt a redundant remove+recreate - currentTarget = localOpencodeDir; - } - if (currentTarget !== localOpencodeDir) { - // Remove old symlink and create new one - unlinkSync(globalOpencodeLink); - symlinkSync(localOpencodeDir, globalOpencodeLink, "dir"); - } - // If it already points to our location, nothing to do + if (stats.isSymbolicLink()) { + // It's already a symlink - check if it points to our location + let currentTarget: string; + try { + currentTarget = realpathSync(globalOpencodeLink); + } catch { + // Symlink target doesn't exist (broken symlink) — remove and recreate + unlinkSync(globalOpencodeLink); + symlinkSync(localOpencodeDir, globalOpencodeLink, "dir"); + // Assign so the subsequent check sees a defined, correct value + // and doesn't attempt a redundant remove+recreate + currentTarget = localOpencodeDir; + } + + if (currentTarget !== localOpencodeDir) { + // Remove old symlink and create new one + unlinkSync(globalOpencodeLink); + symlinkSync(localOpencodeDir, globalOpencodeLink, "dir"); + } + // If it already points to our location, nothing to do } else if (stats.isDirectory()) { // It's a real directory - backup and replace with symlink const backupPath = `${globalOpencodeLink}.backup-${Date.now()}`; From 60e4b0284870cc9bc5e3a219fddd66e3bfc3f83e Mon Sep 17 00:00:00 2001 From: Steffen Zellmer <151627820+Steffen025@users.noreply.github.com> Date: Thu, 12 Mar 2026 13:37:27 +0100 Subject: [PATCH 7/7] fix(wp-n9): defensive copy legacy alias exports to isolate from PROVIDER_MODELS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aliases ZEN_FREE_MODELS, ANTHROPIC_MODELS, OPENROUTER_MODELS, OPENAI_MODELS were direct object references. A consumer mutating any property would silently corrupt the shared PROVIDER_MODELS map. Use shallow spread { ...PROVIDER_MODELS.x } — ModelTierMap is a flat { quick, standard, advanced } object so shallow copy is sufficient. --- PAI-Install/engine/steps-fresh.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/PAI-Install/engine/steps-fresh.ts b/PAI-Install/engine/steps-fresh.ts index c1f34350..dcb28b42 100644 --- a/PAI-Install/engine/steps-fresh.ts +++ b/PAI-Install/engine/steps-fresh.ts @@ -119,11 +119,12 @@ export interface ProviderConfig { // Re-export for consumers that imported these from this module export { PROVIDER_MODELS, PROVIDER_LABELS } from "./provider-models.ts"; -// Legacy aliases kept for CLI quick-install.ts compatibility -export const ZEN_FREE_MODELS = PROVIDER_MODELS.zen; -export const ANTHROPIC_MODELS = PROVIDER_MODELS.anthropic; -export const OPENROUTER_MODELS = PROVIDER_MODELS.openrouter; -export const OPENAI_MODELS = PROVIDER_MODELS.openai; +// Legacy aliases kept for CLI quick-install.ts compatibility. +// Spread into new objects so mutations by consumers cannot corrupt PROVIDER_MODELS. +export const ZEN_FREE_MODELS = { ...PROVIDER_MODELS.zen }; +export const ANTHROPIC_MODELS = { ...PROVIDER_MODELS.anthropic }; +export const OPENROUTER_MODELS = { ...PROVIDER_MODELS.openrouter }; +export const OPENAI_MODELS = { ...PROVIDER_MODELS.openai }; export async function stepProviderConfig( state: InstallState,