@mousedev/harness 0.1.1
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/LICENSE +21 -0
- package/README.md +43 -0
- package/dist/mouse.mjs +1688 -0
- package/package.json +44 -0
package/dist/mouse.mjs
ADDED
|
@@ -0,0 +1,1688 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __createRequire } from 'node:module';
|
|
3
|
+
const require = __createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
// src/main.ts
|
|
6
|
+
import { mkdirSync as mkdirSync2, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { homedir as homedir2, tmpdir } from "node:os";
|
|
8
|
+
import path4 from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { parseArgs } from "node:util";
|
|
11
|
+
|
|
12
|
+
// ../core/src/workspace.ts
|
|
13
|
+
function shellPath(p) {
|
|
14
|
+
return /^[A-Za-z0-9_./-]+$/.test(p) ? p : `'${p.replace(/'/g, `'\\''`)}'`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ../core/src/detect.ts
|
|
18
|
+
var EMPTY_MANIFEST = {
|
|
19
|
+
packageJson: null,
|
|
20
|
+
packageManager: "npm",
|
|
21
|
+
pyproject: false,
|
|
22
|
+
pytestIni: false,
|
|
23
|
+
setupCfg: false,
|
|
24
|
+
toxIni: false,
|
|
25
|
+
testsDir: false,
|
|
26
|
+
uvLock: false,
|
|
27
|
+
poetryLock: false,
|
|
28
|
+
goMod: false,
|
|
29
|
+
cargoToml: false,
|
|
30
|
+
makefileTest: false
|
|
31
|
+
};
|
|
32
|
+
var DEFAULT_SCRIPTS = [
|
|
33
|
+
["build", ["build", "typecheck"]],
|
|
34
|
+
["test", ["test"]],
|
|
35
|
+
["lint", ["lint"]]
|
|
36
|
+
];
|
|
37
|
+
var PYTEST_ARGS = "-q --maxfail=25 -p no:cacheprovider";
|
|
38
|
+
function parseJsonObject(raw) {
|
|
39
|
+
if (!raw?.trim()) return null;
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(raw);
|
|
42
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function runCommand(pm, script) {
|
|
48
|
+
return pm === "npm" ? `npm run ${script} --if-present` : `${pm} run ${script}`;
|
|
49
|
+
}
|
|
50
|
+
function checksFromPackageJson(packageJson, pm = "npm") {
|
|
51
|
+
const scripts = parseJsonObject(packageJson)?.scripts;
|
|
52
|
+
if (!scripts || typeof scripts !== "object") return [];
|
|
53
|
+
const has = (s) => typeof scripts[s] === "string";
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const [name, candidates] of DEFAULT_SCRIPTS) {
|
|
56
|
+
const script = candidates.find(has);
|
|
57
|
+
if (script) out.push({ name, command: runCommand(pm, script) });
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function pytestCommand(m) {
|
|
62
|
+
if (m.uvLock) return `uv run --frozen pytest ${PYTEST_ARGS}`;
|
|
63
|
+
if (m.poetryLock) return `poetry run pytest ${PYTEST_ARGS}`;
|
|
64
|
+
return `python3 -m pytest ${PYTEST_ARGS}`;
|
|
65
|
+
}
|
|
66
|
+
function checksFromEcosystem(m) {
|
|
67
|
+
const out = checksFromPackageJson(m.packageJson, m.packageManager);
|
|
68
|
+
const python = m.pyproject || m.pytestIni || m.toxIni || m.setupCfg && m.testsDir;
|
|
69
|
+
if (python) out.push({ name: "pytest", command: pytestCommand(m) });
|
|
70
|
+
if (m.goMod) out.push({ name: "go-test", command: "go test ./..." });
|
|
71
|
+
if (m.cargoToml) out.push({ name: "cargo-test", command: "cargo test" });
|
|
72
|
+
if (m.makefileTest && out.length === 0) out.push({ name: "make-test", command: "make test" });
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
var MANIFEST_FILES = [
|
|
76
|
+
"pyproject.toml",
|
|
77
|
+
"pytest.ini",
|
|
78
|
+
"setup.cfg",
|
|
79
|
+
"tox.ini",
|
|
80
|
+
"uv.lock",
|
|
81
|
+
"poetry.lock",
|
|
82
|
+
"go.mod",
|
|
83
|
+
"Cargo.toml"
|
|
84
|
+
];
|
|
85
|
+
function detectEcosystemCommand(root) {
|
|
86
|
+
return [
|
|
87
|
+
`cd ${shellPath(root)} 2>/dev/null || exit 0`,
|
|
88
|
+
`for f in ${MANIFEST_FILES.join(" ")}; do [ -e "$f" ] && echo "F:$f"; done`,
|
|
89
|
+
'{ [ -d tests ] || [ -d test ]; } && echo "D:tests"',
|
|
90
|
+
"grep -qE '^test:' Makefile 2>/dev/null && echo 'M:test'",
|
|
91
|
+
'[ -f pnpm-lock.yaml ] && echo "PM:pnpm"',
|
|
92
|
+
'[ -f yarn.lock ] && echo "PM:yarn"',
|
|
93
|
+
'[ -f package.json ] && { echo "PKG_BEGIN"; cat package.json; echo; echo "PKG_END"; }',
|
|
94
|
+
"true"
|
|
95
|
+
].join("; ");
|
|
96
|
+
}
|
|
97
|
+
var DETECT_ECOSYSTEM_COMMAND = detectEcosystemCommand("/workspace");
|
|
98
|
+
function parseEcosystemOutput(stdout) {
|
|
99
|
+
const m = { ...EMPTY_MANIFEST };
|
|
100
|
+
const lines = stdout.split("\n");
|
|
101
|
+
const pkgStart = lines.indexOf("PKG_BEGIN");
|
|
102
|
+
const pkgEnd = lines.indexOf("PKG_END");
|
|
103
|
+
if (pkgStart >= 0 && pkgEnd > pkgStart) {
|
|
104
|
+
m.packageJson = lines.slice(pkgStart + 1, pkgEnd).join("\n");
|
|
105
|
+
}
|
|
106
|
+
for (const line of lines) {
|
|
107
|
+
switch (line.trim()) {
|
|
108
|
+
case "F:pyproject.toml":
|
|
109
|
+
m.pyproject = true;
|
|
110
|
+
break;
|
|
111
|
+
case "F:pytest.ini":
|
|
112
|
+
m.pytestIni = true;
|
|
113
|
+
break;
|
|
114
|
+
case "F:setup.cfg":
|
|
115
|
+
m.setupCfg = true;
|
|
116
|
+
break;
|
|
117
|
+
case "F:tox.ini":
|
|
118
|
+
m.toxIni = true;
|
|
119
|
+
break;
|
|
120
|
+
case "F:uv.lock":
|
|
121
|
+
m.uvLock = true;
|
|
122
|
+
break;
|
|
123
|
+
case "F:poetry.lock":
|
|
124
|
+
m.poetryLock = true;
|
|
125
|
+
break;
|
|
126
|
+
case "F:go.mod":
|
|
127
|
+
m.goMod = true;
|
|
128
|
+
break;
|
|
129
|
+
case "F:Cargo.toml":
|
|
130
|
+
m.cargoToml = true;
|
|
131
|
+
break;
|
|
132
|
+
case "D:tests":
|
|
133
|
+
m.testsDir = true;
|
|
134
|
+
break;
|
|
135
|
+
case "M:test":
|
|
136
|
+
m.makefileTest = true;
|
|
137
|
+
break;
|
|
138
|
+
case "PM:pnpm":
|
|
139
|
+
m.packageManager = "pnpm";
|
|
140
|
+
break;
|
|
141
|
+
case "PM:yarn":
|
|
142
|
+
if (m.packageManager === "npm") m.packageManager = "yarn";
|
|
143
|
+
break;
|
|
144
|
+
default:
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return m;
|
|
149
|
+
}
|
|
150
|
+
async function detectEcosystem(ws) {
|
|
151
|
+
const res = await ws.exec(detectEcosystemCommand(ws.root), { cwd: ws.root, timeoutMs: 1e4 }).catch(() => ({ stdout: "" }));
|
|
152
|
+
return parseEcosystemOutput(res.stdout);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ../core/src/exec.ts
|
|
156
|
+
import { spawn } from "node:child_process";
|
|
157
|
+
import { existsSync } from "node:fs";
|
|
158
|
+
var LOCAL_EXEC_TIMEOUT_CODE = 124;
|
|
159
|
+
function localWorkspace(opts) {
|
|
160
|
+
const root = opts.root.replace(/\/+$/, "") || "/";
|
|
161
|
+
const resolveCwd = (cwd) => cwd && existsSync(cwd) ? cwd : root;
|
|
162
|
+
function run(file, argv, execOpts) {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
const child = spawn(file, argv, {
|
|
165
|
+
cwd: resolveCwd(execOpts?.cwd),
|
|
166
|
+
env: opts.env ?? process.env,
|
|
167
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
168
|
+
});
|
|
169
|
+
let stdout = "";
|
|
170
|
+
let stderr = "";
|
|
171
|
+
let timedOut = false;
|
|
172
|
+
child.stdout.on("data", (d) => {
|
|
173
|
+
stdout += String(d);
|
|
174
|
+
});
|
|
175
|
+
child.stderr.on("data", (d) => {
|
|
176
|
+
stderr += String(d);
|
|
177
|
+
});
|
|
178
|
+
const timer = execOpts?.timeoutMs ? setTimeout(() => {
|
|
179
|
+
timedOut = true;
|
|
180
|
+
child.kill("SIGKILL");
|
|
181
|
+
}, execOpts.timeoutMs) : null;
|
|
182
|
+
const onAbort = () => child.kill("SIGKILL");
|
|
183
|
+
execOpts?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
184
|
+
child.on("error", (e) => {
|
|
185
|
+
if (timer) clearTimeout(timer);
|
|
186
|
+
execOpts?.signal?.removeEventListener("abort", onAbort);
|
|
187
|
+
reject(e);
|
|
188
|
+
});
|
|
189
|
+
child.on("close", (code) => {
|
|
190
|
+
if (timer) clearTimeout(timer);
|
|
191
|
+
execOpts?.signal?.removeEventListener("abort", onAbort);
|
|
192
|
+
resolve({ stdout, stderr, code: timedOut ? LOCAL_EXEC_TIMEOUT_CODE : code ?? 1 });
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
root,
|
|
198
|
+
exec: (cmd, execOpts) => run("bash", ["-lc", cmd], execOpts)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ../core/src/failures.ts
|
|
203
|
+
function stringifyInferenceUnknown(raw) {
|
|
204
|
+
if (raw == null) return "";
|
|
205
|
+
if (typeof raw === "string") return raw;
|
|
206
|
+
try {
|
|
207
|
+
return JSON.stringify(raw);
|
|
208
|
+
} catch {
|
|
209
|
+
return String(raw);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function classifyInferenceFailure(message, raw) {
|
|
213
|
+
const blob = `${message}
|
|
214
|
+
${stringifyInferenceUnknown(raw)}`.toLowerCase();
|
|
215
|
+
if (/\b429\b/.test(blob) || /\b503\b/.test(blob) || /\b502\b/.test(blob) || blob.includes("rate limit") || blob.includes("ratelimit") || blob.includes("too many requests") || blob.includes("overload") || blob.includes("overloaded") || blob.includes("econnreset") || blob.includes("etimedout") || blob.includes("timed out") || blob.includes("timeout") || blob.includes("temporarily unavailable") || blob.includes("service unavailable") || blob.includes("please retry") || blob.includes("retry your request")) {
|
|
216
|
+
return "transient";
|
|
217
|
+
}
|
|
218
|
+
if (blob.includes("model_not_found") || blob.includes("model not found") || blob.includes("invalid model") || blob.includes("no such model") || blob.includes("unknown model") || blob.includes("does not exist") && blob.includes("model") || blob.includes("not available") && (blob.includes("model") || blob.includes("provider")) || blob.includes("model id") || blob.includes("unsupported model")) {
|
|
219
|
+
return "model_route";
|
|
220
|
+
}
|
|
221
|
+
return "other";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ../core/src/git.ts
|
|
225
|
+
var git = (ws) => `git -C ${shellPath(ws.root)}`;
|
|
226
|
+
async function headSha(ws) {
|
|
227
|
+
const r = await ws.exec(`${git(ws)} rev-parse HEAD`, { cwd: ws.root, timeoutMs: 1e4 }).catch(() => ({ stdout: "", code: 1 }));
|
|
228
|
+
if (r.code !== 0) return null;
|
|
229
|
+
const s = r.stdout.trim();
|
|
230
|
+
return s.length >= 7 ? s : null;
|
|
231
|
+
}
|
|
232
|
+
async function fingerprint(ws, baseSha) {
|
|
233
|
+
const p = await ws.exec(`${git(ws)} status --porcelain`, { cwd: ws.root, timeoutMs: 1e4 });
|
|
234
|
+
if (!baseSha) return p.stdout;
|
|
235
|
+
const diff = await ws.exec(`${git(ws)} diff --shortstat ${baseSha} --`, {
|
|
236
|
+
cwd: ws.root,
|
|
237
|
+
timeoutMs: 2e4
|
|
238
|
+
});
|
|
239
|
+
return `${p.stdout}
|
|
240
|
+
${diff.stdout}`;
|
|
241
|
+
}
|
|
242
|
+
var VERIFICATION_PATH = /(^|\/)(tests?|spec|__tests__)(\/|$)|\.github\/workflows\//;
|
|
243
|
+
async function deletedVerificationFiles(ws, baseSha) {
|
|
244
|
+
if (!baseSha) return [];
|
|
245
|
+
const r = await ws.exec(`${git(ws)} diff --diff-filter=D --name-only ${baseSha} --`, {
|
|
246
|
+
cwd: ws.root,
|
|
247
|
+
timeoutMs: 2e4
|
|
248
|
+
}).catch(() => ({ stdout: "", code: 1 }));
|
|
249
|
+
if (r.code !== 0) return [];
|
|
250
|
+
return r.stdout.split("\n").map((l) => l.trim()).filter((p) => p && VERIFICATION_PATH.test(p));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ../core/src/mea.ts
|
|
254
|
+
function initTaskState(objective) {
|
|
255
|
+
const trimmed = objective.trim();
|
|
256
|
+
const chunks = trimmed.split(/(?:^|\n)\s*(?:\d+[.)]\s+|[-*]\s+)/).map((s) => s.trim()).filter((s) => s.length > 8);
|
|
257
|
+
const requirementTexts = chunks.length >= 2 ? chunks.slice(0, 8) : [
|
|
258
|
+
trimmed.slice(0, 500),
|
|
259
|
+
"Hard gates pass: build, tests, lint, and no verification tampering.",
|
|
260
|
+
"Changes are committed and reviewable (focused diff, clear commit message)."
|
|
261
|
+
];
|
|
262
|
+
return {
|
|
263
|
+
objective: trimmed,
|
|
264
|
+
requirements: requirementTexts.map((text, i) => ({
|
|
265
|
+
id: `req_${i + 1}`,
|
|
266
|
+
text,
|
|
267
|
+
status: "pending",
|
|
268
|
+
evidenceRefs: []
|
|
269
|
+
})),
|
|
270
|
+
artifacts: [],
|
|
271
|
+
facts: [],
|
|
272
|
+
round: 0
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function applyAudit(state, audit) {
|
|
276
|
+
const completed = new Set(audit.integrity === "clean" ? audit.completedRequirementIds : []);
|
|
277
|
+
const unmet = new Set(audit.unmetRequirementIds);
|
|
278
|
+
const blocked = audit.completion === "blocked";
|
|
279
|
+
const requirements = state.requirements.map((r) => {
|
|
280
|
+
if (completed.has(r.id)) {
|
|
281
|
+
return {
|
|
282
|
+
...r,
|
|
283
|
+
status: "completed",
|
|
284
|
+
evidenceRefs: [...r.evidenceRefs, ...audit.facts.map((f) => f.evidenceRef)].slice(0, 8)
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (audit.integrity === "violation") {
|
|
288
|
+
return { ...r, status: "untrusted" };
|
|
289
|
+
}
|
|
290
|
+
if (blocked && unmet.has(r.id)) {
|
|
291
|
+
return { ...r, status: "blocked" };
|
|
292
|
+
}
|
|
293
|
+
if (unmet.has(r.id) && r.status === "completed") {
|
|
294
|
+
return { ...r, status: "untrusted" };
|
|
295
|
+
}
|
|
296
|
+
return r;
|
|
297
|
+
});
|
|
298
|
+
const newFacts = audit.facts.map((f, i) => ({
|
|
299
|
+
id: `fact_${state.round}_${i + 1}`,
|
|
300
|
+
text: f.text,
|
|
301
|
+
status: audit.integrity === "violation" ? "untrusted" : "completed",
|
|
302
|
+
evidenceRefs: [f.evidenceRef]
|
|
303
|
+
}));
|
|
304
|
+
return {
|
|
305
|
+
...state,
|
|
306
|
+
requirements,
|
|
307
|
+
facts: [...state.facts, ...newFacts].slice(-40),
|
|
308
|
+
round: state.round + 1
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function auditFromSandboxProbe(input) {
|
|
312
|
+
const facts = [
|
|
313
|
+
{
|
|
314
|
+
text: `workspace ${input.workspaceChanged ? "changed" : "unchanged"} since the run started`,
|
|
315
|
+
evidenceRef: "fingerprint"
|
|
316
|
+
}
|
|
317
|
+
];
|
|
318
|
+
const checks = input.checks ?? [];
|
|
319
|
+
for (const c of checks) {
|
|
320
|
+
facts.push({
|
|
321
|
+
text: `check ${c.name}: ${c.pass ? "pass" : `fail (exit ${c.exitCode ?? "timeout"})`}`,
|
|
322
|
+
evidenceRef: `check:${c.name}`
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
const tampering = input.tampering ?? [];
|
|
326
|
+
if (tampering.length > 0) {
|
|
327
|
+
facts.push({
|
|
328
|
+
text: `Verification tampering: ${tampering.slice(0, 5).join(", ")}`,
|
|
329
|
+
evidenceRef: "verification_tampering"
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
const integrity = tampering.length > 0 ? "violation" : "clean";
|
|
333
|
+
const failedChecks = checks.filter((c) => !c.pass).map((c) => c.name);
|
|
334
|
+
const checksOk = failedChecks.length === 0;
|
|
335
|
+
const passes = input.workspaceChanged && checksOk && integrity === "clean";
|
|
336
|
+
const completedRequirementIds = [];
|
|
337
|
+
const unmetRequirementIds = [];
|
|
338
|
+
const gaps = [];
|
|
339
|
+
for (const id of input.contractTargets) {
|
|
340
|
+
if (passes) completedRequirementIds.push(id);
|
|
341
|
+
else {
|
|
342
|
+
unmetRequirementIds.push(id);
|
|
343
|
+
if (!input.workspaceChanged) gaps.push("Workspace unchanged since the run started.");
|
|
344
|
+
if (!checksOk) gaps.push(`Checks failed: ${failedChecks.join(", ")}.`);
|
|
345
|
+
if (integrity !== "clean") gaps.push("Verification files were deleted.");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (passes) {
|
|
349
|
+
for (const r of input.state.requirements) {
|
|
350
|
+
if (r.status === "pending" && /commit|hard gates|build|tests/i.test(r.text)) {
|
|
351
|
+
completedRequirementIds.push(r.id);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const completion = integrity === "violation" ? "blocked" : completedRequirementIds.length > 0 && unmetRequirementIds.length === 0 ? "complete" : "incomplete";
|
|
356
|
+
const passedNames = checks.filter((c) => c.pass).map((c) => c.name);
|
|
357
|
+
return {
|
|
358
|
+
completion,
|
|
359
|
+
integrity,
|
|
360
|
+
completedRequirementIds: [...new Set(completedRequirementIds)],
|
|
361
|
+
unmetRequirementIds: [...new Set(unmetRequirementIds)],
|
|
362
|
+
facts,
|
|
363
|
+
gaps: [...new Set(gaps)],
|
|
364
|
+
summary: completion === "complete" ? passedNames.length > 0 ? `Sandbox audit: workspace changed; ${passedNames.join(", ")} passed.` : "Sandbox audit: workspace changed; no checks to run." : `Sandbox audit incomplete: ${[...new Set(gaps)].join(" ") || "criteria unmet"}`
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ../core/src/loop.ts
|
|
369
|
+
var MIN_ROUND_MS = 6e4;
|
|
370
|
+
var ESCALATE_AFTER_NON_PROGRESS_ROUNDS = 2;
|
|
371
|
+
var INSTRUCTION_MAX_CHARS = 6e3;
|
|
372
|
+
var EXCERPT_MAX_CHARS = 1500;
|
|
373
|
+
var AUDIT_OPEN = "MOUSE_AUDIT";
|
|
374
|
+
var AUDIT_CLOSE = "END_MOUSE_AUDIT";
|
|
375
|
+
function parseAuditBlock(text) {
|
|
376
|
+
let open = text.lastIndexOf(AUDIT_OPEN);
|
|
377
|
+
while (open > 0 && text.startsWith(AUDIT_CLOSE, open - "END_".length)) {
|
|
378
|
+
open = text.lastIndexOf(AUDIT_OPEN, open - 1);
|
|
379
|
+
}
|
|
380
|
+
if (open < 0) return null;
|
|
381
|
+
const closeAt = text.indexOf(AUDIT_CLOSE, open + AUDIT_OPEN.length);
|
|
382
|
+
const body = text.slice(open + AUDIT_OPEN.length, closeAt < 0 ? void 0 : closeAt);
|
|
383
|
+
const done = [];
|
|
384
|
+
const todo = [];
|
|
385
|
+
for (const raw of body.split("\n")) {
|
|
386
|
+
const m = /^\s*[-*]\s*\[(done|todo|x|X| )\]\s*(.*)$/.exec(raw);
|
|
387
|
+
if (!m) continue;
|
|
388
|
+
const item = m[2].trim();
|
|
389
|
+
if (m[1] === "done" || m[1] === "x" || m[1] === "X") done.push(item);
|
|
390
|
+
else todo.push(item);
|
|
391
|
+
}
|
|
392
|
+
if (done.length === 0 && todo.length === 0) return null;
|
|
393
|
+
return { done, todo };
|
|
394
|
+
}
|
|
395
|
+
function auditFooter() {
|
|
396
|
+
return [
|
|
397
|
+
"When you are finished, end your reply with an audit of the original task, one line per requirement, in exactly this form:",
|
|
398
|
+
AUDIT_OPEN,
|
|
399
|
+
"- [done] <requirement>: <the file, test, or command output that proves it>",
|
|
400
|
+
"- [todo] <requirement>: <what is still missing>",
|
|
401
|
+
AUDIT_CLOSE,
|
|
402
|
+
"Mark a requirement done only with evidence you produced in this workspace."
|
|
403
|
+
].join("\n");
|
|
404
|
+
}
|
|
405
|
+
function requirementLines(state) {
|
|
406
|
+
return state.requirements.map((r) => `- ${r.text}`);
|
|
407
|
+
}
|
|
408
|
+
function buildContinuePrompt(kind, ctx) {
|
|
409
|
+
const lines = [];
|
|
410
|
+
if (kind === "fix") {
|
|
411
|
+
lines.push("Verification failed after your changes.", "");
|
|
412
|
+
for (const c of ctx.failed) {
|
|
413
|
+
lines.push(
|
|
414
|
+
`Check \`${c.name}\` (\`${c.command}\`) ${c.exitCode == null ? "timed out" : `exited ${c.exitCode}`}:`,
|
|
415
|
+
"```",
|
|
416
|
+
c.excerpt.slice(-EXCERPT_MAX_CHARS),
|
|
417
|
+
"```",
|
|
418
|
+
""
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
lines.push(
|
|
422
|
+
"Continue working on the task. Do not stop until these checks pass and every requirement below is done. Never weaken, skip, or delete tests to make them pass."
|
|
423
|
+
);
|
|
424
|
+
} else if (kind === "nochange") {
|
|
425
|
+
lines.push(
|
|
426
|
+
"No files in the workspace have changed, so the task is not done. Read the task again, then implement it here. If the task only requires producing files or output, produce them now."
|
|
427
|
+
);
|
|
428
|
+
} else {
|
|
429
|
+
lines.push(
|
|
430
|
+
"Before finishing, verify the task is complete. Re-read each requirement below and confirm it is implemented and checked in this workspace. If anything is missing, untested, or only partially done, do it now."
|
|
431
|
+
);
|
|
432
|
+
if (ctx.audit && ctx.audit.todo.length > 0) {
|
|
433
|
+
lines.push("", "You previously listed these as not done:");
|
|
434
|
+
for (const t of ctx.audit.todo) lines.push(`- ${t}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
lines.push("", "## Requirements", ...requirementLines(ctx.state));
|
|
438
|
+
lines.push("", "## Original task", ctx.instruction.slice(0, INSTRUCTION_MAX_CHARS));
|
|
439
|
+
lines.push("", auditFooter());
|
|
440
|
+
return lines.join("\n");
|
|
441
|
+
}
|
|
442
|
+
async function runCompletionLoop(input) {
|
|
443
|
+
const now = input.now ?? Date.now;
|
|
444
|
+
const { probe, budget, signal } = input;
|
|
445
|
+
const onEvent = input.onEvent ?? (() => {
|
|
446
|
+
});
|
|
447
|
+
let state = initTaskState(input.instruction);
|
|
448
|
+
const targets = state.requirements.map((r) => r.id);
|
|
449
|
+
const rounds = [];
|
|
450
|
+
let nonProgress = 0;
|
|
451
|
+
let model = input.model;
|
|
452
|
+
const finish = (outcome) => ({
|
|
453
|
+
outcome,
|
|
454
|
+
rounds,
|
|
455
|
+
totalSteps: input.steps(),
|
|
456
|
+
state
|
|
457
|
+
});
|
|
458
|
+
for (let round = 1; ; round++) {
|
|
459
|
+
if (signal.aborted) return finish("aborted");
|
|
460
|
+
const elapsed = now() - input.startedAt;
|
|
461
|
+
if (elapsed >= budget.maxWallMs) return finish("wall_clock");
|
|
462
|
+
if (input.steps() >= budget.maxTotalSteps) return finish("step_budget");
|
|
463
|
+
const fp = await probe.fingerprint();
|
|
464
|
+
const changed = fp !== input.initialFingerprint;
|
|
465
|
+
const checks = changed ? await probe.checks() : [];
|
|
466
|
+
const tampering = changed ? await probe.tampering() : [];
|
|
467
|
+
for (const c of checks) {
|
|
468
|
+
await onEvent({
|
|
469
|
+
type: "check_status",
|
|
470
|
+
name: c.name,
|
|
471
|
+
conclusion: c.pass ? "success" : "failure",
|
|
472
|
+
...c.pass ? {} : { detail: c.excerpt.slice(-EXCERPT_MAX_CHARS), retryable: true }
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
if (tampering.length > 0) {
|
|
476
|
+
await onEvent({
|
|
477
|
+
type: "notice",
|
|
478
|
+
message: `Verification files were deleted: ${tampering.slice(0, 5).join(", ")}`,
|
|
479
|
+
level: "warn"
|
|
480
|
+
});
|
|
481
|
+
return finish("blocked");
|
|
482
|
+
}
|
|
483
|
+
const audit = auditFromSandboxProbe({
|
|
484
|
+
state,
|
|
485
|
+
contractTargets: targets,
|
|
486
|
+
workspaceChanged: changed,
|
|
487
|
+
checks,
|
|
488
|
+
tampering
|
|
489
|
+
});
|
|
490
|
+
state = applyAudit(state, audit);
|
|
491
|
+
const failed = checks.filter((c) => !c.pass);
|
|
492
|
+
const modelAudit = parseAuditBlock(input.lastAssistantText());
|
|
493
|
+
if (changed && failed.length === 0 && modelAudit && modelAudit.todo.length === 0) {
|
|
494
|
+
return finish("satisfied");
|
|
495
|
+
}
|
|
496
|
+
const kind = !changed ? "nochange" : failed.length > 0 ? "fix" : "audit";
|
|
497
|
+
if (budget.maxWallMs - elapsed < (budget.minRoundMs ?? MIN_ROUND_MS))
|
|
498
|
+
return finish("wall_clock");
|
|
499
|
+
if (nonProgress >= ESCALATE_AFTER_NON_PROGRESS_ROUNDS && input.escalate) {
|
|
500
|
+
const next = await input.escalate(nonProgress);
|
|
501
|
+
if (next) model = next;
|
|
502
|
+
}
|
|
503
|
+
const prompt = buildContinuePrompt(kind, {
|
|
504
|
+
instruction: input.instruction,
|
|
505
|
+
state,
|
|
506
|
+
failed,
|
|
507
|
+
audit: modelAudit
|
|
508
|
+
});
|
|
509
|
+
try {
|
|
510
|
+
await input.engine.prompt(input.sessionId, prompt, signal, { model });
|
|
511
|
+
} catch (e) {
|
|
512
|
+
if (e?.name === "AbortError" || signal.aborted) return finish("aborted");
|
|
513
|
+
throw e;
|
|
514
|
+
}
|
|
515
|
+
const after = await probe.fingerprint();
|
|
516
|
+
const progressed = after !== fp;
|
|
517
|
+
nonProgress = progressed ? 0 : nonProgress + 1;
|
|
518
|
+
const record = {
|
|
519
|
+
round,
|
|
520
|
+
kind,
|
|
521
|
+
checksRun: checks.map((c) => c.name),
|
|
522
|
+
checksFailed: failed.map((c) => c.name),
|
|
523
|
+
changed,
|
|
524
|
+
audit: modelAudit,
|
|
525
|
+
progressed,
|
|
526
|
+
stepsAfter: input.steps(),
|
|
527
|
+
elapsedMs: now() - input.startedAt,
|
|
528
|
+
model
|
|
529
|
+
};
|
|
530
|
+
rounds.push(record);
|
|
531
|
+
await input.onRound?.(record);
|
|
532
|
+
const claimedDone = parseAuditBlock(input.lastAssistantText());
|
|
533
|
+
if (!progressed && claimedDone && claimedDone.todo.length === 0 && failed.length === 0) {
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (nonProgress >= budget.maxNonProgressRounds) return finish("stalled");
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ../core/src/paths.ts
|
|
541
|
+
import { homedir } from "node:os";
|
|
542
|
+
import path from "node:path";
|
|
543
|
+
function mouseHome(env = process.env) {
|
|
544
|
+
return env.MOUSE_HOME?.trim() || path.join(homedir(), ".mouse");
|
|
545
|
+
}
|
|
546
|
+
function workspaceSlug(cwd) {
|
|
547
|
+
const abs = path.resolve(cwd).replace(/[/\\:]+/g, "-").replace(/^-+|-+$/g, "");
|
|
548
|
+
return `--${abs || "root"}--`;
|
|
549
|
+
}
|
|
550
|
+
function runsDir(cwd, env = process.env) {
|
|
551
|
+
return path.join(mouseHome(env), "runs", workspaceSlug(cwd));
|
|
552
|
+
}
|
|
553
|
+
function traceFile(cwd, env = process.env, now = /* @__PURE__ */ new Date()) {
|
|
554
|
+
const stamp = now.toISOString().replace(/[:.]/g, "-");
|
|
555
|
+
return path.join(runsDir(cwd, env), `${stamp}-${process.pid}.jsonl`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ../core/src/policy.ts
|
|
559
|
+
var DEFAULT_POLICY = {
|
|
560
|
+
version: 1,
|
|
561
|
+
verify: { checks: null, timeoutSec: 900 },
|
|
562
|
+
loop: {
|
|
563
|
+
maxWallSec: 780,
|
|
564
|
+
maxSteps: 600,
|
|
565
|
+
nonProgressRounds: 3,
|
|
566
|
+
idleTimeoutSec: 600,
|
|
567
|
+
minRoundSec: 60
|
|
568
|
+
},
|
|
569
|
+
context: { prune: { enabled: false, thresholdChars: 8192, headChars: 4096, tailChars: 1024 } },
|
|
570
|
+
permissions: {},
|
|
571
|
+
extra: {}
|
|
572
|
+
};
|
|
573
|
+
var KNOWN_KEYS = /* @__PURE__ */ new Set(["version", "verify", "loop", "context", "permissions"]);
|
|
574
|
+
var ACTIONS = /* @__PURE__ */ new Set(["allow", "ask", "deny"]);
|
|
575
|
+
function obj(v) {
|
|
576
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
577
|
+
}
|
|
578
|
+
function int(v, fallback, min = 1, max = Number.MAX_SAFE_INTEGER) {
|
|
579
|
+
return typeof v === "number" && Number.isInteger(v) && v >= min && v <= max ? v : fallback;
|
|
580
|
+
}
|
|
581
|
+
function bool(v, fallback) {
|
|
582
|
+
return typeof v === "boolean" ? v : fallback;
|
|
583
|
+
}
|
|
584
|
+
function parseChecks(v) {
|
|
585
|
+
if (Array.isArray(v)) {
|
|
586
|
+
const out2 = [];
|
|
587
|
+
for (const item of v) {
|
|
588
|
+
const o2 = obj(item);
|
|
589
|
+
if (o2 && typeof o2.name === "string" && typeof o2.command === "string" && o2.command.trim()) {
|
|
590
|
+
out2.push({ name: o2.name, command: o2.command });
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return out2;
|
|
594
|
+
}
|
|
595
|
+
const o = obj(v);
|
|
596
|
+
if (!o) return null;
|
|
597
|
+
const out = [];
|
|
598
|
+
for (const [name, command] of Object.entries(o)) {
|
|
599
|
+
if (typeof command === "string" && command.trim()) out.push({ name, command });
|
|
600
|
+
}
|
|
601
|
+
return out;
|
|
602
|
+
}
|
|
603
|
+
function parseToolEntry(v) {
|
|
604
|
+
if (typeof v === "string") return ACTIONS.has(v) ? v : void 0;
|
|
605
|
+
const o = obj(v);
|
|
606
|
+
if (!o) return void 0;
|
|
607
|
+
const out = {};
|
|
608
|
+
for (const [pattern, action] of Object.entries(o)) {
|
|
609
|
+
if (typeof action === "string" && ACTIONS.has(action))
|
|
610
|
+
out[pattern] = action;
|
|
611
|
+
}
|
|
612
|
+
return out;
|
|
613
|
+
}
|
|
614
|
+
function parsePermissions(v) {
|
|
615
|
+
const o = obj(v);
|
|
616
|
+
if (!o) return {};
|
|
617
|
+
const out = {};
|
|
618
|
+
for (const tool of ["bash", "write", "edit"]) {
|
|
619
|
+
const entry = parseToolEntry(o[tool]);
|
|
620
|
+
if (entry !== void 0) out[tool] = entry;
|
|
621
|
+
}
|
|
622
|
+
if (typeof o.doom_loop === "string" && ACTIONS.has(o.doom_loop)) {
|
|
623
|
+
out.doom_loop = o.doom_loop;
|
|
624
|
+
}
|
|
625
|
+
return out;
|
|
626
|
+
}
|
|
627
|
+
function parsePolicy(raw) {
|
|
628
|
+
const o = obj(raw) ?? {};
|
|
629
|
+
const d = DEFAULT_POLICY;
|
|
630
|
+
const verify = obj(o.verify) ?? {};
|
|
631
|
+
const loop = obj(o.loop) ?? {};
|
|
632
|
+
const prune = obj(obj(o.context)?.prune) ?? {};
|
|
633
|
+
const extra = {};
|
|
634
|
+
for (const [k, v] of Object.entries(o)) if (!KNOWN_KEYS.has(k)) extra[k] = v;
|
|
635
|
+
return {
|
|
636
|
+
version: 1,
|
|
637
|
+
verify: {
|
|
638
|
+
checks: "checks" in verify ? parseChecks(verify.checks) : d.verify.checks,
|
|
639
|
+
timeoutSec: int(verify.timeoutSec, d.verify.timeoutSec, 1, 86400)
|
|
640
|
+
},
|
|
641
|
+
loop: {
|
|
642
|
+
maxWallSec: int(loop.maxWallSec, d.loop.maxWallSec),
|
|
643
|
+
maxSteps: int(loop.maxSteps, d.loop.maxSteps),
|
|
644
|
+
nonProgressRounds: int(loop.nonProgressRounds, d.loop.nonProgressRounds),
|
|
645
|
+
idleTimeoutSec: int(loop.idleTimeoutSec, d.loop.idleTimeoutSec),
|
|
646
|
+
minRoundSec: int(loop.minRoundSec, d.loop.minRoundSec, 0)
|
|
647
|
+
},
|
|
648
|
+
context: {
|
|
649
|
+
prune: {
|
|
650
|
+
enabled: bool(prune.enabled, d.context.prune.enabled),
|
|
651
|
+
thresholdChars: int(prune.thresholdChars, d.context.prune.thresholdChars),
|
|
652
|
+
headChars: int(prune.headChars, d.context.prune.headChars, 0),
|
|
653
|
+
tailChars: int(prune.tailChars, d.context.prune.tailChars, 0)
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
permissions: parsePermissions(o.permissions),
|
|
657
|
+
extra
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
function permissionRulesForTool(entry) {
|
|
661
|
+
if (!entry) return [];
|
|
662
|
+
if (typeof entry === "string") return [["*", entry]];
|
|
663
|
+
return Object.entries(entry);
|
|
664
|
+
}
|
|
665
|
+
async function readWorkspaceFile(ws, relPath) {
|
|
666
|
+
if (relPath.includes("..") || relPath.startsWith("/")) return null;
|
|
667
|
+
const abs = `${ws.root.replace(/\/+$/, "")}/${relPath}`;
|
|
668
|
+
try {
|
|
669
|
+
const res = await ws.exec(`test -f ${shellPath(abs)} && cat ${shellPath(abs)} || true`, {
|
|
670
|
+
cwd: ws.root,
|
|
671
|
+
timeoutMs: 5e3
|
|
672
|
+
});
|
|
673
|
+
if (res.code !== 0) return null;
|
|
674
|
+
return res.stdout || null;
|
|
675
|
+
} catch {
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
function parseJson(raw) {
|
|
680
|
+
if (!raw?.trim()) return null;
|
|
681
|
+
try {
|
|
682
|
+
return obj(JSON.parse(raw));
|
|
683
|
+
} catch {
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
async function loadPolicy(ws) {
|
|
688
|
+
const raw = parseJson(await readWorkspaceFile(ws, ".mouse/policy.json")) ?? {};
|
|
689
|
+
if (!obj(raw.verify)) {
|
|
690
|
+
const app = parseJson(await readWorkspaceFile(ws, ".mouse/app.json"));
|
|
691
|
+
const verify = obj(app?.verify);
|
|
692
|
+
if (verify) {
|
|
693
|
+
const alias = {};
|
|
694
|
+
if ("checks" in verify) alias.checks = verify.checks;
|
|
695
|
+
if (typeof verify.timeoutSec === "number") alias.timeoutSec = verify.timeoutSec;
|
|
696
|
+
raw.verify = alias;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return parsePolicy(raw);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// ../core/src/probe.ts
|
|
703
|
+
var DEFAULT_CHECK_TIMEOUT_MS = 9e5;
|
|
704
|
+
var FALLBACK_LIST_MAX = 2e3;
|
|
705
|
+
var FALLBACK_LIST_TIMEOUT_MS = 2e4;
|
|
706
|
+
function makeProbe(o) {
|
|
707
|
+
const ws = o.workspace;
|
|
708
|
+
const timeoutMs = o.checkTimeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS;
|
|
709
|
+
let checksCache = o.checks ?? null;
|
|
710
|
+
async function detected() {
|
|
711
|
+
if (checksCache) return checksCache;
|
|
712
|
+
checksCache = checksFromEcosystem(await detectEcosystem(ws));
|
|
713
|
+
return checksCache;
|
|
714
|
+
}
|
|
715
|
+
return {
|
|
716
|
+
async checks() {
|
|
717
|
+
const out = [];
|
|
718
|
+
for (const check of await detected()) {
|
|
719
|
+
if (o.signal.aborted) break;
|
|
720
|
+
const res = await ws.exec(check.command, { cwd: ws.root, timeoutMs, signal: o.signal }).catch((e) => ({ stdout: "", stderr: String(e), code: 1 }));
|
|
721
|
+
out.push({
|
|
722
|
+
...check,
|
|
723
|
+
pass: res.code === 0,
|
|
724
|
+
exitCode: res.code === LOCAL_EXEC_TIMEOUT_CODE ? null : res.code,
|
|
725
|
+
excerpt: `${res.stdout}
|
|
726
|
+
${res.stderr}`.trim().slice(-EXCERPT_MAX_CHARS)
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
return out;
|
|
730
|
+
},
|
|
731
|
+
tampering: () => deletedVerificationFiles(ws, o.runStartSha),
|
|
732
|
+
async fingerprint() {
|
|
733
|
+
if (o.runStartSha) {
|
|
734
|
+
const fp = await fingerprint(ws, o.runStartSha).catch(() => null);
|
|
735
|
+
if (fp !== null) return fp;
|
|
736
|
+
}
|
|
737
|
+
const r = await ws.exec(
|
|
738
|
+
`find ${shellPath(ws.root)} -xdev -type f -newer ${shellPath(o.marker)} -not -path '*/.git/*' -not -path '*/node_modules/*' 2>/dev/null | sort | head -${FALLBACK_LIST_MAX}`,
|
|
739
|
+
{ cwd: ws.root, timeoutMs: FALLBACK_LIST_TIMEOUT_MS }
|
|
740
|
+
);
|
|
741
|
+
return r.stdout;
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// ../core/src/prompt.ts
|
|
747
|
+
var BUILD_PROMPT = `You are Mouse, a coding agent working in a terminal.
|
|
748
|
+
|
|
749
|
+
Persist until the task is fully handled end to end in this turn: explore, implement, run the repo's tests, and fix what fails. Only stop when you are sure the problem is solved. Never claim done without a check you actually ran. If a tool call fails, read the error and change approach; do not repeat the same call.
|
|
750
|
+
|
|
751
|
+
Working style:
|
|
752
|
+
- Inspect before acting: read the relevant code and existing tests before editing. Reproduce a bug before fixing it.
|
|
753
|
+
- Fix the root cause. When you change a function, find every caller.
|
|
754
|
+
- Do the whole ask. A feature that spans several modules is done only when every module and its tests are updated.
|
|
755
|
+
- Prefer the simplest change that fully works: reuse what the repo has, add no dependency unless required, add no speculative abstractions.
|
|
756
|
+
- Validate at trust boundaries; keep internal logic plain.
|
|
757
|
+
- For multi-step work keep the todo list current, with exactly one item in progress.
|
|
758
|
+
|
|
759
|
+
Tools:
|
|
760
|
+
- Use rg for search. Batch independent reads and searches in one response.
|
|
761
|
+
- Prefer read/edit/write for files; use bash to build, test, and run.
|
|
762
|
+
- Do not re-read a file right after editing it; the edit tool fails loudly.
|
|
763
|
+
- Pass an explicit timeout to bash for long commands such as test suites and builds. Run long-lived servers in the background.
|
|
764
|
+
- Run the narrowest relevant test first, then the full suite before finishing. Never weaken, skip, or delete tests to make them pass.
|
|
765
|
+
|
|
766
|
+
Finish with a short summary: what changed, what you ran, and what it showed. Instructions in AGENTS.md or CLAUDE.md in the repo take precedence over these.`;
|
|
767
|
+
var WRITING_MODES = /* @__PURE__ */ new Set(["build", "overnight"]);
|
|
768
|
+
function buildAgentPrompt(mode, _opts = { profile: "product" }) {
|
|
769
|
+
return WRITING_MODES.has(mode) ? BUILD_PROMPT : void 0;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// ../core/src/trace.ts
|
|
773
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
774
|
+
import path2 from "node:path";
|
|
775
|
+
function openTrace(file) {
|
|
776
|
+
const abs = path2.resolve(file);
|
|
777
|
+
mkdirSync(path2.dirname(abs), { recursive: true });
|
|
778
|
+
return {
|
|
779
|
+
file: abs,
|
|
780
|
+
write(record) {
|
|
781
|
+
appendFileSync(abs, `${JSON.stringify({ ts: Date.now(), ...record })}
|
|
782
|
+
`);
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// ../core/src/version.ts
|
|
788
|
+
var MOUSE_VERSION = "0.1.1";
|
|
789
|
+
|
|
790
|
+
// ../opencode/src/compat/opencode-compat.json
|
|
791
|
+
var opencode_compat_default = {
|
|
792
|
+
$comment: "OpenCode versions the Mouse harness has been run against. `supported` versions are exercised in CI (mouse doctor + mouse config --profile bench); `tested` carries what was verified on that version. Anything else prints a warning from `mouse doctor` and fails `--strict-compat`.",
|
|
793
|
+
supported: ["1.18.27", "1.14.22"],
|
|
794
|
+
tested: {
|
|
795
|
+
"1.18.27": {
|
|
796
|
+
benchmark: "FrontierHarness Eval, Kimi K3 via Fireworks, under the benchmark's unmodified run-trials.sh on golden checkpoint fh-golden-mouse-v3",
|
|
797
|
+
result: "25/30 (datacurve 8/9, terminal-bench 17/21), run 2026-09-08-mouse-c, harness commit 315e2b8, all 30 trials valid; an earlier self-run through Harbor on 2026-09-03 scored 24/30",
|
|
798
|
+
notes: "The version both runs used: the adapter installed opencode-ai@latest, which was 1.18.27 on 2026-09-03, and the adapters have pinned it since."
|
|
799
|
+
},
|
|
800
|
+
"1.14.22": {
|
|
801
|
+
benchmark: "none",
|
|
802
|
+
result: "CI compat job only: mouse doctor --strict-compat and a bench config write succeed",
|
|
803
|
+
notes: "The version the hosted product's SDK pins. No benchmark run on it."
|
|
804
|
+
}
|
|
805
|
+
},
|
|
806
|
+
notes: {
|
|
807
|
+
"1.18.19": "FrontierHarness pinned 1.18.19 for its stock OpenCode control. Not run; 1.18.27 is the same minor line."
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
// ../opencode/src/compat.ts
|
|
812
|
+
function loadCompatManifest() {
|
|
813
|
+
return opencode_compat_default;
|
|
814
|
+
}
|
|
815
|
+
function parseOpencodeVersion(output) {
|
|
816
|
+
const m = /(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/.exec(output);
|
|
817
|
+
return m?.[1] ?? null;
|
|
818
|
+
}
|
|
819
|
+
function checkCompat(versionOutput, manifest = loadCompatManifest()) {
|
|
820
|
+
const version = versionOutput ? parseOpencodeVersion(versionOutput) : null;
|
|
821
|
+
if (!version) return { level: "unknown", version: null };
|
|
822
|
+
if (manifest.supported.includes(version)) return { level: "supported", version };
|
|
823
|
+
return { level: "untested", version, supported: manifest.supported };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// ../opencode/src/config.ts
|
|
827
|
+
var SET_CACHE_KEY_PROVIDER_IDS = ["anthropic", "openrouter", "fireworks-ai"];
|
|
828
|
+
var NO_CACHE_KEY_WHEN_SERVING = ["fireworks-ai"];
|
|
829
|
+
function providerCacheOptions(extra = []) {
|
|
830
|
+
const out = {};
|
|
831
|
+
for (const id of [...SET_CACHE_KEY_PROVIDER_IDS, ...extra]) {
|
|
832
|
+
out[id] = { options: { setCacheKey: true } };
|
|
833
|
+
}
|
|
834
|
+
return out;
|
|
835
|
+
}
|
|
836
|
+
function runProviderCacheOptions(modelProvider) {
|
|
837
|
+
const provider = providerCacheOptions(modelProvider ? [modelProvider] : []);
|
|
838
|
+
const skip = NO_CACHE_KEY_WHEN_SERVING;
|
|
839
|
+
if (modelProvider && skip.includes(modelProvider)) delete provider[modelProvider];
|
|
840
|
+
return provider;
|
|
841
|
+
}
|
|
842
|
+
var BENCH_COMPACTION = { auto: true, prune: true, reserved: 1e4 };
|
|
843
|
+
var OPENROUTER_PIN_FIREWORKS = {
|
|
844
|
+
provider: { order: ["Fireworks"], allow_fallbacks: false }
|
|
845
|
+
};
|
|
846
|
+
var MODE_PERMISSIONS = {
|
|
847
|
+
ask: { edit: "deny", bash: "deny" },
|
|
848
|
+
plan: { edit: "deny", bash: "ask" },
|
|
849
|
+
debug: { edit: "ask", bash: "allow" },
|
|
850
|
+
build: { edit: "allow", bash: "allow" }
|
|
851
|
+
};
|
|
852
|
+
var DISABLED_TOOLS = ["webfetch", "websearch", "task", "skill"];
|
|
853
|
+
function bashPermission(base, policy) {
|
|
854
|
+
const denies = policy ? permissionRulesForTool(policy.bash).filter(([, action]) => action === "deny").map(([pattern]) => pattern) : [];
|
|
855
|
+
if (denies.length === 0) return base;
|
|
856
|
+
const out = { "*": base };
|
|
857
|
+
for (const pattern of denies) out[pattern] = "deny";
|
|
858
|
+
return out;
|
|
859
|
+
}
|
|
860
|
+
function editPermission(base, permissionMode) {
|
|
861
|
+
if (permissionMode === "bypass" && base === "ask") return "allow";
|
|
862
|
+
return base;
|
|
863
|
+
}
|
|
864
|
+
var PLAN_EDIT = {
|
|
865
|
+
"*": "deny",
|
|
866
|
+
".mouse/plans/*.md": "allow"
|
|
867
|
+
};
|
|
868
|
+
function agentNameForMode(mode) {
|
|
869
|
+
return mode;
|
|
870
|
+
}
|
|
871
|
+
function buildOpencodeConfig(input) {
|
|
872
|
+
const profile = input.profile ?? "product";
|
|
873
|
+
const withPrompt = profile !== "product";
|
|
874
|
+
const modes = [
|
|
875
|
+
...Object.entries(MODE_PERMISSIONS),
|
|
876
|
+
...Object.entries(input.extraModes ?? {})
|
|
877
|
+
];
|
|
878
|
+
const agent = {};
|
|
879
|
+
for (const [mode, posture] of modes) {
|
|
880
|
+
const prompt = withPrompt ? buildAgentPrompt(mode, { profile }) : void 0;
|
|
881
|
+
agent[agentNameForMode(mode)] = {
|
|
882
|
+
mode: "primary",
|
|
883
|
+
...prompt ? { prompt } : {},
|
|
884
|
+
permission: {
|
|
885
|
+
edit: mode === "plan" ? PLAN_EDIT : editPermission(posture.edit, input.permissionMode),
|
|
886
|
+
bash: bashPermission(posture.bash, input.permissions),
|
|
887
|
+
// Native fetch is off; belt-and-braces alongside `tools.webfetch: false`.
|
|
888
|
+
webfetch: "deny",
|
|
889
|
+
// Hosts run their own doom-loop detector; OpenCode's would double-prompt.
|
|
890
|
+
doom_loop: "allow"
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
const tools = {};
|
|
895
|
+
for (const name of DISABLED_TOOLS) tools[name] = false;
|
|
896
|
+
const config = { agent, tools };
|
|
897
|
+
const smallModel = input.smallModel?.trim();
|
|
898
|
+
if (smallModel) config.small_model = smallModel;
|
|
899
|
+
if (profile === "bench") {
|
|
900
|
+
const bench = config;
|
|
901
|
+
bench.$schema = "https://opencode.ai/config.json";
|
|
902
|
+
bench.autoupdate = false;
|
|
903
|
+
bench.share = "disabled";
|
|
904
|
+
bench.compaction = { ...BENCH_COMPACTION };
|
|
905
|
+
const modelProvider = input.model?.includes("/") ? input.model.split("/")[0] : void 0;
|
|
906
|
+
const provider = runProviderCacheOptions(modelProvider);
|
|
907
|
+
if (modelProvider === "openrouter") {
|
|
908
|
+
provider.openrouter.options.extraBody = OPENROUTER_PIN_FIREWORKS;
|
|
909
|
+
}
|
|
910
|
+
bench.provider = provider;
|
|
911
|
+
if (input.model) bench.small_model = input.model;
|
|
912
|
+
} else if (profile === "local") {
|
|
913
|
+
const local = config;
|
|
914
|
+
local.compaction = { ...BENCH_COMPACTION };
|
|
915
|
+
const modelProvider = input.model?.includes("/") ? input.model.split("/")[0] : void 0;
|
|
916
|
+
local.provider = runProviderCacheOptions(modelProvider);
|
|
917
|
+
}
|
|
918
|
+
return config;
|
|
919
|
+
}
|
|
920
|
+
function mergeConfig(base, over) {
|
|
921
|
+
const out = { ...base };
|
|
922
|
+
for (const [k, v] of Object.entries(over)) {
|
|
923
|
+
const prev = out[k];
|
|
924
|
+
out[k] = v && typeof v === "object" && !Array.isArray(v) && prev && typeof prev === "object" && !Array.isArray(prev) ? mergeConfig(prev, v) : v;
|
|
925
|
+
}
|
|
926
|
+
return out;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// ../opencode/src/locate.ts
|
|
930
|
+
import { execFileSync } from "node:child_process";
|
|
931
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
932
|
+
import path3 from "node:path";
|
|
933
|
+
function locateOpencode(o = {}) {
|
|
934
|
+
const env = o.env ?? process.env;
|
|
935
|
+
if (o.flag) {
|
|
936
|
+
if (o.flag.includes(path3.sep)) return existsSync2(o.flag) ? o.flag : null;
|
|
937
|
+
return whichNamed(o.flag, env, o.platform) ?? o.flag;
|
|
938
|
+
}
|
|
939
|
+
if (env.MOUSE_OPENCODE_BIN?.trim()) return env.MOUSE_OPENCODE_BIN.trim();
|
|
940
|
+
const onPath = whichNamed("opencode", env, o.platform);
|
|
941
|
+
if (onPath) return onPath;
|
|
942
|
+
let dir = path3.resolve(o.cwd ?? process.cwd());
|
|
943
|
+
for (; ; ) {
|
|
944
|
+
const candidate = path3.join(dir, "node_modules", ".bin", "opencode");
|
|
945
|
+
if (existsSync2(candidate)) return candidate;
|
|
946
|
+
const parent = path3.dirname(dir);
|
|
947
|
+
if (parent === dir) return null;
|
|
948
|
+
dir = parent;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
function whichNamed(name, env, platform = process.platform) {
|
|
952
|
+
const suffixes = platform === "win32" && !path3.extname(name) ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT").toLowerCase().split(";").filter(Boolean)] : [""];
|
|
953
|
+
for (const dir of (env.PATH ?? "").split(path3.delimiter)) {
|
|
954
|
+
if (!dir) continue;
|
|
955
|
+
for (const ext of suffixes) {
|
|
956
|
+
const candidate = path3.join(dir, name + ext);
|
|
957
|
+
if (existsSync2(candidate)) return candidate;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
function opencodeVersion(bin, timeoutMs = 2e4) {
|
|
963
|
+
try {
|
|
964
|
+
return execFileSync(bin, ["--version"], { encoding: "utf8", timeout: timeoutMs }).trim();
|
|
965
|
+
} catch {
|
|
966
|
+
return null;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// ../opencode/src/plugins/prune.ts
|
|
971
|
+
var PRUNE_PLUGIN_FILENAME = "mouse-prune.mjs";
|
|
972
|
+
function prunePluginSource(p) {
|
|
973
|
+
return `// Written by the Mouse harness from .mouse/policy.json (context.prune).
|
|
974
|
+
const THRESHOLD = ${p.thresholdChars}, HEAD = ${p.headChars}, TAIL = ${p.tailChars};
|
|
975
|
+
export default {
|
|
976
|
+
name: "mouse-prune",
|
|
977
|
+
events: {
|
|
978
|
+
"tool.execute.after": async (_input, output) => {
|
|
979
|
+
if (!output || typeof output.output !== "string") return;
|
|
980
|
+
const s = output.output;
|
|
981
|
+
if (s.length <= THRESHOLD) return;
|
|
982
|
+
output.output = s.slice(0, HEAD) + "\\n\\n[... " + (s.length - HEAD - TAIL) + " chars omitted by mouse-prune ...]\\n\\n" + s.slice(-TAIL);
|
|
983
|
+
},
|
|
984
|
+
},
|
|
985
|
+
};
|
|
986
|
+
`;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// ../opencode/src/run-engine.ts
|
|
990
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
991
|
+
import { createInterface } from "node:readline";
|
|
992
|
+
var DEFAULT_IDLE_MS = 6e5;
|
|
993
|
+
var DEFAULT_ATTEMPTS = 3;
|
|
994
|
+
var RETRY_BASE_MS = 2e3;
|
|
995
|
+
var RETRY_MAX_MS = 3e4;
|
|
996
|
+
function retryDelayMs(attempt) {
|
|
997
|
+
return Math.min(RETRY_MAX_MS, RETRY_BASE_MS * 2 ** (attempt - 1));
|
|
998
|
+
}
|
|
999
|
+
function num(v) {
|
|
1000
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
1001
|
+
}
|
|
1002
|
+
var OpencodeRunEngine = class {
|
|
1003
|
+
constructor(opts) {
|
|
1004
|
+
this.opts = opts;
|
|
1005
|
+
}
|
|
1006
|
+
opts;
|
|
1007
|
+
sessionId = null;
|
|
1008
|
+
steps = 0;
|
|
1009
|
+
lastText = "";
|
|
1010
|
+
tokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
1011
|
+
title;
|
|
1012
|
+
child = null;
|
|
1013
|
+
/**
|
|
1014
|
+
* `opencode run` creates the session on the first prompt, so `open` only
|
|
1015
|
+
* records what to name it. Pass `sessionId` to continue an existing one.
|
|
1016
|
+
*/
|
|
1017
|
+
async open(opts = {}) {
|
|
1018
|
+
if (opts.sessionId) this.sessionId = opts.sessionId;
|
|
1019
|
+
this.title = opts.title;
|
|
1020
|
+
return this.sessionId ?? "";
|
|
1021
|
+
}
|
|
1022
|
+
async prompt(_sessionId, text, signal, opts) {
|
|
1023
|
+
const max = this.opts.maxAttempts ?? DEFAULT_ATTEMPTS;
|
|
1024
|
+
const model = opts?.model ?? this.opts.model;
|
|
1025
|
+
let steps = 0;
|
|
1026
|
+
for (let attempt = 1; attempt <= max; attempt++) {
|
|
1027
|
+
if (signal.aborted) throw Object.assign(new Error("Aborted"), { name: "AbortError" });
|
|
1028
|
+
const r = await this.spawnOnce(text, model, signal);
|
|
1029
|
+
this.steps += r.steps;
|
|
1030
|
+
steps += r.steps;
|
|
1031
|
+
if (r.text) this.lastText = r.text;
|
|
1032
|
+
this.opts.trace?.({
|
|
1033
|
+
type: "mouse.attempt",
|
|
1034
|
+
attempt,
|
|
1035
|
+
steps: r.steps,
|
|
1036
|
+
exitCode: r.exitCode,
|
|
1037
|
+
timedOut: r.timedOut,
|
|
1038
|
+
transientError: r.transientError,
|
|
1039
|
+
error: r.errorMessage,
|
|
1040
|
+
sessionId: this.sessionId
|
|
1041
|
+
});
|
|
1042
|
+
if (r.steps > 0) return { steps, text: this.lastText };
|
|
1043
|
+
const retryable = r.timedOut || r.transientError || r.exitCode !== 0;
|
|
1044
|
+
if (!retryable) return { steps, text: this.lastText };
|
|
1045
|
+
if (attempt === max) {
|
|
1046
|
+
throw new Error(
|
|
1047
|
+
`opencode run produced no steps after ${max} attempts${r.errorMessage ? `: ${r.errorMessage}` : ""}`
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
await new Promise((res) => setTimeout(res, retryDelayMs(attempt)));
|
|
1051
|
+
}
|
|
1052
|
+
return { steps, text: this.lastText };
|
|
1053
|
+
}
|
|
1054
|
+
async abort() {
|
|
1055
|
+
this.child?.kill("SIGKILL");
|
|
1056
|
+
}
|
|
1057
|
+
/** The argv for one turn; exported for tests and for `mouse doctor`. */
|
|
1058
|
+
argsFor(prompt, model = this.opts.model) {
|
|
1059
|
+
return [
|
|
1060
|
+
"run",
|
|
1061
|
+
"--format=json",
|
|
1062
|
+
"--agent",
|
|
1063
|
+
this.opts.agent ?? "build",
|
|
1064
|
+
"--model",
|
|
1065
|
+
model,
|
|
1066
|
+
...this.opts.skipPermissions === false ? [] : ["--dangerously-skip-permissions"],
|
|
1067
|
+
...this.sessionId ? ["--session", this.sessionId] : [],
|
|
1068
|
+
// Passed on every turn, continuation included, as the benchmark run did.
|
|
1069
|
+
...this.title ? ["--title", this.title] : [],
|
|
1070
|
+
...this.opts.extraArgs ?? [],
|
|
1071
|
+
"--",
|
|
1072
|
+
prompt
|
|
1073
|
+
];
|
|
1074
|
+
}
|
|
1075
|
+
spawnOnce(prompt, model, signal) {
|
|
1076
|
+
return new Promise((resolve, reject) => {
|
|
1077
|
+
const child = spawn2(this.opts.bin ?? "opencode", this.argsFor(prompt, model), {
|
|
1078
|
+
cwd: this.opts.cwd,
|
|
1079
|
+
env: this.opts.env ?? process.env,
|
|
1080
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1081
|
+
});
|
|
1082
|
+
this.child = child;
|
|
1083
|
+
const attempt = {
|
|
1084
|
+
exitCode: null,
|
|
1085
|
+
steps: 0,
|
|
1086
|
+
text: "",
|
|
1087
|
+
timedOut: false,
|
|
1088
|
+
transientError: false,
|
|
1089
|
+
errorMessage: null
|
|
1090
|
+
};
|
|
1091
|
+
const texts = [];
|
|
1092
|
+
const idleMs = this.opts.idleTimeoutMs ?? DEFAULT_IDLE_MS;
|
|
1093
|
+
let idle = setTimeout(onIdle, idleMs);
|
|
1094
|
+
function onIdle() {
|
|
1095
|
+
attempt.timedOut = true;
|
|
1096
|
+
child.kill("SIGKILL");
|
|
1097
|
+
}
|
|
1098
|
+
const touch = () => {
|
|
1099
|
+
clearTimeout(idle);
|
|
1100
|
+
idle = setTimeout(onIdle, idleMs);
|
|
1101
|
+
};
|
|
1102
|
+
const onAbort = () => child.kill("SIGKILL");
|
|
1103
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1104
|
+
const out = createInterface({ input: child.stdout });
|
|
1105
|
+
out.on("line", (line) => {
|
|
1106
|
+
touch();
|
|
1107
|
+
this.opts.passthrough?.(line);
|
|
1108
|
+
let ev;
|
|
1109
|
+
try {
|
|
1110
|
+
ev = JSON.parse(line);
|
|
1111
|
+
} catch {
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
if (!ev || typeof ev !== "object") return;
|
|
1115
|
+
if (!this.sessionId && typeof ev.sessionID === "string" && ev.sessionID) {
|
|
1116
|
+
this.sessionId = ev.sessionID;
|
|
1117
|
+
}
|
|
1118
|
+
const part = ev.part ?? {};
|
|
1119
|
+
switch (ev.type) {
|
|
1120
|
+
case "step_finish": {
|
|
1121
|
+
attempt.steps += 1;
|
|
1122
|
+
const tokens = part.tokens ?? {};
|
|
1123
|
+
const cache = tokens.cache ?? {};
|
|
1124
|
+
this.tokens.input += num(tokens.input);
|
|
1125
|
+
this.tokens.output += num(tokens.output);
|
|
1126
|
+
this.tokens.cacheRead += num(cache.read);
|
|
1127
|
+
this.tokens.cacheWrite += num(cache.write);
|
|
1128
|
+
this.tokens.cost += num(part.cost);
|
|
1129
|
+
break;
|
|
1130
|
+
}
|
|
1131
|
+
case "text": {
|
|
1132
|
+
if (typeof part.text === "string" && part.text.trim()) texts.push(part.text);
|
|
1133
|
+
break;
|
|
1134
|
+
}
|
|
1135
|
+
case "error": {
|
|
1136
|
+
const message = JSON.stringify(ev.error ?? "");
|
|
1137
|
+
attempt.errorMessage = message.slice(0, 500);
|
|
1138
|
+
if (classifyInferenceFailure(message, ev.error) === "transient") {
|
|
1139
|
+
attempt.transientError = true;
|
|
1140
|
+
}
|
|
1141
|
+
break;
|
|
1142
|
+
}
|
|
1143
|
+
default:
|
|
1144
|
+
break;
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
const err = createInterface({ input: child.stderr });
|
|
1148
|
+
err.on("line", (line) => {
|
|
1149
|
+
touch();
|
|
1150
|
+
this.opts.passthroughErr?.(line);
|
|
1151
|
+
});
|
|
1152
|
+
child.on("error", (e) => {
|
|
1153
|
+
clearTimeout(idle);
|
|
1154
|
+
signal.removeEventListener("abort", onAbort);
|
|
1155
|
+
this.child = null;
|
|
1156
|
+
reject(e);
|
|
1157
|
+
});
|
|
1158
|
+
child.on("close", (code) => {
|
|
1159
|
+
clearTimeout(idle);
|
|
1160
|
+
signal.removeEventListener("abort", onAbort);
|
|
1161
|
+
this.child = null;
|
|
1162
|
+
attempt.exitCode = code;
|
|
1163
|
+
attempt.text = texts.join("\n");
|
|
1164
|
+
resolve(attempt);
|
|
1165
|
+
});
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
// ../opencode/src/version.ts
|
|
1171
|
+
function versionLine(opencode) {
|
|
1172
|
+
return `mouse/${MOUSE_VERSION} opencode/${opencode ?? "unavailable"}`;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// src/main.ts
|
|
1176
|
+
var EXIT = {
|
|
1177
|
+
satisfied: 0,
|
|
1178
|
+
error: 1,
|
|
1179
|
+
usage: 2,
|
|
1180
|
+
budget: 3,
|
|
1181
|
+
blocked: 4,
|
|
1182
|
+
aborted: 130
|
|
1183
|
+
};
|
|
1184
|
+
function exitCodeFor(outcome) {
|
|
1185
|
+
switch (outcome) {
|
|
1186
|
+
case "satisfied":
|
|
1187
|
+
return EXIT.satisfied;
|
|
1188
|
+
case "blocked":
|
|
1189
|
+
return EXIT.blocked;
|
|
1190
|
+
case "aborted":
|
|
1191
|
+
return EXIT.aborted;
|
|
1192
|
+
case "stalled":
|
|
1193
|
+
case "wall_clock":
|
|
1194
|
+
case "step_budget":
|
|
1195
|
+
return EXIT.budget;
|
|
1196
|
+
default: {
|
|
1197
|
+
const unhandled = outcome;
|
|
1198
|
+
throw new Error(`unhandled outcome ${String(unhandled)}`);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
function configHomeFromEnv(env = process.env) {
|
|
1203
|
+
return path4.join(env.XDG_CONFIG_HOME ?? path4.join(homedir2(), ".config"), "opencode");
|
|
1204
|
+
}
|
|
1205
|
+
function fail(message) {
|
|
1206
|
+
throw new UsageError(message);
|
|
1207
|
+
}
|
|
1208
|
+
var UsageError = class extends Error {
|
|
1209
|
+
};
|
|
1210
|
+
function writeBenchConfig(configHome, model, policy) {
|
|
1211
|
+
mkdirSync2(configHome, { recursive: true });
|
|
1212
|
+
const file = path4.join(configHome, "opencode.json");
|
|
1213
|
+
let existing = {};
|
|
1214
|
+
let raw = null;
|
|
1215
|
+
try {
|
|
1216
|
+
raw = readFileSync(file, "utf8");
|
|
1217
|
+
} catch {
|
|
1218
|
+
raw = null;
|
|
1219
|
+
}
|
|
1220
|
+
if (raw !== null) {
|
|
1221
|
+
try {
|
|
1222
|
+
existing = JSON.parse(raw);
|
|
1223
|
+
} catch (e) {
|
|
1224
|
+
throw new Error(`${file} is not valid JSON (${e.message}); fix or move it first`);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
const config = mergeConfig(
|
|
1228
|
+
existing,
|
|
1229
|
+
buildOpencodeConfig({ profile: "bench", model, permissions: policy.permissions })
|
|
1230
|
+
);
|
|
1231
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}
|
|
1232
|
+
`);
|
|
1233
|
+
writePrunePlugin(configHome, policy);
|
|
1234
|
+
return file;
|
|
1235
|
+
}
|
|
1236
|
+
function writePrunePlugin(configHome, policy) {
|
|
1237
|
+
const prune = policy.context.prune;
|
|
1238
|
+
if (!prune.enabled && process.env.MOUSE_TOOL_OUTPUT_PRUNE !== "1") return null;
|
|
1239
|
+
const pluginDir = path4.join(configHome, "plugin");
|
|
1240
|
+
mkdirSync2(pluginDir, { recursive: true });
|
|
1241
|
+
const file = path4.join(pluginDir, PRUNE_PLUGIN_FILENAME);
|
|
1242
|
+
writeFileSync(file, prunePluginSource(prune));
|
|
1243
|
+
return file;
|
|
1244
|
+
}
|
|
1245
|
+
function positiveInt(v) {
|
|
1246
|
+
if (v === void 0) return void 0;
|
|
1247
|
+
if (!/^\d+$/.test(v) || Number(v) <= 0) fail(`expected a positive integer, got ${v}`);
|
|
1248
|
+
return Number(v);
|
|
1249
|
+
}
|
|
1250
|
+
function parseRunArgs(argv, env = process.env, isTTY = Boolean(process.stdout.isTTY)) {
|
|
1251
|
+
const { values, positionals } = parseArgs({
|
|
1252
|
+
args: argv,
|
|
1253
|
+
allowPositionals: true,
|
|
1254
|
+
options: {
|
|
1255
|
+
"instruction-file": { type: "string" },
|
|
1256
|
+
model: { type: "string", short: "m" },
|
|
1257
|
+
workspace: { type: "string" },
|
|
1258
|
+
"config-home": { type: "string" },
|
|
1259
|
+
log: { type: "string" },
|
|
1260
|
+
profile: { type: "string" },
|
|
1261
|
+
yolo: { type: "boolean", default: false },
|
|
1262
|
+
format: { type: "string" },
|
|
1263
|
+
session: { type: "string" },
|
|
1264
|
+
"max-wall-sec": { type: "string" },
|
|
1265
|
+
"max-steps": { type: "string" },
|
|
1266
|
+
"non-progress-rounds": { type: "string" },
|
|
1267
|
+
"idle-timeout-sec": { type: "string" },
|
|
1268
|
+
"opencode-bin": { type: "string" }
|
|
1269
|
+
},
|
|
1270
|
+
strict: true
|
|
1271
|
+
});
|
|
1272
|
+
let instruction = positionals.join(" ").trim();
|
|
1273
|
+
if (values["instruction-file"]) {
|
|
1274
|
+
if (instruction) fail("pass the task as text or --instruction-file, not both");
|
|
1275
|
+
instruction = readFileSync(values["instruction-file"], "utf8").trim();
|
|
1276
|
+
}
|
|
1277
|
+
if (!instruction) fail('a task is required: mouse run "task" or --instruction-file FILE');
|
|
1278
|
+
const model = values.model ?? env.MOUSE_MODEL;
|
|
1279
|
+
if (!model?.includes("/")) fail("--model provider/model is required (or set MOUSE_MODEL)");
|
|
1280
|
+
const profile = values.profile ?? "local";
|
|
1281
|
+
if (profile !== "local" && profile !== "bench")
|
|
1282
|
+
fail(`--profile must be local or bench, got ${profile}`);
|
|
1283
|
+
const format = values.format ?? (isTTY ? "text" : "json");
|
|
1284
|
+
if (format !== "text" && format !== "json") fail(`--format must be text or json, got ${format}`);
|
|
1285
|
+
return {
|
|
1286
|
+
instruction,
|
|
1287
|
+
model,
|
|
1288
|
+
workspace: path4.resolve(values.workspace ?? process.cwd()),
|
|
1289
|
+
configHome: values["config-home"] ?? configHomeFromEnv(env),
|
|
1290
|
+
logFile: values.log ?? env.MOUSE_HARNESS_LOG,
|
|
1291
|
+
profile,
|
|
1292
|
+
yolo: values.yolo,
|
|
1293
|
+
// `opencode run` reads no stdin, so a permission prompt could never be
|
|
1294
|
+
// answered; without a terminal the only workable mode is to skip them.
|
|
1295
|
+
skipPermissions: values.yolo || !isTTY,
|
|
1296
|
+
warnNoTerminal: !values.yolo && !isTTY,
|
|
1297
|
+
format,
|
|
1298
|
+
session: values.session,
|
|
1299
|
+
maxWallSec: positiveInt(values["max-wall-sec"] ?? env.MOUSE_MAX_WALL_SEC),
|
|
1300
|
+
maxSteps: positiveInt(values["max-steps"]),
|
|
1301
|
+
nonProgressRounds: positiveInt(values["non-progress-rounds"]),
|
|
1302
|
+
idleSec: positiveInt(values["idle-timeout-sec"]),
|
|
1303
|
+
opencodeBin: values["opencode-bin"]
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
async function runCommand2(argv) {
|
|
1307
|
+
const args = parseRunArgs(argv);
|
|
1308
|
+
const startedAt = Date.now();
|
|
1309
|
+
const ws = localWorkspace({ root: args.workspace });
|
|
1310
|
+
const policy = await loadPolicy(ws);
|
|
1311
|
+
const budget = {
|
|
1312
|
+
maxWallSec: args.maxWallSec ?? policy.loop.maxWallSec,
|
|
1313
|
+
maxSteps: args.maxSteps ?? policy.loop.maxSteps,
|
|
1314
|
+
nonProgressRounds: args.nonProgressRounds ?? policy.loop.nonProgressRounds,
|
|
1315
|
+
idleSec: args.idleSec ?? policy.loop.idleTimeoutSec
|
|
1316
|
+
};
|
|
1317
|
+
const trace = openTrace(args.logFile ?? traceFile(args.workspace));
|
|
1318
|
+
const say = (line) => {
|
|
1319
|
+
if (args.format === "text") process.stdout.write(`${line}
|
|
1320
|
+
`);
|
|
1321
|
+
};
|
|
1322
|
+
const bin = locateOpencode({ flag: args.opencodeBin, cwd: args.workspace });
|
|
1323
|
+
if (!bin)
|
|
1324
|
+
fail(
|
|
1325
|
+
"opencode not found: install it (npm i -g opencode-ai), or pass --opencode-bin / MOUSE_OPENCODE_BIN"
|
|
1326
|
+
);
|
|
1327
|
+
const env = { ...process.env };
|
|
1328
|
+
let configFile;
|
|
1329
|
+
if (args.profile === "bench") {
|
|
1330
|
+
configFile = writeBenchConfig(args.configHome, args.model, policy);
|
|
1331
|
+
} else {
|
|
1332
|
+
env.OPENCODE_CONFIG_CONTENT = JSON.stringify(
|
|
1333
|
+
buildOpencodeConfig({ profile: "local", model: args.model, permissions: policy.permissions })
|
|
1334
|
+
);
|
|
1335
|
+
const plugin = writePrunePlugin(args.configHome, policy);
|
|
1336
|
+
if (plugin) say(`mouse: wrote ${plugin} (context.prune is enabled in .mouse/policy.json)`);
|
|
1337
|
+
}
|
|
1338
|
+
if (args.warnNoTerminal) {
|
|
1339
|
+
process.stderr.write(
|
|
1340
|
+
"mouse: no terminal to answer permission prompts; running as if --yolo was passed\n"
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
const marker = path4.join(tmpdir(), `mouse-${process.pid}.marker`);
|
|
1344
|
+
writeFileSync(marker, "");
|
|
1345
|
+
const abort = new AbortController();
|
|
1346
|
+
const onSignal = () => abort.abort();
|
|
1347
|
+
process.on("SIGTERM", onSignal);
|
|
1348
|
+
process.on("SIGINT", onSignal);
|
|
1349
|
+
const runStartSha = await headSha(ws);
|
|
1350
|
+
const probe = makeProbe({
|
|
1351
|
+
workspace: ws,
|
|
1352
|
+
runStartSha,
|
|
1353
|
+
marker,
|
|
1354
|
+
signal: abort.signal,
|
|
1355
|
+
checks: policy.verify.checks,
|
|
1356
|
+
checkTimeoutMs: policy.verify.timeoutSec * 1e3
|
|
1357
|
+
});
|
|
1358
|
+
const initialFingerprint = await probe.fingerprint();
|
|
1359
|
+
trace.write({
|
|
1360
|
+
type: "mouse.start",
|
|
1361
|
+
v: 1,
|
|
1362
|
+
version: MOUSE_VERSION,
|
|
1363
|
+
model: args.model,
|
|
1364
|
+
workspace: args.workspace,
|
|
1365
|
+
profile: args.profile,
|
|
1366
|
+
runStartSha,
|
|
1367
|
+
maxWallSec: budget.maxWallSec,
|
|
1368
|
+
...configFile ? { configFile } : {}
|
|
1369
|
+
});
|
|
1370
|
+
const engine = new OpencodeRunEngine({
|
|
1371
|
+
bin,
|
|
1372
|
+
cwd: args.workspace,
|
|
1373
|
+
model: args.model,
|
|
1374
|
+
agent: "build",
|
|
1375
|
+
env,
|
|
1376
|
+
idleTimeoutMs: budget.idleSec * 1e3,
|
|
1377
|
+
skipPermissions: args.skipPermissions,
|
|
1378
|
+
passthrough: (line) => {
|
|
1379
|
+
if (args.format === "json") process.stdout.write(`${line}
|
|
1380
|
+
`);
|
|
1381
|
+
},
|
|
1382
|
+
passthroughErr: (line) => process.stderr.write(`${line}
|
|
1383
|
+
`),
|
|
1384
|
+
trace: (r) => trace.write(r)
|
|
1385
|
+
});
|
|
1386
|
+
say(`mouse ${MOUSE_VERSION}: ${args.model}, profile ${args.profile}, trace ${trace.file}`);
|
|
1387
|
+
let exitCode = EXIT.error;
|
|
1388
|
+
try {
|
|
1389
|
+
const sessionId = await engine.open({
|
|
1390
|
+
sessionId: args.session,
|
|
1391
|
+
title: args.instruction.slice(0, 60).replace(/\s+/g, " ")
|
|
1392
|
+
});
|
|
1393
|
+
const first = await engine.prompt(sessionId, args.instruction, abort.signal);
|
|
1394
|
+
trace.write({
|
|
1395
|
+
type: "mouse.turn",
|
|
1396
|
+
steps: engine.steps,
|
|
1397
|
+
tokens: engine.tokens,
|
|
1398
|
+
sessionId: engine.sessionId
|
|
1399
|
+
});
|
|
1400
|
+
say(`turn 1: ${first.steps} steps`);
|
|
1401
|
+
const result = await runCompletionLoop({
|
|
1402
|
+
probe,
|
|
1403
|
+
engine,
|
|
1404
|
+
sessionId: engine.sessionId ?? sessionId,
|
|
1405
|
+
model: args.model,
|
|
1406
|
+
signal: abort.signal,
|
|
1407
|
+
instruction: args.instruction,
|
|
1408
|
+
onEvent: (e) => {
|
|
1409
|
+
trace.write({ type: "mouse.event", event: e });
|
|
1410
|
+
if (e.type === "check_status") say(`check ${e.name}: ${e.conclusion}`);
|
|
1411
|
+
else say(`notice: ${e.message}`);
|
|
1412
|
+
},
|
|
1413
|
+
budget: {
|
|
1414
|
+
maxWallMs: budget.maxWallSec * 1e3,
|
|
1415
|
+
maxTotalSteps: budget.maxSteps,
|
|
1416
|
+
maxNonProgressRounds: budget.nonProgressRounds,
|
|
1417
|
+
minRoundMs: policy.loop.minRoundSec * 1e3
|
|
1418
|
+
},
|
|
1419
|
+
startedAt,
|
|
1420
|
+
initialFingerprint,
|
|
1421
|
+
steps: () => engine.steps,
|
|
1422
|
+
lastAssistantText: () => engine.lastText,
|
|
1423
|
+
onRound: (r) => {
|
|
1424
|
+
trace.write({ type: "mouse.round", ...r });
|
|
1425
|
+
say(
|
|
1426
|
+
`round ${r.round} (${r.kind}): ${r.progressed ? "progressed" : "no change"}, ${r.stepsAfter} steps total`
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
trace.write({
|
|
1431
|
+
type: "mouse.done",
|
|
1432
|
+
outcome: result.outcome,
|
|
1433
|
+
rounds: result.rounds.length,
|
|
1434
|
+
checksRun: result.rounds.at(-1)?.checksRun ?? [],
|
|
1435
|
+
totalSteps: result.totalSteps,
|
|
1436
|
+
tokens: engine.tokens,
|
|
1437
|
+
elapsedMs: Date.now() - startedAt,
|
|
1438
|
+
sessionId: engine.sessionId
|
|
1439
|
+
});
|
|
1440
|
+
if (args.format === "text" && engine.lastText) say(`
|
|
1441
|
+
${engine.lastText}
|
|
1442
|
+
`);
|
|
1443
|
+
say(
|
|
1444
|
+
`outcome: ${result.outcome} after ${result.rounds.length} round(s), ${result.totalSteps} steps, ${Math.round((Date.now() - startedAt) / 1e3)}s, $${engine.tokens.cost.toFixed(2)}`
|
|
1445
|
+
);
|
|
1446
|
+
exitCode = exitCodeFor(result.outcome);
|
|
1447
|
+
} catch (e) {
|
|
1448
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1449
|
+
trace.write({
|
|
1450
|
+
type: "mouse.error",
|
|
1451
|
+
error: message,
|
|
1452
|
+
steps: engine.steps,
|
|
1453
|
+
elapsedMs: Date.now() - startedAt
|
|
1454
|
+
});
|
|
1455
|
+
process.stderr.write(`mouse: ${message}
|
|
1456
|
+
`);
|
|
1457
|
+
exitCode = EXIT.error;
|
|
1458
|
+
} finally {
|
|
1459
|
+
process.off("SIGTERM", onSignal);
|
|
1460
|
+
process.off("SIGINT", onSignal);
|
|
1461
|
+
rmSync(marker, { force: true });
|
|
1462
|
+
}
|
|
1463
|
+
return exitCode;
|
|
1464
|
+
}
|
|
1465
|
+
async function configCommand(argv) {
|
|
1466
|
+
const { values } = parseArgs({
|
|
1467
|
+
args: argv,
|
|
1468
|
+
options: {
|
|
1469
|
+
out: { type: "string" },
|
|
1470
|
+
model: { type: "string" },
|
|
1471
|
+
profile: { type: "string" },
|
|
1472
|
+
workspace: { type: "string" }
|
|
1473
|
+
},
|
|
1474
|
+
strict: true
|
|
1475
|
+
});
|
|
1476
|
+
const profile = values.profile ?? "local";
|
|
1477
|
+
const ws = localWorkspace({ root: path4.resolve(values.workspace ?? process.cwd()) });
|
|
1478
|
+
const policy = await loadPolicy(ws);
|
|
1479
|
+
if (profile === "bench") {
|
|
1480
|
+
const file = writeBenchConfig(values.out ?? configHomeFromEnv(), values.model, policy);
|
|
1481
|
+
process.stdout.write(
|
|
1482
|
+
`wrote ${file}
|
|
1483
|
+
|
|
1484
|
+
--- agent prompt (build) ---
|
|
1485
|
+
${buildAgentPrompt("build", { profile })}
|
|
1486
|
+
`
|
|
1487
|
+
);
|
|
1488
|
+
return 0;
|
|
1489
|
+
}
|
|
1490
|
+
const config = buildOpencodeConfig({
|
|
1491
|
+
profile: "local",
|
|
1492
|
+
model: values.model,
|
|
1493
|
+
permissions: policy.permissions
|
|
1494
|
+
});
|
|
1495
|
+
process.stdout.write(`${JSON.stringify(config, null, 2)}
|
|
1496
|
+
`);
|
|
1497
|
+
return 0;
|
|
1498
|
+
}
|
|
1499
|
+
var POLICY_SKELETON = {
|
|
1500
|
+
$docs: "https://github.com/mousedev/mouse-harness/blob/main/docs/config.md",
|
|
1501
|
+
version: 1,
|
|
1502
|
+
verify: { timeoutSec: DEFAULT_POLICY.verify.timeoutSec },
|
|
1503
|
+
loop: { ...DEFAULT_POLICY.loop },
|
|
1504
|
+
context: { prune: { ...DEFAULT_POLICY.context.prune } },
|
|
1505
|
+
permissions: { bash: { "git push*": "deny", "rm -rf *": "deny" } }
|
|
1506
|
+
};
|
|
1507
|
+
function initCommand(argv) {
|
|
1508
|
+
const { values } = parseArgs({
|
|
1509
|
+
args: argv,
|
|
1510
|
+
options: { workspace: { type: "string" } },
|
|
1511
|
+
strict: true
|
|
1512
|
+
});
|
|
1513
|
+
const root = path4.resolve(values.workspace ?? process.cwd());
|
|
1514
|
+
const dir = path4.join(root, ".mouse");
|
|
1515
|
+
const file = path4.join(dir, "policy.json");
|
|
1516
|
+
try {
|
|
1517
|
+
readFileSync(file);
|
|
1518
|
+
process.stdout.write(`${file} already exists; nothing written
|
|
1519
|
+
`);
|
|
1520
|
+
return 0;
|
|
1521
|
+
} catch {
|
|
1522
|
+
mkdirSync2(dir, { recursive: true });
|
|
1523
|
+
writeFileSync(file, `${JSON.stringify(POLICY_SKELETON, null, 2)}
|
|
1524
|
+
`);
|
|
1525
|
+
process.stdout.write(`wrote ${file}
|
|
1526
|
+
`);
|
|
1527
|
+
return 0;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
var PROVIDER_KEY_ENV = {
|
|
1531
|
+
anthropic: ["ANTHROPIC_API_KEY"],
|
|
1532
|
+
openai: ["OPENAI_API_KEY"],
|
|
1533
|
+
openrouter: ["OPENROUTER_API_KEY"],
|
|
1534
|
+
"fireworks-ai": ["FIREWORKS_API_KEY"],
|
|
1535
|
+
moonshotai: ["MOONSHOT_API_KEY"],
|
|
1536
|
+
togetherai: ["TOGETHER_API_KEY"],
|
|
1537
|
+
google: ["GOOGLE_GENERATIVE_AI_API_KEY", "GEMINI_API_KEY"]
|
|
1538
|
+
};
|
|
1539
|
+
async function doctorCommand(argv) {
|
|
1540
|
+
const { values } = parseArgs({
|
|
1541
|
+
args: argv,
|
|
1542
|
+
options: {
|
|
1543
|
+
model: { type: "string" },
|
|
1544
|
+
workspace: { type: "string" },
|
|
1545
|
+
"opencode-bin": { type: "string" },
|
|
1546
|
+
"strict-compat": { type: "boolean", default: false }
|
|
1547
|
+
},
|
|
1548
|
+
strict: true
|
|
1549
|
+
});
|
|
1550
|
+
const lines = [];
|
|
1551
|
+
let failed = false;
|
|
1552
|
+
const root = path4.resolve(values.workspace ?? process.cwd());
|
|
1553
|
+
const bin = locateOpencode({ flag: values["opencode-bin"], cwd: root });
|
|
1554
|
+
const version = bin ? opencodeVersion(bin) : null;
|
|
1555
|
+
const compat = checkCompat(version);
|
|
1556
|
+
lines.push(`mouse ${MOUSE_VERSION}`);
|
|
1557
|
+
lines.push(
|
|
1558
|
+
`opencode ${bin ?? "not found (npm i -g opencode-ai)"}${version ? ` (${version})` : ""}`
|
|
1559
|
+
);
|
|
1560
|
+
if (!bin || !version) failed = true;
|
|
1561
|
+
if (compat.level === "supported") lines.push(`compat ${compat.version}: supported`);
|
|
1562
|
+
else if (compat.level === "untested") {
|
|
1563
|
+
lines.push(
|
|
1564
|
+
`compat ${compat.version}: untested (supported: ${compat.supported.join(", ")})`
|
|
1565
|
+
);
|
|
1566
|
+
if (values["strict-compat"]) failed = true;
|
|
1567
|
+
} else lines.push("compat unknown");
|
|
1568
|
+
const model = values.model ?? process.env.MOUSE_MODEL;
|
|
1569
|
+
if (model?.includes("/")) {
|
|
1570
|
+
const provider = model.split("/")[0];
|
|
1571
|
+
const envs = PROVIDER_KEY_ENV[provider] ?? [
|
|
1572
|
+
`${provider.toUpperCase().replace(/-/g, "_")}_API_KEY`
|
|
1573
|
+
];
|
|
1574
|
+
const present = envs.find((e) => process.env[e]);
|
|
1575
|
+
lines.push(
|
|
1576
|
+
`provider ${provider}: ${present ? `${present} set` : `${envs.join("/")} not in env (opencode's own auth store may hold it: opencode auth list)`}`
|
|
1577
|
+
);
|
|
1578
|
+
} else {
|
|
1579
|
+
lines.push("provider pass --model provider/model to check its key");
|
|
1580
|
+
}
|
|
1581
|
+
const ws = localWorkspace({ root });
|
|
1582
|
+
const sha = await headSha(ws);
|
|
1583
|
+
lines.push(
|
|
1584
|
+
`git ${sha ? `repo at ${sha.slice(0, 12)}` : "not a git repo (fingerprint falls back to mtime scan)"}`
|
|
1585
|
+
);
|
|
1586
|
+
const policy = await loadPolicy(ws);
|
|
1587
|
+
const checks = policy.verify.checks ?? checksFromEcosystem(await detectEcosystem(ws));
|
|
1588
|
+
lines.push(
|
|
1589
|
+
`checks ${checks.length ? checks.map((c) => `${c.name} (${c.command})`).join("; ") : "none detected; the loop can only prove that files changed"}${policy.verify.checks ? " [from .mouse/policy.json]" : ""}`
|
|
1590
|
+
);
|
|
1591
|
+
lines.push(`traces ${runsDir(root)}`);
|
|
1592
|
+
process.stdout.write(`${lines.join("\n")}
|
|
1593
|
+
`);
|
|
1594
|
+
return failed ? 1 : 0;
|
|
1595
|
+
}
|
|
1596
|
+
function versionCommand(argv) {
|
|
1597
|
+
const { values } = parseArgs({
|
|
1598
|
+
args: argv,
|
|
1599
|
+
options: { "opencode-bin": { type: "string" } },
|
|
1600
|
+
strict: true
|
|
1601
|
+
});
|
|
1602
|
+
const bin = locateOpencode({ flag: values["opencode-bin"] });
|
|
1603
|
+
process.stdout.write(`${versionLine(bin ? opencodeVersion(bin) : null)}
|
|
1604
|
+
`);
|
|
1605
|
+
return 0;
|
|
1606
|
+
}
|
|
1607
|
+
var USAGE = `mouse ${MOUSE_VERSION}: run OpenCode in a repo until its checks pass.
|
|
1608
|
+
|
|
1609
|
+
usage:
|
|
1610
|
+
mouse run "task" | --instruction-file F --model provider/model [--workspace DIR]
|
|
1611
|
+
[--profile local|bench] [--yolo] [--format text|json] [--log FILE] [--session ID]
|
|
1612
|
+
[--max-wall-sec N] [--max-steps N] [--non-progress-rounds N] [--idle-timeout-sec N]
|
|
1613
|
+
[--config-home DIR] [--opencode-bin PATH]
|
|
1614
|
+
mouse config [--profile local|bench] [--model M] [--out DIR]
|
|
1615
|
+
mouse init [--workspace DIR]
|
|
1616
|
+
mouse doctor [--model M] [--workspace DIR] [--strict-compat]
|
|
1617
|
+
mouse --version
|
|
1618
|
+
|
|
1619
|
+
exit codes: 0 satisfied, 1 error, 2 usage, 3 budget (stalled, wall clock, steps), 4 blocked, 130 aborted
|
|
1620
|
+
`;
|
|
1621
|
+
async function main(argv = process.argv.slice(2)) {
|
|
1622
|
+
const [command, ...rest] = argv;
|
|
1623
|
+
try {
|
|
1624
|
+
switch (command) {
|
|
1625
|
+
case "run":
|
|
1626
|
+
return await runCommand2(rest);
|
|
1627
|
+
case "config":
|
|
1628
|
+
return await configCommand(rest);
|
|
1629
|
+
case "init":
|
|
1630
|
+
return initCommand(rest);
|
|
1631
|
+
case "doctor":
|
|
1632
|
+
return await doctorCommand(rest);
|
|
1633
|
+
case "version":
|
|
1634
|
+
case "--version":
|
|
1635
|
+
case "-v":
|
|
1636
|
+
return versionCommand(rest);
|
|
1637
|
+
case "help":
|
|
1638
|
+
case "--help":
|
|
1639
|
+
case "-h":
|
|
1640
|
+
process.stdout.write(USAGE);
|
|
1641
|
+
return 0;
|
|
1642
|
+
default:
|
|
1643
|
+
process.stderr.write(USAGE);
|
|
1644
|
+
return EXIT.usage;
|
|
1645
|
+
}
|
|
1646
|
+
} catch (e) {
|
|
1647
|
+
if (e instanceof UsageError || isParseArgsError(e)) {
|
|
1648
|
+
process.stderr.write(`mouse: ${e.message}
|
|
1649
|
+
`);
|
|
1650
|
+
return EXIT.usage;
|
|
1651
|
+
}
|
|
1652
|
+
if (e instanceof Error) {
|
|
1653
|
+
process.stderr.write(`mouse: ${e.message}
|
|
1654
|
+
`);
|
|
1655
|
+
return EXIT.error;
|
|
1656
|
+
}
|
|
1657
|
+
throw e;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
function isParseArgsError(e) {
|
|
1661
|
+
return e instanceof Error && String(e.code).startsWith("ERR_PARSE_ARGS_");
|
|
1662
|
+
}
|
|
1663
|
+
function invokedDirectly() {
|
|
1664
|
+
const entry = process.argv[1];
|
|
1665
|
+
if (typeof entry !== "string") return false;
|
|
1666
|
+
try {
|
|
1667
|
+
return realpathSync(entry) === fileURLToPath(import.meta.url);
|
|
1668
|
+
} catch {
|
|
1669
|
+
return false;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
if (invokedDirectly()) {
|
|
1673
|
+
main().then(
|
|
1674
|
+
(code) => process.exit(code),
|
|
1675
|
+
(e) => {
|
|
1676
|
+
process.stderr.write(`mouse: ${e instanceof Error ? e.message : String(e)}
|
|
1677
|
+
`);
|
|
1678
|
+
process.exit(EXIT.error);
|
|
1679
|
+
}
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
export {
|
|
1683
|
+
EXIT,
|
|
1684
|
+
exitCodeFor,
|
|
1685
|
+
main,
|
|
1686
|
+
parseRunArgs,
|
|
1687
|
+
writeBenchConfig
|
|
1688
|
+
};
|