pitroom 0.22.0 → 0.23.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +30 -0
- package/README.md +37 -4
- package/dist/lib.mjs +1234 -659
- package/dist/pitroom.mjs +1270 -495
- package/dist/ui/app.css +1 -1
- package/dist/ui/app.js +32 -30
- package/docs/backends.md +56 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/policies/gemini/shell.toml +4 -1
- package/policies/qwen/worker-defaults.json +9 -0
package/dist/pitroom.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
4
|
+
import fs41 from "node:fs";
|
|
5
5
|
|
|
6
6
|
// src/core/errors.ts
|
|
7
7
|
var UserError = class extends Error {
|
|
@@ -220,7 +220,10 @@ var DENY_BASH = [
|
|
|
220
220
|
"opencode",
|
|
221
221
|
"claude",
|
|
222
222
|
"codex",
|
|
223
|
-
"gemini"
|
|
223
|
+
"gemini",
|
|
224
|
+
"qwen",
|
|
225
|
+
"kilo",
|
|
226
|
+
"kilocode"
|
|
224
227
|
].map((c) => `Bash(${c}:*)`);
|
|
225
228
|
var SECRETS = ["**/.env", "**/.env.*", "**/*.pem", "**/id_rsa*", "**/id_ed25519*"].map((p) => `Read(${p})`);
|
|
226
229
|
function toolsFor(mode, web2) {
|
|
@@ -940,6 +943,10 @@ var gemini = {
|
|
|
940
943
|
doctor: doctor3
|
|
941
944
|
};
|
|
942
945
|
|
|
946
|
+
// src/backends/kilo/index.ts
|
|
947
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
948
|
+
import path7 from "node:path";
|
|
949
|
+
|
|
943
950
|
// src/backends/opencode/index.ts
|
|
944
951
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
945
952
|
|
|
@@ -1149,17 +1156,19 @@ var FORBIDDEN_BASH = [
|
|
|
1149
1156
|
"twine upload*",
|
|
1150
1157
|
// no recursive delegation
|
|
1151
1158
|
"opencode*",
|
|
1159
|
+
"kilo *",
|
|
1160
|
+
"kilocode*",
|
|
1152
1161
|
"pitroom *",
|
|
1153
1162
|
"*pitroom*"
|
|
1154
1163
|
];
|
|
1155
|
-
function scratchDirs() {
|
|
1164
|
+
function scratchDirs(app) {
|
|
1156
1165
|
const data = process.env.XDG_DATA_HOME ?? path6.join(os6.homedir(), ".local", "share");
|
|
1157
1166
|
const rules = {
|
|
1158
1167
|
"*": "deny",
|
|
1159
|
-
[path6.join(data,
|
|
1160
|
-
[path6.join(data,
|
|
1168
|
+
[path6.join(data, app, "tool-output", "*")]: "allow",
|
|
1169
|
+
[path6.join(data, app, "shell", "*", "*")]: "allow"
|
|
1161
1170
|
};
|
|
1162
|
-
for (const tmp of /* @__PURE__ */ new Set([os6.tmpdir(), realpath(os6.tmpdir())])) rules[path6.join(tmp,
|
|
1171
|
+
for (const tmp of /* @__PURE__ */ new Set([os6.tmpdir(), realpath(os6.tmpdir())])) rules[path6.join(tmp, app, "*")] = "allow";
|
|
1163
1172
|
return rules;
|
|
1164
1173
|
}
|
|
1165
1174
|
function realpath(p) {
|
|
@@ -1180,7 +1189,7 @@ var COMMON = {
|
|
|
1180
1189
|
doom_loop: "deny"
|
|
1181
1190
|
};
|
|
1182
1191
|
var web = (on) => ({ webfetch: on ? "allow" : "deny", websearch: on ? "allow" : "deny" });
|
|
1183
|
-
function readProfile(allowWeb = false) {
|
|
1192
|
+
function readProfile(allowWeb = false, app = "opencode") {
|
|
1184
1193
|
return {
|
|
1185
1194
|
"*": "deny",
|
|
1186
1195
|
read: READ_FILES,
|
|
@@ -1196,11 +1205,11 @@ function readProfile(allowWeb = false) {
|
|
|
1196
1205
|
bash: READ_ONLY_BASH,
|
|
1197
1206
|
shell: READ_ONLY_BASH,
|
|
1198
1207
|
edit: "deny",
|
|
1199
|
-
external_directory: scratchDirs(),
|
|
1208
|
+
external_directory: scratchDirs(app),
|
|
1200
1209
|
...COMMON
|
|
1201
1210
|
};
|
|
1202
1211
|
}
|
|
1203
|
-
function writeProfile(allowWeb = false) {
|
|
1212
|
+
function writeProfile(allowWeb = false, app = "opencode") {
|
|
1204
1213
|
const bash = { "*": "allow" };
|
|
1205
1214
|
for (const pattern of FORBIDDEN_BASH) bash[pattern] = "deny";
|
|
1206
1215
|
return {
|
|
@@ -1209,25 +1218,26 @@ function writeProfile(allowWeb = false) {
|
|
|
1209
1218
|
...web(allowWeb),
|
|
1210
1219
|
bash,
|
|
1211
1220
|
shell: bash,
|
|
1212
|
-
external_directory: scratchDirs(),
|
|
1221
|
+
external_directory: scratchDirs(app),
|
|
1213
1222
|
...COMMON
|
|
1214
1223
|
};
|
|
1215
1224
|
}
|
|
1216
1225
|
function agentFor(mode) {
|
|
1217
1226
|
return mode === "read" ? AGENT.read : AGENT.write;
|
|
1218
1227
|
}
|
|
1219
|
-
function configContent(existing, allowWeb = false) {
|
|
1228
|
+
function configContent(existing, allowWeb = false, app = "opencode", extra = {}) {
|
|
1220
1229
|
const ours = {
|
|
1230
|
+
...extra,
|
|
1221
1231
|
agent: {
|
|
1222
1232
|
[AGENT.read]: {
|
|
1223
1233
|
mode: "primary",
|
|
1224
1234
|
description: "pitroom: read-only worker (research, search, review)",
|
|
1225
|
-
permission: readProfile(allowWeb)
|
|
1235
|
+
permission: readProfile(allowWeb, app)
|
|
1226
1236
|
},
|
|
1227
1237
|
[AGENT.write]: {
|
|
1228
1238
|
mode: "primary",
|
|
1229
1239
|
description: "pitroom: editing worker (no git history changes, no bulk deletes)",
|
|
1230
|
-
permission: writeProfile(allowWeb)
|
|
1240
|
+
permission: writeProfile(allowWeb, app)
|
|
1231
1241
|
}
|
|
1232
1242
|
}
|
|
1233
1243
|
};
|
|
@@ -1404,10 +1414,348 @@ var opencode = {
|
|
|
1404
1414
|
doctor: doctor4
|
|
1405
1415
|
};
|
|
1406
1416
|
|
|
1417
|
+
// src/backends/kilo/index.ts
|
|
1418
|
+
var binary5 = () => findBinary("kilo", "PITROOM_KILO_BIN", ["~/.kilo/bin/kilo"]);
|
|
1419
|
+
function kilo(args, timeout = 6e4) {
|
|
1420
|
+
const { command, prefix } = resolveCommand(binary5());
|
|
1421
|
+
const r = spawnSync5(command, [...prefix, ...args], { encoding: "utf8", timeout, stdio: ["ignore", "pipe", "pipe"], maxBuffer: 64 * 1024 * 1024 });
|
|
1422
|
+
return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
|
|
1423
|
+
}
|
|
1424
|
+
function invocation5(req) {
|
|
1425
|
+
const args = [
|
|
1426
|
+
"run",
|
|
1427
|
+
"--agent",
|
|
1428
|
+
agentFor(req.mode),
|
|
1429
|
+
"--format",
|
|
1430
|
+
"json",
|
|
1431
|
+
"--title",
|
|
1432
|
+
req.title,
|
|
1433
|
+
// Some failure causes only appear in the server logs (Kilo takes the level in capitals only).
|
|
1434
|
+
"--print-logs",
|
|
1435
|
+
"--log-level",
|
|
1436
|
+
"ERROR"
|
|
1437
|
+
];
|
|
1438
|
+
if (req.model) args.push("--model", req.model);
|
|
1439
|
+
if (req.sessionId) args.push("--session", req.sessionId);
|
|
1440
|
+
for (const f of req.files) args.push("--file", f);
|
|
1441
|
+
args.push(req.prompt);
|
|
1442
|
+
const { command, prefix } = resolveCommand(binary5());
|
|
1443
|
+
return {
|
|
1444
|
+
command,
|
|
1445
|
+
args: [...prefix, ...args],
|
|
1446
|
+
env: {
|
|
1447
|
+
// A worker run must not upgrade Kilo underneath the user.
|
|
1448
|
+
KILO_CONFIG_CONTENT: configContent(process.env.KILO_CONFIG_CONTENT, req.web, "kilo", { autoupdate: false }),
|
|
1449
|
+
// No daemon to attach to: a private server per run (see above).
|
|
1450
|
+
XDG_STATE_HOME: path7.join(home(), "kilo-state")
|
|
1451
|
+
}
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
function failure5(run2, stderr, exitCode) {
|
|
1455
|
+
if (exitCode === 0 && !run2.error) return void 0;
|
|
1456
|
+
const detail = stderr.match(/error="([^"]+)"/g)?.pop()?.slice(7, -1);
|
|
1457
|
+
const raw = run2.error ?? `kilo exited with code ${exitCode}`;
|
|
1458
|
+
const message = detail && !raw.includes(detail) ? `${raw}: ${detail}` : raw;
|
|
1459
|
+
return { kind: classify4(message), message };
|
|
1460
|
+
}
|
|
1461
|
+
function listModels2() {
|
|
1462
|
+
return kilo(["models"]).out.split("\n").map((s) => s.trim()).filter((s) => /^[\w.-]+\/\S+$/.test(s));
|
|
1463
|
+
}
|
|
1464
|
+
function defaultModel4() {
|
|
1465
|
+
try {
|
|
1466
|
+
const out = kilo(["debug", "config"], 3e4).out;
|
|
1467
|
+
const model = JSON.parse(out.slice(out.indexOf("{"))).model;
|
|
1468
|
+
return typeof model === "string" ? model : void 0;
|
|
1469
|
+
} catch {
|
|
1470
|
+
return void 0;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
function catalog5() {
|
|
1474
|
+
return { models: listModels2().map((id) => ({ id })), source: "`kilo models` (reasoning variants are provider-specific: provider/model#variant)" };
|
|
1475
|
+
}
|
|
1476
|
+
function doctor5({ models, hasFallback }) {
|
|
1477
|
+
const version = kilo(["--version"]);
|
|
1478
|
+
if (version.missing || !version.ok) {
|
|
1479
|
+
return [{ level: "fail", message: `kilo not runnable (${binary5()}). Install Kilo Code's CLI (npm i -g @kilocode/cli), or set PITROOM_KILO_BIN` }];
|
|
1480
|
+
}
|
|
1481
|
+
const checks = [{ level: "ok", message: `Kilo Code ${version.out.trim().split("\n").pop()} at ${binary5()} (beta worker: checked against 7.8)` }];
|
|
1482
|
+
const def = defaultModel4();
|
|
1483
|
+
for (const m of models) {
|
|
1484
|
+
const model = m ?? def;
|
|
1485
|
+
checks.push(model ? { level: "ok", message: `Kilo Code ${m ? "model" : "default model"}: ${model}` } : { level: "warn", message: 'Kilo Code has no default model: pass -W kilo:<provider/model> or set "model" in its config' });
|
|
1486
|
+
}
|
|
1487
|
+
if (!hasFallback) checks.push({ level: "warn", message: "no fallback workers configured for Kilo Code runs" });
|
|
1488
|
+
checks.push({ level: "ok", message: `permission profiles ${AGENT.read}/${AGENT.write} injected per run through KILO_CONFIG_CONTENT` });
|
|
1489
|
+
return checks;
|
|
1490
|
+
}
|
|
1491
|
+
var kiloBackend = {
|
|
1492
|
+
id: "kilo",
|
|
1493
|
+
name: "Kilo Code",
|
|
1494
|
+
capabilities: { readOnly: "permission-rules", resume: "by-id", reportsCost: true, attachFiles: true },
|
|
1495
|
+
binary: binary5,
|
|
1496
|
+
invocation: invocation5,
|
|
1497
|
+
parse: parseEvents4,
|
|
1498
|
+
failure: failure5,
|
|
1499
|
+
defaultModel: defaultModel4,
|
|
1500
|
+
listModels: listModels2,
|
|
1501
|
+
catalog: catalog5,
|
|
1502
|
+
doctor: doctor5
|
|
1503
|
+
};
|
|
1504
|
+
|
|
1505
|
+
// src/backends/qwen/index.ts
|
|
1506
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
1507
|
+
import fs8 from "node:fs";
|
|
1508
|
+
import os7 from "node:os";
|
|
1509
|
+
import path8 from "node:path";
|
|
1510
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1511
|
+
|
|
1512
|
+
// src/backends/qwen/events.ts
|
|
1513
|
+
var EDIT_TOOLS4 = /* @__PURE__ */ new Set(["write_file", "edit", "notebook_edit"]);
|
|
1514
|
+
var SHELL = "run_shell_command";
|
|
1515
|
+
var DENIED4 = /permission was declined|denied by permission rules|requires permission|not allowed/i;
|
|
1516
|
+
var API_ERROR = /^\[API Error: /;
|
|
1517
|
+
function parseEvents5(jsonl) {
|
|
1518
|
+
const usage2 = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0, steps: 0, toolCalls: 0, denied: 0 };
|
|
1519
|
+
const tools = {};
|
|
1520
|
+
const edits = [];
|
|
1521
|
+
const pendingEdits = /* @__PURE__ */ new Map();
|
|
1522
|
+
const timeline = [];
|
|
1523
|
+
const stepOf = /* @__PURE__ */ new Map();
|
|
1524
|
+
let sessionId;
|
|
1525
|
+
let model;
|
|
1526
|
+
let lastText = "";
|
|
1527
|
+
let finalText;
|
|
1528
|
+
let lastActivity;
|
|
1529
|
+
let error;
|
|
1530
|
+
for (const line of jsonl.split("\n")) {
|
|
1531
|
+
if (!line.trim()) continue;
|
|
1532
|
+
let e;
|
|
1533
|
+
try {
|
|
1534
|
+
e = JSON.parse(line);
|
|
1535
|
+
} catch {
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
sessionId ??= e.session_id;
|
|
1539
|
+
if (e.type === "system" && e.subtype === "init") {
|
|
1540
|
+
model = e.model;
|
|
1541
|
+
} else if (e.type === "system" && e.subtype === "retry") {
|
|
1542
|
+
lastActivity = `waiting to retry (attempt ${e.data?.attempt ?? "?"} of ${e.data?.maxRetries ?? "?"})`;
|
|
1543
|
+
} else if (e.type === "assistant") {
|
|
1544
|
+
const content = e.message?.content ?? [];
|
|
1545
|
+
const text = content.filter((c) => c.type === "text").map((c) => String(c.text ?? "")).join(" ").trim();
|
|
1546
|
+
if (API_ERROR.test(text)) {
|
|
1547
|
+
error = apiError(text);
|
|
1548
|
+
continue;
|
|
1549
|
+
}
|
|
1550
|
+
usage2.steps++;
|
|
1551
|
+
for (const c of content) {
|
|
1552
|
+
if (c.type === "text" && String(c.text ?? "").trim()) {
|
|
1553
|
+
lastText = String(c.text).trim();
|
|
1554
|
+
lastActivity = `says: ${oneLine5(lastText)}`;
|
|
1555
|
+
if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip5(lastText, 600) });
|
|
1556
|
+
} else if (c.type === "tool_use") {
|
|
1557
|
+
usage2.toolCalls++;
|
|
1558
|
+
tools[c.name] = (tools[c.name] ?? 0) + 1;
|
|
1559
|
+
const target = c.input?.file_path ?? c.input?.path ?? c.input?.pattern ?? c.input?.command ?? c.input?.url;
|
|
1560
|
+
lastActivity = `${c.name} ${target ? oneLine5(String(target), 60) : ""}`.trim();
|
|
1561
|
+
if (EDIT_TOOLS4.has(c.name) && c.input?.file_path) pendingEdits.set(String(c.id), String(c.input.file_path));
|
|
1562
|
+
if (timeline.length < MAX_STEPS) {
|
|
1563
|
+
const step = { kind: EDIT_TOOLS4.has(c.name) ? "edit" : c.name === SHELL ? "shell" : "tool", name: c.name, text: clip5(String(target ?? ""), 240) };
|
|
1564
|
+
timeline.push(step);
|
|
1565
|
+
stepOf.set(String(c.id), step);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
} else if (e.type === "user") {
|
|
1570
|
+
for (const c of e.message?.content ?? []) {
|
|
1571
|
+
if (c?.type !== "tool_result") continue;
|
|
1572
|
+
const text = typeof c.content === "string" ? c.content : JSON.stringify(c.content ?? "");
|
|
1573
|
+
const step = stepOf.get(String(c.tool_use_id));
|
|
1574
|
+
if (step) step.ok = !c.is_error;
|
|
1575
|
+
if (c.is_error) {
|
|
1576
|
+
if (DENIED4.test(text)) usage2.denied++;
|
|
1577
|
+
} else if (pendingEdits.has(String(c.tool_use_id))) {
|
|
1578
|
+
edits.push(pendingEdits.get(String(c.tool_use_id)));
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
} else if (e.type === "result") {
|
|
1582
|
+
const u = e.usage ?? {};
|
|
1583
|
+
usage2.input += Math.max(0, num5(u.input_tokens) - num5(u.cache_read_input_tokens));
|
|
1584
|
+
usage2.cacheRead += num5(u.cache_read_input_tokens);
|
|
1585
|
+
usage2.output += num5(u.output_tokens);
|
|
1586
|
+
usage2.total += num5(u.total_tokens) || num5(u.input_tokens) + num5(u.output_tokens);
|
|
1587
|
+
usage2.denied = Math.max(usage2.denied, Array.isArray(e.permission_denials) ? e.permission_denials.length : 0);
|
|
1588
|
+
if (e.is_error) {
|
|
1589
|
+
error ??= apiError(String(e.error?.message ?? e.result ?? e.subtype ?? "Qwen Code error"));
|
|
1590
|
+
} else if (typeof e.result === "string") {
|
|
1591
|
+
finalText = e.result.trim();
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
return { sessionId, model, finalText: finalText ?? (error ? "" : lastText), usage: usage2, tools, edits, lastActivity, timeline, error };
|
|
1596
|
+
}
|
|
1597
|
+
var apiError = (text) => text.replace(API_ERROR, "").replace(/\](?=\s*(\n|$))/, "").replace(/\s+/g, " ").trim() || text;
|
|
1598
|
+
var clip5 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
1599
|
+
var num5 = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
1600
|
+
var oneLine5 = (s, max = 80) => {
|
|
1601
|
+
const t = s.replace(/\s+/g, " ").trim();
|
|
1602
|
+
return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
|
|
1603
|
+
};
|
|
1604
|
+
|
|
1605
|
+
// src/backends/qwen/index.ts
|
|
1606
|
+
var binary6 = () => findBinary("qwen", "PITROOM_QWEN_BIN", ["~/.local/bin/qwen", "~/.npm-global/bin/qwen"]);
|
|
1607
|
+
var policyDir2 = () => path8.resolve(path8.dirname(fileURLToPath2(import.meta.url)), "..", "policies", "qwen");
|
|
1608
|
+
var EXCLUDED = [
|
|
1609
|
+
"agent",
|
|
1610
|
+
"send_message",
|
|
1611
|
+
"skill",
|
|
1612
|
+
"manage_memory",
|
|
1613
|
+
"search_memory",
|
|
1614
|
+
"enter_worktree",
|
|
1615
|
+
"exit_worktree",
|
|
1616
|
+
"record_artifact",
|
|
1617
|
+
"cron_create",
|
|
1618
|
+
"cron_list",
|
|
1619
|
+
"cron_delete",
|
|
1620
|
+
"loop_wakeup",
|
|
1621
|
+
"task_stop",
|
|
1622
|
+
"update_goal",
|
|
1623
|
+
"get_goal",
|
|
1624
|
+
"list_agents",
|
|
1625
|
+
"report_findings",
|
|
1626
|
+
"tool_call",
|
|
1627
|
+
"tool_search",
|
|
1628
|
+
"read_mcp_resource"
|
|
1629
|
+
];
|
|
1630
|
+
var WEB = ["web_fetch", "web_search"];
|
|
1631
|
+
var EDITS_HERE = ["Edit(./**)", "write_file(./**)", "notebook_edit(./**)"];
|
|
1632
|
+
var DENY_SHELL = [
|
|
1633
|
+
"git commit",
|
|
1634
|
+
"git push",
|
|
1635
|
+
"git pull",
|
|
1636
|
+
"git reset",
|
|
1637
|
+
"git checkout",
|
|
1638
|
+
"git restore",
|
|
1639
|
+
"git clean",
|
|
1640
|
+
"git stash",
|
|
1641
|
+
"git rebase",
|
|
1642
|
+
"git merge",
|
|
1643
|
+
"git switch",
|
|
1644
|
+
"git branch",
|
|
1645
|
+
"git tag",
|
|
1646
|
+
"git rm",
|
|
1647
|
+
"git worktree",
|
|
1648
|
+
"git update-ref",
|
|
1649
|
+
"git config",
|
|
1650
|
+
"git remote",
|
|
1651
|
+
"git cherry-pick",
|
|
1652
|
+
"git revert",
|
|
1653
|
+
"git am",
|
|
1654
|
+
"git add",
|
|
1655
|
+
"rm -rf",
|
|
1656
|
+
"rm -fr",
|
|
1657
|
+
"sudo",
|
|
1658
|
+
"chown",
|
|
1659
|
+
"kill",
|
|
1660
|
+
"pkill",
|
|
1661
|
+
"killall",
|
|
1662
|
+
"gh",
|
|
1663
|
+
"npm publish",
|
|
1664
|
+
"pitroom",
|
|
1665
|
+
"opencode",
|
|
1666
|
+
"claude",
|
|
1667
|
+
"codex",
|
|
1668
|
+
"gemini",
|
|
1669
|
+
"qwen",
|
|
1670
|
+
"kilo",
|
|
1671
|
+
"kilocode"
|
|
1672
|
+
].map((c) => `run_shell_command(${c})`);
|
|
1673
|
+
function invocation6(req) {
|
|
1674
|
+
const read3 = req.mode === "read";
|
|
1675
|
+
const exclude = [...EXCLUDED, ...req.web ? [] : WEB, ...read3 ? [] : DENY_SHELL];
|
|
1676
|
+
const args = [
|
|
1677
|
+
// The prompt is the first word: the list options below take every word that follows them, and Qwen Code reads
|
|
1678
|
+
// no prompt after "--". One that starts with "-" would be read as an option: a leading space keeps it the prompt.
|
|
1679
|
+
req.prompt.startsWith("-") ? ` ${req.prompt}` : req.prompt,
|
|
1680
|
+
"--safe-mode",
|
|
1681
|
+
"--output-format",
|
|
1682
|
+
"stream-json",
|
|
1683
|
+
"--approval-mode",
|
|
1684
|
+
read3 ? "plan" : "default",
|
|
1685
|
+
"--exclude-tools",
|
|
1686
|
+
...exclude
|
|
1687
|
+
];
|
|
1688
|
+
if (!read3) args.push("--allowed-tools", ...EDITS_HERE, "run_shell_command");
|
|
1689
|
+
const model = (req.model ?? "").split("#")[0];
|
|
1690
|
+
if (model) args.push("--model", model);
|
|
1691
|
+
if (req.sessionId) args.push("--resume", req.sessionId);
|
|
1692
|
+
const { command, prefix } = resolveCommand(binary6());
|
|
1693
|
+
return {
|
|
1694
|
+
command,
|
|
1695
|
+
args: [...prefix, ...args],
|
|
1696
|
+
env: {
|
|
1697
|
+
QWEN_CODE_SYSTEM_DEFAULTS_PATH: path8.join(policyDir2(), "worker-defaults.json"),
|
|
1698
|
+
QWEN_CODE_DISABLE_CRON: "1",
|
|
1699
|
+
QWEN_DISABLE_AUTO_TITLE: "1"
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
function classify5(message) {
|
|
1704
|
+
if (/\b40[13]\b|incorrect api key|invalid api key|unauthori[sz]ed|authenticat|not logged in|no auth/i.test(message)) return "auth";
|
|
1705
|
+
if (/\b429\b|rate.?limit|too many requests|quota|capacity|overloaded|\b529\b/i.test(message)) return "rate-limited";
|
|
1706
|
+
if (/model[^.]*(not found|does not exist|not available|not supported)|invalid model|model_not_found|\b404\b/i.test(message)) return "model-unavailable";
|
|
1707
|
+
return "other";
|
|
1708
|
+
}
|
|
1709
|
+
function failure6(run2, stderr, exitCode) {
|
|
1710
|
+
if (exitCode === 0 && !run2.error) return void 0;
|
|
1711
|
+
const detail = stderr.trim().split("\n").filter((l) => l.trim() && !/SAFE MODE/.test(l)).pop();
|
|
1712
|
+
const message = run2.error ?? detail ?? `qwen exited with code ${exitCode}`;
|
|
1713
|
+
return { kind: classify5(message), message };
|
|
1714
|
+
}
|
|
1715
|
+
function userSettings() {
|
|
1716
|
+
try {
|
|
1717
|
+
return JSON.parse(fs8.readFileSync(path8.join(process.env.QWEN_HOME ?? path8.join(os7.homedir(), ".qwen"), "settings.json"), "utf8"));
|
|
1718
|
+
} catch {
|
|
1719
|
+
return {};
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
function defaultModel5() {
|
|
1723
|
+
const name = userSettings().model?.name;
|
|
1724
|
+
return typeof name === "string" && name ? name : void 0;
|
|
1725
|
+
}
|
|
1726
|
+
function doctor6({ models, hasFallback }) {
|
|
1727
|
+
const { command, prefix } = resolveCommand(binary6());
|
|
1728
|
+
const r = spawnSync6(command, [...prefix, "--version"], { encoding: "utf8", timeout: 6e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
1729
|
+
if (r.error || r.status !== 0) return [{ level: "fail", message: `qwen not runnable (${binary6()}). Install Qwen Code (npm i -g @qwen-code/qwen-code), or set PITROOM_QWEN_BIN` }];
|
|
1730
|
+
const checks = [{ level: "ok", message: `Qwen Code ${r.stdout.trim()} at ${binary6()} (beta worker: checked against Qwen Code 0.25)` }];
|
|
1731
|
+
const auth = userSettings().security?.auth?.selectedType;
|
|
1732
|
+
const key = process.env.OPENAI_API_KEY || process.env.DASHSCOPE_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.GEMINI_API_KEY;
|
|
1733
|
+
checks.push(
|
|
1734
|
+
auth || key ? { level: "ok", message: `Qwen Code: sign-in ${auth ?? "from an API key in the environment"}` } : { level: "warn", message: "Qwen Code has no sign-in method set: run `qwen` once to choose one, or set OPENAI_API_KEY (and OPENAI_BASE_URL) for an OpenAI-compatible provider" }
|
|
1735
|
+
);
|
|
1736
|
+
for (const m of models) {
|
|
1737
|
+
const model = m ?? defaultModel5();
|
|
1738
|
+
checks.push({ level: model ? "ok" : "warn", message: model ? `Qwen Code model: ${model}` : "Qwen Code model: none pinned (pass -W qwen:<model> or set model.name in ~/.qwen/settings.json)" });
|
|
1739
|
+
}
|
|
1740
|
+
if (!hasFallback) checks.push({ level: "warn", message: "no fallback workers configured for Qwen Code runs" });
|
|
1741
|
+
return checks;
|
|
1742
|
+
}
|
|
1743
|
+
var qwen = {
|
|
1744
|
+
id: "qwen",
|
|
1745
|
+
name: "Qwen Code",
|
|
1746
|
+
capabilities: { readOnly: "approval-mode", resume: "by-id", reportsCost: false, attachFiles: false },
|
|
1747
|
+
binary: binary6,
|
|
1748
|
+
invocation: invocation6,
|
|
1749
|
+
parse: parseEvents5,
|
|
1750
|
+
failure: failure6,
|
|
1751
|
+
defaultModel: defaultModel5,
|
|
1752
|
+
doctor: doctor6
|
|
1753
|
+
};
|
|
1754
|
+
|
|
1407
1755
|
// src/backends/index.ts
|
|
1408
|
-
var REGISTRY = new Map([opencode, codex, claude, gemini].map((b) => [b.id, b]));
|
|
1756
|
+
var REGISTRY = new Map([opencode, codex, claude, gemini, qwen, kiloBackend].map((b) => [b.id, b]));
|
|
1409
1757
|
var DEFAULT_BACKEND = opencode.id;
|
|
1410
|
-
var PLANNED = [];
|
|
1758
|
+
var PLANNED = ["hermes"];
|
|
1411
1759
|
var backendIds = () => [...REGISTRY.keys()];
|
|
1412
1760
|
var allBackends = () => [...REGISTRY.values()];
|
|
1413
1761
|
var isBackendId = (id) => REGISTRY.has(id) || PLANNED.includes(id);
|
|
@@ -1422,12 +1770,12 @@ function getBackend(id) {
|
|
|
1422
1770
|
}
|
|
1423
1771
|
|
|
1424
1772
|
// src/cli/args.ts
|
|
1425
|
-
import
|
|
1773
|
+
import fs10 from "node:fs";
|
|
1426
1774
|
|
|
1427
1775
|
// src/core/config.ts
|
|
1428
|
-
import
|
|
1429
|
-
import
|
|
1430
|
-
import
|
|
1776
|
+
import fs9 from "node:fs";
|
|
1777
|
+
import os8 from "node:os";
|
|
1778
|
+
import path9 from "node:path";
|
|
1431
1779
|
var SCHEMA = {
|
|
1432
1780
|
worker: "string",
|
|
1433
1781
|
fallback: "string[]",
|
|
@@ -1441,6 +1789,8 @@ var SCHEMA = {
|
|
|
1441
1789
|
tiers: "record",
|
|
1442
1790
|
costs: "numbers",
|
|
1443
1791
|
audit: "number",
|
|
1792
|
+
auditFix: "boolean",
|
|
1793
|
+
auditFocus: "boolean",
|
|
1444
1794
|
readIn: "string",
|
|
1445
1795
|
cacheDays: "number",
|
|
1446
1796
|
countRateLimits: "boolean",
|
|
@@ -1451,12 +1801,14 @@ var SCHEMA = {
|
|
|
1451
1801
|
workerPrices: "record",
|
|
1452
1802
|
priceFeed: "boolean",
|
|
1453
1803
|
priceFeedUrl: "string",
|
|
1454
|
-
priceFeedHours: "number"
|
|
1804
|
+
priceFeedHours: "number",
|
|
1805
|
+
budgetDaily: "number",
|
|
1806
|
+
rankWorkers: "boolean"
|
|
1455
1807
|
};
|
|
1456
1808
|
function configPath() {
|
|
1457
|
-
if (process.env.PITROOM_CONFIG) return
|
|
1458
|
-
const base2 = process.platform === "win32" ? process.env.APPDATA ??
|
|
1459
|
-
return
|
|
1809
|
+
if (process.env.PITROOM_CONFIG) return path9.resolve(process.env.PITROOM_CONFIG);
|
|
1810
|
+
const base2 = process.platform === "win32" ? process.env.APPDATA ?? path9.join(os8.homedir(), "AppData", "Roaming") : process.env.XDG_CONFIG_HOME ?? path9.join(os8.homedir(), ".config");
|
|
1811
|
+
return path9.join(base2, "pitroom", "config.json");
|
|
1460
1812
|
}
|
|
1461
1813
|
var cached;
|
|
1462
1814
|
function loadConfig() {
|
|
@@ -1464,10 +1816,10 @@ function loadConfig() {
|
|
|
1464
1816
|
const file2 = configPath();
|
|
1465
1817
|
const config = {};
|
|
1466
1818
|
const warnings = [];
|
|
1467
|
-
if (
|
|
1819
|
+
if (fs9.existsSync(file2)) {
|
|
1468
1820
|
let raw;
|
|
1469
1821
|
try {
|
|
1470
|
-
raw = JSON.parse(
|
|
1822
|
+
raw = JSON.parse(fs9.readFileSync(file2, "utf8"));
|
|
1471
1823
|
} catch (e) {
|
|
1472
1824
|
warnings.push(`${file2}: invalid JSON (${e.message}); ignored`);
|
|
1473
1825
|
}
|
|
@@ -1509,6 +1861,10 @@ var rate = (v) => {
|
|
|
1509
1861
|
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
1510
1862
|
return typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.min(n, 1) : void 0;
|
|
1511
1863
|
};
|
|
1864
|
+
var amount = (v) => {
|
|
1865
|
+
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
1866
|
+
return typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : void 0;
|
|
1867
|
+
};
|
|
1512
1868
|
var days = (v) => {
|
|
1513
1869
|
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
1514
1870
|
return typeof n === "number" && Number.isInteger(n) && n >= 0 ? n : void 0;
|
|
@@ -1543,6 +1899,8 @@ function effective(flags = {}) {
|
|
|
1543
1899
|
tiers: setting(void 0, void 0, c.tiers, {}),
|
|
1544
1900
|
costs: setting(void 0, void 0, c.costs, {}),
|
|
1545
1901
|
audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0),
|
|
1902
|
+
auditFix: setting(void 0, flag01(e.PITROOM_AUDIT_FIX), c.auditFix, false),
|
|
1903
|
+
auditFocus: setting(void 0, flag01(e.PITROOM_AUDIT_FOCUS), c.auditFocus, false),
|
|
1546
1904
|
readIn: setting(void 0, readIn(e.PITROOM_READ_IN), readIn(c.readIn), "auto"),
|
|
1547
1905
|
cacheDays: setting(void 0, days(e.PITROOM_CACHE_DAYS), days(c.cacheDays), 7),
|
|
1548
1906
|
countRateLimits: setting(void 0, void 0, c.countRateLimits, false),
|
|
@@ -1553,7 +1911,9 @@ function effective(flags = {}) {
|
|
|
1553
1911
|
workerPrices: setting(void 0, void 0, c.workerPrices, {}),
|
|
1554
1912
|
priceFeed: setting(void 0, flag01(e.PITROOM_PRICE_FEED), c.priceFeed, false),
|
|
1555
1913
|
priceFeedUrl: setting(void 0, e.PITROOM_PRICE_FEED_URL?.trim() || void 0, c.priceFeedUrl?.trim() || void 0, "https://models.dev/api.json"),
|
|
1556
|
-
priceFeedHours: setting(void 0, positiveInt(e.PITROOM_PRICE_FEED_HOURS), positiveInt(c.priceFeedHours), 24)
|
|
1914
|
+
priceFeedHours: setting(void 0, positiveInt(e.PITROOM_PRICE_FEED_HOURS), positiveInt(c.priceFeedHours), 24),
|
|
1915
|
+
rankWorkers: setting(void 0, flag01(e.PITROOM_RANK_WORKERS), c.rankWorkers, false),
|
|
1916
|
+
budgetDaily: setting(void 0, amount(e.PITROOM_BUDGET_DAILY), amount(c.budgetDaily), void 0)
|
|
1557
1917
|
};
|
|
1558
1918
|
}
|
|
1559
1919
|
|
|
@@ -1689,9 +2049,9 @@ function exitCodeFor(meta) {
|
|
|
1689
2049
|
}
|
|
1690
2050
|
function readTask(p) {
|
|
1691
2051
|
const file2 = flag(p, "task-file");
|
|
1692
|
-
if (file2) return
|
|
2052
|
+
if (file2) return fs10.readFileSync(file2, "utf8");
|
|
1693
2053
|
const words = p.positional;
|
|
1694
|
-
if (words.length === 1 && words[0] === "-") return
|
|
2054
|
+
if (words.length === 1 && words[0] === "-") return fs10.readFileSync(0, "utf8");
|
|
1695
2055
|
return words.join(" ");
|
|
1696
2056
|
}
|
|
1697
2057
|
function runOptions(p, task) {
|
|
@@ -1739,16 +2099,16 @@ function effortFlag(p) {
|
|
|
1739
2099
|
import readline from "node:readline";
|
|
1740
2100
|
|
|
1741
2101
|
// src/core/run.ts
|
|
1742
|
-
import { spawn as spawn4, spawnSync as
|
|
1743
|
-
import
|
|
1744
|
-
import
|
|
2102
|
+
import { spawn as spawn4, spawnSync as spawnSync8 } from "node:child_process";
|
|
2103
|
+
import fs29 from "node:fs";
|
|
2104
|
+
import path25 from "node:path";
|
|
1745
2105
|
|
|
1746
2106
|
// src/vcs/git.ts
|
|
1747
|
-
import { spawnSync as
|
|
2107
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
1748
2108
|
import crypto2 from "node:crypto";
|
|
1749
|
-
import
|
|
1750
|
-
import
|
|
1751
|
-
import
|
|
2109
|
+
import fs11 from "node:fs";
|
|
2110
|
+
import os9 from "node:os";
|
|
2111
|
+
import path10 from "node:path";
|
|
1752
2112
|
var SAFE = [
|
|
1753
2113
|
"-c",
|
|
1754
2114
|
"color.ui=false",
|
|
@@ -1760,7 +2120,7 @@ var SAFE = [
|
|
|
1760
2120
|
"diff.mnemonicPrefix=false"
|
|
1761
2121
|
];
|
|
1762
2122
|
function git(cwd, args, env) {
|
|
1763
|
-
const r =
|
|
2123
|
+
const r = spawnSync7("git", [...SAFE, ...args], {
|
|
1764
2124
|
cwd,
|
|
1765
2125
|
env: env ?? process.env,
|
|
1766
2126
|
encoding: "utf8",
|
|
@@ -1780,31 +2140,31 @@ function gitAvailable() {
|
|
|
1780
2140
|
}
|
|
1781
2141
|
function canonical(dir) {
|
|
1782
2142
|
try {
|
|
1783
|
-
return
|
|
2143
|
+
return fs11.realpathSync.native(dir);
|
|
1784
2144
|
} catch {
|
|
1785
2145
|
return dir;
|
|
1786
2146
|
}
|
|
1787
2147
|
}
|
|
1788
2148
|
function repoRoot(dir) {
|
|
1789
2149
|
const r = git(dir, ["rev-parse", "--show-toplevel"]);
|
|
1790
|
-
return r.code === 0 ? canonical(
|
|
2150
|
+
return r.code === 0 ? canonical(path10.resolve(r.stdout.trim())) : void 0;
|
|
1791
2151
|
}
|
|
1792
2152
|
function snapshotTree(root, exclude = [], drop2 = []) {
|
|
1793
|
-
const tmp =
|
|
1794
|
-
const real =
|
|
2153
|
+
const tmp = path10.join(os9.tmpdir(), `pitroom-index-${process.pid}-${crypto2.randomBytes(4).toString("hex")}`);
|
|
2154
|
+
const real = path10.resolve(root, must(root, ["rev-parse", "--git-path", "index"]).trim());
|
|
1795
2155
|
const env = { ...process.env, GIT_INDEX_FILE: tmp };
|
|
1796
2156
|
try {
|
|
1797
|
-
if (
|
|
2157
|
+
if (fs11.existsSync(real)) fs11.copyFileSync(real, tmp);
|
|
1798
2158
|
else must(root, ["read-tree", "--empty"], env);
|
|
1799
2159
|
must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`), ...drop2.map((p) => `:(top,literal,exclude)${p}`)], env);
|
|
1800
2160
|
if (drop2.length) {
|
|
1801
|
-
const r =
|
|
2161
|
+
const r = spawnSync7("git", ["update-index", "--force-remove", "-z", "--stdin"], { cwd: root, env, input: `${drop2.join("\0")}\0`, encoding: "utf8" });
|
|
1802
2162
|
if (r.status !== 0) throw new Error(`git update-index failed: ${r.stderr.trim()}`);
|
|
1803
2163
|
}
|
|
1804
2164
|
return must(root, ["write-tree"], env).trim();
|
|
1805
2165
|
} finally {
|
|
1806
|
-
|
|
1807
|
-
|
|
2166
|
+
fs11.rmSync(tmp, { force: true });
|
|
2167
|
+
fs11.rmSync(`${tmp}.lock`, { force: true });
|
|
1808
2168
|
}
|
|
1809
2169
|
}
|
|
1810
2170
|
var DIFF = ["diff", "--no-ext-diff", "--no-textconv", "--no-renames"];
|
|
@@ -1824,27 +2184,27 @@ function diffTrees(root, a, b) {
|
|
|
1824
2184
|
return { changes, stats: { files: changes.length, insertions, deletions }, patch };
|
|
1825
2185
|
}
|
|
1826
2186
|
function createIsolatedCopy(root, tree, dest) {
|
|
1827
|
-
const objects =
|
|
1828
|
-
|
|
1829
|
-
must(
|
|
1830
|
-
|
|
2187
|
+
const objects = path10.join(path10.resolve(root, must(root, ["rev-parse", "--git-common-dir"]).trim()), "objects");
|
|
2188
|
+
fs11.mkdirSync(path10.dirname(dest), { recursive: true });
|
|
2189
|
+
must(path10.dirname(dest), ["init", "--quiet", dest]);
|
|
2190
|
+
fs11.writeFileSync(path10.join(dest, ".git", "objects", "info", "alternates"), `${objects}
|
|
1831
2191
|
`);
|
|
1832
2192
|
must(dest, ["read-tree", tree]);
|
|
1833
2193
|
must(dest, ["checkout-index", "--all", "--force"]);
|
|
1834
2194
|
}
|
|
1835
2195
|
function removeIsolatedCopy(dest, ownedBy) {
|
|
1836
|
-
const rel =
|
|
1837
|
-
if (!rel || rel.startsWith("..") ||
|
|
1838
|
-
|
|
2196
|
+
const rel = path10.relative(ownedBy, dest);
|
|
2197
|
+
if (!rel || rel.startsWith("..") || path10.isAbsolute(rel)) throw new Error(`refusing to remove ${dest}`);
|
|
2198
|
+
fs11.rmSync(dest, { recursive: true, force: true });
|
|
1839
2199
|
}
|
|
1840
2200
|
function linkIntoWorktree(root, worktree, rels) {
|
|
1841
2201
|
const linked = [];
|
|
1842
2202
|
for (const rel of rels) {
|
|
1843
|
-
const src =
|
|
1844
|
-
const dst =
|
|
1845
|
-
if (!
|
|
1846
|
-
|
|
1847
|
-
|
|
2203
|
+
const src = path10.join(root, rel);
|
|
2204
|
+
const dst = path10.join(worktree, rel);
|
|
2205
|
+
if (!fs11.existsSync(src) || fs11.existsSync(dst)) continue;
|
|
2206
|
+
fs11.mkdirSync(path10.dirname(dst), { recursive: true });
|
|
2207
|
+
fs11.symlinkSync(src, dst, process.platform === "win32" ? "junction" : void 0);
|
|
1848
2208
|
linked.push(rel);
|
|
1849
2209
|
}
|
|
1850
2210
|
return linked;
|
|
@@ -1950,11 +2310,11 @@ function resolveChain(flags = {}) {
|
|
|
1950
2310
|
|
|
1951
2311
|
// src/core/process.ts
|
|
1952
2312
|
import { spawn } from "node:child_process";
|
|
1953
|
-
import
|
|
2313
|
+
import fs13 from "node:fs";
|
|
1954
2314
|
|
|
1955
2315
|
// src/vcs/guard.ts
|
|
1956
|
-
import
|
|
1957
|
-
import
|
|
2316
|
+
import fs12 from "node:fs";
|
|
2317
|
+
import path11 from "node:path";
|
|
1958
2318
|
var VERSION = 2;
|
|
1959
2319
|
var WINDOWS = process.platform === "win32";
|
|
1960
2320
|
var fwd = (p) => WINDOWS ? p.replace(/\\/g, "/") : p;
|
|
@@ -2031,29 +2391,29 @@ esac
|
|
|
2031
2391
|
exec "$real" "$@"
|
|
2032
2392
|
`;
|
|
2033
2393
|
function shimDir() {
|
|
2034
|
-
return
|
|
2394
|
+
return path11.join(home(), "shim", `v${VERSION}`);
|
|
2035
2395
|
}
|
|
2036
2396
|
function findRealGit(skip) {
|
|
2037
2397
|
const same2 = (a, b) => WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
2038
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
2039
|
-
if (same2(
|
|
2040
|
-
const p =
|
|
2398
|
+
for (const dir of (process.env.PATH ?? "").split(path11.delimiter).filter(Boolean)) {
|
|
2399
|
+
if (same2(path11.resolve(dir), skip)) continue;
|
|
2400
|
+
const p = path11.join(dir, WINDOWS ? "git.exe" : "git");
|
|
2041
2401
|
try {
|
|
2042
|
-
|
|
2043
|
-
if (
|
|
2402
|
+
fs12.accessSync(p, fs12.constants.X_OK);
|
|
2403
|
+
if (fs12.statSync(p).isFile()) return p;
|
|
2044
2404
|
} catch {
|
|
2045
2405
|
}
|
|
2046
2406
|
}
|
|
2047
2407
|
return void 0;
|
|
2048
2408
|
}
|
|
2049
2409
|
function findGitSh(realGit) {
|
|
2050
|
-
let dir =
|
|
2410
|
+
let dir = path11.dirname(realGit);
|
|
2051
2411
|
for (let i = 0; i < 4; i++) {
|
|
2052
2412
|
for (const rel of ["bin/sh.exe", "usr/bin/sh.exe"]) {
|
|
2053
|
-
const p =
|
|
2054
|
-
if (
|
|
2413
|
+
const p = path11.join(dir, rel);
|
|
2414
|
+
if (fs12.existsSync(p)) return p;
|
|
2055
2415
|
}
|
|
2056
|
-
const up =
|
|
2416
|
+
const up = path11.dirname(dir);
|
|
2057
2417
|
if (up === dir) break;
|
|
2058
2418
|
dir = up;
|
|
2059
2419
|
}
|
|
@@ -2071,12 +2431,12 @@ var REF_HOOK = `#!/bin/sh
|
|
|
2071
2431
|
echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
|
|
2072
2432
|
exit 1
|
|
2073
2433
|
`;
|
|
2074
|
-
var hooksDir = () =>
|
|
2434
|
+
var hooksDir = () => path11.join(home(), "git-hooks", `v${HOOK_VERSION}`);
|
|
2075
2435
|
function writeIfChanged(file2, content) {
|
|
2076
|
-
if (
|
|
2077
|
-
|
|
2436
|
+
if (fs12.existsSync(file2) && fs12.readFileSync(file2, "utf8") === content) return;
|
|
2437
|
+
fs12.mkdirSync(path11.dirname(file2), { recursive: true });
|
|
2078
2438
|
const tmp = `${file2}.${process.pid}.tmp`;
|
|
2079
|
-
|
|
2439
|
+
fs12.writeFileSync(tmp, content, { mode: 493 });
|
|
2080
2440
|
renameOver(tmp, file2);
|
|
2081
2441
|
}
|
|
2082
2442
|
function pathKey(env) {
|
|
@@ -2108,7 +2468,7 @@ function guardEnv(env) {
|
|
|
2108
2468
|
GIT_PAGER: "cat",
|
|
2109
2469
|
PAGER: "cat"
|
|
2110
2470
|
};
|
|
2111
|
-
writeIfChanged(
|
|
2471
|
+
writeIfChanged(path11.join(hooksDir(), "reference-transaction"), REF_HOOK);
|
|
2112
2472
|
env = withGitConfig(onePath(env), [
|
|
2113
2473
|
["core.hooksPath", fwd(hooksDir())],
|
|
2114
2474
|
["url.pitroom-push-blocked://.pushInsteadOf", ""]
|
|
@@ -2120,11 +2480,11 @@ function guardEnv(env) {
|
|
|
2120
2480
|
if (WINDOWS) {
|
|
2121
2481
|
sh = findGitSh(real);
|
|
2122
2482
|
if (!sh) return { ...env, ...quiet };
|
|
2123
|
-
writeIfChanged(
|
|
2483
|
+
writeIfChanged(path11.join(dir, "git.cmd"), cmdShim(sh));
|
|
2124
2484
|
}
|
|
2125
|
-
writeIfChanged(
|
|
2485
|
+
writeIfChanged(path11.join(dir, "git"), SHIM);
|
|
2126
2486
|
const key = pathKey(env);
|
|
2127
|
-
return { ...env, ...quiet, PITROOM_REAL_GIT: fwd(real), [key]: `${dir}${
|
|
2487
|
+
return { ...env, ...quiet, PITROOM_REAL_GIT: fwd(real), [key]: `${dir}${path11.delimiter}${env[key] ?? ""}` };
|
|
2128
2488
|
}
|
|
2129
2489
|
function shimReady() {
|
|
2130
2490
|
const env = guardEnv({ ...process.env });
|
|
@@ -2133,8 +2493,8 @@ function shimReady() {
|
|
|
2133
2493
|
|
|
2134
2494
|
// src/core/process.ts
|
|
2135
2495
|
async function spawnWorker(inv, opts) {
|
|
2136
|
-
const out =
|
|
2137
|
-
const err =
|
|
2496
|
+
const out = fs13.openSync(opts.stdoutFile, "w");
|
|
2497
|
+
const err = fs13.openSync(opts.stderrFile, "w");
|
|
2138
2498
|
const res = { code: null, timedOut: false, stopped: false };
|
|
2139
2499
|
const child = spawn(inv.command, inv.args, {
|
|
2140
2500
|
cwd: opts.cwd,
|
|
@@ -2157,7 +2517,7 @@ async function spawnWorker(inv, opts) {
|
|
|
2157
2517
|
}, opts.timeoutSec * 1e3);
|
|
2158
2518
|
res.code = await new Promise((resolve2) => {
|
|
2159
2519
|
child.on("error", (e) => {
|
|
2160
|
-
res.spawnError = e.code === "ENOENT" ?
|
|
2520
|
+
res.spawnError = e.code === "ENOENT" ? fs13.existsSync(opts.cwd) ? `${inv.command} not found` : `the working directory ${opts.cwd} does not exist` : e.message;
|
|
2161
2521
|
resolve2(127);
|
|
2162
2522
|
});
|
|
2163
2523
|
child.on("close", (c) => resolve2(c));
|
|
@@ -2166,8 +2526,8 @@ async function spawnWorker(inv, opts) {
|
|
|
2166
2526
|
if (killer) clearTimeout(killer);
|
|
2167
2527
|
process.off("SIGINT", stop);
|
|
2168
2528
|
process.off("SIGTERM", stop);
|
|
2169
|
-
|
|
2170
|
-
|
|
2529
|
+
fs13.closeSync(out);
|
|
2530
|
+
fs13.closeSync(err);
|
|
2171
2531
|
return res;
|
|
2172
2532
|
}
|
|
2173
2533
|
|
|
@@ -2199,12 +2559,12 @@ function parseAudit(text) {
|
|
|
2199
2559
|
}
|
|
2200
2560
|
|
|
2201
2561
|
// src/core/cooldown.ts
|
|
2202
|
-
import
|
|
2203
|
-
import
|
|
2562
|
+
import fs14 from "node:fs";
|
|
2563
|
+
import path12 from "node:path";
|
|
2204
2564
|
var MAX_MS = 24 * 36e5;
|
|
2205
2565
|
var OVERLOAD_MS = 20 * 6e4;
|
|
2206
2566
|
var DAILY_MS = 4 * 36e5;
|
|
2207
|
-
var file = () =>
|
|
2567
|
+
var file = () => path12.join(home(), "cooldowns.json");
|
|
2208
2568
|
function cooldownKey(target, backend) {
|
|
2209
2569
|
const model = (target.model ?? backend.defaultModel?.() ?? "").split("#")[0];
|
|
2210
2570
|
return `${target.backend}:${model}`;
|
|
@@ -2227,16 +2587,16 @@ function cooldownMs(message) {
|
|
|
2227
2587
|
}
|
|
2228
2588
|
function read() {
|
|
2229
2589
|
try {
|
|
2230
|
-
const raw = JSON.parse(
|
|
2590
|
+
const raw = JSON.parse(fs14.readFileSync(file(), "utf8"));
|
|
2231
2591
|
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
2232
2592
|
} catch {
|
|
2233
2593
|
return {};
|
|
2234
2594
|
}
|
|
2235
2595
|
}
|
|
2236
2596
|
function write(all) {
|
|
2237
|
-
|
|
2597
|
+
fs14.mkdirSync(path12.dirname(file()), { recursive: true });
|
|
2238
2598
|
const tmp = `${file()}.${process.pid}.tmp`;
|
|
2239
|
-
|
|
2599
|
+
fs14.writeFileSync(tmp, `${JSON.stringify(all, null, 2)}
|
|
2240
2600
|
`);
|
|
2241
2601
|
renameOver(tmp, file());
|
|
2242
2602
|
}
|
|
@@ -2266,14 +2626,14 @@ function untilText(c, now = Date.now()) {
|
|
|
2266
2626
|
}
|
|
2267
2627
|
|
|
2268
2628
|
// src/core/snapshot.ts
|
|
2269
|
-
import
|
|
2270
|
-
import
|
|
2629
|
+
import fs16 from "node:fs";
|
|
2630
|
+
import path14 from "node:path";
|
|
2271
2631
|
import crypto3 from "node:crypto";
|
|
2272
2632
|
|
|
2273
2633
|
// src/core/secrets.ts
|
|
2274
2634
|
import { execFileSync } from "node:child_process";
|
|
2275
|
-
import
|
|
2276
|
-
import
|
|
2635
|
+
import fs15 from "node:fs";
|
|
2636
|
+
import path13 from "node:path";
|
|
2277
2637
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
2278
2638
|
var MAX_VISITED = 2e4;
|
|
2279
2639
|
var MAX_DEPTH = 4;
|
|
@@ -2291,16 +2651,16 @@ function findSecretFiles(dir) {
|
|
|
2291
2651
|
if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
|
|
2292
2652
|
let entries;
|
|
2293
2653
|
try {
|
|
2294
|
-
entries =
|
|
2654
|
+
entries = fs15.readdirSync(d, { withFileTypes: true });
|
|
2295
2655
|
} catch {
|
|
2296
2656
|
return;
|
|
2297
2657
|
}
|
|
2298
2658
|
for (const e of entries) {
|
|
2299
2659
|
visited++;
|
|
2300
2660
|
if (e.isDirectory()) {
|
|
2301
|
-
if (!SKIP_DIRS.has(e.name)) walk(
|
|
2661
|
+
if (!SKIP_DIRS.has(e.name)) walk(path13.join(d, e.name), depth + 1);
|
|
2302
2662
|
} else if (e.isFile() && looksSecret(e.name)) {
|
|
2303
|
-
found.push(slash(
|
|
2663
|
+
found.push(slash(path13.relative(dir, path13.join(d, e.name))));
|
|
2304
2664
|
}
|
|
2305
2665
|
}
|
|
2306
2666
|
};
|
|
@@ -2310,7 +2670,7 @@ function findSecretFiles(dir) {
|
|
|
2310
2670
|
function findSecretFilesInTree(root, dir) {
|
|
2311
2671
|
try {
|
|
2312
2672
|
const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
2313
|
-
return out.split("\0").filter((f) => f && looksSecret(
|
|
2673
|
+
return out.split("\0").filter((f) => f && looksSecret(path13.basename(f)) && !path13.relative(dir, path13.join(root, f)).startsWith("..")).map((f) => slash(path13.relative(dir, path13.join(root, f)))).sort();
|
|
2314
2674
|
} catch {
|
|
2315
2675
|
return [];
|
|
2316
2676
|
}
|
|
@@ -2322,7 +2682,7 @@ function secretWarning(files, mode) {
|
|
|
2322
2682
|
const fix = mode === "isolate" ? "add them to .gitignore, or" : "use an isolated copy (-i, git-ignored files are left out) or a clean checkout, or";
|
|
2323
2683
|
return `secret-looking files ${where} (${shown}): the worker is told not to read them, but nothing stops it, and its model may be hosted by a third party; ${fix} set PITROOM_NO_SECRET_WARNING=1 to silence this`;
|
|
2324
2684
|
}
|
|
2325
|
-
var slash = (p) => p.split(
|
|
2685
|
+
var slash = (p) => p.split(path13.sep).join("/");
|
|
2326
2686
|
|
|
2327
2687
|
// src/core/snapshot.ts
|
|
2328
2688
|
var READ_IN = ["auto", "snapshot", "project"];
|
|
@@ -2330,7 +2690,7 @@ var KEEP_MS = 24 * 36e5;
|
|
|
2330
2690
|
var PRUNE_AGE_MS = 3 * 24 * 36e5;
|
|
2331
2691
|
var KEEP_NEWEST = 6;
|
|
2332
2692
|
var JUST_USED_MS = 10 * 6e4;
|
|
2333
|
-
var snapshotsDir = () =>
|
|
2693
|
+
var snapshotsDir = () => path14.join(home(), "snapshots");
|
|
2334
2694
|
function wantSnapshot(readIn2, root, dir) {
|
|
2335
2695
|
if (!root || readIn2 === "project") return void 0;
|
|
2336
2696
|
const secrets = findSecretFiles(dir);
|
|
@@ -2339,19 +2699,19 @@ function wantSnapshot(readIn2, root, dir) {
|
|
|
2339
2699
|
function readSnapshot(root) {
|
|
2340
2700
|
const drop2 = findSecretFilesInTree(root, root);
|
|
2341
2701
|
const tree = snapshotTree(root, [], drop2);
|
|
2342
|
-
const dest =
|
|
2343
|
-
if (
|
|
2702
|
+
const dest = path14.join(snapshotsDir(), tree);
|
|
2703
|
+
if (fs16.existsSync(path14.join(dest, ".git"))) {
|
|
2344
2704
|
touch(dest);
|
|
2345
2705
|
return { dir: dest, tree, created: false };
|
|
2346
2706
|
}
|
|
2347
|
-
const tmp =
|
|
2707
|
+
const tmp = path14.join(snapshotsDir(), `.tmp-${process.pid}-${crypto3.randomBytes(3).toString("hex")}`);
|
|
2348
2708
|
try {
|
|
2349
2709
|
createIsolatedCopy(root, tree, tmp);
|
|
2350
2710
|
readOnly(tmp);
|
|
2351
2711
|
try {
|
|
2352
|
-
|
|
2712
|
+
fs16.renameSync(tmp, dest);
|
|
2353
2713
|
} catch (e) {
|
|
2354
|
-
if (!
|
|
2714
|
+
if (!fs16.existsSync(path14.join(dest, ".git"))) throw e;
|
|
2355
2715
|
removeIsolatedCopy(tmp, snapshotsDir());
|
|
2356
2716
|
}
|
|
2357
2717
|
} catch (e) {
|
|
@@ -2367,17 +2727,17 @@ function readSnapshot(root) {
|
|
|
2367
2727
|
function touch(dir) {
|
|
2368
2728
|
try {
|
|
2369
2729
|
const now = /* @__PURE__ */ new Date();
|
|
2370
|
-
|
|
2730
|
+
fs16.utimesSync(dir, now, now);
|
|
2371
2731
|
} catch {
|
|
2372
2732
|
}
|
|
2373
2733
|
}
|
|
2374
2734
|
function readOnly(dir) {
|
|
2375
2735
|
const walk = (d) => {
|
|
2376
|
-
for (const e of
|
|
2736
|
+
for (const e of fs16.readdirSync(d, { withFileTypes: true })) {
|
|
2377
2737
|
if (e.name === ".git") continue;
|
|
2378
|
-
const p =
|
|
2738
|
+
const p = path14.join(d, e.name);
|
|
2379
2739
|
if (e.isDirectory()) walk(p);
|
|
2380
|
-
else if (e.isFile())
|
|
2740
|
+
else if (e.isFile()) fs16.chmodSync(p, fs16.statSync(p).mode & ~146);
|
|
2381
2741
|
}
|
|
2382
2742
|
};
|
|
2383
2743
|
try {
|
|
@@ -2388,7 +2748,7 @@ function readOnly(dir) {
|
|
|
2388
2748
|
function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWEST) {
|
|
2389
2749
|
let names;
|
|
2390
2750
|
try {
|
|
2391
|
-
names =
|
|
2751
|
+
names = fs16.readdirSync(snapshotsDir());
|
|
2392
2752
|
} catch {
|
|
2393
2753
|
return 0;
|
|
2394
2754
|
}
|
|
@@ -2396,14 +2756,14 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
|
|
|
2396
2756
|
for (const id of listRunIds()) {
|
|
2397
2757
|
try {
|
|
2398
2758
|
const m = readMeta(id);
|
|
2399
|
-
if (m.snapshot?.dir && isActive(m.state)) inUse.add(
|
|
2759
|
+
if (m.snapshot?.dir && isActive(m.state)) inUse.add(path14.resolve(m.snapshot.dir));
|
|
2400
2760
|
} catch {
|
|
2401
2761
|
}
|
|
2402
2762
|
}
|
|
2403
2763
|
const entries = names.map((n) => {
|
|
2404
|
-
const dir =
|
|
2764
|
+
const dir = path14.join(snapshotsDir(), n);
|
|
2405
2765
|
try {
|
|
2406
|
-
return { n, dir, age: now -
|
|
2766
|
+
return { n, dir, age: now - fs16.statSync(dir).mtimeMs };
|
|
2407
2767
|
} catch {
|
|
2408
2768
|
return void 0;
|
|
2409
2769
|
}
|
|
@@ -2411,7 +2771,7 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
|
|
|
2411
2771
|
let removed = 0;
|
|
2412
2772
|
let rank = 0;
|
|
2413
2773
|
for (const e of entries) {
|
|
2414
|
-
if (inUse.has(
|
|
2774
|
+
if (inUse.has(path14.resolve(e.dir))) continue;
|
|
2415
2775
|
const half = e.n.startsWith(".tmp-");
|
|
2416
2776
|
const old = e.age >= (half ? KEEP_MS : maxAgeMs);
|
|
2417
2777
|
const surplus = !half && ++rank > keepNewest && e.age >= JUST_USED_MS;
|
|
@@ -2427,10 +2787,10 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
|
|
|
2427
2787
|
}
|
|
2428
2788
|
function makeWritable(dir) {
|
|
2429
2789
|
const walk = (d) => {
|
|
2430
|
-
for (const e of
|
|
2431
|
-
const p =
|
|
2790
|
+
for (const e of fs16.readdirSync(d, { withFileTypes: true })) {
|
|
2791
|
+
const p = path14.join(d, e.name);
|
|
2432
2792
|
if (e.isDirectory()) walk(p);
|
|
2433
|
-
else if (e.isFile())
|
|
2793
|
+
else if (e.isFile()) fs16.chmodSync(p, fs16.statSync(p).mode | 128);
|
|
2434
2794
|
}
|
|
2435
2795
|
};
|
|
2436
2796
|
try {
|
|
@@ -2441,17 +2801,17 @@ function makeWritable(dir) {
|
|
|
2441
2801
|
function backToProject(text, snapshotDir, root) {
|
|
2442
2802
|
let real = snapshotDir;
|
|
2443
2803
|
try {
|
|
2444
|
-
real =
|
|
2804
|
+
real = fs16.realpathSync(snapshotDir);
|
|
2445
2805
|
} catch {
|
|
2446
2806
|
}
|
|
2447
2807
|
let out = text;
|
|
2448
|
-
for (const dir of /* @__PURE__ */ new Set([real, snapshotDir])) out = out.split(`${dir}${
|
|
2808
|
+
for (const dir of /* @__PURE__ */ new Set([real, snapshotDir])) out = out.split(`${dir}${path14.sep}`).join(`${root}${path14.sep}`).split(dir).join(root);
|
|
2449
2809
|
return out;
|
|
2450
2810
|
}
|
|
2451
2811
|
|
|
2452
2812
|
// src/core/audit.ts
|
|
2453
2813
|
import crypto4 from "node:crypto";
|
|
2454
|
-
import
|
|
2814
|
+
import path15 from "node:path";
|
|
2455
2815
|
var ANSWER_MAX = 8e3;
|
|
2456
2816
|
function auditRate(meta) {
|
|
2457
2817
|
return meta.auditRate ?? effective().audit.value;
|
|
@@ -2462,10 +2822,27 @@ function draw(id) {
|
|
|
2462
2822
|
function auditable(meta, answer) {
|
|
2463
2823
|
return meta.mode === "read" && meta.state === "done" && !meta.reviewOf && !meta.auditOf && !meta.audit && !!answer.trim();
|
|
2464
2824
|
}
|
|
2465
|
-
function sampled(meta) {
|
|
2466
|
-
const rate2 = auditRate(meta);
|
|
2825
|
+
function sampled(meta, rate2 = auditRate(meta)) {
|
|
2467
2826
|
return rate2 > 0 && (rate2 >= 1 || draw(meta.id) < rate2);
|
|
2468
2827
|
}
|
|
2828
|
+
var ASKS_FOR_LIST = /\b(all|every|each|list|lists|enumerate|how many|count|which)\b/i;
|
|
2829
|
+
function focusedRate(base2, task, record2) {
|
|
2830
|
+
if (base2 <= 0 || base2 >= 1) return { rate: base2, why: [] };
|
|
2831
|
+
let rate2 = base2;
|
|
2832
|
+
const why = [];
|
|
2833
|
+
if (ASKS_FOR_LIST.test(task)) {
|
|
2834
|
+
rate2 *= 2;
|
|
2835
|
+
why.push("asks for a list or a count");
|
|
2836
|
+
}
|
|
2837
|
+
if (record2 && record2.audited >= 3 && record2.agreed / record2.audited < 0.5) {
|
|
2838
|
+
rate2 *= 2;
|
|
2839
|
+
why.push(`the worker's audited answers were confirmed ${record2.agreed} of ${record2.audited} times`);
|
|
2840
|
+
} else if (record2 && record2.audited >= 5 && record2.agreed === record2.audited) {
|
|
2841
|
+
rate2 /= 2;
|
|
2842
|
+
why.push(`the worker's ${record2.audited} audited answers were all confirmed`);
|
|
2843
|
+
}
|
|
2844
|
+
return { rate: Math.min(rate2, 1), why };
|
|
2845
|
+
}
|
|
2469
2846
|
function pickAuditor(meta) {
|
|
2470
2847
|
const eff = effective();
|
|
2471
2848
|
const tiers = eff.tiers.value;
|
|
@@ -2478,9 +2855,9 @@ function relativeToAuditor(answer, meta) {
|
|
|
2478
2855
|
let out = answer;
|
|
2479
2856
|
const roots = [...new Set([meta.dir, meta.repoRoot].filter((r) => !!r))].sort((a, b) => b.length - a.length);
|
|
2480
2857
|
for (const root of roots) {
|
|
2481
|
-
const rel =
|
|
2482
|
-
const prefix = rel ? `${rel.split(
|
|
2483
|
-
out = out.split(`${root}${
|
|
2858
|
+
const rel = path15.relative(meta.dir, root);
|
|
2859
|
+
const prefix = rel ? `${rel.split(path15.sep).join("/")}/` : "";
|
|
2860
|
+
out = out.split(`${root}${path15.sep}`).join(prefix).replace(new RegExp(`${root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w.-])`, "g"), rel || ".");
|
|
2484
2861
|
}
|
|
2485
2862
|
return out;
|
|
2486
2863
|
}
|
|
@@ -2513,18 +2890,34 @@ function auditTask(meta, answer) {
|
|
|
2513
2890
|
'Under DISPUTED list only claims you checked and found wrong or unsupported; write "- (none)" when there are none.'
|
|
2514
2891
|
].join("\n");
|
|
2515
2892
|
}
|
|
2893
|
+
function wantsFix(target, resumable) {
|
|
2894
|
+
const a = target.audit;
|
|
2895
|
+
return effective().auditFix.value && resumable && !!target.sessionId && !target.fixOf && !a?.fix && a?.state === "done" && (a.verdict === "partial" || a.verdict === "disagree") && !!a.disputed?.length;
|
|
2896
|
+
}
|
|
2897
|
+
function fixTask(disputed) {
|
|
2898
|
+
return [
|
|
2899
|
+
"Another worker audited your answer above and disputed these claims:",
|
|
2900
|
+
...disputed.map((d) => `- ${d}`),
|
|
2901
|
+
"",
|
|
2902
|
+
"Check each one yourself against the files (the auditor can be wrong too). Then give your whole answer again,",
|
|
2903
|
+
"corrected, in the same answer format: keep what holds, fix what does not, and add what was missing.",
|
|
2904
|
+
"Under OPEN ISSUES, say which disputed claims you accepted and which you kept, and why."
|
|
2905
|
+
].join("\n");
|
|
2906
|
+
}
|
|
2516
2907
|
function auditLines(meta) {
|
|
2908
|
+
const fixing = meta.fixOf ? [`\u2500\u2500 correction: the answer of ${meta.fixOf}, checked again against what its audit disputed`] : [];
|
|
2517
2909
|
if (meta.auditOf) {
|
|
2518
2910
|
return meta.state === "done" ? [`\u2500\u2500 audit of ${meta.auditOf}: ${(meta.auditVerdict ?? "unclear").toUpperCase()}${meta.auditDisputed?.length ? ` \xB7 ${meta.auditDisputed.length} disputed` : ""}`] : [];
|
|
2519
2911
|
}
|
|
2520
2912
|
const a = meta.audit;
|
|
2521
|
-
if (!a) return
|
|
2913
|
+
if (!a) return fixing;
|
|
2522
2914
|
if (a.state === "done") {
|
|
2523
2915
|
const head = `\u2500\u2500 audit (run ${a.id}): ${(a.verdict ?? "unclear").toUpperCase()}${a.verdict === "agree" ? "" : a.verdict === "unclear" ? " (the auditor did not answer in the expected form)" : ""}`;
|
|
2524
|
-
|
|
2916
|
+
const fix = a.fix ? [`\u2500\u2500 fix: the worker is correcting its answer as a follow-up (pitroom show ${a.fix})`] : [];
|
|
2917
|
+
return [...fixing, head, ...(a.disputed ?? []).map((d) => ` disputed: ${d}`), ...fix];
|
|
2525
2918
|
}
|
|
2526
|
-
if (a.state === "running" || a.state === "queued") return [`\u2500\u2500 audit: another worker is re-checking this answer in the background (pitroom show ${a.id})`];
|
|
2527
|
-
return [`\u2500\u2500 audit (run ${a.id}) ${a.state}: no verdict`];
|
|
2919
|
+
if (a.state === "running" || a.state === "queued") return [...fixing, `\u2500\u2500 audit: another worker is re-checking this answer in the background (pitroom show ${a.id})`];
|
|
2920
|
+
return [...fixing, `\u2500\u2500 audit (run ${a.id}) ${a.state}: no verdict`];
|
|
2528
2921
|
}
|
|
2529
2922
|
function auditBadge(meta) {
|
|
2530
2923
|
const a = meta.audit;
|
|
@@ -2533,26 +2926,20 @@ function auditBadge(meta) {
|
|
|
2533
2926
|
return a.state === "running" || a.state === "queued" ? "PENDING" : "FAILED";
|
|
2534
2927
|
}
|
|
2535
2928
|
|
|
2536
|
-
// src/core/notify.ts
|
|
2537
|
-
import { spawn as spawn3 } from "node:child_process";
|
|
2538
|
-
import crypto5 from "node:crypto";
|
|
2539
|
-
import fs22 from "node:fs";
|
|
2540
|
-
import path18 from "node:path";
|
|
2541
|
-
|
|
2542
2929
|
// src/core/history.ts
|
|
2543
|
-
import
|
|
2930
|
+
import fs20 from "node:fs";
|
|
2544
2931
|
import { createRequire } from "node:module";
|
|
2545
|
-
import
|
|
2932
|
+
import path18 from "node:path";
|
|
2546
2933
|
import zlib from "node:zlib";
|
|
2547
2934
|
|
|
2548
2935
|
// src/core/receipt.ts
|
|
2549
|
-
import
|
|
2550
|
-
import
|
|
2936
|
+
import fs18 from "node:fs";
|
|
2937
|
+
import path17 from "node:path";
|
|
2551
2938
|
|
|
2552
2939
|
// src/core/prices.ts
|
|
2553
2940
|
import { spawn as spawn2 } from "node:child_process";
|
|
2554
|
-
import
|
|
2555
|
-
import
|
|
2941
|
+
import fs17 from "node:fs";
|
|
2942
|
+
import path16 from "node:path";
|
|
2556
2943
|
var DEFAULT_URL = "https://models.dev/api.json";
|
|
2557
2944
|
var RETRY_AFTER_FAILURE_MS = 60 * 6e4;
|
|
2558
2945
|
var MIN_MODELS = 100;
|
|
@@ -2565,8 +2952,8 @@ function catalogLabel() {
|
|
|
2565
2952
|
return url;
|
|
2566
2953
|
}
|
|
2567
2954
|
}
|
|
2568
|
-
var catalogFile = () =>
|
|
2569
|
-
var attemptFile = () =>
|
|
2955
|
+
var catalogFile = () => path16.join(home(), "prices.json");
|
|
2956
|
+
var attemptFile = () => path16.join(home(), "prices.attempt");
|
|
2570
2957
|
function trim(raw) {
|
|
2571
2958
|
const out = {};
|
|
2572
2959
|
if (!raw || typeof raw !== "object") return out;
|
|
@@ -2584,19 +2971,19 @@ var cached2;
|
|
|
2584
2971
|
function readCatalog() {
|
|
2585
2972
|
const file2 = catalogFile();
|
|
2586
2973
|
try {
|
|
2587
|
-
const { mtimeMs } =
|
|
2974
|
+
const { mtimeMs } = fs17.statSync(file2);
|
|
2588
2975
|
if (cached2?.file === file2 && cached2.mtimeMs === mtimeMs) return cached2.catalog;
|
|
2589
|
-
const parsed = JSON.parse(
|
|
2590
|
-
const
|
|
2591
|
-
cached2 = { file: file2, mtimeMs, catalog:
|
|
2592
|
-
return
|
|
2976
|
+
const parsed = JSON.parse(fs17.readFileSync(file2, "utf8"));
|
|
2977
|
+
const catalog6 = parsed && typeof parsed.models === "object" && parsed.models && Number.isFinite(Date.parse(parsed.fetchedAt)) ? parsed : void 0;
|
|
2978
|
+
cached2 = { file: file2, mtimeMs, catalog: catalog6 };
|
|
2979
|
+
return catalog6;
|
|
2593
2980
|
} catch {
|
|
2594
2981
|
return void 0;
|
|
2595
2982
|
}
|
|
2596
2983
|
}
|
|
2597
2984
|
var ageMs = (file2) => {
|
|
2598
2985
|
try {
|
|
2599
|
-
return Date.now() -
|
|
2986
|
+
return Date.now() - fs17.statSync(file2).mtimeMs;
|
|
2600
2987
|
} catch {
|
|
2601
2988
|
return Infinity;
|
|
2602
2989
|
}
|
|
@@ -2615,17 +3002,17 @@ async function refreshCatalog() {
|
|
|
2615
3002
|
const models = trim(await r.json());
|
|
2616
3003
|
const n = Object.keys(models).length;
|
|
2617
3004
|
if (n < MIN_MODELS) throw new Error(`only ${n} priced models in it (a catalog has thousands): not used`);
|
|
2618
|
-
|
|
3005
|
+
fs17.mkdirSync(home(), { recursive: true });
|
|
2619
3006
|
const tmp = `${catalogFile()}.${process.pid}.tmp`;
|
|
2620
|
-
const
|
|
2621
|
-
|
|
3007
|
+
const catalog6 = { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), source: url, models };
|
|
3008
|
+
fs17.writeFileSync(tmp, JSON.stringify(catalog6));
|
|
2622
3009
|
renameOver(tmp, catalogFile());
|
|
2623
|
-
|
|
3010
|
+
fs17.rmSync(attemptFile(), { force: true });
|
|
2624
3011
|
return { state: "updated", models: n };
|
|
2625
3012
|
} catch (e) {
|
|
2626
3013
|
try {
|
|
2627
|
-
|
|
2628
|
-
|
|
3014
|
+
fs17.mkdirSync(home(), { recursive: true });
|
|
3015
|
+
fs17.writeFileSync(attemptFile(), `${(/* @__PURE__ */ new Date()).toISOString()} ${e.message}
|
|
2629
3016
|
`);
|
|
2630
3017
|
} catch {
|
|
2631
3018
|
}
|
|
@@ -2637,8 +3024,8 @@ function refreshInBackground() {
|
|
|
2637
3024
|
if (!refreshDue()) return;
|
|
2638
3025
|
const script = process.argv[1];
|
|
2639
3026
|
if (!script) return;
|
|
2640
|
-
|
|
2641
|
-
|
|
3027
|
+
fs17.mkdirSync(home(), { recursive: true });
|
|
3028
|
+
fs17.writeFileSync(attemptFile(), `${(/* @__PURE__ */ new Date()).toISOString()} refresh started
|
|
2642
3029
|
`);
|
|
2643
3030
|
const child = spawn2(process.execPath, [script, "prices", "--refresh", "--quiet"], { detached: true, stdio: "ignore", env: process.env });
|
|
2644
3031
|
child.on("error", () => void 0);
|
|
@@ -2648,14 +3035,14 @@ function refreshInBackground() {
|
|
|
2648
3035
|
}
|
|
2649
3036
|
var OWN = { codex: ["openai"], claude: ["anthropic"], gemini: ["google"] };
|
|
2650
3037
|
function catalogPrice(backend, ...models) {
|
|
2651
|
-
const
|
|
2652
|
-
if (!
|
|
3038
|
+
const catalog6 = readCatalog();
|
|
3039
|
+
if (!catalog6) return void 0;
|
|
2653
3040
|
for (const raw of models) {
|
|
2654
3041
|
const id = raw?.split("#")[0];
|
|
2655
3042
|
if (!id) continue;
|
|
2656
3043
|
const keys = [id, ...[...OWN[backend] ?? [], "anthropic", "openai", "google"].map((p) => `${p}/${id}`)];
|
|
2657
|
-
const any = keys.find((k) =>
|
|
2658
|
-
const triple = any ?
|
|
3044
|
+
const any = keys.find((k) => catalog6.models[k]) ?? Object.keys(catalog6.models).filter((k) => k.endsWith(`/${id}`)).sort()[0];
|
|
3045
|
+
const triple = any ? catalog6.models[any] : void 0;
|
|
2659
3046
|
if (any && triple && triple.length === 3 && triple.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
2660
3047
|
const [input, output, cachedInput] = triple;
|
|
2661
3048
|
return { input, output, cachedInput, key: any };
|
|
@@ -2668,7 +3055,7 @@ function catalogStatus() {
|
|
|
2668
3055
|
const c = readCatalog();
|
|
2669
3056
|
let lastFailure;
|
|
2670
3057
|
try {
|
|
2671
|
-
const t =
|
|
3058
|
+
const t = fs17.readFileSync(attemptFile(), "utf8").trim();
|
|
2672
3059
|
if (t && !t.endsWith("refresh started")) lastFailure = t;
|
|
2673
3060
|
} catch {
|
|
2674
3061
|
}
|
|
@@ -2739,13 +3126,13 @@ function record(meta) {
|
|
|
2739
3126
|
saved: meta.savedUsd ?? 0,
|
|
2740
3127
|
price: primaryPrice().name
|
|
2741
3128
|
};
|
|
2742
|
-
|
|
2743
|
-
|
|
3129
|
+
fs18.mkdirSync(path17.dirname(ledgerFile()), { recursive: true });
|
|
3130
|
+
fs18.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
|
|
2744
3131
|
`);
|
|
2745
3132
|
}
|
|
2746
3133
|
function readLedger(sinceMs2) {
|
|
2747
|
-
if (!
|
|
2748
|
-
return
|
|
3134
|
+
if (!fs18.existsSync(ledgerFile())) return [];
|
|
3135
|
+
return fs18.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
|
|
2749
3136
|
try {
|
|
2750
3137
|
return [JSON.parse(l)];
|
|
2751
3138
|
} catch {
|
|
@@ -2820,7 +3207,7 @@ ${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.rou
|
|
|
2820
3207
|
}
|
|
2821
3208
|
|
|
2822
3209
|
// src/core/ui.ts
|
|
2823
|
-
import
|
|
3210
|
+
import fs19 from "node:fs";
|
|
2824
3211
|
var WEEK_MS = 7 * 24 * 3600 * 1e3;
|
|
2825
3212
|
var RECENT = 40;
|
|
2826
3213
|
var RECENT_HOURS = 1;
|
|
@@ -2911,9 +3298,9 @@ function cardsFor(m) {
|
|
|
2911
3298
|
const out = [];
|
|
2912
3299
|
for (const [phase, text] of phases) {
|
|
2913
3300
|
const marker = runFile(m.id, `card-${phase}`);
|
|
2914
|
-
if (
|
|
3301
|
+
if (fs19.existsSync(marker)) continue;
|
|
2915
3302
|
if (!(phase === "started" && !isActive(m.state))) out.push(text());
|
|
2916
|
-
|
|
3303
|
+
fs19.writeFileSync(marker, "");
|
|
2917
3304
|
}
|
|
2918
3305
|
return out;
|
|
2919
3306
|
}
|
|
@@ -2924,10 +3311,10 @@ function hookCards(input) {
|
|
|
2924
3311
|
const response = event.tool_response;
|
|
2925
3312
|
const output = typeof response === "string" ? response : [response?.stdout, response?.stderr].filter(Boolean).join("\n");
|
|
2926
3313
|
const ids = [...new Set(`${command}
|
|
2927
|
-
${output}`.match(RUN_ID) ?? [])].filter((id) =>
|
|
3314
|
+
${output}`.match(RUN_ID) ?? [])].filter((id) => fs19.existsSync(`${runsDir()}/${id}`));
|
|
2928
3315
|
const since2 = Date.now() - RECENT_HOURS * 36e5;
|
|
2929
3316
|
for (const id of listRunIds().slice(-RECENT)) {
|
|
2930
|
-
if (ids.includes(id) ||
|
|
3317
|
+
if (ids.includes(id) || fs19.existsSync(runFile(id, "card-ended"))) continue;
|
|
2931
3318
|
try {
|
|
2932
3319
|
const m = readMeta(id);
|
|
2933
3320
|
if (!isActive(m.state) && Date.parse(m.endedAt ?? m.startedAt) > since2) ids.push(id);
|
|
@@ -2949,7 +3336,7 @@ var SCHEMA_VERSION = 1;
|
|
|
2949
3336
|
var PATCH_MAX = 1e6;
|
|
2950
3337
|
var ANSWER_MAX2 = 2e5;
|
|
2951
3338
|
var cached3;
|
|
2952
|
-
var historyFile = () =>
|
|
3339
|
+
var historyFile = () => path18.join(home(), "history.db");
|
|
2953
3340
|
function openDb() {
|
|
2954
3341
|
const file2 = historyFile();
|
|
2955
3342
|
if (cached3?.file === file2) return cached3.db;
|
|
@@ -2963,7 +3350,7 @@ function openDb() {
|
|
|
2963
3350
|
} finally {
|
|
2964
3351
|
process.emitWarning = emit;
|
|
2965
3352
|
}
|
|
2966
|
-
|
|
3353
|
+
fs20.mkdirSync(path18.dirname(file2), { recursive: true });
|
|
2967
3354
|
db = new DatabaseSync(file2);
|
|
2968
3355
|
db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
|
|
2969
3356
|
migrate(db);
|
|
@@ -2998,35 +3385,35 @@ function migrate(db) {
|
|
|
2998
3385
|
function readRunFile(id, name) {
|
|
2999
3386
|
const f = runFile(id, name);
|
|
3000
3387
|
try {
|
|
3001
|
-
if (
|
|
3002
|
-
if (
|
|
3388
|
+
if (fs20.existsSync(f)) return fs20.readFileSync(f, "utf8");
|
|
3389
|
+
if (fs20.existsSync(`${f}.gz`)) return zlib.gunzipSync(fs20.readFileSync(`${f}.gz`)).toString("utf8");
|
|
3003
3390
|
} catch {
|
|
3004
3391
|
}
|
|
3005
3392
|
return void 0;
|
|
3006
3393
|
}
|
|
3007
3394
|
function gzipFile(f) {
|
|
3008
|
-
if (!
|
|
3009
|
-
|
|
3010
|
-
|
|
3395
|
+
if (!fs20.existsSync(f)) return;
|
|
3396
|
+
fs20.writeFileSync(`${f}.gz`, zlib.gzipSync(fs20.readFileSync(f)));
|
|
3397
|
+
fs20.rmSync(f);
|
|
3011
3398
|
}
|
|
3012
3399
|
function compactRun(meta) {
|
|
3013
3400
|
if (!TERMINAL.includes(meta.state)) return;
|
|
3014
|
-
const dir =
|
|
3401
|
+
const dir = path18.dirname(runFile(meta.id, "meta.json"));
|
|
3015
3402
|
let names;
|
|
3016
3403
|
try {
|
|
3017
|
-
names =
|
|
3404
|
+
names = fs20.readdirSync(dir);
|
|
3018
3405
|
} catch {
|
|
3019
3406
|
return;
|
|
3020
3407
|
}
|
|
3021
3408
|
for (const n of names) {
|
|
3022
|
-
if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(
|
|
3409
|
+
if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(path18.join(dir, n));
|
|
3023
3410
|
else if (/^stderr(\.attempt-\d+)?\.log$/.test(n)) {
|
|
3024
|
-
if (meta.state === "done" && !/attempt/.test(n))
|
|
3025
|
-
else gzipFile(
|
|
3411
|
+
if (meta.state === "done" && !/attempt/.test(n)) fs20.rmSync(path18.join(dir, n), { force: true });
|
|
3412
|
+
else gzipFile(path18.join(dir, n));
|
|
3026
3413
|
}
|
|
3027
3414
|
}
|
|
3028
3415
|
}
|
|
3029
|
-
var
|
|
3416
|
+
var clip6 = (s, n) => s && s.length > n ? s.slice(0, n) : s;
|
|
3030
3417
|
function timelineOf(meta) {
|
|
3031
3418
|
const raw = readRunFile(meta.id, "events.jsonl");
|
|
3032
3419
|
if (!raw) return [];
|
|
@@ -3043,8 +3430,8 @@ function recordRun(meta) {
|
|
|
3043
3430
|
try {
|
|
3044
3431
|
const u = meta.usage;
|
|
3045
3432
|
const known = db.prepare("SELECT answer IS NOT NULL AS has FROM runs WHERE id = ?").get(meta.id);
|
|
3046
|
-
const answer = known?.has ? void 0 :
|
|
3047
|
-
const patch = known?.has ? void 0 :
|
|
3433
|
+
const answer = known?.has ? void 0 : clip6(readRunFile(meta.id, "summary.md")?.trim(), ANSWER_MAX2);
|
|
3434
|
+
const patch = known?.has ? void 0 : clip6(readRunFile(meta.id, "changes.patch"), PATCH_MAX);
|
|
3048
3435
|
const seconds = meta.endedAt ? Math.max(0, Math.round((Date.parse(meta.endedAt) - Date.parse(meta.startedAt)) / 1e3)) : null;
|
|
3049
3436
|
const t = meta.ran ?? meta.worker;
|
|
3050
3437
|
const fields = {
|
|
@@ -3056,7 +3443,7 @@ function recordRun(meta) {
|
|
|
3056
3443
|
kind: kind(meta),
|
|
3057
3444
|
backend: t.backend,
|
|
3058
3445
|
model: meta.resolvedModel ?? t.model ?? null,
|
|
3059
|
-
task:
|
|
3446
|
+
task: clip6(meta.task, 2e4) ?? "",
|
|
3060
3447
|
grp: meta.group ?? null,
|
|
3061
3448
|
dir: meta.dir,
|
|
3062
3449
|
review_of: meta.reviewOf ?? meta.auditOf ?? null,
|
|
@@ -3070,7 +3457,7 @@ function recordRun(meta) {
|
|
|
3070
3457
|
saved: meta.savedUsd ?? null,
|
|
3071
3458
|
files_changed: meta.changes?.length ?? 0,
|
|
3072
3459
|
applied: meta.applied ? 1 : 0,
|
|
3073
|
-
error:
|
|
3460
|
+
error: clip6(meta.error, 2e3) ?? null,
|
|
3074
3461
|
meta_json: JSON.stringify(meta)
|
|
3075
3462
|
};
|
|
3076
3463
|
db.exec("BEGIN IMMEDIATE");
|
|
@@ -3230,6 +3617,51 @@ function auditStats(db, since2) {
|
|
|
3230
3617
|
}
|
|
3231
3618
|
return { total, byWorker };
|
|
3232
3619
|
}
|
|
3620
|
+
function workerRecord(backend, model, sinceMs2) {
|
|
3621
|
+
const db = openDb();
|
|
3622
|
+
if (!db) return void 0;
|
|
3623
|
+
const since2 = new Date(sinceMs2).toISOString();
|
|
3624
|
+
const fits = (m) => !model || m === model || !!m?.endsWith(`/${model}`) || model.endsWith(`/${m}`);
|
|
3625
|
+
const rec = { runs: 0, ok: 0, audited: 0, agreed: 0 };
|
|
3626
|
+
try {
|
|
3627
|
+
for (const r of db.prepare(`SELECT model, SUM(NOT ${LIMITED}) runs, SUM(state='done') ok FROM runs WHERE backend = ? AND started_at >= ? AND ${NOT_AUDIT} GROUP BY model`).all(backend, since2)) {
|
|
3628
|
+
if (!fits(r.model)) continue;
|
|
3629
|
+
rec.runs += r.runs ?? 0;
|
|
3630
|
+
rec.ok += r.ok ?? 0;
|
|
3631
|
+
}
|
|
3632
|
+
const audits = db.prepare(`SELECT r.model, json_extract(a.meta_json, '$.auditVerdict') v, COUNT(*) n
|
|
3633
|
+
FROM runs a JOIN runs r ON r.id = json_extract(a.meta_json, '$.auditOf')
|
|
3634
|
+
WHERE r.backend = ? AND a.started_at >= ? AND a.state = 'done' AND json_extract(a.meta_json, '$.auditOf') IS NOT NULL
|
|
3635
|
+
GROUP BY r.model, v`).all(backend, since2);
|
|
3636
|
+
for (const r of audits) {
|
|
3637
|
+
if (!fits(r.model)) continue;
|
|
3638
|
+
rec.audited += r.n;
|
|
3639
|
+
if (r.v === "agree") rec.agreed += r.n;
|
|
3640
|
+
}
|
|
3641
|
+
} catch {
|
|
3642
|
+
return void 0;
|
|
3643
|
+
}
|
|
3644
|
+
return rec;
|
|
3645
|
+
}
|
|
3646
|
+
function spentSince(sinceMs2) {
|
|
3647
|
+
const db = openDb();
|
|
3648
|
+
if (!db) return void 0;
|
|
3649
|
+
try {
|
|
3650
|
+
return db.prepare("SELECT COALESCE(SUM(cost),0) spent FROM runs WHERE started_at >= ?").get(new Date(sinceMs2).toISOString()).spent;
|
|
3651
|
+
} catch {
|
|
3652
|
+
return void 0;
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
function recentCosts(backend, model, n = 5) {
|
|
3656
|
+
const db = openDb();
|
|
3657
|
+
if (!db) return [];
|
|
3658
|
+
try {
|
|
3659
|
+
const rows = db.prepare("SELECT model, cost FROM runs WHERE backend = ? AND cost IS NOT NULL AND steps > 0 ORDER BY started_at DESC LIMIT 200").all(backend);
|
|
3660
|
+
return rows.filter((r) => !model || r.model === model || r.model?.endsWith(`/${model}`) || model.endsWith(`/${r.model}`)).slice(0, n).map((r) => r.cost);
|
|
3661
|
+
} catch {
|
|
3662
|
+
return [];
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3233
3665
|
function workerRows(rows, ledger, audited) {
|
|
3234
3666
|
const out = rows.map((r) => ({ backend: r.backend, model: r.model ?? void 0, runs: r.runs, ok: r.ok ?? 0, limited: r.limited ?? 0, avgSeconds: r.avg_s, avgTokens: r.avg_t, saved: 0, audited: 0, agreed: 0 }));
|
|
3235
3667
|
const key = (backend, model) => `${backend ?? ""}\0${model ?? ""}`;
|
|
@@ -3278,12 +3710,96 @@ function historyStats(sinceMs2) {
|
|
|
3278
3710
|
}
|
|
3279
3711
|
}
|
|
3280
3712
|
|
|
3281
|
-
// src/core/
|
|
3713
|
+
// src/core/budget.ts
|
|
3282
3714
|
import fs21 from "node:fs";
|
|
3715
|
+
var startOfDay = (now = /* @__PURE__ */ new Date()) => new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
3716
|
+
function activeSpend(sinceMs2) {
|
|
3717
|
+
let total = 0;
|
|
3718
|
+
for (const id of listRunIds()) {
|
|
3719
|
+
try {
|
|
3720
|
+
const m = freshMeta(id);
|
|
3721
|
+
if (!isActive(m.state) || Date.parse(m.startedAt) < sinceMs2) continue;
|
|
3722
|
+
const target = m.ran ?? m.worker;
|
|
3723
|
+
const events = runFile(id, "events.jsonl");
|
|
3724
|
+
if (!fs21.existsSync(events)) continue;
|
|
3725
|
+
const run2 = getBackend(target.backend).parse(fs21.readFileSync(events, "utf8"));
|
|
3726
|
+
if (run2.usage.cost !== void 0) total += run2.usage.cost;
|
|
3727
|
+
else {
|
|
3728
|
+
const price = workerPrice(target.backend, run2.model, target.model);
|
|
3729
|
+
if (price) total += costAt(run2.usage, price);
|
|
3730
|
+
}
|
|
3731
|
+
} catch {
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
return total;
|
|
3735
|
+
}
|
|
3736
|
+
function isFree(t) {
|
|
3737
|
+
let model = t.model;
|
|
3738
|
+
try {
|
|
3739
|
+
model ??= getBackend(t.backend).defaultModel?.();
|
|
3740
|
+
} catch {
|
|
3741
|
+
}
|
|
3742
|
+
const price = workerPrice(t.backend, model);
|
|
3743
|
+
if (price) return price.input === 0 && price.output === 0 && price.cachedInput === 0;
|
|
3744
|
+
const costs = recentCosts(t.backend, model);
|
|
3745
|
+
return costs.length > 0 && costs.every((c) => c === 0);
|
|
3746
|
+
}
|
|
3747
|
+
function withinBudget(chain, now = /* @__PURE__ */ new Date()) {
|
|
3748
|
+
const budget = effective().budgetDaily.value;
|
|
3749
|
+
if (budget === void 0) return { chain };
|
|
3750
|
+
const finished = spentSince(startOfDay(now));
|
|
3751
|
+
if (finished === void 0) return { chain };
|
|
3752
|
+
const spent = finished + activeSpend(startOfDay(now));
|
|
3753
|
+
if (spent < budget) {
|
|
3754
|
+
return { chain, warning: spent >= 0.8 * budget ? `the workers cost ${usd(spent)} today, of your ${usd(budget)} daily budget ("budgetDaily")` : void 0 };
|
|
3755
|
+
}
|
|
3756
|
+
const free = chain.filter(isFree);
|
|
3757
|
+
if (!free.length) {
|
|
3758
|
+
throw new UserError(
|
|
3759
|
+
`the workers cost ${usd(spent)} today, which reaches your daily budget of ${usd(budget)} ("budgetDaily"): only workers that cost nothing run now, and ${chain.map(describeTarget).join(", ")} ${chain.length === 1 ? "is" : "are"} not known to be free. Raise the budget, name a free worker (-W), or wait until tomorrow.`,
|
|
3760
|
+
3
|
|
3761
|
+
);
|
|
3762
|
+
}
|
|
3763
|
+
const left = chain.length - free.length;
|
|
3764
|
+
return { chain: free, warning: `the daily budget (${usd(budget)}) is spent (${usd(spent)} today): only the free workers run${left ? `, ${left} paid one${left === 1 ? "" : "s"} left out` : ""}` };
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3767
|
+
// src/core/rank.ts
|
|
3768
|
+
var MIN_RUNS = 5;
|
|
3769
|
+
var MIN_AUDITS = 3;
|
|
3770
|
+
function scoreOf(r) {
|
|
3771
|
+
if (!r || r.runs < MIN_RUNS) return void 0;
|
|
3772
|
+
const ok = Math.min(r.ok, r.runs) / r.runs;
|
|
3773
|
+
return r.audited >= MIN_AUDITS ? ok * (r.agreed / r.audited) : ok;
|
|
3774
|
+
}
|
|
3775
|
+
var pct = (x) => `${Math.round(x * 100)}%`;
|
|
3776
|
+
function rankChain(chain, recordOf) {
|
|
3777
|
+
const scored = chain.map((t, i) => ({ t, i, score: scoreOf(recordOf(t)) }));
|
|
3778
|
+
const slots = scored.filter((s) => s.score !== void 0);
|
|
3779
|
+
if (slots.length < 2) return { chain };
|
|
3780
|
+
const ranked = [...slots].sort((a, b) => Math.round(b.score * 10) - Math.round(a.score * 10) || a.i - b.i);
|
|
3781
|
+
const out = [...chain];
|
|
3782
|
+
slots.forEach((slot, k) => out[slot.i] = ranked[k].t);
|
|
3783
|
+
if (out[0] === chain[0]) return { chain: out };
|
|
3784
|
+
const by = (t) => scored.find((s) => s.t === t).score;
|
|
3785
|
+
return {
|
|
3786
|
+
chain: out,
|
|
3787
|
+
note: `ranked by record ("rankWorkers"): ${describeTarget(out[0])} (${pct(by(out[0]))}) goes before ${describeTarget(chain[0])} (${pct(by(chain[0]))})`
|
|
3788
|
+
};
|
|
3789
|
+
}
|
|
3790
|
+
|
|
3791
|
+
// src/core/notify.ts
|
|
3792
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
3793
|
+
import crypto5 from "node:crypto";
|
|
3794
|
+
import fs24 from "node:fs";
|
|
3795
|
+
import path20 from "node:path";
|
|
3796
|
+
|
|
3797
|
+
// src/core/report.ts
|
|
3798
|
+
import fs23 from "node:fs";
|
|
3283
3799
|
|
|
3284
3800
|
// src/core/plan.ts
|
|
3285
|
-
import
|
|
3286
|
-
import
|
|
3801
|
+
import fs22 from "node:fs";
|
|
3802
|
+
import path19 from "node:path";
|
|
3287
3803
|
var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
3288
3804
|
var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
|
|
3289
3805
|
function structure(lines) {
|
|
@@ -3330,9 +3846,9 @@ function parsePlan(text, file2 = "") {
|
|
|
3330
3846
|
return { file: file2, title: titleHeading?.text ?? "", header, constraints, tasks };
|
|
3331
3847
|
}
|
|
3332
3848
|
function loadPlan(file2) {
|
|
3333
|
-
const abs =
|
|
3334
|
-
if (!
|
|
3335
|
-
const plan = parsePlan(
|
|
3849
|
+
const abs = path19.resolve(file2);
|
|
3850
|
+
if (!fs22.existsSync(abs) || !fs22.statSync(abs).isFile()) throw new UserError(`plan not found: ${file2}`);
|
|
3851
|
+
const plan = parsePlan(fs22.readFileSync(abs, "utf8"), fs22.realpathSync(abs));
|
|
3336
3852
|
if (!plan.tasks.length) throw new UserError(`${file2} has no "Task N" headings (see pitroom-writing-plans)`);
|
|
3337
3853
|
return plan;
|
|
3338
3854
|
}
|
|
@@ -3351,7 +3867,7 @@ function brief(plan, task) {
|
|
|
3351
3867
|
].filter(Boolean).join("\n\n")}
|
|
3352
3868
|
`;
|
|
3353
3869
|
}
|
|
3354
|
-
var planName = (file2) =>
|
|
3870
|
+
var planName = (file2) => path19.basename(file2).replace(/\.md$/i, "");
|
|
3355
3871
|
|
|
3356
3872
|
// src/core/style.ts
|
|
3357
3873
|
var enabled = () => {
|
|
@@ -3411,7 +3927,7 @@ function duration(meta) {
|
|
|
3411
3927
|
}
|
|
3412
3928
|
function readSummary(meta) {
|
|
3413
3929
|
const f = runFile(meta.id, "summary.md");
|
|
3414
|
-
return
|
|
3930
|
+
return fs23.existsSync(f) ? fs23.readFileSync(f, "utf8").trim() : archivedRun(meta.id)?.answer ?? "";
|
|
3415
3931
|
}
|
|
3416
3932
|
function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
3417
3933
|
const out = [];
|
|
@@ -3497,8 +4013,8 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
|
3497
4013
|
}
|
|
3498
4014
|
function live(meta) {
|
|
3499
4015
|
const f = runFile(meta.id, "events.jsonl");
|
|
3500
|
-
if (!
|
|
3501
|
-
const p = getBackend((meta.ran ?? meta.worker).backend).parse(
|
|
4016
|
+
if (!fs23.existsSync(f)) return { steps: 0, toolCalls: 0 };
|
|
4017
|
+
const p = getBackend((meta.ran ?? meta.worker).backend).parse(fs23.readFileSync(f, "utf8"));
|
|
3502
4018
|
return { steps: p.usage.steps, toolCalls: p.usage.toolCalls, last: p.lastActivity };
|
|
3503
4019
|
}
|
|
3504
4020
|
function progress(meta) {
|
|
@@ -3518,21 +4034,33 @@ function groupIds(group) {
|
|
|
3518
4034
|
}
|
|
3519
4035
|
});
|
|
3520
4036
|
}
|
|
3521
|
-
var
|
|
4037
|
+
var retryShell = (platform = process.platform) => platform === "win32" ? "powershell" : "sh";
|
|
4038
|
+
var quoteFor = (shell) => (s) => /^[\w./:@=\\-]+$/.test(s) && !(shell === "sh" && s.includes("\\")) ? s : shell === "sh" ? `'${s.replace(/'/g, `'\\''`)}'` : `'${s.replace(/'/g, "''")}'`;
|
|
4039
|
+
function retryCommands(metas, shell = retryShell()) {
|
|
4040
|
+
const quote = quoteFor(shell);
|
|
4041
|
+
return metas.filter((m) => ["failed", "timeout", "stopped"].includes(m.state) && !m.auditOf && !m.reviewOf && !m.parent && !m.fixOf).map((m) => {
|
|
4042
|
+
const where = ["-d", quote(m.dir), ...m.group ? ["-g", quote(m.group)] : []];
|
|
4043
|
+
if (m.plan) return ["pitroom run", m.mode === "write" ? "-w" : "-i", "--plan", quote(m.plan.file), "--step", String(m.plan.step), ...where, "--bg"].join(" ");
|
|
4044
|
+
const mode = m.mode === "isolate" ? ["-i"] : m.mode === "write" ? ["-w"] : [];
|
|
4045
|
+
const end = shell === "sh" || m.task.startsWith("-") ? ["--"] : [];
|
|
4046
|
+
return ["pitroom run", ...mode, ...where, "--bg", ...end, quote(m.task)].join(" ");
|
|
4047
|
+
});
|
|
4048
|
+
}
|
|
4049
|
+
var oneLine6 = (s, max) => {
|
|
3522
4050
|
const t = s.replace(/\s+/g, " ").trim();
|
|
3523
4051
|
return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
|
|
3524
4052
|
};
|
|
3525
4053
|
function headline(meta, max = 160) {
|
|
3526
4054
|
const text = readSummary(meta);
|
|
3527
4055
|
const line = text.split("\n").find((l) => /^SUMMARY:/i.test(l.trim())) ?? text.split("\n").find((l) => l.trim()) ?? "";
|
|
3528
|
-
return
|
|
4056
|
+
return oneLine6(line.replace(/^SUMMARY:\s*/i, ""), max);
|
|
3529
4057
|
}
|
|
3530
4058
|
function table(metas) {
|
|
3531
4059
|
if (!metas.length) return "no runs";
|
|
3532
4060
|
const rows = metas.map((m) => {
|
|
3533
4061
|
const l = m.state === "running" ? live(m) : void 0;
|
|
3534
4062
|
const steps = l ? String(l.steps) : m.usage ? String(m.usage.steps) : "-";
|
|
3535
|
-
const note = l?.last ?
|
|
4063
|
+
const note = l?.last ? oneLine6(l.last, 48) : isActive(m.state) ? oneLine6(m.task, 48) : headline(m, 48) || oneLine6(m.error ?? m.task, 48);
|
|
3536
4064
|
return [m.id, m.state, m.mode, duration(m), steps, describeTarget(m.ran ?? m.worker), note];
|
|
3537
4065
|
});
|
|
3538
4066
|
const head = ["RUN", "STATE", "MODE", "TIME", "STEPS", "WORKER", "NOW / RESULT"];
|
|
@@ -3574,15 +4102,15 @@ async function watch(select, opts) {
|
|
|
3574
4102
|
const prev = seen.get(m.id);
|
|
3575
4103
|
const l = m.state === "running" ? live(m) : { steps: m.usage?.steps ?? 0, toolCalls: m.usage?.toolCalls ?? 0 };
|
|
3576
4104
|
const attempts = m.attempts?.length ?? 0;
|
|
3577
|
-
const base2 = { run: m.id, mode: m.mode, task:
|
|
4105
|
+
const base2 = { run: m.id, mode: m.mode, task: oneLine6(m.task, 60) };
|
|
3578
4106
|
if (!prev) emit({ event: m.state === "queued" ? "queued" : "started", ...base2 }, m);
|
|
3579
4107
|
else if (prev.state === "queued" && m.state === "running") emit({ event: "started", ...base2 }, m);
|
|
3580
4108
|
if (attempts > (prev?.attempts ?? 0)) {
|
|
3581
4109
|
const a = m.attempts[attempts - 1];
|
|
3582
|
-
emit({ event: "fallback", run: m.id, failed: a.target, reason:
|
|
4110
|
+
emit({ event: "fallback", run: m.id, failed: a.target, reason: oneLine6(a.error, 120) }, m);
|
|
3583
4111
|
}
|
|
3584
4112
|
if (m.state === "running" && l.steps > (prev?.steps ?? 0)) {
|
|
3585
|
-
emit({ event: "progress", run: m.id, steps: l.steps, tools: l.toolCalls, last: "last" in l && l.last ?
|
|
4113
|
+
emit({ event: "progress", run: m.id, steps: l.steps, tools: l.toolCalls, last: "last" in l && l.last ? oneLine6(String(l.last), 80) : void 0 });
|
|
3586
4114
|
}
|
|
3587
4115
|
if (TERMINAL.includes(m.state) && prev?.state !== m.state) {
|
|
3588
4116
|
emit({
|
|
@@ -3591,7 +4119,7 @@ async function watch(select, opts) {
|
|
|
3591
4119
|
time: duration(m),
|
|
3592
4120
|
worker: describeTarget(m.ran ?? m.worker),
|
|
3593
4121
|
summary: headline(m) || void 0,
|
|
3594
|
-
error: m.error ?
|
|
4122
|
+
error: m.error ? oneLine6(m.error, 160) : void 0,
|
|
3595
4123
|
refs: m.refs ? `${m.refs.valid}/${m.refs.total}` : void 0,
|
|
3596
4124
|
changes: m.changes?.length || void 0
|
|
3597
4125
|
}, m);
|
|
@@ -3658,12 +4186,12 @@ function claimGroup(group) {
|
|
|
3658
4186
|
try {
|
|
3659
4187
|
const ids = groupIds(group).sort();
|
|
3660
4188
|
if (ids.length < 2) return true;
|
|
3661
|
-
const dir =
|
|
3662
|
-
|
|
3663
|
-
for (const f of
|
|
3664
|
-
if (Date.now() -
|
|
4189
|
+
const dir = path20.join(home(), "notified");
|
|
4190
|
+
fs24.mkdirSync(dir, { recursive: true });
|
|
4191
|
+
for (const f of fs24.readdirSync(dir)) {
|
|
4192
|
+
if (Date.now() - fs24.statSync(path20.join(dir, f)).mtimeMs > 30 * 864e5) fs24.rmSync(path20.join(dir, f), { force: true });
|
|
3665
4193
|
}
|
|
3666
|
-
|
|
4194
|
+
fs24.writeFileSync(path20.join(dir, crypto5.createHash("sha1").update(ids.join(",")).digest("hex").slice(0, 16)), "", { flag: "wx" });
|
|
3667
4195
|
return true;
|
|
3668
4196
|
} catch {
|
|
3669
4197
|
return false;
|
|
@@ -3697,30 +4225,30 @@ function notifyEnded(meta) {
|
|
|
3697
4225
|
}
|
|
3698
4226
|
|
|
3699
4227
|
// src/core/templates.ts
|
|
3700
|
-
import
|
|
3701
|
-
import
|
|
4228
|
+
import fs26 from "node:fs";
|
|
4229
|
+
import path22 from "node:path";
|
|
3702
4230
|
|
|
3703
4231
|
// src/core/install.ts
|
|
3704
|
-
import
|
|
3705
|
-
import
|
|
3706
|
-
import
|
|
3707
|
-
import { fileURLToPath as
|
|
4232
|
+
import fs25 from "node:fs";
|
|
4233
|
+
import os10 from "node:os";
|
|
4234
|
+
import path21 from "node:path";
|
|
4235
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
3708
4236
|
var LEGACY = ["pitroom", "opencode-worker"];
|
|
3709
4237
|
function packageRoot() {
|
|
3710
|
-
return
|
|
4238
|
+
return path21.resolve(path21.dirname(fileURLToPath3(import.meta.url)), "..");
|
|
3711
4239
|
}
|
|
3712
4240
|
function skillNames(root = packageRoot()) {
|
|
3713
|
-
const dir =
|
|
3714
|
-
if (!
|
|
3715
|
-
return
|
|
4241
|
+
const dir = path21.join(root, "skills");
|
|
4242
|
+
if (!fs25.existsSync(dir)) return [];
|
|
4243
|
+
return fs25.readdirSync(dir).filter((d) => fs25.existsSync(path21.join(dir, d, "SKILL.md"))).sort();
|
|
3716
4244
|
}
|
|
3717
4245
|
function skillTargets() {
|
|
3718
|
-
const home3 =
|
|
3719
|
-
const targets = [
|
|
3720
|
-
if (
|
|
4246
|
+
const home3 = os10.homedir();
|
|
4247
|
+
const targets = [path21.join(home3, ".agents", "skills")];
|
|
4248
|
+
if (fs25.existsSync(path21.join(home3, ".claude"))) targets.push(path21.join(home3, ".claude", "skills"));
|
|
3721
4249
|
return targets;
|
|
3722
4250
|
}
|
|
3723
|
-
var launcherPath = () =>
|
|
4251
|
+
var launcherPath = () => path21.join(os10.homedir(), ".local", "bin", process.platform === "win32" ? "pitroom.cmd" : "pitroom");
|
|
3724
4252
|
var LAUNCHER_MARK = "# pitroom launcher";
|
|
3725
4253
|
function launcherScript(bundle) {
|
|
3726
4254
|
if (process.platform === "win32") {
|
|
@@ -3750,7 +4278,7 @@ exit 127
|
|
|
3750
4278
|
function isOurLauncher(file2, root) {
|
|
3751
4279
|
if (linksInto(file2, root)) return true;
|
|
3752
4280
|
try {
|
|
3753
|
-
const s =
|
|
4281
|
+
const s = fs25.readFileSync(file2, "utf8");
|
|
3754
4282
|
return s.includes(LAUNCHER_MARK) && s.includes(root);
|
|
3755
4283
|
} catch {
|
|
3756
4284
|
return false;
|
|
@@ -3758,64 +4286,64 @@ function isOurLauncher(file2, root) {
|
|
|
3758
4286
|
}
|
|
3759
4287
|
function isPitroomLauncher(file2) {
|
|
3760
4288
|
try {
|
|
3761
|
-
return
|
|
4289
|
+
return fs25.readFileSync(file2, "utf8").includes(LAUNCHER_MARK);
|
|
3762
4290
|
} catch {
|
|
3763
4291
|
return false;
|
|
3764
4292
|
}
|
|
3765
4293
|
}
|
|
3766
4294
|
function placeLauncher(bundle, root, force) {
|
|
3767
4295
|
const dest = launcherPath();
|
|
3768
|
-
|
|
3769
|
-
const exists =
|
|
4296
|
+
fs25.mkdirSync(path21.dirname(dest), { recursive: true });
|
|
4297
|
+
const exists = fs25.lstatSync(dest, { throwIfNoEntry: false });
|
|
3770
4298
|
if (exists && !isOurLauncher(dest, root)) {
|
|
3771
4299
|
if (!force) return `! ${dest} exists and is not Pitroom's launcher; kept (use --force to back it up and replace)`;
|
|
3772
|
-
|
|
4300
|
+
fs25.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
3773
4301
|
} else if (exists) {
|
|
3774
|
-
|
|
4302
|
+
fs25.rmSync(dest, { force: true });
|
|
3775
4303
|
}
|
|
3776
|
-
|
|
4304
|
+
fs25.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
|
|
3777
4305
|
return `\u2714 ${dest} \u2192 launcher for ${bundle} (Node 22.13+)`;
|
|
3778
4306
|
}
|
|
3779
4307
|
function linksInto(link, dir) {
|
|
3780
|
-
const st =
|
|
4308
|
+
const st = fs25.lstatSync(link, { throwIfNoEntry: false });
|
|
3781
4309
|
if (!st?.isSymbolicLink()) return false;
|
|
3782
|
-
const target =
|
|
3783
|
-
const rel =
|
|
3784
|
-
return !rel.startsWith("..") && !
|
|
4310
|
+
const target = path21.resolve(path21.dirname(link), fs25.readlinkSync(link));
|
|
4311
|
+
const rel = path21.relative(dir, target);
|
|
4312
|
+
return !rel.startsWith("..") && !path21.isAbsolute(rel);
|
|
3785
4313
|
}
|
|
3786
4314
|
function place(src, dest, opts) {
|
|
3787
|
-
|
|
3788
|
-
const st =
|
|
4315
|
+
fs25.mkdirSync(path21.dirname(dest), { recursive: true });
|
|
4316
|
+
const st = fs25.lstatSync(dest, { throwIfNoEntry: false });
|
|
3789
4317
|
if (st && !st.isSymbolicLink()) {
|
|
3790
4318
|
if (!opts.force) return `! ${dest} exists and is not a link; kept (use --force to back it up and replace)`;
|
|
3791
|
-
|
|
4319
|
+
fs25.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
3792
4320
|
} else if (st) {
|
|
3793
|
-
|
|
4321
|
+
fs25.unlinkSync(dest);
|
|
3794
4322
|
}
|
|
3795
|
-
if (opts.copy)
|
|
3796
|
-
else
|
|
4323
|
+
if (opts.copy) fs25.cpSync(src, dest, { recursive: true });
|
|
4324
|
+
else fs25.symlinkSync(src, dest, process.platform === "win32" ? "junction" : fs25.statSync(src).isDirectory() ? "dir" : "file");
|
|
3797
4325
|
return `\u2714 ${dest} \u2192 ${opts.copy ? "copied" : src}`;
|
|
3798
4326
|
}
|
|
3799
4327
|
function install(opts) {
|
|
3800
4328
|
const root = packageRoot();
|
|
3801
4329
|
const skills2 = opts.skills === false ? [] : skillNames(root);
|
|
3802
|
-
if (opts.skills !== false && !skills2.length) throw new Error(`no skills found under ${
|
|
4330
|
+
if (opts.skills !== false && !skills2.length) throw new Error(`no skills found under ${path21.join(root, "skills")}`);
|
|
3803
4331
|
const out = [];
|
|
3804
4332
|
for (const base2 of opts.skills === false ? [] : skillTargets()) {
|
|
3805
4333
|
for (const legacy of LEGACY) {
|
|
3806
|
-
const l =
|
|
4334
|
+
const l = path21.join(base2, legacy);
|
|
3807
4335
|
if (!skills2.includes(legacy) && linksInto(l, root)) {
|
|
3808
|
-
|
|
4336
|
+
fs25.unlinkSync(l);
|
|
3809
4337
|
out.push(`\u2714 removed old link ${l}`);
|
|
3810
4338
|
}
|
|
3811
4339
|
}
|
|
3812
|
-
for (const name of skills2) out.push(place(
|
|
4340
|
+
for (const name of skills2) out.push(place(path21.join(root, "skills", name), path21.join(base2, name), opts));
|
|
3813
4341
|
}
|
|
3814
|
-
const bundle =
|
|
3815
|
-
if (
|
|
4342
|
+
const bundle = path21.join(root, "dist", "pitroom.mjs");
|
|
4343
|
+
if (fs25.existsSync(bundle)) {
|
|
3816
4344
|
out.push(placeLauncher(bundle, root, opts.force));
|
|
3817
|
-
const onPath = (process.env.PATH ?? "").split(
|
|
3818
|
-
if (!onPath) out.push(`! ${
|
|
4345
|
+
const onPath = (process.env.PATH ?? "").split(path21.delimiter).some((d) => path21.resolve(d) === path21.dirname(launcherPath()));
|
|
4346
|
+
if (!onPath) out.push(`! ${path21.dirname(launcherPath())} is not on PATH; add it, or run ${launcherPath()} directly`);
|
|
3819
4347
|
}
|
|
3820
4348
|
return out;
|
|
3821
4349
|
}
|
|
@@ -3823,24 +4351,24 @@ function uninstall() {
|
|
|
3823
4351
|
const root = packageRoot();
|
|
3824
4352
|
const out = [];
|
|
3825
4353
|
for (const base2 of skillTargets()) {
|
|
3826
|
-
if (!
|
|
3827
|
-
for (const name of
|
|
3828
|
-
const l =
|
|
4354
|
+
if (!fs25.existsSync(base2)) continue;
|
|
4355
|
+
for (const name of fs25.readdirSync(base2)) {
|
|
4356
|
+
const l = path21.join(base2, name);
|
|
3829
4357
|
if (linksInto(l, root)) {
|
|
3830
|
-
|
|
4358
|
+
fs25.unlinkSync(l);
|
|
3831
4359
|
out.push(`\u2714 removed ${l}`);
|
|
3832
4360
|
}
|
|
3833
4361
|
}
|
|
3834
4362
|
}
|
|
3835
4363
|
if (isOurLauncher(launcherPath(), root)) {
|
|
3836
|
-
|
|
4364
|
+
fs25.rmSync(launcherPath(), { force: true });
|
|
3837
4365
|
out.push(`\u2714 removed ${launcherPath()}`);
|
|
3838
4366
|
}
|
|
3839
4367
|
return out.length ? out : ["nothing to remove"];
|
|
3840
4368
|
}
|
|
3841
4369
|
function installedSkills() {
|
|
3842
4370
|
const names = skillNames();
|
|
3843
|
-
return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) =>
|
|
4371
|
+
return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs25.existsSync(path21.join(base2, n, "SKILL.md"))) }));
|
|
3844
4372
|
}
|
|
3845
4373
|
|
|
3846
4374
|
// src/core/templates.ts
|
|
@@ -3851,9 +4379,9 @@ var FILES = {
|
|
|
3851
4379
|
"code-reviewer": "pitroom-review/code-reviewer.md"
|
|
3852
4380
|
};
|
|
3853
4381
|
function loadTemplate(name, root = packageRoot()) {
|
|
3854
|
-
const file2 =
|
|
3855
|
-
if (!
|
|
3856
|
-
return
|
|
4382
|
+
const file2 = path22.join(root, "skills", FILES[name]);
|
|
4383
|
+
if (!fs26.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
|
|
4384
|
+
return fs26.readFileSync(file2, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
|
|
3857
4385
|
}
|
|
3858
4386
|
function fill(template, values) {
|
|
3859
4387
|
const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
|
|
@@ -3863,10 +4391,10 @@ function fill(template, values) {
|
|
|
3863
4391
|
|
|
3864
4392
|
// src/core/slots.ts
|
|
3865
4393
|
import crypto6 from "node:crypto";
|
|
3866
|
-
import
|
|
3867
|
-
import
|
|
3868
|
-
var slotsDir = () =>
|
|
3869
|
-
var locksDir = () =>
|
|
4394
|
+
import fs27 from "node:fs";
|
|
4395
|
+
import path23 from "node:path";
|
|
4396
|
+
var slotsDir = () => path23.join(home(), "slots");
|
|
4397
|
+
var locksDir = () => path23.join(home(), "locks");
|
|
3870
4398
|
var STARTUP_GRACE_MS = 3e4;
|
|
3871
4399
|
function holderActive(runId, self) {
|
|
3872
4400
|
if (!runId || runId === self) return false;
|
|
@@ -3880,23 +4408,23 @@ function holderActive(runId, self) {
|
|
|
3880
4408
|
}
|
|
3881
4409
|
}
|
|
3882
4410
|
function tryClaim(file2, runId) {
|
|
3883
|
-
|
|
4411
|
+
fs27.mkdirSync(path23.dirname(file2), { recursive: true });
|
|
3884
4412
|
try {
|
|
3885
|
-
|
|
4413
|
+
fs27.writeFileSync(file2, runId, { flag: "wx" });
|
|
3886
4414
|
return true;
|
|
3887
4415
|
} catch (e) {
|
|
3888
4416
|
if (e.code !== "EEXIST") throw e;
|
|
3889
4417
|
}
|
|
3890
4418
|
let owner = "";
|
|
3891
4419
|
try {
|
|
3892
|
-
owner =
|
|
4420
|
+
owner = fs27.readFileSync(file2, "utf8").trim();
|
|
3893
4421
|
} catch {
|
|
3894
4422
|
}
|
|
3895
4423
|
if (owner === runId) return true;
|
|
3896
4424
|
if (holderActive(owner, runId)) return false;
|
|
3897
|
-
|
|
4425
|
+
fs27.rmSync(file2, { force: true });
|
|
3898
4426
|
try {
|
|
3899
|
-
|
|
4427
|
+
fs27.writeFileSync(file2, runId, { flag: "wx" });
|
|
3900
4428
|
return true;
|
|
3901
4429
|
} catch {
|
|
3902
4430
|
return false;
|
|
@@ -3905,25 +4433,25 @@ function tryClaim(file2, runId) {
|
|
|
3905
4433
|
function releaseIfOwner(file2, runId) {
|
|
3906
4434
|
if (!file2) return;
|
|
3907
4435
|
try {
|
|
3908
|
-
if (
|
|
4436
|
+
if (fs27.readFileSync(file2, "utf8").trim() === runId) fs27.rmSync(file2, { force: true });
|
|
3909
4437
|
} catch {
|
|
3910
4438
|
}
|
|
3911
4439
|
}
|
|
3912
4440
|
function tryAcquireSlot(runId, maxParallel) {
|
|
3913
4441
|
for (let n = 0; n < Math.max(1, maxParallel); n++) {
|
|
3914
|
-
const file2 =
|
|
4442
|
+
const file2 = path23.join(slotsDir(), `slot-${n}`);
|
|
3915
4443
|
if (tryClaim(file2, runId)) return file2;
|
|
3916
4444
|
}
|
|
3917
4445
|
return void 0;
|
|
3918
4446
|
}
|
|
3919
4447
|
var releaseSlot = (file2, runId) => releaseIfOwner(file2, runId);
|
|
3920
|
-
var lockFile = (repoRoot2) =>
|
|
4448
|
+
var lockFile = (repoRoot2) => path23.join(locksDir(), `write-${crypto6.createHash("sha1").update(path23.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
|
|
3921
4449
|
function acquireWriteLock(repoRoot2, runId) {
|
|
3922
4450
|
const file2 = lockFile(repoRoot2);
|
|
3923
4451
|
if (tryClaim(file2, runId)) return;
|
|
3924
4452
|
let owner = "";
|
|
3925
4453
|
try {
|
|
3926
|
-
owner =
|
|
4454
|
+
owner = fs27.readFileSync(file2, "utf8").trim();
|
|
3927
4455
|
} catch {
|
|
3928
4456
|
}
|
|
3929
4457
|
throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
|
|
@@ -3943,35 +4471,38 @@ var RULES = {
|
|
|
3943
4471
|
"Run the relevant tests, type checks or linters if they are cheap and available, and report the result."
|
|
3944
4472
|
]
|
|
3945
4473
|
};
|
|
3946
|
-
|
|
4474
|
+
var COMPLETE = "When the task asks for all of something, a list or a count: search the whole scope (e.g. a project-wide grep) instead of stopping at the first hits, give the complete list or the exact count, and say how you found it. If you could not cover everything, say what is left out under OPEN ISSUES.";
|
|
4475
|
+
function buildPrompt(mode, task, followUp, audit = false) {
|
|
3947
4476
|
if (followUp) return `Follow-up from the primary agent. Same rules and answer format as before.
|
|
3948
4477
|
|
|
3949
4478
|
${task}`;
|
|
4479
|
+
const rules = mode === "read" && !audit ? [...RULES[mode], COMPLETE] : RULES[mode];
|
|
4480
|
+
const economy = audit ? "" : "\n- Be economical: open only what you need, but do not cut a search short when the task asks for a complete list or a count.";
|
|
4481
|
+
const format = audit ? "End with the reply form the task gives, and nothing else." : `End with this answer format (plain text, no preamble):
|
|
4482
|
+
SUMMARY: 1-5 lines that directly answer the task.
|
|
4483
|
+
DETAILS: key findings with file:line references; only what the primary agent needs, but a list or count the task asks for goes here in full.
|
|
4484
|
+
FILES CHANGED: paths, or "none".
|
|
4485
|
+
VERIFICATION: commands you ran (searches included) and their results, or "not run".
|
|
4486
|
+
OPEN ISSUES: risks, uncertainties, follow-ups, or "none".`;
|
|
3950
4487
|
return `You are a worker agent. A primary coding agent delegated this bounded task to you. Your answer is draft work that the primary agent will verify, and it is the only thing the primary sees, so make it self-contained.
|
|
3951
4488
|
|
|
3952
4489
|
Rules:
|
|
3953
|
-
- ${
|
|
4490
|
+
- ${rules.join("\n- ")}
|
|
3954
4491
|
- Stay inside the task scope and the project directory.
|
|
3955
4492
|
- Never commit, push, reset, checkout, restore, stash, clean, rebase or merge, and never discard or overwrite uncommitted work you did not create.
|
|
3956
4493
|
- Never read or reveal secrets (.env files, keys, tokens).
|
|
3957
4494
|
- Delete a file only when the task explicitly asks for it, and name every file you delete under FILES CHANGED. Whether anything else should be deleted or overwritten is the primary agent's decision: propose it under OPEN ISSUES and leave the file alone.
|
|
3958
|
-
- Do not ask questions. If something is ambiguous, choose the safest reasonable interpretation and state the assumption
|
|
3959
|
-
- Be economical: open only what you need.
|
|
4495
|
+
- Do not ask questions. If something is ambiguous, choose the safest reasonable interpretation and state the assumption.${economy}
|
|
3960
4496
|
|
|
3961
|
-
|
|
3962
|
-
SUMMARY: 1-5 lines that directly answer the task.
|
|
3963
|
-
DETAILS: key findings with file:line references; only what the primary agent needs.
|
|
3964
|
-
FILES CHANGED: paths, or "none".
|
|
3965
|
-
VERIFICATION: commands you ran and their results, or "not run".
|
|
3966
|
-
OPEN ISSUES: risks, uncertainties, follow-ups, or "none".
|
|
4497
|
+
${format}
|
|
3967
4498
|
|
|
3968
4499
|
Task:
|
|
3969
4500
|
${task}`;
|
|
3970
4501
|
}
|
|
3971
4502
|
|
|
3972
4503
|
// src/core/refs.ts
|
|
3973
|
-
import
|
|
3974
|
-
import
|
|
4504
|
+
import fs28 from "node:fs";
|
|
4505
|
+
import path24 from "node:path";
|
|
3975
4506
|
var FILE_EXTENSIONS = new Set(
|
|
3976
4507
|
"ts tsx mts cts js jsx mjs cjs json jsonc json5 md mdx txt rst py pyi rb go rs java kt kts swift c h cc cpp cxx hpp hh cs fs php lua r jl sh bash zsh fish ps1 bat yml yaml toml ini cfg conf env html htm css scss sass less vue svelte astro sql graphql gql proto lock xml svg csv tsv gradle tf hcl dart ex exs erl hs ml scala clj vim el mk cmake dockerfile gitignore gitattributes editorconfig npmrc nvmrc".split(" ")
|
|
3977
4508
|
);
|
|
@@ -4022,13 +4553,13 @@ function extractRefs(answer) {
|
|
|
4022
4553
|
return [...seen.values()];
|
|
4023
4554
|
}
|
|
4024
4555
|
function verifyRefs(refs, dirs) {
|
|
4025
|
-
const roots = [...new Set(dirs.filter(Boolean).map((d) =>
|
|
4556
|
+
const roots = [...new Set(dirs.filter(Boolean).map((d) => path24.resolve(d)))];
|
|
4026
4557
|
const invalid = [];
|
|
4027
4558
|
const lineCache = /* @__PURE__ */ new Map();
|
|
4028
4559
|
const load = (file2) => {
|
|
4029
4560
|
if (!lineCache.has(file2)) {
|
|
4030
|
-
const size =
|
|
4031
|
-
const lines = size > MAX_BYTES ? null :
|
|
4561
|
+
const size = fs28.statSync(file2).size;
|
|
4562
|
+
const lines = size > MAX_BYTES ? null : fs28.readFileSync(file2, "utf8").split("\n");
|
|
4032
4563
|
if (lines && lines[lines.length - 1] === "") lines.pop();
|
|
4033
4564
|
lineCache.set(file2, lines);
|
|
4034
4565
|
}
|
|
@@ -4039,9 +4570,9 @@ function verifyRefs(refs, dirs) {
|
|
|
4039
4570
|
const candidates = (ref) => {
|
|
4040
4571
|
const direct = resolve(ref.file, roots);
|
|
4041
4572
|
if (direct) return [direct];
|
|
4042
|
-
if (
|
|
4573
|
+
if (path24.isAbsolute(ref.file)) return [];
|
|
4043
4574
|
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
4044
|
-
return (byName().get(
|
|
4575
|
+
return (byName().get(path24.basename(wanted)) ?? []).filter((f) => f.split(path24.sep).join("/").endsWith(`/${wanted}`) || path24.basename(f) === wanted).slice(0, 20);
|
|
4045
4576
|
};
|
|
4046
4577
|
const check = (file2, ref) => {
|
|
4047
4578
|
const lines = load(file2);
|
|
@@ -4056,7 +4587,7 @@ function verifyRefs(refs, dirs) {
|
|
|
4056
4587
|
if (!files.length) {
|
|
4057
4588
|
if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
|
|
4058
4589
|
total++;
|
|
4059
|
-
invalid.push({ ref: ref.text, reason:
|
|
4590
|
+
invalid.push({ ref: ref.text, reason: path24.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
|
|
4060
4591
|
continue;
|
|
4061
4592
|
}
|
|
4062
4593
|
total++;
|
|
@@ -4077,7 +4608,7 @@ function enclosedBy(lines, ref) {
|
|
|
4077
4608
|
return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
|
|
4078
4609
|
}
|
|
4079
4610
|
var isFileName = (name) => {
|
|
4080
|
-
const base2 =
|
|
4611
|
+
const base2 = path24.basename(name);
|
|
4081
4612
|
return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
|
|
4082
4613
|
};
|
|
4083
4614
|
function indexFiles(roots) {
|
|
@@ -4086,19 +4617,19 @@ function indexFiles(roots) {
|
|
|
4086
4617
|
const walk = (dir) => {
|
|
4087
4618
|
let entries;
|
|
4088
4619
|
try {
|
|
4089
|
-
entries =
|
|
4620
|
+
entries = fs28.readdirSync(dir, { withFileTypes: true });
|
|
4090
4621
|
} catch {
|
|
4091
4622
|
return;
|
|
4092
4623
|
}
|
|
4093
4624
|
for (const e of entries) {
|
|
4094
4625
|
if (count >= MAX_INDEXED) return;
|
|
4095
4626
|
if (e.isDirectory()) {
|
|
4096
|
-
if (!SKIP_DIRS2.has(e.name)) walk(
|
|
4627
|
+
if (!SKIP_DIRS2.has(e.name)) walk(path24.join(dir, e.name));
|
|
4097
4628
|
} else if (e.isFile()) {
|
|
4098
4629
|
count++;
|
|
4099
4630
|
const list2 = index.get(e.name);
|
|
4100
|
-
if (list2) list2.push(
|
|
4101
|
-
else index.set(e.name, [
|
|
4631
|
+
if (list2) list2.push(path24.join(dir, e.name));
|
|
4632
|
+
else index.set(e.name, [path24.join(dir, e.name)]);
|
|
4102
4633
|
}
|
|
4103
4634
|
}
|
|
4104
4635
|
};
|
|
@@ -4106,10 +4637,10 @@ function indexFiles(roots) {
|
|
|
4106
4637
|
return index;
|
|
4107
4638
|
}
|
|
4108
4639
|
function resolve(ref, roots) {
|
|
4109
|
-
const candidates =
|
|
4640
|
+
const candidates = path24.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path24.join(r, ref));
|
|
4110
4641
|
return candidates.find((c) => {
|
|
4111
4642
|
try {
|
|
4112
|
-
return
|
|
4643
|
+
return fs28.statSync(c).isFile() && inside(c, roots);
|
|
4113
4644
|
} catch {
|
|
4114
4645
|
return false;
|
|
4115
4646
|
}
|
|
@@ -4117,14 +4648,14 @@ function resolve(ref, roots) {
|
|
|
4117
4648
|
}
|
|
4118
4649
|
function inside(file2, roots) {
|
|
4119
4650
|
return roots.some((r) => {
|
|
4120
|
-
const rel =
|
|
4121
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
4651
|
+
const rel = path24.relative(r, path24.resolve(file2));
|
|
4652
|
+
return rel !== "" && !rel.startsWith("..") && !path24.isAbsolute(rel);
|
|
4122
4653
|
});
|
|
4123
4654
|
}
|
|
4124
4655
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4125
4656
|
|
|
4126
4657
|
// src/core/run.ts
|
|
4127
|
-
var VERSION2 = true ? "0.
|
|
4658
|
+
var VERSION2 = true ? "0.23.0" : "0.0.0-dev";
|
|
4128
4659
|
function planWork(o) {
|
|
4129
4660
|
if (!o.plan) return { task: o.task.trim(), tier: o.tier };
|
|
4130
4661
|
if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
|
|
@@ -4156,7 +4687,7 @@ function prepareRun(o) {
|
|
|
4156
4687
|
parent = readMeta(o.continueFrom);
|
|
4157
4688
|
if (o.worker || o.tier || o.effort) throw new UserError("a follow-up runs on the same worker as its parent; drop --worker/--tier/--effort");
|
|
4158
4689
|
if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
|
|
4159
|
-
if (parent.mode === "isolate" && (!parent.worktree || !
|
|
4690
|
+
if (parent.mode === "isolate" && (!parent.worktree || !fs29.existsSync(parent.worktree))) {
|
|
4160
4691
|
throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
|
|
4161
4692
|
}
|
|
4162
4693
|
const ran = parent.ran ?? parent.worker;
|
|
@@ -4166,7 +4697,16 @@ function prepareRun(o) {
|
|
|
4166
4697
|
const chain = resolveChain({ worker: o.worker, model: o.model, tier: work.tier, effort: o.effort, noFallback: o.noFallback });
|
|
4167
4698
|
({ worker, fallback } = chain);
|
|
4168
4699
|
warnings.push(...chain.warnings);
|
|
4700
|
+
if (effective().rankWorkers.value && !o.worker && !o.model && !work.tier && fallback.length) {
|
|
4701
|
+
const since2 = Date.now() - 30 * 864e5;
|
|
4702
|
+
const ranked = rankChain([worker, ...fallback], (t) => workerRecord(t.backend, t.model, since2));
|
|
4703
|
+
[worker, ...fallback] = ranked.chain;
|
|
4704
|
+
if (ranked.note) warnings.push(ranked.note);
|
|
4705
|
+
}
|
|
4169
4706
|
}
|
|
4707
|
+
const budget = withinBudget([worker, ...fallback]);
|
|
4708
|
+
[worker, ...fallback] = budget.chain;
|
|
4709
|
+
if (budget.warning) warnings.push(budget.warning);
|
|
4170
4710
|
const backend = getBackend(worker.backend);
|
|
4171
4711
|
if (parent && backend.capabilities.resume === "none") {
|
|
4172
4712
|
throw new UserError(`the ${backend.name} worker cannot continue sessions`, 3);
|
|
@@ -4175,18 +4715,18 @@ function prepareRun(o) {
|
|
|
4175
4715
|
throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
|
|
4176
4716
|
}
|
|
4177
4717
|
const mode = parent?.mode ?? o.mode;
|
|
4178
|
-
const dir = canonical(
|
|
4179
|
-
if (!
|
|
4718
|
+
const dir = canonical(path25.resolve(parent?.dir ?? o.dir));
|
|
4719
|
+
if (!fs29.existsSync(dir) || !fs29.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
|
|
4180
4720
|
const root = repoRoot(dir);
|
|
4181
4721
|
if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
|
|
4182
4722
|
if (mode === "write" && !root && !o.allowNonGit) {
|
|
4183
4723
|
throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
|
|
4184
4724
|
}
|
|
4185
|
-
const files = o.files.map((f) =>
|
|
4186
|
-
for (const f of files) if (!
|
|
4725
|
+
const files = o.files.map((f) => path25.resolve(f));
|
|
4726
|
+
for (const f of files) if (!fs29.existsSync(f)) throw new UserError(`file not found: ${f}`);
|
|
4187
4727
|
const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
|
|
4188
4728
|
const snap = mode === "read" && !o.review && !o.inPlace && !parent ? wantSnapshot(readIn2, root, dir) : void 0;
|
|
4189
|
-
if (parent?.snapshot?.dir && !
|
|
4729
|
+
if (parent?.snapshot?.dir && !fs29.existsSync(parent.snapshot.dir)) {
|
|
4190
4730
|
throw new UserError(`the read snapshot of run ${parent.id} is gone (cleaned up); ask the question again as a new run`, 3);
|
|
4191
4731
|
}
|
|
4192
4732
|
if (!parent && !snap && !process.env.PITROOM_NO_SECRET_WARNING) {
|
|
@@ -4220,6 +4760,7 @@ function prepareRun(o) {
|
|
|
4220
4760
|
worktree: parent?.mode === "isolate" ? parent.worktree : void 0,
|
|
4221
4761
|
reviewOf: o.review?.of,
|
|
4222
4762
|
auditOf: o.audit?.of,
|
|
4763
|
+
fixOf: o.fix?.of,
|
|
4223
4764
|
auditRate: o.auditRate,
|
|
4224
4765
|
inPlace: o.inPlace || void 0,
|
|
4225
4766
|
cache: o.cache,
|
|
@@ -4233,13 +4774,13 @@ function prepareRun(o) {
|
|
|
4233
4774
|
try {
|
|
4234
4775
|
acquireWriteLock(root, meta.id);
|
|
4235
4776
|
} catch (e) {
|
|
4236
|
-
|
|
4777
|
+
fs29.rmSync(runDir(meta.id), { recursive: true, force: true });
|
|
4237
4778
|
throw e;
|
|
4238
4779
|
}
|
|
4239
4780
|
}
|
|
4240
|
-
|
|
4781
|
+
fs29.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
|
|
4241
4782
|
`);
|
|
4242
|
-
if (work.brief)
|
|
4783
|
+
if (work.brief) fs29.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
|
|
4243
4784
|
return meta;
|
|
4244
4785
|
}
|
|
4245
4786
|
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -4321,7 +4862,7 @@ async function execute(meta) {
|
|
|
4321
4862
|
(meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
|
|
4322
4863
|
for (const f of ["events.jsonl", "stderr.log"]) {
|
|
4323
4864
|
const from = runFile(meta.id, f);
|
|
4324
|
-
if (
|
|
4865
|
+
if (fs29.existsSync(from)) fs29.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
|
|
4325
4866
|
}
|
|
4326
4867
|
writeMeta(meta);
|
|
4327
4868
|
}
|
|
@@ -4340,7 +4881,18 @@ async function execute(meta) {
|
|
|
4340
4881
|
}
|
|
4341
4882
|
function autoAudit(meta) {
|
|
4342
4883
|
try {
|
|
4343
|
-
if (!auditable(meta, read2(runFile(meta.id, "summary.md")))
|
|
4884
|
+
if (!auditable(meta, read2(runFile(meta.id, "summary.md")))) return;
|
|
4885
|
+
let rate2 = auditRate(meta);
|
|
4886
|
+
if (meta.auditRate === void 0 && effective().auditFocus.value) {
|
|
4887
|
+
const ran = meta.ran ?? meta.worker;
|
|
4888
|
+
const focus = focusedRate(rate2, meta.task, workerRecord(ran.backend, meta.resolvedModel ?? ran.model, Date.now() - 30 * 864e5));
|
|
4889
|
+
rate2 = focus.rate;
|
|
4890
|
+
if (focus.why.length) {
|
|
4891
|
+
meta.auditFocus = { rate: rate2, why: focus.why };
|
|
4892
|
+
writeMeta(meta);
|
|
4893
|
+
}
|
|
4894
|
+
}
|
|
4895
|
+
if (!sampled(meta, rate2)) return;
|
|
4344
4896
|
startAudit(meta);
|
|
4345
4897
|
} catch {
|
|
4346
4898
|
}
|
|
@@ -4374,6 +4926,30 @@ function settleAudit(a) {
|
|
|
4374
4926
|
const target = readMeta(a.auditOf);
|
|
4375
4927
|
target.audit = { id: a.id, state: a.state, verdict: a.state === "done" ? a.auditVerdict : void 0, disputed: a.state === "done" ? a.auditDisputed : void 0 };
|
|
4376
4928
|
writeMeta(target);
|
|
4929
|
+
startFix(target);
|
|
4930
|
+
} catch {
|
|
4931
|
+
}
|
|
4932
|
+
}
|
|
4933
|
+
function startFix(target) {
|
|
4934
|
+
try {
|
|
4935
|
+
const ran = target.ran ?? target.worker;
|
|
4936
|
+
if (!wantsFix(target, getBackend(ran.backend).capabilities.resume !== "none")) return;
|
|
4937
|
+
const f = prepareRun({
|
|
4938
|
+
mode: target.mode,
|
|
4939
|
+
task: fixTask(target.audit.disputed),
|
|
4940
|
+
dir: target.dir,
|
|
4941
|
+
files: [],
|
|
4942
|
+
link: [],
|
|
4943
|
+
timeoutSec: target.timeoutSec,
|
|
4944
|
+
continueFrom: target.id,
|
|
4945
|
+
allowNonGit: true,
|
|
4946
|
+
web: false,
|
|
4947
|
+
noFallback: false,
|
|
4948
|
+
fix: { of: target.id }
|
|
4949
|
+
});
|
|
4950
|
+
target.audit = { ...target.audit, fix: f.id };
|
|
4951
|
+
writeMeta(target);
|
|
4952
|
+
startInBackground(f);
|
|
4377
4953
|
} catch {
|
|
4378
4954
|
}
|
|
4379
4955
|
}
|
|
@@ -4385,8 +4961,8 @@ function prepareTree(meta) {
|
|
|
4385
4961
|
const s = readSnapshot(root);
|
|
4386
4962
|
meta.snapshot.dir = s.dir;
|
|
4387
4963
|
meta.snapshot.tree = s.tree;
|
|
4388
|
-
meta.cwd =
|
|
4389
|
-
if (!
|
|
4964
|
+
meta.cwd = path25.join(s.dir, path25.relative(root, meta.dir));
|
|
4965
|
+
if (!fs29.existsSync(meta.cwd)) meta.cwd = s.dir;
|
|
4390
4966
|
if (shown) meta.warnings.push(`read in a clean snapshot of your project: the secret-looking files (${shown}) and git-ignored files are not in it; --in-place reads the directory itself`);
|
|
4391
4967
|
} catch (e) {
|
|
4392
4968
|
meta.snapshot = void 0;
|
|
@@ -4396,9 +4972,9 @@ function prepareTree(meta) {
|
|
|
4396
4972
|
if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
|
|
4397
4973
|
if (root && meta.mode === "isolate" && !meta.worktree) {
|
|
4398
4974
|
meta.baseTree = snapshotTree(root);
|
|
4399
|
-
meta.worktree =
|
|
4975
|
+
meta.worktree = path25.join(worktreesDir(), meta.id);
|
|
4400
4976
|
createIsolatedCopy(root, meta.baseTree, meta.worktree);
|
|
4401
|
-
meta.cwd =
|
|
4977
|
+
meta.cwd = path25.join(meta.worktree, path25.relative(root, meta.dir));
|
|
4402
4978
|
const linked = linkIntoWorktree(root, meta.worktree, meta.link);
|
|
4403
4979
|
if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
|
|
4404
4980
|
}
|
|
@@ -4406,7 +4982,7 @@ function prepareTree(meta) {
|
|
|
4406
4982
|
function attempt(meta, backend, target) {
|
|
4407
4983
|
const inv = backend.invocation({
|
|
4408
4984
|
mode: meta.mode,
|
|
4409
|
-
prompt: buildPrompt(meta.mode, meta.task, !!meta.parent),
|
|
4985
|
+
prompt: buildPrompt(meta.mode, meta.task, !!meta.parent, !!meta.auditOf),
|
|
4410
4986
|
cwd: meta.cwd,
|
|
4411
4987
|
model: target.model,
|
|
4412
4988
|
sessionId: meta.sessionId,
|
|
@@ -4421,7 +4997,7 @@ function attempt(meta, backend, target) {
|
|
|
4421
4997
|
timeoutSec: meta.timeoutSec
|
|
4422
4998
|
});
|
|
4423
4999
|
}
|
|
4424
|
-
var read2 = (f) =>
|
|
5000
|
+
var read2 = (f) => fs29.existsSync(f) ? fs29.readFileSync(f, "utf8") : "";
|
|
4425
5001
|
function retryableFailure(meta, backend, res) {
|
|
4426
5002
|
const run2 = backend.parse(read2(runFile(meta.id, "events.jsonl")));
|
|
4427
5003
|
if (run2.usage.steps > 0) return void 0;
|
|
@@ -4441,12 +5017,12 @@ function finalize(meta, res) {
|
|
|
4441
5017
|
if (meta.snapshot?.dir && meta.repoRoot) run2.finalText = backToProject(run2.finalText, meta.snapshot.dir, meta.repoRoot);
|
|
4442
5018
|
meta.sessionId = run2.sessionId ?? meta.sessionId;
|
|
4443
5019
|
meta.usage = run2.usage;
|
|
4444
|
-
|
|
5020
|
+
fs29.writeFileSync(runFile(meta.id, "summary.md"), `${run2.finalText}
|
|
4445
5021
|
`);
|
|
4446
5022
|
if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run2.finalText);
|
|
4447
5023
|
if (meta.reviewOf) {
|
|
4448
5024
|
meta.verdict = parseVerdict(run2.finalText);
|
|
4449
|
-
if (meta.packageFile)
|
|
5025
|
+
if (meta.packageFile) fs29.rmSync(meta.packageFile, { force: true });
|
|
4450
5026
|
}
|
|
4451
5027
|
if (!res.timedOut && !res.stopped && !meta.error) {
|
|
4452
5028
|
const f = backend.failure(run2, read2(runFile(meta.id, "stderr.log")), res.code);
|
|
@@ -4506,16 +5082,16 @@ function captureChanges(meta) {
|
|
|
4506
5082
|
const d = diffTrees(target, meta.baseTree, meta.afterTree);
|
|
4507
5083
|
meta.changes = d.changes;
|
|
4508
5084
|
meta.stats = d.stats;
|
|
4509
|
-
|
|
5085
|
+
fs29.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
|
|
4510
5086
|
}
|
|
4511
5087
|
function verifyPath() {
|
|
4512
|
-
const extra = [
|
|
4513
|
-
const parts = (process.env.PATH ?? "").split(
|
|
4514
|
-
for (const d of process.platform === "win32" ? [
|
|
4515
|
-
return parts.join(
|
|
5088
|
+
const extra = [path25.dirname(process.execPath), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
|
|
5089
|
+
const parts = (process.env.PATH ?? "").split(path25.delimiter).filter(Boolean);
|
|
5090
|
+
for (const d of process.platform === "win32" ? [path25.dirname(process.execPath)] : extra) if (!parts.includes(d)) parts.push(d);
|
|
5091
|
+
return parts.join(path25.delimiter);
|
|
4516
5092
|
}
|
|
4517
5093
|
function runVerify(meta) {
|
|
4518
|
-
const r =
|
|
5094
|
+
const r = spawnSync8(meta.verify, {
|
|
4519
5095
|
cwd: meta.cwd,
|
|
4520
5096
|
shell: true,
|
|
4521
5097
|
encoding: "utf8",
|
|
@@ -4525,7 +5101,7 @@ function runVerify(meta) {
|
|
|
4525
5101
|
env: { ...process.env, PATH: verifyPath(), PWD: meta.cwd, PITROOM_ACTIVE: "1" }
|
|
4526
5102
|
});
|
|
4527
5103
|
const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
|
4528
|
-
|
|
5104
|
+
fs29.writeFileSync(runFile(meta.id, "verify.log"), output);
|
|
4529
5105
|
const notFound = process.platform === "win32" && r.status === 1 && /is not recognized as an internal or external command/.test(output);
|
|
4530
5106
|
return { ok: r.status === 0, code: notFound ? 127 : r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
|
|
4531
5107
|
}
|
|
@@ -4580,12 +5156,12 @@ function discardRun(meta) {
|
|
|
4580
5156
|
return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
|
|
4581
5157
|
}
|
|
4582
5158
|
function cleanupWorktree(meta) {
|
|
4583
|
-
if (meta.worktree &&
|
|
5159
|
+
if (meta.worktree && fs29.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
|
|
4584
5160
|
}
|
|
4585
5161
|
|
|
4586
5162
|
// src/cli/mcp-extras.ts
|
|
4587
|
-
import
|
|
4588
|
-
import
|
|
5163
|
+
import fs30 from "node:fs";
|
|
5164
|
+
import path26 from "node:path";
|
|
4589
5165
|
|
|
4590
5166
|
// src/cli/mcp-support.ts
|
|
4591
5167
|
import { spawn as spawn5 } from "node:child_process";
|
|
@@ -4650,7 +5226,7 @@ function workerFlags(a, only) {
|
|
|
4650
5226
|
return out;
|
|
4651
5227
|
}
|
|
4652
5228
|
var strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
4653
|
-
var
|
|
5229
|
+
var clip7 = (s) => s.length > MAX_OUTPUT ? `${s.slice(0, MAX_OUTPUT)}
|
|
4654
5230
|
\u2026 (${s.length - MAX_OUTPUT} more characters)` : s;
|
|
4655
5231
|
function pit(args, timeoutMs = 60 * 6e4, signal) {
|
|
4656
5232
|
return new Promise((resolve2) => {
|
|
@@ -4675,10 +5251,10 @@ function pit(args, timeoutMs = 60 * 6e4, signal) {
|
|
|
4675
5251
|
}
|
|
4676
5252
|
function asResult(r, still) {
|
|
4677
5253
|
const text = [r.out, r.err && r.code !== 0 ? r.err : ""].filter(Boolean).join("\n\n") || "(no output)";
|
|
4678
|
-
if (r.code === 75 && still) return { text:
|
|
5254
|
+
if (r.code === 75 && still) return { text: clip7(`${text}
|
|
4679
5255
|
|
|
4680
5256
|
${still}`) };
|
|
4681
|
-
return { text:
|
|
5257
|
+
return { text: clip7(text), isError: r.code === 2 || r.code === 3 || r.code === 5 || r.code === 6 || r.code === 1 && !r.out };
|
|
4682
5258
|
}
|
|
4683
5259
|
var plain = async (args, ctx, timeoutMs = 6e4) => asResult(await pit(args, timeoutMs, ctx.signal));
|
|
4684
5260
|
function trackProgress(ctx, ids) {
|
|
@@ -4793,7 +5369,7 @@ var RpcError = class extends Error {
|
|
|
4793
5369
|
var RUN = /^pitroom:\/\/run\/(\d{8}-\d{6}-[0-9a-f]{4})(\/patch)?$/;
|
|
4794
5370
|
var LISTED = 30;
|
|
4795
5371
|
var runOfUri = (uri) => RUN.exec(uri)?.[1];
|
|
4796
|
-
var
|
|
5372
|
+
var oneLine7 = (text, n) => {
|
|
4797
5373
|
const flat = text.replace(/\s+/g, " ").trim();
|
|
4798
5374
|
return flat.length > n ? `${flat.slice(0, n - 1)}\u2026` : flat;
|
|
4799
5375
|
};
|
|
@@ -4803,8 +5379,8 @@ function listResources() {
|
|
|
4803
5379
|
try {
|
|
4804
5380
|
const m = freshMeta(id);
|
|
4805
5381
|
const what2 = `${m.state} \xB7 ${m.mode} \xB7 ${m.worker.backend}${m.worker.model ? `:${m.worker.model.split("/").pop()}` : ""}`;
|
|
4806
|
-
resources.push({ uri: `pitroom://run/${id}`, name: `run ${id}`, title:
|
|
4807
|
-
if (m.changes?.length) resources.push({ uri: `pitroom://run/${id}/patch`, name: `patch ${id}`, title: `Patch of ${
|
|
5382
|
+
resources.push({ uri: `pitroom://run/${id}`, name: `run ${id}`, title: oneLine7(m.task, 70), description: `${what2}: the report`, mimeType: "text/plain" });
|
|
5383
|
+
if (m.changes?.length) resources.push({ uri: `pitroom://run/${id}/patch`, name: `patch ${id}`, title: `Patch of ${oneLine7(m.task, 60)}`, description: `${what2}: the exact diff, ${m.changes.length} file(s)`, mimeType: "text/x-diff" });
|
|
4808
5384
|
} catch {
|
|
4809
5385
|
}
|
|
4810
5386
|
}
|
|
@@ -4822,7 +5398,7 @@ async function readResource(uri, ctx) {
|
|
|
4822
5398
|
const patch = m[2] !== void 0;
|
|
4823
5399
|
const r = await pit(["show", m[1], patch ? "--patch" : "--full"], 6e4, ctx.signal);
|
|
4824
5400
|
if (r.code !== 0) throw new RpcError(r.code === 3 || r.code === 2 ? -32002 : -32603, r.err || r.out || `run ${m[1]} cannot be read`);
|
|
4825
|
-
return { contents: [{ uri, mimeType: patch ? "text/x-diff" : "text/plain", text:
|
|
5401
|
+
return { contents: [{ uri, mimeType: patch ? "text/x-diff" : "text/plain", text: clip7(r.out) }] };
|
|
4826
5402
|
}
|
|
4827
5403
|
var PROMPTS = [
|
|
4828
5404
|
{
|
|
@@ -4870,7 +5446,7 @@ function skillPrompts() {
|
|
|
4870
5446
|
const root = packageRoot();
|
|
4871
5447
|
for (const name of skillNames(root)) {
|
|
4872
5448
|
try {
|
|
4873
|
-
const text =
|
|
5449
|
+
const text = fs30.readFileSync(path26.join(root, "skills", name, "SKILL.md"), "utf8");
|
|
4874
5450
|
const front = /^---\n([\s\S]*?)\n---\n/.exec(text);
|
|
4875
5451
|
const body = (front ? text.slice(front[0].length) : text).trim();
|
|
4876
5452
|
const description = /^description:\s*(.+)$/m.exec(front?.[1] ?? "")?.[1]?.trim() ?? `The ${name} skill.`;
|
|
@@ -5388,22 +5964,22 @@ async function serveMcp() {
|
|
|
5388
5964
|
|
|
5389
5965
|
// src/cli/mcp-http.ts
|
|
5390
5966
|
import crypto8 from "node:crypto";
|
|
5391
|
-
import
|
|
5967
|
+
import fs32 from "node:fs";
|
|
5392
5968
|
import http2 from "node:http";
|
|
5393
|
-
import
|
|
5969
|
+
import path28 from "node:path";
|
|
5394
5970
|
|
|
5395
5971
|
// src/core/dash.ts
|
|
5396
5972
|
import { spawn as spawn6 } from "node:child_process";
|
|
5397
5973
|
import crypto7 from "node:crypto";
|
|
5398
|
-
import
|
|
5974
|
+
import fs31 from "node:fs";
|
|
5399
5975
|
import http from "node:http";
|
|
5400
|
-
import
|
|
5401
|
-
import { fileURLToPath as
|
|
5976
|
+
import path27 from "node:path";
|
|
5977
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
5402
5978
|
|
|
5403
5979
|
// src/core/file-diff.ts
|
|
5404
5980
|
var PREVIEW_LINES = 300;
|
|
5405
5981
|
function gitPath(value, prefix = true) {
|
|
5406
|
-
let
|
|
5982
|
+
let path36 = value;
|
|
5407
5983
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
5408
5984
|
const bytes = [];
|
|
5409
5985
|
const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
|
|
@@ -5411,9 +5987,9 @@ function gitPath(value, prefix = true) {
|
|
|
5411
5987
|
if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
|
|
5412
5988
|
else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
|
|
5413
5989
|
}
|
|
5414
|
-
|
|
5990
|
+
path36 = new TextDecoder().decode(new Uint8Array(bytes));
|
|
5415
5991
|
}
|
|
5416
|
-
return prefix ?
|
|
5992
|
+
return prefix ? path36.replace(/^[ab]\//, "") : path36;
|
|
5417
5993
|
}
|
|
5418
5994
|
function headerPath(header) {
|
|
5419
5995
|
const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
|
|
@@ -5528,7 +6104,7 @@ var WEEK_MS2 = 7 * 24 * 3600 * 1e3;
|
|
|
5528
6104
|
var SCAN = 400;
|
|
5529
6105
|
var RUN_ID3 = /^\d{8}-\d{6}-[0-9a-f]{4}$/;
|
|
5530
6106
|
var LOCAL_HOST = /^(127\.0\.0\.1|localhost|\[::1\]):\d+$/i;
|
|
5531
|
-
var
|
|
6107
|
+
var oneLine8 = (s, max) => {
|
|
5532
6108
|
const t = s.replace(/\s+/g, " ").trim();
|
|
5533
6109
|
return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
|
|
5534
6110
|
};
|
|
@@ -5539,6 +6115,22 @@ function findings(m) {
|
|
|
5539
6115
|
return parts.length ? parts.join(" \xB7 ") : "no findings";
|
|
5540
6116
|
}
|
|
5541
6117
|
var canDiscard = (m) => m.mode === "isolate" && !isActive(m.state) && !m.applied && !m.discarded;
|
|
6118
|
+
function groupAction(group, verb) {
|
|
6119
|
+
const metas = groupIds(group).map((id) => freshMeta(id));
|
|
6120
|
+
let done = 0;
|
|
6121
|
+
for (const m of metas) {
|
|
6122
|
+
try {
|
|
6123
|
+
if (verb === "stop" && isActive(m.state) && stopRun(m.id)) done++;
|
|
6124
|
+
if (verb === "discard" && canDiscard(m)) {
|
|
6125
|
+
discardRun(m);
|
|
6126
|
+
done++;
|
|
6127
|
+
}
|
|
6128
|
+
} catch {
|
|
6129
|
+
}
|
|
6130
|
+
}
|
|
6131
|
+
const what2 = verb === "stop" ? "stopping" : "discarded the isolated copies of";
|
|
6132
|
+
return { done, message: done ? `${what2} ${done} run${done === 1 ? "" : "s"} of ${group}` : `nothing to ${verb} in ${group}` };
|
|
6133
|
+
}
|
|
5542
6134
|
function toRun(m) {
|
|
5543
6135
|
const l = m.state === "running" ? live(m) : void 0;
|
|
5544
6136
|
return {
|
|
@@ -5560,7 +6152,7 @@ function toRun(m) {
|
|
|
5560
6152
|
changes: m.changes?.length || void 0,
|
|
5561
6153
|
applied: m.applied || void 0,
|
|
5562
6154
|
discardable: canDiscard(m) || void 0,
|
|
5563
|
-
note: l?.last ?
|
|
6155
|
+
note: l?.last ? oneLine8(String(l.last), 140) : isActive(m.state) ? "" : m.verifyResult && !m.verifyResult.ok ? `verify failed: ${m.verify ?? ""}` : m.verdict ? findings(m) : m.auditOf && m.state === "done" ? m.auditDisputed?.length ? `${m.auditDisputed.length} disputed` : "nothing disputed" : headline(m, 200) || oneLine8(m.error ?? "", 200)
|
|
5564
6156
|
};
|
|
5565
6157
|
}
|
|
5566
6158
|
function dashState(opts = {}) {
|
|
@@ -5663,15 +6255,15 @@ var HEADERS = {
|
|
|
5663
6255
|
"content-security-policy": `default-src 'none'; script-src 'self' 'sha256-${crypto7.createHash("sha256").update(THEME_SCRIPT).digest("base64")}'; style-src 'self' 'unsafe-inline'; img-src data:; connect-src 'self'; base-uri 'none'; form-action 'none'`
|
|
5664
6256
|
};
|
|
5665
6257
|
var ASSETS = { "/assets/app.js": "text/javascript; charset=utf-8", "/assets/app.css": "text/css; charset=utf-8" };
|
|
5666
|
-
var assetsDir = () =>
|
|
6258
|
+
var assetsDir = () => fileURLToPath4(new URL("./ui/", import.meta.url));
|
|
5667
6259
|
var assetCache = /* @__PURE__ */ new Map();
|
|
5668
6260
|
function readAsset(route) {
|
|
5669
6261
|
try {
|
|
5670
|
-
const file2 =
|
|
5671
|
-
const { mtimeMs } =
|
|
6262
|
+
const file2 = path27.join(assetsDir(), path27.basename(route));
|
|
6263
|
+
const { mtimeMs } = fs31.statSync(file2);
|
|
5672
6264
|
const hit = assetCache.get(route);
|
|
5673
6265
|
if (hit && hit.mtimeMs === mtimeMs) return hit.data;
|
|
5674
|
-
const data =
|
|
6266
|
+
const data = fs31.readFileSync(file2);
|
|
5675
6267
|
assetCache.set(route, { mtimeMs, data });
|
|
5676
6268
|
return data;
|
|
5677
6269
|
} catch {
|
|
@@ -5690,6 +6282,18 @@ function handler(touch2, token) {
|
|
|
5690
6282
|
const given = Buffer.from(String(req.headers["x-pitroom-token"] ?? ""));
|
|
5691
6283
|
if (given.length !== tokenBuf.length || !crypto7.timingSafeEqual(given, tokenBuf)) return json(res, 403, { error: "forbidden" });
|
|
5692
6284
|
if (req.headers.origin !== `http://${req.headers.host}`) return json(res, 403, { error: "forbidden origin" });
|
|
6285
|
+
const g = /^\/api\/group\/([^/]+)\/(stop|discard)$/.exec(pathname);
|
|
6286
|
+
if (g) {
|
|
6287
|
+
let group;
|
|
6288
|
+
try {
|
|
6289
|
+
group = decodeURIComponent(g[1]);
|
|
6290
|
+
} catch {
|
|
6291
|
+
return json(res, 404, { error: "no such group" });
|
|
6292
|
+
}
|
|
6293
|
+
if (!group || group.length > 100 || !groupIds(group).length) return json(res, 404, { error: "no such group" });
|
|
6294
|
+
const r = groupAction(group, g[2]);
|
|
6295
|
+
return r.done ? json(res, 200, { ok: true, message: r.message }) : json(res, 409, { error: r.message });
|
|
6296
|
+
}
|
|
5693
6297
|
const m = /^\/api\/run\/([^/]+)\/(stop|discard)$/.exec(pathname);
|
|
5694
6298
|
if (!m) return json(res, 404, { error: "not found" });
|
|
5695
6299
|
let meta;
|
|
@@ -5746,6 +6350,17 @@ function handler(touch2, token) {
|
|
|
5746
6350
|
const days2 = Number(url.searchParams.get("days") ?? 30);
|
|
5747
6351
|
return json(res, 200, { ...historyStats(days2 > 0 ? Date.now() - days2 * 864e5 : void 0), price: primaryPrice().name });
|
|
5748
6352
|
}
|
|
6353
|
+
const retry = /^\/api\/group\/([^/]+)\/retry$/.exec(url.pathname);
|
|
6354
|
+
if (retry) {
|
|
6355
|
+
let group = "";
|
|
6356
|
+
try {
|
|
6357
|
+
group = decodeURIComponent(retry[1]);
|
|
6358
|
+
} catch {
|
|
6359
|
+
}
|
|
6360
|
+
const ids = group && group.length <= 100 ? groupIds(group) : [];
|
|
6361
|
+
if (!ids.length) return json(res, 404, { error: "no such group" });
|
|
6362
|
+
return json(res, 200, { commands: retryCommands(ids.map((id) => freshMeta(id))), shell: retryShell() });
|
|
6363
|
+
}
|
|
5749
6364
|
const m = /^\/api\/run\/([^/]+)$/.exec(url.pathname);
|
|
5750
6365
|
if (m) {
|
|
5751
6366
|
const d = runDetail(m[1]);
|
|
@@ -5777,7 +6392,7 @@ function startDash(opts) {
|
|
|
5777
6392
|
});
|
|
5778
6393
|
});
|
|
5779
6394
|
}
|
|
5780
|
-
var registryFile = () =>
|
|
6395
|
+
var registryFile = () => path27.join(home(), "dash.json");
|
|
5781
6396
|
async function ping(port) {
|
|
5782
6397
|
try {
|
|
5783
6398
|
const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
|
|
@@ -5789,12 +6404,12 @@ async function ping(port) {
|
|
|
5789
6404
|
async function runningDash() {
|
|
5790
6405
|
let r;
|
|
5791
6406
|
try {
|
|
5792
|
-
r = JSON.parse(
|
|
6407
|
+
r = JSON.parse(fs31.readFileSync(registryFile(), "utf8"));
|
|
5793
6408
|
} catch {
|
|
5794
6409
|
return void 0;
|
|
5795
6410
|
}
|
|
5796
6411
|
if (!isAlive(r.pid)) {
|
|
5797
|
-
|
|
6412
|
+
fs31.rmSync(registryFile(), { force: true });
|
|
5798
6413
|
return void 0;
|
|
5799
6414
|
}
|
|
5800
6415
|
return await ping(r.port) ? { ...r, url: `http://127.0.0.1:${r.port}/` } : void 0;
|
|
@@ -5811,7 +6426,7 @@ async function dashCommand(o) {
|
|
|
5811
6426
|
if (o.stop) {
|
|
5812
6427
|
if (!existing) throw new UserError("pitroom dash is not running");
|
|
5813
6428
|
process.kill(existing.pid, "SIGTERM");
|
|
5814
|
-
|
|
6429
|
+
fs31.rmSync(registryFile(), { force: true });
|
|
5815
6430
|
o.log(`stopped pitroom dash (${existing.url})`);
|
|
5816
6431
|
return 0;
|
|
5817
6432
|
}
|
|
@@ -5848,9 +6463,9 @@ async function dashCommand(o) {
|
|
|
5848
6463
|
if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
|
|
5849
6464
|
dash = await startDash({ port: 0, idleMs: o.idleMs });
|
|
5850
6465
|
}
|
|
5851
|
-
|
|
6466
|
+
fs31.mkdirSync(home(), { recursive: true });
|
|
5852
6467
|
const tmp = `${registryFile()}.${process.pid}.tmp`;
|
|
5853
|
-
|
|
6468
|
+
fs31.writeFileSync(tmp, JSON.stringify({ pid: process.pid, port: dash.port }));
|
|
5854
6469
|
renameOver(tmp, registryFile());
|
|
5855
6470
|
const stop = () => void dash.close();
|
|
5856
6471
|
process.once("SIGINT", stop);
|
|
@@ -5860,7 +6475,7 @@ async function dashCommand(o) {
|
|
|
5860
6475
|
if (o.open) openBrowser(dash.url);
|
|
5861
6476
|
await dash.closed;
|
|
5862
6477
|
try {
|
|
5863
|
-
if (JSON.parse(
|
|
6478
|
+
if (JSON.parse(fs31.readFileSync(registryFile(), "utf8")).pid === process.pid) fs31.rmSync(registryFile(), { force: true });
|
|
5864
6479
|
} catch {
|
|
5865
6480
|
}
|
|
5866
6481
|
return 0;
|
|
@@ -5872,10 +6487,10 @@ var MAX_BODY = 4e6;
|
|
|
5872
6487
|
var MAX_SESSIONS = 64;
|
|
5873
6488
|
var IDLE_MS = 60 * 6e4;
|
|
5874
6489
|
var PATH = "/mcp";
|
|
5875
|
-
var tokenFile = () =>
|
|
6490
|
+
var tokenFile = () => path28.join(home(), "mcp-token");
|
|
5876
6491
|
var readToken = (file2) => {
|
|
5877
6492
|
try {
|
|
5878
|
-
const kept =
|
|
6493
|
+
const kept = fs32.readFileSync(file2, "utf8").trim();
|
|
5879
6494
|
return kept.length >= 16 ? kept : void 0;
|
|
5880
6495
|
} catch {
|
|
5881
6496
|
return void 0;
|
|
@@ -5890,10 +6505,10 @@ function mcpToken() {
|
|
|
5890
6505
|
const file2 = tokenFile();
|
|
5891
6506
|
let token = readToken(file2);
|
|
5892
6507
|
if (!token) {
|
|
5893
|
-
|
|
6508
|
+
fs32.mkdirSync(path28.dirname(file2), { recursive: true });
|
|
5894
6509
|
token = crypto8.randomBytes(32).toString("hex");
|
|
5895
6510
|
try {
|
|
5896
|
-
|
|
6511
|
+
fs32.writeFileSync(file2, `${token}
|
|
5897
6512
|
`, { mode: 384, flag: "wx" });
|
|
5898
6513
|
} catch (e) {
|
|
5899
6514
|
if (e.code !== "EEXIST") throw e;
|
|
@@ -5902,7 +6517,7 @@ function mcpToken() {
|
|
|
5902
6517
|
}
|
|
5903
6518
|
}
|
|
5904
6519
|
try {
|
|
5905
|
-
|
|
6520
|
+
fs32.chmodSync(file2, 384);
|
|
5906
6521
|
} catch {
|
|
5907
6522
|
}
|
|
5908
6523
|
return { token, from: file2 };
|
|
@@ -5928,7 +6543,7 @@ function reply(res, status, body, headers = {}) {
|
|
|
5928
6543
|
res.writeHead(status, { ...text ? { "Content-Type": "application/json" } : {}, "Content-Length": Buffer.byteLength(text), ...headers });
|
|
5929
6544
|
res.end(text);
|
|
5930
6545
|
}
|
|
5931
|
-
var
|
|
6546
|
+
var failure7 = (code, message) => ({ jsonrpc: "2.0", id: null, error: { code, message } });
|
|
5932
6547
|
function readBody(req) {
|
|
5933
6548
|
return new Promise((resolve2, reject) => {
|
|
5934
6549
|
const chunks = [];
|
|
@@ -5955,23 +6570,23 @@ function evictIdle(sessions) {
|
|
|
5955
6570
|
function makeHandler(token, sessions) {
|
|
5956
6571
|
return async (req, res) => {
|
|
5957
6572
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
5958
|
-
if (url.pathname !== PATH) return reply(res, 404,
|
|
5959
|
-
if (!LOCAL_HOST.test(req.headers.host ?? "")) return reply(res, 403,
|
|
6573
|
+
if (url.pathname !== PATH) return reply(res, 404, failure7(-32600, `not found: the endpoint is ${PATH}`));
|
|
6574
|
+
if (!LOCAL_HOST.test(req.headers.host ?? "")) return reply(res, 403, failure7(-32600, "forbidden host"));
|
|
5960
6575
|
const origin = req.headers.origin;
|
|
5961
|
-
if (origin !== void 0 && !LOCAL_ORIGIN.test(origin)) return reply(res, 403,
|
|
6576
|
+
if (origin !== void 0 && !LOCAL_ORIGIN.test(origin)) return reply(res, 403, failure7(-32600, "forbidden origin"));
|
|
5962
6577
|
const auth = /^Bearer\s+(.+)$/i.exec(req.headers.authorization ?? "")?.[1]?.trim();
|
|
5963
|
-
if (!auth || !sameSecret(auth, token)) return reply(res, 401,
|
|
6578
|
+
if (!auth || !sameSecret(auth, token)) return reply(res, 401, failure7(-32001, "a bearer token is needed"), { "WWW-Authenticate": "Bearer" });
|
|
5964
6579
|
const sid = typeof req.headers["mcp-session-id"] === "string" ? req.headers["mcp-session-id"] : void 0;
|
|
5965
6580
|
if (req.method === "DELETE") {
|
|
5966
|
-
if (!sid || !sessions.has(sid)) return reply(res, 404,
|
|
6581
|
+
if (!sid || !sessions.has(sid)) return reply(res, 404, failure7(-32600, "no such session"));
|
|
5967
6582
|
drop(sessions, sid);
|
|
5968
6583
|
return reply(res, 204);
|
|
5969
6584
|
}
|
|
5970
6585
|
if (req.method === "GET") {
|
|
5971
|
-
if (!/text\/event-stream|\*\/\*/.test(req.headers.accept ?? "")) return reply(res, 406,
|
|
5972
|
-
if (!sid) return reply(res, 400,
|
|
6586
|
+
if (!/text\/event-stream|\*\/\*/.test(req.headers.accept ?? "")) return reply(res, 406, failure7(-32600, "a GET opens the event stream: send Accept: text/event-stream"));
|
|
6587
|
+
if (!sid) return reply(res, 400, failure7(-32600, "Mcp-Session-Id is missing: initialize first"));
|
|
5973
6588
|
const kept2 = sessions.get(sid);
|
|
5974
|
-
if (!kept2) return reply(res, 404,
|
|
6589
|
+
if (!kept2) return reply(res, 404, failure7(-32600, "no such session: initialize again"));
|
|
5975
6590
|
kept2.closeStream?.();
|
|
5976
6591
|
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" });
|
|
5977
6592
|
res.write(": open\n\n");
|
|
@@ -6000,9 +6615,9 @@ data: ${JSON.stringify(msg)}
|
|
|
6000
6615
|
res.on("close", close);
|
|
6001
6616
|
return;
|
|
6002
6617
|
}
|
|
6003
|
-
if (req.method !== "POST") return reply(res, 405,
|
|
6618
|
+
if (req.method !== "POST") return reply(res, 405, failure7(-32600, "GET, POST or DELETE"), { Allow: "GET, POST, DELETE" });
|
|
6004
6619
|
const body = await readBody(req);
|
|
6005
|
-
if (body === "too-big") return reply(res, 413,
|
|
6620
|
+
if (body === "too-big") return reply(res, 413, failure7(-32600, `a request body is at most ${MAX_BODY} bytes`));
|
|
6006
6621
|
let parsed;
|
|
6007
6622
|
try {
|
|
6008
6623
|
parsed = JSON.parse(body);
|
|
@@ -6014,15 +6629,15 @@ data: ${JSON.stringify(msg)}
|
|
|
6014
6629
|
const single = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
6015
6630
|
if (single?.method === "initialize") {
|
|
6016
6631
|
if (sid) drop(sessions, sid);
|
|
6017
|
-
if (sessions.size >= MAX_SESSIONS && !evictIdle(sessions)) return reply(res, 503,
|
|
6632
|
+
if (sessions.size >= MAX_SESSIONS && !evictIdle(sessions)) return reply(res, 503, failure7(-32e3, `${MAX_SESSIONS} sessions are busy with requests: try again when one ends`));
|
|
6018
6633
|
const id = crypto8.randomUUID();
|
|
6019
6634
|
kept = { session: newSession(), lastSeen: Date.now() };
|
|
6020
6635
|
sessions.set(id, kept);
|
|
6021
6636
|
headers["Mcp-Session-Id"] = id;
|
|
6022
6637
|
} else {
|
|
6023
|
-
if (!sid) return reply(res, 400,
|
|
6638
|
+
if (!sid) return reply(res, 400, failure7(-32600, "Mcp-Session-Id is missing: initialize first"));
|
|
6024
6639
|
kept = sessions.get(sid);
|
|
6025
|
-
if (!kept) return reply(res, 404,
|
|
6640
|
+
if (!kept) return reply(res, 404, failure7(-32600, "no such session: initialize again"));
|
|
6026
6641
|
}
|
|
6027
6642
|
kept.lastSeen = Date.now();
|
|
6028
6643
|
if (!wantsStream(parsed)) {
|
|
@@ -6052,7 +6667,7 @@ async function serveMcpHttp(opts) {
|
|
|
6052
6667
|
handler2(req, res).catch((e) => {
|
|
6053
6668
|
process.stderr.write(`pitroom mcp: ${String(e.stack ?? e)}
|
|
6054
6669
|
`);
|
|
6055
|
-
reply(res, 500,
|
|
6670
|
+
reply(res, 500, failure7(-32603, "internal error"));
|
|
6056
6671
|
});
|
|
6057
6672
|
});
|
|
6058
6673
|
const sweep = setInterval(() => {
|
|
@@ -6083,23 +6698,23 @@ async function serveMcpHttp(opts) {
|
|
|
6083
6698
|
}
|
|
6084
6699
|
|
|
6085
6700
|
// src/cli/commands.ts
|
|
6086
|
-
import { spawnSync as
|
|
6087
|
-
import
|
|
6088
|
-
import
|
|
6089
|
-
import { fileURLToPath as
|
|
6701
|
+
import { spawnSync as spawnSync11 } from "node:child_process";
|
|
6702
|
+
import fs39 from "node:fs";
|
|
6703
|
+
import path34 from "node:path";
|
|
6704
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
6090
6705
|
|
|
6091
6706
|
// src/core/init.ts
|
|
6092
|
-
import
|
|
6093
|
-
import
|
|
6707
|
+
import fs33 from "node:fs";
|
|
6708
|
+
import path29 from "node:path";
|
|
6094
6709
|
var FREE = /-free$/;
|
|
6095
6710
|
var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
|
|
6096
|
-
var isFound = (
|
|
6711
|
+
var isFound = (binary7) => path29.isAbsolute(binary7) && fs33.existsSync(binary7);
|
|
6097
6712
|
function planInit(opts = {}) {
|
|
6098
6713
|
const file2 = configPath();
|
|
6099
6714
|
const workers = backendIds().map((id) => {
|
|
6100
6715
|
const b = getBackend(id);
|
|
6101
|
-
const
|
|
6102
|
-
return { id, name: b.name, found: isFound(
|
|
6716
|
+
const binary7 = b.binary();
|
|
6717
|
+
return { id, name: b.name, found: isFound(binary7), binary: binary7 };
|
|
6103
6718
|
});
|
|
6104
6719
|
const notes = [];
|
|
6105
6720
|
const config = {};
|
|
@@ -6113,15 +6728,15 @@ function planInit(opts = {}) {
|
|
|
6113
6728
|
} catch {
|
|
6114
6729
|
notes.push("could not list OpenCode models (is it logged in?)");
|
|
6115
6730
|
}
|
|
6116
|
-
const
|
|
6117
|
-
const chosen = opts.model ??
|
|
6731
|
+
const defaultModel6 = b.defaultModel?.();
|
|
6732
|
+
const chosen = opts.model ?? defaultModel6;
|
|
6118
6733
|
const free = models.filter((m) => FREE.test(m) && m !== chosen);
|
|
6119
|
-
opencode2 = { defaultModel:
|
|
6734
|
+
opencode2 = { defaultModel: defaultModel6, models: models.length, free: free.slice(0, 6) };
|
|
6120
6735
|
if (opts.model) {
|
|
6121
6736
|
if (models.length && !models.includes(opts.model)) throw new UserError(`"${opts.model}" is not in \`opencode models\``);
|
|
6122
6737
|
if (!models.length) notes.push(`could not check "${opts.model}": \`opencode models\` listed nothing`);
|
|
6123
6738
|
config.models = { opencode: opts.model };
|
|
6124
|
-
} else if (!
|
|
6739
|
+
} else if (!defaultModel6) {
|
|
6125
6740
|
blocked = `OpenCode has no default model: choose one with --model <id>${free.length ? ` (free models you have: ${free.slice(0, 4).join(", ")})` : ""}`;
|
|
6126
6741
|
}
|
|
6127
6742
|
const fallback = opts.fallback ?? free.slice(0, 2);
|
|
@@ -6139,15 +6754,15 @@ function planInit(opts = {}) {
|
|
|
6139
6754
|
if (Object.keys(tiers).length > 1) config.tiers = tiers;
|
|
6140
6755
|
if (!workers.some((w) => w.found)) blocked = "no worker CLI found: install OpenCode (https://opencode.ai), Codex CLI, Claude Code or Gemini CLI first";
|
|
6141
6756
|
else if (!blocked && !Object.keys(config).length) notes.push("nothing to propose: the default model and the single worker need no config");
|
|
6142
|
-
return { path: file2, exists:
|
|
6757
|
+
return { path: file2, exists: fs33.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
|
|
6143
6758
|
}
|
|
6144
6759
|
function writeInit(plan, force) {
|
|
6145
6760
|
if (plan.blocked) throw new UserError(plan.blocked);
|
|
6146
6761
|
if (!Object.keys(plan.config).length) throw new UserError("nothing to write: the proposal is empty");
|
|
6147
|
-
if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${
|
|
6148
|
-
|
|
6149
|
-
if (plan.exists)
|
|
6150
|
-
|
|
6762
|
+
if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${path29.basename(plan.path)}.bak)`);
|
|
6763
|
+
fs33.mkdirSync(path29.dirname(plan.path), { recursive: true });
|
|
6764
|
+
if (plan.exists) fs33.copyFileSync(plan.path, `${plan.path}.bak`);
|
|
6765
|
+
fs33.writeFileSync(plan.path, `${JSON.stringify(plan.config, null, 2)}
|
|
6151
6766
|
`);
|
|
6152
6767
|
return plan.path;
|
|
6153
6768
|
}
|
|
@@ -6167,18 +6782,18 @@ function formatInit(plan, written) {
|
|
|
6167
6782
|
}
|
|
6168
6783
|
|
|
6169
6784
|
// src/core/mcp-install.ts
|
|
6170
|
-
import { spawnSync as
|
|
6171
|
-
import
|
|
6172
|
-
import
|
|
6173
|
-
import
|
|
6785
|
+
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
6786
|
+
import fs34 from "node:fs";
|
|
6787
|
+
import os11 from "node:os";
|
|
6788
|
+
import path30 from "node:path";
|
|
6174
6789
|
var CLIENT_IDS = ["claude-code", "codex", "gemini", "cursor", "claude-desktop"];
|
|
6175
6790
|
var NAME = "pitroom";
|
|
6176
6791
|
function mcpCommand() {
|
|
6177
6792
|
const launcher = launcherPath();
|
|
6178
6793
|
if (isPitroomLauncher(launcher)) return { command: launcher, args: ["mcp"] };
|
|
6179
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
6180
|
-
const p =
|
|
6181
|
-
if (
|
|
6794
|
+
for (const dir of (process.env.PATH ?? "").split(path30.delimiter).filter(Boolean)) {
|
|
6795
|
+
const p = path30.join(dir, process.platform === "win32" ? "pitroom.cmd" : "pitroom");
|
|
6796
|
+
if (fs34.existsSync(p)) return { command: path30.resolve(p), args: ["mcp"] };
|
|
6182
6797
|
}
|
|
6183
6798
|
return { command: process.execPath, args: [process.argv[1] ?? "pitroom", "mcp"] };
|
|
6184
6799
|
}
|
|
@@ -6188,13 +6803,13 @@ var same = (a, b) => {
|
|
|
6188
6803
|
};
|
|
6189
6804
|
function run(backendId, args) {
|
|
6190
6805
|
const { command, prefix } = resolveCommand(getBackend(backendId).binary());
|
|
6191
|
-
const r =
|
|
6806
|
+
const r = spawnSync9(command, [...prefix, ...args], { encoding: "utf8", timeout: 6e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
6192
6807
|
return { ok: r.status === 0 && !r.error, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
|
|
6193
6808
|
}
|
|
6194
6809
|
var runnable = (backendId) => run(backendId, ["--version"]).ok;
|
|
6195
6810
|
var readJson = (file2) => {
|
|
6196
6811
|
try {
|
|
6197
|
-
return JSON.parse(
|
|
6812
|
+
return JSON.parse(fs34.readFileSync(file2, "utf8"));
|
|
6198
6813
|
} catch {
|
|
6199
6814
|
return void 0;
|
|
6200
6815
|
}
|
|
@@ -6203,8 +6818,8 @@ var jsonState = (file2, c) => {
|
|
|
6203
6818
|
const entry = readJson(file2)?.mcpServers?.[NAME];
|
|
6204
6819
|
return entry === void 0 ? "absent" : same(c, entry) ? "same" : "different";
|
|
6205
6820
|
};
|
|
6206
|
-
var home2 = () =>
|
|
6207
|
-
var geminiDir = () =>
|
|
6821
|
+
var home2 = () => os11.homedir();
|
|
6822
|
+
var geminiDir = () => path30.join(process.env.GEMINI_CLI_HOME ?? home2(), ".gemini");
|
|
6208
6823
|
function cliClient(id, backend, name, where, o) {
|
|
6209
6824
|
return {
|
|
6210
6825
|
id,
|
|
@@ -6227,43 +6842,43 @@ function jsonClient(id, name, file2, dir) {
|
|
|
6227
6842
|
return {
|
|
6228
6843
|
id,
|
|
6229
6844
|
name,
|
|
6230
|
-
found: () =>
|
|
6845
|
+
found: () => fs34.existsSync(dir()),
|
|
6231
6846
|
state: (c) => jsonState(file2(), c),
|
|
6232
6847
|
add: (c) => {
|
|
6233
6848
|
let config = {};
|
|
6234
|
-
if (
|
|
6849
|
+
if (fs34.existsSync(file2())) {
|
|
6235
6850
|
config = readJson(file2());
|
|
6236
6851
|
if (!config || typeof config !== "object" || Array.isArray(config)) return { ok: false, message: `${rel()} is not valid JSON: left alone` };
|
|
6237
6852
|
const backup = `${file2()}.bak-pitroom`;
|
|
6238
|
-
if (!
|
|
6853
|
+
if (!fs34.existsSync(backup)) fs34.copyFileSync(file2(), backup);
|
|
6239
6854
|
}
|
|
6240
6855
|
config.mcpServers = { ...config.mcpServers ?? {}, [NAME]: { command: c.command, args: c.args } };
|
|
6241
|
-
|
|
6856
|
+
fs34.mkdirSync(path30.dirname(file2()), { recursive: true });
|
|
6242
6857
|
const tmp = `${file2()}.${process.pid}.tmp`;
|
|
6243
|
-
|
|
6858
|
+
fs34.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
|
|
6244
6859
|
`);
|
|
6245
|
-
|
|
6860
|
+
fs34.renameSync(tmp, file2());
|
|
6246
6861
|
return { ok: true, message: rel() };
|
|
6247
6862
|
},
|
|
6248
6863
|
remove: () => {
|
|
6249
6864
|
const config = readJson(file2());
|
|
6250
6865
|
if (!config?.mcpServers?.[NAME]) return { ok: true, message: rel() };
|
|
6251
6866
|
delete config.mcpServers[NAME];
|
|
6252
|
-
|
|
6867
|
+
fs34.writeFileSync(file2(), `${JSON.stringify(config, null, 2)}
|
|
6253
6868
|
`);
|
|
6254
6869
|
return { ok: true, message: rel() };
|
|
6255
6870
|
}
|
|
6256
6871
|
};
|
|
6257
6872
|
}
|
|
6258
6873
|
function desktopFile() {
|
|
6259
|
-
if (process.platform === "darwin") return
|
|
6260
|
-
if (process.platform === "win32") return
|
|
6261
|
-
return
|
|
6874
|
+
if (process.platform === "darwin") return path30.join(home2(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
6875
|
+
if (process.platform === "win32") return path30.join(process.env.APPDATA ?? path30.join(home2(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
6876
|
+
return path30.join(process.env.XDG_CONFIG_HOME ?? path30.join(home2(), ".config"), "Claude", "claude_desktop_config.json");
|
|
6262
6877
|
}
|
|
6263
6878
|
function codexState(c) {
|
|
6264
6879
|
let text;
|
|
6265
6880
|
try {
|
|
6266
|
-
text =
|
|
6881
|
+
text = fs34.readFileSync(path30.join(process.env.CODEX_HOME ?? path30.join(home2(), ".codex"), "config.toml"), "utf8");
|
|
6267
6882
|
} catch {
|
|
6268
6883
|
return "absent";
|
|
6269
6884
|
}
|
|
@@ -6281,7 +6896,7 @@ function codexState(c) {
|
|
|
6281
6896
|
function clients() {
|
|
6282
6897
|
return [
|
|
6283
6898
|
cliClient("claude-code", "claude", "Claude Code", "user settings (~/.claude.json)", {
|
|
6284
|
-
state: (c) => jsonState(
|
|
6899
|
+
state: (c) => jsonState(path30.join(home2(), ".claude.json"), c),
|
|
6285
6900
|
add: (c) => ["mcp", "add", "--scope", "user", NAME, "--", c.command, ...c.args],
|
|
6286
6901
|
remove: () => ["mcp", "remove", NAME, "--scope", "user"]
|
|
6287
6902
|
}),
|
|
@@ -6291,12 +6906,12 @@ function clients() {
|
|
|
6291
6906
|
remove: () => ["mcp", "remove", NAME]
|
|
6292
6907
|
}),
|
|
6293
6908
|
cliClient("gemini", "gemini", "Gemini CLI", "user settings (~/.gemini/settings.json)", {
|
|
6294
|
-
state: (c) => jsonState(
|
|
6909
|
+
state: (c) => jsonState(path30.join(geminiDir(), "settings.json"), c),
|
|
6295
6910
|
add: (c) => ["mcp", "add", "--scope", "user", NAME, c.command, ...c.args],
|
|
6296
6911
|
remove: () => ["mcp", "remove", "--scope", "user", NAME]
|
|
6297
6912
|
}),
|
|
6298
|
-
jsonClient("cursor", "Cursor", () =>
|
|
6299
|
-
jsonClient("claude-desktop", "Claude Desktop", desktopFile, () =>
|
|
6913
|
+
jsonClient("cursor", "Cursor", () => path30.join(home2(), ".cursor", "mcp.json"), () => path30.join(home2(), ".cursor")),
|
|
6914
|
+
jsonClient("claude-desktop", "Claude Desktop", desktopFile, () => path30.dirname(desktopFile()))
|
|
6300
6915
|
];
|
|
6301
6916
|
}
|
|
6302
6917
|
function attempt2(f) {
|
|
@@ -6367,16 +6982,16 @@ var parseClients = (value) => {
|
|
|
6367
6982
|
|
|
6368
6983
|
// src/core/plan-status.ts
|
|
6369
6984
|
import crypto9 from "node:crypto";
|
|
6370
|
-
import
|
|
6371
|
-
import
|
|
6985
|
+
import fs35 from "node:fs";
|
|
6986
|
+
import path31 from "node:path";
|
|
6372
6987
|
function notesFile(plan) {
|
|
6373
6988
|
const id = crypto9.createHash("sha1").update(plan.file).digest("hex").slice(0, 12);
|
|
6374
|
-
return
|
|
6989
|
+
return path31.join(home(), "plans", id, "notes.md");
|
|
6375
6990
|
}
|
|
6376
6991
|
function readNotes(plan) {
|
|
6377
6992
|
const f = notesFile(plan);
|
|
6378
|
-
if (!
|
|
6379
|
-
return
|
|
6993
|
+
if (!fs35.existsSync(f)) return [];
|
|
6994
|
+
return fs35.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
|
|
6380
6995
|
const m = /^(\d{4}-\d\d-\d\d \d\d:\d\d) (.*)$/.exec(l);
|
|
6381
6996
|
return m ? { at: m[1], text: m[2] } : { at: "", text: l };
|
|
6382
6997
|
});
|
|
@@ -6384,11 +6999,11 @@ function readNotes(plan) {
|
|
|
6384
6999
|
function addNote(planFile, text) {
|
|
6385
7000
|
const plan = loadPlan(planFile);
|
|
6386
7001
|
const f = notesFile(plan);
|
|
6387
|
-
|
|
6388
|
-
if (!
|
|
7002
|
+
fs35.mkdirSync(path31.dirname(f), { recursive: true });
|
|
7003
|
+
if (!fs35.existsSync(f)) fs35.writeFileSync(f, `# plan: ${plan.file}
|
|
6389
7004
|
`);
|
|
6390
7005
|
const at = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
|
|
6391
|
-
|
|
7006
|
+
fs35.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
|
|
6392
7007
|
`);
|
|
6393
7008
|
return f;
|
|
6394
7009
|
}
|
|
@@ -6456,26 +7071,26 @@ ${rulings}`,
|
|
|
6456
7071
|
|
|
6457
7072
|
// src/core/cache.ts
|
|
6458
7073
|
import crypto10 from "node:crypto";
|
|
6459
|
-
import
|
|
6460
|
-
import
|
|
7074
|
+
import fs36 from "node:fs";
|
|
7075
|
+
import path32 from "node:path";
|
|
6461
7076
|
var SCAN2 = 500;
|
|
6462
7077
|
function cacheKey(o) {
|
|
6463
7078
|
if (o.verify || o.mode !== "read" || o.continueFrom || o.web || o.plan || o.review || o.audit || o.auditRate === 1) return void 0;
|
|
6464
7079
|
if (!(effective().cacheDays.value > 0)) return void 0;
|
|
6465
|
-
const dir = canonical(
|
|
7080
|
+
const dir = canonical(path32.resolve(o.dir));
|
|
6466
7081
|
const root = repoRoot(dir);
|
|
6467
7082
|
if (!root) return void 0;
|
|
6468
7083
|
try {
|
|
6469
7084
|
const tree = snapshotTree(root);
|
|
6470
7085
|
const head = commitOf(root, "HEAD") ?? "";
|
|
6471
7086
|
const files = o.files.map((f) => {
|
|
6472
|
-
const abs =
|
|
6473
|
-
return [abs, crypto10.createHash("sha256").update(
|
|
7087
|
+
const abs = path32.resolve(f);
|
|
7088
|
+
return [abs, crypto10.createHash("sha256").update(fs36.readFileSync(abs)).digest("hex")];
|
|
6474
7089
|
});
|
|
6475
7090
|
const question = o.task.replace(/\s+/g, " ").trim();
|
|
6476
7091
|
const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
|
|
6477
7092
|
const where = !o.inPlace && wantSnapshot(readIn2, root, dir) ? "snapshot" : "project";
|
|
6478
|
-
const key = crypto10.createHash("sha256").update(JSON.stringify([question,
|
|
7093
|
+
const key = crypto10.createHash("sha256").update(JSON.stringify([question, path32.relative(root, dir), files, where])).digest("hex");
|
|
6479
7094
|
return { key, state: `${head}:${tree}` };
|
|
6480
7095
|
} catch {
|
|
6481
7096
|
return void 0;
|
|
@@ -6517,10 +7132,138 @@ function ago(iso, now = Date.now()) {
|
|
|
6517
7132
|
}
|
|
6518
7133
|
var cachedNote = (m) => `pitroom \u27F2 cached answer \xB7 the same question on the same code as run ${m.id} (${ago(m.endedAt ?? m.startedAt)}) \xB7 no worker ran \xB7 --fresh asks one again`;
|
|
6519
7134
|
|
|
7135
|
+
// src/core/eval.ts
|
|
7136
|
+
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
7137
|
+
import fs37 from "node:fs";
|
|
7138
|
+
var KINDS = ["definition", "count", "set"];
|
|
7139
|
+
function loadEval(file2) {
|
|
7140
|
+
let raw;
|
|
7141
|
+
try {
|
|
7142
|
+
raw = JSON.parse(fs37.readFileSync(file2, "utf8"));
|
|
7143
|
+
} catch (e) {
|
|
7144
|
+
throw new UserError(`cannot read ${file2}: ${e.message}`);
|
|
7145
|
+
}
|
|
7146
|
+
const list2 = raw?.questions;
|
|
7147
|
+
if (!Array.isArray(list2) || !list2.length) throw new UserError(`${file2}: expected {"questions": [{"id", "task", "kind", "truth"}, \u2026]}`);
|
|
7148
|
+
const ids = /* @__PURE__ */ new Set();
|
|
7149
|
+
return list2.map((q, i) => {
|
|
7150
|
+
const where = `${file2}: question ${i + 1}`;
|
|
7151
|
+
if (!q || typeof q !== "object") throw new UserError(`${where} is not an object`);
|
|
7152
|
+
const id = typeof q.id === "string" && q.id.trim() ? q.id.trim() : `q${i + 1}`;
|
|
7153
|
+
if (ids.has(id)) throw new UserError(`${where}: the id "${id}" is used twice`);
|
|
7154
|
+
ids.add(id);
|
|
7155
|
+
if (typeof q.task !== "string" || !q.task.trim()) throw new UserError(`${where} (${id}) has no task`);
|
|
7156
|
+
if (!KINDS.includes(q.kind)) throw new UserError(`${where} (${id}): kind must be one of ${KINDS.join(", ")}`);
|
|
7157
|
+
const t = q.truth;
|
|
7158
|
+
const grep = t && typeof t === "object" && !Array.isArray(t) && typeof t.grep === "string" && t.grep;
|
|
7159
|
+
const literal = q.kind === "definition" && typeof t === "string" && /:\d+$/.test(t) || q.kind === "count" && typeof t === "number" && Number.isInteger(t) && t >= 0 || q.kind === "set" && Array.isArray(t) && t.every((s) => typeof s === "string");
|
|
7160
|
+
if (!grep && !literal) {
|
|
7161
|
+
const want = { definition: '"path:line"', count: "a whole number", set: "a list of paths" }[q.kind];
|
|
7162
|
+
throw new UserError(`${where} (${id}): truth must be ${want} or {"grep": "pattern", \u2026}`);
|
|
7163
|
+
}
|
|
7164
|
+
return { id, task: q.task.trim(), kind: q.kind, truth: t };
|
|
7165
|
+
});
|
|
7166
|
+
}
|
|
7167
|
+
var clean = (p) => p.replace(/\\/g, "/").replace(/^\.?\//, "");
|
|
7168
|
+
function gitGrep(root, flags, t) {
|
|
7169
|
+
const r = spawnSync10("git", ["-C", root, "grep", ...flags, t.regex ? "-E" : "-F", ...t.word ? ["-w"] : [], "-e", t.grep, "--", t.dir ?? "."], {
|
|
7170
|
+
encoding: "utf8",
|
|
7171
|
+
maxBuffer: 64 * 1024 * 1024
|
|
7172
|
+
});
|
|
7173
|
+
if (r.status === 1) return [];
|
|
7174
|
+
if (r.status !== 0) throw new UserError(`git grep "${t.grep}" failed: ${(r.stderr || r.error?.message || "").trim()}`);
|
|
7175
|
+
const exclude = t.exclude ? new RegExp(t.exclude) : void 0;
|
|
7176
|
+
return r.stdout.split("\n").filter(Boolean).filter((l) => !exclude?.test(l.split(":")[0]));
|
|
7177
|
+
}
|
|
7178
|
+
function truthOf(q, root) {
|
|
7179
|
+
const t = q.truth;
|
|
7180
|
+
if (t && typeof t === "object" && !Array.isArray(t)) {
|
|
7181
|
+
if (!root) throw new UserError(`question ${q.id}: a "grep" truth needs a git repository`);
|
|
7182
|
+
if (q.kind === "definition") {
|
|
7183
|
+
const hit = gitGrep(root, ["-n"], t).map((l) => /^(.*?):(\d+):/.exec(l)).find(Boolean);
|
|
7184
|
+
if (!hit) throw new UserError(`question ${q.id}: git grep found no definition for "${t.grep}"`);
|
|
7185
|
+
return { path: clean(hit[1]), line: Number(hit[2]) };
|
|
7186
|
+
}
|
|
7187
|
+
const files = [...new Set(gitGrep(root, ["-l"], t).map(clean))];
|
|
7188
|
+
return q.kind === "count" ? { count: files.length } : { files };
|
|
7189
|
+
}
|
|
7190
|
+
if (q.kind === "definition") {
|
|
7191
|
+
const m = /^(.*):(\d+)$/.exec(t);
|
|
7192
|
+
return { path: clean(m[1]), line: Number(m[2]) };
|
|
7193
|
+
}
|
|
7194
|
+
if (q.kind === "count") return { count: t };
|
|
7195
|
+
return { files: [...new Set(t.map(clean))] };
|
|
7196
|
+
}
|
|
7197
|
+
function answerParts(answer) {
|
|
7198
|
+
const text = answer.replace(/`/g, "");
|
|
7199
|
+
const s = /SUMMARY:\s*([\s\S]*?)(?=^\s*(?:DETAILS|FILES CHANGED|VERIFICATION|OPEN ISSUES):|$(?![\s\S]))/m.exec(text);
|
|
7200
|
+
const d = /^\s*DETAILS:\s*([\s\S]*?)(?=^\s*(?:FILES CHANGED|VERIFICATION|OPEN ISSUES):|$(?![\s\S]))/m.exec(text);
|
|
7201
|
+
const summary = s ? s[1] : text.split(/^\s*(?:DETAILS|FILES CHANGED|VERIFICATION|OPEN ISSUES):/m)[0];
|
|
7202
|
+
return { summary: summary.trim(), details: (d?.[1] ?? "").trim() };
|
|
7203
|
+
}
|
|
7204
|
+
function pathsIn(text) {
|
|
7205
|
+
const found = text.replace(/\\/g, "/").match(/[A-Za-z0-9_.@\-/]+\.[A-Za-z0-9]{1,8}(?::\d+)?/g) ?? [];
|
|
7206
|
+
return [...new Set(found.map((p) => clean(p.replace(/:\d+$/, ""))))];
|
|
7207
|
+
}
|
|
7208
|
+
function scoreAnswer(q, truth, answer, root) {
|
|
7209
|
+
const prefix = root ? `${clean(root).replace(/\/$/, "")}/` : "";
|
|
7210
|
+
const text = prefix ? answer.replace(/\\/g, "/").split(prefix).join("") : answer;
|
|
7211
|
+
const { summary, details } = answerParts(text);
|
|
7212
|
+
if (!summary && !details) return { score: 0, got: "(no answer)" };
|
|
7213
|
+
if ("line" in truth) {
|
|
7214
|
+
const flat = summary.replace(/\s+/g, " ");
|
|
7215
|
+
if (new RegExp(`(^|[^\\w./-])\\.?/?${escape2(truth.path)}:${truth.line}(?!\\d)`).test(flat)) return { score: 1, got: `${truth.path}:${truth.line}` };
|
|
7216
|
+
const named = pathsIn(flat).includes(truth.path);
|
|
7217
|
+
return { score: named ? 0.5 : 0, got: flat.slice(0, 120) };
|
|
7218
|
+
}
|
|
7219
|
+
if ("count" in truth) {
|
|
7220
|
+
const n = /\d[\d,]*/.exec(summary);
|
|
7221
|
+
const got2 = n ? Number(n[0].replace(/,/g, "")) : void 0;
|
|
7222
|
+
return { score: got2 === truth.count ? 1 : 0, got: got2 === void 0 ? "(no number)" : String(got2) };
|
|
7223
|
+
}
|
|
7224
|
+
let got = pathsIn(summary);
|
|
7225
|
+
if (!got.length) got = pathsIn(details);
|
|
7226
|
+
const hit = got.filter((p) => truth.files.includes(p)).length;
|
|
7227
|
+
const precision = got.length ? hit / got.length : 0;
|
|
7228
|
+
const recall = truth.files.length ? hit / truth.files.length : got.length ? 0 : 1;
|
|
7229
|
+
const f1 = precision + recall ? 2 * precision * recall / (precision + recall) : 0;
|
|
7230
|
+
return { score: !truth.files.length && !got.length ? 1 : f1, got: `${hit}/${truth.files.length} found, ${got.length - hit} extra` };
|
|
7231
|
+
}
|
|
7232
|
+
var escape2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7233
|
+
var mean = (a) => a.length ? a.reduce((x, y) => x + y, 0) / a.length : 0;
|
|
7234
|
+
var median = (a) => {
|
|
7235
|
+
const s = a.filter((x) => x !== void 0).sort((x, y) => x - y);
|
|
7236
|
+
return s.length ? s[Math.floor(s.length / 2)] : void 0;
|
|
7237
|
+
};
|
|
7238
|
+
function summarize(rows) {
|
|
7239
|
+
const by = /* @__PURE__ */ new Map();
|
|
7240
|
+
for (const r of rows) by.set(r.worker, [...by.get(r.worker) ?? [], r]);
|
|
7241
|
+
return [...by.entries()].map(([worker, rs]) => ({
|
|
7242
|
+
worker,
|
|
7243
|
+
questions: rs.length,
|
|
7244
|
+
failed: rs.filter((r) => r.state !== "done").length,
|
|
7245
|
+
score: mean(rs.map((r) => r.score)),
|
|
7246
|
+
byKind: Object.fromEntries(KINDS.filter((k) => rs.some((r) => r.kind === k)).map((k) => [k, mean(rs.filter((r) => r.kind === k).map((r) => r.score))])),
|
|
7247
|
+
medianSeconds: median(rs.map((r) => r.seconds)),
|
|
7248
|
+
medianTokens: median(rs.map((r) => r.tokens))
|
|
7249
|
+
})).sort((a, b) => b.score - a.score || (a.medianSeconds ?? 1e9) - (b.medianSeconds ?? 1e9));
|
|
7250
|
+
}
|
|
7251
|
+
var pct2 = (x) => x === void 0 ? "-" : `${Math.round(x * 100)}%`;
|
|
7252
|
+
function formatEval(rows, group) {
|
|
7253
|
+
const out = [`pitroom eval "${group}": ${new Set(rows.map((r) => r.question)).size} questions \xD7 ${new Set(rows.map((r) => r.worker)).size} workers`, ""];
|
|
7254
|
+
for (const r of rows) out.push(` ${pct2(r.score).padStart(4)} ${r.question.padEnd(16)} ${r.worker.padEnd(28)} ${r.state === "done" ? r.got : `(${r.state})`}`);
|
|
7255
|
+
out.push("", "| Worker | Score | Definition | Count | List | Failed | Median time | Median tokens |", "|---|---|---|---|---|---|---|---|");
|
|
7256
|
+
for (const s of summarize(rows)) {
|
|
7257
|
+
out.push(`| ${s.worker} | **${pct2(s.score)}** | ${pct2(s.byKind.definition)} | ${pct2(s.byKind.count)} | ${pct2(s.byKind.set)} | ${s.failed}/${s.questions} | ${s.medianSeconds ?? "-"} s | ${s.medianTokens === void 0 ? "-" : `${Math.round(s.medianTokens / 1e3)}k`} |`);
|
|
7258
|
+
}
|
|
7259
|
+
out.push("", `Every run is in the history: pitroom wait -g ${group}, or the dashboard's group picker.`);
|
|
7260
|
+
return out.join("\n");
|
|
7261
|
+
}
|
|
7262
|
+
|
|
6520
7263
|
// src/core/review.ts
|
|
6521
7264
|
import crypto11 from "node:crypto";
|
|
6522
|
-
import
|
|
6523
|
-
import
|
|
7265
|
+
import fs38 from "node:fs";
|
|
7266
|
+
import path33 from "node:path";
|
|
6524
7267
|
var TEMPLATE2 = { task: "task-reviewer", fix: "re-review", range: "code-reviewer" };
|
|
6525
7268
|
var section = (title, body) => `## ${title}
|
|
6526
7269
|
|
|
@@ -6538,7 +7281,7 @@ function ancestors(m) {
|
|
|
6538
7281
|
function briefOf(m) {
|
|
6539
7282
|
const root = [m, ...ancestors(m)].at(-1);
|
|
6540
7283
|
const f = runFile(root.id, "brief.md");
|
|
6541
|
-
return
|
|
7284
|
+
return fs38.existsSync(f) ? fs38.readFileSync(f, "utf8") : root.task;
|
|
6542
7285
|
}
|
|
6543
7286
|
function latestReview(ids) {
|
|
6544
7287
|
for (const id of listRunIds().reverse()) {
|
|
@@ -6552,7 +7295,7 @@ function latestReview(ids) {
|
|
|
6552
7295
|
}
|
|
6553
7296
|
function diffOf(m, from = m.baseTree) {
|
|
6554
7297
|
const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
|
|
6555
|
-
if (where &&
|
|
7298
|
+
if (where && fs38.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
|
|
6556
7299
|
if (from !== m.baseTree) {
|
|
6557
7300
|
throw new UserError(`the isolated copy of ${m.id} is gone, so its fix round cannot be shown on its own; review a run that is not applied yet`, 3);
|
|
6558
7301
|
}
|
|
@@ -6566,7 +7309,7 @@ function runReview(id) {
|
|
|
6566
7309
|
if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
|
|
6567
7310
|
const chain = ancestors(m);
|
|
6568
7311
|
const previous = latestReview(chain.map((a) => a.id));
|
|
6569
|
-
const dir = m.mode === "isolate" && m.worktree &&
|
|
7312
|
+
const dir = m.mode === "isolate" && m.worktree && fs38.existsSync(m.worktree) ? m.cwd : m.dir;
|
|
6570
7313
|
const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
|
|
6571
7314
|
const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
|
|
6572
7315
|
if (!previous) {
|
|
@@ -6642,19 +7385,19 @@ function writePackage(job) {
|
|
|
6642
7385
|
const root = repoRoot(job.dir);
|
|
6643
7386
|
if (!g || !root) throw new UserError(`not a git repository: ${job.dir}`);
|
|
6644
7387
|
const name = `review-${crypto11.randomBytes(4).toString("hex")}.md`;
|
|
6645
|
-
const inside2 = !
|
|
6646
|
-
const file2 = inside2 ?
|
|
7388
|
+
const inside2 = !path33.relative(fs38.realpathSync(root), fs38.realpathSync(g)).startsWith("..");
|
|
7389
|
+
const file2 = inside2 ? path33.join(g, "pitroom", name) : path33.join(root, ".pitroom", name);
|
|
6647
7390
|
if (!inside2) ignoreInGit(root, "/.pitroom/");
|
|
6648
|
-
|
|
6649
|
-
|
|
7391
|
+
fs38.mkdirSync(path33.dirname(file2), { recursive: true });
|
|
7392
|
+
fs38.writeFileSync(file2, job.package);
|
|
6650
7393
|
return file2;
|
|
6651
7394
|
}
|
|
6652
7395
|
function ignoreInGit(root, pattern) {
|
|
6653
|
-
const exclude =
|
|
6654
|
-
const text =
|
|
7396
|
+
const exclude = path33.resolve(root, git(root, ["rev-parse", "--git-path", "info/exclude"]).stdout.trim());
|
|
7397
|
+
const text = fs38.existsSync(exclude) ? fs38.readFileSync(exclude, "utf8") : "";
|
|
6655
7398
|
if (text.split(/\r?\n/).includes(pattern)) return;
|
|
6656
|
-
|
|
6657
|
-
|
|
7399
|
+
fs38.mkdirSync(path33.dirname(exclude), { recursive: true });
|
|
7400
|
+
fs38.appendFileSync(exclude, `${text && !text.endsWith("\n") ? "\n" : ""}# Pitroom review packages in worktrees (removed after each review)
|
|
6658
7401
|
${pattern}
|
|
6659
7402
|
`);
|
|
6660
7403
|
}
|
|
@@ -6698,9 +7441,9 @@ function modelTable(opts = {}) {
|
|
|
6698
7441
|
const known = /* @__PURE__ */ new Set();
|
|
6699
7442
|
for (const b of allBackends()) {
|
|
6700
7443
|
if (opts.backend && b.id !== opts.backend) continue;
|
|
6701
|
-
const
|
|
6702
|
-
sources.push(`${b.id}: ${
|
|
6703
|
-
for (const m of
|
|
7444
|
+
const catalog6 = b.catalog?.() ?? { models: [], source: "no model list for this worker" };
|
|
7445
|
+
sources.push(`${b.id}: ${catalog6.source}`);
|
|
7446
|
+
for (const m of catalog6.models) {
|
|
6704
7447
|
const key = `${b.id}:${m.id}`;
|
|
6705
7448
|
known.add(key);
|
|
6706
7449
|
const s = seen.get(key);
|
|
@@ -6810,10 +7553,10 @@ async function cmdReview(p) {
|
|
|
6810
7553
|
writeMeta(meta);
|
|
6811
7554
|
}
|
|
6812
7555
|
} catch (e) {
|
|
6813
|
-
|
|
7556
|
+
fs39.rmSync(packageFile, { force: true });
|
|
6814
7557
|
throw e;
|
|
6815
7558
|
}
|
|
6816
|
-
|
|
7559
|
+
fs39.writeFileSync(runFile(meta.id, "package.md"), job.package);
|
|
6817
7560
|
return launch(p, meta);
|
|
6818
7561
|
}
|
|
6819
7562
|
async function cmdAudit(p) {
|
|
@@ -6922,7 +7665,7 @@ function cmdPlan(p) {
|
|
|
6922
7665
|
}
|
|
6923
7666
|
function cmdCrew(p) {
|
|
6924
7667
|
const file2 = flag(p, "task-file");
|
|
6925
|
-
const tasks = (file2 ?
|
|
7668
|
+
const tasks = (file2 ? fs39.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
|
|
6926
7669
|
if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
|
|
6927
7670
|
if (has(p, "write") && tasks.length > 1) {
|
|
6928
7671
|
throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
|
|
@@ -6943,6 +7686,32 @@ function cmdCrew(p) {
|
|
|
6943
7686
|
results: pitroom wait -g ${group}`);
|
|
6944
7687
|
return 0;
|
|
6945
7688
|
}
|
|
7689
|
+
async function cmdEval(p) {
|
|
7690
|
+
const file2 = p.positional[0];
|
|
7691
|
+
if (!file2 || p.positional.length > 1) throw new UserError("eval takes one questions file: pitroom eval QUESTIONS.json [-W worker]\u2026 (see pitroom --help)");
|
|
7692
|
+
if (has(p, "write") || has(p, "isolate") || has(p, "continue") || has(p, "plan")) throw new UserError("eval asks read questions: drop -w/-i/--continue/--plan");
|
|
7693
|
+
const questions = loadEval(file2);
|
|
7694
|
+
const base2 = runOptions(p, "eval");
|
|
7695
|
+
const root = repoRoot(path34.resolve(base2.dir));
|
|
7696
|
+
const truths = new Map(questions.map((q) => [q.id, truthOf(q, root)]));
|
|
7697
|
+
const workers = [...new Set(p.flags.get("worker") ?? [effective().worker.value])];
|
|
7698
|
+
const group = flag(p, "group") ?? `eval-${(/* @__PURE__ */ new Date()).toISOString().slice(11, 19).replace(/:/g, "")}`;
|
|
7699
|
+
const started = workers.flatMap(
|
|
7700
|
+
(worker) => questions.map((q) => ({ q, worker, meta: startInBackground(prepareRun({ ...runOptions(p, q.task), worker, auditRate: 0, noFallback: true, group })) }))
|
|
7701
|
+
);
|
|
7702
|
+
if (!has(p, "json")) console.error(`pitroom eval "${group}": ${questions.length} questions \xD7 ${workers.length} worker${workers.length === 1 ? "" : "s"} started (pitroom watch -g ${group})`);
|
|
7703
|
+
const rounds = Math.ceil(started.length / effective().maxParallel.value);
|
|
7704
|
+
const { metas } = await waitMany(started.map((s) => s.meta.id), { any: false, timeoutMs: (base2.timeoutSec * rounds + 120) * 1e3 });
|
|
7705
|
+
const rows = started.map((s, i) => {
|
|
7706
|
+
const m = metas[i];
|
|
7707
|
+
const answer = readRunFile(m.id, "summary.md") ?? "";
|
|
7708
|
+
const res = m.state === "done" ? scoreAnswer(s.q, truths.get(s.q.id), answer, root) : { score: 0, got: `(${m.state})` };
|
|
7709
|
+
const seconds = m.endedAt ? Math.round((Date.parse(m.endedAt) - Date.parse(m.startedAt)) / 1e3) : void 0;
|
|
7710
|
+
return { question: s.q.id, kind: s.q.kind, worker: s.worker, run: m.id, state: m.state, score: res.score, got: res.got, seconds, tokens: m.usage?.total };
|
|
7711
|
+
});
|
|
7712
|
+
console.log(has(p, "json") ? JSON.stringify({ group, truths: Object.fromEntries(truths), summary: summarize(rows), rows }, null, 2) : formatEval(rows, group));
|
|
7713
|
+
return 0;
|
|
7714
|
+
}
|
|
6946
7715
|
function selectIds(p, fallback) {
|
|
6947
7716
|
const group = flag(p, "group");
|
|
6948
7717
|
if (group) {
|
|
@@ -7123,13 +7892,13 @@ function sinceMs(s) {
|
|
|
7123
7892
|
if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
|
|
7124
7893
|
return Date.now() - Number(m[1]) * 864e5;
|
|
7125
7894
|
}
|
|
7126
|
-
var readStdin = () => process.stdin.isTTY ? "" :
|
|
7895
|
+
var readStdin = () => process.stdin.isTTY ? "" : fs39.readFileSync(0, "utf8");
|
|
7127
7896
|
function cmdStatusline(p) {
|
|
7128
7897
|
const input = readStdin();
|
|
7129
7898
|
const lines = [];
|
|
7130
7899
|
const then = flag(p, "then");
|
|
7131
7900
|
if (then) {
|
|
7132
|
-
const r = process.platform === "win32" ?
|
|
7901
|
+
const r = process.platform === "win32" ? spawnSync11(then, { shell: true, input, encoding: "utf8", timeout: 5e3 }) : spawnSync11("/bin/sh", ["-c", then], { input, encoding: "utf8", timeout: 5e3 });
|
|
7133
7902
|
if (r.stdout?.trimEnd()) lines.push(r.stdout.trimEnd());
|
|
7134
7903
|
}
|
|
7135
7904
|
try {
|
|
@@ -7163,8 +7932,8 @@ function cmdModels(p) {
|
|
|
7163
7932
|
return 0;
|
|
7164
7933
|
}
|
|
7165
7934
|
function cmdHookStart() {
|
|
7166
|
-
const script =
|
|
7167
|
-
if (
|
|
7935
|
+
const script = path34.join(path34.dirname(path34.dirname(fileURLToPath5(import.meta.url))), "hooks", "session-start.mjs");
|
|
7936
|
+
if (fs39.existsSync(script)) spawnSync11(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
|
|
7168
7937
|
return 0;
|
|
7169
7938
|
}
|
|
7170
7939
|
function cmdInit(p) {
|
|
@@ -7204,8 +7973,8 @@ function cmdSavings(p) {
|
|
|
7204
7973
|
est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
|
|
7205
7974
|
const out = flag(p, "card");
|
|
7206
7975
|
if (out) {
|
|
7207
|
-
|
|
7208
|
-
console.log(`card written to ${
|
|
7976
|
+
fs39.writeFileSync(out, card(t, period));
|
|
7977
|
+
console.log(`card written to ${path34.resolve(out)}`);
|
|
7209
7978
|
}
|
|
7210
7979
|
if (has(p, "badge")) console.log(`})`);
|
|
7211
7980
|
return 0;
|
|
@@ -7247,7 +8016,7 @@ function cmdConfig(p) {
|
|
|
7247
8016
|
console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
|
|
7248
8017
|
return 0;
|
|
7249
8018
|
}
|
|
7250
|
-
console.log(`config file: ${configPath()}${
|
|
8019
|
+
console.log(`config file: ${configPath()}${fs39.existsSync(configPath()) ? "" : " (not present)"}`);
|
|
7251
8020
|
for (const [key, s] of Object.entries(eff)) {
|
|
7252
8021
|
const v = Array.isArray(s.value) ? s.value.join(", ") || "\u2014" : s.value && typeof s.value === "object" ? Object.entries(s.value).map(([k, m]) => `${k}=${m}`).join(", ") || "\u2014" : s.value === void 0 ? key === "model" ? "the worker's default" : "\u2014" : String(s.value);
|
|
7253
8022
|
console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
|
|
@@ -7271,7 +8040,7 @@ function cmdClean(p) {
|
|
|
7271
8040
|
const m = readMeta(id);
|
|
7272
8041
|
if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
|
|
7273
8042
|
recordRun(m);
|
|
7274
|
-
|
|
8043
|
+
fs39.rmSync(runDir(id), { recursive: true, force: true });
|
|
7275
8044
|
}
|
|
7276
8045
|
const snapshots = prune(Date.now(), 0);
|
|
7277
8046
|
console.log(`removed ${old.length} run(s)${snapshots ? ` and ${snapshots} read snapshot${snapshots === 1 ? "" : "s"}` : ""}; the history and the savings ledger are kept`);
|
|
@@ -7281,10 +8050,10 @@ var cmdRevert = (p) => (console.log(revertRun(freshMeta(resolveRun(p.positional[
|
|
|
7281
8050
|
var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positional[0])))), 0);
|
|
7282
8051
|
|
|
7283
8052
|
// src/core/doctor.ts
|
|
7284
|
-
import { spawnSync as
|
|
7285
|
-
import
|
|
7286
|
-
import
|
|
7287
|
-
import
|
|
8053
|
+
import { spawnSync as spawnSync12 } from "node:child_process";
|
|
8054
|
+
import fs40 from "node:fs";
|
|
8055
|
+
import os12 from "node:os";
|
|
8056
|
+
import path35 from "node:path";
|
|
7288
8057
|
var MARK2 = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
|
|
7289
8058
|
var NEXT = [
|
|
7290
8059
|
{ when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
|
|
@@ -7307,14 +8076,14 @@ function usageLine() {
|
|
|
7307
8076
|
return `${ids.length} run${ids.length === 1 ? "" : "s"} so far${when2} (pitroom savings shows what they saved)`;
|
|
7308
8077
|
}
|
|
7309
8078
|
function firstNodeOnPath() {
|
|
7310
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
7311
|
-
const file2 =
|
|
8079
|
+
for (const dir of (process.env.PATH ?? "").split(path35.delimiter).filter(Boolean)) {
|
|
8080
|
+
const file2 = path35.join(dir, process.platform === "win32" ? "node.exe" : "node");
|
|
7312
8081
|
try {
|
|
7313
|
-
if (!
|
|
8082
|
+
if (!fs40.statSync(file2).isFile()) continue;
|
|
7314
8083
|
} catch {
|
|
7315
8084
|
continue;
|
|
7316
8085
|
}
|
|
7317
|
-
const r =
|
|
8086
|
+
const r = spawnSync12(file2, ["-p", "process.versions.node"], { encoding: "utf8", timeout: 5e3 });
|
|
7318
8087
|
return r.status === 0 ? { path: file2, version: r.stdout.trim() } : { path: file2, version: "" };
|
|
7319
8088
|
}
|
|
7320
8089
|
return void 0;
|
|
@@ -7329,7 +8098,7 @@ var READ_ONLY_HOW = {
|
|
|
7329
8098
|
"tool-allowlist": "a tool allowlist",
|
|
7330
8099
|
"approval-mode": "the CLI's read-only approval mode"
|
|
7331
8100
|
};
|
|
7332
|
-
function
|
|
8101
|
+
function doctor7(probe) {
|
|
7333
8102
|
const checks = [];
|
|
7334
8103
|
let current = "Setup";
|
|
7335
8104
|
const section2 = (name) => void (current = name);
|
|
@@ -7337,12 +8106,12 @@ function doctor5(probe) {
|
|
|
7337
8106
|
const addAll = (list2) => list2.forEach((c) => add(c.level, c.message));
|
|
7338
8107
|
add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
|
|
7339
8108
|
const onPath = firstNodeOnPath();
|
|
7340
|
-
if (onPath && onPath.version && tooOld(onPath.version) &&
|
|
8109
|
+
if (onPath && onPath.version && tooOld(onPath.version) && path35.resolve(onPath.path) !== path35.resolve(process.execPath)) {
|
|
7341
8110
|
add("warn", `the first \`node\` on PATH is v${onPath.version} (${onPath.path}), older than the 22.13 Pitroom needs: shells that agent apps start may use it (the pitroom command finds a newer Node itself; other tools may not)`);
|
|
7342
8111
|
}
|
|
7343
8112
|
add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
|
|
7344
8113
|
const cfg = loadConfig();
|
|
7345
|
-
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${
|
|
8114
|
+
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs40.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
|
|
7346
8115
|
for (const w of cfg.warnings) add("warn", w);
|
|
7347
8116
|
if (shimReady()) add("ok", "git guard shim ready");
|
|
7348
8117
|
else if (process.platform === "win32") add("warn", "git guard: the ref and push guard is on, but the git shim needs the sh.exe of Git for Windows next to git (not found), so a worker's `git add`, `restore` or `clean` is not blocked; prefer --isolate and check the patch before apply");
|
|
@@ -7485,7 +8254,7 @@ function liveProbe(target) {
|
|
|
7485
8254
|
title: "pitroom doctor probe"
|
|
7486
8255
|
});
|
|
7487
8256
|
const t0 = Date.now();
|
|
7488
|
-
const r =
|
|
8257
|
+
const r = spawnSync12(inv.command, inv.args, {
|
|
7489
8258
|
encoding: "utf8",
|
|
7490
8259
|
timeout: 3e5,
|
|
7491
8260
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -7515,20 +8284,20 @@ function skillChecks() {
|
|
|
7515
8284
|
else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
|
|
7516
8285
|
}
|
|
7517
8286
|
if (geminiExtensionInstalled()) {
|
|
7518
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
8287
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path35.sep}.agents${path35.sep}`) && i.names.length);
|
|
7519
8288
|
checks.push(
|
|
7520
8289
|
linked ? { level: "warn", message: "Pitroom is installed as a Gemini CLI extension and linked into ~/.agents/skills: Gemini loads the skills twice; run `pitroom uninstall` or `gemini extensions uninstall pitroom`" } : { level: "ok", message: "Gemini CLI extension installed (skills + context)" }
|
|
7521
8290
|
);
|
|
7522
8291
|
}
|
|
7523
8292
|
if (viaPlugin) {
|
|
7524
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
8293
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path35.sep}.claude${path35.sep}`) && i.names.length);
|
|
7525
8294
|
checks.push(
|
|
7526
8295
|
linked ? { level: "warn", message: "Pitroom is installed as a Claude Code plugin and linked into ~/.claude/skills: skills load twice; run `pitroom uninstall` or remove the plugin" } : { level: "ok", message: "Claude Code plugin installed (skills + session-start hook)" }
|
|
7527
8296
|
);
|
|
7528
8297
|
}
|
|
7529
8298
|
const launcher = launcherPath();
|
|
7530
|
-
if (
|
|
7531
|
-
const r =
|
|
8299
|
+
if (fs40.existsSync(launcher)) {
|
|
8300
|
+
const r = spawnSync12(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
|
|
7532
8301
|
checks.push(
|
|
7533
8302
|
r.status === 0 ? { level: "ok", message: `launcher ${launcher} \u2192 pitroom ${r.stdout.trim()}` } : { level: "fail", message: `launcher ${launcher} does not start: ${(r.stderr || r.stdout).trim().slice(0, 200)}` }
|
|
7534
8303
|
);
|
|
@@ -7538,13 +8307,13 @@ function skillChecks() {
|
|
|
7538
8307
|
return checks;
|
|
7539
8308
|
}
|
|
7540
8309
|
function geminiExtensionInstalled() {
|
|
7541
|
-
const home3 = process.env.GEMINI_CLI_HOME ??
|
|
7542
|
-
return
|
|
8310
|
+
const home3 = process.env.GEMINI_CLI_HOME ?? os12.homedir();
|
|
8311
|
+
return fs40.existsSync(path35.join(home3, ".gemini", "extensions", "pitroom"));
|
|
7543
8312
|
}
|
|
7544
8313
|
function pluginInstalled() {
|
|
7545
8314
|
try {
|
|
7546
|
-
const f =
|
|
7547
|
-
const plugins = JSON.parse(
|
|
8315
|
+
const f = path35.join(os12.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
8316
|
+
const plugins = JSON.parse(fs40.readFileSync(f, "utf8")).plugins ?? {};
|
|
7548
8317
|
return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
|
|
7549
8318
|
} catch {
|
|
7550
8319
|
return false;
|
|
@@ -7561,24 +8330,24 @@ function superpowersActive() {
|
|
|
7561
8330
|
for (const p of openCodePlugins()) {
|
|
7562
8331
|
if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
|
|
7563
8332
|
}
|
|
7564
|
-
for (const base2 of [
|
|
7565
|
-
const dir =
|
|
7566
|
-
if (
|
|
8333
|
+
for (const base2 of [path35.join(os12.homedir(), ".agents", "skills"), path35.join(os12.homedir(), ".claude", "skills")]) {
|
|
8334
|
+
const dir = path35.join(base2, "using-superpowers");
|
|
8335
|
+
if (fs40.existsSync(path35.join(dir, "SKILL.md"))) found.push(dir);
|
|
7567
8336
|
}
|
|
7568
8337
|
return found;
|
|
7569
8338
|
}
|
|
7570
8339
|
function claudePlugins() {
|
|
7571
8340
|
try {
|
|
7572
|
-
const f =
|
|
7573
|
-
return Object.keys(JSON.parse(
|
|
8341
|
+
const f = path35.join(os12.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
8342
|
+
return Object.keys(JSON.parse(fs40.readFileSync(f, "utf8")).plugins ?? {});
|
|
7574
8343
|
} catch {
|
|
7575
8344
|
return [];
|
|
7576
8345
|
}
|
|
7577
8346
|
}
|
|
7578
8347
|
function claudePluginEnabled(key) {
|
|
7579
8348
|
try {
|
|
7580
|
-
const f =
|
|
7581
|
-
return JSON.parse(
|
|
8349
|
+
const f = path35.join(os12.homedir(), ".claude", "settings.json");
|
|
8350
|
+
return JSON.parse(fs40.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
|
|
7582
8351
|
} catch {
|
|
7583
8352
|
return true;
|
|
7584
8353
|
}
|
|
@@ -7587,7 +8356,7 @@ function codexPlugins() {
|
|
|
7587
8356
|
const plugins = /* @__PURE__ */ new Map();
|
|
7588
8357
|
let text;
|
|
7589
8358
|
try {
|
|
7590
|
-
text =
|
|
8359
|
+
text = fs40.readFileSync(path35.join(process.env.CODEX_HOME ?? path35.join(os12.homedir(), ".codex"), "config.toml"), "utf8");
|
|
7591
8360
|
} catch {
|
|
7592
8361
|
return plugins;
|
|
7593
8362
|
}
|
|
@@ -7605,11 +8374,11 @@ function codexPlugins() {
|
|
|
7605
8374
|
return plugins;
|
|
7606
8375
|
}
|
|
7607
8376
|
function openCodePlugins() {
|
|
7608
|
-
const dir =
|
|
8377
|
+
const dir = path35.join(process.env.XDG_CONFIG_HOME ?? path35.join(os12.homedir(), ".config"), "opencode");
|
|
7609
8378
|
const plugins = [];
|
|
7610
8379
|
for (const name of ["opencode.json", "opencode.jsonc"]) {
|
|
7611
8380
|
try {
|
|
7612
|
-
const text =
|
|
8381
|
+
const text = fs40.readFileSync(path35.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
|
|
7613
8382
|
const list2 = JSON.parse(text).plugin;
|
|
7614
8383
|
if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
|
|
7615
8384
|
} catch {
|
|
@@ -7617,7 +8386,7 @@ function openCodePlugins() {
|
|
|
7617
8386
|
}
|
|
7618
8387
|
for (const folder of ["plugin", "plugins"]) {
|
|
7619
8388
|
try {
|
|
7620
|
-
for (const f of
|
|
8389
|
+
for (const f of fs40.readdirSync(path35.join(dir, folder))) plugins.push(path35.join(dir, folder, f));
|
|
7621
8390
|
} catch {
|
|
7622
8391
|
}
|
|
7623
8392
|
}
|
|
@@ -7639,6 +8408,9 @@ Usage
|
|
|
7639
8408
|
fix round) or of a commit range; by default on another worker
|
|
7640
8409
|
pitroom audit RUN [-W worker] another worker re-checks a read run's answer (AGREE / PARTIAL / DISAGREE);
|
|
7641
8410
|
"audit" in the config (0 to 1) does it for a share of read runs
|
|
8411
|
+
pitroom eval QUESTIONS.json [-W worker]\u2026 [--json]
|
|
8412
|
+
your questions with known answers, put to each worker and scored
|
|
8413
|
+
(definition: path:line, count: the number, set: F1 of the paths)
|
|
7642
8414
|
pitroom plan status PLAN [--json] a plan's progress: runs, STATUS, review, fix rounds, applied
|
|
7643
8415
|
pitroom plan note PLAN "Task N: \u2026" record a completion, deferred finding or ruling (outside the repo)
|
|
7644
8416
|
pitroom status [run | -g NAME] state / live progress (default: latest run)
|
|
@@ -7722,14 +8494,17 @@ Workers: ${backendIds().join(", ")} (targets: "opencode", "opencode:provider/mod
|
|
|
7722
8494
|
Env: PITROOM_WORKER, PITROOM_MODEL, PITROOM_FALLBACK="t1,t2", PITROOM_TIMEOUT, PITROOM_MAX_PARALLEL,
|
|
7723
8495
|
PITROOM_PRIMARY=sonnet|opus|haiku|gpt-5, PITROOM_PRICE="in,out", PITROOM_HOME, PITROOM_CONFIG, PITROOM_CACHE_DAYS,
|
|
7724
8496
|
PITROOM_NOTIFY, PITROOM_NOTIFY_COMMAND, PITROOM_NOTIFY_AFTER, PITROOM_MCP_DASH, PITROOM_PRICE_FEED, PITROOM_PRICE_FEED_URL,
|
|
7725
|
-
PITROOM_PRICE_FEED_HOURS,
|
|
8497
|
+
PITROOM_PRICE_FEED_HOURS, PITROOM_AUDIT, PITROOM_AUDIT_FIX, PITROOM_AUDIT_FOCUS, PITROOM_BUDGET_DAILY, PITROOM_RANK_WORKERS,
|
|
8498
|
+
PITROOM_<WORKER>_BIN
|
|
7726
8499
|
Config: ~/.config/pitroom/config.json (worker, fallback, models, tiers, timeout, primary, price, link, web, maxParallel, audit, readIn, cacheDays,
|
|
7727
|
-
countRateLimits, mcpDash, notify, notifyCommand, notifyAfter, workerPrices, priceFeed, priceFeedUrl, priceFeedHours
|
|
8500
|
+
countRateLimits, mcpDash, notify, notifyCommand, notifyAfter, workerPrices, priceFeed, priceFeedUrl, priceFeedHours,
|
|
8501
|
+
auditFix, auditFocus, budgetDaily, rankWorkers)`;
|
|
7728
8502
|
var COMMANDS = {
|
|
7729
8503
|
run: cmdRun,
|
|
7730
8504
|
crew: cmdCrew,
|
|
7731
8505
|
review: cmdReview,
|
|
7732
8506
|
audit: cmdAudit,
|
|
8507
|
+
eval: cmdEval,
|
|
7733
8508
|
cooldown: cmdCooldown,
|
|
7734
8509
|
prices: cmdPrices,
|
|
7735
8510
|
mcp: (p) => {
|
|
@@ -7738,7 +8513,7 @@ var COMMANDS = {
|
|
|
7738
8513
|
if (port !== void 0 && !(/^\d+$/.test(port) && Number(port) <= 65535)) throw new UserError("--port takes a number from 0 to 65535 (0 picks a free one)");
|
|
7739
8514
|
const dir = flag(p, "dir");
|
|
7740
8515
|
if (dir !== void 0) {
|
|
7741
|
-
if (!
|
|
8516
|
+
if (!fs41.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) throw new UserError(`-d ${dir}: no such directory`);
|
|
7742
8517
|
process.chdir(dir);
|
|
7743
8518
|
process.env.PWD = process.cwd();
|
|
7744
8519
|
}
|
|
@@ -7762,7 +8537,7 @@ var COMMANDS = {
|
|
|
7762
8537
|
statusline: cmdStatusline,
|
|
7763
8538
|
"hook-card": cmdHookCard,
|
|
7764
8539
|
"hook-start": cmdHookStart,
|
|
7765
|
-
doctor: (p) =>
|
|
8540
|
+
doctor: (p) => doctor7(has(p, "probe")),
|
|
7766
8541
|
config: cmdConfig,
|
|
7767
8542
|
init: cmdInit,
|
|
7768
8543
|
install: cmdInstall,
|