pitroom 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +30 -0
- package/README.md +37 -4
- package/dist/lib.mjs +1234 -659
- package/dist/pitroom.mjs +1270 -495
- package/dist/ui/app.css +1 -1
- package/dist/ui/app.js +32 -30
- package/docs/backends.md +56 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/policies/gemini/shell.toml +4 -1
- package/policies/qwen/worker-defaults.json +9 -0
package/dist/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) {
|
|
@@ -924,6 +927,10 @@ var gemini = {
|
|
|
924
927
|
doctor: doctor3
|
|
925
928
|
};
|
|
926
929
|
|
|
930
|
+
// src/backends/kilo/index.ts
|
|
931
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
932
|
+
import path7 from "node:path";
|
|
933
|
+
|
|
927
934
|
// src/backends/opencode/index.ts
|
|
928
935
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
929
936
|
|
|
@@ -982,9 +989,9 @@ function parseEvents4(ndjson) {
|
|
|
982
989
|
}
|
|
983
990
|
lastActivity = `${name} ${describe2(st.input)}`.trim();
|
|
984
991
|
if (timeline.length < MAX_STEPS) {
|
|
985
|
-
const
|
|
986
|
-
const text =
|
|
987
|
-
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) });
|
|
988
995
|
}
|
|
989
996
|
break;
|
|
990
997
|
}
|
|
@@ -1133,17 +1140,19 @@ var FORBIDDEN_BASH = [
|
|
|
1133
1140
|
"twine upload*",
|
|
1134
1141
|
// no recursive delegation
|
|
1135
1142
|
"opencode*",
|
|
1143
|
+
"kilo *",
|
|
1144
|
+
"kilocode*",
|
|
1136
1145
|
"pitroom *",
|
|
1137
1146
|
"*pitroom*"
|
|
1138
1147
|
];
|
|
1139
|
-
function scratchDirs() {
|
|
1148
|
+
function scratchDirs(app) {
|
|
1140
1149
|
const data = process.env.XDG_DATA_HOME ?? path6.join(os6.homedir(), ".local", "share");
|
|
1141
1150
|
const rules = {
|
|
1142
1151
|
"*": "deny",
|
|
1143
|
-
[path6.join(data,
|
|
1144
|
-
[path6.join(data,
|
|
1152
|
+
[path6.join(data, app, "tool-output", "*")]: "allow",
|
|
1153
|
+
[path6.join(data, app, "shell", "*", "*")]: "allow"
|
|
1145
1154
|
};
|
|
1146
|
-
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";
|
|
1147
1156
|
return rules;
|
|
1148
1157
|
}
|
|
1149
1158
|
function realpath(p) {
|
|
@@ -1164,7 +1173,7 @@ var COMMON = {
|
|
|
1164
1173
|
doom_loop: "deny"
|
|
1165
1174
|
};
|
|
1166
1175
|
var web = (on) => ({ webfetch: on ? "allow" : "deny", websearch: on ? "allow" : "deny" });
|
|
1167
|
-
function readProfile(allowWeb = false) {
|
|
1176
|
+
function readProfile(allowWeb = false, app = "opencode") {
|
|
1168
1177
|
return {
|
|
1169
1178
|
"*": "deny",
|
|
1170
1179
|
read: READ_FILES,
|
|
@@ -1180,11 +1189,11 @@ function readProfile(allowWeb = false) {
|
|
|
1180
1189
|
bash: READ_ONLY_BASH,
|
|
1181
1190
|
shell: READ_ONLY_BASH,
|
|
1182
1191
|
edit: "deny",
|
|
1183
|
-
external_directory: scratchDirs(),
|
|
1192
|
+
external_directory: scratchDirs(app),
|
|
1184
1193
|
...COMMON
|
|
1185
1194
|
};
|
|
1186
1195
|
}
|
|
1187
|
-
function writeProfile(allowWeb = false) {
|
|
1196
|
+
function writeProfile(allowWeb = false, app = "opencode") {
|
|
1188
1197
|
const bash = { "*": "allow" };
|
|
1189
1198
|
for (const pattern of FORBIDDEN_BASH) bash[pattern] = "deny";
|
|
1190
1199
|
return {
|
|
@@ -1193,25 +1202,26 @@ function writeProfile(allowWeb = false) {
|
|
|
1193
1202
|
...web(allowWeb),
|
|
1194
1203
|
bash,
|
|
1195
1204
|
shell: bash,
|
|
1196
|
-
external_directory: scratchDirs(),
|
|
1205
|
+
external_directory: scratchDirs(app),
|
|
1197
1206
|
...COMMON
|
|
1198
1207
|
};
|
|
1199
1208
|
}
|
|
1200
1209
|
function agentFor(mode) {
|
|
1201
1210
|
return mode === "read" ? AGENT.read : AGENT.write;
|
|
1202
1211
|
}
|
|
1203
|
-
function configContent(existing, allowWeb = false) {
|
|
1212
|
+
function configContent(existing, allowWeb = false, app = "opencode", extra = {}) {
|
|
1204
1213
|
const ours = {
|
|
1214
|
+
...extra,
|
|
1205
1215
|
agent: {
|
|
1206
1216
|
[AGENT.read]: {
|
|
1207
1217
|
mode: "primary",
|
|
1208
1218
|
description: "pitroom: read-only worker (research, search, review)",
|
|
1209
|
-
permission: readProfile(allowWeb)
|
|
1219
|
+
permission: readProfile(allowWeb, app)
|
|
1210
1220
|
},
|
|
1211
1221
|
[AGENT.write]: {
|
|
1212
1222
|
mode: "primary",
|
|
1213
1223
|
description: "pitroom: editing worker (no git history changes, no bulk deletes)",
|
|
1214
|
-
permission: writeProfile(allowWeb)
|
|
1224
|
+
permission: writeProfile(allowWeb, app)
|
|
1215
1225
|
}
|
|
1216
1226
|
}
|
|
1217
1227
|
};
|
|
@@ -1388,10 +1398,348 @@ var opencode = {
|
|
|
1388
1398
|
doctor: doctor4
|
|
1389
1399
|
};
|
|
1390
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
|
+
|
|
1391
1739
|
// src/backends/index.ts
|
|
1392
|
-
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]));
|
|
1393
1741
|
var DEFAULT_BACKEND = opencode.id;
|
|
1394
|
-
var PLANNED = [];
|
|
1742
|
+
var PLANNED = ["hermes"];
|
|
1395
1743
|
var backendIds = () => [...REGISTRY.keys()];
|
|
1396
1744
|
var allBackends = () => [...REGISTRY.values()];
|
|
1397
1745
|
var isBackendId = (id) => REGISTRY.has(id) || PLANNED.includes(id);
|
|
@@ -1421,8 +1769,8 @@ var formatTarget = (t) => t.model ? `${t.backend}:${t.model}` : t.backend;
|
|
|
1421
1769
|
var describeTarget = (t) => t.model ? formatTarget(t) : `${t.backend} (default model)`;
|
|
1422
1770
|
|
|
1423
1771
|
// src/core/refs.ts
|
|
1424
|
-
import
|
|
1425
|
-
import
|
|
1772
|
+
import fs9 from "node:fs";
|
|
1773
|
+
import path9 from "node:path";
|
|
1426
1774
|
var FILE_EXTENSIONS = new Set(
|
|
1427
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(" ")
|
|
1428
1776
|
);
|
|
@@ -1473,13 +1821,13 @@ function extractRefs(answer) {
|
|
|
1473
1821
|
return [...seen.values()];
|
|
1474
1822
|
}
|
|
1475
1823
|
function verifyRefs(refs, dirs) {
|
|
1476
|
-
const roots = [...new Set(dirs.filter(Boolean).map((d) =>
|
|
1824
|
+
const roots = [...new Set(dirs.filter(Boolean).map((d) => path9.resolve(d)))];
|
|
1477
1825
|
const invalid = [];
|
|
1478
1826
|
const lineCache = /* @__PURE__ */ new Map();
|
|
1479
1827
|
const load = (file) => {
|
|
1480
1828
|
if (!lineCache.has(file)) {
|
|
1481
|
-
const size =
|
|
1482
|
-
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");
|
|
1483
1831
|
if (lines && lines[lines.length - 1] === "") lines.pop();
|
|
1484
1832
|
lineCache.set(file, lines);
|
|
1485
1833
|
}
|
|
@@ -1490,9 +1838,9 @@ function verifyRefs(refs, dirs) {
|
|
|
1490
1838
|
const candidates = (ref) => {
|
|
1491
1839
|
const direct = resolve(ref.file, roots);
|
|
1492
1840
|
if (direct) return [direct];
|
|
1493
|
-
if (
|
|
1841
|
+
if (path9.isAbsolute(ref.file)) return [];
|
|
1494
1842
|
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
1495
|
-
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);
|
|
1496
1844
|
};
|
|
1497
1845
|
const check = (file, ref) => {
|
|
1498
1846
|
const lines = load(file);
|
|
@@ -1507,7 +1855,7 @@ function verifyRefs(refs, dirs) {
|
|
|
1507
1855
|
if (!files.length) {
|
|
1508
1856
|
if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
|
|
1509
1857
|
total++;
|
|
1510
|
-
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" });
|
|
1511
1859
|
continue;
|
|
1512
1860
|
}
|
|
1513
1861
|
total++;
|
|
@@ -1528,7 +1876,7 @@ function enclosedBy(lines, ref) {
|
|
|
1528
1876
|
return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
|
|
1529
1877
|
}
|
|
1530
1878
|
var isFileName = (name) => {
|
|
1531
|
-
const base =
|
|
1879
|
+
const base = path9.basename(name);
|
|
1532
1880
|
return !base.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base) || FILE_EXTENSIONS.has(base.split(".").pop().toLowerCase());
|
|
1533
1881
|
};
|
|
1534
1882
|
function indexFiles(roots) {
|
|
@@ -1537,19 +1885,19 @@ function indexFiles(roots) {
|
|
|
1537
1885
|
const walk = (dir) => {
|
|
1538
1886
|
let entries;
|
|
1539
1887
|
try {
|
|
1540
|
-
entries =
|
|
1888
|
+
entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
1541
1889
|
} catch {
|
|
1542
1890
|
return;
|
|
1543
1891
|
}
|
|
1544
1892
|
for (const e of entries) {
|
|
1545
1893
|
if (count >= MAX_INDEXED) return;
|
|
1546
1894
|
if (e.isDirectory()) {
|
|
1547
|
-
if (!SKIP_DIRS.has(e.name)) walk(
|
|
1895
|
+
if (!SKIP_DIRS.has(e.name)) walk(path9.join(dir, e.name));
|
|
1548
1896
|
} else if (e.isFile()) {
|
|
1549
1897
|
count++;
|
|
1550
1898
|
const list2 = index.get(e.name);
|
|
1551
|
-
if (list2) list2.push(
|
|
1552
|
-
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)]);
|
|
1553
1901
|
}
|
|
1554
1902
|
}
|
|
1555
1903
|
};
|
|
@@ -1557,10 +1905,10 @@ function indexFiles(roots) {
|
|
|
1557
1905
|
return index;
|
|
1558
1906
|
}
|
|
1559
1907
|
function resolve(ref, roots) {
|
|
1560
|
-
const candidates =
|
|
1908
|
+
const candidates = path9.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path9.join(r, ref));
|
|
1561
1909
|
return candidates.find((c) => {
|
|
1562
1910
|
try {
|
|
1563
|
-
return
|
|
1911
|
+
return fs9.statSync(c).isFile() && inside(c, roots);
|
|
1564
1912
|
} catch {
|
|
1565
1913
|
return false;
|
|
1566
1914
|
}
|
|
@@ -1568,21 +1916,21 @@ function resolve(ref, roots) {
|
|
|
1568
1916
|
}
|
|
1569
1917
|
function inside(file, roots) {
|
|
1570
1918
|
return roots.some((r) => {
|
|
1571
|
-
const rel =
|
|
1572
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
1919
|
+
const rel = path9.relative(r, path9.resolve(file));
|
|
1920
|
+
return rel !== "" && !rel.startsWith("..") && !path9.isAbsolute(rel);
|
|
1573
1921
|
});
|
|
1574
1922
|
}
|
|
1575
1923
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1576
1924
|
|
|
1577
1925
|
// src/core/templates.ts
|
|
1578
|
-
import
|
|
1579
|
-
import
|
|
1926
|
+
import fs10 from "node:fs";
|
|
1927
|
+
import path11 from "node:path";
|
|
1580
1928
|
|
|
1581
1929
|
// src/core/install.ts
|
|
1582
|
-
import
|
|
1583
|
-
import { fileURLToPath as
|
|
1930
|
+
import path10 from "node:path";
|
|
1931
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1584
1932
|
function packageRoot() {
|
|
1585
|
-
return
|
|
1933
|
+
return path10.resolve(path10.dirname(fileURLToPath3(import.meta.url)), "..");
|
|
1586
1934
|
}
|
|
1587
1935
|
|
|
1588
1936
|
// src/core/templates.ts
|
|
@@ -1593,9 +1941,9 @@ var FILES = {
|
|
|
1593
1941
|
"code-reviewer": "pitroom-review/code-reviewer.md"
|
|
1594
1942
|
};
|
|
1595
1943
|
function loadTemplate(name, root = packageRoot()) {
|
|
1596
|
-
const file =
|
|
1597
|
-
if (!
|
|
1598
|
-
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*/, "");
|
|
1599
1947
|
}
|
|
1600
1948
|
function fill(template, values) {
|
|
1601
1949
|
const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
|
|
@@ -1651,689 +1999,904 @@ function parseAudit(text) {
|
|
|
1651
1999
|
return { verdict, disputed };
|
|
1652
2000
|
}
|
|
1653
2001
|
|
|
1654
|
-
// src/core/
|
|
1655
|
-
import
|
|
1656
|
-
import
|
|
1657
|
-
|
|
1658
|
-
var
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
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");
|
|
1680
2039
|
}
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
const
|
|
1685
|
-
const
|
|
1686
|
-
const
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
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
|
+
}
|
|
1699
2065
|
}
|
|
1700
|
-
|
|
2066
|
+
cached = { config, warnings };
|
|
2067
|
+
return cached;
|
|
1701
2068
|
}
|
|
1702
|
-
function
|
|
1703
|
-
|
|
1704
|
-
if (
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
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;
|
|
1708
2079
|
}
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
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" };
|
|
1713
2110
|
}
|
|
1714
|
-
function
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
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
|
+
};
|
|
1723
2145
|
}
|
|
1724
|
-
var planName = (file) => path10.basename(file).replace(/\.md$/i, "");
|
|
1725
2146
|
|
|
1726
|
-
// src/
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
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");
|
|
1744
2175
|
}
|
|
1745
|
-
sub=""; skip=""; after=""
|
|
1746
|
-
for a do
|
|
1747
|
-
if [ -n "$sub" ]; then after="$after $a"; continue; fi
|
|
1748
|
-
case "$skip" in
|
|
1749
|
-
c) skip=""; case "$a" in [Aa][Ll][Ii][Aa][Ss].*) block "-c $a" ;; esac; continue ;;
|
|
1750
|
-
v) skip=""; continue ;;
|
|
1751
|
-
esac
|
|
1752
|
-
case "$a" in
|
|
1753
|
-
-c|--config-env) skip=c ;;
|
|
1754
|
-
-C|--git-dir|--work-tree|--namespace|--super-prefix|--exec-path) skip=v ;;
|
|
1755
|
-
-*) ;;
|
|
1756
|
-
*) sub="$a" ;;
|
|
1757
|
-
esac
|
|
1758
|
-
done
|
|
1759
|
-
[ -z "$sub" ] && exec "$real" "$@"
|
|
1760
2176
|
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
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
|
+
}
|
|
1770
2200
|
|
|
1771
|
-
|
|
1772
|
-
|
|
2201
|
+
// src/core/receipt.ts
|
|
2202
|
+
import fs13 from "node:fs";
|
|
1773
2203
|
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
remote)
|
|
1789
|
-
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 ;;
|
|
1790
|
-
fetch)
|
|
1791
|
-
for w in $after; do case "$w" in -*) ;; *:*) block "fetch $w (writes a local ref)" ;; esac; done ;;
|
|
1792
|
-
apply)
|
|
1793
|
-
has --index --cached -3 --3way && block "apply$after" ;;
|
|
1794
|
-
reflog)
|
|
1795
|
-
has expire delete && block "reflog$after" ;;
|
|
1796
|
-
symbolic-ref)
|
|
1797
|
-
has -d --delete && block "symbolic-ref$after"
|
|
1798
|
-
[ "$(count)" -ge 2 ] && block "symbolic-ref$after" ;;
|
|
1799
|
-
submodule)
|
|
1800
|
-
for w in $after; do case "$w" in -*) ;; status|summary) break ;; *) block "submodule $w" ;; esac; done ;;
|
|
1801
|
-
esac
|
|
1802
|
-
exec "$real" "$@"
|
|
1803
|
-
`;
|
|
1804
|
-
function shimDir() {
|
|
1805
|
-
return path11.join(home(), "shim", `v${VERSION}`);
|
|
1806
|
-
}
|
|
1807
|
-
function findRealGit(skip) {
|
|
1808
|
-
const same = (a, b) => WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
1809
|
-
for (const dir of (process.env.PATH ?? "").split(path11.delimiter).filter(Boolean)) {
|
|
1810
|
-
if (same(path11.resolve(dir), skip)) continue;
|
|
1811
|
-
const p = path11.join(dir, WINDOWS ? "git.exe" : "git");
|
|
1812
|
-
try {
|
|
1813
|
-
fs11.accessSync(p, fs11.constants.X_OK);
|
|
1814
|
-
if (fs11.statSync(p).isFile()) return p;
|
|
1815
|
-
} 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];
|
|
1816
2218
|
}
|
|
1817
2219
|
}
|
|
1818
|
-
return
|
|
2220
|
+
return out;
|
|
1819
2221
|
}
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
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 };
|
|
1826
2249
|
}
|
|
1827
|
-
const up = path11.dirname(dir);
|
|
1828
|
-
if (up === dir) break;
|
|
1829
|
-
dir = up;
|
|
1830
2250
|
}
|
|
1831
2251
|
return void 0;
|
|
1832
2252
|
}
|
|
1833
|
-
var cmdShim = (sh) => `@echo off\r
|
|
1834
|
-
set "MSYS2_ARG_CONV_EXCL=*"\r
|
|
1835
|
-
"${sh}" "%~dp0git" %*\r
|
|
1836
|
-
exit /b %errorlevel%\r
|
|
1837
|
-
`;
|
|
1838
|
-
var HOOK_VERSION = 1;
|
|
1839
|
-
var REF_HOOK = `#!/bin/sh
|
|
1840
|
-
# pitroom git guard (layer 2, v${HOOK_VERSION}): refuse ref updates made by workers.
|
|
1841
|
-
[ "$1" = prepared ] || exit 0
|
|
1842
|
-
echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
|
|
1843
|
-
exit 1
|
|
1844
|
-
`;
|
|
1845
|
-
var hooksDir = () => path11.join(home(), "git-hooks", `v${HOOK_VERSION}`);
|
|
1846
|
-
function writeIfChanged(file, content) {
|
|
1847
|
-
if (fs11.existsSync(file) && fs11.readFileSync(file, "utf8") === content) return;
|
|
1848
|
-
fs11.mkdirSync(path11.dirname(file), { recursive: true });
|
|
1849
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
1850
|
-
fs11.writeFileSync(tmp, content, { mode: 493 });
|
|
1851
|
-
renameOver(tmp, file);
|
|
1852
|
-
}
|
|
1853
|
-
function pathKey(env) {
|
|
1854
|
-
const keys = Object.keys(env).filter((k) => k.toLowerCase() === "path");
|
|
1855
|
-
return keys.includes("PATH") ? "PATH" : keys[0] ?? "PATH";
|
|
1856
|
-
}
|
|
1857
|
-
function onePath(env) {
|
|
1858
|
-
const key = pathKey(env);
|
|
1859
|
-
const out = { ...env };
|
|
1860
|
-
for (const k of Object.keys(out)) if (k !== key && k.toLowerCase() === "path") delete out[k];
|
|
1861
|
-
return out;
|
|
1862
|
-
}
|
|
1863
|
-
function withGitConfig(env, entries) {
|
|
1864
|
-
const out = { ...env };
|
|
1865
|
-
let n = Number(env.GIT_CONFIG_COUNT) || 0;
|
|
1866
|
-
for (const [key, value] of entries) {
|
|
1867
|
-
out[`GIT_CONFIG_KEY_${n}`] = key;
|
|
1868
|
-
out[`GIT_CONFIG_VALUE_${n}`] = value;
|
|
1869
|
-
n++;
|
|
1870
|
-
}
|
|
1871
|
-
out.GIT_CONFIG_COUNT = String(n);
|
|
1872
|
-
return out;
|
|
1873
|
-
}
|
|
1874
|
-
function guardEnv(env) {
|
|
1875
|
-
const quiet = {
|
|
1876
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
1877
|
-
GIT_EDITOR: "true",
|
|
1878
|
-
GIT_SEQUENCE_EDITOR: "true",
|
|
1879
|
-
GIT_PAGER: "cat",
|
|
1880
|
-
PAGER: "cat"
|
|
1881
|
-
};
|
|
1882
|
-
writeIfChanged(path11.join(hooksDir(), "reference-transaction"), REF_HOOK);
|
|
1883
|
-
env = withGitConfig(onePath(env), [
|
|
1884
|
-
["core.hooksPath", fwd(hooksDir())],
|
|
1885
|
-
["url.pitroom-push-blocked://.pushInsteadOf", ""]
|
|
1886
|
-
]);
|
|
1887
|
-
const dir = shimDir();
|
|
1888
|
-
const real = findRealGit(dir);
|
|
1889
|
-
if (!real) return { ...env, ...quiet };
|
|
1890
|
-
let sh;
|
|
1891
|
-
if (WINDOWS) {
|
|
1892
|
-
sh = findGitSh(real);
|
|
1893
|
-
if (!sh) return { ...env, ...quiet };
|
|
1894
|
-
writeIfChanged(path11.join(dir, "git.cmd"), cmdShim(sh));
|
|
1895
|
-
}
|
|
1896
|
-
writeIfChanged(path11.join(dir, "git"), SHIM);
|
|
1897
|
-
const key = pathKey(env);
|
|
1898
|
-
return { ...env, ...quiet, PITROOM_REAL_GIT: fwd(real), [key]: `${dir}${path11.delimiter}${env[key] ?? ""}` };
|
|
1899
|
-
}
|
|
1900
|
-
function shimReady() {
|
|
1901
|
-
const env = guardEnv({ ...process.env });
|
|
1902
|
-
return Boolean(env[pathKey(env)]?.startsWith(shimDir()));
|
|
1903
|
-
}
|
|
1904
2253
|
|
|
1905
|
-
// src/core/
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
function
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
if (m.pid) return isAlive(m.pid);
|
|
1918
|
-
return Date.now() - Date.parse(m.startedAt) < STARTUP_GRACE_MS;
|
|
1919
|
-
} catch {
|
|
1920
|
-
return false;
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
function tryClaim(file, runId) {
|
|
1924
|
-
fs12.mkdirSync(path12.dirname(file), { recursive: true });
|
|
1925
|
-
try {
|
|
1926
|
-
fs12.writeFileSync(file, runId, { flag: "wx" });
|
|
1927
|
-
return true;
|
|
1928
|
-
} catch (e) {
|
|
1929
|
-
if (e.code !== "EEXIST") throw e;
|
|
1930
|
-
}
|
|
1931
|
-
let owner = "";
|
|
1932
|
-
try {
|
|
1933
|
-
owner = fs12.readFileSync(file, "utf8").trim();
|
|
1934
|
-
} catch {
|
|
1935
|
-
}
|
|
1936
|
-
if (owner === runId) return true;
|
|
1937
|
-
if (holderActive(owner, runId)) return false;
|
|
1938
|
-
fs12.rmSync(file, { force: true });
|
|
1939
|
-
try {
|
|
1940
|
-
fs12.writeFileSync(file, runId, { flag: "wx" });
|
|
1941
|
-
return true;
|
|
1942
|
-
} catch {
|
|
1943
|
-
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 };
|
|
1944
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;
|
|
1945
2272
|
}
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
}
|
|
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 };
|
|
1952
2278
|
}
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
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);
|
|
1959
2284
|
}
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
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) => {
|
|
1964
2288
|
try {
|
|
1965
|
-
return
|
|
2289
|
+
return [JSON.parse(l)];
|
|
1966
2290
|
} catch {
|
|
1967
|
-
return
|
|
2291
|
+
return [];
|
|
1968
2292
|
}
|
|
1969
|
-
}).filter((
|
|
1970
|
-
}
|
|
1971
|
-
var lockFile = (repoRoot) => path12.join(locksDir(), `write-${crypto2.createHash("sha1").update(path12.resolve(repoRoot)).digest("hex").slice(0, 16)}`);
|
|
1972
|
-
function acquireWriteLock(repoRoot, runId) {
|
|
1973
|
-
const file = lockFile(repoRoot);
|
|
1974
|
-
if (tryClaim(file, runId)) return;
|
|
1975
|
-
let owner = "";
|
|
1976
|
-
try {
|
|
1977
|
-
owner = fs12.readFileSync(file, "utf8").trim();
|
|
1978
|
-
} catch {
|
|
1979
|
-
}
|
|
1980
|
-
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);
|
|
1981
2294
|
}
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
if (TEMPLATE.test(name)) return false;
|
|
1995
|
-
if (name === ".env" || name.startsWith(".env.") || name.endsWith(".env")) return true;
|
|
1996
|
-
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 };
|
|
1997
2307
|
}
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
try {
|
|
2005
|
-
entries = fs13.readdirSync(d, { withFileTypes: true });
|
|
2006
|
-
} catch {
|
|
2007
|
-
return;
|
|
2008
|
-
}
|
|
2009
|
-
for (const e of entries) {
|
|
2010
|
-
visited++;
|
|
2011
|
-
if (e.isDirectory()) {
|
|
2012
|
-
if (!SKIP_DIRS2.has(e.name)) walk(path13.join(d, e.name), depth + 1);
|
|
2013
|
-
} else if (e.isFile() && looksSecret(e.name)) {
|
|
2014
|
-
found.push(slash(path13.relative(dir, path13.join(d, e.name))));
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
};
|
|
2018
|
-
walk(dir, 0);
|
|
2019
|
-
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));
|
|
2020
2314
|
}
|
|
2021
|
-
function
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
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();
|
|
2025
|
-
} catch {
|
|
2026
|
-
return [];
|
|
2027
|
-
}
|
|
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`;
|
|
2028
2318
|
}
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
const
|
|
2032
|
-
const
|
|
2033
|
-
|
|
2034
|
-
|
|
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
|
+
`;
|
|
2035
2359
|
}
|
|
2036
|
-
var slash = (p) => p.split(path13.sep).join("/");
|
|
2037
2360
|
|
|
2038
|
-
// src/core/
|
|
2039
|
-
|
|
2361
|
+
// src/core/ui.ts
|
|
2362
|
+
var WEEK_MS = 7 * 24 * 3600 * 1e3;
|
|
2040
2363
|
|
|
2041
|
-
// src/core/
|
|
2364
|
+
// src/core/plan.ts
|
|
2042
2365
|
import fs14 from "node:fs";
|
|
2043
|
-
import os7 from "node:os";
|
|
2044
2366
|
import path14 from "node:path";
|
|
2045
|
-
var
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
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";
|
|
2069
2441
|
};
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
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
|
+
});
|
|
2074
2463
|
}
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
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}`);
|
|
2087
2493
|
}
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
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]) };
|
|
2099
2516
|
}
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
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));
|
|
2103
2580
|
}
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
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
|
+
}
|
|
2108
2673
|
}
|
|
2109
|
-
|
|
2110
|
-
|
|
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;
|
|
2111
2686
|
}
|
|
2112
|
-
|
|
2113
|
-
return typeof v === type;
|
|
2687
|
+
return void 0;
|
|
2114
2688
|
}
|
|
2115
|
-
var
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
const
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
var readIn = (v) => v === "auto" || v === "snapshot" || v === "project" ? v : void 0;
|
|
2135
|
-
var list = (s) => s?.split(",").map((x) => x.trim()).filter(Boolean);
|
|
2136
|
-
function setting(flag, env, conf, fallback) {
|
|
2137
|
-
if (flag !== void 0) return { value: flag, source: "flag" };
|
|
2138
|
-
if (env !== void 0) return { value: env, source: "env" };
|
|
2139
|
-
if (conf !== void 0) return { value: conf, source: "config" };
|
|
2140
|
-
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);
|
|
2141
2708
|
}
|
|
2142
|
-
function
|
|
2143
|
-
const
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
primary: setting(void 0, e.PITROOM_PRIMARY, c.primary, "sonnet"),
|
|
2152
|
-
price: setting(void 0, e.PITROOM_PRICE, c.price, void 0),
|
|
2153
|
-
link: setting(void 0, void 0, c.link, []),
|
|
2154
|
-
web: setting(void 0, void 0, c.web, false),
|
|
2155
|
-
maxParallel: clampParallel(setting(void 0, positiveInt(e.PITROOM_MAX_PARALLEL), positiveInt(c.maxParallel), DEFAULT_PARALLEL)),
|
|
2156
|
-
models: setting(void 0, void 0, c.models, {}),
|
|
2157
|
-
tiers: setting(void 0, void 0, c.tiers, {}),
|
|
2158
|
-
costs: setting(void 0, void 0, c.costs, {}),
|
|
2159
|
-
audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0),
|
|
2160
|
-
readIn: setting(void 0, readIn(e.PITROOM_READ_IN), readIn(c.readIn), "auto"),
|
|
2161
|
-
cacheDays: setting(void 0, days(e.PITROOM_CACHE_DAYS), days(c.cacheDays), 7),
|
|
2162
|
-
countRateLimits: setting(void 0, void 0, c.countRateLimits, false),
|
|
2163
|
-
mcpDash: setting(void 0, flag01(e.PITROOM_MCP_DASH), c.mcpDash, true),
|
|
2164
|
-
notify: setting(void 0, flag01(e.PITROOM_NOTIFY), c.notify, false),
|
|
2165
|
-
notifyCommand: setting(void 0, e.PITROOM_NOTIFY_COMMAND?.trim() || void 0, c.notifyCommand?.trim() || void 0, void 0),
|
|
2166
|
-
notifyAfter: setting(void 0, days(e.PITROOM_NOTIFY_AFTER), days(c.notifyAfter), 15),
|
|
2167
|
-
workerPrices: setting(void 0, void 0, c.workerPrices, {}),
|
|
2168
|
-
priceFeed: setting(void 0, flag01(e.PITROOM_PRICE_FEED), c.priceFeed, false),
|
|
2169
|
-
priceFeedUrl: setting(void 0, e.PITROOM_PRICE_FEED_URL?.trim() || void 0, c.priceFeedUrl?.trim() || void 0, "https://models.dev/api.json"),
|
|
2170
|
-
priceFeedHours: setting(void 0, positiveInt(e.PITROOM_PRICE_FEED_HOURS), positiveInt(c.priceFeedHours), 24)
|
|
2171
|
-
};
|
|
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;
|
|
2172
2718
|
}
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
const out = {};
|
|
2181
|
-
if (!raw || typeof raw !== "object") return out;
|
|
2182
|
-
for (const [provider, p] of Object.entries(raw)) {
|
|
2183
|
-
for (const [id, m] of Object.entries(p?.models ?? {})) {
|
|
2184
|
-
const c = m?.cost;
|
|
2185
|
-
const ok = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
2186
|
-
if (!c || !ok(c.input) || !ok(c.output)) continue;
|
|
2187
|
-
out[`${provider}/${id}`] = [c.input, c.output, ok(c.cache_read) ? c.cache_read : c.input / 10];
|
|
2188
|
-
}
|
|
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++;
|
|
2189
2726
|
}
|
|
2727
|
+
out.GIT_CONFIG_COUNT = String(n);
|
|
2190
2728
|
return out;
|
|
2191
2729
|
}
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
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;
|
|
2195
2770
|
try {
|
|
2196
|
-
const
|
|
2197
|
-
if (
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
cached2 = { file, mtimeMs, catalog: catalog5 };
|
|
2201
|
-
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;
|
|
2202
2775
|
} catch {
|
|
2203
|
-
return
|
|
2776
|
+
return false;
|
|
2204
2777
|
}
|
|
2205
2778
|
}
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
if (
|
|
2213
|
-
const keys = [id, ...[...OWN[backend] ?? [], "anthropic", "openai", "google"].map((p) => `${p}/${id}`)];
|
|
2214
|
-
const any = keys.find((k) => catalog5.models[k]) ?? Object.keys(catalog5.models).filter((k) => k.endsWith(`/${id}`)).sort()[0];
|
|
2215
|
-
const triple = any ? catalog5.models[any] : void 0;
|
|
2216
|
-
if (any && triple && triple.length === 3 && triple.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
2217
|
-
const [input, output, cachedInput] = triple;
|
|
2218
|
-
return { input, output, cachedInput, key: any };
|
|
2219
|
-
}
|
|
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;
|
|
2220
2786
|
}
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
};
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
if (custom && custom.length >= 2 && custom.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
2235
|
-
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;
|
|
2236
2800
|
}
|
|
2237
|
-
const named = eff.primary.value;
|
|
2238
|
-
const preset = PRESETS[named.toLowerCase()];
|
|
2239
|
-
if (preset) return preset;
|
|
2240
|
-
const c = eff.priceFeed.value ? catalogPrice("", named) : void 0;
|
|
2241
|
-
return c ? { name: named, source: "catalog", input: c.input, output: c.output, cachedInput: c.cachedInput } : PRESETS.sonnet;
|
|
2242
2801
|
}
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
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
|
+
}
|
|
2248
2808
|
}
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
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;
|
|
2254
2815
|
}
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2816
|
+
var releaseSlot = (file, runId) => releaseIfOwner(file, runId);
|
|
2817
|
+
function slotHolders() {
|
|
2818
|
+
if (!fs17.existsSync(slotsDir())) return [];
|
|
2819
|
+
return fs17.readdirSync(slotsDir()).map((f) => {
|
|
2258
2820
|
try {
|
|
2259
|
-
return
|
|
2821
|
+
return fs17.readFileSync(path16.join(slotsDir(), f), "utf8").trim();
|
|
2260
2822
|
} catch {
|
|
2261
|
-
return
|
|
2823
|
+
return "";
|
|
2262
2824
|
}
|
|
2263
|
-
}).filter((
|
|
2264
|
-
}
|
|
2265
|
-
function totals(entries) {
|
|
2266
|
-
const t = entries.reduce(
|
|
2267
|
-
(a, e) => ({
|
|
2268
|
-
runs: a.runs + 1,
|
|
2269
|
-
tokens: a.tokens + e.tokens,
|
|
2270
|
-
returned: a.returned + e.returned,
|
|
2271
|
-
workerCost: a.workerCost + e.workerCost,
|
|
2272
|
-
saved: a.saved + e.saved
|
|
2273
|
-
}),
|
|
2274
|
-
{ runs: 0, tokens: 0, returned: 0, workerCost: 0, saved: 0 }
|
|
2275
|
-
);
|
|
2276
|
-
return { ...t, ratio: t.returned ? t.tokens / t.returned : 0 };
|
|
2825
|
+
}).filter((id) => holderActive(id, ""));
|
|
2277
2826
|
}
|
|
2278
|
-
var
|
|
2279
|
-
function
|
|
2280
|
-
const
|
|
2281
|
-
if (
|
|
2282
|
-
|
|
2283
|
-
|
|
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);
|
|
2284
2837
|
}
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
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);
|
|
2288
2853
|
}
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
const
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
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
|
+
}
|
|
2296
2873
|
};
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
.
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
.val{font-size:26px;font-weight:650;letter-spacing:-.02em}
|
|
2308
|
-
.foot{font-size:11.5px;fill:#71717a}
|
|
2309
|
-
</style>
|
|
2310
|
-
<defs>
|
|
2311
|
-
<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>
|
|
2312
|
-
<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>
|
|
2313
|
-
<linearGradient id="m" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#ff6a2b"/><stop offset="1" stop-color="#ffa86b"/></linearGradient>
|
|
2314
|
-
<clipPath id="c"><rect width="${W}" height="${H}" rx="20"/></clipPath>
|
|
2315
|
-
</defs>
|
|
2316
|
-
<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>
|
|
2317
|
-
<rect x=".5" y=".5" width="${W - 1}" height="${H - 1}" rx="19.5" fill="none" stroke="#fff" stroke-opacity=".1"/>
|
|
2318
|
-
<rect x="${M}" y="28" width="38" height="38" rx="11" fill="url(#m)"/>
|
|
2319
|
-
<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)"/>
|
|
2320
|
-
<text x="${M + 52}" y="45" class="name">Pitroom</text><text x="${M + 52}" y="63" class="sub">Your agent's pit crew</text>
|
|
2321
|
-
<rect x="${W - M - pill}" y="34" width="${pill}" height="26" rx="13" fill="#fff" fill-opacity=".04" stroke="#fff" stroke-opacity=".14"/>
|
|
2322
|
-
<text x="${W - M - pill / 2}" y="51" class="pill" text-anchor="middle">${esc(when)}</text>
|
|
2323
|
-
<text x="${M}" y="112" class="lab">SAVED ON YOUR MAIN CODING AGENT</text>
|
|
2324
|
-
<text x="${M}" y="168" class="big">${esc(usd(t.saved))}</text>
|
|
2325
|
-
${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")}
|
|
2326
|
-
<text x="${M}" y="296" class="foot">Estimated against ${esc(primaryPrice().name)} pricing \xB7 npx pitroom</text>
|
|
2327
|
-
</svg>
|
|
2328
|
-
`;
|
|
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
|
+
}
|
|
2329
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("/");
|
|
2330
2893
|
|
|
2331
2894
|
// src/core/process.ts
|
|
2332
2895
|
import { spawn } from "node:child_process";
|
|
2333
|
-
import
|
|
2896
|
+
import fs19 from "node:fs";
|
|
2334
2897
|
async function spawnWorker(inv, opts) {
|
|
2335
|
-
const out =
|
|
2336
|
-
const err =
|
|
2898
|
+
const out = fs19.openSync(opts.stdoutFile, "w");
|
|
2899
|
+
const err = fs19.openSync(opts.stderrFile, "w");
|
|
2337
2900
|
const res = { code: null, timedOut: false, stopped: false };
|
|
2338
2901
|
const child = spawn(inv.command, inv.args, {
|
|
2339
2902
|
cwd: opts.cwd,
|
|
@@ -2356,7 +2919,7 @@ async function spawnWorker(inv, opts) {
|
|
|
2356
2919
|
}, opts.timeoutSec * 1e3);
|
|
2357
2920
|
res.code = await new Promise((resolve2) => {
|
|
2358
2921
|
child.on("error", (e) => {
|
|
2359
|
-
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;
|
|
2360
2923
|
resolve2(127);
|
|
2361
2924
|
});
|
|
2362
2925
|
child.on("close", (c) => resolve2(c));
|
|
@@ -2365,16 +2928,18 @@ async function spawnWorker(inv, opts) {
|
|
|
2365
2928
|
if (killer) clearTimeout(killer);
|
|
2366
2929
|
process.off("SIGINT", stop);
|
|
2367
2930
|
process.off("SIGTERM", stop);
|
|
2368
|
-
|
|
2369
|
-
|
|
2931
|
+
fs19.closeSync(out);
|
|
2932
|
+
fs19.closeSync(err);
|
|
2370
2933
|
return res;
|
|
2371
2934
|
}
|
|
2372
2935
|
export {
|
|
2936
|
+
ASKS_FOR_LIST,
|
|
2373
2937
|
DEFAULT_BACKEND,
|
|
2374
2938
|
STOP_FILE,
|
|
2375
2939
|
TERMINAL,
|
|
2376
2940
|
acquireWriteLock,
|
|
2377
2941
|
allBackends,
|
|
2942
|
+
answerParts,
|
|
2378
2943
|
backendIds,
|
|
2379
2944
|
badgeUrl,
|
|
2380
2945
|
brief,
|
|
@@ -2389,6 +2954,8 @@ export {
|
|
|
2389
2954
|
fill,
|
|
2390
2955
|
findSecretFiles,
|
|
2391
2956
|
findSecretFilesInTree,
|
|
2957
|
+
fixTask,
|
|
2958
|
+
focusedRate,
|
|
2392
2959
|
formatTarget,
|
|
2393
2960
|
freshMeta,
|
|
2394
2961
|
getBackend,
|
|
@@ -2397,6 +2964,7 @@ export {
|
|
|
2397
2964
|
isActive,
|
|
2398
2965
|
isAlive,
|
|
2399
2966
|
listRunIds,
|
|
2967
|
+
loadEval,
|
|
2400
2968
|
loadPlan,
|
|
2401
2969
|
loadTemplate,
|
|
2402
2970
|
looksSecret,
|
|
@@ -2407,9 +2975,11 @@ export {
|
|
|
2407
2975
|
parseStatus,
|
|
2408
2976
|
parseTarget,
|
|
2409
2977
|
parseVerdict,
|
|
2978
|
+
pathsIn,
|
|
2410
2979
|
planName,
|
|
2411
2980
|
planTask,
|
|
2412
2981
|
primaryPrice,
|
|
2982
|
+
rankChain,
|
|
2413
2983
|
readCatalog,
|
|
2414
2984
|
readLedger,
|
|
2415
2985
|
readMeta,
|
|
@@ -2418,17 +2988,22 @@ export {
|
|
|
2418
2988
|
requestStop,
|
|
2419
2989
|
resolveRun,
|
|
2420
2990
|
retryAfterMs,
|
|
2991
|
+
retryCommands,
|
|
2421
2992
|
runDir,
|
|
2422
2993
|
runFile,
|
|
2423
2994
|
runsDir,
|
|
2424
2995
|
savedUsd,
|
|
2996
|
+
scoreAnswer,
|
|
2997
|
+
scoreOf,
|
|
2425
2998
|
secretWarning,
|
|
2426
2999
|
shimDir,
|
|
2427
3000
|
shimReady,
|
|
2428
3001
|
slotHolders,
|
|
2429
3002
|
spawnWorker,
|
|
3003
|
+
summarize,
|
|
2430
3004
|
totals,
|
|
2431
3005
|
trim,
|
|
3006
|
+
truthOf,
|
|
2432
3007
|
tryAcquireSlot,
|
|
2433
3008
|
usd,
|
|
2434
3009
|
verifyRefs,
|