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