Crafter.Review/interfaces/Crafter.Review-OpenCode.cppm

78 lines
5.2 KiB
Text
Raw Permalink Normal View History

// 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<std::string_view, 2> 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<std::string, std::string> 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();
// <scratch>/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<const std::string> 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<std::string, std::string> BaseEnv(const std::filesystem::path& home, std::string_view key,
const std::optional<std::map<std::string, std::string>>& base = std::nullopt);
// The claude harness's --allowedTools entries as OpenCode permission rules:
// Read/Glob/Grep allowed, every `Bash(<prefix>:*)` the bash pattern
// `<prefix>*`, everything else denied (the last matching rule wins, so the
// catch-all comes first).
Json Permissions(std::span<const std::string> 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<std::string, std::string> ChildEnv(const std::map<std::string, std::string>& base, const std::filesystem::path& systemPath, const std::filesystem::path& soPath,
std::span<const std::string> allowList, const std::filesystem::path& dataDir);
// An empty variant passes none.
std::vector<std::string> 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<bool, std::string> Warm(const std::string& bin, const std::filesystem::path& home, const std::map<std::string, std::string>& env, std::string_view model,
const std::function<void(const std::string&)>& 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<std::filesystem::path>& soPath = std::nullopt);
// (status, reason) as Stream::Classify: ok | limit | credits | auth |
// timeout | error.
std::pair<std::string, std::string> Classify(const Json& parsed, std::int64_t exitCode, std::string_view stderrText, bool timedOut = false);
}