pitroom 0.21.0 → 0.23.0

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