The tool source generated from the schema, the home with its view, the shared and per-process environments, the permission rules, the commands, the warm-up under the supervisor, the event stream and the classification, its patterns as hand-written scanners. The goldens record the answer for each, a warm-up against a fake opencode included. A timed-out supervised command now gets SIGTERM, so the supervisor takes its tree down (SIGKILL 10 s later); any other child gets SIGKILL. :Json gains DumpCompact. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
78 lines
5.2 KiB
C++
78 lines
5.2 KiB
C++
// 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);
|
|
}
|