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