diff --git a/CHANGELOG.md b/CHANGELOG.md index e53b5d0..a850203 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,42 @@ Changes are tracked via git tags. Each release tag corresponds to an entry here. ## [Unreleased] -_No changes yet._ +### Fixed — Item 6.34: Help Assistant + Product Architect launchers now accept `claude-code-ollama` + +The agent picker in `cfcf init` and the web Settings page surfaces every +detected adapter for every role — but the Help Assistant and Product +Architect have their **own** launchers (separate from the standard +`spawnProcess` path because they need true TUI takeover via +`stdio: "inherit"`) with hardcoded `switch` statements for argv +composition. The new adapters from item 6.28 (`opencode`, +`claude-code-ollama`, `opencode-ollama`) were never wired up to those +switches, so picking `claude-code-ollama` for HA or PA at `cfcf init` +would save fine but throw at launch time: + +``` +[ha] failed to launch: Help Assistant doesn't support adapter +"claude-code-ollama" yet. Supported: claude-code, codex. +``` + +- Added a `case "claude-code-ollama"` arm to both launchers. Wraps the + existing claude-code argv with `ollama launch claude --model + --yes -- `. For HA: no + `--dangerously-skip-permissions` (HA is interactive, user reviews + tool calls). For PA: `--dangerously-skip-permissions` flows through + in non-safe mode (matches the direct claude-code behaviour). +- The error message for the still-unsupported variants + (`opencode`, `opencode-ollama`) now lists `claude-code-ollama` as + the supported ollama path, so users on `opencode-ollama` see the + alternative immediately. +- 7 new unit tests across `help-assistant/launcher.test.ts` and + `product-architect/launcher.test.ts` (argv shape, model omission, + safe-mode behaviour for PA, helpful-error-message check). +- **Out of scope for this fix**: `opencode` and `opencode-ollama` + interactive support — opencode's interactive default reads + `AGENTS.md` from cwd, which doesn't fit cf²'s ephemeral-tempfile + pattern. Needs investigation into opencode's runtime + system-prompt-injection mechanism. Tracked as the deferred half of + item 6.34 in `docs/plan.md`. ## [0.22.0] -- 2026-05-09 diff --git a/docs/plan.md b/docs/plan.md index ca23b1e..da805e2 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -325,6 +325,7 @@ The tables below are the authoritative view of iteration progress. The **Notes** | 6.29 | ❌ | macOS notification fix: per-app bundle ID via shim or `terminal-notifier` | **Surfaced 2026-05-08** during iter-6 dogfood. Symptom: macOS desktop notifications from cfcf events (`loop.paused`, `loop.completed`, `agent.failed`) appear with severe delays — sometimes minutes-to-hours late, often batched. Root cause: cfcf uses `osascript -e 'display notification …'` which macOS attributes to **"Script Editor"** with no separate bundle ID. Three downstream consequences: (1) per-app rate-limiting kicks in around ~5 notifications/min sustained — beyond that, macOS silently queues + batches; (2) DND / Focus Mode queues all of them until DND ends, causing the "minutes-to-hours-later dump" pattern; (3) without a bundle ID, macOS can't merge cfcf events into a coordinated stream — each notification is independent, hitting quotas faster. **Three implementation options**: *(option 1, preferred)* Bundle a tiny `.app` shim (`cfcf-notifier.app/Contents/{Info.plist,MacOS/cfcf-notifier}`) with its own bundle ID + a tiny shell or Swift wrapper that calls `display notification`. cfcf invokes the shim instead of `osascript`. ~50 LOC of plist + a small wrapper script + updates to the install pipeline so the shim ships with cfcf. *(option 2)* Detect `terminal-notifier` if installed (community tool, has its own bundle ID + a `-group` flag for grouping). Use it when present, fall back to `osascript`. Document as a recommended optional dep for macOS users. ~10 LOC of detection + wrapper. *(option 3)* Switch to a Swift / ObjC `NSUserNotification` shim. Overkill compared to option 1; bundle-app shim is simpler. **Out of scope**: rewriting the notification dispatcher or adding new channels — only the macos channel needs the fix. **Alternative if neither option lands by end of iter-6**: remove the macos channel entirely + document terminal-bell as the primary on-the-machine notification path. Linux / cross-platform users already use terminal-bell via the existing dispatcher. **Tests**: a smoke test that confirms the shim binary is reachable post-install (`cfcf doctor` check); a CI-skipped integration test that fires a notification and asserts the shim's path was used. The actual delivery is hard to assert in CI — that part stays manual. **Cross-refs**: `packages/core/src/notifications/channels/macos.ts` is the current implementation; `cfcf-notifier.app` would live under a new `scripts/macos-notifier/` directory + be staged into dist by `scripts/stage-dist.sh`. **Effort estimate**: 0.5-1 session for option 1 (bundle shim); 0.25 session for option 2 (terminal-notifier detection). **Surfaced 2026-05-08 during iter-6 dogfood.** | | 6.30 | ✅ | API parse errors with non-coder ollama models on claude-code-ollama (refined: opencode-ollama is the fall-back, not "use coder-tuned models" alone) | **Shipped 2026-05-08**. **Refined finding from re-test**: original framing "gemma4 is bad at tool calls" was wrong. Same gemma4:31b model, two routes: `claude-code-ollama + gemma4:31b` → fails with `API Error: Content block not found` (Anthropic-strict Messages API parser rejects); `opencode-ollama + gemma4:31b` → wrote all four documenter files cleanly. The model IS capable; it's the strict-Anthropic-shape translation layer that rejects its output. The OpenAI-compatible endpoint used by opencode-ollama is more tolerant. **Re-confirmed after the v0.20.0 process-tree-kill fix landed** (so it's not a queue-starvation confound). **Shipped scope**: (a) **`isApiParseRisk()` helper** in `packages/core/src/adapters/index.ts` (sister to `isClaudeCodeHarnessRisk`); 4 unit tests in `adapters.test.ts`. (b) **New blue info callout** in `` (web Settings + workspace Config) explaining the parse-error symptom and recommending `opencode-ollama` as the fall-back; positioned between the existing yellow policy callout and the blue log-visibility callout. (c) **Inline ⚠ row indicator** next to the adapter selector for any unattended row on `claude-code` (direct) — visual link to the policy callout below the table. Scoped via a new `isUnattendedRole` check in ServerInfo so PA / HA rows don't flag (they're allowed-interactive). (d) **Architect always counted as unattended** for these warnings (`UNATTENDED_ROLE_NAMES` updated in `@cfcf/core`): the loop invokes architect on `refine_plan` resume and judge `NEEDS_REFINEMENT` verdicts as well as the pre-loop `autoReviewSpecs=true` path; the same adapter setting drives all three loop paths AND the manual `cfcf review` path, so the warning has to reflect the worst case. Drops the previous `(autoReviewSpecs=true)` qualifier on the architect role label. (e) **CLI `cfcf init` banner** updated to mirror the new web callouts — same three notices in the same order. (f) **`docs/guides/anthropic-policy.md`** documenter row updated with the refined finding: two workable paths (coder-tuned model on claude-code-ollama, OR any model on opencode-ollama). **Out of scope (deferred)**: tolerant retry logic in the iteration-loop spawn (option b from original framing — defer to iter-7 if a similar failure pattern shows up with non-gemma models); deeper investigation of ollama's Anthropic-API translation layer (option c — would belong upstream in ollama, not cfcf). **Cross-refs**: `~/.cfcf/logs/calc-04c553/documenter-001.log` + `documenter-004.log` (the 56-byte claude-code-ollama failure logs); `documenter-005.log` (864-byte opencode-ollama success log on the same model). | | 6.31 | ✅ | Orphan agent-process cleanup on `cfcf server stop` / `start` / interactive reap | **Surfaced 2026-05-08** during 6.28's opencode-ollama dogfood. When the user stops + starts the cfcf server while a loop iteration is in flight, the spawned agent processes (`claude` / `codex` / `opencode` / `ollama launch `) are NOT terminated. They keep running until they finish, fail, or are manually killed. **Failure mode observed**: 4 orphans from earlier loop runs (2 claude documenter+architect, 2 opencode architects) accumulated across server restarts and serialized on ollama's model runner — each held the qwen3-coder model busy with up-to-10-minute timed-out inference requests. The new opencode iteration's `/v1/chat/completions` call queued behind them and starved. From the user's POV: loop "hangs" with no clear error, log file is 40 bytes, no obvious culprit. Required `pgrep -f \"ollama launch\" \| xargs kill` to recover. **Scope**: (a) **[SHIPPED in v0.20.0]** `start.ts` `gracefulShutdown()` enumerates every active spawn from the in-memory `activeProcesses` registry (`packages/core/src/active-processes.ts`) and sends SIGTERM to the **process group** (`process.kill(-pid, "SIGTERM")` — agents are now spawned with `detached: true`), then schedules SIGKILL 1.5s later via `setTimeout(...).unref()`. The shutdown handler awaits a 2-second grace window before `process.exit()` so the SIGKILL timer has time to fire — without that wait, the timer dies with the parent and orphans of `init` accumulate. **(b) [SHIPPED 2026-05-08, post-v0.20.0]** On `startServer()` boot, `start.ts` calls `findOrphanAgentProcesses()` from the new `packages/core/src/orphan-reaper.ts` module. Three conjoined filters (PPID==1 + same effective user + cfcf-spawned command shape) identify orphans confidently — false positives near-zero. Auto-reaps via `reapOrphans()` (group-SIGTERM → 1.5s grace → SIGKILL); logs `[server] Reaping N stale agent process(es) from a previous server PID:` followed by one line per orphan. Best-effort: a scan failure never blocks server boot. **(c) [SHIPPED 2026-05-08, post-v0.20.0]** New `cfcf server reap` subcommand uses the same matcher and prints candidates with `pid=… kind=… elapsed=… ` lines, then prompts `Kill these N process(es)? [y/N]:`. On `y`: reap. On `N` or empty: `Aborted. No processes killed.`. Empty list: `No zombie agent processes detected.`. Supports `-y / --yes` for non-interactive use. Pure system call — does NOT require the cfcf server to be running. **Tests shipped** (25 in `packages/core/src/orphan-reaper.test.ts`): the classifier covers every cfcf-spawn pattern + negative cases (interactive claude, non-cfcf opencode, ollama serve/pull, unrelated commands like node/python); the parser handles standard `ps` output, header-only input, and malformed lines without throwing; the orphan filter validates each of the three filters in isolation (PPID, user, command shape); `reapOrphans` covers empty input, the SIGTERM-then-SIGKILL flow with mocked `process.kill`, the group-then-direct fallback when group-kill throws ESRCH, and the failed-count when both kills fail. **Cross-refs**: `packages/core/src/orphan-reaper.ts` (matcher + reaper), `packages/server/src/start.ts` (boot-time hook), `packages/cli/src/commands/server.ts` (`reap` subcommand). | +| 6.34 | 🔄 | Help Assistant + Product Architect launchers — ollama-routed adapter support | **Round 1 shipped 2026-05-09, post-v0.22.0**. **Surfaced 2026-05-09**: a user with `helpAssistantAgent.adapter = "claude-code-ollama"` (configured at `cfcf init` since 6.28 surfaced the new adapters in the picker) hit `Help Assistant doesn't support adapter "claude-code-ollama" yet. Supported: claude-code, codex.` when running `cfcf help assistant`. **Root cause**: the picker is general-purpose and offers every detected adapter for every role, but the PA + HA launchers (`packages/core/src/{product-architect,help-assistant}/launcher.ts`) have hardcoded `switch` statements for argv composition because they need true TUI takeover via `Bun.spawn(... { stdio: "inherit" })` — the standard `AgentAdapter.buildCommand()` pipeline assumes headless `-p` and isn't a fit. The new adapters from 6.28 (`opencode`, `claude-code-ollama`, `opencode-ollama`) were never added to those switches. **Shipped (round 1)**: added a `case "claude-code-ollama"` arm to both the HA and PA launcher switches that wraps the existing claude-code argv composition with `ollama launch claude --model --yes -- `. The `--model` flag is the ollama-side model name (claude itself doesn't get `--model` — ollama serves whichever local model). For HA: no `--dangerously-skip-permissions` (interactive Q&A — user reviews tool calls); for PA: `--dangerously-skip-permissions` flows through after `--` in non-safe mode (mirrors the direct claude-code path). 7 new unit tests across both launcher test files (argv shape, model omission, safe-mode behaviour for PA, helpful error message for opencode-ollama). The error message for the still-unsupported adapters now lists `claude-code-ollama` so users on `opencode-ollama` see the alternative, and points at this plan row. **Out of scope for round 1 (deferred)**: `opencode` and `opencode-ollama` interactive support. opencode's interactive default is to read `AGENTS.md` from cwd, which doesn't fit cf²'s ephemeral-tempfile pattern (would either pollute the user's repo or sandbox opencode away from the user's working tree). Needs investigation into opencode's runtime system-prompt-injection flag (`--prompt`? `-c instructions=...`? config-file-only?) before we can wire it cleanly. **Cross-refs**: `packages/core/src/help-assistant/launcher.ts`, `packages/core/src/product-architect/launcher.ts`. | | 6.33 | ✅ | Auto-refresh `availableOllamaModels` on server boot + manual refresh button | **Shipped 2026-05-08, post-v0.21.0**. **Surfaced 2026-05-08** during dogfood: user pulled a new ollama model, restarted the cfcf server, and the new model didn't show up in the role-picker dropdowns — because `listOllamaModels()` was only invoked at `cfcf init` (interactive setup) and `cfcf doctor` (read-only display); neither the server nor the web UI ever re-detected. **Shipped scope**: (a) **`refreshOllamaModelsInConfig()` helper** in `packages/core/src/ollama-detection.ts` — detects ollama, lists models, persists to `availableOllamaModels` if the live list differs from saved (order-insensitive comparison since `ollama list` reorders by mtime). Returns `{ models, updated, error? }`. Best-effort: never throws; surfaces the missing-ollama case via `error` field. (b) **Boot-time auto-refresh** in `packages/server/src/start.ts` — runs after the orphan reaper, single log line if list changed. (c) **`POST /api/agents/refresh-ollama-models`** endpoint — returns the same shape as the helper. Always 200 (the most common error case is "ollama not installed", which isn't an HTTP failure for this endpoint). (d) **"Refresh ollama models" button** in the Agent roles section of both web Settings (`ServerInfo.tsx`) and per-workspace Config (`ConfigDisplay.tsx`). Clicking calls the endpoint, displays a status message, and bumps a `modelsRev` counter that triggers a re-fetch of `/api/agents/models` so the `*-ollama` dropdowns pick up new entries. **Tests added**: 4 new unit tests in `ollama-detection.test.ts` (shape, no-config-no-write, list-equality persistence guard, ordering insensitivity); 2 new endpoint tests in `agent-models.test.ts` (shape, no-500-on-missing-ollama). **Cross-refs**: `packages/core/src/ollama-detection.ts`, `packages/server/src/start.ts`, `packages/server/src/routes/agent-models.ts`, `packages/web/src/api.ts`, `packages/web/src/pages/ServerInfo.tsx`, `packages/web/src/components/ConfigDisplay.tsx`. | | 6.32 | ❌ | Opencode-ollama hang-detection + reduced-deadlock surface | **Surfaced 2026-05-08** during 6.28 dogfood. Despite the documented `opencode run` "scriptable" contract + `--dangerously-skip-permissions` flag, opencode-ollama silently hangs in cf²'s harness pattern when (a) ollama's model runner is busy / has dead orphan requests in queue, (b) the prior session's hardcoded permission denies trip an internal stdin-prompt code path, or (c) opencode's stream-json over the OpenAI-compatible `/v1/chat/completions` API gets aborted server-side (500) and opencode doesn't surface the error loudly. **Symptom**: opencode process at 0% CPU, no TCP connection to ollama, log file frozen mid-session at "service=llm ... stream", cfcf log file 40 bytes. No timeout, no error, no exit. **Three angles to investigate**: *(a)* hard timeout on agent spawns in `process-manager.ts` (e.g. 15-min default per role, configurable per role) so a hung agent eventually kills itself + cfcf marks the iteration failed instead of the loop hanging forever. *(b)* Detect opencode's specific failure modes by following `~/.local/share/opencode/log/.log` in addition to stdout — opencode's internal log captures the full session lifecycle including provider errors. The cfcf log writer could optionally tail this file as a side-channel. *(c)* Test whether `--format json` on `opencode run` produces cleaner streaming events than the default formatted output (might help with the buffering UX too — sister concern to the claude-code-ollama buffering problem). **Recommendation in the meantime**: prefer `claude-code-ollama` over `opencode-ollama` for unattended roles until opencode's stability matures (when running against a local ollama backend). Update `anthropic-policy.md` with this caveat. **Cross-refs**: github/anomalyco/opencode#13851 (the permission-deny-causes-cancel-state issue we already knew about); the 2026-05-08 calc-workspace dogfood log session at `~/.local/share/opencode/log/2026-05-08T082733.log` is the canonical reproduction. **Effort**: 0.25 session for the docs caveat (immediately useful); 1–2 sessions for the spawn-timeout + opencode-log-tail investigation (defer until 6.31 ships, since the orphan cleanup is the more-bang-for-buck blocker). | diff --git a/packages/core/src/help-assistant/launcher.test.ts b/packages/core/src/help-assistant/launcher.test.ts index 43d5a12..aec4d30 100644 --- a/packages/core/src/help-assistant/launcher.test.ts +++ b/packages/core/src/help-assistant/launcher.test.ts @@ -121,4 +121,51 @@ describe("buildLaunchArgs", () => { buildLaunchArgs({ adapter: "fake-agent" as "claude-code" }, "x", "Hi"), ).toThrow(/Help Assistant doesn't support adapter "fake-agent"/); }); + + // claude-code-ollama (item 6.34) + + it("claude-code-ollama: wraps with `ollama launch claude --model X --yes --` then claude's flags", () => { + const { command, args, tempPromptFile } = buildLaunchArgs( + { adapter: "claude-code-ollama", model: "qwen3-coder:latest" }, + "HA system prompt", + "Hello", + ); + expect(command).toBe("ollama"); + // ollama-side flags + expect(args[0]).toBe("launch"); + expect(args[1]).toBe("claude"); + const modelIdx = args.indexOf("--model"); + expect(modelIdx).toBeGreaterThanOrEqual(0); + expect(args[modelIdx + 1]).toBe("qwen3-coder:latest"); + expect(args).toContain("--yes"); + // mandatory `--` separator + const sepIdx = args.indexOf("--"); + expect(sepIdx).toBeGreaterThanOrEqual(0); + // claude's flags AFTER the separator + expect(args.slice(sepIdx + 1)).toContain("--append-system-prompt"); + expect(args.slice(sepIdx + 1)).toContain("HA system prompt"); + // No --dangerously-skip-permissions (HA is interactive) + expect(args).not.toContain("--dangerously-skip-permissions"); + // firstUserMessage must be the LAST positional (Flavour A) + expect(args[args.length - 1]).toBe("Hello"); + expect(tempPromptFile).toBeNull(); + }); + + it("claude-code-ollama: omits --model when no agent.model is set", () => { + const { args } = buildLaunchArgs( + { adapter: "claude-code-ollama" }, + "x", + "Hi", + ); + expect(args).not.toContain("--model"); + // Still has `--yes --` + expect(args).toContain("--yes"); + expect(args).toContain("--"); + }); + + it("error message lists claude-code-ollama as supported (helps users on opencode-ollama)", () => { + expect(() => + buildLaunchArgs({ adapter: "opencode-ollama" as "claude-code" }, "x", "Hi"), + ).toThrow(/claude-code, codex, claude-code-ollama/); + }); }); diff --git a/packages/core/src/help-assistant/launcher.ts b/packages/core/src/help-assistant/launcher.ts index d61b0ef..0b50f20 100644 --- a/packages/core/src/help-assistant/launcher.ts +++ b/packages/core/src/help-assistant/launcher.ts @@ -147,11 +147,41 @@ export function buildLaunchArgs( args.push(firstUserMessage); // positional [PROMPT] (Flavour A) return { command: "codex", args, tempPromptFile: promptFile }; } + case "claude-code-ollama": { + // Same shape as the `claude-code` case, wrapped in `ollama launch + // claude --model --yes -- `. The + // `--model` flag here goes to OLLAMA (selects which local model + // to serve); claude itself doesn't get `--model` because ollama + // already pinned the model serving the API surface. So + // `agent.model` is the ollama-side model name (e.g. + // `qwen3-coder:latest`, `gemma4:31b`), not a Claude alias. + // + // Interactive mode (no `-p`); --append-system-prompt is a claude + // flag and passes through after the `--` separator unchanged. + // No --dangerously-skip-permissions: HA is interactive, the user + // reviews tool calls. + const ollamaArgs: string[] = ["launch", "claude"]; + if (agent.model) { + ollamaArgs.push("--model", agent.model); + } + ollamaArgs.push("--yes", "--"); + + const claudeArgs: string[] = ["--append-system-prompt", systemPrompt]; + claudeArgs.push(firstUserMessage); // positional [prompt] (Flavour A) + + return { + command: "ollama", + args: [...ollamaArgs, ...claudeArgs], + tempPromptFile: null, + }; + } default: throw new Error( `Help Assistant doesn't support adapter "${agent.adapter}" yet. ` + - `Supported: claude-code, codex. ` + - `Set helpAssistantAgent in your config (cfcf config edit) to one of those.`, + `Supported: claude-code, codex, claude-code-ollama. ` + + `(opencode + opencode-ollama interactive support is a known follow-up — ` + + `their system-prompt-injection flag is non-trivial; see plan item 6.34.) ` + + `Set helpAssistantAgent in your config (cfcf config edit) to one of the supported adapters.`, ); } } diff --git a/packages/core/src/product-architect/launcher.test.ts b/packages/core/src/product-architect/launcher.test.ts index aeadda0..2c7f05e 100644 --- a/packages/core/src/product-architect/launcher.test.ts +++ b/packages/core/src/product-architect/launcher.test.ts @@ -137,4 +137,61 @@ describe("buildLaunchArgs (Pattern A)", () => { // /cfcf-docs/AGENTS.md). expect(cx.tempPromptFile).not.toContain("cfcf-docs"); }); + + // claude-code-ollama (item 6.34) + + it("claude-code-ollama: wraps with `ollama launch claude --model X --yes --` then claude flags", () => { + const out = buildLaunchArgs( + { adapter: "claude-code-ollama", model: "qwen3-coder:latest" }, + "PA system prompt", + "Hello", + ); + expect(out.command).toBe("ollama"); + expect(out.tempPromptFile).toBeNull(); + expect(out.args[0]).toBe("launch"); + expect(out.args[1]).toBe("claude"); + const modelIdx = out.args.indexOf("--model"); + expect(modelIdx).toBeGreaterThanOrEqual(0); + expect(out.args[modelIdx + 1]).toBe("qwen3-coder:latest"); + expect(out.args).toContain("--yes"); + const sepIdx = out.args.indexOf("--"); + expect(sepIdx).toBeGreaterThanOrEqual(0); + // claude's flags AFTER the separator + const after = out.args.slice(sepIdx + 1); + expect(after).toContain("--append-system-prompt"); + expect(after).toContain("PA system prompt"); + // Default (non-safe) mode passes --dangerously-skip-permissions + // through to claude. + expect(after).toContain("--dangerously-skip-permissions"); + // firstUserMessage must be the LAST positional (Flavour A) + expect(out.args[out.args.length - 1]).toBe("Hello"); + }); + + it("claude-code-ollama in safe mode: drops --dangerously-skip-permissions", () => { + const out = buildLaunchArgs( + { adapter: "claude-code-ollama", model: "qwen3-coder:latest" }, + "PA", + "Hello", + true, // safe = true + ); + expect(out.args).not.toContain("--dangerously-skip-permissions"); + }); + + it("claude-code-ollama: omits --model when no agent.model is set", () => { + const out = buildLaunchArgs( + { adapter: "claude-code-ollama" }, + "PA", + "Hello", + ); + expect(out.args).not.toContain("--model"); + // Still has `--yes --` + expect(out.args).toContain("--yes"); + expect(out.args).toContain("--"); + }); + + it("error message lists claude-code-ollama as supported (so users on opencode-ollama see the alternative)", () => { + expect(() => + buildLaunchArgs({ adapter: "opencode-ollama" }, "x", "Hello"), + ).toThrow(/claude-code, codex, claude-code-ollama/); + }); }); diff --git a/packages/core/src/product-architect/launcher.ts b/packages/core/src/product-architect/launcher.ts index 35d9dc3..9a17a17 100644 --- a/packages/core/src/product-architect/launcher.ts +++ b/packages/core/src/product-architect/launcher.ts @@ -176,11 +176,45 @@ export function buildLaunchArgs( args.push(firstUserMessage); // positional [PROMPT] return { command: "codex", args, tempPromptFile: promptFile }; } + case "claude-code-ollama": { + // Same shape as the `claude-code` case, wrapped in + // `ollama launch claude --model --yes -- `. + // The `--model` here goes to OLLAMA (selects which local model to + // serve); claude itself doesn't get `--model` because ollama + // already pinned the model serving the API surface. So + // `agent.model` is the ollama-side model name (e.g. + // `qwen3-coder:latest`, `gemma4:31b`), not a Claude alias. + // + // Permissions + system-prompt injection work the same way as the + // direct claude-code case — the flags passed after `--` are + // claude's own flags, so `--append-system-prompt` and + // `--dangerously-skip-permissions` (in non-safe mode) flow + // through the ollama launch wrapper unchanged. + const ollamaArgs: string[] = ["launch", "claude"]; + if (agent.model) { + ollamaArgs.push("--model", agent.model); + } + ollamaArgs.push("--yes", "--"); + + const claudeArgs: string[] = ["--append-system-prompt", systemPrompt]; + if (!safe) { + claudeArgs.push("--dangerously-skip-permissions"); + } + claudeArgs.push(firstUserMessage); // positional [prompt] (Flavour A) + + return { + command: "ollama", + args: [...ollamaArgs, ...claudeArgs], + tempPromptFile: null, + }; + } default: throw new Error( `Product Architect doesn't support adapter "${agent.adapter}" yet. ` + - `Supported: claude-code, codex. ` + - `Set productArchitectAgent in your config (cfcf config edit) to one of those.`, + `Supported: claude-code, codex, claude-code-ollama. ` + + `(opencode + opencode-ollama interactive support is a known follow-up — ` + + `their system-prompt-injection flag is non-trivial; see plan item 6.34.) ` + + `Set productArchitectAgent in your config (cfcf config edit) to one of the supported adapters.`, ); } }