// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® export module Crafter.Review:OpenCode; import std; import :Json; // The OpenCode harness (2026-10-12): a lens pass as an `opencode run` // process instead of `claude --print`, for models that are not Anthropic's - // the lane for the hours when every Anthropic seat is out. A lens whose // environment needs an MCP server never runs here. The pass keeps its // prompts byte for byte: the appended system prompt becomes the agent's // prompt (`{file:...}` in an inline config), the stdin user message its // positional message, the read-only tool allowlist its permission rules, and // the structured output a custom tool with the same schema that writes the // result to a file the driver reads. // // OpenCode runs in a HOME of its own under the scratch dir: config, the custom // tool and the SDK packages it installs on first use (warmed once before the // pool starts), one data dir per process (its sqlite session store is never // shared), and a view directory as the project - symlinks to the repository's // top-level entries, so no instruction file of the repository root reaches the // model. export namespace Crafter::Review::OpenCode { // The view links every other top-level entry of the consumer's checkout. constexpr std::array ViewSkip = {".git", "node_modules"}; constexpr std::string_view Agent = "lens"; constexpr std::string_view Tool = "StructuredOutput"; constexpr std::string_view SoEnv = "REVIEW_GATES_STRUCTURED_OUT"; // The model's reasoning variants are low/medium/high (OpenRouter // `reasoning.effort`). constexpr std::string_view DefaultVariant = "high"; // Provider routing for OpenRouter models (Jorijn, 2026-10-12): zero data // retention only, fp8 the lowest precision, cheapest first. Json Routing(); // REVIEW_GATES_OPENCODE_BIN, else `opencode` on PATH. std::string DefaultBin(); // `openrouter/xiaomi/mimo-v2.6-pro` -> (openrouter, xiaomi/mimo-v2.6-pro); a // bare id is OpenRouter's. std::pair ModelId(std::string_view model); // StructuredOutput.ts: the lens result schema as the tool's zod arguments // (one tool for block and whole passes: the whole-only fields optional). std::string ToolSource(); // /opencode: the XDG home (config + tool), cache and state dirs, // the view dir. Idempotent. std::filesystem::path PrepareHome(const std::filesystem::path& scratch, const std::filesystem::path& repo, std::span models, const Json& routing = Routing()); // The environment every OpenCode process of the run shares: its own XDG // home, the OpenRouter key, no Anthropic credential or endpoint, no // auto-update. nullopt: this process's environment. std::map BaseEnv(const std::filesystem::path& home, std::string_view key, const std::optional>& base = std::nullopt); // The claude harness's --allowedTools entries as OpenCode permission rules: // Read/Glob/Grep allowed, every `Bash(:*)` the bash pattern // `*`, everything else denied (the last matching rule wins, so the // catch-all comes first). Json Permissions(std::span allowList); // One process: the agent (its prompt the pass's system prompt file), its // permissions, the structured-output file, a data dir of its own. std::map ChildEnv(const std::map& base, const std::filesystem::path& systemPath, const std::filesystem::path& soPath, std::span allowList, const std::filesystem::path& dataDir); // An empty variant passes none. std::vector Cmd(std::string_view bin, std::string_view model, const std::filesystem::path& view, std::string_view title, std::string_view variant = {}, std::string_view prompt = {}); // One short run before the pool: OpenCode installs the provider SDK into // the config dir on first use (minutes; a wave of concurrent first runs // would all do it). (ok, the tail of its stderr or why it failed). std::pair Warm(const std::string& bin, const std::filesystem::path& home, const std::map& env, std::string_view model, const std::function& log, std::int64_t timeoutS = 600); // The `--format json` event stream as Stream::ParseStream gives the claude // one: usage totals, steps as turns, tool calls and errors, OpenCode's own // cost, and the structured output - the last StructuredOutput call's // arguments, the file it wrote the fallback. Json ParseEvents(std::string_view text, const std::optional& soPath = std::nullopt); // (status, reason) as Stream::Classify: ok | limit | credits | auth | // timeout | error. std::pair Classify(const Json& parsed, std::int64_t exitCode, std::string_view stderrText, bool timedOut = false); }