pitroom 0.21.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 +38 -0
- package/README.md +38 -5
- package/dist/lib.mjs +1236 -660
- package/dist/pitroom.mjs +1276 -498
- package/dist/ui/app.css +1 -1
- package/dist/ui/app.js +32 -30
- package/docs/backends.md +58 -2
- 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) {
|
|
@@ -843,6 +846,7 @@ function invocation3(req) {
|
|
|
843
846
|
"none"
|
|
844
847
|
];
|
|
845
848
|
for (const p of policies) args.push("--policy", path5.join(dir, p));
|
|
849
|
+
if (req.sessionId) args.push("--resume", req.sessionId);
|
|
846
850
|
if (process.env.PITROOM_GEMINI_TRUST === "1") args.push("--skip-trust");
|
|
847
851
|
const [model] = (req.model ?? "").split("#");
|
|
848
852
|
if (model) args.push("--model", model);
|
|
@@ -929,7 +933,7 @@ function catalog3() {
|
|
|
929
933
|
var gemini = {
|
|
930
934
|
id: "gemini",
|
|
931
935
|
name: "Gemini CLI",
|
|
932
|
-
capabilities: { readOnly: "approval-mode", resume: "
|
|
936
|
+
capabilities: { readOnly: "approval-mode", resume: "by-id", reportsCost: false, attachFiles: false },
|
|
933
937
|
binary: binary3,
|
|
934
938
|
catalog: catalog3,
|
|
935
939
|
invocation: invocation3,
|
|
@@ -939,6 +943,10 @@ var gemini = {
|
|
|
939
943
|
doctor: doctor3
|
|
940
944
|
};
|
|
941
945
|
|
|
946
|
+
// src/backends/kilo/index.ts
|
|
947
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
948
|
+
import path7 from "node:path";
|
|
949
|
+
|
|
942
950
|
// src/backends/opencode/index.ts
|
|
943
951
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
944
952
|
|
|
@@ -1148,17 +1156,19 @@ var FORBIDDEN_BASH = [
|
|
|
1148
1156
|
"twine upload*",
|
|
1149
1157
|
// no recursive delegation
|
|
1150
1158
|
"opencode*",
|
|
1159
|
+
"kilo *",
|
|
1160
|
+
"kilocode*",
|
|
1151
1161
|
"pitroom *",
|
|
1152
1162
|
"*pitroom*"
|
|
1153
1163
|
];
|
|
1154
|
-
function scratchDirs() {
|
|
1164
|
+
function scratchDirs(app) {
|
|
1155
1165
|
const data = process.env.XDG_DATA_HOME ?? path6.join(os6.homedir(), ".local", "share");
|
|
1156
1166
|
const rules = {
|
|
1157
1167
|
"*": "deny",
|
|
1158
|
-
[path6.join(data,
|
|
1159
|
-
[path6.join(data,
|
|
1168
|
+
[path6.join(data, app, "tool-output", "*")]: "allow",
|
|
1169
|
+
[path6.join(data, app, "shell", "*", "*")]: "allow"
|
|
1160
1170
|
};
|
|
1161
|
-
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";
|
|
1162
1172
|
return rules;
|
|
1163
1173
|
}
|
|
1164
1174
|
function realpath(p) {
|
|
@@ -1179,7 +1189,7 @@ var COMMON = {
|
|
|
1179
1189
|
doom_loop: "deny"
|
|
1180
1190
|
};
|
|
1181
1191
|
var web = (on) => ({ webfetch: on ? "allow" : "deny", websearch: on ? "allow" : "deny" });
|
|
1182
|
-
function readProfile(allowWeb = false) {
|
|
1192
|
+
function readProfile(allowWeb = false, app = "opencode") {
|
|
1183
1193
|
return {
|
|
1184
1194
|
"*": "deny",
|
|
1185
1195
|
read: READ_FILES,
|
|
@@ -1195,11 +1205,11 @@ function readProfile(allowWeb = false) {
|
|
|
1195
1205
|
bash: READ_ONLY_BASH,
|
|
1196
1206
|
shell: READ_ONLY_BASH,
|
|
1197
1207
|
edit: "deny",
|
|
1198
|
-
external_directory: scratchDirs(),
|
|
1208
|
+
external_directory: scratchDirs(app),
|
|
1199
1209
|
...COMMON
|
|
1200
1210
|
};
|
|
1201
1211
|
}
|
|
1202
|
-
function writeProfile(allowWeb = false) {
|
|
1212
|
+
function writeProfile(allowWeb = false, app = "opencode") {
|
|
1203
1213
|
const bash = { "*": "allow" };
|
|
1204
1214
|
for (const pattern of FORBIDDEN_BASH) bash[pattern] = "deny";
|
|
1205
1215
|
return {
|
|
@@ -1208,25 +1218,26 @@ function writeProfile(allowWeb = false) {
|
|
|
1208
1218
|
...web(allowWeb),
|
|
1209
1219
|
bash,
|
|
1210
1220
|
shell: bash,
|
|
1211
|
-
external_directory: scratchDirs(),
|
|
1221
|
+
external_directory: scratchDirs(app),
|
|
1212
1222
|
...COMMON
|
|
1213
1223
|
};
|
|
1214
1224
|
}
|
|
1215
1225
|
function agentFor(mode) {
|
|
1216
1226
|
return mode === "read" ? AGENT.read : AGENT.write;
|
|
1217
1227
|
}
|
|
1218
|
-
function configContent(existing, allowWeb = false) {
|
|
1228
|
+
function configContent(existing, allowWeb = false, app = "opencode", extra = {}) {
|
|
1219
1229
|
const ours = {
|
|
1230
|
+
...extra,
|
|
1220
1231
|
agent: {
|
|
1221
1232
|
[AGENT.read]: {
|
|
1222
1233
|
mode: "primary",
|
|
1223
1234
|
description: "pitroom: read-only worker (research, search, review)",
|
|
1224
|
-
permission: readProfile(allowWeb)
|
|
1235
|
+
permission: readProfile(allowWeb, app)
|
|
1225
1236
|
},
|
|
1226
1237
|
[AGENT.write]: {
|
|
1227
1238
|
mode: "primary",
|
|
1228
1239
|
description: "pitroom: editing worker (no git history changes, no bulk deletes)",
|
|
1229
|
-
permission: writeProfile(allowWeb)
|
|
1240
|
+
permission: writeProfile(allowWeb, app)
|
|
1230
1241
|
}
|
|
1231
1242
|
}
|
|
1232
1243
|
};
|
|
@@ -1403,10 +1414,348 @@ var opencode = {
|
|
|
1403
1414
|
doctor: doctor4
|
|
1404
1415
|
};
|
|
1405
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
|
+
|
|
1406
1755
|
// src/backends/index.ts
|
|
1407
|
-
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]));
|
|
1408
1757
|
var DEFAULT_BACKEND = opencode.id;
|
|
1409
|
-
var PLANNED = [];
|
|
1758
|
+
var PLANNED = ["hermes"];
|
|
1410
1759
|
var backendIds = () => [...REGISTRY.keys()];
|
|
1411
1760
|
var allBackends = () => [...REGISTRY.values()];
|
|
1412
1761
|
var isBackendId = (id) => REGISTRY.has(id) || PLANNED.includes(id);
|
|
@@ -1421,12 +1770,12 @@ function getBackend(id) {
|
|
|
1421
1770
|
}
|
|
1422
1771
|
|
|
1423
1772
|
// src/cli/args.ts
|
|
1424
|
-
import
|
|
1773
|
+
import fs10 from "node:fs";
|
|
1425
1774
|
|
|
1426
1775
|
// src/core/config.ts
|
|
1427
|
-
import
|
|
1428
|
-
import
|
|
1429
|
-
import
|
|
1776
|
+
import fs9 from "node:fs";
|
|
1777
|
+
import os8 from "node:os";
|
|
1778
|
+
import path9 from "node:path";
|
|
1430
1779
|
var SCHEMA = {
|
|
1431
1780
|
worker: "string",
|
|
1432
1781
|
fallback: "string[]",
|
|
@@ -1440,6 +1789,8 @@ var SCHEMA = {
|
|
|
1440
1789
|
tiers: "record",
|
|
1441
1790
|
costs: "numbers",
|
|
1442
1791
|
audit: "number",
|
|
1792
|
+
auditFix: "boolean",
|
|
1793
|
+
auditFocus: "boolean",
|
|
1443
1794
|
readIn: "string",
|
|
1444
1795
|
cacheDays: "number",
|
|
1445
1796
|
countRateLimits: "boolean",
|
|
@@ -1450,12 +1801,14 @@ var SCHEMA = {
|
|
|
1450
1801
|
workerPrices: "record",
|
|
1451
1802
|
priceFeed: "boolean",
|
|
1452
1803
|
priceFeedUrl: "string",
|
|
1453
|
-
priceFeedHours: "number"
|
|
1804
|
+
priceFeedHours: "number",
|
|
1805
|
+
budgetDaily: "number",
|
|
1806
|
+
rankWorkers: "boolean"
|
|
1454
1807
|
};
|
|
1455
1808
|
function configPath() {
|
|
1456
|
-
if (process.env.PITROOM_CONFIG) return
|
|
1457
|
-
const base2 = process.platform === "win32" ? process.env.APPDATA ??
|
|
1458
|
-
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");
|
|
1459
1812
|
}
|
|
1460
1813
|
var cached;
|
|
1461
1814
|
function loadConfig() {
|
|
@@ -1463,10 +1816,10 @@ function loadConfig() {
|
|
|
1463
1816
|
const file2 = configPath();
|
|
1464
1817
|
const config = {};
|
|
1465
1818
|
const warnings = [];
|
|
1466
|
-
if (
|
|
1819
|
+
if (fs9.existsSync(file2)) {
|
|
1467
1820
|
let raw;
|
|
1468
1821
|
try {
|
|
1469
|
-
raw = JSON.parse(
|
|
1822
|
+
raw = JSON.parse(fs9.readFileSync(file2, "utf8"));
|
|
1470
1823
|
} catch (e) {
|
|
1471
1824
|
warnings.push(`${file2}: invalid JSON (${e.message}); ignored`);
|
|
1472
1825
|
}
|
|
@@ -1508,6 +1861,10 @@ var rate = (v) => {
|
|
|
1508
1861
|
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
1509
1862
|
return typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.min(n, 1) : void 0;
|
|
1510
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
|
+
};
|
|
1511
1868
|
var days = (v) => {
|
|
1512
1869
|
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
1513
1870
|
return typeof n === "number" && Number.isInteger(n) && n >= 0 ? n : void 0;
|
|
@@ -1542,6 +1899,8 @@ function effective(flags = {}) {
|
|
|
1542
1899
|
tiers: setting(void 0, void 0, c.tiers, {}),
|
|
1543
1900
|
costs: setting(void 0, void 0, c.costs, {}),
|
|
1544
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),
|
|
1545
1904
|
readIn: setting(void 0, readIn(e.PITROOM_READ_IN), readIn(c.readIn), "auto"),
|
|
1546
1905
|
cacheDays: setting(void 0, days(e.PITROOM_CACHE_DAYS), days(c.cacheDays), 7),
|
|
1547
1906
|
countRateLimits: setting(void 0, void 0, c.countRateLimits, false),
|
|
@@ -1552,7 +1911,9 @@ function effective(flags = {}) {
|
|
|
1552
1911
|
workerPrices: setting(void 0, void 0, c.workerPrices, {}),
|
|
1553
1912
|
priceFeed: setting(void 0, flag01(e.PITROOM_PRICE_FEED), c.priceFeed, false),
|
|
1554
1913
|
priceFeedUrl: setting(void 0, e.PITROOM_PRICE_FEED_URL?.trim() || void 0, c.priceFeedUrl?.trim() || void 0, "https://models.dev/api.json"),
|
|
1555
|
-
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)
|
|
1556
1917
|
};
|
|
1557
1918
|
}
|
|
1558
1919
|
|
|
@@ -1688,9 +2049,9 @@ function exitCodeFor(meta) {
|
|
|
1688
2049
|
}
|
|
1689
2050
|
function readTask(p) {
|
|
1690
2051
|
const file2 = flag(p, "task-file");
|
|
1691
|
-
if (file2) return
|
|
2052
|
+
if (file2) return fs10.readFileSync(file2, "utf8");
|
|
1692
2053
|
const words = p.positional;
|
|
1693
|
-
if (words.length === 1 && words[0] === "-") return
|
|
2054
|
+
if (words.length === 1 && words[0] === "-") return fs10.readFileSync(0, "utf8");
|
|
1694
2055
|
return words.join(" ");
|
|
1695
2056
|
}
|
|
1696
2057
|
function runOptions(p, task) {
|
|
@@ -1738,16 +2099,16 @@ function effortFlag(p) {
|
|
|
1738
2099
|
import readline from "node:readline";
|
|
1739
2100
|
|
|
1740
2101
|
// src/core/run.ts
|
|
1741
|
-
import { spawn as spawn4, spawnSync as
|
|
1742
|
-
import
|
|
1743
|
-
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";
|
|
1744
2105
|
|
|
1745
2106
|
// src/vcs/git.ts
|
|
1746
|
-
import { spawnSync as
|
|
2107
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
1747
2108
|
import crypto2 from "node:crypto";
|
|
1748
|
-
import
|
|
1749
|
-
import
|
|
1750
|
-
import
|
|
2109
|
+
import fs11 from "node:fs";
|
|
2110
|
+
import os9 from "node:os";
|
|
2111
|
+
import path10 from "node:path";
|
|
1751
2112
|
var SAFE = [
|
|
1752
2113
|
"-c",
|
|
1753
2114
|
"color.ui=false",
|
|
@@ -1759,7 +2120,7 @@ var SAFE = [
|
|
|
1759
2120
|
"diff.mnemonicPrefix=false"
|
|
1760
2121
|
];
|
|
1761
2122
|
function git(cwd, args, env) {
|
|
1762
|
-
const r =
|
|
2123
|
+
const r = spawnSync7("git", [...SAFE, ...args], {
|
|
1763
2124
|
cwd,
|
|
1764
2125
|
env: env ?? process.env,
|
|
1765
2126
|
encoding: "utf8",
|
|
@@ -1779,31 +2140,31 @@ function gitAvailable() {
|
|
|
1779
2140
|
}
|
|
1780
2141
|
function canonical(dir) {
|
|
1781
2142
|
try {
|
|
1782
|
-
return
|
|
2143
|
+
return fs11.realpathSync.native(dir);
|
|
1783
2144
|
} catch {
|
|
1784
2145
|
return dir;
|
|
1785
2146
|
}
|
|
1786
2147
|
}
|
|
1787
2148
|
function repoRoot(dir) {
|
|
1788
2149
|
const r = git(dir, ["rev-parse", "--show-toplevel"]);
|
|
1789
|
-
return r.code === 0 ? canonical(
|
|
2150
|
+
return r.code === 0 ? canonical(path10.resolve(r.stdout.trim())) : void 0;
|
|
1790
2151
|
}
|
|
1791
2152
|
function snapshotTree(root, exclude = [], drop2 = []) {
|
|
1792
|
-
const tmp =
|
|
1793
|
-
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());
|
|
1794
2155
|
const env = { ...process.env, GIT_INDEX_FILE: tmp };
|
|
1795
2156
|
try {
|
|
1796
|
-
if (
|
|
2157
|
+
if (fs11.existsSync(real)) fs11.copyFileSync(real, tmp);
|
|
1797
2158
|
else must(root, ["read-tree", "--empty"], env);
|
|
1798
2159
|
must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`), ...drop2.map((p) => `:(top,literal,exclude)${p}`)], env);
|
|
1799
2160
|
if (drop2.length) {
|
|
1800
|
-
const r =
|
|
2161
|
+
const r = spawnSync7("git", ["update-index", "--force-remove", "-z", "--stdin"], { cwd: root, env, input: `${drop2.join("\0")}\0`, encoding: "utf8" });
|
|
1801
2162
|
if (r.status !== 0) throw new Error(`git update-index failed: ${r.stderr.trim()}`);
|
|
1802
2163
|
}
|
|
1803
2164
|
return must(root, ["write-tree"], env).trim();
|
|
1804
2165
|
} finally {
|
|
1805
|
-
|
|
1806
|
-
|
|
2166
|
+
fs11.rmSync(tmp, { force: true });
|
|
2167
|
+
fs11.rmSync(`${tmp}.lock`, { force: true });
|
|
1807
2168
|
}
|
|
1808
2169
|
}
|
|
1809
2170
|
var DIFF = ["diff", "--no-ext-diff", "--no-textconv", "--no-renames"];
|
|
@@ -1823,27 +2184,27 @@ function diffTrees(root, a, b) {
|
|
|
1823
2184
|
return { changes, stats: { files: changes.length, insertions, deletions }, patch };
|
|
1824
2185
|
}
|
|
1825
2186
|
function createIsolatedCopy(root, tree, dest) {
|
|
1826
|
-
const objects =
|
|
1827
|
-
|
|
1828
|
-
must(
|
|
1829
|
-
|
|
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}
|
|
1830
2191
|
`);
|
|
1831
2192
|
must(dest, ["read-tree", tree]);
|
|
1832
2193
|
must(dest, ["checkout-index", "--all", "--force"]);
|
|
1833
2194
|
}
|
|
1834
2195
|
function removeIsolatedCopy(dest, ownedBy) {
|
|
1835
|
-
const rel =
|
|
1836
|
-
if (!rel || rel.startsWith("..") ||
|
|
1837
|
-
|
|
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 });
|
|
1838
2199
|
}
|
|
1839
2200
|
function linkIntoWorktree(root, worktree, rels) {
|
|
1840
2201
|
const linked = [];
|
|
1841
2202
|
for (const rel of rels) {
|
|
1842
|
-
const src =
|
|
1843
|
-
const dst =
|
|
1844
|
-
if (!
|
|
1845
|
-
|
|
1846
|
-
|
|
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);
|
|
1847
2208
|
linked.push(rel);
|
|
1848
2209
|
}
|
|
1849
2210
|
return linked;
|
|
@@ -1949,11 +2310,11 @@ function resolveChain(flags = {}) {
|
|
|
1949
2310
|
|
|
1950
2311
|
// src/core/process.ts
|
|
1951
2312
|
import { spawn } from "node:child_process";
|
|
1952
|
-
import
|
|
2313
|
+
import fs13 from "node:fs";
|
|
1953
2314
|
|
|
1954
2315
|
// src/vcs/guard.ts
|
|
1955
|
-
import
|
|
1956
|
-
import
|
|
2316
|
+
import fs12 from "node:fs";
|
|
2317
|
+
import path11 from "node:path";
|
|
1957
2318
|
var VERSION = 2;
|
|
1958
2319
|
var WINDOWS = process.platform === "win32";
|
|
1959
2320
|
var fwd = (p) => WINDOWS ? p.replace(/\\/g, "/") : p;
|
|
@@ -2030,29 +2391,29 @@ esac
|
|
|
2030
2391
|
exec "$real" "$@"
|
|
2031
2392
|
`;
|
|
2032
2393
|
function shimDir() {
|
|
2033
|
-
return
|
|
2394
|
+
return path11.join(home(), "shim", `v${VERSION}`);
|
|
2034
2395
|
}
|
|
2035
2396
|
function findRealGit(skip) {
|
|
2036
2397
|
const same2 = (a, b) => WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
2037
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
2038
|
-
if (same2(
|
|
2039
|
-
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");
|
|
2040
2401
|
try {
|
|
2041
|
-
|
|
2042
|
-
if (
|
|
2402
|
+
fs12.accessSync(p, fs12.constants.X_OK);
|
|
2403
|
+
if (fs12.statSync(p).isFile()) return p;
|
|
2043
2404
|
} catch {
|
|
2044
2405
|
}
|
|
2045
2406
|
}
|
|
2046
2407
|
return void 0;
|
|
2047
2408
|
}
|
|
2048
2409
|
function findGitSh(realGit) {
|
|
2049
|
-
let dir =
|
|
2410
|
+
let dir = path11.dirname(realGit);
|
|
2050
2411
|
for (let i = 0; i < 4; i++) {
|
|
2051
2412
|
for (const rel of ["bin/sh.exe", "usr/bin/sh.exe"]) {
|
|
2052
|
-
const p =
|
|
2053
|
-
if (
|
|
2413
|
+
const p = path11.join(dir, rel);
|
|
2414
|
+
if (fs12.existsSync(p)) return p;
|
|
2054
2415
|
}
|
|
2055
|
-
const up =
|
|
2416
|
+
const up = path11.dirname(dir);
|
|
2056
2417
|
if (up === dir) break;
|
|
2057
2418
|
dir = up;
|
|
2058
2419
|
}
|
|
@@ -2070,12 +2431,12 @@ var REF_HOOK = `#!/bin/sh
|
|
|
2070
2431
|
echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
|
|
2071
2432
|
exit 1
|
|
2072
2433
|
`;
|
|
2073
|
-
var hooksDir = () =>
|
|
2434
|
+
var hooksDir = () => path11.join(home(), "git-hooks", `v${HOOK_VERSION}`);
|
|
2074
2435
|
function writeIfChanged(file2, content) {
|
|
2075
|
-
if (
|
|
2076
|
-
|
|
2436
|
+
if (fs12.existsSync(file2) && fs12.readFileSync(file2, "utf8") === content) return;
|
|
2437
|
+
fs12.mkdirSync(path11.dirname(file2), { recursive: true });
|
|
2077
2438
|
const tmp = `${file2}.${process.pid}.tmp`;
|
|
2078
|
-
|
|
2439
|
+
fs12.writeFileSync(tmp, content, { mode: 493 });
|
|
2079
2440
|
renameOver(tmp, file2);
|
|
2080
2441
|
}
|
|
2081
2442
|
function pathKey(env) {
|
|
@@ -2107,7 +2468,7 @@ function guardEnv(env) {
|
|
|
2107
2468
|
GIT_PAGER: "cat",
|
|
2108
2469
|
PAGER: "cat"
|
|
2109
2470
|
};
|
|
2110
|
-
writeIfChanged(
|
|
2471
|
+
writeIfChanged(path11.join(hooksDir(), "reference-transaction"), REF_HOOK);
|
|
2111
2472
|
env = withGitConfig(onePath(env), [
|
|
2112
2473
|
["core.hooksPath", fwd(hooksDir())],
|
|
2113
2474
|
["url.pitroom-push-blocked://.pushInsteadOf", ""]
|
|
@@ -2119,11 +2480,11 @@ function guardEnv(env) {
|
|
|
2119
2480
|
if (WINDOWS) {
|
|
2120
2481
|
sh = findGitSh(real);
|
|
2121
2482
|
if (!sh) return { ...env, ...quiet };
|
|
2122
|
-
writeIfChanged(
|
|
2483
|
+
writeIfChanged(path11.join(dir, "git.cmd"), cmdShim(sh));
|
|
2123
2484
|
}
|
|
2124
|
-
writeIfChanged(
|
|
2485
|
+
writeIfChanged(path11.join(dir, "git"), SHIM);
|
|
2125
2486
|
const key = pathKey(env);
|
|
2126
|
-
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] ?? ""}` };
|
|
2127
2488
|
}
|
|
2128
2489
|
function shimReady() {
|
|
2129
2490
|
const env = guardEnv({ ...process.env });
|
|
@@ -2132,8 +2493,8 @@ function shimReady() {
|
|
|
2132
2493
|
|
|
2133
2494
|
// src/core/process.ts
|
|
2134
2495
|
async function spawnWorker(inv, opts) {
|
|
2135
|
-
const out =
|
|
2136
|
-
const err =
|
|
2496
|
+
const out = fs13.openSync(opts.stdoutFile, "w");
|
|
2497
|
+
const err = fs13.openSync(opts.stderrFile, "w");
|
|
2137
2498
|
const res = { code: null, timedOut: false, stopped: false };
|
|
2138
2499
|
const child = spawn(inv.command, inv.args, {
|
|
2139
2500
|
cwd: opts.cwd,
|
|
@@ -2156,7 +2517,7 @@ async function spawnWorker(inv, opts) {
|
|
|
2156
2517
|
}, opts.timeoutSec * 1e3);
|
|
2157
2518
|
res.code = await new Promise((resolve2) => {
|
|
2158
2519
|
child.on("error", (e) => {
|
|
2159
|
-
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;
|
|
2160
2521
|
resolve2(127);
|
|
2161
2522
|
});
|
|
2162
2523
|
child.on("close", (c) => resolve2(c));
|
|
@@ -2165,8 +2526,8 @@ async function spawnWorker(inv, opts) {
|
|
|
2165
2526
|
if (killer) clearTimeout(killer);
|
|
2166
2527
|
process.off("SIGINT", stop);
|
|
2167
2528
|
process.off("SIGTERM", stop);
|
|
2168
|
-
|
|
2169
|
-
|
|
2529
|
+
fs13.closeSync(out);
|
|
2530
|
+
fs13.closeSync(err);
|
|
2170
2531
|
return res;
|
|
2171
2532
|
}
|
|
2172
2533
|
|
|
@@ -2198,12 +2559,12 @@ function parseAudit(text) {
|
|
|
2198
2559
|
}
|
|
2199
2560
|
|
|
2200
2561
|
// src/core/cooldown.ts
|
|
2201
|
-
import
|
|
2202
|
-
import
|
|
2562
|
+
import fs14 from "node:fs";
|
|
2563
|
+
import path12 from "node:path";
|
|
2203
2564
|
var MAX_MS = 24 * 36e5;
|
|
2204
2565
|
var OVERLOAD_MS = 20 * 6e4;
|
|
2205
2566
|
var DAILY_MS = 4 * 36e5;
|
|
2206
|
-
var file = () =>
|
|
2567
|
+
var file = () => path12.join(home(), "cooldowns.json");
|
|
2207
2568
|
function cooldownKey(target, backend) {
|
|
2208
2569
|
const model = (target.model ?? backend.defaultModel?.() ?? "").split("#")[0];
|
|
2209
2570
|
return `${target.backend}:${model}`;
|
|
@@ -2226,16 +2587,16 @@ function cooldownMs(message) {
|
|
|
2226
2587
|
}
|
|
2227
2588
|
function read() {
|
|
2228
2589
|
try {
|
|
2229
|
-
const raw = JSON.parse(
|
|
2590
|
+
const raw = JSON.parse(fs14.readFileSync(file(), "utf8"));
|
|
2230
2591
|
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
2231
2592
|
} catch {
|
|
2232
2593
|
return {};
|
|
2233
2594
|
}
|
|
2234
2595
|
}
|
|
2235
2596
|
function write(all) {
|
|
2236
|
-
|
|
2597
|
+
fs14.mkdirSync(path12.dirname(file()), { recursive: true });
|
|
2237
2598
|
const tmp = `${file()}.${process.pid}.tmp`;
|
|
2238
|
-
|
|
2599
|
+
fs14.writeFileSync(tmp, `${JSON.stringify(all, null, 2)}
|
|
2239
2600
|
`);
|
|
2240
2601
|
renameOver(tmp, file());
|
|
2241
2602
|
}
|
|
@@ -2265,14 +2626,14 @@ function untilText(c, now = Date.now()) {
|
|
|
2265
2626
|
}
|
|
2266
2627
|
|
|
2267
2628
|
// src/core/snapshot.ts
|
|
2268
|
-
import
|
|
2269
|
-
import
|
|
2629
|
+
import fs16 from "node:fs";
|
|
2630
|
+
import path14 from "node:path";
|
|
2270
2631
|
import crypto3 from "node:crypto";
|
|
2271
2632
|
|
|
2272
2633
|
// src/core/secrets.ts
|
|
2273
2634
|
import { execFileSync } from "node:child_process";
|
|
2274
|
-
import
|
|
2275
|
-
import
|
|
2635
|
+
import fs15 from "node:fs";
|
|
2636
|
+
import path13 from "node:path";
|
|
2276
2637
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
2277
2638
|
var MAX_VISITED = 2e4;
|
|
2278
2639
|
var MAX_DEPTH = 4;
|
|
@@ -2290,16 +2651,16 @@ function findSecretFiles(dir) {
|
|
|
2290
2651
|
if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
|
|
2291
2652
|
let entries;
|
|
2292
2653
|
try {
|
|
2293
|
-
entries =
|
|
2654
|
+
entries = fs15.readdirSync(d, { withFileTypes: true });
|
|
2294
2655
|
} catch {
|
|
2295
2656
|
return;
|
|
2296
2657
|
}
|
|
2297
2658
|
for (const e of entries) {
|
|
2298
2659
|
visited++;
|
|
2299
2660
|
if (e.isDirectory()) {
|
|
2300
|
-
if (!SKIP_DIRS.has(e.name)) walk(
|
|
2661
|
+
if (!SKIP_DIRS.has(e.name)) walk(path13.join(d, e.name), depth + 1);
|
|
2301
2662
|
} else if (e.isFile() && looksSecret(e.name)) {
|
|
2302
|
-
found.push(slash(
|
|
2663
|
+
found.push(slash(path13.relative(dir, path13.join(d, e.name))));
|
|
2303
2664
|
}
|
|
2304
2665
|
}
|
|
2305
2666
|
};
|
|
@@ -2309,7 +2670,7 @@ function findSecretFiles(dir) {
|
|
|
2309
2670
|
function findSecretFilesInTree(root, dir) {
|
|
2310
2671
|
try {
|
|
2311
2672
|
const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
2312
|
-
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();
|
|
2313
2674
|
} catch {
|
|
2314
2675
|
return [];
|
|
2315
2676
|
}
|
|
@@ -2321,7 +2682,7 @@ function secretWarning(files, mode) {
|
|
|
2321
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";
|
|
2322
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`;
|
|
2323
2684
|
}
|
|
2324
|
-
var slash = (p) => p.split(
|
|
2685
|
+
var slash = (p) => p.split(path13.sep).join("/");
|
|
2325
2686
|
|
|
2326
2687
|
// src/core/snapshot.ts
|
|
2327
2688
|
var READ_IN = ["auto", "snapshot", "project"];
|
|
@@ -2329,7 +2690,7 @@ var KEEP_MS = 24 * 36e5;
|
|
|
2329
2690
|
var PRUNE_AGE_MS = 3 * 24 * 36e5;
|
|
2330
2691
|
var KEEP_NEWEST = 6;
|
|
2331
2692
|
var JUST_USED_MS = 10 * 6e4;
|
|
2332
|
-
var snapshotsDir = () =>
|
|
2693
|
+
var snapshotsDir = () => path14.join(home(), "snapshots");
|
|
2333
2694
|
function wantSnapshot(readIn2, root, dir) {
|
|
2334
2695
|
if (!root || readIn2 === "project") return void 0;
|
|
2335
2696
|
const secrets = findSecretFiles(dir);
|
|
@@ -2338,19 +2699,19 @@ function wantSnapshot(readIn2, root, dir) {
|
|
|
2338
2699
|
function readSnapshot(root) {
|
|
2339
2700
|
const drop2 = findSecretFilesInTree(root, root);
|
|
2340
2701
|
const tree = snapshotTree(root, [], drop2);
|
|
2341
|
-
const dest =
|
|
2342
|
-
if (
|
|
2702
|
+
const dest = path14.join(snapshotsDir(), tree);
|
|
2703
|
+
if (fs16.existsSync(path14.join(dest, ".git"))) {
|
|
2343
2704
|
touch(dest);
|
|
2344
2705
|
return { dir: dest, tree, created: false };
|
|
2345
2706
|
}
|
|
2346
|
-
const tmp =
|
|
2707
|
+
const tmp = path14.join(snapshotsDir(), `.tmp-${process.pid}-${crypto3.randomBytes(3).toString("hex")}`);
|
|
2347
2708
|
try {
|
|
2348
2709
|
createIsolatedCopy(root, tree, tmp);
|
|
2349
2710
|
readOnly(tmp);
|
|
2350
2711
|
try {
|
|
2351
|
-
|
|
2712
|
+
fs16.renameSync(tmp, dest);
|
|
2352
2713
|
} catch (e) {
|
|
2353
|
-
if (!
|
|
2714
|
+
if (!fs16.existsSync(path14.join(dest, ".git"))) throw e;
|
|
2354
2715
|
removeIsolatedCopy(tmp, snapshotsDir());
|
|
2355
2716
|
}
|
|
2356
2717
|
} catch (e) {
|
|
@@ -2366,17 +2727,17 @@ function readSnapshot(root) {
|
|
|
2366
2727
|
function touch(dir) {
|
|
2367
2728
|
try {
|
|
2368
2729
|
const now = /* @__PURE__ */ new Date();
|
|
2369
|
-
|
|
2730
|
+
fs16.utimesSync(dir, now, now);
|
|
2370
2731
|
} catch {
|
|
2371
2732
|
}
|
|
2372
2733
|
}
|
|
2373
2734
|
function readOnly(dir) {
|
|
2374
2735
|
const walk = (d) => {
|
|
2375
|
-
for (const e of
|
|
2736
|
+
for (const e of fs16.readdirSync(d, { withFileTypes: true })) {
|
|
2376
2737
|
if (e.name === ".git") continue;
|
|
2377
|
-
const p =
|
|
2738
|
+
const p = path14.join(d, e.name);
|
|
2378
2739
|
if (e.isDirectory()) walk(p);
|
|
2379
|
-
else if (e.isFile())
|
|
2740
|
+
else if (e.isFile()) fs16.chmodSync(p, fs16.statSync(p).mode & ~146);
|
|
2380
2741
|
}
|
|
2381
2742
|
};
|
|
2382
2743
|
try {
|
|
@@ -2387,7 +2748,7 @@ function readOnly(dir) {
|
|
|
2387
2748
|
function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWEST) {
|
|
2388
2749
|
let names;
|
|
2389
2750
|
try {
|
|
2390
|
-
names =
|
|
2751
|
+
names = fs16.readdirSync(snapshotsDir());
|
|
2391
2752
|
} catch {
|
|
2392
2753
|
return 0;
|
|
2393
2754
|
}
|
|
@@ -2395,14 +2756,14 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
|
|
|
2395
2756
|
for (const id of listRunIds()) {
|
|
2396
2757
|
try {
|
|
2397
2758
|
const m = readMeta(id);
|
|
2398
|
-
if (m.snapshot?.dir && isActive(m.state)) inUse.add(
|
|
2759
|
+
if (m.snapshot?.dir && isActive(m.state)) inUse.add(path14.resolve(m.snapshot.dir));
|
|
2399
2760
|
} catch {
|
|
2400
2761
|
}
|
|
2401
2762
|
}
|
|
2402
2763
|
const entries = names.map((n) => {
|
|
2403
|
-
const dir =
|
|
2764
|
+
const dir = path14.join(snapshotsDir(), n);
|
|
2404
2765
|
try {
|
|
2405
|
-
return { n, dir, age: now -
|
|
2766
|
+
return { n, dir, age: now - fs16.statSync(dir).mtimeMs };
|
|
2406
2767
|
} catch {
|
|
2407
2768
|
return void 0;
|
|
2408
2769
|
}
|
|
@@ -2410,7 +2771,7 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
|
|
|
2410
2771
|
let removed = 0;
|
|
2411
2772
|
let rank = 0;
|
|
2412
2773
|
for (const e of entries) {
|
|
2413
|
-
if (inUse.has(
|
|
2774
|
+
if (inUse.has(path14.resolve(e.dir))) continue;
|
|
2414
2775
|
const half = e.n.startsWith(".tmp-");
|
|
2415
2776
|
const old = e.age >= (half ? KEEP_MS : maxAgeMs);
|
|
2416
2777
|
const surplus = !half && ++rank > keepNewest && e.age >= JUST_USED_MS;
|
|
@@ -2426,10 +2787,10 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
|
|
|
2426
2787
|
}
|
|
2427
2788
|
function makeWritable(dir) {
|
|
2428
2789
|
const walk = (d) => {
|
|
2429
|
-
for (const e of
|
|
2430
|
-
const p =
|
|
2790
|
+
for (const e of fs16.readdirSync(d, { withFileTypes: true })) {
|
|
2791
|
+
const p = path14.join(d, e.name);
|
|
2431
2792
|
if (e.isDirectory()) walk(p);
|
|
2432
|
-
else if (e.isFile())
|
|
2793
|
+
else if (e.isFile()) fs16.chmodSync(p, fs16.statSync(p).mode | 128);
|
|
2433
2794
|
}
|
|
2434
2795
|
};
|
|
2435
2796
|
try {
|
|
@@ -2440,17 +2801,17 @@ function makeWritable(dir) {
|
|
|
2440
2801
|
function backToProject(text, snapshotDir, root) {
|
|
2441
2802
|
let real = snapshotDir;
|
|
2442
2803
|
try {
|
|
2443
|
-
real =
|
|
2804
|
+
real = fs16.realpathSync(snapshotDir);
|
|
2444
2805
|
} catch {
|
|
2445
2806
|
}
|
|
2446
2807
|
let out = text;
|
|
2447
|
-
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);
|
|
2448
2809
|
return out;
|
|
2449
2810
|
}
|
|
2450
2811
|
|
|
2451
2812
|
// src/core/audit.ts
|
|
2452
2813
|
import crypto4 from "node:crypto";
|
|
2453
|
-
import
|
|
2814
|
+
import path15 from "node:path";
|
|
2454
2815
|
var ANSWER_MAX = 8e3;
|
|
2455
2816
|
function auditRate(meta) {
|
|
2456
2817
|
return meta.auditRate ?? effective().audit.value;
|
|
@@ -2461,10 +2822,27 @@ function draw(id) {
|
|
|
2461
2822
|
function auditable(meta, answer) {
|
|
2462
2823
|
return meta.mode === "read" && meta.state === "done" && !meta.reviewOf && !meta.auditOf && !meta.audit && !!answer.trim();
|
|
2463
2824
|
}
|
|
2464
|
-
function sampled(meta) {
|
|
2465
|
-
const rate2 = auditRate(meta);
|
|
2825
|
+
function sampled(meta, rate2 = auditRate(meta)) {
|
|
2466
2826
|
return rate2 > 0 && (rate2 >= 1 || draw(meta.id) < rate2);
|
|
2467
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
|
+
}
|
|
2468
2846
|
function pickAuditor(meta) {
|
|
2469
2847
|
const eff = effective();
|
|
2470
2848
|
const tiers = eff.tiers.value;
|
|
@@ -2477,9 +2855,9 @@ function relativeToAuditor(answer, meta) {
|
|
|
2477
2855
|
let out = answer;
|
|
2478
2856
|
const roots = [...new Set([meta.dir, meta.repoRoot].filter((r) => !!r))].sort((a, b) => b.length - a.length);
|
|
2479
2857
|
for (const root of roots) {
|
|
2480
|
-
const rel =
|
|
2481
|
-
const prefix = rel ? `${rel.split(
|
|
2482
|
-
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 || ".");
|
|
2483
2861
|
}
|
|
2484
2862
|
return out;
|
|
2485
2863
|
}
|
|
@@ -2512,18 +2890,34 @@ function auditTask(meta, answer) {
|
|
|
2512
2890
|
'Under DISPUTED list only claims you checked and found wrong or unsupported; write "- (none)" when there are none.'
|
|
2513
2891
|
].join("\n");
|
|
2514
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
|
+
}
|
|
2515
2907
|
function auditLines(meta) {
|
|
2908
|
+
const fixing = meta.fixOf ? [`\u2500\u2500 correction: the answer of ${meta.fixOf}, checked again against what its audit disputed`] : [];
|
|
2516
2909
|
if (meta.auditOf) {
|
|
2517
2910
|
return meta.state === "done" ? [`\u2500\u2500 audit of ${meta.auditOf}: ${(meta.auditVerdict ?? "unclear").toUpperCase()}${meta.auditDisputed?.length ? ` \xB7 ${meta.auditDisputed.length} disputed` : ""}`] : [];
|
|
2518
2911
|
}
|
|
2519
2912
|
const a = meta.audit;
|
|
2520
|
-
if (!a) return
|
|
2913
|
+
if (!a) return fixing;
|
|
2521
2914
|
if (a.state === "done") {
|
|
2522
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)" : ""}`;
|
|
2523
|
-
|
|
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];
|
|
2524
2918
|
}
|
|
2525
|
-
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})`];
|
|
2526
|
-
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`];
|
|
2527
2921
|
}
|
|
2528
2922
|
function auditBadge(meta) {
|
|
2529
2923
|
const a = meta.audit;
|
|
@@ -2532,26 +2926,20 @@ function auditBadge(meta) {
|
|
|
2532
2926
|
return a.state === "running" || a.state === "queued" ? "PENDING" : "FAILED";
|
|
2533
2927
|
}
|
|
2534
2928
|
|
|
2535
|
-
// src/core/notify.ts
|
|
2536
|
-
import { spawn as spawn3 } from "node:child_process";
|
|
2537
|
-
import crypto5 from "node:crypto";
|
|
2538
|
-
import fs22 from "node:fs";
|
|
2539
|
-
import path18 from "node:path";
|
|
2540
|
-
|
|
2541
2929
|
// src/core/history.ts
|
|
2542
|
-
import
|
|
2930
|
+
import fs20 from "node:fs";
|
|
2543
2931
|
import { createRequire } from "node:module";
|
|
2544
|
-
import
|
|
2932
|
+
import path18 from "node:path";
|
|
2545
2933
|
import zlib from "node:zlib";
|
|
2546
2934
|
|
|
2547
2935
|
// src/core/receipt.ts
|
|
2548
|
-
import
|
|
2549
|
-
import
|
|
2936
|
+
import fs18 from "node:fs";
|
|
2937
|
+
import path17 from "node:path";
|
|
2550
2938
|
|
|
2551
2939
|
// src/core/prices.ts
|
|
2552
2940
|
import { spawn as spawn2 } from "node:child_process";
|
|
2553
|
-
import
|
|
2554
|
-
import
|
|
2941
|
+
import fs17 from "node:fs";
|
|
2942
|
+
import path16 from "node:path";
|
|
2555
2943
|
var DEFAULT_URL = "https://models.dev/api.json";
|
|
2556
2944
|
var RETRY_AFTER_FAILURE_MS = 60 * 6e4;
|
|
2557
2945
|
var MIN_MODELS = 100;
|
|
@@ -2564,8 +2952,8 @@ function catalogLabel() {
|
|
|
2564
2952
|
return url;
|
|
2565
2953
|
}
|
|
2566
2954
|
}
|
|
2567
|
-
var catalogFile = () =>
|
|
2568
|
-
var attemptFile = () =>
|
|
2955
|
+
var catalogFile = () => path16.join(home(), "prices.json");
|
|
2956
|
+
var attemptFile = () => path16.join(home(), "prices.attempt");
|
|
2569
2957
|
function trim(raw) {
|
|
2570
2958
|
const out = {};
|
|
2571
2959
|
if (!raw || typeof raw !== "object") return out;
|
|
@@ -2583,19 +2971,19 @@ var cached2;
|
|
|
2583
2971
|
function readCatalog() {
|
|
2584
2972
|
const file2 = catalogFile();
|
|
2585
2973
|
try {
|
|
2586
|
-
const { mtimeMs } =
|
|
2974
|
+
const { mtimeMs } = fs17.statSync(file2);
|
|
2587
2975
|
if (cached2?.file === file2 && cached2.mtimeMs === mtimeMs) return cached2.catalog;
|
|
2588
|
-
const parsed = JSON.parse(
|
|
2589
|
-
const
|
|
2590
|
-
cached2 = { file: file2, mtimeMs, catalog:
|
|
2591
|
-
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;
|
|
2592
2980
|
} catch {
|
|
2593
2981
|
return void 0;
|
|
2594
2982
|
}
|
|
2595
2983
|
}
|
|
2596
2984
|
var ageMs = (file2) => {
|
|
2597
2985
|
try {
|
|
2598
|
-
return Date.now() -
|
|
2986
|
+
return Date.now() - fs17.statSync(file2).mtimeMs;
|
|
2599
2987
|
} catch {
|
|
2600
2988
|
return Infinity;
|
|
2601
2989
|
}
|
|
@@ -2614,17 +3002,17 @@ async function refreshCatalog() {
|
|
|
2614
3002
|
const models = trim(await r.json());
|
|
2615
3003
|
const n = Object.keys(models).length;
|
|
2616
3004
|
if (n < MIN_MODELS) throw new Error(`only ${n} priced models in it (a catalog has thousands): not used`);
|
|
2617
|
-
|
|
3005
|
+
fs17.mkdirSync(home(), { recursive: true });
|
|
2618
3006
|
const tmp = `${catalogFile()}.${process.pid}.tmp`;
|
|
2619
|
-
const
|
|
2620
|
-
|
|
3007
|
+
const catalog6 = { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), source: url, models };
|
|
3008
|
+
fs17.writeFileSync(tmp, JSON.stringify(catalog6));
|
|
2621
3009
|
renameOver(tmp, catalogFile());
|
|
2622
|
-
|
|
3010
|
+
fs17.rmSync(attemptFile(), { force: true });
|
|
2623
3011
|
return { state: "updated", models: n };
|
|
2624
3012
|
} catch (e) {
|
|
2625
3013
|
try {
|
|
2626
|
-
|
|
2627
|
-
|
|
3014
|
+
fs17.mkdirSync(home(), { recursive: true });
|
|
3015
|
+
fs17.writeFileSync(attemptFile(), `${(/* @__PURE__ */ new Date()).toISOString()} ${e.message}
|
|
2628
3016
|
`);
|
|
2629
3017
|
} catch {
|
|
2630
3018
|
}
|
|
@@ -2636,8 +3024,8 @@ function refreshInBackground() {
|
|
|
2636
3024
|
if (!refreshDue()) return;
|
|
2637
3025
|
const script = process.argv[1];
|
|
2638
3026
|
if (!script) return;
|
|
2639
|
-
|
|
2640
|
-
|
|
3027
|
+
fs17.mkdirSync(home(), { recursive: true });
|
|
3028
|
+
fs17.writeFileSync(attemptFile(), `${(/* @__PURE__ */ new Date()).toISOString()} refresh started
|
|
2641
3029
|
`);
|
|
2642
3030
|
const child = spawn2(process.execPath, [script, "prices", "--refresh", "--quiet"], { detached: true, stdio: "ignore", env: process.env });
|
|
2643
3031
|
child.on("error", () => void 0);
|
|
@@ -2647,14 +3035,14 @@ function refreshInBackground() {
|
|
|
2647
3035
|
}
|
|
2648
3036
|
var OWN = { codex: ["openai"], claude: ["anthropic"], gemini: ["google"] };
|
|
2649
3037
|
function catalogPrice(backend, ...models) {
|
|
2650
|
-
const
|
|
2651
|
-
if (!
|
|
3038
|
+
const catalog6 = readCatalog();
|
|
3039
|
+
if (!catalog6) return void 0;
|
|
2652
3040
|
for (const raw of models) {
|
|
2653
3041
|
const id = raw?.split("#")[0];
|
|
2654
3042
|
if (!id) continue;
|
|
2655
3043
|
const keys = [id, ...[...OWN[backend] ?? [], "anthropic", "openai", "google"].map((p) => `${p}/${id}`)];
|
|
2656
|
-
const any = keys.find((k) =>
|
|
2657
|
-
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;
|
|
2658
3046
|
if (any && triple && triple.length === 3 && triple.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
2659
3047
|
const [input, output, cachedInput] = triple;
|
|
2660
3048
|
return { input, output, cachedInput, key: any };
|
|
@@ -2667,7 +3055,7 @@ function catalogStatus() {
|
|
|
2667
3055
|
const c = readCatalog();
|
|
2668
3056
|
let lastFailure;
|
|
2669
3057
|
try {
|
|
2670
|
-
const t =
|
|
3058
|
+
const t = fs17.readFileSync(attemptFile(), "utf8").trim();
|
|
2671
3059
|
if (t && !t.endsWith("refresh started")) lastFailure = t;
|
|
2672
3060
|
} catch {
|
|
2673
3061
|
}
|
|
@@ -2738,13 +3126,13 @@ function record(meta) {
|
|
|
2738
3126
|
saved: meta.savedUsd ?? 0,
|
|
2739
3127
|
price: primaryPrice().name
|
|
2740
3128
|
};
|
|
2741
|
-
|
|
2742
|
-
|
|
3129
|
+
fs18.mkdirSync(path17.dirname(ledgerFile()), { recursive: true });
|
|
3130
|
+
fs18.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
|
|
2743
3131
|
`);
|
|
2744
3132
|
}
|
|
2745
3133
|
function readLedger(sinceMs2) {
|
|
2746
|
-
if (!
|
|
2747
|
-
return
|
|
3134
|
+
if (!fs18.existsSync(ledgerFile())) return [];
|
|
3135
|
+
return fs18.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
|
|
2748
3136
|
try {
|
|
2749
3137
|
return [JSON.parse(l)];
|
|
2750
3138
|
} catch {
|
|
@@ -2819,7 +3207,7 @@ ${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.rou
|
|
|
2819
3207
|
}
|
|
2820
3208
|
|
|
2821
3209
|
// src/core/ui.ts
|
|
2822
|
-
import
|
|
3210
|
+
import fs19 from "node:fs";
|
|
2823
3211
|
var WEEK_MS = 7 * 24 * 3600 * 1e3;
|
|
2824
3212
|
var RECENT = 40;
|
|
2825
3213
|
var RECENT_HOURS = 1;
|
|
@@ -2910,9 +3298,9 @@ function cardsFor(m) {
|
|
|
2910
3298
|
const out = [];
|
|
2911
3299
|
for (const [phase, text] of phases) {
|
|
2912
3300
|
const marker = runFile(m.id, `card-${phase}`);
|
|
2913
|
-
if (
|
|
3301
|
+
if (fs19.existsSync(marker)) continue;
|
|
2914
3302
|
if (!(phase === "started" && !isActive(m.state))) out.push(text());
|
|
2915
|
-
|
|
3303
|
+
fs19.writeFileSync(marker, "");
|
|
2916
3304
|
}
|
|
2917
3305
|
return out;
|
|
2918
3306
|
}
|
|
@@ -2923,10 +3311,10 @@ function hookCards(input) {
|
|
|
2923
3311
|
const response = event.tool_response;
|
|
2924
3312
|
const output = typeof response === "string" ? response : [response?.stdout, response?.stderr].filter(Boolean).join("\n");
|
|
2925
3313
|
const ids = [...new Set(`${command}
|
|
2926
|
-
${output}`.match(RUN_ID) ?? [])].filter((id) =>
|
|
3314
|
+
${output}`.match(RUN_ID) ?? [])].filter((id) => fs19.existsSync(`${runsDir()}/${id}`));
|
|
2927
3315
|
const since2 = Date.now() - RECENT_HOURS * 36e5;
|
|
2928
3316
|
for (const id of listRunIds().slice(-RECENT)) {
|
|
2929
|
-
if (ids.includes(id) ||
|
|
3317
|
+
if (ids.includes(id) || fs19.existsSync(runFile(id, "card-ended"))) continue;
|
|
2930
3318
|
try {
|
|
2931
3319
|
const m = readMeta(id);
|
|
2932
3320
|
if (!isActive(m.state) && Date.parse(m.endedAt ?? m.startedAt) > since2) ids.push(id);
|
|
@@ -2948,7 +3336,7 @@ var SCHEMA_VERSION = 1;
|
|
|
2948
3336
|
var PATCH_MAX = 1e6;
|
|
2949
3337
|
var ANSWER_MAX2 = 2e5;
|
|
2950
3338
|
var cached3;
|
|
2951
|
-
var historyFile = () =>
|
|
3339
|
+
var historyFile = () => path18.join(home(), "history.db");
|
|
2952
3340
|
function openDb() {
|
|
2953
3341
|
const file2 = historyFile();
|
|
2954
3342
|
if (cached3?.file === file2) return cached3.db;
|
|
@@ -2962,7 +3350,7 @@ function openDb() {
|
|
|
2962
3350
|
} finally {
|
|
2963
3351
|
process.emitWarning = emit;
|
|
2964
3352
|
}
|
|
2965
|
-
|
|
3353
|
+
fs20.mkdirSync(path18.dirname(file2), { recursive: true });
|
|
2966
3354
|
db = new DatabaseSync(file2);
|
|
2967
3355
|
db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
|
|
2968
3356
|
migrate(db);
|
|
@@ -2997,35 +3385,35 @@ function migrate(db) {
|
|
|
2997
3385
|
function readRunFile(id, name) {
|
|
2998
3386
|
const f = runFile(id, name);
|
|
2999
3387
|
try {
|
|
3000
|
-
if (
|
|
3001
|
-
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");
|
|
3002
3390
|
} catch {
|
|
3003
3391
|
}
|
|
3004
3392
|
return void 0;
|
|
3005
3393
|
}
|
|
3006
3394
|
function gzipFile(f) {
|
|
3007
|
-
if (!
|
|
3008
|
-
|
|
3009
|
-
|
|
3395
|
+
if (!fs20.existsSync(f)) return;
|
|
3396
|
+
fs20.writeFileSync(`${f}.gz`, zlib.gzipSync(fs20.readFileSync(f)));
|
|
3397
|
+
fs20.rmSync(f);
|
|
3010
3398
|
}
|
|
3011
3399
|
function compactRun(meta) {
|
|
3012
3400
|
if (!TERMINAL.includes(meta.state)) return;
|
|
3013
|
-
const dir =
|
|
3401
|
+
const dir = path18.dirname(runFile(meta.id, "meta.json"));
|
|
3014
3402
|
let names;
|
|
3015
3403
|
try {
|
|
3016
|
-
names =
|
|
3404
|
+
names = fs20.readdirSync(dir);
|
|
3017
3405
|
} catch {
|
|
3018
3406
|
return;
|
|
3019
3407
|
}
|
|
3020
3408
|
for (const n of names) {
|
|
3021
|
-
if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(
|
|
3409
|
+
if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(path18.join(dir, n));
|
|
3022
3410
|
else if (/^stderr(\.attempt-\d+)?\.log$/.test(n)) {
|
|
3023
|
-
if (meta.state === "done" && !/attempt/.test(n))
|
|
3024
|
-
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));
|
|
3025
3413
|
}
|
|
3026
3414
|
}
|
|
3027
3415
|
}
|
|
3028
|
-
var
|
|
3416
|
+
var clip6 = (s, n) => s && s.length > n ? s.slice(0, n) : s;
|
|
3029
3417
|
function timelineOf(meta) {
|
|
3030
3418
|
const raw = readRunFile(meta.id, "events.jsonl");
|
|
3031
3419
|
if (!raw) return [];
|
|
@@ -3042,8 +3430,8 @@ function recordRun(meta) {
|
|
|
3042
3430
|
try {
|
|
3043
3431
|
const u = meta.usage;
|
|
3044
3432
|
const known = db.prepare("SELECT answer IS NOT NULL AS has FROM runs WHERE id = ?").get(meta.id);
|
|
3045
|
-
const answer = known?.has ? void 0 :
|
|
3046
|
-
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);
|
|
3047
3435
|
const seconds = meta.endedAt ? Math.max(0, Math.round((Date.parse(meta.endedAt) - Date.parse(meta.startedAt)) / 1e3)) : null;
|
|
3048
3436
|
const t = meta.ran ?? meta.worker;
|
|
3049
3437
|
const fields = {
|
|
@@ -3055,7 +3443,7 @@ function recordRun(meta) {
|
|
|
3055
3443
|
kind: kind(meta),
|
|
3056
3444
|
backend: t.backend,
|
|
3057
3445
|
model: meta.resolvedModel ?? t.model ?? null,
|
|
3058
|
-
task:
|
|
3446
|
+
task: clip6(meta.task, 2e4) ?? "",
|
|
3059
3447
|
grp: meta.group ?? null,
|
|
3060
3448
|
dir: meta.dir,
|
|
3061
3449
|
review_of: meta.reviewOf ?? meta.auditOf ?? null,
|
|
@@ -3069,7 +3457,7 @@ function recordRun(meta) {
|
|
|
3069
3457
|
saved: meta.savedUsd ?? null,
|
|
3070
3458
|
files_changed: meta.changes?.length ?? 0,
|
|
3071
3459
|
applied: meta.applied ? 1 : 0,
|
|
3072
|
-
error:
|
|
3460
|
+
error: clip6(meta.error, 2e3) ?? null,
|
|
3073
3461
|
meta_json: JSON.stringify(meta)
|
|
3074
3462
|
};
|
|
3075
3463
|
db.exec("BEGIN IMMEDIATE");
|
|
@@ -3229,6 +3617,51 @@ function auditStats(db, since2) {
|
|
|
3229
3617
|
}
|
|
3230
3618
|
return { total, byWorker };
|
|
3231
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
|
+
}
|
|
3232
3665
|
function workerRows(rows, ledger, audited) {
|
|
3233
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 }));
|
|
3234
3667
|
const key = (backend, model) => `${backend ?? ""}\0${model ?? ""}`;
|
|
@@ -3277,12 +3710,96 @@ function historyStats(sinceMs2) {
|
|
|
3277
3710
|
}
|
|
3278
3711
|
}
|
|
3279
3712
|
|
|
3280
|
-
// src/core/
|
|
3713
|
+
// src/core/budget.ts
|
|
3281
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";
|
|
3282
3799
|
|
|
3283
3800
|
// src/core/plan.ts
|
|
3284
|
-
import
|
|
3285
|
-
import
|
|
3801
|
+
import fs22 from "node:fs";
|
|
3802
|
+
import path19 from "node:path";
|
|
3286
3803
|
var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
3287
3804
|
var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
|
|
3288
3805
|
function structure(lines) {
|
|
@@ -3329,9 +3846,9 @@ function parsePlan(text, file2 = "") {
|
|
|
3329
3846
|
return { file: file2, title: titleHeading?.text ?? "", header, constraints, tasks };
|
|
3330
3847
|
}
|
|
3331
3848
|
function loadPlan(file2) {
|
|
3332
|
-
const abs =
|
|
3333
|
-
if (!
|
|
3334
|
-
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));
|
|
3335
3852
|
if (!plan.tasks.length) throw new UserError(`${file2} has no "Task N" headings (see pitroom-writing-plans)`);
|
|
3336
3853
|
return plan;
|
|
3337
3854
|
}
|
|
@@ -3350,7 +3867,7 @@ function brief(plan, task) {
|
|
|
3350
3867
|
].filter(Boolean).join("\n\n")}
|
|
3351
3868
|
`;
|
|
3352
3869
|
}
|
|
3353
|
-
var planName = (file2) =>
|
|
3870
|
+
var planName = (file2) => path19.basename(file2).replace(/\.md$/i, "");
|
|
3354
3871
|
|
|
3355
3872
|
// src/core/style.ts
|
|
3356
3873
|
var enabled = () => {
|
|
@@ -3410,7 +3927,7 @@ function duration(meta) {
|
|
|
3410
3927
|
}
|
|
3411
3928
|
function readSummary(meta) {
|
|
3412
3929
|
const f = runFile(meta.id, "summary.md");
|
|
3413
|
-
return
|
|
3930
|
+
return fs23.existsSync(f) ? fs23.readFileSync(f, "utf8").trim() : archivedRun(meta.id)?.answer ?? "";
|
|
3414
3931
|
}
|
|
3415
3932
|
function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
3416
3933
|
const out = [];
|
|
@@ -3496,8 +4013,8 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
|
3496
4013
|
}
|
|
3497
4014
|
function live(meta) {
|
|
3498
4015
|
const f = runFile(meta.id, "events.jsonl");
|
|
3499
|
-
if (!
|
|
3500
|
-
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"));
|
|
3501
4018
|
return { steps: p.usage.steps, toolCalls: p.usage.toolCalls, last: p.lastActivity };
|
|
3502
4019
|
}
|
|
3503
4020
|
function progress(meta) {
|
|
@@ -3517,21 +4034,33 @@ function groupIds(group) {
|
|
|
3517
4034
|
}
|
|
3518
4035
|
});
|
|
3519
4036
|
}
|
|
3520
|
-
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) => {
|
|
3521
4050
|
const t = s.replace(/\s+/g, " ").trim();
|
|
3522
4051
|
return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
|
|
3523
4052
|
};
|
|
3524
4053
|
function headline(meta, max = 160) {
|
|
3525
4054
|
const text = readSummary(meta);
|
|
3526
4055
|
const line = text.split("\n").find((l) => /^SUMMARY:/i.test(l.trim())) ?? text.split("\n").find((l) => l.trim()) ?? "";
|
|
3527
|
-
return
|
|
4056
|
+
return oneLine6(line.replace(/^SUMMARY:\s*/i, ""), max);
|
|
3528
4057
|
}
|
|
3529
4058
|
function table(metas) {
|
|
3530
4059
|
if (!metas.length) return "no runs";
|
|
3531
4060
|
const rows = metas.map((m) => {
|
|
3532
4061
|
const l = m.state === "running" ? live(m) : void 0;
|
|
3533
4062
|
const steps = l ? String(l.steps) : m.usage ? String(m.usage.steps) : "-";
|
|
3534
|
-
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);
|
|
3535
4064
|
return [m.id, m.state, m.mode, duration(m), steps, describeTarget(m.ran ?? m.worker), note];
|
|
3536
4065
|
});
|
|
3537
4066
|
const head = ["RUN", "STATE", "MODE", "TIME", "STEPS", "WORKER", "NOW / RESULT"];
|
|
@@ -3573,15 +4102,15 @@ async function watch(select, opts) {
|
|
|
3573
4102
|
const prev = seen.get(m.id);
|
|
3574
4103
|
const l = m.state === "running" ? live(m) : { steps: m.usage?.steps ?? 0, toolCalls: m.usage?.toolCalls ?? 0 };
|
|
3575
4104
|
const attempts = m.attempts?.length ?? 0;
|
|
3576
|
-
const base2 = { run: m.id, mode: m.mode, task:
|
|
4105
|
+
const base2 = { run: m.id, mode: m.mode, task: oneLine6(m.task, 60) };
|
|
3577
4106
|
if (!prev) emit({ event: m.state === "queued" ? "queued" : "started", ...base2 }, m);
|
|
3578
4107
|
else if (prev.state === "queued" && m.state === "running") emit({ event: "started", ...base2 }, m);
|
|
3579
4108
|
if (attempts > (prev?.attempts ?? 0)) {
|
|
3580
4109
|
const a = m.attempts[attempts - 1];
|
|
3581
|
-
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);
|
|
3582
4111
|
}
|
|
3583
4112
|
if (m.state === "running" && l.steps > (prev?.steps ?? 0)) {
|
|
3584
|
-
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 });
|
|
3585
4114
|
}
|
|
3586
4115
|
if (TERMINAL.includes(m.state) && prev?.state !== m.state) {
|
|
3587
4116
|
emit({
|
|
@@ -3590,7 +4119,7 @@ async function watch(select, opts) {
|
|
|
3590
4119
|
time: duration(m),
|
|
3591
4120
|
worker: describeTarget(m.ran ?? m.worker),
|
|
3592
4121
|
summary: headline(m) || void 0,
|
|
3593
|
-
error: m.error ?
|
|
4122
|
+
error: m.error ? oneLine6(m.error, 160) : void 0,
|
|
3594
4123
|
refs: m.refs ? `${m.refs.valid}/${m.refs.total}` : void 0,
|
|
3595
4124
|
changes: m.changes?.length || void 0
|
|
3596
4125
|
}, m);
|
|
@@ -3657,12 +4186,12 @@ function claimGroup(group) {
|
|
|
3657
4186
|
try {
|
|
3658
4187
|
const ids = groupIds(group).sort();
|
|
3659
4188
|
if (ids.length < 2) return true;
|
|
3660
|
-
const dir =
|
|
3661
|
-
|
|
3662
|
-
for (const f of
|
|
3663
|
-
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 });
|
|
3664
4193
|
}
|
|
3665
|
-
|
|
4194
|
+
fs24.writeFileSync(path20.join(dir, crypto5.createHash("sha1").update(ids.join(",")).digest("hex").slice(0, 16)), "", { flag: "wx" });
|
|
3666
4195
|
return true;
|
|
3667
4196
|
} catch {
|
|
3668
4197
|
return false;
|
|
@@ -3696,30 +4225,30 @@ function notifyEnded(meta) {
|
|
|
3696
4225
|
}
|
|
3697
4226
|
|
|
3698
4227
|
// src/core/templates.ts
|
|
3699
|
-
import
|
|
3700
|
-
import
|
|
4228
|
+
import fs26 from "node:fs";
|
|
4229
|
+
import path22 from "node:path";
|
|
3701
4230
|
|
|
3702
4231
|
// src/core/install.ts
|
|
3703
|
-
import
|
|
3704
|
-
import
|
|
3705
|
-
import
|
|
3706
|
-
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";
|
|
3707
4236
|
var LEGACY = ["pitroom", "opencode-worker"];
|
|
3708
4237
|
function packageRoot() {
|
|
3709
|
-
return
|
|
4238
|
+
return path21.resolve(path21.dirname(fileURLToPath3(import.meta.url)), "..");
|
|
3710
4239
|
}
|
|
3711
4240
|
function skillNames(root = packageRoot()) {
|
|
3712
|
-
const dir =
|
|
3713
|
-
if (!
|
|
3714
|
-
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();
|
|
3715
4244
|
}
|
|
3716
4245
|
function skillTargets() {
|
|
3717
|
-
const home3 =
|
|
3718
|
-
const targets = [
|
|
3719
|
-
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"));
|
|
3720
4249
|
return targets;
|
|
3721
4250
|
}
|
|
3722
|
-
var launcherPath = () =>
|
|
4251
|
+
var launcherPath = () => path21.join(os10.homedir(), ".local", "bin", process.platform === "win32" ? "pitroom.cmd" : "pitroom");
|
|
3723
4252
|
var LAUNCHER_MARK = "# pitroom launcher";
|
|
3724
4253
|
function launcherScript(bundle) {
|
|
3725
4254
|
if (process.platform === "win32") {
|
|
@@ -3749,7 +4278,7 @@ exit 127
|
|
|
3749
4278
|
function isOurLauncher(file2, root) {
|
|
3750
4279
|
if (linksInto(file2, root)) return true;
|
|
3751
4280
|
try {
|
|
3752
|
-
const s =
|
|
4281
|
+
const s = fs25.readFileSync(file2, "utf8");
|
|
3753
4282
|
return s.includes(LAUNCHER_MARK) && s.includes(root);
|
|
3754
4283
|
} catch {
|
|
3755
4284
|
return false;
|
|
@@ -3757,64 +4286,64 @@ function isOurLauncher(file2, root) {
|
|
|
3757
4286
|
}
|
|
3758
4287
|
function isPitroomLauncher(file2) {
|
|
3759
4288
|
try {
|
|
3760
|
-
return
|
|
4289
|
+
return fs25.readFileSync(file2, "utf8").includes(LAUNCHER_MARK);
|
|
3761
4290
|
} catch {
|
|
3762
4291
|
return false;
|
|
3763
4292
|
}
|
|
3764
4293
|
}
|
|
3765
4294
|
function placeLauncher(bundle, root, force) {
|
|
3766
4295
|
const dest = launcherPath();
|
|
3767
|
-
|
|
3768
|
-
const exists =
|
|
4296
|
+
fs25.mkdirSync(path21.dirname(dest), { recursive: true });
|
|
4297
|
+
const exists = fs25.lstatSync(dest, { throwIfNoEntry: false });
|
|
3769
4298
|
if (exists && !isOurLauncher(dest, root)) {
|
|
3770
4299
|
if (!force) return `! ${dest} exists and is not Pitroom's launcher; kept (use --force to back it up and replace)`;
|
|
3771
|
-
|
|
4300
|
+
fs25.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
3772
4301
|
} else if (exists) {
|
|
3773
|
-
|
|
4302
|
+
fs25.rmSync(dest, { force: true });
|
|
3774
4303
|
}
|
|
3775
|
-
|
|
4304
|
+
fs25.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
|
|
3776
4305
|
return `\u2714 ${dest} \u2192 launcher for ${bundle} (Node 22.13+)`;
|
|
3777
4306
|
}
|
|
3778
4307
|
function linksInto(link, dir) {
|
|
3779
|
-
const st =
|
|
4308
|
+
const st = fs25.lstatSync(link, { throwIfNoEntry: false });
|
|
3780
4309
|
if (!st?.isSymbolicLink()) return false;
|
|
3781
|
-
const target =
|
|
3782
|
-
const rel =
|
|
3783
|
-
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);
|
|
3784
4313
|
}
|
|
3785
4314
|
function place(src, dest, opts) {
|
|
3786
|
-
|
|
3787
|
-
const st =
|
|
4315
|
+
fs25.mkdirSync(path21.dirname(dest), { recursive: true });
|
|
4316
|
+
const st = fs25.lstatSync(dest, { throwIfNoEntry: false });
|
|
3788
4317
|
if (st && !st.isSymbolicLink()) {
|
|
3789
4318
|
if (!opts.force) return `! ${dest} exists and is not a link; kept (use --force to back it up and replace)`;
|
|
3790
|
-
|
|
4319
|
+
fs25.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
3791
4320
|
} else if (st) {
|
|
3792
|
-
|
|
4321
|
+
fs25.unlinkSync(dest);
|
|
3793
4322
|
}
|
|
3794
|
-
if (opts.copy)
|
|
3795
|
-
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");
|
|
3796
4325
|
return `\u2714 ${dest} \u2192 ${opts.copy ? "copied" : src}`;
|
|
3797
4326
|
}
|
|
3798
4327
|
function install(opts) {
|
|
3799
4328
|
const root = packageRoot();
|
|
3800
4329
|
const skills2 = opts.skills === false ? [] : skillNames(root);
|
|
3801
|
-
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")}`);
|
|
3802
4331
|
const out = [];
|
|
3803
4332
|
for (const base2 of opts.skills === false ? [] : skillTargets()) {
|
|
3804
4333
|
for (const legacy of LEGACY) {
|
|
3805
|
-
const l =
|
|
4334
|
+
const l = path21.join(base2, legacy);
|
|
3806
4335
|
if (!skills2.includes(legacy) && linksInto(l, root)) {
|
|
3807
|
-
|
|
4336
|
+
fs25.unlinkSync(l);
|
|
3808
4337
|
out.push(`\u2714 removed old link ${l}`);
|
|
3809
4338
|
}
|
|
3810
4339
|
}
|
|
3811
|
-
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));
|
|
3812
4341
|
}
|
|
3813
|
-
const bundle =
|
|
3814
|
-
if (
|
|
4342
|
+
const bundle = path21.join(root, "dist", "pitroom.mjs");
|
|
4343
|
+
if (fs25.existsSync(bundle)) {
|
|
3815
4344
|
out.push(placeLauncher(bundle, root, opts.force));
|
|
3816
|
-
const onPath = (process.env.PATH ?? "").split(
|
|
3817
|
-
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`);
|
|
3818
4347
|
}
|
|
3819
4348
|
return out;
|
|
3820
4349
|
}
|
|
@@ -3822,24 +4351,24 @@ function uninstall() {
|
|
|
3822
4351
|
const root = packageRoot();
|
|
3823
4352
|
const out = [];
|
|
3824
4353
|
for (const base2 of skillTargets()) {
|
|
3825
|
-
if (!
|
|
3826
|
-
for (const name of
|
|
3827
|
-
const l =
|
|
4354
|
+
if (!fs25.existsSync(base2)) continue;
|
|
4355
|
+
for (const name of fs25.readdirSync(base2)) {
|
|
4356
|
+
const l = path21.join(base2, name);
|
|
3828
4357
|
if (linksInto(l, root)) {
|
|
3829
|
-
|
|
4358
|
+
fs25.unlinkSync(l);
|
|
3830
4359
|
out.push(`\u2714 removed ${l}`);
|
|
3831
4360
|
}
|
|
3832
4361
|
}
|
|
3833
4362
|
}
|
|
3834
4363
|
if (isOurLauncher(launcherPath(), root)) {
|
|
3835
|
-
|
|
4364
|
+
fs25.rmSync(launcherPath(), { force: true });
|
|
3836
4365
|
out.push(`\u2714 removed ${launcherPath()}`);
|
|
3837
4366
|
}
|
|
3838
4367
|
return out.length ? out : ["nothing to remove"];
|
|
3839
4368
|
}
|
|
3840
4369
|
function installedSkills() {
|
|
3841
4370
|
const names = skillNames();
|
|
3842
|
-
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"))) }));
|
|
3843
4372
|
}
|
|
3844
4373
|
|
|
3845
4374
|
// src/core/templates.ts
|
|
@@ -3850,9 +4379,9 @@ var FILES = {
|
|
|
3850
4379
|
"code-reviewer": "pitroom-review/code-reviewer.md"
|
|
3851
4380
|
};
|
|
3852
4381
|
function loadTemplate(name, root = packageRoot()) {
|
|
3853
|
-
const file2 =
|
|
3854
|
-
if (!
|
|
3855
|
-
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*/, "");
|
|
3856
4385
|
}
|
|
3857
4386
|
function fill(template, values) {
|
|
3858
4387
|
const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
|
|
@@ -3862,10 +4391,10 @@ function fill(template, values) {
|
|
|
3862
4391
|
|
|
3863
4392
|
// src/core/slots.ts
|
|
3864
4393
|
import crypto6 from "node:crypto";
|
|
3865
|
-
import
|
|
3866
|
-
import
|
|
3867
|
-
var slotsDir = () =>
|
|
3868
|
-
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");
|
|
3869
4398
|
var STARTUP_GRACE_MS = 3e4;
|
|
3870
4399
|
function holderActive(runId, self) {
|
|
3871
4400
|
if (!runId || runId === self) return false;
|
|
@@ -3879,23 +4408,23 @@ function holderActive(runId, self) {
|
|
|
3879
4408
|
}
|
|
3880
4409
|
}
|
|
3881
4410
|
function tryClaim(file2, runId) {
|
|
3882
|
-
|
|
4411
|
+
fs27.mkdirSync(path23.dirname(file2), { recursive: true });
|
|
3883
4412
|
try {
|
|
3884
|
-
|
|
4413
|
+
fs27.writeFileSync(file2, runId, { flag: "wx" });
|
|
3885
4414
|
return true;
|
|
3886
4415
|
} catch (e) {
|
|
3887
4416
|
if (e.code !== "EEXIST") throw e;
|
|
3888
4417
|
}
|
|
3889
4418
|
let owner = "";
|
|
3890
4419
|
try {
|
|
3891
|
-
owner =
|
|
4420
|
+
owner = fs27.readFileSync(file2, "utf8").trim();
|
|
3892
4421
|
} catch {
|
|
3893
4422
|
}
|
|
3894
4423
|
if (owner === runId) return true;
|
|
3895
4424
|
if (holderActive(owner, runId)) return false;
|
|
3896
|
-
|
|
4425
|
+
fs27.rmSync(file2, { force: true });
|
|
3897
4426
|
try {
|
|
3898
|
-
|
|
4427
|
+
fs27.writeFileSync(file2, runId, { flag: "wx" });
|
|
3899
4428
|
return true;
|
|
3900
4429
|
} catch {
|
|
3901
4430
|
return false;
|
|
@@ -3904,25 +4433,25 @@ function tryClaim(file2, runId) {
|
|
|
3904
4433
|
function releaseIfOwner(file2, runId) {
|
|
3905
4434
|
if (!file2) return;
|
|
3906
4435
|
try {
|
|
3907
|
-
if (
|
|
4436
|
+
if (fs27.readFileSync(file2, "utf8").trim() === runId) fs27.rmSync(file2, { force: true });
|
|
3908
4437
|
} catch {
|
|
3909
4438
|
}
|
|
3910
4439
|
}
|
|
3911
4440
|
function tryAcquireSlot(runId, maxParallel) {
|
|
3912
4441
|
for (let n = 0; n < Math.max(1, maxParallel); n++) {
|
|
3913
|
-
const file2 =
|
|
4442
|
+
const file2 = path23.join(slotsDir(), `slot-${n}`);
|
|
3914
4443
|
if (tryClaim(file2, runId)) return file2;
|
|
3915
4444
|
}
|
|
3916
4445
|
return void 0;
|
|
3917
4446
|
}
|
|
3918
4447
|
var releaseSlot = (file2, runId) => releaseIfOwner(file2, runId);
|
|
3919
|
-
var lockFile = (repoRoot2) =>
|
|
4448
|
+
var lockFile = (repoRoot2) => path23.join(locksDir(), `write-${crypto6.createHash("sha1").update(path23.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
|
|
3920
4449
|
function acquireWriteLock(repoRoot2, runId) {
|
|
3921
4450
|
const file2 = lockFile(repoRoot2);
|
|
3922
4451
|
if (tryClaim(file2, runId)) return;
|
|
3923
4452
|
let owner = "";
|
|
3924
4453
|
try {
|
|
3925
|
-
owner =
|
|
4454
|
+
owner = fs27.readFileSync(file2, "utf8").trim();
|
|
3926
4455
|
} catch {
|
|
3927
4456
|
}
|
|
3928
4457
|
throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
|
|
@@ -3942,35 +4471,38 @@ var RULES = {
|
|
|
3942
4471
|
"Run the relevant tests, type checks or linters if they are cheap and available, and report the result."
|
|
3943
4472
|
]
|
|
3944
4473
|
};
|
|
3945
|
-
|
|
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) {
|
|
3946
4476
|
if (followUp) return `Follow-up from the primary agent. Same rules and answer format as before.
|
|
3947
4477
|
|
|
3948
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".`;
|
|
3949
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.
|
|
3950
4488
|
|
|
3951
4489
|
Rules:
|
|
3952
|
-
- ${
|
|
4490
|
+
- ${rules.join("\n- ")}
|
|
3953
4491
|
- Stay inside the task scope and the project directory.
|
|
3954
4492
|
- Never commit, push, reset, checkout, restore, stash, clean, rebase or merge, and never discard or overwrite uncommitted work you did not create.
|
|
3955
4493
|
- Never read or reveal secrets (.env files, keys, tokens).
|
|
3956
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.
|
|
3957
|
-
- Do not ask questions. If something is ambiguous, choose the safest reasonable interpretation and state the assumption
|
|
3958
|
-
- 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}
|
|
3959
4496
|
|
|
3960
|
-
|
|
3961
|
-
SUMMARY: 1-5 lines that directly answer the task.
|
|
3962
|
-
DETAILS: key findings with file:line references; only what the primary agent needs.
|
|
3963
|
-
FILES CHANGED: paths, or "none".
|
|
3964
|
-
VERIFICATION: commands you ran and their results, or "not run".
|
|
3965
|
-
OPEN ISSUES: risks, uncertainties, follow-ups, or "none".
|
|
4497
|
+
${format}
|
|
3966
4498
|
|
|
3967
4499
|
Task:
|
|
3968
4500
|
${task}`;
|
|
3969
4501
|
}
|
|
3970
4502
|
|
|
3971
4503
|
// src/core/refs.ts
|
|
3972
|
-
import
|
|
3973
|
-
import
|
|
4504
|
+
import fs28 from "node:fs";
|
|
4505
|
+
import path24 from "node:path";
|
|
3974
4506
|
var FILE_EXTENSIONS = new Set(
|
|
3975
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(" ")
|
|
3976
4508
|
);
|
|
@@ -4021,13 +4553,13 @@ function extractRefs(answer) {
|
|
|
4021
4553
|
return [...seen.values()];
|
|
4022
4554
|
}
|
|
4023
4555
|
function verifyRefs(refs, dirs) {
|
|
4024
|
-
const roots = [...new Set(dirs.filter(Boolean).map((d) =>
|
|
4556
|
+
const roots = [...new Set(dirs.filter(Boolean).map((d) => path24.resolve(d)))];
|
|
4025
4557
|
const invalid = [];
|
|
4026
4558
|
const lineCache = /* @__PURE__ */ new Map();
|
|
4027
4559
|
const load = (file2) => {
|
|
4028
4560
|
if (!lineCache.has(file2)) {
|
|
4029
|
-
const size =
|
|
4030
|
-
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");
|
|
4031
4563
|
if (lines && lines[lines.length - 1] === "") lines.pop();
|
|
4032
4564
|
lineCache.set(file2, lines);
|
|
4033
4565
|
}
|
|
@@ -4038,9 +4570,9 @@ function verifyRefs(refs, dirs) {
|
|
|
4038
4570
|
const candidates = (ref) => {
|
|
4039
4571
|
const direct = resolve(ref.file, roots);
|
|
4040
4572
|
if (direct) return [direct];
|
|
4041
|
-
if (
|
|
4573
|
+
if (path24.isAbsolute(ref.file)) return [];
|
|
4042
4574
|
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
4043
|
-
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);
|
|
4044
4576
|
};
|
|
4045
4577
|
const check = (file2, ref) => {
|
|
4046
4578
|
const lines = load(file2);
|
|
@@ -4055,7 +4587,7 @@ function verifyRefs(refs, dirs) {
|
|
|
4055
4587
|
if (!files.length) {
|
|
4056
4588
|
if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
|
|
4057
4589
|
total++;
|
|
4058
|
-
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" });
|
|
4059
4591
|
continue;
|
|
4060
4592
|
}
|
|
4061
4593
|
total++;
|
|
@@ -4076,7 +4608,7 @@ function enclosedBy(lines, ref) {
|
|
|
4076
4608
|
return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
|
|
4077
4609
|
}
|
|
4078
4610
|
var isFileName = (name) => {
|
|
4079
|
-
const base2 =
|
|
4611
|
+
const base2 = path24.basename(name);
|
|
4080
4612
|
return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
|
|
4081
4613
|
};
|
|
4082
4614
|
function indexFiles(roots) {
|
|
@@ -4085,19 +4617,19 @@ function indexFiles(roots) {
|
|
|
4085
4617
|
const walk = (dir) => {
|
|
4086
4618
|
let entries;
|
|
4087
4619
|
try {
|
|
4088
|
-
entries =
|
|
4620
|
+
entries = fs28.readdirSync(dir, { withFileTypes: true });
|
|
4089
4621
|
} catch {
|
|
4090
4622
|
return;
|
|
4091
4623
|
}
|
|
4092
4624
|
for (const e of entries) {
|
|
4093
4625
|
if (count >= MAX_INDEXED) return;
|
|
4094
4626
|
if (e.isDirectory()) {
|
|
4095
|
-
if (!SKIP_DIRS2.has(e.name)) walk(
|
|
4627
|
+
if (!SKIP_DIRS2.has(e.name)) walk(path24.join(dir, e.name));
|
|
4096
4628
|
} else if (e.isFile()) {
|
|
4097
4629
|
count++;
|
|
4098
4630
|
const list2 = index.get(e.name);
|
|
4099
|
-
if (list2) list2.push(
|
|
4100
|
-
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)]);
|
|
4101
4633
|
}
|
|
4102
4634
|
}
|
|
4103
4635
|
};
|
|
@@ -4105,10 +4637,10 @@ function indexFiles(roots) {
|
|
|
4105
4637
|
return index;
|
|
4106
4638
|
}
|
|
4107
4639
|
function resolve(ref, roots) {
|
|
4108
|
-
const candidates =
|
|
4640
|
+
const candidates = path24.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path24.join(r, ref));
|
|
4109
4641
|
return candidates.find((c) => {
|
|
4110
4642
|
try {
|
|
4111
|
-
return
|
|
4643
|
+
return fs28.statSync(c).isFile() && inside(c, roots);
|
|
4112
4644
|
} catch {
|
|
4113
4645
|
return false;
|
|
4114
4646
|
}
|
|
@@ -4116,14 +4648,14 @@ function resolve(ref, roots) {
|
|
|
4116
4648
|
}
|
|
4117
4649
|
function inside(file2, roots) {
|
|
4118
4650
|
return roots.some((r) => {
|
|
4119
|
-
const rel =
|
|
4120
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
4651
|
+
const rel = path24.relative(r, path24.resolve(file2));
|
|
4652
|
+
return rel !== "" && !rel.startsWith("..") && !path24.isAbsolute(rel);
|
|
4121
4653
|
});
|
|
4122
4654
|
}
|
|
4123
4655
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4124
4656
|
|
|
4125
4657
|
// src/core/run.ts
|
|
4126
|
-
var VERSION2 = true ? "0.
|
|
4658
|
+
var VERSION2 = true ? "0.23.0" : "0.0.0-dev";
|
|
4127
4659
|
function planWork(o) {
|
|
4128
4660
|
if (!o.plan) return { task: o.task.trim(), tier: o.tier };
|
|
4129
4661
|
if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
|
|
@@ -4155,7 +4687,7 @@ function prepareRun(o) {
|
|
|
4155
4687
|
parent = readMeta(o.continueFrom);
|
|
4156
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");
|
|
4157
4689
|
if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
|
|
4158
|
-
if (parent.mode === "isolate" && (!parent.worktree || !
|
|
4690
|
+
if (parent.mode === "isolate" && (!parent.worktree || !fs29.existsSync(parent.worktree))) {
|
|
4159
4691
|
throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
|
|
4160
4692
|
}
|
|
4161
4693
|
const ran = parent.ran ?? parent.worker;
|
|
@@ -4165,7 +4697,16 @@ function prepareRun(o) {
|
|
|
4165
4697
|
const chain = resolveChain({ worker: o.worker, model: o.model, tier: work.tier, effort: o.effort, noFallback: o.noFallback });
|
|
4166
4698
|
({ worker, fallback } = chain);
|
|
4167
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
|
+
}
|
|
4168
4706
|
}
|
|
4707
|
+
const budget = withinBudget([worker, ...fallback]);
|
|
4708
|
+
[worker, ...fallback] = budget.chain;
|
|
4709
|
+
if (budget.warning) warnings.push(budget.warning);
|
|
4169
4710
|
const backend = getBackend(worker.backend);
|
|
4170
4711
|
if (parent && backend.capabilities.resume === "none") {
|
|
4171
4712
|
throw new UserError(`the ${backend.name} worker cannot continue sessions`, 3);
|
|
@@ -4174,18 +4715,18 @@ function prepareRun(o) {
|
|
|
4174
4715
|
throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
|
|
4175
4716
|
}
|
|
4176
4717
|
const mode = parent?.mode ?? o.mode;
|
|
4177
|
-
const dir = canonical(
|
|
4178
|
-
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}`);
|
|
4179
4720
|
const root = repoRoot(dir);
|
|
4180
4721
|
if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
|
|
4181
4722
|
if (mode === "write" && !root && !o.allowNonGit) {
|
|
4182
4723
|
throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
|
|
4183
4724
|
}
|
|
4184
|
-
const files = o.files.map((f) =>
|
|
4185
|
-
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}`);
|
|
4186
4727
|
const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
|
|
4187
4728
|
const snap = mode === "read" && !o.review && !o.inPlace && !parent ? wantSnapshot(readIn2, root, dir) : void 0;
|
|
4188
|
-
if (parent?.snapshot?.dir && !
|
|
4729
|
+
if (parent?.snapshot?.dir && !fs29.existsSync(parent.snapshot.dir)) {
|
|
4189
4730
|
throw new UserError(`the read snapshot of run ${parent.id} is gone (cleaned up); ask the question again as a new run`, 3);
|
|
4190
4731
|
}
|
|
4191
4732
|
if (!parent && !snap && !process.env.PITROOM_NO_SECRET_WARNING) {
|
|
@@ -4219,6 +4760,7 @@ function prepareRun(o) {
|
|
|
4219
4760
|
worktree: parent?.mode === "isolate" ? parent.worktree : void 0,
|
|
4220
4761
|
reviewOf: o.review?.of,
|
|
4221
4762
|
auditOf: o.audit?.of,
|
|
4763
|
+
fixOf: o.fix?.of,
|
|
4222
4764
|
auditRate: o.auditRate,
|
|
4223
4765
|
inPlace: o.inPlace || void 0,
|
|
4224
4766
|
cache: o.cache,
|
|
@@ -4232,13 +4774,13 @@ function prepareRun(o) {
|
|
|
4232
4774
|
try {
|
|
4233
4775
|
acquireWriteLock(root, meta.id);
|
|
4234
4776
|
} catch (e) {
|
|
4235
|
-
|
|
4777
|
+
fs29.rmSync(runDir(meta.id), { recursive: true, force: true });
|
|
4236
4778
|
throw e;
|
|
4237
4779
|
}
|
|
4238
4780
|
}
|
|
4239
|
-
|
|
4781
|
+
fs29.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
|
|
4240
4782
|
`);
|
|
4241
|
-
if (work.brief)
|
|
4783
|
+
if (work.brief) fs29.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
|
|
4242
4784
|
return meta;
|
|
4243
4785
|
}
|
|
4244
4786
|
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -4320,7 +4862,7 @@ async function execute(meta) {
|
|
|
4320
4862
|
(meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
|
|
4321
4863
|
for (const f of ["events.jsonl", "stderr.log"]) {
|
|
4322
4864
|
const from = runFile(meta.id, f);
|
|
4323
|
-
if (
|
|
4865
|
+
if (fs29.existsSync(from)) fs29.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
|
|
4324
4866
|
}
|
|
4325
4867
|
writeMeta(meta);
|
|
4326
4868
|
}
|
|
@@ -4339,7 +4881,18 @@ async function execute(meta) {
|
|
|
4339
4881
|
}
|
|
4340
4882
|
function autoAudit(meta) {
|
|
4341
4883
|
try {
|
|
4342
|
-
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;
|
|
4343
4896
|
startAudit(meta);
|
|
4344
4897
|
} catch {
|
|
4345
4898
|
}
|
|
@@ -4373,6 +4926,30 @@ function settleAudit(a) {
|
|
|
4373
4926
|
const target = readMeta(a.auditOf);
|
|
4374
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 };
|
|
4375
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);
|
|
4376
4953
|
} catch {
|
|
4377
4954
|
}
|
|
4378
4955
|
}
|
|
@@ -4384,8 +4961,8 @@ function prepareTree(meta) {
|
|
|
4384
4961
|
const s = readSnapshot(root);
|
|
4385
4962
|
meta.snapshot.dir = s.dir;
|
|
4386
4963
|
meta.snapshot.tree = s.tree;
|
|
4387
|
-
meta.cwd =
|
|
4388
|
-
if (!
|
|
4964
|
+
meta.cwd = path25.join(s.dir, path25.relative(root, meta.dir));
|
|
4965
|
+
if (!fs29.existsSync(meta.cwd)) meta.cwd = s.dir;
|
|
4389
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`);
|
|
4390
4967
|
} catch (e) {
|
|
4391
4968
|
meta.snapshot = void 0;
|
|
@@ -4395,9 +4972,9 @@ function prepareTree(meta) {
|
|
|
4395
4972
|
if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
|
|
4396
4973
|
if (root && meta.mode === "isolate" && !meta.worktree) {
|
|
4397
4974
|
meta.baseTree = snapshotTree(root);
|
|
4398
|
-
meta.worktree =
|
|
4975
|
+
meta.worktree = path25.join(worktreesDir(), meta.id);
|
|
4399
4976
|
createIsolatedCopy(root, meta.baseTree, meta.worktree);
|
|
4400
|
-
meta.cwd =
|
|
4977
|
+
meta.cwd = path25.join(meta.worktree, path25.relative(root, meta.dir));
|
|
4401
4978
|
const linked = linkIntoWorktree(root, meta.worktree, meta.link);
|
|
4402
4979
|
if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
|
|
4403
4980
|
}
|
|
@@ -4405,7 +4982,7 @@ function prepareTree(meta) {
|
|
|
4405
4982
|
function attempt(meta, backend, target) {
|
|
4406
4983
|
const inv = backend.invocation({
|
|
4407
4984
|
mode: meta.mode,
|
|
4408
|
-
prompt: buildPrompt(meta.mode, meta.task, !!meta.parent),
|
|
4985
|
+
prompt: buildPrompt(meta.mode, meta.task, !!meta.parent, !!meta.auditOf),
|
|
4409
4986
|
cwd: meta.cwd,
|
|
4410
4987
|
model: target.model,
|
|
4411
4988
|
sessionId: meta.sessionId,
|
|
@@ -4420,7 +4997,7 @@ function attempt(meta, backend, target) {
|
|
|
4420
4997
|
timeoutSec: meta.timeoutSec
|
|
4421
4998
|
});
|
|
4422
4999
|
}
|
|
4423
|
-
var read2 = (f) =>
|
|
5000
|
+
var read2 = (f) => fs29.existsSync(f) ? fs29.readFileSync(f, "utf8") : "";
|
|
4424
5001
|
function retryableFailure(meta, backend, res) {
|
|
4425
5002
|
const run2 = backend.parse(read2(runFile(meta.id, "events.jsonl")));
|
|
4426
5003
|
if (run2.usage.steps > 0) return void 0;
|
|
@@ -4440,12 +5017,12 @@ function finalize(meta, res) {
|
|
|
4440
5017
|
if (meta.snapshot?.dir && meta.repoRoot) run2.finalText = backToProject(run2.finalText, meta.snapshot.dir, meta.repoRoot);
|
|
4441
5018
|
meta.sessionId = run2.sessionId ?? meta.sessionId;
|
|
4442
5019
|
meta.usage = run2.usage;
|
|
4443
|
-
|
|
5020
|
+
fs29.writeFileSync(runFile(meta.id, "summary.md"), `${run2.finalText}
|
|
4444
5021
|
`);
|
|
4445
5022
|
if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run2.finalText);
|
|
4446
5023
|
if (meta.reviewOf) {
|
|
4447
5024
|
meta.verdict = parseVerdict(run2.finalText);
|
|
4448
|
-
if (meta.packageFile)
|
|
5025
|
+
if (meta.packageFile) fs29.rmSync(meta.packageFile, { force: true });
|
|
4449
5026
|
}
|
|
4450
5027
|
if (!res.timedOut && !res.stopped && !meta.error) {
|
|
4451
5028
|
const f = backend.failure(run2, read2(runFile(meta.id, "stderr.log")), res.code);
|
|
@@ -4505,16 +5082,16 @@ function captureChanges(meta) {
|
|
|
4505
5082
|
const d = diffTrees(target, meta.baseTree, meta.afterTree);
|
|
4506
5083
|
meta.changes = d.changes;
|
|
4507
5084
|
meta.stats = d.stats;
|
|
4508
|
-
|
|
5085
|
+
fs29.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
|
|
4509
5086
|
}
|
|
4510
5087
|
function verifyPath() {
|
|
4511
|
-
const extra = [
|
|
4512
|
-
const parts = (process.env.PATH ?? "").split(
|
|
4513
|
-
for (const d of process.platform === "win32" ? [
|
|
4514
|
-
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);
|
|
4515
5092
|
}
|
|
4516
5093
|
function runVerify(meta) {
|
|
4517
|
-
const r =
|
|
5094
|
+
const r = spawnSync8(meta.verify, {
|
|
4518
5095
|
cwd: meta.cwd,
|
|
4519
5096
|
shell: true,
|
|
4520
5097
|
encoding: "utf8",
|
|
@@ -4524,7 +5101,7 @@ function runVerify(meta) {
|
|
|
4524
5101
|
env: { ...process.env, PATH: verifyPath(), PWD: meta.cwd, PITROOM_ACTIVE: "1" }
|
|
4525
5102
|
});
|
|
4526
5103
|
const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
|
4527
|
-
|
|
5104
|
+
fs29.writeFileSync(runFile(meta.id, "verify.log"), output);
|
|
4528
5105
|
const notFound = process.platform === "win32" && r.status === 1 && /is not recognized as an internal or external command/.test(output);
|
|
4529
5106
|
return { ok: r.status === 0, code: notFound ? 127 : r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
|
|
4530
5107
|
}
|
|
@@ -4579,12 +5156,12 @@ function discardRun(meta) {
|
|
|
4579
5156
|
return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
|
|
4580
5157
|
}
|
|
4581
5158
|
function cleanupWorktree(meta) {
|
|
4582
|
-
if (meta.worktree &&
|
|
5159
|
+
if (meta.worktree && fs29.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
|
|
4583
5160
|
}
|
|
4584
5161
|
|
|
4585
5162
|
// src/cli/mcp-extras.ts
|
|
4586
|
-
import
|
|
4587
|
-
import
|
|
5163
|
+
import fs30 from "node:fs";
|
|
5164
|
+
import path26 from "node:path";
|
|
4588
5165
|
|
|
4589
5166
|
// src/cli/mcp-support.ts
|
|
4590
5167
|
import { spawn as spawn5 } from "node:child_process";
|
|
@@ -4649,7 +5226,7 @@ function workerFlags(a, only) {
|
|
|
4649
5226
|
return out;
|
|
4650
5227
|
}
|
|
4651
5228
|
var strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
4652
|
-
var
|
|
5229
|
+
var clip7 = (s) => s.length > MAX_OUTPUT ? `${s.slice(0, MAX_OUTPUT)}
|
|
4653
5230
|
\u2026 (${s.length - MAX_OUTPUT} more characters)` : s;
|
|
4654
5231
|
function pit(args, timeoutMs = 60 * 6e4, signal) {
|
|
4655
5232
|
return new Promise((resolve2) => {
|
|
@@ -4674,10 +5251,10 @@ function pit(args, timeoutMs = 60 * 6e4, signal) {
|
|
|
4674
5251
|
}
|
|
4675
5252
|
function asResult(r, still) {
|
|
4676
5253
|
const text = [r.out, r.err && r.code !== 0 ? r.err : ""].filter(Boolean).join("\n\n") || "(no output)";
|
|
4677
|
-
if (r.code === 75 && still) return { text:
|
|
5254
|
+
if (r.code === 75 && still) return { text: clip7(`${text}
|
|
4678
5255
|
|
|
4679
5256
|
${still}`) };
|
|
4680
|
-
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 };
|
|
4681
5258
|
}
|
|
4682
5259
|
var plain = async (args, ctx, timeoutMs = 6e4) => asResult(await pit(args, timeoutMs, ctx.signal));
|
|
4683
5260
|
function trackProgress(ctx, ids) {
|
|
@@ -4792,7 +5369,7 @@ var RpcError = class extends Error {
|
|
|
4792
5369
|
var RUN = /^pitroom:\/\/run\/(\d{8}-\d{6}-[0-9a-f]{4})(\/patch)?$/;
|
|
4793
5370
|
var LISTED = 30;
|
|
4794
5371
|
var runOfUri = (uri) => RUN.exec(uri)?.[1];
|
|
4795
|
-
var
|
|
5372
|
+
var oneLine7 = (text, n) => {
|
|
4796
5373
|
const flat = text.replace(/\s+/g, " ").trim();
|
|
4797
5374
|
return flat.length > n ? `${flat.slice(0, n - 1)}\u2026` : flat;
|
|
4798
5375
|
};
|
|
@@ -4802,8 +5379,8 @@ function listResources() {
|
|
|
4802
5379
|
try {
|
|
4803
5380
|
const m = freshMeta(id);
|
|
4804
5381
|
const what2 = `${m.state} \xB7 ${m.mode} \xB7 ${m.worker.backend}${m.worker.model ? `:${m.worker.model.split("/").pop()}` : ""}`;
|
|
4805
|
-
resources.push({ uri: `pitroom://run/${id}`, name: `run ${id}`, title:
|
|
4806
|
-
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" });
|
|
4807
5384
|
} catch {
|
|
4808
5385
|
}
|
|
4809
5386
|
}
|
|
@@ -4821,7 +5398,7 @@ async function readResource(uri, ctx) {
|
|
|
4821
5398
|
const patch = m[2] !== void 0;
|
|
4822
5399
|
const r = await pit(["show", m[1], patch ? "--patch" : "--full"], 6e4, ctx.signal);
|
|
4823
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`);
|
|
4824
|
-
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) }] };
|
|
4825
5402
|
}
|
|
4826
5403
|
var PROMPTS = [
|
|
4827
5404
|
{
|
|
@@ -4869,7 +5446,7 @@ function skillPrompts() {
|
|
|
4869
5446
|
const root = packageRoot();
|
|
4870
5447
|
for (const name of skillNames(root)) {
|
|
4871
5448
|
try {
|
|
4872
|
-
const text =
|
|
5449
|
+
const text = fs30.readFileSync(path26.join(root, "skills", name, "SKILL.md"), "utf8");
|
|
4873
5450
|
const front = /^---\n([\s\S]*?)\n---\n/.exec(text);
|
|
4874
5451
|
const body = (front ? text.slice(front[0].length) : text).trim();
|
|
4875
5452
|
const description = /^description:\s*(.+)$/m.exec(front?.[1] ?? "")?.[1]?.trim() ?? `The ${name} skill.`;
|
|
@@ -5387,22 +5964,22 @@ async function serveMcp() {
|
|
|
5387
5964
|
|
|
5388
5965
|
// src/cli/mcp-http.ts
|
|
5389
5966
|
import crypto8 from "node:crypto";
|
|
5390
|
-
import
|
|
5967
|
+
import fs32 from "node:fs";
|
|
5391
5968
|
import http2 from "node:http";
|
|
5392
|
-
import
|
|
5969
|
+
import path28 from "node:path";
|
|
5393
5970
|
|
|
5394
5971
|
// src/core/dash.ts
|
|
5395
5972
|
import { spawn as spawn6 } from "node:child_process";
|
|
5396
5973
|
import crypto7 from "node:crypto";
|
|
5397
|
-
import
|
|
5974
|
+
import fs31 from "node:fs";
|
|
5398
5975
|
import http from "node:http";
|
|
5399
|
-
import
|
|
5400
|
-
import { fileURLToPath as
|
|
5976
|
+
import path27 from "node:path";
|
|
5977
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
5401
5978
|
|
|
5402
5979
|
// src/core/file-diff.ts
|
|
5403
5980
|
var PREVIEW_LINES = 300;
|
|
5404
5981
|
function gitPath(value, prefix = true) {
|
|
5405
|
-
let
|
|
5982
|
+
let path36 = value;
|
|
5406
5983
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
5407
5984
|
const bytes = [];
|
|
5408
5985
|
const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
|
|
@@ -5410,9 +5987,9 @@ function gitPath(value, prefix = true) {
|
|
|
5410
5987
|
if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
|
|
5411
5988
|
else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
|
|
5412
5989
|
}
|
|
5413
|
-
|
|
5990
|
+
path36 = new TextDecoder().decode(new Uint8Array(bytes));
|
|
5414
5991
|
}
|
|
5415
|
-
return prefix ?
|
|
5992
|
+
return prefix ? path36.replace(/^[ab]\//, "") : path36;
|
|
5416
5993
|
}
|
|
5417
5994
|
function headerPath(header) {
|
|
5418
5995
|
const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
|
|
@@ -5527,7 +6104,7 @@ var WEEK_MS2 = 7 * 24 * 3600 * 1e3;
|
|
|
5527
6104
|
var SCAN = 400;
|
|
5528
6105
|
var RUN_ID3 = /^\d{8}-\d{6}-[0-9a-f]{4}$/;
|
|
5529
6106
|
var LOCAL_HOST = /^(127\.0\.0\.1|localhost|\[::1\]):\d+$/i;
|
|
5530
|
-
var
|
|
6107
|
+
var oneLine8 = (s, max) => {
|
|
5531
6108
|
const t = s.replace(/\s+/g, " ").trim();
|
|
5532
6109
|
return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
|
|
5533
6110
|
};
|
|
@@ -5538,6 +6115,22 @@ function findings(m) {
|
|
|
5538
6115
|
return parts.length ? parts.join(" \xB7 ") : "no findings";
|
|
5539
6116
|
}
|
|
5540
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
|
+
}
|
|
5541
6134
|
function toRun(m) {
|
|
5542
6135
|
const l = m.state === "running" ? live(m) : void 0;
|
|
5543
6136
|
return {
|
|
@@ -5559,12 +6152,12 @@ function toRun(m) {
|
|
|
5559
6152
|
changes: m.changes?.length || void 0,
|
|
5560
6153
|
applied: m.applied || void 0,
|
|
5561
6154
|
discardable: canDiscard(m) || void 0,
|
|
5562
|
-
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)
|
|
5563
6156
|
};
|
|
5564
6157
|
}
|
|
5565
6158
|
function dashState(opts = {}) {
|
|
5566
6159
|
const limit = Math.min(Math.max(opts.limit ?? 40, 1), 200);
|
|
5567
|
-
const
|
|
6160
|
+
const metas = [];
|
|
5568
6161
|
const groups = /* @__PURE__ */ new Set();
|
|
5569
6162
|
for (const id of listRunIds().slice(-SCAN).reverse()) {
|
|
5570
6163
|
let m;
|
|
@@ -5575,8 +6168,10 @@ function dashState(opts = {}) {
|
|
|
5575
6168
|
}
|
|
5576
6169
|
if (m.group) groups.add(m.group);
|
|
5577
6170
|
if (opts.group && m.group !== opts.group) continue;
|
|
5578
|
-
|
|
6171
|
+
metas.push(m);
|
|
5579
6172
|
}
|
|
6173
|
+
const whole = new Set(metas.slice(0, limit).flatMap((m) => m.group ? [m.group] : []));
|
|
6174
|
+
const runs = metas.filter((m, i) => i < limit || m.group && whole.has(m.group)).map(toRun);
|
|
5580
6175
|
return {
|
|
5581
6176
|
price: primaryPrice().name,
|
|
5582
6177
|
running: runs.filter((r) => r.state === "running" || r.state === "queued").length,
|
|
@@ -5660,15 +6255,15 @@ var HEADERS = {
|
|
|
5660
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'`
|
|
5661
6256
|
};
|
|
5662
6257
|
var ASSETS = { "/assets/app.js": "text/javascript; charset=utf-8", "/assets/app.css": "text/css; charset=utf-8" };
|
|
5663
|
-
var assetsDir = () =>
|
|
6258
|
+
var assetsDir = () => fileURLToPath4(new URL("./ui/", import.meta.url));
|
|
5664
6259
|
var assetCache = /* @__PURE__ */ new Map();
|
|
5665
6260
|
function readAsset(route) {
|
|
5666
6261
|
try {
|
|
5667
|
-
const file2 =
|
|
5668
|
-
const { mtimeMs } =
|
|
6262
|
+
const file2 = path27.join(assetsDir(), path27.basename(route));
|
|
6263
|
+
const { mtimeMs } = fs31.statSync(file2);
|
|
5669
6264
|
const hit = assetCache.get(route);
|
|
5670
6265
|
if (hit && hit.mtimeMs === mtimeMs) return hit.data;
|
|
5671
|
-
const data =
|
|
6266
|
+
const data = fs31.readFileSync(file2);
|
|
5672
6267
|
assetCache.set(route, { mtimeMs, data });
|
|
5673
6268
|
return data;
|
|
5674
6269
|
} catch {
|
|
@@ -5687,6 +6282,18 @@ function handler(touch2, token) {
|
|
|
5687
6282
|
const given = Buffer.from(String(req.headers["x-pitroom-token"] ?? ""));
|
|
5688
6283
|
if (given.length !== tokenBuf.length || !crypto7.timingSafeEqual(given, tokenBuf)) return json(res, 403, { error: "forbidden" });
|
|
5689
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
|
+
}
|
|
5690
6297
|
const m = /^\/api\/run\/([^/]+)\/(stop|discard)$/.exec(pathname);
|
|
5691
6298
|
if (!m) return json(res, 404, { error: "not found" });
|
|
5692
6299
|
let meta;
|
|
@@ -5743,6 +6350,17 @@ function handler(touch2, token) {
|
|
|
5743
6350
|
const days2 = Number(url.searchParams.get("days") ?? 30);
|
|
5744
6351
|
return json(res, 200, { ...historyStats(days2 > 0 ? Date.now() - days2 * 864e5 : void 0), price: primaryPrice().name });
|
|
5745
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
|
+
}
|
|
5746
6364
|
const m = /^\/api\/run\/([^/]+)$/.exec(url.pathname);
|
|
5747
6365
|
if (m) {
|
|
5748
6366
|
const d = runDetail(m[1]);
|
|
@@ -5774,7 +6392,7 @@ function startDash(opts) {
|
|
|
5774
6392
|
});
|
|
5775
6393
|
});
|
|
5776
6394
|
}
|
|
5777
|
-
var registryFile = () =>
|
|
6395
|
+
var registryFile = () => path27.join(home(), "dash.json");
|
|
5778
6396
|
async function ping(port) {
|
|
5779
6397
|
try {
|
|
5780
6398
|
const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
|
|
@@ -5786,12 +6404,12 @@ async function ping(port) {
|
|
|
5786
6404
|
async function runningDash() {
|
|
5787
6405
|
let r;
|
|
5788
6406
|
try {
|
|
5789
|
-
r = JSON.parse(
|
|
6407
|
+
r = JSON.parse(fs31.readFileSync(registryFile(), "utf8"));
|
|
5790
6408
|
} catch {
|
|
5791
6409
|
return void 0;
|
|
5792
6410
|
}
|
|
5793
6411
|
if (!isAlive(r.pid)) {
|
|
5794
|
-
|
|
6412
|
+
fs31.rmSync(registryFile(), { force: true });
|
|
5795
6413
|
return void 0;
|
|
5796
6414
|
}
|
|
5797
6415
|
return await ping(r.port) ? { ...r, url: `http://127.0.0.1:${r.port}/` } : void 0;
|
|
@@ -5808,7 +6426,7 @@ async function dashCommand(o) {
|
|
|
5808
6426
|
if (o.stop) {
|
|
5809
6427
|
if (!existing) throw new UserError("pitroom dash is not running");
|
|
5810
6428
|
process.kill(existing.pid, "SIGTERM");
|
|
5811
|
-
|
|
6429
|
+
fs31.rmSync(registryFile(), { force: true });
|
|
5812
6430
|
o.log(`stopped pitroom dash (${existing.url})`);
|
|
5813
6431
|
return 0;
|
|
5814
6432
|
}
|
|
@@ -5845,9 +6463,9 @@ async function dashCommand(o) {
|
|
|
5845
6463
|
if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
|
|
5846
6464
|
dash = await startDash({ port: 0, idleMs: o.idleMs });
|
|
5847
6465
|
}
|
|
5848
|
-
|
|
6466
|
+
fs31.mkdirSync(home(), { recursive: true });
|
|
5849
6467
|
const tmp = `${registryFile()}.${process.pid}.tmp`;
|
|
5850
|
-
|
|
6468
|
+
fs31.writeFileSync(tmp, JSON.stringify({ pid: process.pid, port: dash.port }));
|
|
5851
6469
|
renameOver(tmp, registryFile());
|
|
5852
6470
|
const stop = () => void dash.close();
|
|
5853
6471
|
process.once("SIGINT", stop);
|
|
@@ -5857,7 +6475,7 @@ async function dashCommand(o) {
|
|
|
5857
6475
|
if (o.open) openBrowser(dash.url);
|
|
5858
6476
|
await dash.closed;
|
|
5859
6477
|
try {
|
|
5860
|
-
if (JSON.parse(
|
|
6478
|
+
if (JSON.parse(fs31.readFileSync(registryFile(), "utf8")).pid === process.pid) fs31.rmSync(registryFile(), { force: true });
|
|
5861
6479
|
} catch {
|
|
5862
6480
|
}
|
|
5863
6481
|
return 0;
|
|
@@ -5869,10 +6487,10 @@ var MAX_BODY = 4e6;
|
|
|
5869
6487
|
var MAX_SESSIONS = 64;
|
|
5870
6488
|
var IDLE_MS = 60 * 6e4;
|
|
5871
6489
|
var PATH = "/mcp";
|
|
5872
|
-
var tokenFile = () =>
|
|
6490
|
+
var tokenFile = () => path28.join(home(), "mcp-token");
|
|
5873
6491
|
var readToken = (file2) => {
|
|
5874
6492
|
try {
|
|
5875
|
-
const kept =
|
|
6493
|
+
const kept = fs32.readFileSync(file2, "utf8").trim();
|
|
5876
6494
|
return kept.length >= 16 ? kept : void 0;
|
|
5877
6495
|
} catch {
|
|
5878
6496
|
return void 0;
|
|
@@ -5887,10 +6505,10 @@ function mcpToken() {
|
|
|
5887
6505
|
const file2 = tokenFile();
|
|
5888
6506
|
let token = readToken(file2);
|
|
5889
6507
|
if (!token) {
|
|
5890
|
-
|
|
6508
|
+
fs32.mkdirSync(path28.dirname(file2), { recursive: true });
|
|
5891
6509
|
token = crypto8.randomBytes(32).toString("hex");
|
|
5892
6510
|
try {
|
|
5893
|
-
|
|
6511
|
+
fs32.writeFileSync(file2, `${token}
|
|
5894
6512
|
`, { mode: 384, flag: "wx" });
|
|
5895
6513
|
} catch (e) {
|
|
5896
6514
|
if (e.code !== "EEXIST") throw e;
|
|
@@ -5899,7 +6517,7 @@ function mcpToken() {
|
|
|
5899
6517
|
}
|
|
5900
6518
|
}
|
|
5901
6519
|
try {
|
|
5902
|
-
|
|
6520
|
+
fs32.chmodSync(file2, 384);
|
|
5903
6521
|
} catch {
|
|
5904
6522
|
}
|
|
5905
6523
|
return { token, from: file2 };
|
|
@@ -5925,7 +6543,7 @@ function reply(res, status, body, headers = {}) {
|
|
|
5925
6543
|
res.writeHead(status, { ...text ? { "Content-Type": "application/json" } : {}, "Content-Length": Buffer.byteLength(text), ...headers });
|
|
5926
6544
|
res.end(text);
|
|
5927
6545
|
}
|
|
5928
|
-
var
|
|
6546
|
+
var failure7 = (code, message) => ({ jsonrpc: "2.0", id: null, error: { code, message } });
|
|
5929
6547
|
function readBody(req) {
|
|
5930
6548
|
return new Promise((resolve2, reject) => {
|
|
5931
6549
|
const chunks = [];
|
|
@@ -5952,23 +6570,23 @@ function evictIdle(sessions) {
|
|
|
5952
6570
|
function makeHandler(token, sessions) {
|
|
5953
6571
|
return async (req, res) => {
|
|
5954
6572
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
5955
|
-
if (url.pathname !== PATH) return reply(res, 404,
|
|
5956
|
-
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"));
|
|
5957
6575
|
const origin = req.headers.origin;
|
|
5958
|
-
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"));
|
|
5959
6577
|
const auth = /^Bearer\s+(.+)$/i.exec(req.headers.authorization ?? "")?.[1]?.trim();
|
|
5960
|
-
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" });
|
|
5961
6579
|
const sid = typeof req.headers["mcp-session-id"] === "string" ? req.headers["mcp-session-id"] : void 0;
|
|
5962
6580
|
if (req.method === "DELETE") {
|
|
5963
|
-
if (!sid || !sessions.has(sid)) return reply(res, 404,
|
|
6581
|
+
if (!sid || !sessions.has(sid)) return reply(res, 404, failure7(-32600, "no such session"));
|
|
5964
6582
|
drop(sessions, sid);
|
|
5965
6583
|
return reply(res, 204);
|
|
5966
6584
|
}
|
|
5967
6585
|
if (req.method === "GET") {
|
|
5968
|
-
if (!/text\/event-stream|\*\/\*/.test(req.headers.accept ?? "")) return reply(res, 406,
|
|
5969
|
-
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"));
|
|
5970
6588
|
const kept2 = sessions.get(sid);
|
|
5971
|
-
if (!kept2) return reply(res, 404,
|
|
6589
|
+
if (!kept2) return reply(res, 404, failure7(-32600, "no such session: initialize again"));
|
|
5972
6590
|
kept2.closeStream?.();
|
|
5973
6591
|
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" });
|
|
5974
6592
|
res.write(": open\n\n");
|
|
@@ -5997,9 +6615,9 @@ data: ${JSON.stringify(msg)}
|
|
|
5997
6615
|
res.on("close", close);
|
|
5998
6616
|
return;
|
|
5999
6617
|
}
|
|
6000
|
-
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" });
|
|
6001
6619
|
const body = await readBody(req);
|
|
6002
|
-
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`));
|
|
6003
6621
|
let parsed;
|
|
6004
6622
|
try {
|
|
6005
6623
|
parsed = JSON.parse(body);
|
|
@@ -6011,15 +6629,15 @@ data: ${JSON.stringify(msg)}
|
|
|
6011
6629
|
const single = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
6012
6630
|
if (single?.method === "initialize") {
|
|
6013
6631
|
if (sid) drop(sessions, sid);
|
|
6014
|
-
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`));
|
|
6015
6633
|
const id = crypto8.randomUUID();
|
|
6016
6634
|
kept = { session: newSession(), lastSeen: Date.now() };
|
|
6017
6635
|
sessions.set(id, kept);
|
|
6018
6636
|
headers["Mcp-Session-Id"] = id;
|
|
6019
6637
|
} else {
|
|
6020
|
-
if (!sid) return reply(res, 400,
|
|
6638
|
+
if (!sid) return reply(res, 400, failure7(-32600, "Mcp-Session-Id is missing: initialize first"));
|
|
6021
6639
|
kept = sessions.get(sid);
|
|
6022
|
-
if (!kept) return reply(res, 404,
|
|
6640
|
+
if (!kept) return reply(res, 404, failure7(-32600, "no such session: initialize again"));
|
|
6023
6641
|
}
|
|
6024
6642
|
kept.lastSeen = Date.now();
|
|
6025
6643
|
if (!wantsStream(parsed)) {
|
|
@@ -6049,7 +6667,7 @@ async function serveMcpHttp(opts) {
|
|
|
6049
6667
|
handler2(req, res).catch((e) => {
|
|
6050
6668
|
process.stderr.write(`pitroom mcp: ${String(e.stack ?? e)}
|
|
6051
6669
|
`);
|
|
6052
|
-
reply(res, 500,
|
|
6670
|
+
reply(res, 500, failure7(-32603, "internal error"));
|
|
6053
6671
|
});
|
|
6054
6672
|
});
|
|
6055
6673
|
const sweep = setInterval(() => {
|
|
@@ -6080,23 +6698,23 @@ async function serveMcpHttp(opts) {
|
|
|
6080
6698
|
}
|
|
6081
6699
|
|
|
6082
6700
|
// src/cli/commands.ts
|
|
6083
|
-
import { spawnSync as
|
|
6084
|
-
import
|
|
6085
|
-
import
|
|
6086
|
-
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";
|
|
6087
6705
|
|
|
6088
6706
|
// src/core/init.ts
|
|
6089
|
-
import
|
|
6090
|
-
import
|
|
6707
|
+
import fs33 from "node:fs";
|
|
6708
|
+
import path29 from "node:path";
|
|
6091
6709
|
var FREE = /-free$/;
|
|
6092
6710
|
var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
|
|
6093
|
-
var isFound = (
|
|
6711
|
+
var isFound = (binary7) => path29.isAbsolute(binary7) && fs33.existsSync(binary7);
|
|
6094
6712
|
function planInit(opts = {}) {
|
|
6095
6713
|
const file2 = configPath();
|
|
6096
6714
|
const workers = backendIds().map((id) => {
|
|
6097
6715
|
const b = getBackend(id);
|
|
6098
|
-
const
|
|
6099
|
-
return { id, name: b.name, found: isFound(
|
|
6716
|
+
const binary7 = b.binary();
|
|
6717
|
+
return { id, name: b.name, found: isFound(binary7), binary: binary7 };
|
|
6100
6718
|
});
|
|
6101
6719
|
const notes = [];
|
|
6102
6720
|
const config = {};
|
|
@@ -6110,15 +6728,15 @@ function planInit(opts = {}) {
|
|
|
6110
6728
|
} catch {
|
|
6111
6729
|
notes.push("could not list OpenCode models (is it logged in?)");
|
|
6112
6730
|
}
|
|
6113
|
-
const
|
|
6114
|
-
const chosen = opts.model ??
|
|
6731
|
+
const defaultModel6 = b.defaultModel?.();
|
|
6732
|
+
const chosen = opts.model ?? defaultModel6;
|
|
6115
6733
|
const free = models.filter((m) => FREE.test(m) && m !== chosen);
|
|
6116
|
-
opencode2 = { defaultModel:
|
|
6734
|
+
opencode2 = { defaultModel: defaultModel6, models: models.length, free: free.slice(0, 6) };
|
|
6117
6735
|
if (opts.model) {
|
|
6118
6736
|
if (models.length && !models.includes(opts.model)) throw new UserError(`"${opts.model}" is not in \`opencode models\``);
|
|
6119
6737
|
if (!models.length) notes.push(`could not check "${opts.model}": \`opencode models\` listed nothing`);
|
|
6120
6738
|
config.models = { opencode: opts.model };
|
|
6121
|
-
} else if (!
|
|
6739
|
+
} else if (!defaultModel6) {
|
|
6122
6740
|
blocked = `OpenCode has no default model: choose one with --model <id>${free.length ? ` (free models you have: ${free.slice(0, 4).join(", ")})` : ""}`;
|
|
6123
6741
|
}
|
|
6124
6742
|
const fallback = opts.fallback ?? free.slice(0, 2);
|
|
@@ -6136,15 +6754,15 @@ function planInit(opts = {}) {
|
|
|
6136
6754
|
if (Object.keys(tiers).length > 1) config.tiers = tiers;
|
|
6137
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";
|
|
6138
6756
|
else if (!blocked && !Object.keys(config).length) notes.push("nothing to propose: the default model and the single worker need no config");
|
|
6139
|
-
return { path: file2, exists:
|
|
6757
|
+
return { path: file2, exists: fs33.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
|
|
6140
6758
|
}
|
|
6141
6759
|
function writeInit(plan, force) {
|
|
6142
6760
|
if (plan.blocked) throw new UserError(plan.blocked);
|
|
6143
6761
|
if (!Object.keys(plan.config).length) throw new UserError("nothing to write: the proposal is empty");
|
|
6144
|
-
if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${
|
|
6145
|
-
|
|
6146
|
-
if (plan.exists)
|
|
6147
|
-
|
|
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)}
|
|
6148
6766
|
`);
|
|
6149
6767
|
return plan.path;
|
|
6150
6768
|
}
|
|
@@ -6164,18 +6782,18 @@ function formatInit(plan, written) {
|
|
|
6164
6782
|
}
|
|
6165
6783
|
|
|
6166
6784
|
// src/core/mcp-install.ts
|
|
6167
|
-
import { spawnSync as
|
|
6168
|
-
import
|
|
6169
|
-
import
|
|
6170
|
-
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";
|
|
6171
6789
|
var CLIENT_IDS = ["claude-code", "codex", "gemini", "cursor", "claude-desktop"];
|
|
6172
6790
|
var NAME = "pitroom";
|
|
6173
6791
|
function mcpCommand() {
|
|
6174
6792
|
const launcher = launcherPath();
|
|
6175
6793
|
if (isPitroomLauncher(launcher)) return { command: launcher, args: ["mcp"] };
|
|
6176
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
6177
|
-
const p =
|
|
6178
|
-
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"] };
|
|
6179
6797
|
}
|
|
6180
6798
|
return { command: process.execPath, args: [process.argv[1] ?? "pitroom", "mcp"] };
|
|
6181
6799
|
}
|
|
@@ -6185,13 +6803,13 @@ var same = (a, b) => {
|
|
|
6185
6803
|
};
|
|
6186
6804
|
function run(backendId, args) {
|
|
6187
6805
|
const { command, prefix } = resolveCommand(getBackend(backendId).binary());
|
|
6188
|
-
const r =
|
|
6806
|
+
const r = spawnSync9(command, [...prefix, ...args], { encoding: "utf8", timeout: 6e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
6189
6807
|
return { ok: r.status === 0 && !r.error, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
|
|
6190
6808
|
}
|
|
6191
6809
|
var runnable = (backendId) => run(backendId, ["--version"]).ok;
|
|
6192
6810
|
var readJson = (file2) => {
|
|
6193
6811
|
try {
|
|
6194
|
-
return JSON.parse(
|
|
6812
|
+
return JSON.parse(fs34.readFileSync(file2, "utf8"));
|
|
6195
6813
|
} catch {
|
|
6196
6814
|
return void 0;
|
|
6197
6815
|
}
|
|
@@ -6200,8 +6818,8 @@ var jsonState = (file2, c) => {
|
|
|
6200
6818
|
const entry = readJson(file2)?.mcpServers?.[NAME];
|
|
6201
6819
|
return entry === void 0 ? "absent" : same(c, entry) ? "same" : "different";
|
|
6202
6820
|
};
|
|
6203
|
-
var home2 = () =>
|
|
6204
|
-
var geminiDir = () =>
|
|
6821
|
+
var home2 = () => os11.homedir();
|
|
6822
|
+
var geminiDir = () => path30.join(process.env.GEMINI_CLI_HOME ?? home2(), ".gemini");
|
|
6205
6823
|
function cliClient(id, backend, name, where, o) {
|
|
6206
6824
|
return {
|
|
6207
6825
|
id,
|
|
@@ -6224,43 +6842,43 @@ function jsonClient(id, name, file2, dir) {
|
|
|
6224
6842
|
return {
|
|
6225
6843
|
id,
|
|
6226
6844
|
name,
|
|
6227
|
-
found: () =>
|
|
6845
|
+
found: () => fs34.existsSync(dir()),
|
|
6228
6846
|
state: (c) => jsonState(file2(), c),
|
|
6229
6847
|
add: (c) => {
|
|
6230
6848
|
let config = {};
|
|
6231
|
-
if (
|
|
6849
|
+
if (fs34.existsSync(file2())) {
|
|
6232
6850
|
config = readJson(file2());
|
|
6233
6851
|
if (!config || typeof config !== "object" || Array.isArray(config)) return { ok: false, message: `${rel()} is not valid JSON: left alone` };
|
|
6234
6852
|
const backup = `${file2()}.bak-pitroom`;
|
|
6235
|
-
if (!
|
|
6853
|
+
if (!fs34.existsSync(backup)) fs34.copyFileSync(file2(), backup);
|
|
6236
6854
|
}
|
|
6237
6855
|
config.mcpServers = { ...config.mcpServers ?? {}, [NAME]: { command: c.command, args: c.args } };
|
|
6238
|
-
|
|
6856
|
+
fs34.mkdirSync(path30.dirname(file2()), { recursive: true });
|
|
6239
6857
|
const tmp = `${file2()}.${process.pid}.tmp`;
|
|
6240
|
-
|
|
6858
|
+
fs34.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
|
|
6241
6859
|
`);
|
|
6242
|
-
|
|
6860
|
+
fs34.renameSync(tmp, file2());
|
|
6243
6861
|
return { ok: true, message: rel() };
|
|
6244
6862
|
},
|
|
6245
6863
|
remove: () => {
|
|
6246
6864
|
const config = readJson(file2());
|
|
6247
6865
|
if (!config?.mcpServers?.[NAME]) return { ok: true, message: rel() };
|
|
6248
6866
|
delete config.mcpServers[NAME];
|
|
6249
|
-
|
|
6867
|
+
fs34.writeFileSync(file2(), `${JSON.stringify(config, null, 2)}
|
|
6250
6868
|
`);
|
|
6251
6869
|
return { ok: true, message: rel() };
|
|
6252
6870
|
}
|
|
6253
6871
|
};
|
|
6254
6872
|
}
|
|
6255
6873
|
function desktopFile() {
|
|
6256
|
-
if (process.platform === "darwin") return
|
|
6257
|
-
if (process.platform === "win32") return
|
|
6258
|
-
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");
|
|
6259
6877
|
}
|
|
6260
6878
|
function codexState(c) {
|
|
6261
6879
|
let text;
|
|
6262
6880
|
try {
|
|
6263
|
-
text =
|
|
6881
|
+
text = fs34.readFileSync(path30.join(process.env.CODEX_HOME ?? path30.join(home2(), ".codex"), "config.toml"), "utf8");
|
|
6264
6882
|
} catch {
|
|
6265
6883
|
return "absent";
|
|
6266
6884
|
}
|
|
@@ -6278,7 +6896,7 @@ function codexState(c) {
|
|
|
6278
6896
|
function clients() {
|
|
6279
6897
|
return [
|
|
6280
6898
|
cliClient("claude-code", "claude", "Claude Code", "user settings (~/.claude.json)", {
|
|
6281
|
-
state: (c) => jsonState(
|
|
6899
|
+
state: (c) => jsonState(path30.join(home2(), ".claude.json"), c),
|
|
6282
6900
|
add: (c) => ["mcp", "add", "--scope", "user", NAME, "--", c.command, ...c.args],
|
|
6283
6901
|
remove: () => ["mcp", "remove", NAME, "--scope", "user"]
|
|
6284
6902
|
}),
|
|
@@ -6288,12 +6906,12 @@ function clients() {
|
|
|
6288
6906
|
remove: () => ["mcp", "remove", NAME]
|
|
6289
6907
|
}),
|
|
6290
6908
|
cliClient("gemini", "gemini", "Gemini CLI", "user settings (~/.gemini/settings.json)", {
|
|
6291
|
-
state: (c) => jsonState(
|
|
6909
|
+
state: (c) => jsonState(path30.join(geminiDir(), "settings.json"), c),
|
|
6292
6910
|
add: (c) => ["mcp", "add", "--scope", "user", NAME, c.command, ...c.args],
|
|
6293
6911
|
remove: () => ["mcp", "remove", "--scope", "user", NAME]
|
|
6294
6912
|
}),
|
|
6295
|
-
jsonClient("cursor", "Cursor", () =>
|
|
6296
|
-
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()))
|
|
6297
6915
|
];
|
|
6298
6916
|
}
|
|
6299
6917
|
function attempt2(f) {
|
|
@@ -6364,16 +6982,16 @@ var parseClients = (value) => {
|
|
|
6364
6982
|
|
|
6365
6983
|
// src/core/plan-status.ts
|
|
6366
6984
|
import crypto9 from "node:crypto";
|
|
6367
|
-
import
|
|
6368
|
-
import
|
|
6985
|
+
import fs35 from "node:fs";
|
|
6986
|
+
import path31 from "node:path";
|
|
6369
6987
|
function notesFile(plan) {
|
|
6370
6988
|
const id = crypto9.createHash("sha1").update(plan.file).digest("hex").slice(0, 12);
|
|
6371
|
-
return
|
|
6989
|
+
return path31.join(home(), "plans", id, "notes.md");
|
|
6372
6990
|
}
|
|
6373
6991
|
function readNotes(plan) {
|
|
6374
6992
|
const f = notesFile(plan);
|
|
6375
|
-
if (!
|
|
6376
|
-
return
|
|
6993
|
+
if (!fs35.existsSync(f)) return [];
|
|
6994
|
+
return fs35.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
|
|
6377
6995
|
const m = /^(\d{4}-\d\d-\d\d \d\d:\d\d) (.*)$/.exec(l);
|
|
6378
6996
|
return m ? { at: m[1], text: m[2] } : { at: "", text: l };
|
|
6379
6997
|
});
|
|
@@ -6381,11 +6999,11 @@ function readNotes(plan) {
|
|
|
6381
6999
|
function addNote(planFile, text) {
|
|
6382
7000
|
const plan = loadPlan(planFile);
|
|
6383
7001
|
const f = notesFile(plan);
|
|
6384
|
-
|
|
6385
|
-
if (!
|
|
7002
|
+
fs35.mkdirSync(path31.dirname(f), { recursive: true });
|
|
7003
|
+
if (!fs35.existsSync(f)) fs35.writeFileSync(f, `# plan: ${plan.file}
|
|
6386
7004
|
`);
|
|
6387
7005
|
const at = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
|
|
6388
|
-
|
|
7006
|
+
fs35.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
|
|
6389
7007
|
`);
|
|
6390
7008
|
return f;
|
|
6391
7009
|
}
|
|
@@ -6453,26 +7071,26 @@ ${rulings}`,
|
|
|
6453
7071
|
|
|
6454
7072
|
// src/core/cache.ts
|
|
6455
7073
|
import crypto10 from "node:crypto";
|
|
6456
|
-
import
|
|
6457
|
-
import
|
|
7074
|
+
import fs36 from "node:fs";
|
|
7075
|
+
import path32 from "node:path";
|
|
6458
7076
|
var SCAN2 = 500;
|
|
6459
7077
|
function cacheKey(o) {
|
|
6460
7078
|
if (o.verify || o.mode !== "read" || o.continueFrom || o.web || o.plan || o.review || o.audit || o.auditRate === 1) return void 0;
|
|
6461
7079
|
if (!(effective().cacheDays.value > 0)) return void 0;
|
|
6462
|
-
const dir = canonical(
|
|
7080
|
+
const dir = canonical(path32.resolve(o.dir));
|
|
6463
7081
|
const root = repoRoot(dir);
|
|
6464
7082
|
if (!root) return void 0;
|
|
6465
7083
|
try {
|
|
6466
7084
|
const tree = snapshotTree(root);
|
|
6467
7085
|
const head = commitOf(root, "HEAD") ?? "";
|
|
6468
7086
|
const files = o.files.map((f) => {
|
|
6469
|
-
const abs =
|
|
6470
|
-
return [abs, crypto10.createHash("sha256").update(
|
|
7087
|
+
const abs = path32.resolve(f);
|
|
7088
|
+
return [abs, crypto10.createHash("sha256").update(fs36.readFileSync(abs)).digest("hex")];
|
|
6471
7089
|
});
|
|
6472
7090
|
const question = o.task.replace(/\s+/g, " ").trim();
|
|
6473
7091
|
const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
|
|
6474
7092
|
const where = !o.inPlace && wantSnapshot(readIn2, root, dir) ? "snapshot" : "project";
|
|
6475
|
-
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");
|
|
6476
7094
|
return { key, state: `${head}:${tree}` };
|
|
6477
7095
|
} catch {
|
|
6478
7096
|
return void 0;
|
|
@@ -6514,10 +7132,138 @@ function ago(iso, now = Date.now()) {
|
|
|
6514
7132
|
}
|
|
6515
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`;
|
|
6516
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
|
+
|
|
6517
7263
|
// src/core/review.ts
|
|
6518
7264
|
import crypto11 from "node:crypto";
|
|
6519
|
-
import
|
|
6520
|
-
import
|
|
7265
|
+
import fs38 from "node:fs";
|
|
7266
|
+
import path33 from "node:path";
|
|
6521
7267
|
var TEMPLATE2 = { task: "task-reviewer", fix: "re-review", range: "code-reviewer" };
|
|
6522
7268
|
var section = (title, body) => `## ${title}
|
|
6523
7269
|
|
|
@@ -6535,7 +7281,7 @@ function ancestors(m) {
|
|
|
6535
7281
|
function briefOf(m) {
|
|
6536
7282
|
const root = [m, ...ancestors(m)].at(-1);
|
|
6537
7283
|
const f = runFile(root.id, "brief.md");
|
|
6538
|
-
return
|
|
7284
|
+
return fs38.existsSync(f) ? fs38.readFileSync(f, "utf8") : root.task;
|
|
6539
7285
|
}
|
|
6540
7286
|
function latestReview(ids) {
|
|
6541
7287
|
for (const id of listRunIds().reverse()) {
|
|
@@ -6549,7 +7295,7 @@ function latestReview(ids) {
|
|
|
6549
7295
|
}
|
|
6550
7296
|
function diffOf(m, from = m.baseTree) {
|
|
6551
7297
|
const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
|
|
6552
|
-
if (where &&
|
|
7298
|
+
if (where && fs38.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
|
|
6553
7299
|
if (from !== m.baseTree) {
|
|
6554
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);
|
|
6555
7301
|
}
|
|
@@ -6563,7 +7309,7 @@ function runReview(id) {
|
|
|
6563
7309
|
if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
|
|
6564
7310
|
const chain = ancestors(m);
|
|
6565
7311
|
const previous = latestReview(chain.map((a) => a.id));
|
|
6566
|
-
const dir = m.mode === "isolate" && m.worktree &&
|
|
7312
|
+
const dir = m.mode === "isolate" && m.worktree && fs38.existsSync(m.worktree) ? m.cwd : m.dir;
|
|
6567
7313
|
const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
|
|
6568
7314
|
const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
|
|
6569
7315
|
if (!previous) {
|
|
@@ -6639,19 +7385,19 @@ function writePackage(job) {
|
|
|
6639
7385
|
const root = repoRoot(job.dir);
|
|
6640
7386
|
if (!g || !root) throw new UserError(`not a git repository: ${job.dir}`);
|
|
6641
7387
|
const name = `review-${crypto11.randomBytes(4).toString("hex")}.md`;
|
|
6642
|
-
const inside2 = !
|
|
6643
|
-
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);
|
|
6644
7390
|
if (!inside2) ignoreInGit(root, "/.pitroom/");
|
|
6645
|
-
|
|
6646
|
-
|
|
7391
|
+
fs38.mkdirSync(path33.dirname(file2), { recursive: true });
|
|
7392
|
+
fs38.writeFileSync(file2, job.package);
|
|
6647
7393
|
return file2;
|
|
6648
7394
|
}
|
|
6649
7395
|
function ignoreInGit(root, pattern) {
|
|
6650
|
-
const exclude =
|
|
6651
|
-
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") : "";
|
|
6652
7398
|
if (text.split(/\r?\n/).includes(pattern)) return;
|
|
6653
|
-
|
|
6654
|
-
|
|
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)
|
|
6655
7401
|
${pattern}
|
|
6656
7402
|
`);
|
|
6657
7403
|
}
|
|
@@ -6695,9 +7441,9 @@ function modelTable(opts = {}) {
|
|
|
6695
7441
|
const known = /* @__PURE__ */ new Set();
|
|
6696
7442
|
for (const b of allBackends()) {
|
|
6697
7443
|
if (opts.backend && b.id !== opts.backend) continue;
|
|
6698
|
-
const
|
|
6699
|
-
sources.push(`${b.id}: ${
|
|
6700
|
-
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) {
|
|
6701
7447
|
const key = `${b.id}:${m.id}`;
|
|
6702
7448
|
known.add(key);
|
|
6703
7449
|
const s = seen.get(key);
|
|
@@ -6807,10 +7553,10 @@ async function cmdReview(p) {
|
|
|
6807
7553
|
writeMeta(meta);
|
|
6808
7554
|
}
|
|
6809
7555
|
} catch (e) {
|
|
6810
|
-
|
|
7556
|
+
fs39.rmSync(packageFile, { force: true });
|
|
6811
7557
|
throw e;
|
|
6812
7558
|
}
|
|
6813
|
-
|
|
7559
|
+
fs39.writeFileSync(runFile(meta.id, "package.md"), job.package);
|
|
6814
7560
|
return launch(p, meta);
|
|
6815
7561
|
}
|
|
6816
7562
|
async function cmdAudit(p) {
|
|
@@ -6919,7 +7665,7 @@ function cmdPlan(p) {
|
|
|
6919
7665
|
}
|
|
6920
7666
|
function cmdCrew(p) {
|
|
6921
7667
|
const file2 = flag(p, "task-file");
|
|
6922
|
-
const tasks = (file2 ?
|
|
7668
|
+
const tasks = (file2 ? fs39.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
|
|
6923
7669
|
if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
|
|
6924
7670
|
if (has(p, "write") && tasks.length > 1) {
|
|
6925
7671
|
throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
|
|
@@ -6940,6 +7686,32 @@ function cmdCrew(p) {
|
|
|
6940
7686
|
results: pitroom wait -g ${group}`);
|
|
6941
7687
|
return 0;
|
|
6942
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
|
+
}
|
|
6943
7715
|
function selectIds(p, fallback) {
|
|
6944
7716
|
const group = flag(p, "group");
|
|
6945
7717
|
if (group) {
|
|
@@ -7120,13 +7892,13 @@ function sinceMs(s) {
|
|
|
7120
7892
|
if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
|
|
7121
7893
|
return Date.now() - Number(m[1]) * 864e5;
|
|
7122
7894
|
}
|
|
7123
|
-
var readStdin = () => process.stdin.isTTY ? "" :
|
|
7895
|
+
var readStdin = () => process.stdin.isTTY ? "" : fs39.readFileSync(0, "utf8");
|
|
7124
7896
|
function cmdStatusline(p) {
|
|
7125
7897
|
const input = readStdin();
|
|
7126
7898
|
const lines = [];
|
|
7127
7899
|
const then = flag(p, "then");
|
|
7128
7900
|
if (then) {
|
|
7129
|
-
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 });
|
|
7130
7902
|
if (r.stdout?.trimEnd()) lines.push(r.stdout.trimEnd());
|
|
7131
7903
|
}
|
|
7132
7904
|
try {
|
|
@@ -7160,8 +7932,8 @@ function cmdModels(p) {
|
|
|
7160
7932
|
return 0;
|
|
7161
7933
|
}
|
|
7162
7934
|
function cmdHookStart() {
|
|
7163
|
-
const script =
|
|
7164
|
-
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 });
|
|
7165
7937
|
return 0;
|
|
7166
7938
|
}
|
|
7167
7939
|
function cmdInit(p) {
|
|
@@ -7201,8 +7973,8 @@ function cmdSavings(p) {
|
|
|
7201
7973
|
est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
|
|
7202
7974
|
const out = flag(p, "card");
|
|
7203
7975
|
if (out) {
|
|
7204
|
-
|
|
7205
|
-
console.log(`card written to ${
|
|
7976
|
+
fs39.writeFileSync(out, card(t, period));
|
|
7977
|
+
console.log(`card written to ${path34.resolve(out)}`);
|
|
7206
7978
|
}
|
|
7207
7979
|
if (has(p, "badge")) console.log(`})`);
|
|
7208
7980
|
return 0;
|
|
@@ -7244,7 +8016,7 @@ function cmdConfig(p) {
|
|
|
7244
8016
|
console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
|
|
7245
8017
|
return 0;
|
|
7246
8018
|
}
|
|
7247
|
-
console.log(`config file: ${configPath()}${
|
|
8019
|
+
console.log(`config file: ${configPath()}${fs39.existsSync(configPath()) ? "" : " (not present)"}`);
|
|
7248
8020
|
for (const [key, s] of Object.entries(eff)) {
|
|
7249
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);
|
|
7250
8022
|
console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
|
|
@@ -7268,7 +8040,7 @@ function cmdClean(p) {
|
|
|
7268
8040
|
const m = readMeta(id);
|
|
7269
8041
|
if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
|
|
7270
8042
|
recordRun(m);
|
|
7271
|
-
|
|
8043
|
+
fs39.rmSync(runDir(id), { recursive: true, force: true });
|
|
7272
8044
|
}
|
|
7273
8045
|
const snapshots = prune(Date.now(), 0);
|
|
7274
8046
|
console.log(`removed ${old.length} run(s)${snapshots ? ` and ${snapshots} read snapshot${snapshots === 1 ? "" : "s"}` : ""}; the history and the savings ledger are kept`);
|
|
@@ -7278,10 +8050,10 @@ var cmdRevert = (p) => (console.log(revertRun(freshMeta(resolveRun(p.positional[
|
|
|
7278
8050
|
var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positional[0])))), 0);
|
|
7279
8051
|
|
|
7280
8052
|
// src/core/doctor.ts
|
|
7281
|
-
import { spawnSync as
|
|
7282
|
-
import
|
|
7283
|
-
import
|
|
7284
|
-
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";
|
|
7285
8057
|
var MARK2 = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
|
|
7286
8058
|
var NEXT = [
|
|
7287
8059
|
{ when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
|
|
@@ -7304,14 +8076,14 @@ function usageLine() {
|
|
|
7304
8076
|
return `${ids.length} run${ids.length === 1 ? "" : "s"} so far${when2} (pitroom savings shows what they saved)`;
|
|
7305
8077
|
}
|
|
7306
8078
|
function firstNodeOnPath() {
|
|
7307
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
7308
|
-
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");
|
|
7309
8081
|
try {
|
|
7310
|
-
if (!
|
|
8082
|
+
if (!fs40.statSync(file2).isFile()) continue;
|
|
7311
8083
|
} catch {
|
|
7312
8084
|
continue;
|
|
7313
8085
|
}
|
|
7314
|
-
const r =
|
|
8086
|
+
const r = spawnSync12(file2, ["-p", "process.versions.node"], { encoding: "utf8", timeout: 5e3 });
|
|
7315
8087
|
return r.status === 0 ? { path: file2, version: r.stdout.trim() } : { path: file2, version: "" };
|
|
7316
8088
|
}
|
|
7317
8089
|
return void 0;
|
|
@@ -7326,7 +8098,7 @@ var READ_ONLY_HOW = {
|
|
|
7326
8098
|
"tool-allowlist": "a tool allowlist",
|
|
7327
8099
|
"approval-mode": "the CLI's read-only approval mode"
|
|
7328
8100
|
};
|
|
7329
|
-
function
|
|
8101
|
+
function doctor7(probe) {
|
|
7330
8102
|
const checks = [];
|
|
7331
8103
|
let current = "Setup";
|
|
7332
8104
|
const section2 = (name) => void (current = name);
|
|
@@ -7334,12 +8106,12 @@ function doctor5(probe) {
|
|
|
7334
8106
|
const addAll = (list2) => list2.forEach((c) => add(c.level, c.message));
|
|
7335
8107
|
add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
|
|
7336
8108
|
const onPath = firstNodeOnPath();
|
|
7337
|
-
if (onPath && onPath.version && tooOld(onPath.version) &&
|
|
8109
|
+
if (onPath && onPath.version && tooOld(onPath.version) && path35.resolve(onPath.path) !== path35.resolve(process.execPath)) {
|
|
7338
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)`);
|
|
7339
8111
|
}
|
|
7340
8112
|
add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
|
|
7341
8113
|
const cfg = loadConfig();
|
|
7342
|
-
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)"}`);
|
|
7343
8115
|
for (const w of cfg.warnings) add("warn", w);
|
|
7344
8116
|
if (shimReady()) add("ok", "git guard shim ready");
|
|
7345
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");
|
|
@@ -7482,7 +8254,7 @@ function liveProbe(target) {
|
|
|
7482
8254
|
title: "pitroom doctor probe"
|
|
7483
8255
|
});
|
|
7484
8256
|
const t0 = Date.now();
|
|
7485
|
-
const r =
|
|
8257
|
+
const r = spawnSync12(inv.command, inv.args, {
|
|
7486
8258
|
encoding: "utf8",
|
|
7487
8259
|
timeout: 3e5,
|
|
7488
8260
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -7512,20 +8284,20 @@ function skillChecks() {
|
|
|
7512
8284
|
else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
|
|
7513
8285
|
}
|
|
7514
8286
|
if (geminiExtensionInstalled()) {
|
|
7515
|
-
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);
|
|
7516
8288
|
checks.push(
|
|
7517
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)" }
|
|
7518
8290
|
);
|
|
7519
8291
|
}
|
|
7520
8292
|
if (viaPlugin) {
|
|
7521
|
-
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);
|
|
7522
8294
|
checks.push(
|
|
7523
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)" }
|
|
7524
8296
|
);
|
|
7525
8297
|
}
|
|
7526
8298
|
const launcher = launcherPath();
|
|
7527
|
-
if (
|
|
7528
|
-
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" });
|
|
7529
8301
|
checks.push(
|
|
7530
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)}` }
|
|
7531
8303
|
);
|
|
@@ -7535,13 +8307,13 @@ function skillChecks() {
|
|
|
7535
8307
|
return checks;
|
|
7536
8308
|
}
|
|
7537
8309
|
function geminiExtensionInstalled() {
|
|
7538
|
-
const home3 = process.env.GEMINI_CLI_HOME ??
|
|
7539
|
-
return
|
|
8310
|
+
const home3 = process.env.GEMINI_CLI_HOME ?? os12.homedir();
|
|
8311
|
+
return fs40.existsSync(path35.join(home3, ".gemini", "extensions", "pitroom"));
|
|
7540
8312
|
}
|
|
7541
8313
|
function pluginInstalled() {
|
|
7542
8314
|
try {
|
|
7543
|
-
const f =
|
|
7544
|
-
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 ?? {};
|
|
7545
8317
|
return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
|
|
7546
8318
|
} catch {
|
|
7547
8319
|
return false;
|
|
@@ -7558,24 +8330,24 @@ function superpowersActive() {
|
|
|
7558
8330
|
for (const p of openCodePlugins()) {
|
|
7559
8331
|
if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
|
|
7560
8332
|
}
|
|
7561
|
-
for (const base2 of [
|
|
7562
|
-
const dir =
|
|
7563
|
-
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);
|
|
7564
8336
|
}
|
|
7565
8337
|
return found;
|
|
7566
8338
|
}
|
|
7567
8339
|
function claudePlugins() {
|
|
7568
8340
|
try {
|
|
7569
|
-
const f =
|
|
7570
|
-
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 ?? {});
|
|
7571
8343
|
} catch {
|
|
7572
8344
|
return [];
|
|
7573
8345
|
}
|
|
7574
8346
|
}
|
|
7575
8347
|
function claudePluginEnabled(key) {
|
|
7576
8348
|
try {
|
|
7577
|
-
const f =
|
|
7578
|
-
return JSON.parse(
|
|
8349
|
+
const f = path35.join(os12.homedir(), ".claude", "settings.json");
|
|
8350
|
+
return JSON.parse(fs40.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
|
|
7579
8351
|
} catch {
|
|
7580
8352
|
return true;
|
|
7581
8353
|
}
|
|
@@ -7584,7 +8356,7 @@ function codexPlugins() {
|
|
|
7584
8356
|
const plugins = /* @__PURE__ */ new Map();
|
|
7585
8357
|
let text;
|
|
7586
8358
|
try {
|
|
7587
|
-
text =
|
|
8359
|
+
text = fs40.readFileSync(path35.join(process.env.CODEX_HOME ?? path35.join(os12.homedir(), ".codex"), "config.toml"), "utf8");
|
|
7588
8360
|
} catch {
|
|
7589
8361
|
return plugins;
|
|
7590
8362
|
}
|
|
@@ -7602,11 +8374,11 @@ function codexPlugins() {
|
|
|
7602
8374
|
return plugins;
|
|
7603
8375
|
}
|
|
7604
8376
|
function openCodePlugins() {
|
|
7605
|
-
const dir =
|
|
8377
|
+
const dir = path35.join(process.env.XDG_CONFIG_HOME ?? path35.join(os12.homedir(), ".config"), "opencode");
|
|
7606
8378
|
const plugins = [];
|
|
7607
8379
|
for (const name of ["opencode.json", "opencode.jsonc"]) {
|
|
7608
8380
|
try {
|
|
7609
|
-
const text =
|
|
8381
|
+
const text = fs40.readFileSync(path35.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
|
|
7610
8382
|
const list2 = JSON.parse(text).plugin;
|
|
7611
8383
|
if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
|
|
7612
8384
|
} catch {
|
|
@@ -7614,7 +8386,7 @@ function openCodePlugins() {
|
|
|
7614
8386
|
}
|
|
7615
8387
|
for (const folder of ["plugin", "plugins"]) {
|
|
7616
8388
|
try {
|
|
7617
|
-
for (const f of
|
|
8389
|
+
for (const f of fs40.readdirSync(path35.join(dir, folder))) plugins.push(path35.join(dir, folder, f));
|
|
7618
8390
|
} catch {
|
|
7619
8391
|
}
|
|
7620
8392
|
}
|
|
@@ -7636,6 +8408,9 @@ Usage
|
|
|
7636
8408
|
fix round) or of a commit range; by default on another worker
|
|
7637
8409
|
pitroom audit RUN [-W worker] another worker re-checks a read run's answer (AGREE / PARTIAL / DISAGREE);
|
|
7638
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)
|
|
7639
8414
|
pitroom plan status PLAN [--json] a plan's progress: runs, STATUS, review, fix rounds, applied
|
|
7640
8415
|
pitroom plan note PLAN "Task N: \u2026" record a completion, deferred finding or ruling (outside the repo)
|
|
7641
8416
|
pitroom status [run | -g NAME] state / live progress (default: latest run)
|
|
@@ -7719,14 +8494,17 @@ Workers: ${backendIds().join(", ")} (targets: "opencode", "opencode:provider/mod
|
|
|
7719
8494
|
Env: PITROOM_WORKER, PITROOM_MODEL, PITROOM_FALLBACK="t1,t2", PITROOM_TIMEOUT, PITROOM_MAX_PARALLEL,
|
|
7720
8495
|
PITROOM_PRIMARY=sonnet|opus|haiku|gpt-5, PITROOM_PRICE="in,out", PITROOM_HOME, PITROOM_CONFIG, PITROOM_CACHE_DAYS,
|
|
7721
8496
|
PITROOM_NOTIFY, PITROOM_NOTIFY_COMMAND, PITROOM_NOTIFY_AFTER, PITROOM_MCP_DASH, PITROOM_PRICE_FEED, PITROOM_PRICE_FEED_URL,
|
|
7722
|
-
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
|
|
7723
8499
|
Config: ~/.config/pitroom/config.json (worker, fallback, models, tiers, timeout, primary, price, link, web, maxParallel, audit, readIn, cacheDays,
|
|
7724
|
-
countRateLimits, mcpDash, notify, notifyCommand, notifyAfter, workerPrices, priceFeed, priceFeedUrl, priceFeedHours
|
|
8500
|
+
countRateLimits, mcpDash, notify, notifyCommand, notifyAfter, workerPrices, priceFeed, priceFeedUrl, priceFeedHours,
|
|
8501
|
+
auditFix, auditFocus, budgetDaily, rankWorkers)`;
|
|
7725
8502
|
var COMMANDS = {
|
|
7726
8503
|
run: cmdRun,
|
|
7727
8504
|
crew: cmdCrew,
|
|
7728
8505
|
review: cmdReview,
|
|
7729
8506
|
audit: cmdAudit,
|
|
8507
|
+
eval: cmdEval,
|
|
7730
8508
|
cooldown: cmdCooldown,
|
|
7731
8509
|
prices: cmdPrices,
|
|
7732
8510
|
mcp: (p) => {
|
|
@@ -7735,7 +8513,7 @@ var COMMANDS = {
|
|
|
7735
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)");
|
|
7736
8514
|
const dir = flag(p, "dir");
|
|
7737
8515
|
if (dir !== void 0) {
|
|
7738
|
-
if (!
|
|
8516
|
+
if (!fs41.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) throw new UserError(`-d ${dir}: no such directory`);
|
|
7739
8517
|
process.chdir(dir);
|
|
7740
8518
|
process.env.PWD = process.cwd();
|
|
7741
8519
|
}
|
|
@@ -7759,7 +8537,7 @@ var COMMANDS = {
|
|
|
7759
8537
|
statusline: cmdStatusline,
|
|
7760
8538
|
"hook-card": cmdHookCard,
|
|
7761
8539
|
"hook-start": cmdHookStart,
|
|
7762
|
-
doctor: (p) =>
|
|
8540
|
+
doctor: (p) => doctor7(has(p, "probe")),
|
|
7763
8541
|
config: cmdConfig,
|
|
7764
8542
|
init: cmdInit,
|
|
7765
8543
|
install: cmdInstall,
|