@kal-elsam/kairo-runtime 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +76 -0
- package/package.json +2 -1
- package/scripts/cockpit-smoke.mjs +1 -1
- package/scripts/ux-smoke-test.sh +3 -3
- package/src/cli.js +106 -11
- package/src/global/agent-capabilities/create-capability-adapter.js +2 -2
- package/src/global/architect/architect-cli.js +76 -0
- package/src/global/architect/architect-codex.js +146 -0
- package/src/global/architect/architect-manager.js +125 -0
- package/src/global/architect/architect-store.js +377 -0
- package/src/global/architect/architect-types.js +47 -0
- package/src/global/cli-help.js +12 -1
- package/src/global/cockpit/app.js +475 -0
- package/src/global/cockpit/card.js +111 -0
- package/src/global/cockpit/cli.js +33 -0
- package/src/global/cockpit/gauge.js +31 -0
- package/src/global/cockpit/project-overlay.js +683 -0
- package/src/global/cockpit/rows.js +148 -0
- package/src/global/cockpit/theme.js +118 -0
- package/src/global/cockpit/view.js +1263 -0
- package/src/global/control-plane/attention.js +141 -0
- package/src/global/control-plane/build-report.js +146 -0
- package/src/global/control-plane/cli.js +36 -0
- package/src/global/control-plane/constants.js +38 -0
- package/src/global/control-plane/gentle-adapters.js +183 -0
- package/src/global/control-plane/provider.js +69 -0
- package/src/global/control-plane/review-status.js +115 -0
- package/src/global/control-plane/sdd-status.js +49 -0
- package/src/global/control-plane/team.js +63 -0
- package/src/global/conversation/bootstrap-analyzer-adapters.js +251 -0
- package/src/global/conversation/cli.js +53 -0
- package/src/global/conversation/codex-sandbox.js +230 -0
- package/src/global/conversation/cursor-sandbox.js +215 -0
- package/src/global/conversation/project-analysis.js +204 -0
- package/src/global/conversation/project-profile.js +178 -0
- package/src/global/conversation/project-router.js +149 -0
- package/src/global/conversation/project-strategy-store.js +64 -0
- package/src/global/conversation/project-strategy.js +514 -0
- package/src/global/conversation/sanitized-snapshot.js +169 -0
- package/src/global/conversation/secret-scanner.js +71 -0
- package/src/global/conversation/service.js +1063 -0
- package/src/global/conversation/session-store.js +75 -0
- package/src/global/conversation/transcript-store.js +79 -0
- package/src/global/conversation/ui.js +195 -0
- package/src/global/intelligence/capability-scoring.js +480 -0
- package/src/global/intelligence/execution-router.js +444 -0
- package/src/global/intelligence/kairo-telemetry-source.js +59 -0
- package/src/global/intelligence/kairobench-runner.js +85 -0
- package/src/global/intelligence/kairobench-source.js +34 -0
- package/src/global/intelligence/kairobench-tasks.js +47 -0
- package/src/global/intelligence/model-candidate-catalog.js +456 -0
- package/src/global/intelligence/model-capability-registry-sources.js +145 -0
- package/src/global/intelligence/model-capability-registry.js +125 -0
- package/src/global/intelligence/model-intelligence.js +1646 -0
- package/src/global/intelligence/official-benchmark-snapshots.js +162 -0
- package/src/global/intelligence/quick-ask.js +149 -0
- package/src/global/intelligence/role-profiles.js +251 -0
- package/src/global/intelligence/skill-catalog.js +67 -0
- package/src/global/intelligence/subscription-pressure-source.js +41 -0
- package/src/global/mcp/kairo-mcp.js +51 -18
- package/src/global/mcp/work-snapshot-rule.js +4 -2
- package/src/global/mcp/workspace-binding.js +88 -0
- package/src/global/mcp/workspace-mcp-entry.js +74 -0
- package/src/global/mcp-install.js +8 -1
- package/src/global/observability/artificial-analysis-models.js +118 -0
- package/src/global/observability/claude-models.js +31 -0
- package/src/global/observability/claude-usage.js +112 -0
- package/src/global/observability/codex-models.js +96 -0
- package/src/global/observability/codex-usage.js +160 -0
- package/src/global/observability/cursor-auth.js +88 -0
- package/src/global/observability/cursor-models.js +101 -0
- package/src/global/observability/gentle-probe.js +30 -2
- package/src/global/observability/huggingface-leaderboard.js +97 -0
- package/src/global/observability/index.js +2 -1
- package/src/global/observability/opencode-models.js +101 -0
- package/src/global/observability/opencode-usage.js +162 -0
- package/src/global/paths.js +49 -2
- package/src/global/profile.js +23 -1
- package/src/global/runtime/execution-adapters/claude.js +63 -30
- package/src/global/runtime/execution-adapters/codex.js +9 -2
- package/src/global/runtime/execution-adapters/create-execution-adapter.js +6 -1
- package/src/global/runtime/execution-adapters/opencode.js +83 -18
- package/src/global/runtime/execution-worktree-manager.js +924 -0
- package/src/global/runtime/execution-worktree-orchestrator.js +194 -0
- package/src/global/runtime/execution-worktree-store.js +83 -0
- package/src/global/runtime/execution-worktree-types.js +45 -0
- package/src/global/runtime/run-events.js +38 -0
- package/src/global/runtime/run-manager.js +22 -6
- package/src/global/runtime/run-supervisor.js +41 -12
- package/src/global/runtime/usage-manager.js +96 -0
- package/src/global/runtime/usage-store.js +69 -0
- package/src/global/runtime/usage-types.js +62 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { spawn as defaultSpawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
// Real per-provider model catalog via `opencode models <provider> --verbose`
|
|
4
|
+
// — covers both OpenCode Go (provider "opencode-go") and OpenCode Zen
|
|
5
|
+
// (provider "opencode"), the two providers Kairo actually routes through.
|
|
6
|
+
// Same fail-closed contract as codex-models.js: any spawn/parse error or
|
|
7
|
+
// empty catalog yields `unknown`, never a fabricated model list.
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 8_000;
|
|
9
|
+
|
|
10
|
+
function unknown(provider, error = null) {
|
|
11
|
+
return { status: "unknown", source: `opencode models ${provider} --verbose`, provider, models: [], error: error ? String(error) : null };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parses `opencode models <provider> --verbose` output: a "provider/model"
|
|
16
|
+
* header line followed by a pretty-printed JSON object, repeated per model.
|
|
17
|
+
* Strips the header lines and brace-matches the remaining text into
|
|
18
|
+
* complete JSON objects — robust to the exact indentation/whitespace the
|
|
19
|
+
* CLI uses, since it never assumes a fixed line count per model.
|
|
20
|
+
*/
|
|
21
|
+
export function parseOpenCodeModelsVerbose(text) {
|
|
22
|
+
const lines = String(text ?? "").split("\n");
|
|
23
|
+
const jsonText = lines.filter((line) => !/^[\w.-]+\/[\w.-]+$/.test(line.trim())).join("\n");
|
|
24
|
+
const models = [];
|
|
25
|
+
let depth = 0;
|
|
26
|
+
let start = -1;
|
|
27
|
+
for (let i = 0; i < jsonText.length; i += 1) {
|
|
28
|
+
const char = jsonText[i];
|
|
29
|
+
if (char === "{") {
|
|
30
|
+
if (depth === 0) start = i;
|
|
31
|
+
depth += 1;
|
|
32
|
+
} else if (char === "}") {
|
|
33
|
+
depth -= 1;
|
|
34
|
+
if (depth === 0 && start !== -1) {
|
|
35
|
+
try { models.push(JSON.parse(jsonText.slice(start, i + 1))); } catch { /* skip an unparseable entry, don't fail the whole catalog */ }
|
|
36
|
+
start = -1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return models;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeModel(entry) {
|
|
44
|
+
return {
|
|
45
|
+
id: entry.id,
|
|
46
|
+
providerID: entry.providerID,
|
|
47
|
+
displayName: entry.name ?? entry.id,
|
|
48
|
+
status: entry.status ?? "unknown",
|
|
49
|
+
costInputPerMTok: entry.cost?.input ?? null,
|
|
50
|
+
costOutputPerMTok: entry.cost?.output ?? null,
|
|
51
|
+
contextWindow: entry.limit?.context ?? null,
|
|
52
|
+
maxOutput: entry.limit?.output ?? null,
|
|
53
|
+
supportsReasoning: entry.capabilities?.reasoning === true,
|
|
54
|
+
supportsToolCall: entry.capabilities?.toolcall === true
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Reads the real, currently-listed model catalog for one OpenCode provider
|
|
60
|
+
* ("opencode-go" or "opencode" for Zen). Requires the `opencode` CLI to be
|
|
61
|
+
* installed and authenticated — same as the rest of the OpenCode adapter.
|
|
62
|
+
*/
|
|
63
|
+
export async function readOpenCodeModels({
|
|
64
|
+
provider,
|
|
65
|
+
spawn = defaultSpawn,
|
|
66
|
+
cwd = process.cwd(),
|
|
67
|
+
env = process.env,
|
|
68
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
69
|
+
} = {}) {
|
|
70
|
+
if (!provider) return unknown("unknown", "provider is required");
|
|
71
|
+
|
|
72
|
+
let child;
|
|
73
|
+
try {
|
|
74
|
+
child = spawn("opencode", ["models", provider, "--verbose"], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return unknown(provider, error?.message ?? error);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
let stdout = "";
|
|
81
|
+
let finished = false;
|
|
82
|
+
const timer = setTimeout(() => finish(unknown(provider, "opencode models timed out")), timeoutMs);
|
|
83
|
+
|
|
84
|
+
function finish(result) {
|
|
85
|
+
if (finished) return;
|
|
86
|
+
finished = true;
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
try { child.kill?.(); } catch { /* best effort */ }
|
|
89
|
+
resolve(result);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
child.stdout?.on("data", (chunk) => { stdout += chunk; });
|
|
93
|
+
child.once?.("error", (error) => finish(unknown(provider, error?.message ?? error)));
|
|
94
|
+
child.once?.("close", (code) => {
|
|
95
|
+
if (code !== 0 && !stdout.trim()) return finish(unknown(provider, `opencode models exited with code ${code}`));
|
|
96
|
+
const models = parseOpenCodeModelsVerbose(stdout).map(normalizeModel);
|
|
97
|
+
if (models.length === 0) return finish(unknown(provider, "no models parsed"));
|
|
98
|
+
finish({ status: "measured", source: `opencode models ${provider} --verbose`, provider, models, error: null });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { spawn as defaultSpawn } from "node:child_process";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { resolveHomeDir } from "../paths.js";
|
|
4
|
+
|
|
5
|
+
const GO_URL = "https://opencode.ai/zen/go/v1/usage";
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 4000;
|
|
7
|
+
// `opencode stats` scans local session history and can take several seconds on
|
|
8
|
+
// larger databases. It runs at most once per five-minute cache window.
|
|
9
|
+
const STATS_TIMEOUT_MS = 20_000;
|
|
10
|
+
|
|
11
|
+
function unknown(error = null, source = "opencode usage") {
|
|
12
|
+
return { status: "unknown", source, windows: [], error: error ? String(error).replace(/(key|token|secret|authorization)[^\s]*/ig, "$1=[redacted]") : null };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function clamp(value) {
|
|
16
|
+
const n = Number(value);
|
|
17
|
+
return Number.isFinite(n) ? Math.max(0, Math.min(100, n)) : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function normalizeOpenCodeGoUsage(payload) {
|
|
21
|
+
const usage = payload?.usage;
|
|
22
|
+
if (!usage || typeof usage !== "object") return null;
|
|
23
|
+
const windows = [];
|
|
24
|
+
for (const [name, value] of Object.entries(usage)) {
|
|
25
|
+
const percent = clamp(value?.percent);
|
|
26
|
+
if (percent == null) continue;
|
|
27
|
+
windows.push({
|
|
28
|
+
name,
|
|
29
|
+
usedPercent: percent,
|
|
30
|
+
remainingPercent: 100 - percent,
|
|
31
|
+
resetsAt: typeof value.resetsAt === "string" ? value.resetsAt : null,
|
|
32
|
+
status: value.status === "rate-limited" ? "rate-limited" : "ok"
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (!windows.length) return null;
|
|
36
|
+
return {
|
|
37
|
+
status: windows.some((window) => window.status === "rate-limited") ? "rate-limited" : "measured",
|
|
38
|
+
source: GO_URL,
|
|
39
|
+
windows,
|
|
40
|
+
primary: windows.find((window) => window.name === "rolling") ?? null,
|
|
41
|
+
secondary: windows.find((window) => window.name === "weekly") ?? null,
|
|
42
|
+
monthly: windows.find((window) => window.name === "monthly") ?? null
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function readGoUsage({ readFile, fetchImpl = globalThis.fetch, authPath, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
47
|
+
let auth;
|
|
48
|
+
try { auth = JSON.parse(await readFile(authPath, "utf8")); } catch (error) { return unknown(error?.code === "ENOENT" ? "OpenCode Go auth not found" : "invalid OpenCode auth", GO_URL); }
|
|
49
|
+
const credential = auth?.["opencode-go"] ?? (auth?.type === "api" ? auth : null);
|
|
50
|
+
const key = credential?.type === "api" && typeof credential.key === "string" ? credential.key : null;
|
|
51
|
+
if (!key) return unknown("OpenCode Go credential unavailable", GO_URL);
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
54
|
+
try {
|
|
55
|
+
const response = await fetchImpl(GO_URL, { method: "GET", headers: { Authorization: `Bearer ${key}` }, signal: controller.signal });
|
|
56
|
+
if (!response?.ok) return unknown(`OpenCode Go usage HTTP ${response?.status ?? "error"}`, GO_URL);
|
|
57
|
+
const parsed = await response.json();
|
|
58
|
+
return normalizeOpenCodeGoUsage(parsed) ?? unknown("unusable OpenCode Go usage", GO_URL);
|
|
59
|
+
} catch (error) { return unknown(error?.name === "AbortError" ? "OpenCode Go usage timeout" : "OpenCode Go usage request failed", GO_URL); }
|
|
60
|
+
finally { clearTimeout(timer); }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Parse only rows explicitly belonging to OpenCode providers; this is local history, not billing. */
|
|
64
|
+
export function parseOpenCodeStats(text) {
|
|
65
|
+
const stripAnsi = (value) => String(value).replace(/\x1b\[[0-?]*[ -\/]*[@-~]/g, "");
|
|
66
|
+
const parseAmount = (value) => {
|
|
67
|
+
const match = String(value).replace(/,/g, "").match(/([\d.]+)\s*([KMB])?/i);
|
|
68
|
+
if (!match) return null;
|
|
69
|
+
const multiplier = { K: 1e3, M: 1e6, B: 1e9 }[String(match[2] ?? "").toUpperCase()] ?? 1;
|
|
70
|
+
return Number(match[1]) * multiplier;
|
|
71
|
+
};
|
|
72
|
+
const records = [];
|
|
73
|
+
let current = null;
|
|
74
|
+
const push = () => {
|
|
75
|
+
if (!current) return;
|
|
76
|
+
current.tokens = (current.inputTokens ?? 0) + (current.outputTokens ?? 0)
|
|
77
|
+
+ (current.cacheRead ?? 0) + (current.cacheWrite ?? 0) || null;
|
|
78
|
+
if (current.cost != null || current.tokens != null) records.push(current);
|
|
79
|
+
current = null;
|
|
80
|
+
};
|
|
81
|
+
for (const rawLine of String(text ?? "").split(/\r?\n/)) {
|
|
82
|
+
const line = stripAnsi(rawLine);
|
|
83
|
+
const model = line.match(/(?:│|\|)?\s*((?:opencode-go|opencode)\/[^\s│|]+)/i)?.[1];
|
|
84
|
+
if (model) {
|
|
85
|
+
push();
|
|
86
|
+
current = { model, provider: model.startsWith("opencode-go/") ? "opencode-go" : "opencode", cost: null, inputTokens: null, outputTokens: null, cacheRead: null, cacheWrite: null };
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!current) continue;
|
|
90
|
+
const input = line.match(/Input Tokens\s+([\d.,]+\s*[KMB]?)/i)?.[1];
|
|
91
|
+
const output = line.match(/Output Tokens\s+([\d.,]+\s*[KMB]?)/i)?.[1];
|
|
92
|
+
const cost = line.match(/Cost\s+\$\s*([\d.,]+\s*[KMB]?)/i)?.[1];
|
|
93
|
+
if (input) current.inputTokens = parseAmount(input);
|
|
94
|
+
if (output) current.outputTokens = parseAmount(output);
|
|
95
|
+
const cacheRead = line.match(/Cache Read\s+([\d.,]+\s*[KMB]?)/i)?.[1];
|
|
96
|
+
const cacheWrite = line.match(/Cache Write\s+([\d.,]+\s*[KMB]?)/i)?.[1];
|
|
97
|
+
if (cacheRead) current.cacheRead = parseAmount(cacheRead);
|
|
98
|
+
if (cacheWrite) current.cacheWrite = parseAmount(cacheWrite);
|
|
99
|
+
if (cost) current.cost = parseAmount(cost);
|
|
100
|
+
}
|
|
101
|
+
push();
|
|
102
|
+
const zenRecords = records.filter((record) => record.provider === "opencode");
|
|
103
|
+
if (!zenRecords.length) return null;
|
|
104
|
+
return {
|
|
105
|
+
status: "local_recorded",
|
|
106
|
+
source: "opencode stats --days 7 --models",
|
|
107
|
+
billing: "PAYG",
|
|
108
|
+
balance: "unknown",
|
|
109
|
+
autoReload: "unknown",
|
|
110
|
+
kairoPolicy: "PAYG blocked",
|
|
111
|
+
days: 7,
|
|
112
|
+
records: zenRecords,
|
|
113
|
+
goRecords: records.filter((record) => record.provider === "opencode-go"),
|
|
114
|
+
totalCost: zenRecords.reduce((sum, record) => sum + (record.cost ?? 0), 0),
|
|
115
|
+
totalTokens: zenRecords.reduce((sum, record) => sum + (record.tokens ?? 0), 0)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function readStats({ spawn = defaultSpawn, timeoutMs = STATS_TIMEOUT_MS } = {}) {
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
let child;
|
|
122
|
+
try { child = spawn("opencode", ["stats", "--days", "7", "--models"], { stdio: ["ignore", "pipe", "pipe"] }); }
|
|
123
|
+
catch { resolve(unknown("OpenCode stats unavailable", "opencode stats --days 7 --models")); return; }
|
|
124
|
+
let output = "";
|
|
125
|
+
let settled = false;
|
|
126
|
+
const timer = setTimeout(() => finish(unknown("OpenCode stats timeout", "opencode stats --days 7 --models")), timeoutMs);
|
|
127
|
+
const finish = (value) => { if (settled) return; settled = true; clearTimeout(timer); try { child.kill?.(); } catch {} resolve(value); };
|
|
128
|
+
child.stdout?.on("data", (chunk) => { output += String(chunk).slice(0, 100_000 - output.length); });
|
|
129
|
+
child.once?.("error", () => finish(unknown("OpenCode stats failed", "opencode stats --days 7 --models")));
|
|
130
|
+
child.once?.("close", () => finish(parseOpenCodeStats(output) ?? unknown("unparseable OpenCode stats", "opencode stats --days 7 --models")));
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function readOpenCodeGoUsage({
|
|
135
|
+
readFile = (path, encoding) => import("node:fs/promises").then((fs) => fs.readFile(path, encoding)),
|
|
136
|
+
fetchImpl = globalThis.fetch,
|
|
137
|
+
homeDir = resolveHomeDir(),
|
|
138
|
+
authPath = join(homeDir, ".local/share/opencode/auth.json"),
|
|
139
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
140
|
+
} = {}) {
|
|
141
|
+
return readGoUsage({ readFile, fetchImpl, authPath, timeoutMs });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function readOpenCodeStats({ spawn = defaultSpawn, timeoutMs = STATS_TIMEOUT_MS } = {}) {
|
|
145
|
+
return readStats({ spawn, timeoutMs });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function readOpenCodeUsage({
|
|
149
|
+
readFile = (path, encoding) => import("node:fs/promises").then((fs) => fs.readFile(path, encoding)),
|
|
150
|
+
fetchImpl = globalThis.fetch,
|
|
151
|
+
spawn = defaultSpawn,
|
|
152
|
+
homeDir = resolveHomeDir(),
|
|
153
|
+
authPath = join(homeDir, ".local/share/opencode/auth.json"),
|
|
154
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
155
|
+
statsTimeoutMs = STATS_TIMEOUT_MS
|
|
156
|
+
} = {}) {
|
|
157
|
+
const [go, zen] = await Promise.all([
|
|
158
|
+
readOpenCodeGoUsage({ readFile, fetchImpl, authPath, homeDir, timeoutMs }),
|
|
159
|
+
readOpenCodeStats({ spawn, timeoutMs: statsTimeoutMs })
|
|
160
|
+
]);
|
|
161
|
+
return { go, zen };
|
|
162
|
+
}
|
package/src/global/paths.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
3
3
|
|
|
4
4
|
export const HARNESS_DIR_NAME = ".harness";
|
|
5
5
|
|
|
@@ -24,7 +24,11 @@ export function harnessHomePaths(homeDir) {
|
|
|
24
24
|
monitorStatePath: join(root, "monitor", "state.json"),
|
|
25
25
|
coreDir: join(root, "core"),
|
|
26
26
|
backupsDir: join(root, "backups"),
|
|
27
|
-
sessionsDir: join(root, "sessions")
|
|
27
|
+
sessionsDir: join(root, "sessions"),
|
|
28
|
+
worktreesDir: join(root, "worktrees"),
|
|
29
|
+
usageDir: join(root, "usage"),
|
|
30
|
+
modelIntelligencePath: join(root, "model-intelligence.json"),
|
|
31
|
+
huggingfaceLeaderboardPath: join(root, "huggingface-leaderboard.json")
|
|
28
32
|
};
|
|
29
33
|
}
|
|
30
34
|
|
|
@@ -39,3 +43,46 @@ export function runPaths(homeDir, runId) {
|
|
|
39
43
|
transcriptPath: join(runDir, "transcript.jsonl")
|
|
40
44
|
};
|
|
41
45
|
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The exact shape createWorktreeId() (execution-worktree-types.js)
|
|
49
|
+
* produces — `wt_` plus two base36 segments. Enforced here, at the one
|
|
50
|
+
* real boundary every worktree path gets built from, rather than trusted
|
|
51
|
+
* by each caller separately: an unvalidated id (e.g. "../../escape")
|
|
52
|
+
* would otherwise let `join()` resolve outside worktreesDir entirely.
|
|
53
|
+
*/
|
|
54
|
+
export function assertWorktreeId(worktreeId) {
|
|
55
|
+
if (typeof worktreeId !== "string" || !/^wt_[a-z0-9]{1,20}_[a-z0-9]{1,20}$/.test(worktreeId)) {
|
|
56
|
+
throw new Error(`Invalid worktree id "${worktreeId ?? ""}".`);
|
|
57
|
+
}
|
|
58
|
+
return worktreeId;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Every real path an execution worktree needs — the real checked-out git
|
|
63
|
+
* worktree itself (`treePath`, never the project's own directory) plus its
|
|
64
|
+
* own metadata, all scoped under this one worktreeDir so a single
|
|
65
|
+
* `rm -rf` (see execution-worktree-manager.js's rollback) fully undoes an
|
|
66
|
+
* incomplete creation. `worktreeId` is validated here, the one real
|
|
67
|
+
* boundary every consumer of this function goes through — never trusted
|
|
68
|
+
* or re-validated ad hoc by callers.
|
|
69
|
+
*/
|
|
70
|
+
export function worktreePaths(homeDir, worktreeId) {
|
|
71
|
+
assertWorktreeId(worktreeId);
|
|
72
|
+
const { worktreesDir } = harnessHomePaths(homeDir);
|
|
73
|
+
const worktreeDir = join(worktreesDir, worktreeId);
|
|
74
|
+
// Defense in depth alongside the regex above — the same real containment
|
|
75
|
+
// check architect-store.js's taskPaths already applies to task ids.
|
|
76
|
+
const rel = relative(worktreesDir, worktreeDir);
|
|
77
|
+
const isInside = rel !== "" && !isAbsolute(rel) && rel !== ".." && !rel.startsWith("../");
|
|
78
|
+
if (!isInside) {
|
|
79
|
+
throw new Error(`Worktree path escapes worktreesDir for id "${worktreeId}".`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
worktreeDir,
|
|
84
|
+
treePath: join(worktreeDir, "tree"),
|
|
85
|
+
statePath: join(worktreeDir, "state.json"),
|
|
86
|
+
checkpointsPath: join(worktreeDir, "checkpoints.jsonl")
|
|
87
|
+
};
|
|
88
|
+
}
|
package/src/global/profile.js
CHANGED
|
@@ -5,6 +5,7 @@ import { harnessHomePaths } from "./paths.js";
|
|
|
5
5
|
import { AGENT_CAPABILITY_IDS } from "./agent-capabilities/index.js";
|
|
6
6
|
import { classifyCustomBaseUrl, isValidEnvironmentName } from "./intelligence/custom-url.js";
|
|
7
7
|
import { validateRuntimeProfile, RUNTIME_PROFILE_KEYS } from "./runtime/run-profile.js";
|
|
8
|
+
import { EXECUTION_ADAPTER_IDS } from "./runtime/execution-adapters/index.js";
|
|
8
9
|
|
|
9
10
|
export const PROFILE_KEYS = new Set([
|
|
10
11
|
"coordinator",
|
|
@@ -17,6 +18,7 @@ export const PROFILE_KEYS = new Set([
|
|
|
17
18
|
"tokenBudget",
|
|
18
19
|
"stableContextBudget",
|
|
19
20
|
"requestContextBudget",
|
|
21
|
+
"providerTokenBudgets",
|
|
20
22
|
"customProviders",
|
|
21
23
|
...RUNTIME_PROFILE_KEYS
|
|
22
24
|
]);
|
|
@@ -32,6 +34,7 @@ export const DEFAULT_PROFILE = {
|
|
|
32
34
|
tokenBudget: null,
|
|
33
35
|
stableContextBudget: null,
|
|
34
36
|
requestContextBudget: null,
|
|
37
|
+
providerTokenBudgets: {},
|
|
35
38
|
customProviders: [],
|
|
36
39
|
agentAliases: {},
|
|
37
40
|
modelAliases: {},
|
|
@@ -112,6 +115,7 @@ export function buildProfileJson(resolved) {
|
|
|
112
115
|
tokenBudget: profile.tokenBudget,
|
|
113
116
|
stableContextBudget: profile.stableContextBudget,
|
|
114
117
|
requestContextBudget: profile.requestContextBudget,
|
|
118
|
+
providerTokenBudgets: profile.providerTokenBudgets ?? {},
|
|
115
119
|
customProviders: sanitizeCustomProviders(profile.customProviders),
|
|
116
120
|
agentAliases: profile.agentAliases ?? {},
|
|
117
121
|
modelAliases: profile.modelAliases ?? {},
|
|
@@ -191,6 +195,8 @@ function validateProfile(profile) {
|
|
|
191
195
|
}
|
|
192
196
|
}
|
|
193
197
|
|
|
198
|
+
validateProviderTokenBudgets(profile.providerTokenBudgets);
|
|
199
|
+
|
|
194
200
|
if (profile.preferredBackend != null && typeof profile.preferredBackend !== "string") {
|
|
195
201
|
throw new Error("Profile preferredBackend must be a string or null.");
|
|
196
202
|
}
|
|
@@ -213,7 +219,8 @@ const SECRET_KEY_ALLOWLIST = new Set([
|
|
|
213
219
|
"api_key_env",
|
|
214
220
|
"token_budget",
|
|
215
221
|
"stable_context_budget",
|
|
216
|
-
"request_context_budget"
|
|
222
|
+
"request_context_budget",
|
|
223
|
+
"provider_token_budgets"
|
|
217
224
|
]);
|
|
218
225
|
const SECRET_VALUE_PATTERN = /^(sk-[A-Za-z0-9]|sk-or-|gh[pousr]_|xox[baprs]-|AKIA[0-9A-Z]{16}\b|Bearer\s+\S+|eyJ[A-Za-z0-9_-]+\.)|-----BEGIN [A-Z ]*PRIVATE KEY-----/i;
|
|
219
226
|
|
|
@@ -265,6 +272,21 @@ function walkForSecrets(value) {
|
|
|
265
272
|
}
|
|
266
273
|
}
|
|
267
274
|
|
|
275
|
+
function validateProviderTokenBudgets(budgets) {
|
|
276
|
+
if (budgets == null) return;
|
|
277
|
+
if (typeof budgets !== "object" || Array.isArray(budgets)) {
|
|
278
|
+
throw new Error("Profile providerTokenBudgets must be an object mapping provider id to a positive number.");
|
|
279
|
+
}
|
|
280
|
+
for (const [provider, budget] of Object.entries(budgets)) {
|
|
281
|
+
if (!EXECUTION_ADAPTER_IDS.includes(provider)) {
|
|
282
|
+
throw new Error(`Unknown provider "${provider}" in providerTokenBudgets. Use ${EXECUTION_ADAPTER_IDS.join(", ")}.`);
|
|
283
|
+
}
|
|
284
|
+
if (!Number.isFinite(budget) || budget < 1) {
|
|
285
|
+
throw new Error(`Profile providerTokenBudgets.${provider} must be a positive number.`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
268
290
|
function validateCustomProviders(providers) {
|
|
269
291
|
if (providers == null) return;
|
|
270
292
|
if (!Array.isArray(providers)) {
|
|
@@ -1,40 +1,77 @@
|
|
|
1
1
|
import { createExecutionAdapter, parseNdjsonLine, buildPermissionsArgs } from "./create-execution-adapter.js";
|
|
2
|
+
import {
|
|
3
|
+
ReviewExecError, assertBoundedProcessOk, runBoundedProcess
|
|
4
|
+
} from "../review/review-exec.js";
|
|
2
5
|
|
|
3
6
|
const EXECUTABLE = "claude";
|
|
7
|
+
const AUTH_TIMEOUT_MS = 10_000;
|
|
8
|
+
const AUTH_OUTPUT_LIMIT = 16_384;
|
|
9
|
+
const SAFE_ENV_KEYS = Object.freeze([
|
|
10
|
+
"PATH", "HOME", "USER", "LOGNAME", "SHELL", "LANG", "LC_ALL", "LC_CTYPE",
|
|
11
|
+
"TMPDIR", "TERM", "CLAUDE_CONFIG_DIR", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY",
|
|
12
|
+
"http_proxy", "https_proxy", "no_proxy", "NODE_EXTRA_CA_CERTS"
|
|
13
|
+
]);
|
|
14
|
+
const SUBSCRIPTION_TYPE = /^(?:pro|max|team|enterprise)(?:[-_ ].*)?$/i;
|
|
4
15
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"stream-json",
|
|
10
|
-
...buildPermissionsArgs(permissions),
|
|
11
|
-
task
|
|
12
|
-
];
|
|
16
|
+
export const CLAUDE_AUTH_ERRORS = Object.freeze({
|
|
17
|
+
SUBSCRIPTION_REQUIRED: "claude_subscription_required",
|
|
18
|
+
INVALID_STATUS: "claude_auth_status_invalid"
|
|
19
|
+
});
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
export function buildClaudeExecutionEnv(sourceEnv = process.env) {
|
|
22
|
+
const env = Object.create(null);
|
|
23
|
+
for (const key of SAFE_ENV_KEYS) {
|
|
24
|
+
if (sourceEnv[key] != null && sourceEnv[key] !== "") env[key] = sourceEnv[key];
|
|
16
25
|
}
|
|
26
|
+
return env;
|
|
27
|
+
}
|
|
17
28
|
|
|
18
|
-
|
|
29
|
+
export async function verifyClaudeSubscriptionAuth({
|
|
30
|
+
env = process.env, runProcess = runBoundedProcess
|
|
31
|
+
} = {}) {
|
|
32
|
+
const result = await runProcess({
|
|
19
33
|
command: EXECUTABLE,
|
|
20
|
-
args,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
34
|
+
args: ["auth", "status"],
|
|
35
|
+
env: buildClaudeExecutionEnv(env),
|
|
36
|
+
stdin: null,
|
|
37
|
+
timeoutMs: AUTH_TIMEOUT_MS,
|
|
38
|
+
stdoutLimit: AUTH_OUTPUT_LIMIT,
|
|
39
|
+
stderrLimit: AUTH_OUTPUT_LIMIT
|
|
40
|
+
});
|
|
41
|
+
assertBoundedProcessOk(result);
|
|
42
|
+
let status;
|
|
43
|
+
try { status = JSON.parse(String(result.stdout ?? "").trim()); }
|
|
44
|
+
catch {
|
|
45
|
+
throw new ReviewExecError("Claude auth status did not return valid JSON.", {
|
|
46
|
+
code: CLAUDE_AUTH_ERRORS.INVALID_STATUS
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (status?.loggedIn !== true || status?.authMethod !== "claude.ai"
|
|
50
|
+
|| status?.apiProvider !== "firstParty" || !SUBSCRIPTION_TYPE.test(status?.subscriptionType ?? "")) {
|
|
51
|
+
throw new ReviewExecError(
|
|
52
|
+
"Claude execution requires a first-party claude.ai Pro, Max, Team, or Enterprise subscription.",
|
|
53
|
+
{ code: CLAUDE_AUTH_ERRORS.SUBSCRIPTION_REQUIRED }
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return { mode: "subscription", subscriptionType: status.subscriptionType };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function buildClaudeLaunch({ task, cwd, model, permissions = [], env = process.env }) {
|
|
60
|
+
const unsafeArgs = buildPermissionsArgs(permissions);
|
|
61
|
+
const permissionArgs = unsafeArgs.length > 0
|
|
62
|
+
? unsafeArgs
|
|
63
|
+
: ["--permission-mode", "auto", "--permission-prompts", "none"];
|
|
64
|
+
const args = ["-p", "--output-format", "stream-json", ...permissionArgs, task];
|
|
65
|
+
if (model) args.unshift("--model", model);
|
|
66
|
+
return { command: EXECUTABLE, args, cwd, env: buildClaudeExecutionEnv(env) };
|
|
24
67
|
}
|
|
25
68
|
|
|
26
69
|
function parseClaudeEventLine(line) {
|
|
27
70
|
const parsed = parseNdjsonLine(line);
|
|
28
71
|
if (!parsed || typeof parsed !== "object") return null;
|
|
29
|
-
|
|
30
72
|
if (parsed.type === "tool_use" || parsed.type === "tool_call") {
|
|
31
|
-
return {
|
|
32
|
-
type: "tool_call",
|
|
33
|
-
tool_name: parsed.name ?? parsed.tool ?? "unknown",
|
|
34
|
-
status: parsed.status ?? "started"
|
|
35
|
-
};
|
|
73
|
+
return { type: "tool_call", tool_name: parsed.name ?? parsed.tool ?? "unknown", status: parsed.status ?? "started" };
|
|
36
74
|
}
|
|
37
|
-
|
|
38
75
|
if (parsed.type === "usage" || parsed.usage) {
|
|
39
76
|
const usage = parsed.usage ?? parsed;
|
|
40
77
|
return {
|
|
@@ -45,7 +82,6 @@ function parseClaudeEventLine(line) {
|
|
|
45
82
|
cost: usage.cost ?? null
|
|
46
83
|
};
|
|
47
84
|
}
|
|
48
|
-
|
|
49
85
|
return parsed;
|
|
50
86
|
}
|
|
51
87
|
|
|
@@ -54,13 +90,10 @@ export default createExecutionAdapter({
|
|
|
54
90
|
label: "Claude Code",
|
|
55
91
|
executable: EXECUTABLE,
|
|
56
92
|
capabilities: {
|
|
57
|
-
structuredEvents: true,
|
|
58
|
-
|
|
59
|
-
diff: false,
|
|
60
|
-
cancel: true,
|
|
61
|
-
transcript: true,
|
|
62
|
-
permissionModes: ["force", "yolo"]
|
|
93
|
+
structuredEvents: true, tokens: true, diff: false, cancel: true, transcript: true,
|
|
94
|
+
permissionModes: ["safe", "force", "yolo"]
|
|
63
95
|
},
|
|
64
96
|
buildLaunch: buildClaudeLaunch,
|
|
65
|
-
parseEventLine: parseClaudeEventLine
|
|
97
|
+
parseEventLine: parseClaudeEventLine,
|
|
98
|
+
preflight: verifyClaudeSubscriptionAuth
|
|
66
99
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createExecutionAdapter, parseNdjsonLine } from "./create-execution-adapter.js";
|
|
2
|
+
import { verifyCodexSubscriptionAuth } from "../../architect/architect-codex.js";
|
|
2
3
|
|
|
3
4
|
const EXECUTABLE = "codex";
|
|
4
5
|
|
|
@@ -13,7 +14,12 @@ function buildCodexPermissionsArgs(permissions = []) {
|
|
|
13
14
|
return ["--dangerously-bypass-approvals-and-sandbox"];
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
// Safe headless default: sandboxed to the workspace, with approval
|
|
18
|
+
// requests auto-reviewed instead of blocking forever on a human who
|
|
19
|
+
// can't respond in a non-interactive run — mirrors claude.js's
|
|
20
|
+
// `--permission-mode auto --permission-prompts none`. Without this,
|
|
21
|
+
// `codex exec` hangs on its default on-request approval policy.
|
|
22
|
+
return ["--sandbox", "workspace-write", "--approve-for-me"];
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
function buildCodexLaunch({ task, cwd, model, permissions = [] }) {
|
|
@@ -76,5 +82,6 @@ export default createExecutionAdapter({
|
|
|
76
82
|
permissionModes: ["yolo"]
|
|
77
83
|
},
|
|
78
84
|
buildLaunch: buildCodexLaunch,
|
|
79
|
-
parseEventLine: parseCodexEventLine
|
|
85
|
+
parseEventLine: parseCodexEventLine,
|
|
86
|
+
preflight: verifyCodexSubscriptionAuth
|
|
80
87
|
});
|
|
@@ -11,7 +11,8 @@ export function createExecutionAdapter({
|
|
|
11
11
|
buildLaunch,
|
|
12
12
|
parseEventLine = null,
|
|
13
13
|
checkAvailability = null,
|
|
14
|
-
launchable = null
|
|
14
|
+
launchable = null,
|
|
15
|
+
preflight = null
|
|
15
16
|
}) {
|
|
16
17
|
return {
|
|
17
18
|
id,
|
|
@@ -63,6 +64,10 @@ export function createExecutionAdapter({
|
|
|
63
64
|
return buildLaunch(options);
|
|
64
65
|
},
|
|
65
66
|
|
|
67
|
+
async preflight(context = {}) {
|
|
68
|
+
return preflight ? preflight(context) : { ok: true };
|
|
69
|
+
},
|
|
70
|
+
|
|
66
71
|
parseEventLine(line, context = {}) {
|
|
67
72
|
if (!parseEventLine) return null;
|
|
68
73
|
return parseEventLine(line, context);
|