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/lib.mjs
CHANGED
|
@@ -209,7 +209,10 @@ var DENY_BASH = [
|
|
|
209
209
|
"opencode",
|
|
210
210
|
"claude",
|
|
211
211
|
"codex",
|
|
212
|
-
"gemini"
|
|
212
|
+
"gemini",
|
|
213
|
+
"qwen",
|
|
214
|
+
"kilo",
|
|
215
|
+
"kilocode"
|
|
213
216
|
].map((c) => `Bash(${c}:*)`);
|
|
214
217
|
var SECRETS = ["**/.env", "**/.env.*", "**/*.pem", "**/id_rsa*", "**/id_ed25519*"].map((p) => `Read(${p})`);
|
|
215
218
|
function toolsFor(mode, web2) {
|
|
@@ -827,6 +830,7 @@ function invocation3(req) {
|
|
|
827
830
|
"none"
|
|
828
831
|
];
|
|
829
832
|
for (const p of policies) args.push("--policy", path5.join(dir, p));
|
|
833
|
+
if (req.sessionId) args.push("--resume", req.sessionId);
|
|
830
834
|
if (process.env.PITROOM_GEMINI_TRUST === "1") args.push("--skip-trust");
|
|
831
835
|
const [model] = (req.model ?? "").split("#");
|
|
832
836
|
if (model) args.push("--model", model);
|
|
@@ -913,7 +917,7 @@ function catalog3() {
|
|
|
913
917
|
var gemini = {
|
|
914
918
|
id: "gemini",
|
|
915
919
|
name: "Gemini CLI",
|
|
916
|
-
capabilities: { readOnly: "approval-mode", resume: "
|
|
920
|
+
capabilities: { readOnly: "approval-mode", resume: "by-id", reportsCost: false, attachFiles: false },
|
|
917
921
|
binary: binary3,
|
|
918
922
|
catalog: catalog3,
|
|
919
923
|
invocation: invocation3,
|
|
@@ -923,6 +927,10 @@ var gemini = {
|
|
|
923
927
|
doctor: doctor3
|
|
924
928
|
};
|
|
925
929
|
|
|
930
|
+
// src/backends/kilo/index.ts
|
|
931
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
932
|
+
import path7 from "node:path";
|
|
933
|
+
|
|
926
934
|
// src/backends/opencode/index.ts
|
|
927
935
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
928
936
|
|
|
@@ -981,9 +989,9 @@ function parseEvents4(ndjson) {
|
|
|
981
989
|
}
|
|
982
990
|
lastActivity = `${name} ${describe2(st.input)}`.trim();
|
|
983
991
|
if (timeline.length < MAX_STEPS) {
|
|
984
|
-
const
|
|
985
|
-
const text =
|
|
986
|
-
timeline.push({ kind, name, text: clip4(text, 240), ok: st.status === "completed" ? true : st.status === "error" ? false : void 0, at: stamp(e.timestamp) });
|
|
992
|
+
const kind2 = EDIT_TOOLS3.has(name) ? "edit" : /^(bash|shell)$/.test(name) ? "shell" : "tool";
|
|
993
|
+
const text = kind2 === "shell" ? String(st.input?.command ?? "") : String(st.input?.filePath ?? st.input?.path ?? st.input?.pattern ?? st.input?.url ?? "") || describe2(st.input);
|
|
994
|
+
timeline.push({ kind: kind2, name, text: clip4(text, 240), ok: st.status === "completed" ? true : st.status === "error" ? false : void 0, at: stamp(e.timestamp) });
|
|
987
995
|
}
|
|
988
996
|
break;
|
|
989
997
|
}
|
|
@@ -1132,17 +1140,19 @@ var FORBIDDEN_BASH = [
|
|
|
1132
1140
|
"twine upload*",
|
|
1133
1141
|
// no recursive delegation
|
|
1134
1142
|
"opencode*",
|
|
1143
|
+
"kilo *",
|
|
1144
|
+
"kilocode*",
|
|
1135
1145
|
"pitroom *",
|
|
1136
1146
|
"*pitroom*"
|
|
1137
1147
|
];
|
|
1138
|
-
function scratchDirs() {
|
|
1148
|
+
function scratchDirs(app) {
|
|
1139
1149
|
const data = process.env.XDG_DATA_HOME ?? path6.join(os6.homedir(), ".local", "share");
|
|
1140
1150
|
const rules = {
|
|
1141
1151
|
"*": "deny",
|
|
1142
|
-
[path6.join(data,
|
|
1143
|
-
[path6.join(data,
|
|
1152
|
+
[path6.join(data, app, "tool-output", "*")]: "allow",
|
|
1153
|
+
[path6.join(data, app, "shell", "*", "*")]: "allow"
|
|
1144
1154
|
};
|
|
1145
|
-
for (const tmp of /* @__PURE__ */ new Set([os6.tmpdir(), realpath(os6.tmpdir())])) rules[path6.join(tmp,
|
|
1155
|
+
for (const tmp of /* @__PURE__ */ new Set([os6.tmpdir(), realpath(os6.tmpdir())])) rules[path6.join(tmp, app, "*")] = "allow";
|
|
1146
1156
|
return rules;
|
|
1147
1157
|
}
|
|
1148
1158
|
function realpath(p) {
|
|
@@ -1163,7 +1173,7 @@ var COMMON = {
|
|
|
1163
1173
|
doom_loop: "deny"
|
|
1164
1174
|
};
|
|
1165
1175
|
var web = (on) => ({ webfetch: on ? "allow" : "deny", websearch: on ? "allow" : "deny" });
|
|
1166
|
-
function readProfile(allowWeb = false) {
|
|
1176
|
+
function readProfile(allowWeb = false, app = "opencode") {
|
|
1167
1177
|
return {
|
|
1168
1178
|
"*": "deny",
|
|
1169
1179
|
read: READ_FILES,
|
|
@@ -1179,11 +1189,11 @@ function readProfile(allowWeb = false) {
|
|
|
1179
1189
|
bash: READ_ONLY_BASH,
|
|
1180
1190
|
shell: READ_ONLY_BASH,
|
|
1181
1191
|
edit: "deny",
|
|
1182
|
-
external_directory: scratchDirs(),
|
|
1192
|
+
external_directory: scratchDirs(app),
|
|
1183
1193
|
...COMMON
|
|
1184
1194
|
};
|
|
1185
1195
|
}
|
|
1186
|
-
function writeProfile(allowWeb = false) {
|
|
1196
|
+
function writeProfile(allowWeb = false, app = "opencode") {
|
|
1187
1197
|
const bash = { "*": "allow" };
|
|
1188
1198
|
for (const pattern of FORBIDDEN_BASH) bash[pattern] = "deny";
|
|
1189
1199
|
return {
|
|
@@ -1192,25 +1202,26 @@ function writeProfile(allowWeb = false) {
|
|
|
1192
1202
|
...web(allowWeb),
|
|
1193
1203
|
bash,
|
|
1194
1204
|
shell: bash,
|
|
1195
|
-
external_directory: scratchDirs(),
|
|
1205
|
+
external_directory: scratchDirs(app),
|
|
1196
1206
|
...COMMON
|
|
1197
1207
|
};
|
|
1198
1208
|
}
|
|
1199
1209
|
function agentFor(mode) {
|
|
1200
1210
|
return mode === "read" ? AGENT.read : AGENT.write;
|
|
1201
1211
|
}
|
|
1202
|
-
function configContent(existing, allowWeb = false) {
|
|
1212
|
+
function configContent(existing, allowWeb = false, app = "opencode", extra = {}) {
|
|
1203
1213
|
const ours = {
|
|
1214
|
+
...extra,
|
|
1204
1215
|
agent: {
|
|
1205
1216
|
[AGENT.read]: {
|
|
1206
1217
|
mode: "primary",
|
|
1207
1218
|
description: "pitroom: read-only worker (research, search, review)",
|
|
1208
|
-
permission: readProfile(allowWeb)
|
|
1219
|
+
permission: readProfile(allowWeb, app)
|
|
1209
1220
|
},
|
|
1210
1221
|
[AGENT.write]: {
|
|
1211
1222
|
mode: "primary",
|
|
1212
1223
|
description: "pitroom: editing worker (no git history changes, no bulk deletes)",
|
|
1213
|
-
permission: writeProfile(allowWeb)
|
|
1224
|
+
permission: writeProfile(allowWeb, app)
|
|
1214
1225
|
}
|
|
1215
1226
|
}
|
|
1216
1227
|
};
|
|
@@ -1387,10 +1398,348 @@ var opencode = {
|
|
|
1387
1398
|
doctor: doctor4
|
|
1388
1399
|
};
|
|
1389
1400
|
|
|
1401
|
+
// src/backends/kilo/index.ts
|
|
1402
|
+
var binary5 = () => findBinary("kilo", "PITROOM_KILO_BIN", ["~/.kilo/bin/kilo"]);
|
|
1403
|
+
function kilo(args, timeout = 6e4) {
|
|
1404
|
+
const { command, prefix } = resolveCommand(binary5());
|
|
1405
|
+
const r = spawnSync5(command, [...prefix, ...args], { encoding: "utf8", timeout, stdio: ["ignore", "pipe", "pipe"], maxBuffer: 64 * 1024 * 1024 });
|
|
1406
|
+
return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
|
|
1407
|
+
}
|
|
1408
|
+
function invocation5(req) {
|
|
1409
|
+
const args = [
|
|
1410
|
+
"run",
|
|
1411
|
+
"--agent",
|
|
1412
|
+
agentFor(req.mode),
|
|
1413
|
+
"--format",
|
|
1414
|
+
"json",
|
|
1415
|
+
"--title",
|
|
1416
|
+
req.title,
|
|
1417
|
+
// Some failure causes only appear in the server logs (Kilo takes the level in capitals only).
|
|
1418
|
+
"--print-logs",
|
|
1419
|
+
"--log-level",
|
|
1420
|
+
"ERROR"
|
|
1421
|
+
];
|
|
1422
|
+
if (req.model) args.push("--model", req.model);
|
|
1423
|
+
if (req.sessionId) args.push("--session", req.sessionId);
|
|
1424
|
+
for (const f of req.files) args.push("--file", f);
|
|
1425
|
+
args.push(req.prompt);
|
|
1426
|
+
const { command, prefix } = resolveCommand(binary5());
|
|
1427
|
+
return {
|
|
1428
|
+
command,
|
|
1429
|
+
args: [...prefix, ...args],
|
|
1430
|
+
env: {
|
|
1431
|
+
// A worker run must not upgrade Kilo underneath the user.
|
|
1432
|
+
KILO_CONFIG_CONTENT: configContent(process.env.KILO_CONFIG_CONTENT, req.web, "kilo", { autoupdate: false }),
|
|
1433
|
+
// No daemon to attach to: a private server per run (see above).
|
|
1434
|
+
XDG_STATE_HOME: path7.join(home(), "kilo-state")
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
function failure5(run, stderr, exitCode) {
|
|
1439
|
+
if (exitCode === 0 && !run.error) return void 0;
|
|
1440
|
+
const detail = stderr.match(/error="([^"]+)"/g)?.pop()?.slice(7, -1);
|
|
1441
|
+
const raw = run.error ?? `kilo exited with code ${exitCode}`;
|
|
1442
|
+
const message = detail && !raw.includes(detail) ? `${raw}: ${detail}` : raw;
|
|
1443
|
+
return { kind: classify4(message), message };
|
|
1444
|
+
}
|
|
1445
|
+
function listModels2() {
|
|
1446
|
+
return kilo(["models"]).out.split("\n").map((s) => s.trim()).filter((s) => /^[\w.-]+\/\S+$/.test(s));
|
|
1447
|
+
}
|
|
1448
|
+
function defaultModel4() {
|
|
1449
|
+
try {
|
|
1450
|
+
const out = kilo(["debug", "config"], 3e4).out;
|
|
1451
|
+
const model = JSON.parse(out.slice(out.indexOf("{"))).model;
|
|
1452
|
+
return typeof model === "string" ? model : void 0;
|
|
1453
|
+
} catch {
|
|
1454
|
+
return void 0;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
function catalog5() {
|
|
1458
|
+
return { models: listModels2().map((id) => ({ id })), source: "`kilo models` (reasoning variants are provider-specific: provider/model#variant)" };
|
|
1459
|
+
}
|
|
1460
|
+
function doctor5({ models, hasFallback }) {
|
|
1461
|
+
const version = kilo(["--version"]);
|
|
1462
|
+
if (version.missing || !version.ok) {
|
|
1463
|
+
return [{ level: "fail", message: `kilo not runnable (${binary5()}). Install Kilo Code's CLI (npm i -g @kilocode/cli), or set PITROOM_KILO_BIN` }];
|
|
1464
|
+
}
|
|
1465
|
+
const checks = [{ level: "ok", message: `Kilo Code ${version.out.trim().split("\n").pop()} at ${binary5()} (beta worker: checked against 7.8)` }];
|
|
1466
|
+
const def = defaultModel4();
|
|
1467
|
+
for (const m of models) {
|
|
1468
|
+
const model = m ?? def;
|
|
1469
|
+
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' });
|
|
1470
|
+
}
|
|
1471
|
+
if (!hasFallback) checks.push({ level: "warn", message: "no fallback workers configured for Kilo Code runs" });
|
|
1472
|
+
checks.push({ level: "ok", message: `permission profiles ${AGENT.read}/${AGENT.write} injected per run through KILO_CONFIG_CONTENT` });
|
|
1473
|
+
return checks;
|
|
1474
|
+
}
|
|
1475
|
+
var kiloBackend = {
|
|
1476
|
+
id: "kilo",
|
|
1477
|
+
name: "Kilo Code",
|
|
1478
|
+
capabilities: { readOnly: "permission-rules", resume: "by-id", reportsCost: true, attachFiles: true },
|
|
1479
|
+
binary: binary5,
|
|
1480
|
+
invocation: invocation5,
|
|
1481
|
+
parse: parseEvents4,
|
|
1482
|
+
failure: failure5,
|
|
1483
|
+
defaultModel: defaultModel4,
|
|
1484
|
+
listModels: listModels2,
|
|
1485
|
+
catalog: catalog5,
|
|
1486
|
+
doctor: doctor5
|
|
1487
|
+
};
|
|
1488
|
+
|
|
1489
|
+
// src/backends/qwen/index.ts
|
|
1490
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
1491
|
+
import fs8 from "node:fs";
|
|
1492
|
+
import os7 from "node:os";
|
|
1493
|
+
import path8 from "node:path";
|
|
1494
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1495
|
+
|
|
1496
|
+
// src/backends/qwen/events.ts
|
|
1497
|
+
var EDIT_TOOLS4 = /* @__PURE__ */ new Set(["write_file", "edit", "notebook_edit"]);
|
|
1498
|
+
var SHELL = "run_shell_command";
|
|
1499
|
+
var DENIED4 = /permission was declined|denied by permission rules|requires permission|not allowed/i;
|
|
1500
|
+
var API_ERROR = /^\[API Error: /;
|
|
1501
|
+
function parseEvents5(jsonl) {
|
|
1502
|
+
const usage = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0, steps: 0, toolCalls: 0, denied: 0 };
|
|
1503
|
+
const tools = {};
|
|
1504
|
+
const edits = [];
|
|
1505
|
+
const pendingEdits = /* @__PURE__ */ new Map();
|
|
1506
|
+
const timeline = [];
|
|
1507
|
+
const stepOf = /* @__PURE__ */ new Map();
|
|
1508
|
+
let sessionId;
|
|
1509
|
+
let model;
|
|
1510
|
+
let lastText = "";
|
|
1511
|
+
let finalText;
|
|
1512
|
+
let lastActivity;
|
|
1513
|
+
let error;
|
|
1514
|
+
for (const line of jsonl.split("\n")) {
|
|
1515
|
+
if (!line.trim()) continue;
|
|
1516
|
+
let e;
|
|
1517
|
+
try {
|
|
1518
|
+
e = JSON.parse(line);
|
|
1519
|
+
} catch {
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
sessionId ??= e.session_id;
|
|
1523
|
+
if (e.type === "system" && e.subtype === "init") {
|
|
1524
|
+
model = e.model;
|
|
1525
|
+
} else if (e.type === "system" && e.subtype === "retry") {
|
|
1526
|
+
lastActivity = `waiting to retry (attempt ${e.data?.attempt ?? "?"} of ${e.data?.maxRetries ?? "?"})`;
|
|
1527
|
+
} else if (e.type === "assistant") {
|
|
1528
|
+
const content = e.message?.content ?? [];
|
|
1529
|
+
const text = content.filter((c) => c.type === "text").map((c) => String(c.text ?? "")).join(" ").trim();
|
|
1530
|
+
if (API_ERROR.test(text)) {
|
|
1531
|
+
error = apiError(text);
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
usage.steps++;
|
|
1535
|
+
for (const c of content) {
|
|
1536
|
+
if (c.type === "text" && String(c.text ?? "").trim()) {
|
|
1537
|
+
lastText = String(c.text).trim();
|
|
1538
|
+
lastActivity = `says: ${oneLine5(lastText)}`;
|
|
1539
|
+
if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip5(lastText, 600) });
|
|
1540
|
+
} else if (c.type === "tool_use") {
|
|
1541
|
+
usage.toolCalls++;
|
|
1542
|
+
tools[c.name] = (tools[c.name] ?? 0) + 1;
|
|
1543
|
+
const target = c.input?.file_path ?? c.input?.path ?? c.input?.pattern ?? c.input?.command ?? c.input?.url;
|
|
1544
|
+
lastActivity = `${c.name} ${target ? oneLine5(String(target), 60) : ""}`.trim();
|
|
1545
|
+
if (EDIT_TOOLS4.has(c.name) && c.input?.file_path) pendingEdits.set(String(c.id), String(c.input.file_path));
|
|
1546
|
+
if (timeline.length < MAX_STEPS) {
|
|
1547
|
+
const step = { kind: EDIT_TOOLS4.has(c.name) ? "edit" : c.name === SHELL ? "shell" : "tool", name: c.name, text: clip5(String(target ?? ""), 240) };
|
|
1548
|
+
timeline.push(step);
|
|
1549
|
+
stepOf.set(String(c.id), step);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
} else if (e.type === "user") {
|
|
1554
|
+
for (const c of e.message?.content ?? []) {
|
|
1555
|
+
if (c?.type !== "tool_result") continue;
|
|
1556
|
+
const text = typeof c.content === "string" ? c.content : JSON.stringify(c.content ?? "");
|
|
1557
|
+
const step = stepOf.get(String(c.tool_use_id));
|
|
1558
|
+
if (step) step.ok = !c.is_error;
|
|
1559
|
+
if (c.is_error) {
|
|
1560
|
+
if (DENIED4.test(text)) usage.denied++;
|
|
1561
|
+
} else if (pendingEdits.has(String(c.tool_use_id))) {
|
|
1562
|
+
edits.push(pendingEdits.get(String(c.tool_use_id)));
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
} else if (e.type === "result") {
|
|
1566
|
+
const u = e.usage ?? {};
|
|
1567
|
+
usage.input += Math.max(0, num5(u.input_tokens) - num5(u.cache_read_input_tokens));
|
|
1568
|
+
usage.cacheRead += num5(u.cache_read_input_tokens);
|
|
1569
|
+
usage.output += num5(u.output_tokens);
|
|
1570
|
+
usage.total += num5(u.total_tokens) || num5(u.input_tokens) + num5(u.output_tokens);
|
|
1571
|
+
usage.denied = Math.max(usage.denied, Array.isArray(e.permission_denials) ? e.permission_denials.length : 0);
|
|
1572
|
+
if (e.is_error) {
|
|
1573
|
+
error ??= apiError(String(e.error?.message ?? e.result ?? e.subtype ?? "Qwen Code error"));
|
|
1574
|
+
} else if (typeof e.result === "string") {
|
|
1575
|
+
finalText = e.result.trim();
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return { sessionId, model, finalText: finalText ?? (error ? "" : lastText), usage, tools, edits, lastActivity, timeline, error };
|
|
1580
|
+
}
|
|
1581
|
+
var apiError = (text) => text.replace(API_ERROR, "").replace(/\](?=\s*(\n|$))/, "").replace(/\s+/g, " ").trim() || text;
|
|
1582
|
+
var clip5 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
1583
|
+
var num5 = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
1584
|
+
var oneLine5 = (s, max = 80) => {
|
|
1585
|
+
const t = s.replace(/\s+/g, " ").trim();
|
|
1586
|
+
return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
// src/backends/qwen/index.ts
|
|
1590
|
+
var binary6 = () => findBinary("qwen", "PITROOM_QWEN_BIN", ["~/.local/bin/qwen", "~/.npm-global/bin/qwen"]);
|
|
1591
|
+
var policyDir2 = () => path8.resolve(path8.dirname(fileURLToPath2(import.meta.url)), "..", "policies", "qwen");
|
|
1592
|
+
var EXCLUDED = [
|
|
1593
|
+
"agent",
|
|
1594
|
+
"send_message",
|
|
1595
|
+
"skill",
|
|
1596
|
+
"manage_memory",
|
|
1597
|
+
"search_memory",
|
|
1598
|
+
"enter_worktree",
|
|
1599
|
+
"exit_worktree",
|
|
1600
|
+
"record_artifact",
|
|
1601
|
+
"cron_create",
|
|
1602
|
+
"cron_list",
|
|
1603
|
+
"cron_delete",
|
|
1604
|
+
"loop_wakeup",
|
|
1605
|
+
"task_stop",
|
|
1606
|
+
"update_goal",
|
|
1607
|
+
"get_goal",
|
|
1608
|
+
"list_agents",
|
|
1609
|
+
"report_findings",
|
|
1610
|
+
"tool_call",
|
|
1611
|
+
"tool_search",
|
|
1612
|
+
"read_mcp_resource"
|
|
1613
|
+
];
|
|
1614
|
+
var WEB = ["web_fetch", "web_search"];
|
|
1615
|
+
var EDITS_HERE = ["Edit(./**)", "write_file(./**)", "notebook_edit(./**)"];
|
|
1616
|
+
var DENY_SHELL = [
|
|
1617
|
+
"git commit",
|
|
1618
|
+
"git push",
|
|
1619
|
+
"git pull",
|
|
1620
|
+
"git reset",
|
|
1621
|
+
"git checkout",
|
|
1622
|
+
"git restore",
|
|
1623
|
+
"git clean",
|
|
1624
|
+
"git stash",
|
|
1625
|
+
"git rebase",
|
|
1626
|
+
"git merge",
|
|
1627
|
+
"git switch",
|
|
1628
|
+
"git branch",
|
|
1629
|
+
"git tag",
|
|
1630
|
+
"git rm",
|
|
1631
|
+
"git worktree",
|
|
1632
|
+
"git update-ref",
|
|
1633
|
+
"git config",
|
|
1634
|
+
"git remote",
|
|
1635
|
+
"git cherry-pick",
|
|
1636
|
+
"git revert",
|
|
1637
|
+
"git am",
|
|
1638
|
+
"git add",
|
|
1639
|
+
"rm -rf",
|
|
1640
|
+
"rm -fr",
|
|
1641
|
+
"sudo",
|
|
1642
|
+
"chown",
|
|
1643
|
+
"kill",
|
|
1644
|
+
"pkill",
|
|
1645
|
+
"killall",
|
|
1646
|
+
"gh",
|
|
1647
|
+
"npm publish",
|
|
1648
|
+
"pitroom",
|
|
1649
|
+
"opencode",
|
|
1650
|
+
"claude",
|
|
1651
|
+
"codex",
|
|
1652
|
+
"gemini",
|
|
1653
|
+
"qwen",
|
|
1654
|
+
"kilo",
|
|
1655
|
+
"kilocode"
|
|
1656
|
+
].map((c) => `run_shell_command(${c})`);
|
|
1657
|
+
function invocation6(req) {
|
|
1658
|
+
const read = req.mode === "read";
|
|
1659
|
+
const exclude = [...EXCLUDED, ...req.web ? [] : WEB, ...read ? [] : DENY_SHELL];
|
|
1660
|
+
const args = [
|
|
1661
|
+
// The prompt is the first word: the list options below take every word that follows them, and Qwen Code reads
|
|
1662
|
+
// no prompt after "--". One that starts with "-" would be read as an option: a leading space keeps it the prompt.
|
|
1663
|
+
req.prompt.startsWith("-") ? ` ${req.prompt}` : req.prompt,
|
|
1664
|
+
"--safe-mode",
|
|
1665
|
+
"--output-format",
|
|
1666
|
+
"stream-json",
|
|
1667
|
+
"--approval-mode",
|
|
1668
|
+
read ? "plan" : "default",
|
|
1669
|
+
"--exclude-tools",
|
|
1670
|
+
...exclude
|
|
1671
|
+
];
|
|
1672
|
+
if (!read) args.push("--allowed-tools", ...EDITS_HERE, "run_shell_command");
|
|
1673
|
+
const model = (req.model ?? "").split("#")[0];
|
|
1674
|
+
if (model) args.push("--model", model);
|
|
1675
|
+
if (req.sessionId) args.push("--resume", req.sessionId);
|
|
1676
|
+
const { command, prefix } = resolveCommand(binary6());
|
|
1677
|
+
return {
|
|
1678
|
+
command,
|
|
1679
|
+
args: [...prefix, ...args],
|
|
1680
|
+
env: {
|
|
1681
|
+
QWEN_CODE_SYSTEM_DEFAULTS_PATH: path8.join(policyDir2(), "worker-defaults.json"),
|
|
1682
|
+
QWEN_CODE_DISABLE_CRON: "1",
|
|
1683
|
+
QWEN_DISABLE_AUTO_TITLE: "1"
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
function classify5(message) {
|
|
1688
|
+
if (/\b40[13]\b|incorrect api key|invalid api key|unauthori[sz]ed|authenticat|not logged in|no auth/i.test(message)) return "auth";
|
|
1689
|
+
if (/\b429\b|rate.?limit|too many requests|quota|capacity|overloaded|\b529\b/i.test(message)) return "rate-limited";
|
|
1690
|
+
if (/model[^.]*(not found|does not exist|not available|not supported)|invalid model|model_not_found|\b404\b/i.test(message)) return "model-unavailable";
|
|
1691
|
+
return "other";
|
|
1692
|
+
}
|
|
1693
|
+
function failure6(run, stderr, exitCode) {
|
|
1694
|
+
if (exitCode === 0 && !run.error) return void 0;
|
|
1695
|
+
const detail = stderr.trim().split("\n").filter((l) => l.trim() && !/SAFE MODE/.test(l)).pop();
|
|
1696
|
+
const message = run.error ?? detail ?? `qwen exited with code ${exitCode}`;
|
|
1697
|
+
return { kind: classify5(message), message };
|
|
1698
|
+
}
|
|
1699
|
+
function userSettings() {
|
|
1700
|
+
try {
|
|
1701
|
+
return JSON.parse(fs8.readFileSync(path8.join(process.env.QWEN_HOME ?? path8.join(os7.homedir(), ".qwen"), "settings.json"), "utf8"));
|
|
1702
|
+
} catch {
|
|
1703
|
+
return {};
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
function defaultModel5() {
|
|
1707
|
+
const name = userSettings().model?.name;
|
|
1708
|
+
return typeof name === "string" && name ? name : void 0;
|
|
1709
|
+
}
|
|
1710
|
+
function doctor6({ models, hasFallback }) {
|
|
1711
|
+
const { command, prefix } = resolveCommand(binary6());
|
|
1712
|
+
const r = spawnSync6(command, [...prefix, "--version"], { encoding: "utf8", timeout: 6e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
1713
|
+
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` }];
|
|
1714
|
+
const checks = [{ level: "ok", message: `Qwen Code ${r.stdout.trim()} at ${binary6()} (beta worker: checked against Qwen Code 0.25)` }];
|
|
1715
|
+
const auth = userSettings().security?.auth?.selectedType;
|
|
1716
|
+
const key = process.env.OPENAI_API_KEY || process.env.DASHSCOPE_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.GEMINI_API_KEY;
|
|
1717
|
+
checks.push(
|
|
1718
|
+
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" }
|
|
1719
|
+
);
|
|
1720
|
+
for (const m of models) {
|
|
1721
|
+
const model = m ?? defaultModel5();
|
|
1722
|
+
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)" });
|
|
1723
|
+
}
|
|
1724
|
+
if (!hasFallback) checks.push({ level: "warn", message: "no fallback workers configured for Qwen Code runs" });
|
|
1725
|
+
return checks;
|
|
1726
|
+
}
|
|
1727
|
+
var qwen = {
|
|
1728
|
+
id: "qwen",
|
|
1729
|
+
name: "Qwen Code",
|
|
1730
|
+
capabilities: { readOnly: "approval-mode", resume: "by-id", reportsCost: false, attachFiles: false },
|
|
1731
|
+
binary: binary6,
|
|
1732
|
+
invocation: invocation6,
|
|
1733
|
+
parse: parseEvents5,
|
|
1734
|
+
failure: failure6,
|
|
1735
|
+
defaultModel: defaultModel5,
|
|
1736
|
+
doctor: doctor6
|
|
1737
|
+
};
|
|
1738
|
+
|
|
1390
1739
|
// src/backends/index.ts
|
|
1391
|
-
var REGISTRY = new Map([opencode, codex, claude, gemini].map((b) => [b.id, b]));
|
|
1740
|
+
var REGISTRY = new Map([opencode, codex, claude, gemini, qwen, kiloBackend].map((b) => [b.id, b]));
|
|
1392
1741
|
var DEFAULT_BACKEND = opencode.id;
|
|
1393
|
-
var PLANNED = [];
|
|
1742
|
+
var PLANNED = ["hermes"];
|
|
1394
1743
|
var backendIds = () => [...REGISTRY.keys()];
|
|
1395
1744
|
var allBackends = () => [...REGISTRY.values()];
|
|
1396
1745
|
var isBackendId = (id) => REGISTRY.has(id) || PLANNED.includes(id);
|
|
@@ -1420,8 +1769,8 @@ var formatTarget = (t) => t.model ? `${t.backend}:${t.model}` : t.backend;
|
|
|
1420
1769
|
var describeTarget = (t) => t.model ? formatTarget(t) : `${t.backend} (default model)`;
|
|
1421
1770
|
|
|
1422
1771
|
// src/core/refs.ts
|
|
1423
|
-
import
|
|
1424
|
-
import
|
|
1772
|
+
import fs9 from "node:fs";
|
|
1773
|
+
import path9 from "node:path";
|
|
1425
1774
|
var FILE_EXTENSIONS = new Set(
|
|
1426
1775
|
"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(" ")
|
|
1427
1776
|
);
|
|
@@ -1472,13 +1821,13 @@ function extractRefs(answer) {
|
|
|
1472
1821
|
return [...seen.values()];
|
|
1473
1822
|
}
|
|
1474
1823
|
function verifyRefs(refs, dirs) {
|
|
1475
|
-
const roots = [...new Set(dirs.filter(Boolean).map((d) =>
|
|
1824
|
+
const roots = [...new Set(dirs.filter(Boolean).map((d) => path9.resolve(d)))];
|
|
1476
1825
|
const invalid = [];
|
|
1477
1826
|
const lineCache = /* @__PURE__ */ new Map();
|
|
1478
1827
|
const load = (file) => {
|
|
1479
1828
|
if (!lineCache.has(file)) {
|
|
1480
|
-
const size =
|
|
1481
|
-
const lines = size > MAX_BYTES ? null :
|
|
1829
|
+
const size = fs9.statSync(file).size;
|
|
1830
|
+
const lines = size > MAX_BYTES ? null : fs9.readFileSync(file, "utf8").split("\n");
|
|
1482
1831
|
if (lines && lines[lines.length - 1] === "") lines.pop();
|
|
1483
1832
|
lineCache.set(file, lines);
|
|
1484
1833
|
}
|
|
@@ -1489,9 +1838,9 @@ function verifyRefs(refs, dirs) {
|
|
|
1489
1838
|
const candidates = (ref) => {
|
|
1490
1839
|
const direct = resolve(ref.file, roots);
|
|
1491
1840
|
if (direct) return [direct];
|
|
1492
|
-
if (
|
|
1841
|
+
if (path9.isAbsolute(ref.file)) return [];
|
|
1493
1842
|
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
1494
|
-
return (byName().get(
|
|
1843
|
+
return (byName().get(path9.basename(wanted)) ?? []).filter((f) => f.split(path9.sep).join("/").endsWith(`/${wanted}`) || path9.basename(f) === wanted).slice(0, 20);
|
|
1495
1844
|
};
|
|
1496
1845
|
const check = (file, ref) => {
|
|
1497
1846
|
const lines = load(file);
|
|
@@ -1506,7 +1855,7 @@ function verifyRefs(refs, dirs) {
|
|
|
1506
1855
|
if (!files.length) {
|
|
1507
1856
|
if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
|
|
1508
1857
|
total++;
|
|
1509
|
-
invalid.push({ ref: ref.text, reason:
|
|
1858
|
+
invalid.push({ ref: ref.text, reason: path9.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
|
|
1510
1859
|
continue;
|
|
1511
1860
|
}
|
|
1512
1861
|
total++;
|
|
@@ -1527,7 +1876,7 @@ function enclosedBy(lines, ref) {
|
|
|
1527
1876
|
return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
|
|
1528
1877
|
}
|
|
1529
1878
|
var isFileName = (name) => {
|
|
1530
|
-
const base =
|
|
1879
|
+
const base = path9.basename(name);
|
|
1531
1880
|
return !base.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base) || FILE_EXTENSIONS.has(base.split(".").pop().toLowerCase());
|
|
1532
1881
|
};
|
|
1533
1882
|
function indexFiles(roots) {
|
|
@@ -1536,19 +1885,19 @@ function indexFiles(roots) {
|
|
|
1536
1885
|
const walk = (dir) => {
|
|
1537
1886
|
let entries;
|
|
1538
1887
|
try {
|
|
1539
|
-
entries =
|
|
1888
|
+
entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
1540
1889
|
} catch {
|
|
1541
1890
|
return;
|
|
1542
1891
|
}
|
|
1543
1892
|
for (const e of entries) {
|
|
1544
1893
|
if (count >= MAX_INDEXED) return;
|
|
1545
1894
|
if (e.isDirectory()) {
|
|
1546
|
-
if (!SKIP_DIRS.has(e.name)) walk(
|
|
1895
|
+
if (!SKIP_DIRS.has(e.name)) walk(path9.join(dir, e.name));
|
|
1547
1896
|
} else if (e.isFile()) {
|
|
1548
1897
|
count++;
|
|
1549
1898
|
const list2 = index.get(e.name);
|
|
1550
|
-
if (list2) list2.push(
|
|
1551
|
-
else index.set(e.name, [
|
|
1899
|
+
if (list2) list2.push(path9.join(dir, e.name));
|
|
1900
|
+
else index.set(e.name, [path9.join(dir, e.name)]);
|
|
1552
1901
|
}
|
|
1553
1902
|
}
|
|
1554
1903
|
};
|
|
@@ -1556,10 +1905,10 @@ function indexFiles(roots) {
|
|
|
1556
1905
|
return index;
|
|
1557
1906
|
}
|
|
1558
1907
|
function resolve(ref, roots) {
|
|
1559
|
-
const candidates =
|
|
1908
|
+
const candidates = path9.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path9.join(r, ref));
|
|
1560
1909
|
return candidates.find((c) => {
|
|
1561
1910
|
try {
|
|
1562
|
-
return
|
|
1911
|
+
return fs9.statSync(c).isFile() && inside(c, roots);
|
|
1563
1912
|
} catch {
|
|
1564
1913
|
return false;
|
|
1565
1914
|
}
|
|
@@ -1567,21 +1916,21 @@ function resolve(ref, roots) {
|
|
|
1567
1916
|
}
|
|
1568
1917
|
function inside(file, roots) {
|
|
1569
1918
|
return roots.some((r) => {
|
|
1570
|
-
const rel =
|
|
1571
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
1919
|
+
const rel = path9.relative(r, path9.resolve(file));
|
|
1920
|
+
return rel !== "" && !rel.startsWith("..") && !path9.isAbsolute(rel);
|
|
1572
1921
|
});
|
|
1573
1922
|
}
|
|
1574
1923
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1575
1924
|
|
|
1576
1925
|
// src/core/templates.ts
|
|
1577
|
-
import
|
|
1578
|
-
import
|
|
1926
|
+
import fs10 from "node:fs";
|
|
1927
|
+
import path11 from "node:path";
|
|
1579
1928
|
|
|
1580
1929
|
// src/core/install.ts
|
|
1581
|
-
import
|
|
1582
|
-
import { fileURLToPath as
|
|
1930
|
+
import path10 from "node:path";
|
|
1931
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1583
1932
|
function packageRoot() {
|
|
1584
|
-
return
|
|
1933
|
+
return path10.resolve(path10.dirname(fileURLToPath3(import.meta.url)), "..");
|
|
1585
1934
|
}
|
|
1586
1935
|
|
|
1587
1936
|
// src/core/templates.ts
|
|
@@ -1592,9 +1941,9 @@ var FILES = {
|
|
|
1592
1941
|
"code-reviewer": "pitroom-review/code-reviewer.md"
|
|
1593
1942
|
};
|
|
1594
1943
|
function loadTemplate(name, root = packageRoot()) {
|
|
1595
|
-
const file =
|
|
1596
|
-
if (!
|
|
1597
|
-
return
|
|
1944
|
+
const file = path11.join(root, "skills", FILES[name]);
|
|
1945
|
+
if (!fs10.existsSync(file)) throw new UserError(`template missing: ${file} (broken install? run pitroom doctor)`, 3);
|
|
1946
|
+
return fs10.readFileSync(file, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
|
|
1598
1947
|
}
|
|
1599
1948
|
function fill(template, values) {
|
|
1600
1949
|
const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
|
|
@@ -1650,689 +1999,904 @@ function parseAudit(text) {
|
|
|
1650
1999
|
return { verdict, disputed };
|
|
1651
2000
|
}
|
|
1652
2001
|
|
|
1653
|
-
// src/core/
|
|
1654
|
-
import
|
|
1655
|
-
import
|
|
1656
|
-
|
|
1657
|
-
var
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
2002
|
+
// src/core/config.ts
|
|
2003
|
+
import fs11 from "node:fs";
|
|
2004
|
+
import os8 from "node:os";
|
|
2005
|
+
import path12 from "node:path";
|
|
2006
|
+
var SCHEMA = {
|
|
2007
|
+
worker: "string",
|
|
2008
|
+
fallback: "string[]",
|
|
2009
|
+
timeout: "string",
|
|
2010
|
+
primary: "string",
|
|
2011
|
+
price: "string",
|
|
2012
|
+
link: "string[]",
|
|
2013
|
+
web: "boolean",
|
|
2014
|
+
maxParallel: "number",
|
|
2015
|
+
models: "record",
|
|
2016
|
+
tiers: "record",
|
|
2017
|
+
costs: "numbers",
|
|
2018
|
+
audit: "number",
|
|
2019
|
+
auditFix: "boolean",
|
|
2020
|
+
auditFocus: "boolean",
|
|
2021
|
+
readIn: "string",
|
|
2022
|
+
cacheDays: "number",
|
|
2023
|
+
countRateLimits: "boolean",
|
|
2024
|
+
mcpDash: "boolean",
|
|
2025
|
+
notify: "boolean",
|
|
2026
|
+
notifyCommand: "string",
|
|
2027
|
+
notifyAfter: "number",
|
|
2028
|
+
workerPrices: "record",
|
|
2029
|
+
priceFeed: "boolean",
|
|
2030
|
+
priceFeedUrl: "string",
|
|
2031
|
+
priceFeedHours: "number",
|
|
2032
|
+
budgetDaily: "number",
|
|
2033
|
+
rankWorkers: "boolean"
|
|
2034
|
+
};
|
|
2035
|
+
function configPath() {
|
|
2036
|
+
if (process.env.PITROOM_CONFIG) return path12.resolve(process.env.PITROOM_CONFIG);
|
|
2037
|
+
const base = process.platform === "win32" ? process.env.APPDATA ?? path12.join(os8.homedir(), "AppData", "Roaming") : process.env.XDG_CONFIG_HOME ?? path12.join(os8.homedir(), ".config");
|
|
2038
|
+
return path12.join(base, "pitroom", "config.json");
|
|
1679
2039
|
}
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
const
|
|
1684
|
-
const
|
|
1685
|
-
const
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
2040
|
+
var cached;
|
|
2041
|
+
function loadConfig() {
|
|
2042
|
+
if (cached) return cached;
|
|
2043
|
+
const file = configPath();
|
|
2044
|
+
const config = {};
|
|
2045
|
+
const warnings = [];
|
|
2046
|
+
if (fs11.existsSync(file)) {
|
|
2047
|
+
let raw;
|
|
2048
|
+
try {
|
|
2049
|
+
raw = JSON.parse(fs11.readFileSync(file, "utf8"));
|
|
2050
|
+
} catch (e) {
|
|
2051
|
+
warnings.push(`${file}: invalid JSON (${e.message}); ignored`);
|
|
2052
|
+
}
|
|
2053
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
2054
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
2055
|
+
const type = SCHEMA[key];
|
|
2056
|
+
if (!type) {
|
|
2057
|
+
warnings.push(`${file}: unknown key "${key}" ignored`);
|
|
2058
|
+
} else if (matches(value, type)) {
|
|
2059
|
+
config[key] = value;
|
|
2060
|
+
} else {
|
|
2061
|
+
warnings.push(`${file}: "${key}" must be ${type}; ignored`);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
1698
2065
|
}
|
|
1699
|
-
|
|
2066
|
+
cached = { config, warnings };
|
|
2067
|
+
return cached;
|
|
1700
2068
|
}
|
|
1701
|
-
function
|
|
1702
|
-
|
|
1703
|
-
if (
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
2069
|
+
function matches(v, type) {
|
|
2070
|
+
if (type === "string[]") return Array.isArray(v) && v.every((s) => typeof s === "string");
|
|
2071
|
+
if (type === "record") {
|
|
2072
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((s) => typeof s === "string");
|
|
2073
|
+
}
|
|
2074
|
+
if (type === "numbers") {
|
|
2075
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((n) => typeof n === "number" && Number.isFinite(n) && n >= 0);
|
|
2076
|
+
}
|
|
2077
|
+
if (type === "number") return typeof v === "number" && Number.isFinite(v) && v >= 0;
|
|
2078
|
+
return typeof v === type;
|
|
1707
2079
|
}
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
2080
|
+
var DEFAULT_PARALLEL = 20;
|
|
2081
|
+
var MAX_PARALLEL_LIMIT = 30;
|
|
2082
|
+
var clampParallel = (s) => ({ ...s, value: Math.min(s.value, MAX_PARALLEL_LIMIT) });
|
|
2083
|
+
var positiveInt = (v) => {
|
|
2084
|
+
const n = Number(v);
|
|
2085
|
+
return v !== void 0 && v !== "" && Number.isInteger(n) && n > 0 ? n : void 0;
|
|
2086
|
+
};
|
|
2087
|
+
var rate = (v) => {
|
|
2088
|
+
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
2089
|
+
return typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.min(n, 1) : void 0;
|
|
2090
|
+
};
|
|
2091
|
+
var amount = (v) => {
|
|
2092
|
+
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
2093
|
+
return typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : void 0;
|
|
2094
|
+
};
|
|
2095
|
+
var days = (v) => {
|
|
2096
|
+
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
2097
|
+
return typeof n === "number" && Number.isInteger(n) && n >= 0 ? n : void 0;
|
|
2098
|
+
};
|
|
2099
|
+
var flag01 = (v) => {
|
|
2100
|
+
const t = v?.trim().toLowerCase();
|
|
2101
|
+
return t === void 0 || t === "" ? void 0 : ["1", "true", "on", "yes"].includes(t) ? true : ["0", "false", "off", "no"].includes(t) ? false : void 0;
|
|
2102
|
+
};
|
|
2103
|
+
var readIn = (v) => v === "auto" || v === "snapshot" || v === "project" ? v : void 0;
|
|
2104
|
+
var list = (s) => s?.split(",").map((x) => x.trim()).filter(Boolean);
|
|
2105
|
+
function setting(flag, env, conf, fallback) {
|
|
2106
|
+
if (flag !== void 0) return { value: flag, source: "flag" };
|
|
2107
|
+
if (env !== void 0) return { value: env, source: "env" };
|
|
2108
|
+
if (conf !== void 0) return { value: conf, source: "config" };
|
|
2109
|
+
return { value: fallback, source: "default" };
|
|
1712
2110
|
}
|
|
1713
|
-
function
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
2111
|
+
function effective(flags = {}) {
|
|
2112
|
+
const c = loadConfig().config;
|
|
2113
|
+
const e = process.env;
|
|
2114
|
+
return {
|
|
2115
|
+
worker: setting(flags.worker, e.PITROOM_WORKER, c.worker, DEFAULT_BACKEND),
|
|
2116
|
+
/** Overrides the model of the preferred worker only. */
|
|
2117
|
+
model: setting(flags.model, e.PITROOM_MODEL, void 0, void 0),
|
|
2118
|
+
fallback: setting(void 0, list(e.PITROOM_FALLBACK), c.fallback, []),
|
|
2119
|
+
timeout: setting(flags.timeout, e.PITROOM_TIMEOUT, c.timeout, "30m"),
|
|
2120
|
+
primary: setting(void 0, e.PITROOM_PRIMARY, c.primary, "sonnet"),
|
|
2121
|
+
price: setting(void 0, e.PITROOM_PRICE, c.price, void 0),
|
|
2122
|
+
link: setting(void 0, void 0, c.link, []),
|
|
2123
|
+
web: setting(void 0, void 0, c.web, false),
|
|
2124
|
+
maxParallel: clampParallel(setting(void 0, positiveInt(e.PITROOM_MAX_PARALLEL), positiveInt(c.maxParallel), DEFAULT_PARALLEL)),
|
|
2125
|
+
models: setting(void 0, void 0, c.models, {}),
|
|
2126
|
+
tiers: setting(void 0, void 0, c.tiers, {}),
|
|
2127
|
+
costs: setting(void 0, void 0, c.costs, {}),
|
|
2128
|
+
audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0),
|
|
2129
|
+
auditFix: setting(void 0, flag01(e.PITROOM_AUDIT_FIX), c.auditFix, false),
|
|
2130
|
+
auditFocus: setting(void 0, flag01(e.PITROOM_AUDIT_FOCUS), c.auditFocus, false),
|
|
2131
|
+
readIn: setting(void 0, readIn(e.PITROOM_READ_IN), readIn(c.readIn), "auto"),
|
|
2132
|
+
cacheDays: setting(void 0, days(e.PITROOM_CACHE_DAYS), days(c.cacheDays), 7),
|
|
2133
|
+
countRateLimits: setting(void 0, void 0, c.countRateLimits, false),
|
|
2134
|
+
mcpDash: setting(void 0, flag01(e.PITROOM_MCP_DASH), c.mcpDash, true),
|
|
2135
|
+
notify: setting(void 0, flag01(e.PITROOM_NOTIFY), c.notify, false),
|
|
2136
|
+
notifyCommand: setting(void 0, e.PITROOM_NOTIFY_COMMAND?.trim() || void 0, c.notifyCommand?.trim() || void 0, void 0),
|
|
2137
|
+
notifyAfter: setting(void 0, days(e.PITROOM_NOTIFY_AFTER), days(c.notifyAfter), 15),
|
|
2138
|
+
workerPrices: setting(void 0, void 0, c.workerPrices, {}),
|
|
2139
|
+
priceFeed: setting(void 0, flag01(e.PITROOM_PRICE_FEED), c.priceFeed, false),
|
|
2140
|
+
priceFeedUrl: setting(void 0, e.PITROOM_PRICE_FEED_URL?.trim() || void 0, c.priceFeedUrl?.trim() || void 0, "https://models.dev/api.json"),
|
|
2141
|
+
priceFeedHours: setting(void 0, positiveInt(e.PITROOM_PRICE_FEED_HOURS), positiveInt(c.priceFeedHours), 24),
|
|
2142
|
+
rankWorkers: setting(void 0, flag01(e.PITROOM_RANK_WORKERS), c.rankWorkers, false),
|
|
2143
|
+
budgetDaily: setting(void 0, amount(e.PITROOM_BUDGET_DAILY), amount(c.budgetDaily), void 0)
|
|
2144
|
+
};
|
|
1722
2145
|
}
|
|
1723
|
-
var planName = (file) => path10.basename(file).replace(/\.md$/i, "");
|
|
1724
2146
|
|
|
1725
|
-
// src/
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
2147
|
+
// src/core/audit.ts
|
|
2148
|
+
var ASKS_FOR_LIST = /\b(all|every|each|list|lists|enumerate|how many|count|which)\b/i;
|
|
2149
|
+
function focusedRate(base, task, record) {
|
|
2150
|
+
if (base <= 0 || base >= 1) return { rate: base, why: [] };
|
|
2151
|
+
let rate2 = base;
|
|
2152
|
+
const why = [];
|
|
2153
|
+
if (ASKS_FOR_LIST.test(task)) {
|
|
2154
|
+
rate2 *= 2;
|
|
2155
|
+
why.push("asks for a list or a count");
|
|
2156
|
+
}
|
|
2157
|
+
if (record && record.audited >= 3 && record.agreed / record.audited < 0.5) {
|
|
2158
|
+
rate2 *= 2;
|
|
2159
|
+
why.push(`the worker's audited answers were confirmed ${record.agreed} of ${record.audited} times`);
|
|
2160
|
+
} else if (record && record.audited >= 5 && record.agreed === record.audited) {
|
|
2161
|
+
rate2 /= 2;
|
|
2162
|
+
why.push(`the worker's ${record.audited} audited answers were all confirmed`);
|
|
2163
|
+
}
|
|
2164
|
+
return { rate: Math.min(rate2, 1), why };
|
|
2165
|
+
}
|
|
2166
|
+
function fixTask(disputed) {
|
|
2167
|
+
return [
|
|
2168
|
+
"Another worker audited your answer above and disputed these claims:",
|
|
2169
|
+
...disputed.map((d) => `- ${d}`),
|
|
2170
|
+
"",
|
|
2171
|
+
"Check each one yourself against the files (the auditor can be wrong too). Then give your whole answer again,",
|
|
2172
|
+
"corrected, in the same answer format: keep what holds, fix what does not, and add what was missing.",
|
|
2173
|
+
"Under OPEN ISSUES, say which disputed claims you accepted and which you kept, and why."
|
|
2174
|
+
].join("\n");
|
|
1743
2175
|
}
|
|
1744
|
-
sub=""; skip=""; after=""
|
|
1745
|
-
for a do
|
|
1746
|
-
if [ -n "$sub" ]; then after="$after $a"; continue; fi
|
|
1747
|
-
case "$skip" in
|
|
1748
|
-
c) skip=""; case "$a" in [Aa][Ll][Ii][Aa][Ss].*) block "-c $a" ;; esac; continue ;;
|
|
1749
|
-
v) skip=""; continue ;;
|
|
1750
|
-
esac
|
|
1751
|
-
case "$a" in
|
|
1752
|
-
-c|--config-env) skip=c ;;
|
|
1753
|
-
-C|--git-dir|--work-tree|--namespace|--super-prefix|--exec-path) skip=v ;;
|
|
1754
|
-
-*) ;;
|
|
1755
|
-
*) sub="$a" ;;
|
|
1756
|
-
esac
|
|
1757
|
-
done
|
|
1758
|
-
[ -z "$sub" ] && exec "$real" "$@"
|
|
1759
2176
|
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
2177
|
+
// src/core/rank.ts
|
|
2178
|
+
var MIN_RUNS = 5;
|
|
2179
|
+
var MIN_AUDITS = 3;
|
|
2180
|
+
function scoreOf(r) {
|
|
2181
|
+
if (!r || r.runs < MIN_RUNS) return void 0;
|
|
2182
|
+
const ok = Math.min(r.ok, r.runs) / r.runs;
|
|
2183
|
+
return r.audited >= MIN_AUDITS ? ok * (r.agreed / r.audited) : ok;
|
|
2184
|
+
}
|
|
2185
|
+
var pct = (x) => `${Math.round(x * 100)}%`;
|
|
2186
|
+
function rankChain(chain, recordOf) {
|
|
2187
|
+
const scored = chain.map((t, i) => ({ t, i, score: scoreOf(recordOf(t)) }));
|
|
2188
|
+
const slots = scored.filter((s) => s.score !== void 0);
|
|
2189
|
+
if (slots.length < 2) return { chain };
|
|
2190
|
+
const ranked = [...slots].sort((a, b) => Math.round(b.score * 10) - Math.round(a.score * 10) || a.i - b.i);
|
|
2191
|
+
const out = [...chain];
|
|
2192
|
+
slots.forEach((slot, k) => out[slot.i] = ranked[k].t);
|
|
2193
|
+
if (out[0] === chain[0]) return { chain: out };
|
|
2194
|
+
const by = (t) => scored.find((s) => s.t === t).score;
|
|
2195
|
+
return {
|
|
2196
|
+
chain: out,
|
|
2197
|
+
note: `ranked by record ("rankWorkers"): ${describeTarget(out[0])} (${pct(by(out[0]))}) goes before ${describeTarget(chain[0])} (${pct(by(chain[0]))})`
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
1769
2200
|
|
|
1770
|
-
|
|
1771
|
-
|
|
2201
|
+
// src/core/receipt.ts
|
|
2202
|
+
import fs13 from "node:fs";
|
|
1772
2203
|
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
remote)
|
|
1788
|
-
for w in $after; do case "$w" in add|remove|rm|rename|set-url|set-head|set-branches|prune|update) block "remote $w" ;; esac; done ;;
|
|
1789
|
-
fetch)
|
|
1790
|
-
for w in $after; do case "$w" in -*) ;; *:*) block "fetch $w (writes a local ref)" ;; esac; done ;;
|
|
1791
|
-
apply)
|
|
1792
|
-
has --index --cached -3 --3way && block "apply$after" ;;
|
|
1793
|
-
reflog)
|
|
1794
|
-
has expire delete && block "reflog$after" ;;
|
|
1795
|
-
symbolic-ref)
|
|
1796
|
-
has -d --delete && block "symbolic-ref$after"
|
|
1797
|
-
[ "$(count)" -ge 2 ] && block "symbolic-ref$after" ;;
|
|
1798
|
-
submodule)
|
|
1799
|
-
for w in $after; do case "$w" in -*) ;; status|summary) break ;; *) block "submodule $w" ;; esac; done ;;
|
|
1800
|
-
esac
|
|
1801
|
-
exec "$real" "$@"
|
|
1802
|
-
`;
|
|
1803
|
-
function shimDir() {
|
|
1804
|
-
return path11.join(home(), "shim", `v${VERSION}`);
|
|
1805
|
-
}
|
|
1806
|
-
function findRealGit(skip) {
|
|
1807
|
-
const same = (a, b) => WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
1808
|
-
for (const dir of (process.env.PATH ?? "").split(path11.delimiter).filter(Boolean)) {
|
|
1809
|
-
if (same(path11.resolve(dir), skip)) continue;
|
|
1810
|
-
const p = path11.join(dir, WINDOWS ? "git.exe" : "git");
|
|
1811
|
-
try {
|
|
1812
|
-
fs11.accessSync(p, fs11.constants.X_OK);
|
|
1813
|
-
if (fs11.statSync(p).isFile()) return p;
|
|
1814
|
-
} catch {
|
|
2204
|
+
// src/core/prices.ts
|
|
2205
|
+
import fs12 from "node:fs";
|
|
2206
|
+
import path13 from "node:path";
|
|
2207
|
+
var RETRY_AFTER_FAILURE_MS = 60 * 6e4;
|
|
2208
|
+
var catalogFile = () => path13.join(home(), "prices.json");
|
|
2209
|
+
function trim(raw) {
|
|
2210
|
+
const out = {};
|
|
2211
|
+
if (!raw || typeof raw !== "object") return out;
|
|
2212
|
+
for (const [provider, p] of Object.entries(raw)) {
|
|
2213
|
+
for (const [id, m] of Object.entries(p?.models ?? {})) {
|
|
2214
|
+
const c = m?.cost;
|
|
2215
|
+
const ok = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
2216
|
+
if (!c || !ok(c.input) || !ok(c.output)) continue;
|
|
2217
|
+
out[`${provider}/${id}`] = [c.input, c.output, ok(c.cache_read) ? c.cache_read : c.input / 10];
|
|
1815
2218
|
}
|
|
1816
2219
|
}
|
|
1817
|
-
return
|
|
2220
|
+
return out;
|
|
1818
2221
|
}
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
2222
|
+
var cached2;
|
|
2223
|
+
function readCatalog() {
|
|
2224
|
+
const file = catalogFile();
|
|
2225
|
+
try {
|
|
2226
|
+
const { mtimeMs } = fs12.statSync(file);
|
|
2227
|
+
if (cached2?.file === file && cached2.mtimeMs === mtimeMs) return cached2.catalog;
|
|
2228
|
+
const parsed = JSON.parse(fs12.readFileSync(file, "utf8"));
|
|
2229
|
+
const catalog6 = parsed && typeof parsed.models === "object" && parsed.models && Number.isFinite(Date.parse(parsed.fetchedAt)) ? parsed : void 0;
|
|
2230
|
+
cached2 = { file, mtimeMs, catalog: catalog6 };
|
|
2231
|
+
return catalog6;
|
|
2232
|
+
} catch {
|
|
2233
|
+
return void 0;
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
var OWN = { codex: ["openai"], claude: ["anthropic"], gemini: ["google"] };
|
|
2237
|
+
function catalogPrice(backend, ...models) {
|
|
2238
|
+
const catalog6 = readCatalog();
|
|
2239
|
+
if (!catalog6) return void 0;
|
|
2240
|
+
for (const raw of models) {
|
|
2241
|
+
const id = raw?.split("#")[0];
|
|
2242
|
+
if (!id) continue;
|
|
2243
|
+
const keys = [id, ...[...OWN[backend] ?? [], "anthropic", "openai", "google"].map((p) => `${p}/${id}`)];
|
|
2244
|
+
const any = keys.find((k) => catalog6.models[k]) ?? Object.keys(catalog6.models).filter((k) => k.endsWith(`/${id}`)).sort()[0];
|
|
2245
|
+
const triple = any ? catalog6.models[any] : void 0;
|
|
2246
|
+
if (any && triple && triple.length === 3 && triple.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
2247
|
+
const [input, output, cachedInput] = triple;
|
|
2248
|
+
return { input, output, cachedInput, key: any };
|
|
1825
2249
|
}
|
|
1826
|
-
const up = path11.dirname(dir);
|
|
1827
|
-
if (up === dir) break;
|
|
1828
|
-
dir = up;
|
|
1829
2250
|
}
|
|
1830
2251
|
return void 0;
|
|
1831
2252
|
}
|
|
1832
|
-
var cmdShim = (sh) => `@echo off\r
|
|
1833
|
-
set "MSYS2_ARG_CONV_EXCL=*"\r
|
|
1834
|
-
"${sh}" "%~dp0git" %*\r
|
|
1835
|
-
exit /b %errorlevel%\r
|
|
1836
|
-
`;
|
|
1837
|
-
var HOOK_VERSION = 1;
|
|
1838
|
-
var REF_HOOK = `#!/bin/sh
|
|
1839
|
-
# pitroom git guard (layer 2, v${HOOK_VERSION}): refuse ref updates made by workers.
|
|
1840
|
-
[ "$1" = prepared ] || exit 0
|
|
1841
|
-
echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
|
|
1842
|
-
exit 1
|
|
1843
|
-
`;
|
|
1844
|
-
var hooksDir = () => path11.join(home(), "git-hooks", `v${HOOK_VERSION}`);
|
|
1845
|
-
function writeIfChanged(file, content) {
|
|
1846
|
-
if (fs11.existsSync(file) && fs11.readFileSync(file, "utf8") === content) return;
|
|
1847
|
-
fs11.mkdirSync(path11.dirname(file), { recursive: true });
|
|
1848
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
1849
|
-
fs11.writeFileSync(tmp, content, { mode: 493 });
|
|
1850
|
-
renameOver(tmp, file);
|
|
1851
|
-
}
|
|
1852
|
-
function pathKey(env) {
|
|
1853
|
-
const keys = Object.keys(env).filter((k) => k.toLowerCase() === "path");
|
|
1854
|
-
return keys.includes("PATH") ? "PATH" : keys[0] ?? "PATH";
|
|
1855
|
-
}
|
|
1856
|
-
function onePath(env) {
|
|
1857
|
-
const key = pathKey(env);
|
|
1858
|
-
const out = { ...env };
|
|
1859
|
-
for (const k of Object.keys(out)) if (k !== key && k.toLowerCase() === "path") delete out[k];
|
|
1860
|
-
return out;
|
|
1861
|
-
}
|
|
1862
|
-
function withGitConfig(env, entries) {
|
|
1863
|
-
const out = { ...env };
|
|
1864
|
-
let n = Number(env.GIT_CONFIG_COUNT) || 0;
|
|
1865
|
-
for (const [key, value] of entries) {
|
|
1866
|
-
out[`GIT_CONFIG_KEY_${n}`] = key;
|
|
1867
|
-
out[`GIT_CONFIG_VALUE_${n}`] = value;
|
|
1868
|
-
n++;
|
|
1869
|
-
}
|
|
1870
|
-
out.GIT_CONFIG_COUNT = String(n);
|
|
1871
|
-
return out;
|
|
1872
|
-
}
|
|
1873
|
-
function guardEnv(env) {
|
|
1874
|
-
const quiet = {
|
|
1875
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
1876
|
-
GIT_EDITOR: "true",
|
|
1877
|
-
GIT_SEQUENCE_EDITOR: "true",
|
|
1878
|
-
GIT_PAGER: "cat",
|
|
1879
|
-
PAGER: "cat"
|
|
1880
|
-
};
|
|
1881
|
-
writeIfChanged(path11.join(hooksDir(), "reference-transaction"), REF_HOOK);
|
|
1882
|
-
env = withGitConfig(onePath(env), [
|
|
1883
|
-
["core.hooksPath", fwd(hooksDir())],
|
|
1884
|
-
["url.pitroom-push-blocked://.pushInsteadOf", ""]
|
|
1885
|
-
]);
|
|
1886
|
-
const dir = shimDir();
|
|
1887
|
-
const real = findRealGit(dir);
|
|
1888
|
-
if (!real) return { ...env, ...quiet };
|
|
1889
|
-
let sh;
|
|
1890
|
-
if (WINDOWS) {
|
|
1891
|
-
sh = findGitSh(real);
|
|
1892
|
-
if (!sh) return { ...env, ...quiet };
|
|
1893
|
-
writeIfChanged(path11.join(dir, "git.cmd"), cmdShim(sh));
|
|
1894
|
-
}
|
|
1895
|
-
writeIfChanged(path11.join(dir, "git"), SHIM);
|
|
1896
|
-
const key = pathKey(env);
|
|
1897
|
-
return { ...env, ...quiet, PITROOM_REAL_GIT: fwd(real), [key]: `${dir}${path11.delimiter}${env[key] ?? ""}` };
|
|
1898
|
-
}
|
|
1899
|
-
function shimReady() {
|
|
1900
|
-
const env = guardEnv({ ...process.env });
|
|
1901
|
-
return Boolean(env[pathKey(env)]?.startsWith(shimDir()));
|
|
1902
|
-
}
|
|
1903
2253
|
|
|
1904
|
-
// src/core/
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
function
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
if (m.pid) return isAlive(m.pid);
|
|
1917
|
-
return Date.now() - Date.parse(m.startedAt) < STARTUP_GRACE_MS;
|
|
1918
|
-
} catch {
|
|
1919
|
-
return false;
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
function tryClaim(file, runId) {
|
|
1923
|
-
fs12.mkdirSync(path12.dirname(file), { recursive: true });
|
|
1924
|
-
try {
|
|
1925
|
-
fs12.writeFileSync(file, runId, { flag: "wx" });
|
|
1926
|
-
return true;
|
|
1927
|
-
} catch (e) {
|
|
1928
|
-
if (e.code !== "EEXIST") throw e;
|
|
1929
|
-
}
|
|
1930
|
-
let owner = "";
|
|
1931
|
-
try {
|
|
1932
|
-
owner = fs12.readFileSync(file, "utf8").trim();
|
|
1933
|
-
} catch {
|
|
1934
|
-
}
|
|
1935
|
-
if (owner === runId) return true;
|
|
1936
|
-
if (holderActive(owner, runId)) return false;
|
|
1937
|
-
fs12.rmSync(file, { force: true });
|
|
1938
|
-
try {
|
|
1939
|
-
fs12.writeFileSync(file, runId, { flag: "wx" });
|
|
1940
|
-
return true;
|
|
1941
|
-
} catch {
|
|
1942
|
-
return false;
|
|
2254
|
+
// src/core/receipt.ts
|
|
2255
|
+
var PRESETS = {
|
|
2256
|
+
sonnet: { name: "Claude Sonnet", input: 3, output: 15, cachedInput: 0.3 },
|
|
2257
|
+
opus: { name: "Claude Opus", input: 5, output: 25, cachedInput: 0.5 },
|
|
2258
|
+
haiku: { name: "Claude Haiku", input: 1, output: 5, cachedInput: 0.1 },
|
|
2259
|
+
"gpt-5": { name: "GPT-5", input: 1.25, output: 10, cachedInput: 0.125 }
|
|
2260
|
+
};
|
|
2261
|
+
function primaryPrice() {
|
|
2262
|
+
const eff = effective();
|
|
2263
|
+
const custom = eff.price.value?.split(",").map(Number);
|
|
2264
|
+
if (custom && custom.length >= 2 && custom.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
2265
|
+
return { name: "custom", input: custom[0], output: custom[1], cachedInput: custom[2] ?? custom[0] / 10 };
|
|
1943
2266
|
}
|
|
2267
|
+
const named = eff.primary.value;
|
|
2268
|
+
const preset = PRESETS[named.toLowerCase()];
|
|
2269
|
+
if (preset) return preset;
|
|
2270
|
+
const c = eff.priceFeed.value ? catalogPrice("", named) : void 0;
|
|
2271
|
+
return c ? { name: named, source: "catalog", input: c.input, output: c.output, cachedInput: c.cachedInput } : PRESETS.sonnet;
|
|
1944
2272
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
}
|
|
2273
|
+
var estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
2274
|
+
function parsePriceSpec(spec, name = "worker") {
|
|
2275
|
+
const parts = typeof spec === "string" ? spec.split(",").map((x) => Number(x.trim())) : [];
|
|
2276
|
+
if (parts.length < 2 || parts.length > 3 || !parts.every((n) => Number.isFinite(n) && n >= 0)) return void 0;
|
|
2277
|
+
return { name, source: "config", input: parts[0], output: parts[1], cachedInput: parts[2] ?? parts[0] / 10 };
|
|
1951
2278
|
}
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
return void 0;
|
|
2279
|
+
var costAt = (usage, price) => (usage.input * price.input + usage.cacheRead * price.cachedInput + (usage.output + usage.reasoning) * price.output) / 1e6;
|
|
2280
|
+
var workerCostOf = (usage) => usage?.cost ?? usage?.costEstimate;
|
|
2281
|
+
function savedUsd(usage, returnedTokens, price = primaryPrice()) {
|
|
2282
|
+
const readingTheReport = returnedTokens * price.input / 1e6;
|
|
2283
|
+
return Math.max(0, costAt(usage, price) - (workerCostOf(usage) ?? 0) - readingTheReport);
|
|
1958
2284
|
}
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
return fs12.readdirSync(slotsDir()).map((f) => {
|
|
2285
|
+
function readLedger(sinceMs) {
|
|
2286
|
+
if (!fs13.existsSync(ledgerFile())) return [];
|
|
2287
|
+
return fs13.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
|
|
1963
2288
|
try {
|
|
1964
|
-
return
|
|
2289
|
+
return [JSON.parse(l)];
|
|
1965
2290
|
} catch {
|
|
1966
|
-
return
|
|
2291
|
+
return [];
|
|
1967
2292
|
}
|
|
1968
|
-
}).filter((
|
|
1969
|
-
}
|
|
1970
|
-
var lockFile = (repoRoot) => path12.join(locksDir(), `write-${crypto2.createHash("sha1").update(path12.resolve(repoRoot)).digest("hex").slice(0, 16)}`);
|
|
1971
|
-
function acquireWriteLock(repoRoot, runId) {
|
|
1972
|
-
const file = lockFile(repoRoot);
|
|
1973
|
-
if (tryClaim(file, runId)) return;
|
|
1974
|
-
let owner = "";
|
|
1975
|
-
try {
|
|
1976
|
-
owner = fs12.readFileSync(file, "utf8").trim();
|
|
1977
|
-
} catch {
|
|
1978
|
-
}
|
|
1979
|
-
throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
|
|
2293
|
+
}).filter((e) => !sinceMs || Date.parse(e.at) >= sinceMs);
|
|
1980
2294
|
}
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
if (TEMPLATE.test(name)) return false;
|
|
1994
|
-
if (name === ".env" || name.startsWith(".env.") || name.endsWith(".env")) return true;
|
|
1995
|
-
return /\.(pem|p12|pfx|key|jks|keystore)$/.test(name) || /^id_(rsa|dsa|ecdsa|ed25519)$/.test(name) || name === ".netrc" || name === "credentials.json" || /^secrets?\.(json|ya?ml|toml)$/.test(name);
|
|
2295
|
+
function totals(entries) {
|
|
2296
|
+
const t = entries.reduce(
|
|
2297
|
+
(a, e) => ({
|
|
2298
|
+
runs: a.runs + 1,
|
|
2299
|
+
tokens: a.tokens + e.tokens,
|
|
2300
|
+
returned: a.returned + e.returned,
|
|
2301
|
+
workerCost: a.workerCost + e.workerCost,
|
|
2302
|
+
saved: a.saved + e.saved
|
|
2303
|
+
}),
|
|
2304
|
+
{ runs: 0, tokens: 0, returned: 0, workerCost: 0, saved: 0 }
|
|
2305
|
+
);
|
|
2306
|
+
return { ...t, ratio: t.returned ? t.tokens / t.returned : 0 };
|
|
1996
2307
|
}
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
try {
|
|
2004
|
-
entries = fs13.readdirSync(d, { withFileTypes: true });
|
|
2005
|
-
} catch {
|
|
2006
|
-
return;
|
|
2007
|
-
}
|
|
2008
|
-
for (const e of entries) {
|
|
2009
|
-
visited++;
|
|
2010
|
-
if (e.isDirectory()) {
|
|
2011
|
-
if (!SKIP_DIRS2.has(e.name)) walk(path13.join(d, e.name), depth + 1);
|
|
2012
|
-
} else if (e.isFile() && looksSecret(e.name)) {
|
|
2013
|
-
found.push(slash(path13.relative(dir, path13.join(d, e.name))));
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
};
|
|
2017
|
-
walk(dir, 0);
|
|
2018
|
-
return found.sort();
|
|
2308
|
+
var usd = (n) => n >= 100 ? `$${n.toFixed(0)}` : n >= 1 ? `$${n.toFixed(2)}` : `$${n.toFixed(3)}`;
|
|
2309
|
+
function compact(n) {
|
|
2310
|
+
const fmt = (v) => v.toFixed(v >= 100 ? 0 : 1).replace(/\.0$/, "");
|
|
2311
|
+
if (n >= 1e6) return `${fmt(n / 1e6)}M`;
|
|
2312
|
+
if (n >= 1e3) return `${fmt(n / 1e3)}k`;
|
|
2313
|
+
return String(Math.round(n));
|
|
2019
2314
|
}
|
|
2020
|
-
function
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
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();
|
|
2024
|
-
} catch {
|
|
2025
|
-
return [];
|
|
2026
|
-
}
|
|
2315
|
+
function badgeUrl(t) {
|
|
2316
|
+
const msg = encodeURIComponent(`saved ${usd(t.saved)} \xB7 ${compact(t.tokens)} tokens offloaded`).replace(/-/g, "--");
|
|
2317
|
+
return `https://img.shields.io/badge/pitroom-${msg}-7c3aed`;
|
|
2027
2318
|
}
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
const
|
|
2031
|
-
const
|
|
2032
|
-
|
|
2033
|
-
|
|
2319
|
+
var esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
2320
|
+
function card(t, period) {
|
|
2321
|
+
const W = 720, H = 316, M = 32, GAP = 12;
|
|
2322
|
+
const tile = (i, value, label) => {
|
|
2323
|
+
const w = (W - 2 * M - 2 * GAP) / 3, x = M + i * (w + GAP);
|
|
2324
|
+
return `<rect x="${x}" y="196" width="${w}" height="72" rx="14" fill="#fff" fill-opacity=".035" stroke="#fff" stroke-opacity=".1"/>
|
|
2325
|
+
<text x="${x + 18}" y="224" class="lab">${esc(label.toUpperCase())}</text><text x="${x + 18}" y="254" class="val">${esc(value)}</text>`;
|
|
2326
|
+
};
|
|
2327
|
+
const when = period.toUpperCase();
|
|
2328
|
+
const pill = 22 + when.length * 7.4;
|
|
2329
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" role="img" aria-label="pitroom saved ${esc(usd(t.saved))}">
|
|
2330
|
+
<style>
|
|
2331
|
+
text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Inter,Helvetica,Arial,sans-serif;fill:#f4f4f5}
|
|
2332
|
+
.name{font-size:20px;font-weight:650;letter-spacing:-.01em}
|
|
2333
|
+
.sub{font-size:12.5px;fill:#a1a1aa}
|
|
2334
|
+
.pill{font-size:11px;font-weight:600;letter-spacing:.1em;fill:#a1a1aa}
|
|
2335
|
+
.lab{font-size:11px;font-weight:500;letter-spacing:.09em;fill:#a1a1aa}
|
|
2336
|
+
.big{font-size:60px;font-weight:700;letter-spacing:-.03em;fill:#3ddc97}
|
|
2337
|
+
.val{font-size:26px;font-weight:650;letter-spacing:-.02em}
|
|
2338
|
+
.foot{font-size:11.5px;fill:#71717a}
|
|
2339
|
+
</style>
|
|
2340
|
+
<defs>
|
|
2341
|
+
<radialGradient id="o" cx="1" cy="0" r=".7"><stop offset="0" stop-color="#ff6a2b" stop-opacity=".24"/><stop offset="1" stop-color="#ff6a2b" stop-opacity="0"/></radialGradient>
|
|
2342
|
+
<radialGradient id="b" cx="0" cy="0" r=".7"><stop offset="0" stop-color="#38bdf8" stop-opacity=".16"/><stop offset="1" stop-color="#38bdf8" stop-opacity="0"/></radialGradient>
|
|
2343
|
+
<linearGradient id="m" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#ff6a2b"/><stop offset="1" stop-color="#ffa86b"/></linearGradient>
|
|
2344
|
+
<clipPath id="c"><rect width="${W}" height="${H}" rx="20"/></clipPath>
|
|
2345
|
+
</defs>
|
|
2346
|
+
<g clip-path="url(#c)"><rect width="${W}" height="${H}" fill="#0b0b0f"/><rect width="${W}" height="${H}" fill="url(#o)"/><rect width="${W}" height="${H}" fill="url(#b)"/></g>
|
|
2347
|
+
<rect x=".5" y=".5" width="${W - 1}" height="${H - 1}" rx="19.5" fill="none" stroke="#fff" stroke-opacity=".1"/>
|
|
2348
|
+
<rect x="${M}" y="28" width="38" height="38" rx="11" fill="url(#m)"/>
|
|
2349
|
+
<path d="M4 4h4v4H4zm8 0h4v4h-4zM8 8h4v4H8zm8 0h4v4h-4zM4 12h4v4H4zm8 0h4v4h-4zm-4 4h4v4H8zm8 0h4v4h-4z" fill="#fff" transform="translate(${M + 7} 35) scale(1)"/>
|
|
2350
|
+
<text x="${M + 52}" y="45" class="name">Pitroom</text><text x="${M + 52}" y="63" class="sub">Your agent's pit crew</text>
|
|
2351
|
+
<rect x="${W - M - pill}" y="34" width="${pill}" height="26" rx="13" fill="#fff" fill-opacity=".04" stroke="#fff" stroke-opacity=".14"/>
|
|
2352
|
+
<text x="${W - M - pill / 2}" y="51" class="pill" text-anchor="middle">${esc(when)}</text>
|
|
2353
|
+
<text x="${M}" y="112" class="lab">SAVED ON YOUR MAIN CODING AGENT</text>
|
|
2354
|
+
<text x="${M}" y="168" class="big">${esc(usd(t.saved))}</text>
|
|
2355
|
+
${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.round(t.ratio)}\xD7` : "\u2014", "Context compression")}${tile(2, String(t.runs), "Delegated tasks")}
|
|
2356
|
+
<text x="${M}" y="296" class="foot">Estimated against ${esc(primaryPrice().name)} pricing \xB7 npx pitroom</text>
|
|
2357
|
+
</svg>
|
|
2358
|
+
`;
|
|
2034
2359
|
}
|
|
2035
|
-
var slash = (p) => p.split(path13.sep).join("/");
|
|
2036
2360
|
|
|
2037
|
-
// src/core/
|
|
2038
|
-
|
|
2361
|
+
// src/core/ui.ts
|
|
2362
|
+
var WEEK_MS = 7 * 24 * 3600 * 1e3;
|
|
2039
2363
|
|
|
2040
|
-
// src/core/
|
|
2364
|
+
// src/core/plan.ts
|
|
2041
2365
|
import fs14 from "node:fs";
|
|
2042
|
-
import os7 from "node:os";
|
|
2043
2366
|
import path14 from "node:path";
|
|
2044
|
-
var
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2367
|
+
var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
2368
|
+
var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
|
|
2369
|
+
function structure(lines) {
|
|
2370
|
+
const headings = [];
|
|
2371
|
+
const rules = [];
|
|
2372
|
+
const fenced = [];
|
|
2373
|
+
let open;
|
|
2374
|
+
lines.forEach((l, i) => {
|
|
2375
|
+
const f = FENCE.exec(l);
|
|
2376
|
+
if (f) {
|
|
2377
|
+
fenced[i] = true;
|
|
2378
|
+
const mark = f[1];
|
|
2379
|
+
if (!open) open = mark;
|
|
2380
|
+
else if (mark[0] === open[0] && mark.length >= open.length && !f[2].trim()) open = void 0;
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
fenced[i] = !!open;
|
|
2384
|
+
if (open) return;
|
|
2385
|
+
const h = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/.exec(l);
|
|
2386
|
+
if (h) headings.push({ line: i, level: h[1].length, text: h[2] });
|
|
2387
|
+
else if (RULE.test(l)) rules.push(i);
|
|
2388
|
+
});
|
|
2389
|
+
return { headings, rules, fenced };
|
|
2390
|
+
}
|
|
2391
|
+
function parsePlan(text, file = "") {
|
|
2392
|
+
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
2393
|
+
const { headings, rules, fenced } = structure(lines);
|
|
2394
|
+
const after = (line) => Math.min(headings.find((h) => h.line > line)?.line ?? lines.length, rules.find((r) => r > line) ?? lines.length);
|
|
2395
|
+
const titleHeading = headings.find((h) => h.level === 1);
|
|
2396
|
+
const start = titleHeading ? titleHeading.line + 1 : 0;
|
|
2397
|
+
const header = lines.slice(start, after(start - 1)).filter((l) => !/^\s*>/.test(l)).join("\n").trim();
|
|
2398
|
+
const gc = headings.find((h) => /^global constraints\b/i.test(h.text));
|
|
2399
|
+
const constraints = gc ? lines.slice(gc.line + 1, after(gc.line)).join("\n").trim() : "";
|
|
2400
|
+
const tasks = [];
|
|
2401
|
+
for (const h of headings) {
|
|
2402
|
+
const m = /^Task\s+(\d+)\b\s*[:.)\-–—]?\s*(.*)$/i.exec(h.text);
|
|
2403
|
+
if (!m) continue;
|
|
2404
|
+
const end = headings.find((o) => o.line > h.line && o.level <= h.level)?.line ?? lines.length;
|
|
2405
|
+
const body = lines.slice(h.line, end).join("\n").replace(/(\n\s*(?:---|\*\*\*|___)\s*)+$/, "").trimEnd();
|
|
2406
|
+
const unfenced = lines.slice(h.line, end).filter((_, j) => !fenced[h.line + j]).join("\n");
|
|
2407
|
+
const tier = /^\s*[-*]?\s*\*\*Worker:\*\*\s*`?([\w-]+)`?/m.exec(unfenced)?.[1]?.toLowerCase();
|
|
2408
|
+
tasks.push({ step: Number(m[1]), title: m[2].trim(), text: body, ...tier ? { tier } : {} });
|
|
2409
|
+
}
|
|
2410
|
+
return { file, title: titleHeading?.text ?? "", header, constraints, tasks };
|
|
2411
|
+
}
|
|
2412
|
+
function loadPlan(file) {
|
|
2413
|
+
const abs = path14.resolve(file);
|
|
2414
|
+
if (!fs14.existsSync(abs) || !fs14.statSync(abs).isFile()) throw new UserError(`plan not found: ${file}`);
|
|
2415
|
+
const plan = parsePlan(fs14.readFileSync(abs, "utf8"), fs14.realpathSync(abs));
|
|
2416
|
+
if (!plan.tasks.length) throw new UserError(`${file} has no "Task N" headings (see pitroom-writing-plans)`);
|
|
2417
|
+
return plan;
|
|
2418
|
+
}
|
|
2419
|
+
function planTask(plan, step) {
|
|
2420
|
+
const t = plan.tasks.find((x) => x.step === step);
|
|
2421
|
+
if (!t) throw new UserError(`no Task ${step} in ${plan.file}; it has ${plan.tasks.map((x) => `Task ${x.step}`).join(", ")}`);
|
|
2422
|
+
return t;
|
|
2423
|
+
}
|
|
2424
|
+
function brief(plan, task) {
|
|
2425
|
+
return `${[
|
|
2426
|
+
`# ${plan.title || planName(plan.file)}`,
|
|
2427
|
+
plan.header,
|
|
2428
|
+
"## Global Constraints",
|
|
2429
|
+
plan.constraints || "(none stated in the plan)",
|
|
2430
|
+
task.text
|
|
2431
|
+
].filter(Boolean).join("\n\n")}
|
|
2432
|
+
`;
|
|
2433
|
+
}
|
|
2434
|
+
var planName = (file) => path14.basename(file).replace(/\.md$/i, "");
|
|
2435
|
+
|
|
2436
|
+
// src/core/style.ts
|
|
2437
|
+
var enabled = () => {
|
|
2438
|
+
if (process.env.NO_COLOR) return false;
|
|
2439
|
+
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") return true;
|
|
2440
|
+
return !!process.stdout.isTTY && process.env.TERM !== "dumb";
|
|
2068
2441
|
};
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2442
|
+
var wrap = (open, close) => (text) => enabled() ? `\x1B[${open}m${text}\x1B[${close}m` : text;
|
|
2443
|
+
var bold = wrap(1, 22);
|
|
2444
|
+
var dim = wrap(2, 22);
|
|
2445
|
+
var red = wrap(31, 39);
|
|
2446
|
+
var green = wrap(32, 39);
|
|
2447
|
+
var yellow = wrap(33, 39);
|
|
2448
|
+
var blue = wrap(34, 39);
|
|
2449
|
+
var cyan = wrap(36, 39);
|
|
2450
|
+
|
|
2451
|
+
// src/core/group.ts
|
|
2452
|
+
var retryShell = (platform = process.platform) => platform === "win32" ? "powershell" : "sh";
|
|
2453
|
+
var quoteFor = (shell) => (s) => /^[\w./:@=\\-]+$/.test(s) && !(shell === "sh" && s.includes("\\")) ? s : shell === "sh" ? `'${s.replace(/'/g, `'\\''`)}'` : `'${s.replace(/'/g, "''")}'`;
|
|
2454
|
+
function retryCommands(metas, shell = retryShell()) {
|
|
2455
|
+
const quote = quoteFor(shell);
|
|
2456
|
+
return metas.filter((m) => ["failed", "timeout", "stopped"].includes(m.state) && !m.auditOf && !m.reviewOf && !m.parent && !m.fixOf).map((m) => {
|
|
2457
|
+
const where = ["-d", quote(m.dir), ...m.group ? ["-g", quote(m.group)] : []];
|
|
2458
|
+
if (m.plan) return ["pitroom run", m.mode === "write" ? "-w" : "-i", "--plan", quote(m.plan.file), "--step", String(m.plan.step), ...where, "--bg"].join(" ");
|
|
2459
|
+
const mode = m.mode === "isolate" ? ["-i"] : m.mode === "write" ? ["-w"] : [];
|
|
2460
|
+
const end = shell === "sh" || m.task.startsWith("-") ? ["--"] : [];
|
|
2461
|
+
return ["pitroom run", ...mode, ...where, "--bg", ...end, quote(m.task)].join(" ");
|
|
2462
|
+
});
|
|
2073
2463
|
}
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2464
|
+
|
|
2465
|
+
// src/core/eval.ts
|
|
2466
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
2467
|
+
import fs15 from "node:fs";
|
|
2468
|
+
var KINDS = ["definition", "count", "set"];
|
|
2469
|
+
function loadEval(file) {
|
|
2470
|
+
let raw;
|
|
2471
|
+
try {
|
|
2472
|
+
raw = JSON.parse(fs15.readFileSync(file, "utf8"));
|
|
2473
|
+
} catch (e) {
|
|
2474
|
+
throw new UserError(`cannot read ${file}: ${e.message}`);
|
|
2475
|
+
}
|
|
2476
|
+
const list2 = raw?.questions;
|
|
2477
|
+
if (!Array.isArray(list2) || !list2.length) throw new UserError(`${file}: expected {"questions": [{"id", "task", "kind", "truth"}, \u2026]}`);
|
|
2478
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2479
|
+
return list2.map((q, i) => {
|
|
2480
|
+
const where = `${file}: question ${i + 1}`;
|
|
2481
|
+
if (!q || typeof q !== "object") throw new UserError(`${where} is not an object`);
|
|
2482
|
+
const id = typeof q.id === "string" && q.id.trim() ? q.id.trim() : `q${i + 1}`;
|
|
2483
|
+
if (ids.has(id)) throw new UserError(`${where}: the id "${id}" is used twice`);
|
|
2484
|
+
ids.add(id);
|
|
2485
|
+
if (typeof q.task !== "string" || !q.task.trim()) throw new UserError(`${where} (${id}) has no task`);
|
|
2486
|
+
if (!KINDS.includes(q.kind)) throw new UserError(`${where} (${id}): kind must be one of ${KINDS.join(", ")}`);
|
|
2487
|
+
const t = q.truth;
|
|
2488
|
+
const grep = t && typeof t === "object" && !Array.isArray(t) && typeof t.grep === "string" && t.grep;
|
|
2489
|
+
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");
|
|
2490
|
+
if (!grep && !literal) {
|
|
2491
|
+
const want = { definition: '"path:line"', count: "a whole number", set: "a list of paths" }[q.kind];
|
|
2492
|
+
throw new UserError(`${where} (${id}): truth must be ${want} or {"grep": "pattern", \u2026}`);
|
|
2086
2493
|
}
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2494
|
+
return { id, task: q.task.trim(), kind: q.kind, truth: t };
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
var clean = (p) => p.replace(/\\/g, "/").replace(/^\.?\//, "");
|
|
2498
|
+
function gitGrep(root, flags, t) {
|
|
2499
|
+
const r = spawnSync7("git", ["-C", root, "grep", ...flags, t.regex ? "-E" : "-F", ...t.word ? ["-w"] : [], "-e", t.grep, "--", t.dir ?? "."], {
|
|
2500
|
+
encoding: "utf8",
|
|
2501
|
+
maxBuffer: 64 * 1024 * 1024
|
|
2502
|
+
});
|
|
2503
|
+
if (r.status === 1) return [];
|
|
2504
|
+
if (r.status !== 0) throw new UserError(`git grep "${t.grep}" failed: ${(r.stderr || r.error?.message || "").trim()}`);
|
|
2505
|
+
const exclude = t.exclude ? new RegExp(t.exclude) : void 0;
|
|
2506
|
+
return r.stdout.split("\n").filter(Boolean).filter((l) => !exclude?.test(l.split(":")[0]));
|
|
2507
|
+
}
|
|
2508
|
+
function truthOf(q, root) {
|
|
2509
|
+
const t = q.truth;
|
|
2510
|
+
if (t && typeof t === "object" && !Array.isArray(t)) {
|
|
2511
|
+
if (!root) throw new UserError(`question ${q.id}: a "grep" truth needs a git repository`);
|
|
2512
|
+
if (q.kind === "definition") {
|
|
2513
|
+
const hit = gitGrep(root, ["-n"], t).map((l) => /^(.*?):(\d+):/.exec(l)).find(Boolean);
|
|
2514
|
+
if (!hit) throw new UserError(`question ${q.id}: git grep found no definition for "${t.grep}"`);
|
|
2515
|
+
return { path: clean(hit[1]), line: Number(hit[2]) };
|
|
2098
2516
|
}
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2517
|
+
const files = [...new Set(gitGrep(root, ["-l"], t).map(clean))];
|
|
2518
|
+
return q.kind === "count" ? { count: files.length } : { files };
|
|
2519
|
+
}
|
|
2520
|
+
if (q.kind === "definition") {
|
|
2521
|
+
const m = /^(.*):(\d+)$/.exec(t);
|
|
2522
|
+
return { path: clean(m[1]), line: Number(m[2]) };
|
|
2523
|
+
}
|
|
2524
|
+
if (q.kind === "count") return { count: t };
|
|
2525
|
+
return { files: [...new Set(t.map(clean))] };
|
|
2526
|
+
}
|
|
2527
|
+
function answerParts(answer) {
|
|
2528
|
+
const text = answer.replace(/`/g, "");
|
|
2529
|
+
const s = /SUMMARY:\s*([\s\S]*?)(?=^\s*(?:DETAILS|FILES CHANGED|VERIFICATION|OPEN ISSUES):|$(?![\s\S]))/m.exec(text);
|
|
2530
|
+
const d = /^\s*DETAILS:\s*([\s\S]*?)(?=^\s*(?:FILES CHANGED|VERIFICATION|OPEN ISSUES):|$(?![\s\S]))/m.exec(text);
|
|
2531
|
+
const summary = s ? s[1] : text.split(/^\s*(?:DETAILS|FILES CHANGED|VERIFICATION|OPEN ISSUES):/m)[0];
|
|
2532
|
+
return { summary: summary.trim(), details: (d?.[1] ?? "").trim() };
|
|
2533
|
+
}
|
|
2534
|
+
function pathsIn(text) {
|
|
2535
|
+
const found = text.replace(/\\/g, "/").match(/[A-Za-z0-9_.@\-/]+\.[A-Za-z0-9]{1,8}(?::\d+)?/g) ?? [];
|
|
2536
|
+
return [...new Set(found.map((p) => clean(p.replace(/:\d+$/, ""))))];
|
|
2537
|
+
}
|
|
2538
|
+
function scoreAnswer(q, truth, answer, root) {
|
|
2539
|
+
const prefix = root ? `${clean(root).replace(/\/$/, "")}/` : "";
|
|
2540
|
+
const text = prefix ? answer.replace(/\\/g, "/").split(prefix).join("") : answer;
|
|
2541
|
+
const { summary, details } = answerParts(text);
|
|
2542
|
+
if (!summary && !details) return { score: 0, got: "(no answer)" };
|
|
2543
|
+
if ("line" in truth) {
|
|
2544
|
+
const flat = summary.replace(/\s+/g, " ");
|
|
2545
|
+
if (new RegExp(`(^|[^\\w./-])\\.?/?${escape2(truth.path)}:${truth.line}(?!\\d)`).test(flat)) return { score: 1, got: `${truth.path}:${truth.line}` };
|
|
2546
|
+
const named = pathsIn(flat).includes(truth.path);
|
|
2547
|
+
return { score: named ? 0.5 : 0, got: flat.slice(0, 120) };
|
|
2548
|
+
}
|
|
2549
|
+
if ("count" in truth) {
|
|
2550
|
+
const n = /\d[\d,]*/.exec(summary);
|
|
2551
|
+
const got2 = n ? Number(n[0].replace(/,/g, "")) : void 0;
|
|
2552
|
+
return { score: got2 === truth.count ? 1 : 0, got: got2 === void 0 ? "(no number)" : String(got2) };
|
|
2553
|
+
}
|
|
2554
|
+
let got = pathsIn(summary);
|
|
2555
|
+
if (!got.length) got = pathsIn(details);
|
|
2556
|
+
const hit = got.filter((p) => truth.files.includes(p)).length;
|
|
2557
|
+
const precision = got.length ? hit / got.length : 0;
|
|
2558
|
+
const recall = truth.files.length ? hit / truth.files.length : got.length ? 0 : 1;
|
|
2559
|
+
const f1 = precision + recall ? 2 * precision * recall / (precision + recall) : 0;
|
|
2560
|
+
return { score: !truth.files.length && !got.length ? 1 : f1, got: `${hit}/${truth.files.length} found, ${got.length - hit} extra` };
|
|
2561
|
+
}
|
|
2562
|
+
var escape2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2563
|
+
var mean = (a) => a.length ? a.reduce((x, y) => x + y, 0) / a.length : 0;
|
|
2564
|
+
var median = (a) => {
|
|
2565
|
+
const s = a.filter((x) => x !== void 0).sort((x, y) => x - y);
|
|
2566
|
+
return s.length ? s[Math.floor(s.length / 2)] : void 0;
|
|
2567
|
+
};
|
|
2568
|
+
function summarize(rows) {
|
|
2569
|
+
const by = /* @__PURE__ */ new Map();
|
|
2570
|
+
for (const r of rows) by.set(r.worker, [...by.get(r.worker) ?? [], r]);
|
|
2571
|
+
return [...by.entries()].map(([worker, rs]) => ({
|
|
2572
|
+
worker,
|
|
2573
|
+
questions: rs.length,
|
|
2574
|
+
failed: rs.filter((r) => r.state !== "done").length,
|
|
2575
|
+
score: mean(rs.map((r) => r.score)),
|
|
2576
|
+
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))])),
|
|
2577
|
+
medianSeconds: median(rs.map((r) => r.seconds)),
|
|
2578
|
+
medianTokens: median(rs.map((r) => r.tokens))
|
|
2579
|
+
})).sort((a, b) => b.score - a.score || (a.medianSeconds ?? 1e9) - (b.medianSeconds ?? 1e9));
|
|
2102
2580
|
}
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2581
|
+
|
|
2582
|
+
// src/vcs/guard.ts
|
|
2583
|
+
import fs16 from "node:fs";
|
|
2584
|
+
import path15 from "node:path";
|
|
2585
|
+
var VERSION = 2;
|
|
2586
|
+
var WINDOWS = process.platform === "win32";
|
|
2587
|
+
var fwd = (p) => WINDOWS ? p.replace(/\\/g, "/") : p;
|
|
2588
|
+
var SHIM = `#!/bin/sh
|
|
2589
|
+
# pitroom git guard v${VERSION}. Blocks git commands that change history, refs, the
|
|
2590
|
+
# index or discard work. The primary agent reviews and commits, not the worker.
|
|
2591
|
+
set -f
|
|
2592
|
+
real="\${PITROOM_REAL_GIT:-}"
|
|
2593
|
+
if [ -z "$real" ] || [ ! -x "$real" ]; then
|
|
2594
|
+
echo "pitroom: git guard cannot find the real git" >&2
|
|
2595
|
+
exit 127
|
|
2596
|
+
fi
|
|
2597
|
+
block() {
|
|
2598
|
+
echo "pitroom: 'git $1' is blocked for workers (the primary agent reviews and commits)" >&2
|
|
2599
|
+
exit 1
|
|
2600
|
+
}
|
|
2601
|
+
sub=""; skip=""; after=""
|
|
2602
|
+
for a do
|
|
2603
|
+
if [ -n "$sub" ]; then after="$after $a"; continue; fi
|
|
2604
|
+
case "$skip" in
|
|
2605
|
+
c) skip=""; case "$a" in [Aa][Ll][Ii][Aa][Ss].*) block "-c $a" ;; esac; continue ;;
|
|
2606
|
+
v) skip=""; continue ;;
|
|
2607
|
+
esac
|
|
2608
|
+
case "$a" in
|
|
2609
|
+
-c|--config-env) skip=c ;;
|
|
2610
|
+
-C|--git-dir|--work-tree|--namespace|--super-prefix|--exec-path) skip=v ;;
|
|
2611
|
+
-*) ;;
|
|
2612
|
+
*) sub="$a" ;;
|
|
2613
|
+
esac
|
|
2614
|
+
done
|
|
2615
|
+
[ -z "$sub" ] && exec "$real" "$@"
|
|
2616
|
+
|
|
2617
|
+
# Resolve aliases (also those injected via GIT_CONFIG_* env); shell aliases are refused.
|
|
2618
|
+
depth=0
|
|
2619
|
+
while def=$("$real" config --get "alias.$sub" 2>/dev/null) && [ -n "$def" ]; do
|
|
2620
|
+
depth=$((depth+1)); [ "$depth" -gt 5 ] && block "$sub (alias loop)"
|
|
2621
|
+
case "$def" in '!'*) block "$sub (shell alias)" ;; -*) block "$sub (alias with options)" ;; esac
|
|
2622
|
+
next=\${def%% *}
|
|
2623
|
+
[ "$next" != "$def" ] && after=" \${def#* }$after"
|
|
2624
|
+
sub="$next"
|
|
2625
|
+
done
|
|
2626
|
+
|
|
2627
|
+
has() { for f in "$@"; do case " $after " in *" $f "*|*" $f="*) return 0 ;; esac; done; return 1; }
|
|
2628
|
+
count() { n=0; for w in $after; do case "$w" in -*) ;; *) n=$((n+1)) ;; esac; done; echo "$n"; }
|
|
2629
|
+
|
|
2630
|
+
case "$sub" in
|
|
2631
|
+
commit|push|pull|reset|checkout|restore|clean|stash|rebase|merge|switch|cherry-pick|revert|am|\\
|
|
2632
|
+
worktree|update-ref|update-index|filter-branch|filter-repo|gc|prune|replace|notes|add|rm|mv|\\
|
|
2633
|
+
sparse-checkout|read-tree|commit-tree|write-tree|fast-import|bisect)
|
|
2634
|
+
block "$sub" ;;
|
|
2635
|
+
branch)
|
|
2636
|
+
has -d -D --delete -m -M --move -c -C --copy -f --force -u --set-upstream-to --unset-upstream --edit-description && block "branch$after"
|
|
2637
|
+
[ "$(count)" -gt 0 ] && ! has -l --list --contains --no-contains --merged --no-merged --points-at && block "branch$after" ;;
|
|
2638
|
+
tag)
|
|
2639
|
+
has -d --delete -a --annotate -s --sign -u --local-user -f --force -m --message -F --file && block "tag$after"
|
|
2640
|
+
[ "$(count)" -gt 0 ] && ! has -l --list --contains --no-contains --merged --no-merged --points-at && block "tag$after" ;;
|
|
2641
|
+
config)
|
|
2642
|
+
has --unset --unset-all --add --replace-all --rename-section --remove-section -e --edit set unset rename-section remove-section edit && block "config$after"
|
|
2643
|
+
[ "$(count)" -ge 2 ] && ! has get --get --get-all --get-regexp --get-urlmatch && block "config$after" ;;
|
|
2644
|
+
remote)
|
|
2645
|
+
for w in $after; do case "$w" in add|remove|rm|rename|set-url|set-head|set-branches|prune|update) block "remote $w" ;; esac; done ;;
|
|
2646
|
+
fetch)
|
|
2647
|
+
for w in $after; do case "$w" in -*) ;; *:*) block "fetch $w (writes a local ref)" ;; esac; done ;;
|
|
2648
|
+
apply)
|
|
2649
|
+
has --index --cached -3 --3way && block "apply$after" ;;
|
|
2650
|
+
reflog)
|
|
2651
|
+
has expire delete && block "reflog$after" ;;
|
|
2652
|
+
symbolic-ref)
|
|
2653
|
+
has -d --delete && block "symbolic-ref$after"
|
|
2654
|
+
[ "$(count)" -ge 2 ] && block "symbolic-ref$after" ;;
|
|
2655
|
+
submodule)
|
|
2656
|
+
for w in $after; do case "$w" in -*) ;; status|summary) break ;; *) block "submodule $w" ;; esac; done ;;
|
|
2657
|
+
esac
|
|
2658
|
+
exec "$real" "$@"
|
|
2659
|
+
`;
|
|
2660
|
+
function shimDir() {
|
|
2661
|
+
return path15.join(home(), "shim", `v${VERSION}`);
|
|
2662
|
+
}
|
|
2663
|
+
function findRealGit(skip) {
|
|
2664
|
+
const same = (a, b) => WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
2665
|
+
for (const dir of (process.env.PATH ?? "").split(path15.delimiter).filter(Boolean)) {
|
|
2666
|
+
if (same(path15.resolve(dir), skip)) continue;
|
|
2667
|
+
const p = path15.join(dir, WINDOWS ? "git.exe" : "git");
|
|
2668
|
+
try {
|
|
2669
|
+
fs16.accessSync(p, fs16.constants.X_OK);
|
|
2670
|
+
if (fs16.statSync(p).isFile()) return p;
|
|
2671
|
+
} catch {
|
|
2672
|
+
}
|
|
2107
2673
|
}
|
|
2108
|
-
|
|
2109
|
-
|
|
2674
|
+
return void 0;
|
|
2675
|
+
}
|
|
2676
|
+
function findGitSh(realGit) {
|
|
2677
|
+
let dir = path15.dirname(realGit);
|
|
2678
|
+
for (let i = 0; i < 4; i++) {
|
|
2679
|
+
for (const rel of ["bin/sh.exe", "usr/bin/sh.exe"]) {
|
|
2680
|
+
const p = path15.join(dir, rel);
|
|
2681
|
+
if (fs16.existsSync(p)) return p;
|
|
2682
|
+
}
|
|
2683
|
+
const up = path15.dirname(dir);
|
|
2684
|
+
if (up === dir) break;
|
|
2685
|
+
dir = up;
|
|
2110
2686
|
}
|
|
2111
|
-
|
|
2112
|
-
return typeof v === type;
|
|
2687
|
+
return void 0;
|
|
2113
2688
|
}
|
|
2114
|
-
var
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
const
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
var readIn = (v) => v === "auto" || v === "snapshot" || v === "project" ? v : void 0;
|
|
2134
|
-
var list = (s) => s?.split(",").map((x) => x.trim()).filter(Boolean);
|
|
2135
|
-
function setting(flag, env, conf, fallback) {
|
|
2136
|
-
if (flag !== void 0) return { value: flag, source: "flag" };
|
|
2137
|
-
if (env !== void 0) return { value: env, source: "env" };
|
|
2138
|
-
if (conf !== void 0) return { value: conf, source: "config" };
|
|
2139
|
-
return { value: fallback, source: "default" };
|
|
2689
|
+
var cmdShim = (sh) => `@echo off\r
|
|
2690
|
+
set "MSYS2_ARG_CONV_EXCL=*"\r
|
|
2691
|
+
"${sh}" "%~dp0git" %*\r
|
|
2692
|
+
exit /b %errorlevel%\r
|
|
2693
|
+
`;
|
|
2694
|
+
var HOOK_VERSION = 1;
|
|
2695
|
+
var REF_HOOK = `#!/bin/sh
|
|
2696
|
+
# pitroom git guard (layer 2, v${HOOK_VERSION}): refuse ref updates made by workers.
|
|
2697
|
+
[ "$1" = prepared ] || exit 0
|
|
2698
|
+
echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
|
|
2699
|
+
exit 1
|
|
2700
|
+
`;
|
|
2701
|
+
var hooksDir = () => path15.join(home(), "git-hooks", `v${HOOK_VERSION}`);
|
|
2702
|
+
function writeIfChanged(file, content) {
|
|
2703
|
+
if (fs16.existsSync(file) && fs16.readFileSync(file, "utf8") === content) return;
|
|
2704
|
+
fs16.mkdirSync(path15.dirname(file), { recursive: true });
|
|
2705
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
2706
|
+
fs16.writeFileSync(tmp, content, { mode: 493 });
|
|
2707
|
+
renameOver(tmp, file);
|
|
2140
2708
|
}
|
|
2141
|
-
function
|
|
2142
|
-
const
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
primary: setting(void 0, e.PITROOM_PRIMARY, c.primary, "sonnet"),
|
|
2151
|
-
price: setting(void 0, e.PITROOM_PRICE, c.price, void 0),
|
|
2152
|
-
link: setting(void 0, void 0, c.link, []),
|
|
2153
|
-
web: setting(void 0, void 0, c.web, false),
|
|
2154
|
-
maxParallel: clampParallel(setting(void 0, positiveInt(e.PITROOM_MAX_PARALLEL), positiveInt(c.maxParallel), DEFAULT_PARALLEL)),
|
|
2155
|
-
models: setting(void 0, void 0, c.models, {}),
|
|
2156
|
-
tiers: setting(void 0, void 0, c.tiers, {}),
|
|
2157
|
-
costs: setting(void 0, void 0, c.costs, {}),
|
|
2158
|
-
audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0),
|
|
2159
|
-
readIn: setting(void 0, readIn(e.PITROOM_READ_IN), readIn(c.readIn), "auto"),
|
|
2160
|
-
cacheDays: setting(void 0, days(e.PITROOM_CACHE_DAYS), days(c.cacheDays), 7),
|
|
2161
|
-
countRateLimits: setting(void 0, void 0, c.countRateLimits, false),
|
|
2162
|
-
mcpDash: setting(void 0, flag01(e.PITROOM_MCP_DASH), c.mcpDash, true),
|
|
2163
|
-
notify: setting(void 0, flag01(e.PITROOM_NOTIFY), c.notify, false),
|
|
2164
|
-
notifyCommand: setting(void 0, e.PITROOM_NOTIFY_COMMAND?.trim() || void 0, c.notifyCommand?.trim() || void 0, void 0),
|
|
2165
|
-
notifyAfter: setting(void 0, days(e.PITROOM_NOTIFY_AFTER), days(c.notifyAfter), 15),
|
|
2166
|
-
workerPrices: setting(void 0, void 0, c.workerPrices, {}),
|
|
2167
|
-
priceFeed: setting(void 0, flag01(e.PITROOM_PRICE_FEED), c.priceFeed, false),
|
|
2168
|
-
priceFeedUrl: setting(void 0, e.PITROOM_PRICE_FEED_URL?.trim() || void 0, c.priceFeedUrl?.trim() || void 0, "https://models.dev/api.json"),
|
|
2169
|
-
priceFeedHours: setting(void 0, positiveInt(e.PITROOM_PRICE_FEED_HOURS), positiveInt(c.priceFeedHours), 24)
|
|
2170
|
-
};
|
|
2709
|
+
function pathKey(env) {
|
|
2710
|
+
const keys = Object.keys(env).filter((k) => k.toLowerCase() === "path");
|
|
2711
|
+
return keys.includes("PATH") ? "PATH" : keys[0] ?? "PATH";
|
|
2712
|
+
}
|
|
2713
|
+
function onePath(env) {
|
|
2714
|
+
const key = pathKey(env);
|
|
2715
|
+
const out = { ...env };
|
|
2716
|
+
for (const k of Object.keys(out)) if (k !== key && k.toLowerCase() === "path") delete out[k];
|
|
2717
|
+
return out;
|
|
2171
2718
|
}
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
const out = {};
|
|
2180
|
-
if (!raw || typeof raw !== "object") return out;
|
|
2181
|
-
for (const [provider, p] of Object.entries(raw)) {
|
|
2182
|
-
for (const [id, m] of Object.entries(p?.models ?? {})) {
|
|
2183
|
-
const c = m?.cost;
|
|
2184
|
-
const ok = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
2185
|
-
if (!c || !ok(c.input) || !ok(c.output)) continue;
|
|
2186
|
-
out[`${provider}/${id}`] = [c.input, c.output, ok(c.cache_read) ? c.cache_read : c.input / 10];
|
|
2187
|
-
}
|
|
2719
|
+
function withGitConfig(env, entries) {
|
|
2720
|
+
const out = { ...env };
|
|
2721
|
+
let n = Number(env.GIT_CONFIG_COUNT) || 0;
|
|
2722
|
+
for (const [key, value] of entries) {
|
|
2723
|
+
out[`GIT_CONFIG_KEY_${n}`] = key;
|
|
2724
|
+
out[`GIT_CONFIG_VALUE_${n}`] = value;
|
|
2725
|
+
n++;
|
|
2188
2726
|
}
|
|
2727
|
+
out.GIT_CONFIG_COUNT = String(n);
|
|
2189
2728
|
return out;
|
|
2190
2729
|
}
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2730
|
+
function guardEnv(env) {
|
|
2731
|
+
const quiet = {
|
|
2732
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
2733
|
+
GIT_EDITOR: "true",
|
|
2734
|
+
GIT_SEQUENCE_EDITOR: "true",
|
|
2735
|
+
GIT_PAGER: "cat",
|
|
2736
|
+
PAGER: "cat"
|
|
2737
|
+
};
|
|
2738
|
+
writeIfChanged(path15.join(hooksDir(), "reference-transaction"), REF_HOOK);
|
|
2739
|
+
env = withGitConfig(onePath(env), [
|
|
2740
|
+
["core.hooksPath", fwd(hooksDir())],
|
|
2741
|
+
["url.pitroom-push-blocked://.pushInsteadOf", ""]
|
|
2742
|
+
]);
|
|
2743
|
+
const dir = shimDir();
|
|
2744
|
+
const real = findRealGit(dir);
|
|
2745
|
+
if (!real) return { ...env, ...quiet };
|
|
2746
|
+
let sh;
|
|
2747
|
+
if (WINDOWS) {
|
|
2748
|
+
sh = findGitSh(real);
|
|
2749
|
+
if (!sh) return { ...env, ...quiet };
|
|
2750
|
+
writeIfChanged(path15.join(dir, "git.cmd"), cmdShim(sh));
|
|
2751
|
+
}
|
|
2752
|
+
writeIfChanged(path15.join(dir, "git"), SHIM);
|
|
2753
|
+
const key = pathKey(env);
|
|
2754
|
+
return { ...env, ...quiet, PITROOM_REAL_GIT: fwd(real), [key]: `${dir}${path15.delimiter}${env[key] ?? ""}` };
|
|
2755
|
+
}
|
|
2756
|
+
function shimReady() {
|
|
2757
|
+
const env = guardEnv({ ...process.env });
|
|
2758
|
+
return Boolean(env[pathKey(env)]?.startsWith(shimDir()));
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
// src/core/slots.ts
|
|
2762
|
+
import crypto2 from "node:crypto";
|
|
2763
|
+
import fs17 from "node:fs";
|
|
2764
|
+
import path16 from "node:path";
|
|
2765
|
+
var slotsDir = () => path16.join(home(), "slots");
|
|
2766
|
+
var locksDir = () => path16.join(home(), "locks");
|
|
2767
|
+
var STARTUP_GRACE_MS = 3e4;
|
|
2768
|
+
function holderActive(runId, self) {
|
|
2769
|
+
if (!runId || runId === self) return false;
|
|
2194
2770
|
try {
|
|
2195
|
-
const
|
|
2196
|
-
if (
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
cached2 = { file, mtimeMs, catalog: catalog5 };
|
|
2200
|
-
return catalog5;
|
|
2771
|
+
const m = freshMeta(runId);
|
|
2772
|
+
if (!isActive(m.state)) return false;
|
|
2773
|
+
if (m.pid) return isAlive(m.pid);
|
|
2774
|
+
return Date.now() - Date.parse(m.startedAt) < STARTUP_GRACE_MS;
|
|
2201
2775
|
} catch {
|
|
2202
|
-
return
|
|
2776
|
+
return false;
|
|
2203
2777
|
}
|
|
2204
2778
|
}
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
if (
|
|
2212
|
-
const keys = [id, ...[...OWN[backend] ?? [], "anthropic", "openai", "google"].map((p) => `${p}/${id}`)];
|
|
2213
|
-
const any = keys.find((k) => catalog5.models[k]) ?? Object.keys(catalog5.models).filter((k) => k.endsWith(`/${id}`)).sort()[0];
|
|
2214
|
-
const triple = any ? catalog5.models[any] : void 0;
|
|
2215
|
-
if (any && triple && triple.length === 3 && triple.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
2216
|
-
const [input, output, cachedInput] = triple;
|
|
2217
|
-
return { input, output, cachedInput, key: any };
|
|
2218
|
-
}
|
|
2779
|
+
function tryClaim(file, runId) {
|
|
2780
|
+
fs17.mkdirSync(path16.dirname(file), { recursive: true });
|
|
2781
|
+
try {
|
|
2782
|
+
fs17.writeFileSync(file, runId, { flag: "wx" });
|
|
2783
|
+
return true;
|
|
2784
|
+
} catch (e) {
|
|
2785
|
+
if (e.code !== "EEXIST") throw e;
|
|
2219
2786
|
}
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
};
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
if (custom && custom.length >= 2 && custom.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
2234
|
-
return { name: "custom", input: custom[0], output: custom[1], cachedInput: custom[2] ?? custom[0] / 10 };
|
|
2787
|
+
let owner = "";
|
|
2788
|
+
try {
|
|
2789
|
+
owner = fs17.readFileSync(file, "utf8").trim();
|
|
2790
|
+
} catch {
|
|
2791
|
+
}
|
|
2792
|
+
if (owner === runId) return true;
|
|
2793
|
+
if (holderActive(owner, runId)) return false;
|
|
2794
|
+
fs17.rmSync(file, { force: true });
|
|
2795
|
+
try {
|
|
2796
|
+
fs17.writeFileSync(file, runId, { flag: "wx" });
|
|
2797
|
+
return true;
|
|
2798
|
+
} catch {
|
|
2799
|
+
return false;
|
|
2235
2800
|
}
|
|
2236
|
-
const named = eff.primary.value;
|
|
2237
|
-
const preset = PRESETS[named.toLowerCase()];
|
|
2238
|
-
if (preset) return preset;
|
|
2239
|
-
const c = eff.priceFeed.value ? catalogPrice("", named) : void 0;
|
|
2240
|
-
return c ? { name: named, source: "catalog", input: c.input, output: c.output, cachedInput: c.cachedInput } : PRESETS.sonnet;
|
|
2241
2801
|
}
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2802
|
+
function releaseIfOwner(file, runId) {
|
|
2803
|
+
if (!file) return;
|
|
2804
|
+
try {
|
|
2805
|
+
if (fs17.readFileSync(file, "utf8").trim() === runId) fs17.rmSync(file, { force: true });
|
|
2806
|
+
} catch {
|
|
2807
|
+
}
|
|
2247
2808
|
}
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2809
|
+
function tryAcquireSlot(runId, maxParallel) {
|
|
2810
|
+
for (let n = 0; n < Math.max(1, maxParallel); n++) {
|
|
2811
|
+
const file = path16.join(slotsDir(), `slot-${n}`);
|
|
2812
|
+
if (tryClaim(file, runId)) return file;
|
|
2813
|
+
}
|
|
2814
|
+
return void 0;
|
|
2253
2815
|
}
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2816
|
+
var releaseSlot = (file, runId) => releaseIfOwner(file, runId);
|
|
2817
|
+
function slotHolders() {
|
|
2818
|
+
if (!fs17.existsSync(slotsDir())) return [];
|
|
2819
|
+
return fs17.readdirSync(slotsDir()).map((f) => {
|
|
2257
2820
|
try {
|
|
2258
|
-
return
|
|
2821
|
+
return fs17.readFileSync(path16.join(slotsDir(), f), "utf8").trim();
|
|
2259
2822
|
} catch {
|
|
2260
|
-
return
|
|
2823
|
+
return "";
|
|
2261
2824
|
}
|
|
2262
|
-
}).filter((
|
|
2263
|
-
}
|
|
2264
|
-
function totals(entries) {
|
|
2265
|
-
const t = entries.reduce(
|
|
2266
|
-
(a, e) => ({
|
|
2267
|
-
runs: a.runs + 1,
|
|
2268
|
-
tokens: a.tokens + e.tokens,
|
|
2269
|
-
returned: a.returned + e.returned,
|
|
2270
|
-
workerCost: a.workerCost + e.workerCost,
|
|
2271
|
-
saved: a.saved + e.saved
|
|
2272
|
-
}),
|
|
2273
|
-
{ runs: 0, tokens: 0, returned: 0, workerCost: 0, saved: 0 }
|
|
2274
|
-
);
|
|
2275
|
-
return { ...t, ratio: t.returned ? t.tokens / t.returned : 0 };
|
|
2825
|
+
}).filter((id) => holderActive(id, ""));
|
|
2276
2826
|
}
|
|
2277
|
-
var
|
|
2278
|
-
function
|
|
2279
|
-
const
|
|
2280
|
-
if (
|
|
2281
|
-
|
|
2282
|
-
|
|
2827
|
+
var lockFile = (repoRoot) => path16.join(locksDir(), `write-${crypto2.createHash("sha1").update(path16.resolve(repoRoot)).digest("hex").slice(0, 16)}`);
|
|
2828
|
+
function acquireWriteLock(repoRoot, runId) {
|
|
2829
|
+
const file = lockFile(repoRoot);
|
|
2830
|
+
if (tryClaim(file, runId)) return;
|
|
2831
|
+
let owner = "";
|
|
2832
|
+
try {
|
|
2833
|
+
owner = fs17.readFileSync(file, "utf8").trim();
|
|
2834
|
+
} catch {
|
|
2835
|
+
}
|
|
2836
|
+
throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
|
|
2283
2837
|
}
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2838
|
+
var releaseWriteLock = (repoRoot, runId) => releaseIfOwner(lockFile(repoRoot), runId);
|
|
2839
|
+
|
|
2840
|
+
// src/core/secrets.ts
|
|
2841
|
+
import { execFileSync } from "node:child_process";
|
|
2842
|
+
import fs18 from "node:fs";
|
|
2843
|
+
import path17 from "node:path";
|
|
2844
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
2845
|
+
var MAX_VISITED = 2e4;
|
|
2846
|
+
var MAX_DEPTH = 4;
|
|
2847
|
+
var TEMPLATE = /\.(example|sample|template|dist|defaults?|tpl)$/i;
|
|
2848
|
+
function looksSecret(file) {
|
|
2849
|
+
const name = file.toLowerCase();
|
|
2850
|
+
if (TEMPLATE.test(name)) return false;
|
|
2851
|
+
if (name === ".env" || name.startsWith(".env.") || name.endsWith(".env")) return true;
|
|
2852
|
+
return /\.(pem|p12|pfx|key|jks|keystore)$/.test(name) || /^id_(rsa|dsa|ecdsa|ed25519)$/.test(name) || name === ".netrc" || name === "credentials.json" || /^secrets?\.(json|ya?ml|toml)$/.test(name);
|
|
2287
2853
|
}
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
const
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2854
|
+
function findSecretFiles(dir) {
|
|
2855
|
+
const found = [];
|
|
2856
|
+
let visited = 0;
|
|
2857
|
+
const walk = (d, depth) => {
|
|
2858
|
+
if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
|
|
2859
|
+
let entries;
|
|
2860
|
+
try {
|
|
2861
|
+
entries = fs18.readdirSync(d, { withFileTypes: true });
|
|
2862
|
+
} catch {
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
for (const e of entries) {
|
|
2866
|
+
visited++;
|
|
2867
|
+
if (e.isDirectory()) {
|
|
2868
|
+
if (!SKIP_DIRS2.has(e.name)) walk(path17.join(d, e.name), depth + 1);
|
|
2869
|
+
} else if (e.isFile() && looksSecret(e.name)) {
|
|
2870
|
+
found.push(slash(path17.relative(dir, path17.join(d, e.name))));
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2295
2873
|
};
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
.
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
.val{font-size:26px;font-weight:650;letter-spacing:-.02em}
|
|
2307
|
-
.foot{font-size:11.5px;fill:#71717a}
|
|
2308
|
-
</style>
|
|
2309
|
-
<defs>
|
|
2310
|
-
<radialGradient id="o" cx="1" cy="0" r=".7"><stop offset="0" stop-color="#ff6a2b" stop-opacity=".24"/><stop offset="1" stop-color="#ff6a2b" stop-opacity="0"/></radialGradient>
|
|
2311
|
-
<radialGradient id="b" cx="0" cy="0" r=".7"><stop offset="0" stop-color="#38bdf8" stop-opacity=".16"/><stop offset="1" stop-color="#38bdf8" stop-opacity="0"/></radialGradient>
|
|
2312
|
-
<linearGradient id="m" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#ff6a2b"/><stop offset="1" stop-color="#ffa86b"/></linearGradient>
|
|
2313
|
-
<clipPath id="c"><rect width="${W}" height="${H}" rx="20"/></clipPath>
|
|
2314
|
-
</defs>
|
|
2315
|
-
<g clip-path="url(#c)"><rect width="${W}" height="${H}" fill="#0b0b0f"/><rect width="${W}" height="${H}" fill="url(#o)"/><rect width="${W}" height="${H}" fill="url(#b)"/></g>
|
|
2316
|
-
<rect x=".5" y=".5" width="${W - 1}" height="${H - 1}" rx="19.5" fill="none" stroke="#fff" stroke-opacity=".1"/>
|
|
2317
|
-
<rect x="${M}" y="28" width="38" height="38" rx="11" fill="url(#m)"/>
|
|
2318
|
-
<path d="M4 4h4v4H4zm8 0h4v4h-4zM8 8h4v4H8zm8 0h4v4h-4zM4 12h4v4H4zm8 0h4v4h-4zm-4 4h4v4H8zm8 0h4v4h-4z" fill="#fff" transform="translate(${M + 7} 35) scale(1)"/>
|
|
2319
|
-
<text x="${M + 52}" y="45" class="name">Pitroom</text><text x="${M + 52}" y="63" class="sub">Your agent's pit crew</text>
|
|
2320
|
-
<rect x="${W - M - pill}" y="34" width="${pill}" height="26" rx="13" fill="#fff" fill-opacity=".04" stroke="#fff" stroke-opacity=".14"/>
|
|
2321
|
-
<text x="${W - M - pill / 2}" y="51" class="pill" text-anchor="middle">${esc(when)}</text>
|
|
2322
|
-
<text x="${M}" y="112" class="lab">SAVED ON YOUR MAIN CODING AGENT</text>
|
|
2323
|
-
<text x="${M}" y="168" class="big">${esc(usd(t.saved))}</text>
|
|
2324
|
-
${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.round(t.ratio)}\xD7` : "\u2014", "Context compression")}${tile(2, String(t.runs), "Delegated tasks")}
|
|
2325
|
-
<text x="${M}" y="296" class="foot">Estimated against ${esc(primaryPrice().name)} pricing \xB7 npx pitroom</text>
|
|
2326
|
-
</svg>
|
|
2327
|
-
`;
|
|
2874
|
+
walk(dir, 0);
|
|
2875
|
+
return found.sort();
|
|
2876
|
+
}
|
|
2877
|
+
function findSecretFilesInTree(root, dir) {
|
|
2878
|
+
try {
|
|
2879
|
+
const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
2880
|
+
return out.split("\0").filter((f) => f && looksSecret(path17.basename(f)) && !path17.relative(dir, path17.join(root, f)).startsWith("..")).map((f) => slash(path17.relative(dir, path17.join(root, f)))).sort();
|
|
2881
|
+
} catch {
|
|
2882
|
+
return [];
|
|
2883
|
+
}
|
|
2328
2884
|
}
|
|
2885
|
+
function secretWarning(files, mode) {
|
|
2886
|
+
if (!files.length) return void 0;
|
|
2887
|
+
const shown = files.slice(0, 3).join(", ") + (files.length > 3 ? `, +${files.length - 3} more` : "");
|
|
2888
|
+
const where = mode === "isolate" ? "would be copied into the isolated copy" : "sit in the directory the worker runs in";
|
|
2889
|
+
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";
|
|
2890
|
+
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`;
|
|
2891
|
+
}
|
|
2892
|
+
var slash = (p) => p.split(path17.sep).join("/");
|
|
2329
2893
|
|
|
2330
2894
|
// src/core/process.ts
|
|
2331
2895
|
import { spawn } from "node:child_process";
|
|
2332
|
-
import
|
|
2896
|
+
import fs19 from "node:fs";
|
|
2333
2897
|
async function spawnWorker(inv, opts) {
|
|
2334
|
-
const out =
|
|
2335
|
-
const err =
|
|
2898
|
+
const out = fs19.openSync(opts.stdoutFile, "w");
|
|
2899
|
+
const err = fs19.openSync(opts.stderrFile, "w");
|
|
2336
2900
|
const res = { code: null, timedOut: false, stopped: false };
|
|
2337
2901
|
const child = spawn(inv.command, inv.args, {
|
|
2338
2902
|
cwd: opts.cwd,
|
|
@@ -2355,7 +2919,7 @@ async function spawnWorker(inv, opts) {
|
|
|
2355
2919
|
}, opts.timeoutSec * 1e3);
|
|
2356
2920
|
res.code = await new Promise((resolve2) => {
|
|
2357
2921
|
child.on("error", (e) => {
|
|
2358
|
-
res.spawnError = e.code === "ENOENT" ?
|
|
2922
|
+
res.spawnError = e.code === "ENOENT" ? fs19.existsSync(opts.cwd) ? `${inv.command} not found` : `the working directory ${opts.cwd} does not exist` : e.message;
|
|
2359
2923
|
resolve2(127);
|
|
2360
2924
|
});
|
|
2361
2925
|
child.on("close", (c) => resolve2(c));
|
|
@@ -2364,16 +2928,18 @@ async function spawnWorker(inv, opts) {
|
|
|
2364
2928
|
if (killer) clearTimeout(killer);
|
|
2365
2929
|
process.off("SIGINT", stop);
|
|
2366
2930
|
process.off("SIGTERM", stop);
|
|
2367
|
-
|
|
2368
|
-
|
|
2931
|
+
fs19.closeSync(out);
|
|
2932
|
+
fs19.closeSync(err);
|
|
2369
2933
|
return res;
|
|
2370
2934
|
}
|
|
2371
2935
|
export {
|
|
2936
|
+
ASKS_FOR_LIST,
|
|
2372
2937
|
DEFAULT_BACKEND,
|
|
2373
2938
|
STOP_FILE,
|
|
2374
2939
|
TERMINAL,
|
|
2375
2940
|
acquireWriteLock,
|
|
2376
2941
|
allBackends,
|
|
2942
|
+
answerParts,
|
|
2377
2943
|
backendIds,
|
|
2378
2944
|
badgeUrl,
|
|
2379
2945
|
brief,
|
|
@@ -2388,6 +2954,8 @@ export {
|
|
|
2388
2954
|
fill,
|
|
2389
2955
|
findSecretFiles,
|
|
2390
2956
|
findSecretFilesInTree,
|
|
2957
|
+
fixTask,
|
|
2958
|
+
focusedRate,
|
|
2391
2959
|
formatTarget,
|
|
2392
2960
|
freshMeta,
|
|
2393
2961
|
getBackend,
|
|
@@ -2396,6 +2964,7 @@ export {
|
|
|
2396
2964
|
isActive,
|
|
2397
2965
|
isAlive,
|
|
2398
2966
|
listRunIds,
|
|
2967
|
+
loadEval,
|
|
2399
2968
|
loadPlan,
|
|
2400
2969
|
loadTemplate,
|
|
2401
2970
|
looksSecret,
|
|
@@ -2406,9 +2975,11 @@ export {
|
|
|
2406
2975
|
parseStatus,
|
|
2407
2976
|
parseTarget,
|
|
2408
2977
|
parseVerdict,
|
|
2978
|
+
pathsIn,
|
|
2409
2979
|
planName,
|
|
2410
2980
|
planTask,
|
|
2411
2981
|
primaryPrice,
|
|
2982
|
+
rankChain,
|
|
2412
2983
|
readCatalog,
|
|
2413
2984
|
readLedger,
|
|
2414
2985
|
readMeta,
|
|
@@ -2417,17 +2988,22 @@ export {
|
|
|
2417
2988
|
requestStop,
|
|
2418
2989
|
resolveRun,
|
|
2419
2990
|
retryAfterMs,
|
|
2991
|
+
retryCommands,
|
|
2420
2992
|
runDir,
|
|
2421
2993
|
runFile,
|
|
2422
2994
|
runsDir,
|
|
2423
2995
|
savedUsd,
|
|
2996
|
+
scoreAnswer,
|
|
2997
|
+
scoreOf,
|
|
2424
2998
|
secretWarning,
|
|
2425
2999
|
shimDir,
|
|
2426
3000
|
shimReady,
|
|
2427
3001
|
slotHolders,
|
|
2428
3002
|
spawnWorker,
|
|
3003
|
+
summarize,
|
|
2429
3004
|
totals,
|
|
2430
3005
|
trim,
|
|
3006
|
+
truthOf,
|
|
2431
3007
|
tryAcquireSlot,
|
|
2432
3008
|
usd,
|
|
2433
3009
|
verifyRefs,
|