pitroom 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pitroom.mjs CHANGED
@@ -522,9 +522,9 @@ function doctor2({ models, hasFallback }) {
522
522
  return checks;
523
523
  }
524
524
  function catalog2() {
525
- const file = path3.join(codexHome(), "models_cache.json");
525
+ const file2 = path3.join(codexHome(), "models_cache.json");
526
526
  try {
527
- const raw = JSON.parse(fs3.readFileSync(file, "utf8"));
527
+ const raw = JSON.parse(fs3.readFileSync(file2, "utf8"));
528
528
  const models = (raw.models ?? []).filter((m) => m.visibility !== "hide" && typeof m.slug === "string").map((m) => ({
529
529
  id: m.slug,
530
530
  efforts: (m.supported_reasoning_levels ?? []).map(
@@ -589,10 +589,10 @@ function useArchive(hooks) {
589
589
  }
590
590
  function writeMeta(meta) {
591
591
  fs4.mkdirSync(runDir(meta.id), { recursive: true });
592
- const file = runFile(meta.id, "meta.json");
593
- const tmp = `${file}.${process.pid}.tmp`;
592
+ const file2 = runFile(meta.id, "meta.json");
593
+ const tmp = `${file2}.${process.pid}.tmp`;
594
594
  fs4.writeFileSync(tmp, JSON.stringify(meta, null, 2));
595
- fs4.renameSync(tmp, file);
595
+ fs4.renameSync(tmp, file2);
596
596
  if (TERMINAL.includes(meta.state)) {
597
597
  try {
598
598
  onFinished?.(meta);
@@ -601,12 +601,12 @@ function writeMeta(meta) {
601
601
  }
602
602
  }
603
603
  function readMeta(id) {
604
- const file = runFile(id, "meta.json");
605
- if (!fs4.existsSync(file)) {
604
+ const file2 = runFile(id, "meta.json");
605
+ if (!fs4.existsSync(file2)) {
606
606
  const kept = archive?.meta(id);
607
607
  if (kept) return upgrade(kept);
608
608
  }
609
- return upgrade(JSON.parse(fs4.readFileSync(file, "utf8")));
609
+ return upgrade(JSON.parse(fs4.readFileSync(file2, "utf8")));
610
610
  }
611
611
  function upgrade(raw) {
612
612
  if (!raw.worker) raw.worker = raw.model ? { backend: "opencode", model: raw.model } : { backend: "opencode" };
@@ -727,8 +727,8 @@ function parseEvents3(jsonl) {
727
727
  tools[name] = (tools[name] ?? 0) + 1;
728
728
  const target = p.file_path ?? p.absolute_path ?? p.path ?? p.dir_path ?? p.pattern ?? p.command ?? p.query ?? p.url ?? p.prompt;
729
729
  lastActivity = `${name} ${target ? oneLine3(String(target), 60) : ""}`.trim();
730
- const file = p.file_path ?? p.absolute_path ?? p.path;
731
- if (EDIT_TOOLS2.has(name) && file) pendingEdits.set(String(e.tool_id), String(file));
730
+ const file2 = p.file_path ?? p.absolute_path ?? p.path;
731
+ if (EDIT_TOOLS2.has(name) && file2) pendingEdits.set(String(e.tool_id), String(file2));
732
732
  if (timeline.length < MAX_STEPS) {
733
733
  const step = { kind: EDIT_TOOLS2.has(name) ? "edit" : name === "run_shell_command" ? "shell" : "tool", name, text: clip3(String(target ?? ""), 240) };
734
734
  timeline.push(step);
@@ -788,13 +788,13 @@ function workerHome() {
788
788
  const auth = settings().security?.auth;
789
789
  const body = `${JSON.stringify(auth ? { ...base2, security: { ...base2.security, auth } } : base2, null, 2)}
790
790
  `;
791
- const file = path5.join(dir, "settings.json");
791
+ const file2 = path5.join(dir, "settings.json");
792
792
  try {
793
- if (fs5.readFileSync(file, "utf8") === body) return root;
793
+ if (fs5.readFileSync(file2, "utf8") === body) return root;
794
794
  } catch {
795
795
  }
796
796
  fs5.mkdirSync(dir, { recursive: true });
797
- fs5.writeFileSync(file, body);
797
+ fs5.writeFileSync(file2, body);
798
798
  for (const name of ["oauth_creds.json", "google_accounts.json"]) {
799
799
  const from = path5.join(geminiHome(), name);
800
800
  const to = path5.join(dir, name);
@@ -1416,7 +1416,8 @@ var SCHEMA = {
1416
1416
  models: "record",
1417
1417
  tiers: "record",
1418
1418
  costs: "numbers",
1419
- audit: "number"
1419
+ audit: "number",
1420
+ readIn: "string"
1420
1421
  };
1421
1422
  function configPath() {
1422
1423
  if (process.env.PITROOM_CONFIG) return path7.resolve(process.env.PITROOM_CONFIG);
@@ -1426,25 +1427,25 @@ function configPath() {
1426
1427
  var cached;
1427
1428
  function loadConfig() {
1428
1429
  if (cached) return cached;
1429
- const file = configPath();
1430
+ const file2 = configPath();
1430
1431
  const config = {};
1431
1432
  const warnings = [];
1432
- if (fs7.existsSync(file)) {
1433
+ if (fs7.existsSync(file2)) {
1433
1434
  let raw;
1434
1435
  try {
1435
- raw = JSON.parse(fs7.readFileSync(file, "utf8"));
1436
+ raw = JSON.parse(fs7.readFileSync(file2, "utf8"));
1436
1437
  } catch (e) {
1437
- warnings.push(`${file}: invalid JSON (${e.message}); ignored`);
1438
+ warnings.push(`${file2}: invalid JSON (${e.message}); ignored`);
1438
1439
  }
1439
1440
  if (raw && typeof raw === "object" && !Array.isArray(raw)) {
1440
1441
  for (const [key, value] of Object.entries(raw)) {
1441
1442
  const type = SCHEMA[key];
1442
1443
  if (!type) {
1443
- warnings.push(`${file}: unknown key "${key}" ignored`);
1444
+ warnings.push(`${file2}: unknown key "${key}" ignored`);
1444
1445
  } else if (matches(value, type)) {
1445
1446
  config[key] = value;
1446
1447
  } else {
1447
- warnings.push(`${file}: "${key}" must be ${type}; ignored`);
1448
+ warnings.push(`${file2}: "${key}" must be ${type}; ignored`);
1448
1449
  }
1449
1450
  }
1450
1451
  }
@@ -1474,6 +1475,7 @@ var rate = (v) => {
1474
1475
  const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
1475
1476
  return typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.min(n, 1) : void 0;
1476
1477
  };
1478
+ var readIn = (v) => v === "auto" || v === "snapshot" || v === "project" ? v : void 0;
1477
1479
  var list = (s) => s?.split(",").map((x) => x.trim()).filter(Boolean);
1478
1480
  function setting(flag2, env, conf, fallback) {
1479
1481
  if (flag2 !== void 0) return { value: flag2, source: "flag" };
@@ -1498,7 +1500,8 @@ function effective(flags = {}) {
1498
1500
  models: setting(void 0, void 0, c.models, {}),
1499
1501
  tiers: setting(void 0, void 0, c.tiers, {}),
1500
1502
  costs: setting(void 0, void 0, c.costs, {}),
1501
- audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0)
1503
+ audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0),
1504
+ readIn: setting(void 0, readIn(e.PITROOM_READ_IN), readIn(c.readIn), "auto")
1502
1505
  };
1503
1506
  }
1504
1507
 
@@ -1545,6 +1548,7 @@ var BOOL_FLAGS = {
1545
1548
  "-i": "isolate",
1546
1549
  "--isolate": "isolate",
1547
1550
  "--bg": "bg",
1551
+ "--in-place": "in-place",
1548
1552
  "--audit": "audit",
1549
1553
  "--no-audit": "no-audit",
1550
1554
  "--web": "web",
@@ -1556,6 +1560,7 @@ var BOOL_FLAGS = {
1556
1560
  "--full": "full",
1557
1561
  "--badge": "badge",
1558
1562
  "--probe": "probe",
1563
+ "--clear": "clear",
1559
1564
  "--copy": "copy",
1560
1565
  "--all": "all",
1561
1566
  "--models": "models",
@@ -1623,8 +1628,8 @@ function exitCodeFor(meta) {
1623
1628
  }
1624
1629
  }
1625
1630
  function readTask(p) {
1626
- const file = flag(p, "task-file");
1627
- if (file) return fs8.readFileSync(file, "utf8");
1631
+ const file2 = flag(p, "task-file");
1632
+ if (file2) return fs8.readFileSync(file2, "utf8");
1628
1633
  const words = p.positional;
1629
1634
  if (words.length === 1 && words[0] === "-") return fs8.readFileSync(0, "utf8");
1630
1635
  return words.join(" ");
@@ -1638,6 +1643,7 @@ function runOptions(p, task) {
1638
1643
  return {
1639
1644
  mode: modes[0] ?? "read",
1640
1645
  auditRate: has(p, "no-audit") ? 0 : has(p, "audit") ? 1 : void 0,
1646
+ inPlace: has(p, "in-place"),
1641
1647
  task,
1642
1648
  dir: flag(p, "dir") ?? process.cwd(),
1643
1649
  files: p.flags.get("file") ?? [],
@@ -1656,12 +1662,12 @@ function runOptions(p, task) {
1656
1662
  };
1657
1663
  }
1658
1664
  function planStep(p) {
1659
- const file = flag(p, "plan");
1665
+ const file2 = flag(p, "plan");
1660
1666
  const step = flag(p, "step");
1661
- if (!file && !step) return void 0;
1662
- if (!file || !step) throw new UserError("--plan and --step go together: pitroom run -i --plan PLAN.md --step N");
1667
+ if (!file2 && !step) return void 0;
1668
+ if (!file2 || !step) throw new UserError("--plan and --step go together: pitroom run -i --plan PLAN.md --step N");
1663
1669
  if (!/^\d+$/.test(step)) throw new UserError(`--step takes a task number, not "${step}"`);
1664
- return { file, step: Number(step) };
1670
+ return { file: file2, step: Number(step) };
1665
1671
  }
1666
1672
  function effortFlag(p) {
1667
1673
  const v = flag(p, "effort");
@@ -1671,8 +1677,8 @@ function effortFlag(p) {
1671
1677
 
1672
1678
  // src/cli/commands.ts
1673
1679
  import { spawnSync as spawnSync7 } from "node:child_process";
1674
- import fs27 from "node:fs";
1675
- import path23 from "node:path";
1680
+ import fs29 from "node:fs";
1681
+ import path25 from "node:path";
1676
1682
  import { fileURLToPath as fileURLToPath4 } from "node:url";
1677
1683
 
1678
1684
  // src/core/init.ts
@@ -1725,7 +1731,7 @@ var FREE = /-free$/;
1725
1731
  var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
1726
1732
  var isFound = (binary5) => path8.isAbsolute(binary5) && fs9.existsSync(binary5);
1727
1733
  function planInit(opts = {}) {
1728
- const file = configPath();
1734
+ const file2 = configPath();
1729
1735
  const workers = backendIds().map((id) => {
1730
1736
  const b = getBackend(id);
1731
1737
  const binary5 = b.binary();
@@ -1769,7 +1775,7 @@ function planInit(opts = {}) {
1769
1775
  if (Object.keys(tiers).length > 1) config.tiers = tiers;
1770
1776
  if (!workers.some((w) => w.found)) blocked = "no worker CLI found: install OpenCode (https://opencode.ai), Codex CLI, Claude Code or Gemini CLI first";
1771
1777
  else if (!blocked && !Object.keys(config).length) notes.push("nothing to propose: the default model and the single worker need no config");
1772
- return { path: file, exists: fs9.existsSync(file), workers, opencode: opencode2, config, blocked, notes };
1778
+ return { path: file2, exists: fs9.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
1773
1779
  }
1774
1780
  function writeInit(plan, force) {
1775
1781
  if (plan.blocked) throw new UserError(plan.blocked);
@@ -2139,8 +2145,8 @@ var ANSWER_MAX2 = 2e5;
2139
2145
  var cached2;
2140
2146
  var historyFile = () => path10.join(home(), "history.db");
2141
2147
  function openDb() {
2142
- const file = historyFile();
2143
- if (cached2?.file === file) return cached2.db;
2148
+ const file2 = historyFile();
2149
+ if (cached2?.file === file2) return cached2.db;
2144
2150
  let db;
2145
2151
  try {
2146
2152
  const emit = process.emitWarning;
@@ -2151,14 +2157,14 @@ function openDb() {
2151
2157
  } finally {
2152
2158
  process.emitWarning = emit;
2153
2159
  }
2154
- fs12.mkdirSync(path10.dirname(file), { recursive: true });
2155
- db = new DatabaseSync(file);
2160
+ fs12.mkdirSync(path10.dirname(file2), { recursive: true });
2161
+ db = new DatabaseSync(file2);
2156
2162
  db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
2157
2163
  migrate(db);
2158
2164
  } catch {
2159
2165
  db = void 0;
2160
2166
  }
2161
- cached2 = { file, db };
2167
+ cached2 = { file: file2, db };
2162
2168
  return db;
2163
2169
  }
2164
2170
  function migrate(db) {
@@ -2490,7 +2496,7 @@ function structure(lines) {
2490
2496
  });
2491
2497
  return { headings, rules, fenced };
2492
2498
  }
2493
- function parsePlan(text, file = "") {
2499
+ function parsePlan(text, file2 = "") {
2494
2500
  const lines = text.replace(/\r\n?/g, "\n").split("\n");
2495
2501
  const { headings, rules, fenced } = structure(lines);
2496
2502
  const after = (line) => Math.min(headings.find((h) => h.line > line)?.line ?? lines.length, rules.find((r) => r > line) ?? lines.length);
@@ -2509,13 +2515,13 @@ function parsePlan(text, file = "") {
2509
2515
  const tier = /^\s*[-*]?\s*\*\*Worker:\*\*\s*`?([\w-]+)`?/m.exec(unfenced)?.[1]?.toLowerCase();
2510
2516
  tasks.push({ step: Number(m[1]), title: m[2].trim(), text: body, ...tier ? { tier } : {} });
2511
2517
  }
2512
- return { file, title: titleHeading?.text ?? "", header, constraints, tasks };
2518
+ return { file: file2, title: titleHeading?.text ?? "", header, constraints, tasks };
2513
2519
  }
2514
- function loadPlan(file) {
2515
- const abs = path11.resolve(file);
2516
- if (!fs13.existsSync(abs) || !fs13.statSync(abs).isFile()) throw new UserError(`plan not found: ${file}`);
2520
+ function loadPlan(file2) {
2521
+ const abs = path11.resolve(file2);
2522
+ if (!fs13.existsSync(abs) || !fs13.statSync(abs).isFile()) throw new UserError(`plan not found: ${file2}`);
2517
2523
  const plan = parsePlan(fs13.readFileSync(abs, "utf8"), fs13.realpathSync(abs));
2518
- if (!plan.tasks.length) throw new UserError(`${file} has no "Task N" headings (see pitroom-writing-plans)`);
2524
+ if (!plan.tasks.length) throw new UserError(`${file2} has no "Task N" headings (see pitroom-writing-plans)`);
2519
2525
  return plan;
2520
2526
  }
2521
2527
  function planTask(plan, step) {
@@ -2533,7 +2539,7 @@ function brief(plan, task) {
2533
2539
  ].filter(Boolean).join("\n\n")}
2534
2540
  `;
2535
2541
  }
2536
- var planName = (file) => path11.basename(file).replace(/\.md$/i, "");
2542
+ var planName = (file2) => path11.basename(file2).replace(/\.md$/i, "");
2537
2543
 
2538
2544
  // src/core/report.ts
2539
2545
  var ICON2 = {
@@ -2568,7 +2574,7 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
2568
2574
  const status = meta.taskStatus ? ` \xB7 STATUS ${meta.taskStatus}` : "";
2569
2575
  out.push(`plan: ${planName(meta.plan.file)} \xB7 Task ${meta.plan.step}: ${meta.plan.title}${status}`);
2570
2576
  }
2571
- for (const a of meta.attempts ?? []) out.push(`fallback: ${a.target} failed (${a.error.slice(0, 160)})`);
2577
+ for (const a of meta.attempts ?? []) out.push(a.skipped ? `skipped: ${a.target} (${a.error.slice(0, 160)})` : `fallback: ${a.target} failed (${a.error.slice(0, 160)})`);
2572
2578
  if (meta.error) out.push(`error: ${meta.error}`);
2573
2579
  for (const w of meta.warnings) out.push(`warning: ${w}`);
2574
2580
  if (meta.state === "timeout") {
@@ -2789,10 +2795,10 @@ echo "pitroom: needs Node.js 22.13 or newer (set PITROOM_NODE to its path)" >&2
2789
2795
  exit 127
2790
2796
  `;
2791
2797
  }
2792
- function isOurLauncher(file, root) {
2793
- if (linksInto(file, root)) return true;
2798
+ function isOurLauncher(file2, root) {
2799
+ if (linksInto(file2, root)) return true;
2794
2800
  try {
2795
- const s = fs15.readFileSync(file, "utf8");
2801
+ const s = fs15.readFileSync(file2, "utf8");
2796
2802
  return s.includes(LAUNCHER_MARK) && s.includes(root);
2797
2803
  } catch {
2798
2804
  return false;
@@ -2967,10 +2973,10 @@ ${rulings}`,
2967
2973
  ].join("\n");
2968
2974
  }
2969
2975
 
2970
- // src/core/review.ts
2976
+ // src/core/snapshot.ts
2977
+ import fs19 from "node:fs";
2978
+ import path16 from "node:path";
2971
2979
  import crypto5 from "node:crypto";
2972
- import fs18 from "node:fs";
2973
- import path15 from "node:path";
2974
2980
 
2975
2981
  // src/vcs/git.ts
2976
2982
  import { spawnSync as spawnSync5 } from "node:child_process";
@@ -3011,14 +3017,18 @@ function repoRoot(dir) {
3011
3017
  const r = git(dir, ["rev-parse", "--show-toplevel"]);
3012
3018
  return r.code === 0 ? path14.resolve(r.stdout.trim()) : void 0;
3013
3019
  }
3014
- function snapshotTree(root, exclude = []) {
3020
+ function snapshotTree(root, exclude = [], drop = []) {
3015
3021
  const tmp = path14.join(os9.tmpdir(), `pitroom-index-${process.pid}-${crypto4.randomBytes(4).toString("hex")}`);
3016
3022
  const real = path14.resolve(root, must(root, ["rev-parse", "--git-path", "index"]).trim());
3017
3023
  const env = { ...process.env, GIT_INDEX_FILE: tmp };
3018
3024
  try {
3019
3025
  if (fs17.existsSync(real)) fs17.copyFileSync(real, tmp);
3020
3026
  else must(root, ["read-tree", "--empty"], env);
3021
- must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`)], env);
3027
+ must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`), ...drop.map((p) => `:(top,literal,exclude)${p}`)], env);
3028
+ if (drop.length) {
3029
+ const r = spawnSync5("git", ["update-index", "--force-remove", "-z", "--stdin"], { cwd: root, env, input: `${drop.join("\0")}\0`, encoding: "utf8" });
3030
+ if (r.status !== 0) throw new Error(`git update-index failed: ${r.stderr.trim()}`);
3031
+ }
3022
3032
  return must(root, ["write-tree"], env).trim();
3023
3033
  } finally {
3024
3034
  fs17.rmSync(tmp, { force: true });
@@ -3103,8 +3113,255 @@ function gitDir(dir) {
3103
3113
  return r.code === 0 ? r.stdout.trim() : void 0;
3104
3114
  }
3105
3115
 
3116
+ // src/core/secrets.ts
3117
+ import { execFileSync } from "node:child_process";
3118
+ import fs18 from "node:fs";
3119
+ import path15 from "node:path";
3120
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
3121
+ var MAX_VISITED = 2e4;
3122
+ var MAX_DEPTH = 4;
3123
+ var TEMPLATE = /\.(example|sample|template|dist|defaults?|tpl)$/i;
3124
+ function looksSecret(name) {
3125
+ if (TEMPLATE.test(name)) return false;
3126
+ if (name === ".env" || name.startsWith(".env.") || /\.env$/i.test(name)) return true;
3127
+ return /\.(pem|p12|pfx|key|jks|keystore)$/i.test(name) || /^id_(rsa|dsa|ecdsa|ed25519)$/.test(name) || name === ".netrc" || name === "credentials.json" || /^secrets?\.(json|ya?ml|toml)$/i.test(name);
3128
+ }
3129
+ function findSecretFiles(dir) {
3130
+ const found = [];
3131
+ let visited = 0;
3132
+ const walk = (d, depth) => {
3133
+ if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
3134
+ let entries;
3135
+ try {
3136
+ entries = fs18.readdirSync(d, { withFileTypes: true });
3137
+ } catch {
3138
+ return;
3139
+ }
3140
+ for (const e of entries) {
3141
+ visited++;
3142
+ if (e.isDirectory()) {
3143
+ if (!SKIP_DIRS.has(e.name)) walk(path15.join(d, e.name), depth + 1);
3144
+ } else if (e.isFile() && looksSecret(e.name)) {
3145
+ found.push(path15.relative(dir, path15.join(d, e.name)));
3146
+ }
3147
+ }
3148
+ };
3149
+ walk(dir, 0);
3150
+ return found.sort();
3151
+ }
3152
+ function findSecretFilesInTree(root, dir) {
3153
+ try {
3154
+ const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
3155
+ return out.split("\0").filter((f) => f && looksSecret(path15.basename(f)) && !path15.relative(dir, path15.join(root, f)).startsWith("..")).map((f) => path15.relative(dir, path15.join(root, f))).sort();
3156
+ } catch {
3157
+ return [];
3158
+ }
3159
+ }
3160
+ function secretWarning(files, mode) {
3161
+ if (!files.length) return void 0;
3162
+ const shown = files.slice(0, 3).join(", ") + (files.length > 3 ? `, +${files.length - 3} more` : "");
3163
+ const where = mode === "isolate" ? "would be copied into the isolated copy" : "sit in the directory the worker runs in";
3164
+ 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";
3165
+ 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`;
3166
+ }
3167
+
3168
+ // src/core/snapshot.ts
3169
+ var READ_IN = ["auto", "snapshot", "project"];
3170
+ var KEEP_MS = 24 * 36e5;
3171
+ var PRUNE_AGE_MS = 3 * 24 * 36e5;
3172
+ var KEEP_NEWEST = 6;
3173
+ var JUST_USED_MS = 10 * 6e4;
3174
+ var snapshotsDir = () => path16.join(home(), "snapshots");
3175
+ function wantSnapshot(readIn2, root, dir) {
3176
+ if (!root || readIn2 === "project") return void 0;
3177
+ const secrets = findSecretFiles(dir);
3178
+ return readIn2 === "snapshot" || secrets.length ? { secrets } : void 0;
3179
+ }
3180
+ function readSnapshot(root) {
3181
+ const drop = findSecretFilesInTree(root, root);
3182
+ const tree = snapshotTree(root, [], drop);
3183
+ const dest = path16.join(snapshotsDir(), tree);
3184
+ if (fs19.existsSync(path16.join(dest, ".git"))) {
3185
+ touch(dest);
3186
+ return { dir: dest, tree, created: false };
3187
+ }
3188
+ const tmp = path16.join(snapshotsDir(), `.tmp-${process.pid}-${crypto5.randomBytes(3).toString("hex")}`);
3189
+ try {
3190
+ createIsolatedCopy(root, tree, tmp);
3191
+ readOnly(tmp);
3192
+ try {
3193
+ fs19.renameSync(tmp, dest);
3194
+ } catch (e) {
3195
+ if (!fs19.existsSync(path16.join(dest, ".git"))) throw e;
3196
+ removeIsolatedCopy(tmp, snapshotsDir());
3197
+ }
3198
+ } catch (e) {
3199
+ try {
3200
+ removeIsolatedCopy(tmp, snapshotsDir());
3201
+ } catch {
3202
+ }
3203
+ throw e;
3204
+ }
3205
+ prune();
3206
+ return { dir: dest, tree, created: true };
3207
+ }
3208
+ function touch(dir) {
3209
+ try {
3210
+ const now = /* @__PURE__ */ new Date();
3211
+ fs19.utimesSync(dir, now, now);
3212
+ } catch {
3213
+ }
3214
+ }
3215
+ function readOnly(dir) {
3216
+ const walk = (d) => {
3217
+ for (const e of fs19.readdirSync(d, { withFileTypes: true })) {
3218
+ if (e.name === ".git") continue;
3219
+ const p = path16.join(d, e.name);
3220
+ if (e.isDirectory()) walk(p);
3221
+ else if (e.isFile()) fs19.chmodSync(p, fs19.statSync(p).mode & ~146);
3222
+ }
3223
+ };
3224
+ try {
3225
+ walk(dir);
3226
+ } catch {
3227
+ }
3228
+ }
3229
+ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWEST) {
3230
+ let names;
3231
+ try {
3232
+ names = fs19.readdirSync(snapshotsDir());
3233
+ } catch {
3234
+ return 0;
3235
+ }
3236
+ const inUse = /* @__PURE__ */ new Set();
3237
+ for (const id of listRunIds()) {
3238
+ try {
3239
+ const m = readMeta(id);
3240
+ if (m.snapshot?.dir && isActive(m.state)) inUse.add(path16.resolve(m.snapshot.dir));
3241
+ } catch {
3242
+ }
3243
+ }
3244
+ const entries = names.map((n) => {
3245
+ const dir = path16.join(snapshotsDir(), n);
3246
+ try {
3247
+ return { n, dir, age: now - fs19.statSync(dir).mtimeMs };
3248
+ } catch {
3249
+ return void 0;
3250
+ }
3251
+ }).filter((e) => !!e).sort((x, y) => x.age - y.age);
3252
+ let removed = 0;
3253
+ let rank = 0;
3254
+ for (const e of entries) {
3255
+ if (inUse.has(path16.resolve(e.dir))) continue;
3256
+ const half = e.n.startsWith(".tmp-");
3257
+ const old = e.age >= (half ? KEEP_MS : maxAgeMs);
3258
+ const surplus = !half && ++rank > keepNewest && e.age >= JUST_USED_MS;
3259
+ if (!old && !surplus) continue;
3260
+ try {
3261
+ makeWritable(e.dir);
3262
+ removeIsolatedCopy(e.dir, snapshotsDir());
3263
+ removed++;
3264
+ } catch {
3265
+ }
3266
+ }
3267
+ return removed;
3268
+ }
3269
+ function makeWritable(dir) {
3270
+ const walk = (d) => {
3271
+ for (const e of fs19.readdirSync(d, { withFileTypes: true })) {
3272
+ const p = path16.join(d, e.name);
3273
+ if (e.isDirectory()) walk(p);
3274
+ else if (e.isFile()) fs19.chmodSync(p, fs19.statSync(p).mode | 128);
3275
+ }
3276
+ };
3277
+ try {
3278
+ walk(dir);
3279
+ } catch {
3280
+ }
3281
+ }
3282
+ function backToProject(text, snapshotDir, root) {
3283
+ let real = snapshotDir;
3284
+ try {
3285
+ real = fs19.realpathSync(snapshotDir);
3286
+ } catch {
3287
+ }
3288
+ let out = text;
3289
+ for (const dir of /* @__PURE__ */ new Set([real, snapshotDir])) out = out.split(`${dir}${path16.sep}`).join(`${root}${path16.sep}`).split(dir).join(root);
3290
+ return out;
3291
+ }
3292
+
3293
+ // src/core/cooldown.ts
3294
+ import fs20 from "node:fs";
3295
+ import path17 from "node:path";
3296
+ var MAX_MS = 24 * 36e5;
3297
+ var OVERLOAD_MS = 20 * 6e4;
3298
+ var DAILY_MS = 4 * 36e5;
3299
+ var file = () => path17.join(home(), "cooldowns.json");
3300
+ function cooldownKey(target, backend) {
3301
+ const model = (target.model ?? backend.defaultModel?.() ?? "").split("#")[0];
3302
+ return `${target.backend}:${model}`;
3303
+ }
3304
+ function retryAfterMs(message) {
3305
+ const compact3 = /(?:retry|try again|resets?|available again)[^0-9]{0,24}((?:\d+(?:\.\d+)?\s*[dhms]\s*)+)/i.exec(message)?.[1];
3306
+ const words = /(?:retry|try again|resets?)[^0-9]{0,24}(\d+(?:\.\d+)?)\s*(second|minute|hour)s?/i.exec(message);
3307
+ let ms = 0;
3308
+ if (compact3) {
3309
+ for (const [, n, unit] of compact3.matchAll(/(\d+(?:\.\d+)?)\s*([dhms])/gi)) ms += Number(n) * { d: 864e5, h: 36e5, m: 6e4, s: 1e3 }[unit.toLowerCase()];
3310
+ } else if (words) {
3311
+ ms = Number(words[1]) * { second: 1e3, minute: 6e4, hour: 36e5 }[words[2].toLowerCase()];
3312
+ }
3313
+ return ms > 0 ? ms : void 0;
3314
+ }
3315
+ function cooldownMs(message) {
3316
+ const told = retryAfterMs(message);
3317
+ if (told !== void 0) return Math.min(Math.max(told, 6e4), MAX_MS);
3318
+ return /daily|per.?day|per day|exhausted your/i.test(message) ? DAILY_MS : OVERLOAD_MS;
3319
+ }
3320
+ function read() {
3321
+ try {
3322
+ const raw = JSON.parse(fs20.readFileSync(file(), "utf8"));
3323
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
3324
+ } catch {
3325
+ return {};
3326
+ }
3327
+ }
3328
+ function write(all) {
3329
+ fs20.mkdirSync(path17.dirname(file()), { recursive: true });
3330
+ const tmp = `${file()}.${process.pid}.tmp`;
3331
+ fs20.writeFileSync(tmp, `${JSON.stringify(all, null, 2)}
3332
+ `);
3333
+ fs20.renameSync(tmp, file());
3334
+ }
3335
+ function activeCooldowns(now = Date.now()) {
3336
+ return Object.fromEntries(Object.entries(read()).filter(([, c]) => Date.parse(c.until) > now));
3337
+ }
3338
+ var activeCooldown = (key, now = Date.now()) => activeCooldowns(now)[key];
3339
+ function recordCooldown(key, targetName, message, now = Date.now()) {
3340
+ try {
3341
+ const all = activeCooldowns(now);
3342
+ all[key] = { until: new Date(now + cooldownMs(message)).toISOString(), reason: message.replace(/\s+/g, " ").trim().slice(0, 160), target: targetName };
3343
+ write(all);
3344
+ } catch {
3345
+ }
3346
+ }
3347
+ function clearCooldowns(key) {
3348
+ const all = read();
3349
+ const keys = key ? Object.keys(all).filter((k) => k === key) : Object.keys(all);
3350
+ for (const k of keys) delete all[k];
3351
+ if (keys.length) write(all);
3352
+ return keys.length;
3353
+ }
3354
+ function untilText(c, now = Date.now()) {
3355
+ const d = new Date(c.until);
3356
+ const time = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
3357
+ return d.toDateString() === new Date(now).toDateString() ? `until ${time}` : `until ${d.toLocaleDateString()} ${time}`;
3358
+ }
3359
+
3106
3360
  // src/core/review.ts
3107
- var TEMPLATE = { task: "task-reviewer", fix: "re-review", range: "code-reviewer" };
3361
+ import crypto6 from "node:crypto";
3362
+ import fs21 from "node:fs";
3363
+ import path18 from "node:path";
3364
+ var TEMPLATE2 = { task: "task-reviewer", fix: "re-review", range: "code-reviewer" };
3108
3365
  var section = (title, body) => `## ${title}
3109
3366
 
3110
3367
  ${body.trim() || "(empty)"}
@@ -3121,7 +3378,7 @@ function ancestors(m) {
3121
3378
  function briefOf(m) {
3122
3379
  const root = [m, ...ancestors(m)].at(-1);
3123
3380
  const f = runFile(root.id, "brief.md");
3124
- return fs18.existsSync(f) ? fs18.readFileSync(f, "utf8") : root.task;
3381
+ return fs21.existsSync(f) ? fs21.readFileSync(f, "utf8") : root.task;
3125
3382
  }
3126
3383
  function latestReview(ids) {
3127
3384
  for (const id of listRunIds().reverse()) {
@@ -3135,7 +3392,7 @@ function latestReview(ids) {
3135
3392
  }
3136
3393
  function diffOf(m, from = m.baseTree) {
3137
3394
  const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
3138
- if (where && fs18.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
3395
+ if (where && fs21.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
3139
3396
  if (from !== m.baseTree) {
3140
3397
  throw new UserError(`the isolated copy of ${m.id} is gone, so its fix round cannot be shown on its own; review a run that is not applied yet`, 3);
3141
3398
  }
@@ -3149,7 +3406,7 @@ function runReview(id) {
3149
3406
  if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
3150
3407
  const chain = ancestors(m);
3151
3408
  const previous = latestReview(chain.map((a) => a.id));
3152
- const dir = m.mode === "isolate" && m.worktree && fs18.existsSync(m.worktree) ? m.cwd : m.dir;
3409
+ const dir = m.mode === "isolate" && m.worktree && fs21.existsSync(m.worktree) ? m.cwd : m.dir;
3153
3410
  const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
3154
3411
  const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
3155
3412
  if (!previous) {
@@ -3223,15 +3480,15 @@ function pickReviewer(job) {
3223
3480
  function writePackage(job) {
3224
3481
  const g = gitDir(job.dir);
3225
3482
  if (!g) throw new UserError(`not a git repository: ${job.dir}`);
3226
- const file = path15.join(g, "pitroom", `review-${crypto5.randomBytes(4).toString("hex")}.md`);
3227
- fs18.mkdirSync(path15.dirname(file), { recursive: true });
3228
- fs18.writeFileSync(file, job.package);
3229
- return file;
3483
+ const file2 = path18.join(g, "pitroom", `review-${crypto6.randomBytes(4).toString("hex")}.md`);
3484
+ fs21.mkdirSync(path18.dirname(file2), { recursive: true });
3485
+ fs21.writeFileSync(file2, job.package);
3486
+ return file2;
3230
3487
  }
3231
3488
 
3232
3489
  // src/core/templates.ts
3233
- import fs19 from "node:fs";
3234
- import path16 from "node:path";
3490
+ import fs22 from "node:fs";
3491
+ import path19 from "node:path";
3235
3492
  var FILES = {
3236
3493
  implementer: "pitroom-driven-development/implementer-prompt.md",
3237
3494
  "task-reviewer": "pitroom-driven-development/task-reviewer-prompt.md",
@@ -3239,9 +3496,9 @@ var FILES = {
3239
3496
  "code-reviewer": "pitroom-review/code-reviewer.md"
3240
3497
  };
3241
3498
  function loadTemplate(name, root = packageRoot()) {
3242
- const file = path16.join(root, "skills", FILES[name]);
3243
- if (!fs19.existsSync(file)) throw new UserError(`template missing: ${file} (broken install? run pitroom doctor)`, 3);
3244
- return fs19.readFileSync(file, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
3499
+ const file2 = path19.join(root, "skills", FILES[name]);
3500
+ if (!fs22.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
3501
+ return fs22.readFileSync(file2, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
3245
3502
  }
3246
3503
  function fill(template, values) {
3247
3504
  const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
@@ -3347,16 +3604,16 @@ function formatModels(t) {
3347
3604
 
3348
3605
  // src/core/dash.ts
3349
3606
  import { spawn } from "node:child_process";
3350
- import crypto6 from "node:crypto";
3351
- import fs20 from "node:fs";
3607
+ import crypto7 from "node:crypto";
3608
+ import fs23 from "node:fs";
3352
3609
  import http from "node:http";
3353
- import path17 from "node:path";
3610
+ import path20 from "node:path";
3354
3611
  import { fileURLToPath as fileURLToPath3 } from "node:url";
3355
3612
 
3356
3613
  // src/core/file-diff.ts
3357
3614
  var PREVIEW_LINES = 300;
3358
3615
  function gitPath(value, prefix = true) {
3359
- let path25 = value;
3616
+ let path27 = value;
3360
3617
  if (value.startsWith('"') && value.endsWith('"')) {
3361
3618
  const bytes = [];
3362
3619
  const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
@@ -3364,9 +3621,9 @@ function gitPath(value, prefix = true) {
3364
3621
  if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
3365
3622
  else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
3366
3623
  }
3367
- path25 = new TextDecoder().decode(new Uint8Array(bytes));
3624
+ path27 = new TextDecoder().decode(new Uint8Array(bytes));
3368
3625
  }
3369
- return prefix ? path25.replace(/^[ab]\//, "") : path25;
3626
+ return prefix ? path27.replace(/^[ab]\//, "") : path27;
3370
3627
  }
3371
3628
  function headerPath(header) {
3372
3629
  const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
@@ -3375,7 +3632,7 @@ function headerPath(header) {
3375
3632
  }
3376
3633
  function fileDiffs(patch, changes, sourceTruncated = false) {
3377
3634
  const files = [];
3378
- let file;
3635
+ let file2;
3379
3636
  let oldLine = 0;
3380
3637
  let newLine = 0;
3381
3638
  let oldLeft = 0;
@@ -3383,25 +3640,25 @@ function fileDiffs(patch, changes, sourceTruncated = false) {
3383
3640
  let inHunk = false;
3384
3641
  let row = 0;
3385
3642
  const finishHunk = () => {
3386
- if (file && (oldLeft > 0 || newLeft > 0)) file.incomplete = true;
3643
+ if (file2 && (oldLeft > 0 || newLeft > 0)) file2.incomplete = true;
3387
3644
  oldLeft = newLeft = 0;
3388
3645
  inHunk = false;
3389
3646
  };
3390
3647
  const add = (line) => {
3391
- if (!file) return;
3392
- if (file.lines.length < PREVIEW_LINES) file.lines.push({ ...line, id: String(row) });
3393
- else file.omittedLines++;
3648
+ if (!file2) return;
3649
+ if (file2.lines.length < PREVIEW_LINES) file2.lines.push({ ...line, id: String(row) });
3650
+ else file2.omittedLines++;
3394
3651
  row++;
3395
3652
  };
3396
3653
  for (const line of patch.split("\n")) {
3397
3654
  if (line.startsWith("diff --git ")) {
3398
3655
  finishHunk();
3399
- file = { path: headerPath(line), status: "M", lines: [], additions: 0, deletions: 0, omittedLines: 0, binary: false, incomplete: false };
3400
- files.push(file);
3656
+ file2 = { path: headerPath(line), status: "M", lines: [], additions: 0, deletions: 0, omittedLines: 0, binary: false, incomplete: false };
3657
+ files.push(file2);
3401
3658
  row = 0;
3402
3659
  continue;
3403
3660
  }
3404
- if (!file) continue;
3661
+ if (!file2) continue;
3405
3662
  const hunk = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line);
3406
3663
  if (hunk) {
3407
3664
  finishHunk();
@@ -3415,11 +3672,11 @@ function fileDiffs(patch, changes, sourceTruncated = false) {
3415
3672
  add({ type: "meta", content: line });
3416
3673
  } else if (inHunk && (oldLeft > 0 || newLeft > 0)) {
3417
3674
  if (line.startsWith("+") && newLeft > 0) {
3418
- file.additions++;
3675
+ file2.additions++;
3419
3676
  newLeft--;
3420
3677
  add({ type: "added", newLine: newLine++, content: line.slice(1) });
3421
3678
  } else if (line.startsWith("-") && oldLeft > 0) {
3422
- file.deletions++;
3679
+ file2.deletions++;
3423
3680
  oldLeft--;
3424
3681
  add({ type: "removed", oldLine: oldLine++, content: line.slice(1) });
3425
3682
  } else if (line.startsWith(" ") && oldLeft > 0 && newLeft > 0) {
@@ -3427,25 +3684,25 @@ function fileDiffs(patch, changes, sourceTruncated = false) {
3427
3684
  newLeft--;
3428
3685
  add({ type: "context", oldLine: oldLine++, newLine: newLine++, content: line.slice(1) });
3429
3686
  } else {
3430
- file.incomplete = true;
3687
+ file2.incomplete = true;
3431
3688
  }
3432
3689
  } else if (!inHunk) {
3433
- if (line.startsWith("+++ ") && line !== "+++ /dev/null") file.path = gitPath(line.slice(4).replace(/\t$/, ""));
3434
- else if (line.startsWith("--- ") && line !== "--- /dev/null") file.path = gitPath(line.slice(4).replace(/\t$/, ""));
3435
- else if (/^(GIT binary patch|Binary files .* differ)$/.test(line)) file.binary = true;
3690
+ if (line.startsWith("+++ ") && line !== "+++ /dev/null") file2.path = gitPath(line.slice(4).replace(/\t$/, ""));
3691
+ else if (line.startsWith("--- ") && line !== "--- /dev/null") file2.path = gitPath(line.slice(4).replace(/\t$/, ""));
3692
+ else if (/^(GIT binary patch|Binary files .* differ)$/.test(line)) file2.binary = true;
3436
3693
  else if (/^(new file mode|deleted file mode|old mode|new mode|rename from|rename to|similarity index) /.test(line)) {
3437
- if (line.startsWith("new file mode ")) file.status = "A";
3438
- if (line.startsWith("deleted file mode ")) file.status = "D";
3694
+ if (line.startsWith("new file mode ")) file2.status = "A";
3695
+ if (line.startsWith("deleted file mode ")) file2.status = "D";
3439
3696
  if (line.startsWith("rename to ")) {
3440
- file.path = gitPath(line.slice(10), false);
3441
- file.status = "R";
3697
+ file2.path = gitPath(line.slice(10), false);
3698
+ file2.status = "R";
3442
3699
  }
3443
3700
  add({ type: "meta", content: line });
3444
3701
  }
3445
3702
  }
3446
3703
  }
3447
3704
  finishHunk();
3448
- if (sourceTruncated && file) file.incomplete = true;
3705
+ if (sourceTruncated && file2) file2.incomplete = true;
3449
3706
  for (const f of files) {
3450
3707
  if (f.incomplete || f.binary) f.additions = f.deletions = void 0;
3451
3708
  }
@@ -3607,32 +3864,32 @@ var HEADERS = {
3607
3864
  "x-content-type-options": "nosniff",
3608
3865
  "referrer-policy": "no-referrer",
3609
3866
  // Own files only, plus the hash of the one inline theme script; styles may be inline (the components position popups with them).
3610
- "content-security-policy": `default-src 'none'; script-src 'self' 'sha256-${crypto6.createHash("sha256").update(THEME_SCRIPT).digest("base64")}'; style-src 'self' 'unsafe-inline'; img-src data:; connect-src 'self'; base-uri 'none'; form-action 'none'`
3867
+ "content-security-policy": `default-src 'none'; script-src 'self' 'sha256-${crypto7.createHash("sha256").update(THEME_SCRIPT).digest("base64")}'; style-src 'self' 'unsafe-inline'; img-src data:; connect-src 'self'; base-uri 'none'; form-action 'none'`
3611
3868
  };
3612
3869
  var ASSETS = { "/assets/app.js": "text/javascript; charset=utf-8", "/assets/app.css": "text/css; charset=utf-8" };
3613
3870
  var assetsDir = () => fileURLToPath3(new URL("./ui/", import.meta.url));
3614
3871
  var assetCache = /* @__PURE__ */ new Map();
3615
3872
  function readAsset(route) {
3616
3873
  try {
3617
- const file = path17.join(assetsDir(), path17.basename(route));
3618
- const { mtimeMs } = fs20.statSync(file);
3874
+ const file2 = path20.join(assetsDir(), path20.basename(route));
3875
+ const { mtimeMs } = fs23.statSync(file2);
3619
3876
  const hit = assetCache.get(route);
3620
3877
  if (hit && hit.mtimeMs === mtimeMs) return hit.data;
3621
- const data = fs20.readFileSync(file);
3878
+ const data = fs23.readFileSync(file2);
3622
3879
  assetCache.set(route, { mtimeMs, data });
3623
3880
  return data;
3624
3881
  } catch {
3625
3882
  return void 0;
3626
3883
  }
3627
3884
  }
3628
- function handler(touch) {
3885
+ function handler(touch2) {
3629
3886
  const send = (res, code, type, body) => {
3630
3887
  res.writeHead(code, { ...HEADERS, "content-type": type });
3631
3888
  res.end(body);
3632
3889
  };
3633
3890
  const json = (res, code, value) => send(res, code, "application/json; charset=utf-8", JSON.stringify(value));
3634
3891
  return (req, res) => {
3635
- touch();
3892
+ touch2();
3636
3893
  if (!LOCAL_HOST.test(req.headers.host ?? "")) return json(res, 403, { error: "forbidden host" });
3637
3894
  if (req.method !== "GET" && req.method !== "HEAD") return json(res, 405, { error: "read-only" });
3638
3895
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -3698,7 +3955,7 @@ function startDash(opts) {
3698
3955
  });
3699
3956
  });
3700
3957
  }
3701
- var registryFile = () => path17.join(home(), "dash.json");
3958
+ var registryFile = () => path20.join(home(), "dash.json");
3702
3959
  async function ping(port) {
3703
3960
  try {
3704
3961
  const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
@@ -3709,11 +3966,11 @@ async function ping(port) {
3709
3966
  }
3710
3967
  async function runningDash() {
3711
3968
  try {
3712
- const r = JSON.parse(fs20.readFileSync(registryFile(), "utf8"));
3969
+ const r = JSON.parse(fs23.readFileSync(registryFile(), "utf8"));
3713
3970
  if (isAlive(r.pid) && await ping(r.port)) return { ...r, url: `http://127.0.0.1:${r.port}/` };
3714
3971
  } catch {
3715
3972
  }
3716
- fs20.rmSync(registryFile(), { force: true });
3973
+ fs23.rmSync(registryFile(), { force: true });
3717
3974
  return void 0;
3718
3975
  }
3719
3976
  function openBrowser(url) {
@@ -3728,7 +3985,7 @@ async function dashCommand(o) {
3728
3985
  if (o.stop) {
3729
3986
  if (!existing) throw new UserError("pitroom dash is not running");
3730
3987
  process.kill(existing.pid, "SIGTERM");
3731
- fs20.rmSync(registryFile(), { force: true });
3988
+ fs23.rmSync(registryFile(), { force: true });
3732
3989
  o.log(`stopped pitroom dash (${existing.url})`);
3733
3990
  return 0;
3734
3991
  }
@@ -3765,8 +4022,8 @@ async function dashCommand(o) {
3765
4022
  if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
3766
4023
  dash = await startDash({ port: 0, idleMs: o.idleMs });
3767
4024
  }
3768
- fs20.mkdirSync(home(), { recursive: true });
3769
- fs20.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
4025
+ fs23.mkdirSync(home(), { recursive: true });
4026
+ fs23.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
3770
4027
  const stop = () => void dash.close();
3771
4028
  process.once("SIGINT", stop);
3772
4029
  process.once("SIGTERM", stop);
@@ -3775,7 +4032,7 @@ async function dashCommand(o) {
3775
4032
  if (o.open) openBrowser(dash.url);
3776
4033
  await dash.closed;
3777
4034
  try {
3778
- if (JSON.parse(fs20.readFileSync(registryFile(), "utf8")).pid === process.pid) fs20.rmSync(registryFile(), { force: true });
4035
+ if (JSON.parse(fs23.readFileSync(registryFile(), "utf8")).pid === process.pid) fs23.rmSync(registryFile(), { force: true });
3779
4036
  } catch {
3780
4037
  }
3781
4038
  return 0;
@@ -3783,8 +4040,8 @@ async function dashCommand(o) {
3783
4040
 
3784
4041
  // src/core/run.ts
3785
4042
  import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
3786
- import fs26 from "node:fs";
3787
- import path22 from "node:path";
4043
+ import fs28 from "node:fs";
4044
+ import path24 from "node:path";
3788
4045
 
3789
4046
  // src/core/chain.ts
3790
4047
  function resolveChain(flags = {}) {
@@ -3834,11 +4091,11 @@ function resolveChain(flags = {}) {
3834
4091
 
3835
4092
  // src/core/process.ts
3836
4093
  import { spawn as spawn2 } from "node:child_process";
3837
- import fs22 from "node:fs";
4094
+ import fs25 from "node:fs";
3838
4095
 
3839
4096
  // src/vcs/guard.ts
3840
- import fs21 from "node:fs";
3841
- import path18 from "node:path";
4097
+ import fs24 from "node:fs";
4098
+ import path21 from "node:path";
3842
4099
  var VERSION = 1;
3843
4100
  var SHIM = `#!/bin/sh
3844
4101
  # pitroom git guard v${VERSION}. Blocks git commands that change history, refs, the
@@ -3913,15 +4170,15 @@ esac
3913
4170
  exec "$real" "$@"
3914
4171
  `;
3915
4172
  function shimDir() {
3916
- return path18.join(home(), "shim", `v${VERSION}`);
4173
+ return path21.join(home(), "shim", `v${VERSION}`);
3917
4174
  }
3918
4175
  function findRealGit(skip) {
3919
- for (const dir of (process.env.PATH ?? "").split(path18.delimiter).filter(Boolean)) {
3920
- if (path18.resolve(dir) === skip) continue;
3921
- const p = path18.join(dir, "git");
4176
+ for (const dir of (process.env.PATH ?? "").split(path21.delimiter).filter(Boolean)) {
4177
+ if (path21.resolve(dir) === skip) continue;
4178
+ const p = path21.join(dir, "git");
3922
4179
  try {
3923
- fs21.accessSync(p, fs21.constants.X_OK);
3924
- if (fs21.statSync(p).isFile()) return p;
4180
+ fs24.accessSync(p, fs24.constants.X_OK);
4181
+ if (fs24.statSync(p).isFile()) return p;
3925
4182
  } catch {
3926
4183
  }
3927
4184
  }
@@ -3934,13 +4191,13 @@ var REF_HOOK = `#!/bin/sh
3934
4191
  echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
3935
4192
  exit 1
3936
4193
  `;
3937
- var hooksDir = () => path18.join(home(), "git-hooks", `v${HOOK_VERSION}`);
3938
- function writeIfChanged(file, content) {
3939
- if (fs21.existsSync(file) && fs21.readFileSync(file, "utf8") === content) return;
3940
- fs21.mkdirSync(path18.dirname(file), { recursive: true });
3941
- const tmp = `${file}.${process.pid}.tmp`;
3942
- fs21.writeFileSync(tmp, content, { mode: 493 });
3943
- fs21.renameSync(tmp, file);
4194
+ var hooksDir = () => path21.join(home(), "git-hooks", `v${HOOK_VERSION}`);
4195
+ function writeIfChanged(file2, content) {
4196
+ if (fs24.existsSync(file2) && fs24.readFileSync(file2, "utf8") === content) return;
4197
+ fs24.mkdirSync(path21.dirname(file2), { recursive: true });
4198
+ const tmp = `${file2}.${process.pid}.tmp`;
4199
+ fs24.writeFileSync(tmp, content, { mode: 493 });
4200
+ fs24.renameSync(tmp, file2);
3944
4201
  }
3945
4202
  function withGitConfig(env, entries) {
3946
4203
  const out = { ...env };
@@ -3962,7 +4219,7 @@ function guardEnv(env) {
3962
4219
  PAGER: "cat"
3963
4220
  };
3964
4221
  if (process.platform === "win32") return { ...env, ...quiet };
3965
- writeIfChanged(path18.join(hooksDir(), "reference-transaction"), REF_HOOK);
4222
+ writeIfChanged(path21.join(hooksDir(), "reference-transaction"), REF_HOOK);
3966
4223
  env = withGitConfig(env, [
3967
4224
  ["core.hooksPath", hooksDir()],
3968
4225
  ["url.pitroom-push-blocked://.pushInsteadOf", ""]
@@ -3970,14 +4227,14 @@ function guardEnv(env) {
3970
4227
  const dir = shimDir();
3971
4228
  const real = findRealGit(dir);
3972
4229
  if (!real) return { ...env, ...quiet };
3973
- writeIfChanged(path18.join(dir, "git"), SHIM);
3974
- return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${path18.delimiter}${env.PATH ?? ""}` };
4230
+ writeIfChanged(path21.join(dir, "git"), SHIM);
4231
+ return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${path21.delimiter}${env.PATH ?? ""}` };
3975
4232
  }
3976
4233
 
3977
4234
  // src/core/process.ts
3978
4235
  async function spawnWorker(inv, opts) {
3979
- const out = fs22.openSync(opts.stdoutFile, "w");
3980
- const err = fs22.openSync(opts.stderrFile, "w");
4236
+ const out = fs25.openSync(opts.stdoutFile, "w");
4237
+ const err = fs25.openSync(opts.stderrFile, "w");
3981
4238
  const res = { code: null, timedOut: false, stopped: false };
3982
4239
  const child = spawn2(inv.command, inv.args, {
3983
4240
  cwd: opts.cwd,
@@ -4009,8 +4266,8 @@ async function spawnWorker(inv, opts) {
4009
4266
  if (killer) clearTimeout(killer);
4010
4267
  process.off("SIGINT", stop);
4011
4268
  process.off("SIGTERM", stop);
4012
- fs22.closeSync(out);
4013
- fs22.closeSync(err);
4269
+ fs25.closeSync(out);
4270
+ fs25.closeSync(err);
4014
4271
  return res;
4015
4272
  }
4016
4273
 
@@ -4042,11 +4299,11 @@ function parseAudit(text) {
4042
4299
  }
4043
4300
 
4044
4301
  // src/core/slots.ts
4045
- import crypto7 from "node:crypto";
4046
- import fs23 from "node:fs";
4047
- import path19 from "node:path";
4048
- var slotsDir = () => path19.join(home(), "slots");
4049
- var locksDir = () => path19.join(home(), "locks");
4302
+ import crypto8 from "node:crypto";
4303
+ import fs26 from "node:fs";
4304
+ import path22 from "node:path";
4305
+ var slotsDir = () => path22.join(home(), "slots");
4306
+ var locksDir = () => path22.join(home(), "locks");
4050
4307
  var STARTUP_GRACE_MS = 3e4;
4051
4308
  function holderActive(runId, self) {
4052
4309
  if (!runId || runId === self) return false;
@@ -4059,51 +4316,51 @@ function holderActive(runId, self) {
4059
4316
  return false;
4060
4317
  }
4061
4318
  }
4062
- function tryClaim(file, runId) {
4063
- fs23.mkdirSync(path19.dirname(file), { recursive: true });
4319
+ function tryClaim(file2, runId) {
4320
+ fs26.mkdirSync(path22.dirname(file2), { recursive: true });
4064
4321
  try {
4065
- fs23.writeFileSync(file, runId, { flag: "wx" });
4322
+ fs26.writeFileSync(file2, runId, { flag: "wx" });
4066
4323
  return true;
4067
4324
  } catch (e) {
4068
4325
  if (e.code !== "EEXIST") throw e;
4069
4326
  }
4070
4327
  let owner = "";
4071
4328
  try {
4072
- owner = fs23.readFileSync(file, "utf8").trim();
4329
+ owner = fs26.readFileSync(file2, "utf8").trim();
4073
4330
  } catch {
4074
4331
  }
4075
4332
  if (owner === runId) return true;
4076
4333
  if (holderActive(owner, runId)) return false;
4077
- fs23.rmSync(file, { force: true });
4334
+ fs26.rmSync(file2, { force: true });
4078
4335
  try {
4079
- fs23.writeFileSync(file, runId, { flag: "wx" });
4336
+ fs26.writeFileSync(file2, runId, { flag: "wx" });
4080
4337
  return true;
4081
4338
  } catch {
4082
4339
  return false;
4083
4340
  }
4084
4341
  }
4085
- function releaseIfOwner(file, runId) {
4086
- if (!file) return;
4342
+ function releaseIfOwner(file2, runId) {
4343
+ if (!file2) return;
4087
4344
  try {
4088
- if (fs23.readFileSync(file, "utf8").trim() === runId) fs23.rmSync(file, { force: true });
4345
+ if (fs26.readFileSync(file2, "utf8").trim() === runId) fs26.rmSync(file2, { force: true });
4089
4346
  } catch {
4090
4347
  }
4091
4348
  }
4092
4349
  function tryAcquireSlot(runId, maxParallel) {
4093
4350
  for (let n = 0; n < Math.max(1, maxParallel); n++) {
4094
- const file = path19.join(slotsDir(), `slot-${n}`);
4095
- if (tryClaim(file, runId)) return file;
4351
+ const file2 = path22.join(slotsDir(), `slot-${n}`);
4352
+ if (tryClaim(file2, runId)) return file2;
4096
4353
  }
4097
4354
  return void 0;
4098
4355
  }
4099
- var releaseSlot = (file, runId) => releaseIfOwner(file, runId);
4100
- var lockFile = (repoRoot2) => path19.join(locksDir(), `write-${crypto7.createHash("sha1").update(path19.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
4356
+ var releaseSlot = (file2, runId) => releaseIfOwner(file2, runId);
4357
+ var lockFile = (repoRoot2) => path22.join(locksDir(), `write-${crypto8.createHash("sha1").update(path22.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
4101
4358
  function acquireWriteLock(repoRoot2, runId) {
4102
- const file = lockFile(repoRoot2);
4103
- if (tryClaim(file, runId)) return;
4359
+ const file2 = lockFile(repoRoot2);
4360
+ if (tryClaim(file2, runId)) return;
4104
4361
  let owner = "";
4105
4362
  try {
4106
- owner = fs23.readFileSync(file, "utf8").trim();
4363
+ owner = fs26.readFileSync(file2, "utf8").trim();
4107
4364
  } catch {
4108
4365
  }
4109
4366
  throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
@@ -4150,12 +4407,12 @@ ${task}`;
4150
4407
  }
4151
4408
 
4152
4409
  // src/core/refs.ts
4153
- import fs24 from "node:fs";
4154
- import path20 from "node:path";
4410
+ import fs27 from "node:fs";
4411
+ import path23 from "node:path";
4155
4412
  var FILE_EXTENSIONS = new Set(
4156
4413
  "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(" ")
4157
4414
  );
4158
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
4415
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
4159
4416
  var MAX_INDEXED = 6e4;
4160
4417
  var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
4161
4418
  var REF = new RegExp(
@@ -4202,29 +4459,29 @@ function extractRefs(answer) {
4202
4459
  return [...seen.values()];
4203
4460
  }
4204
4461
  function verifyRefs(refs, dirs) {
4205
- const roots = [...new Set(dirs.filter(Boolean).map((d) => path20.resolve(d)))];
4462
+ const roots = [...new Set(dirs.filter(Boolean).map((d) => path23.resolve(d)))];
4206
4463
  const invalid = [];
4207
4464
  const lineCache = /* @__PURE__ */ new Map();
4208
- const load = (file) => {
4209
- if (!lineCache.has(file)) {
4210
- const size = fs24.statSync(file).size;
4211
- const lines = size > MAX_BYTES ? null : fs24.readFileSync(file, "utf8").split("\n");
4465
+ const load = (file2) => {
4466
+ if (!lineCache.has(file2)) {
4467
+ const size = fs27.statSync(file2).size;
4468
+ const lines = size > MAX_BYTES ? null : fs27.readFileSync(file2, "utf8").split("\n");
4212
4469
  if (lines && lines[lines.length - 1] === "") lines.pop();
4213
- lineCache.set(file, lines);
4470
+ lineCache.set(file2, lines);
4214
4471
  }
4215
- return lineCache.get(file);
4472
+ return lineCache.get(file2);
4216
4473
  };
4217
4474
  let index;
4218
4475
  const byName = () => index ??= indexFiles(roots);
4219
4476
  const candidates = (ref) => {
4220
4477
  const direct = resolve(ref.file, roots);
4221
4478
  if (direct) return [direct];
4222
- if (path20.isAbsolute(ref.file)) return [];
4479
+ if (path23.isAbsolute(ref.file)) return [];
4223
4480
  const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
4224
- return (byName().get(path20.basename(wanted)) ?? []).filter((f) => f.endsWith(`/${wanted}`) || path20.basename(f) === wanted).slice(0, 20);
4481
+ return (byName().get(path23.basename(wanted)) ?? []).filter((f) => f.endsWith(`/${wanted}`) || path23.basename(f) === wanted).slice(0, 20);
4225
4482
  };
4226
- const check = (file, ref) => {
4227
- const lines = load(file);
4483
+ const check = (file2, ref) => {
4484
+ const lines = load(file2);
4228
4485
  if (!lines) return void 0;
4229
4486
  if (ref.end > lines.length) return `file has ${lines.length} lines`;
4230
4487
  if (ref.symbol && !mentions(lines, ref) && !enclosedBy(lines, ref)) return `\`${ref.symbol}\` not near line ${ref.start}`;
@@ -4236,7 +4493,7 @@ function verifyRefs(refs, dirs) {
4236
4493
  if (!files.length) {
4237
4494
  if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
4238
4495
  total++;
4239
- invalid.push({ ref: ref.text, reason: path20.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
4496
+ invalid.push({ ref: ref.text, reason: path23.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
4240
4497
  continue;
4241
4498
  }
4242
4499
  total++;
@@ -4257,7 +4514,7 @@ function enclosedBy(lines, ref) {
4257
4514
  return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
4258
4515
  }
4259
4516
  var isFileName = (name) => {
4260
- const base2 = path20.basename(name);
4517
+ const base2 = path23.basename(name);
4261
4518
  return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
4262
4519
  };
4263
4520
  function indexFiles(roots) {
@@ -4266,19 +4523,19 @@ function indexFiles(roots) {
4266
4523
  const walk = (dir) => {
4267
4524
  let entries;
4268
4525
  try {
4269
- entries = fs24.readdirSync(dir, { withFileTypes: true });
4526
+ entries = fs27.readdirSync(dir, { withFileTypes: true });
4270
4527
  } catch {
4271
4528
  return;
4272
4529
  }
4273
4530
  for (const e of entries) {
4274
4531
  if (count >= MAX_INDEXED) return;
4275
4532
  if (e.isDirectory()) {
4276
- if (!SKIP_DIRS.has(e.name)) walk(path20.join(dir, e.name));
4533
+ if (!SKIP_DIRS2.has(e.name)) walk(path23.join(dir, e.name));
4277
4534
  } else if (e.isFile()) {
4278
4535
  count++;
4279
4536
  const list2 = index.get(e.name);
4280
- if (list2) list2.push(path20.join(dir, e.name));
4281
- else index.set(e.name, [path20.join(dir, e.name)]);
4537
+ if (list2) list2.push(path23.join(dir, e.name));
4538
+ else index.set(e.name, [path23.join(dir, e.name)]);
4282
4539
  }
4283
4540
  }
4284
4541
  };
@@ -4286,76 +4543,25 @@ function indexFiles(roots) {
4286
4543
  return index;
4287
4544
  }
4288
4545
  function resolve(ref, roots) {
4289
- const candidates = path20.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path20.join(r, ref));
4546
+ const candidates = path23.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path23.join(r, ref));
4290
4547
  return candidates.find((c) => {
4291
4548
  try {
4292
- return fs24.statSync(c).isFile() && inside(c, roots);
4549
+ return fs27.statSync(c).isFile() && inside(c, roots);
4293
4550
  } catch {
4294
4551
  return false;
4295
4552
  }
4296
4553
  });
4297
4554
  }
4298
- function inside(file, roots) {
4555
+ function inside(file2, roots) {
4299
4556
  return roots.some((r) => {
4300
- const rel = path20.relative(r, path20.resolve(file));
4301
- return rel !== "" && !rel.startsWith("..") && !path20.isAbsolute(rel);
4557
+ const rel = path23.relative(r, path23.resolve(file2));
4558
+ return rel !== "" && !rel.startsWith("..") && !path23.isAbsolute(rel);
4302
4559
  });
4303
4560
  }
4304
4561
  var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4305
4562
 
4306
- // src/core/secrets.ts
4307
- import { execFileSync } from "node:child_process";
4308
- import fs25 from "node:fs";
4309
- import path21 from "node:path";
4310
- var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
4311
- var MAX_VISITED = 2e4;
4312
- var MAX_DEPTH = 4;
4313
- var TEMPLATE2 = /\.(example|sample|template|dist|defaults?|tpl)$/i;
4314
- function looksSecret(name) {
4315
- if (name === ".env" || name.startsWith(".env.") && !TEMPLATE2.test(name)) return true;
4316
- return /\.(pem|p12|pfx)$/i.test(name) || /^id_(rsa|dsa|ecdsa|ed25519)$/.test(name) || name === ".netrc" || name === "credentials.json";
4317
- }
4318
- function findSecretFiles(dir) {
4319
- const found = [];
4320
- let visited = 0;
4321
- const walk = (d, depth) => {
4322
- if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
4323
- let entries;
4324
- try {
4325
- entries = fs25.readdirSync(d, { withFileTypes: true });
4326
- } catch {
4327
- return;
4328
- }
4329
- for (const e of entries) {
4330
- visited++;
4331
- if (e.isDirectory()) {
4332
- if (!SKIP_DIRS2.has(e.name)) walk(path21.join(d, e.name), depth + 1);
4333
- } else if (e.isFile() && looksSecret(e.name)) {
4334
- found.push(path21.relative(dir, path21.join(d, e.name)));
4335
- }
4336
- }
4337
- };
4338
- walk(dir, 0);
4339
- return found.sort();
4340
- }
4341
- function findSecretFilesInTree(root, dir) {
4342
- try {
4343
- const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
4344
- return out.split("\0").filter((f) => f && looksSecret(path21.basename(f)) && !path21.relative(dir, path21.join(root, f)).startsWith("..")).map((f) => path21.relative(dir, path21.join(root, f))).sort();
4345
- } catch {
4346
- return [];
4347
- }
4348
- }
4349
- function secretWarning(files, mode) {
4350
- if (!files.length) return void 0;
4351
- const shown = files.slice(0, 3).join(", ") + (files.length > 3 ? `, +${files.length - 3} more` : "");
4352
- const where = mode === "isolate" ? "would be copied into the isolated copy" : "sit in the directory the worker runs in";
4353
- 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";
4354
- 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`;
4355
- }
4356
-
4357
4563
  // src/core/run.ts
4358
- var VERSION2 = true ? "0.11.0" : "0.0.0-dev";
4564
+ var VERSION2 = true ? "0.13.0" : "0.0.0-dev";
4359
4565
  function planWork(o) {
4360
4566
  if (!o.plan) return { task: o.task.trim(), tier: o.tier };
4361
4567
  if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
@@ -4387,7 +4593,7 @@ function prepareRun(o) {
4387
4593
  parent = readMeta(o.continueFrom);
4388
4594
  if (o.worker || o.tier || o.effort) throw new UserError("a follow-up runs on the same worker as its parent; drop --worker/--tier/--effort");
4389
4595
  if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
4390
- if (parent.mode === "isolate" && (!parent.worktree || !fs26.existsSync(parent.worktree))) {
4596
+ if (parent.mode === "isolate" && (!parent.worktree || !fs28.existsSync(parent.worktree))) {
4391
4597
  throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
4392
4598
  }
4393
4599
  const ran = parent.ran ?? parent.worker;
@@ -4406,16 +4612,21 @@ function prepareRun(o) {
4406
4612
  throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
4407
4613
  }
4408
4614
  const mode = parent?.mode ?? o.mode;
4409
- const dir = path22.resolve(parent?.dir ?? o.dir);
4410
- if (!fs26.existsSync(dir) || !fs26.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
4615
+ const dir = path24.resolve(parent?.dir ?? o.dir);
4616
+ if (!fs28.existsSync(dir) || !fs28.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
4411
4617
  const root = repoRoot(dir);
4412
4618
  if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
4413
4619
  if (mode === "write" && !root && !o.allowNonGit) {
4414
4620
  throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
4415
4621
  }
4416
- const files = o.files.map((f) => path22.resolve(f));
4417
- for (const f of files) if (!fs26.existsSync(f)) throw new UserError(`file not found: ${f}`);
4418
- if (!parent && !process.env.PITROOM_NO_SECRET_WARNING) {
4622
+ const files = o.files.map((f) => path24.resolve(f));
4623
+ for (const f of files) if (!fs28.existsSync(f)) throw new UserError(`file not found: ${f}`);
4624
+ const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
4625
+ const snap = mode === "read" && !o.review && !o.inPlace && !parent ? wantSnapshot(readIn2, root, dir) : void 0;
4626
+ if (parent?.snapshot?.dir && !fs28.existsSync(parent.snapshot.dir)) {
4627
+ throw new UserError(`the read snapshot of run ${parent.id} is gone (cleaned up); ask the question again as a new run`, 3);
4628
+ }
4629
+ if (!parent && !snap && !process.env.PITROOM_NO_SECRET_WARNING) {
4419
4630
  const secrets = mode === "isolate" && root ? findSecretFilesInTree(root, dir) : findSecretFiles(dir);
4420
4631
  const note = secretWarning(secrets, mode);
4421
4632
  if (note) warnings.push(note);
@@ -4447,6 +4658,8 @@ function prepareRun(o) {
4447
4658
  reviewOf: o.review?.of,
4448
4659
  auditOf: o.audit?.of,
4449
4660
  auditRate: o.auditRate,
4661
+ inPlace: o.inPlace || void 0,
4662
+ snapshot: parent?.snapshot ?? (snap ? { dir: "", tree: "", left: snap.secrets } : void 0),
4450
4663
  plan: work.plan ?? o.review?.plan ?? parent?.plan,
4451
4664
  reviewKind: o.review?.kind,
4452
4665
  packageFile: o.review?.packageFile
@@ -4456,13 +4669,13 @@ function prepareRun(o) {
4456
4669
  try {
4457
4670
  acquireWriteLock(root, meta.id);
4458
4671
  } catch (e) {
4459
- fs26.rmSync(runDir(meta.id), { recursive: true, force: true });
4672
+ fs28.rmSync(runDir(meta.id), { recursive: true, force: true });
4460
4673
  throw e;
4461
4674
  }
4462
4675
  }
4463
- fs26.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
4676
+ fs28.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
4464
4677
  `);
4465
- if (work.brief) fs26.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
4678
+ if (work.brief) fs28.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
4466
4679
  return meta;
4467
4680
  }
4468
4681
  var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -4519,12 +4732,19 @@ async function execute(meta) {
4519
4732
  for (let i = 0; i < chain.length; i++) {
4520
4733
  const target = chain[i];
4521
4734
  const backend = getBackend(target.backend);
4735
+ const cooling = activeCooldown(cooldownKey(target, backend));
4736
+ if (cooling && chain.slice(i + 1).some((t) => !activeCooldown(cooldownKey(t, getBackend(t.backend))))) {
4737
+ (meta.attempts ??= []).push({ target: describeTarget(target), error: `cooling down ${untilText(cooling)}: ${cooling.reason}`, skipped: true });
4738
+ writeMeta(meta);
4739
+ continue;
4740
+ }
4522
4741
  meta.ran = target;
4523
4742
  writeMeta(meta);
4524
4743
  result = await attempt(meta, backend, target);
4525
4744
  if (result.timedOut || result.stopped || result.spawnError) break;
4526
4745
  const why = retryableFailure(meta, backend, result);
4527
4746
  if (!why) break;
4747
+ if (why.kind === "rate-limited") recordCooldown(cooldownKey(target, backend), describeTarget(target), why.message);
4528
4748
  if (!target.model && backend.defaultModel) {
4529
4749
  const failed = backend.defaultModel();
4530
4750
  for (let j = chain.length - 1; j > i; j--) {
@@ -4535,7 +4755,7 @@ async function execute(meta) {
4535
4755
  (meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
4536
4756
  for (const f of ["events.jsonl", "stderr.log"]) {
4537
4757
  const from = runFile(meta.id, f);
4538
- if (fs26.existsSync(from)) fs26.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
4758
+ if (fs28.existsSync(from)) fs28.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
4539
4759
  }
4540
4760
  writeMeta(meta);
4541
4761
  }
@@ -4554,13 +4774,13 @@ async function execute(meta) {
4554
4774
  }
4555
4775
  function autoAudit(meta) {
4556
4776
  try {
4557
- if (!auditable(meta, read(runFile(meta.id, "summary.md"))) || !sampled(meta)) return;
4777
+ if (!auditable(meta, read2(runFile(meta.id, "summary.md"))) || !sampled(meta)) return;
4558
4778
  startAudit(meta);
4559
4779
  } catch {
4560
4780
  }
4561
4781
  }
4562
4782
  function startAudit(meta, worker) {
4563
- const answer = read(runFile(meta.id, "summary.md"));
4783
+ const answer = read2(runFile(meta.id, "summary.md"));
4564
4784
  const auditor = worker ?? pickAuditor(meta);
4565
4785
  if (!auditor) return void 0;
4566
4786
  const a = prepareRun({
@@ -4593,12 +4813,26 @@ function settleAudit(a) {
4593
4813
  }
4594
4814
  function prepareTree(meta) {
4595
4815
  const root = meta.repoRoot;
4816
+ if (root && meta.mode === "read" && meta.snapshot && !meta.snapshot.dir) {
4817
+ const shown = meta.snapshot.left.slice(0, 3).join(", ") + (meta.snapshot.left.length > 3 ? `, +${meta.snapshot.left.length - 3} more` : "");
4818
+ try {
4819
+ const s = readSnapshot(root);
4820
+ meta.snapshot.dir = s.dir;
4821
+ meta.snapshot.tree = s.tree;
4822
+ meta.cwd = path24.join(s.dir, path24.relative(root, meta.dir));
4823
+ if (!fs28.existsSync(meta.cwd)) meta.cwd = s.dir;
4824
+ if (shown) meta.warnings.push(`read in a clean snapshot of your project: the secret-looking files (${shown}) and git-ignored files are not in it; --in-place reads the directory itself`);
4825
+ } catch (e) {
4826
+ meta.snapshot = void 0;
4827
+ meta.warnings.push(`could not make a clean snapshot (${e.message.slice(0, 120)}): the worker reads the directory itself${shown ? `, where secret-looking files sit (${shown})` : ""}`);
4828
+ }
4829
+ }
4596
4830
  if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
4597
4831
  if (root && meta.mode === "isolate" && !meta.worktree) {
4598
4832
  meta.baseTree = snapshotTree(root);
4599
- meta.worktree = path22.join(worktreesDir(), meta.id);
4833
+ meta.worktree = path24.join(worktreesDir(), meta.id);
4600
4834
  createIsolatedCopy(root, meta.baseTree, meta.worktree);
4601
- meta.cwd = path22.join(meta.worktree, path22.relative(root, meta.dir));
4835
+ meta.cwd = path24.join(meta.worktree, path24.relative(root, meta.dir));
4602
4836
  const linked = linkIntoWorktree(root, meta.worktree, meta.link);
4603
4837
  if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
4604
4838
  }
@@ -4621,11 +4855,11 @@ function attempt(meta, backend, target) {
4621
4855
  timeoutSec: meta.timeoutSec
4622
4856
  });
4623
4857
  }
4624
- var read = (f) => fs26.existsSync(f) ? fs26.readFileSync(f, "utf8") : "";
4858
+ var read2 = (f) => fs28.existsSync(f) ? fs28.readFileSync(f, "utf8") : "";
4625
4859
  function retryableFailure(meta, backend, res) {
4626
- const run = backend.parse(read(runFile(meta.id, "events.jsonl")));
4860
+ const run = backend.parse(read2(runFile(meta.id, "events.jsonl")));
4627
4861
  if (run.usage.steps > 0) return void 0;
4628
- const f = backend.failure(run, read(runFile(meta.id, "stderr.log")), res.code);
4862
+ const f = backend.failure(run, read2(runFile(meta.id, "stderr.log")), res.code);
4629
4863
  return f && f.kind !== "other" ? f : void 0;
4630
4864
  }
4631
4865
  var HINTS = {
@@ -4637,18 +4871,19 @@ var HINTS = {
4637
4871
  function finalize(meta, res) {
4638
4872
  const ran = meta.ran ?? meta.worker;
4639
4873
  const backend = getBackend(ran.backend);
4640
- const run = backend.parse(read(runFile(meta.id, "events.jsonl")));
4874
+ const run = backend.parse(read2(runFile(meta.id, "events.jsonl")));
4875
+ if (meta.snapshot?.dir && meta.repoRoot) run.finalText = backToProject(run.finalText, meta.snapshot.dir, meta.repoRoot);
4641
4876
  meta.sessionId = run.sessionId ?? meta.sessionId;
4642
4877
  meta.usage = run.usage;
4643
- fs26.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
4878
+ fs28.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
4644
4879
  `);
4645
4880
  if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run.finalText);
4646
4881
  if (meta.reviewOf) {
4647
4882
  meta.verdict = parseVerdict(run.finalText);
4648
- if (meta.packageFile) fs26.rmSync(meta.packageFile, { force: true });
4883
+ if (meta.packageFile) fs28.rmSync(meta.packageFile, { force: true });
4649
4884
  }
4650
4885
  if (!res.timedOut && !res.stopped && !meta.error) {
4651
- const f = backend.failure(run, read(runFile(meta.id, "stderr.log")), res.code);
4886
+ const f = backend.failure(run, read2(runFile(meta.id, "stderr.log")), res.code);
4652
4887
  if (f) meta.error = HINTS[f.kind] ? `${f.message} (${HINTS[f.kind]})` : f.message;
4653
4888
  }
4654
4889
  try {
@@ -4693,7 +4928,7 @@ function captureChanges(meta) {
4693
4928
  const d = diffTrees(target, meta.baseTree, meta.afterTree);
4694
4929
  meta.changes = d.changes;
4695
4930
  meta.stats = d.stats;
4696
- fs26.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
4931
+ fs28.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
4697
4932
  }
4698
4933
  function runVerify(meta) {
4699
4934
  const r = spawnSync6(meta.verify, {
@@ -4706,7 +4941,7 @@ function runVerify(meta) {
4706
4941
  env: { ...process.env, PWD: meta.cwd, PITROOM_ACTIVE: "1" }
4707
4942
  });
4708
4943
  const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
4709
- fs26.writeFileSync(runFile(meta.id, "verify.log"), output);
4944
+ fs28.writeFileSync(runFile(meta.id, "verify.log"), output);
4710
4945
  return { ok: r.status === 0, code: r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
4711
4946
  }
4712
4947
  function applyRun(meta, allowDelete = false) {
@@ -4751,7 +4986,7 @@ function discardRun(meta) {
4751
4986
  return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
4752
4987
  }
4753
4988
  function cleanupWorktree(meta) {
4754
- if (meta.worktree && fs26.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
4989
+ if (meta.worktree && fs28.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
4755
4990
  }
4756
4991
 
4757
4992
  // src/cli/commands.ts
@@ -4786,7 +5021,7 @@ async function cmdReview(p) {
4786
5021
  let meta;
4787
5022
  try {
4788
5023
  meta = prepareRun({
4789
- ...runOptions(p, fill(loadTemplate(TEMPLATE[job.kind]), { PACKAGE_FILE: packageFile })),
5024
+ ...runOptions(p, fill(loadTemplate(TEMPLATE2[job.kind]), { PACKAGE_FILE: packageFile })),
4790
5025
  mode: "read",
4791
5026
  dir: job.dir,
4792
5027
  worker: flag(p, "worker") ?? (flag(p, "tier") ? void 0 : pickReviewer(job)),
@@ -4801,10 +5036,10 @@ async function cmdReview(p) {
4801
5036
  writeMeta(meta);
4802
5037
  }
4803
5038
  } catch (e) {
4804
- fs27.rmSync(packageFile, { force: true });
5039
+ fs29.rmSync(packageFile, { force: true });
4805
5040
  throw e;
4806
5041
  }
4807
- fs27.writeFileSync(runFile(meta.id, "package.md"), job.package);
5042
+ fs29.writeFileSync(runFile(meta.id, "package.md"), job.package);
4808
5043
  return launch(p, meta);
4809
5044
  }
4810
5045
  async function cmdAudit(p) {
@@ -4834,24 +5069,45 @@ async function cmdAudit(p) {
4834
5069
  writeMeta(m);
4835
5070
  return launch(p, a);
4836
5071
  }
5072
+ function cmdCooldown(p) {
5073
+ if (has(p, "clear")) {
5074
+ const n = clearCooldowns();
5075
+ console.log(n ? `cleared ${n} cooldown${n === 1 ? "" : "s"}: those models are tried again` : "no cooldowns");
5076
+ return 0;
5077
+ }
5078
+ const all = activeCooldowns();
5079
+ if (has(p, "json")) {
5080
+ console.log(JSON.stringify(all, null, 2));
5081
+ return 0;
5082
+ }
5083
+ const rows = Object.values(all);
5084
+ if (!rows.length) {
5085
+ console.log("no model is cooling down");
5086
+ return 0;
5087
+ }
5088
+ console.log("Models left alone for now (runs go to the next worker; they are tried again after the time shown):");
5089
+ for (const c of rows) console.log(` ${c.target.padEnd(52)} ${untilText(c)} \xB7 ${c.reason.slice(0, 90)}`);
5090
+ console.log("\npitroom cooldown --clear try them again now");
5091
+ return 0;
5092
+ }
4837
5093
  function cmdPlan(p) {
4838
- const [sub, file, ...rest] = p.positional;
4839
- if (sub === "status" && file) {
4840
- const s = planStatus(file);
5094
+ const [sub, file2, ...rest] = p.positional;
5095
+ if (sub === "status" && file2) {
5096
+ const s = planStatus(file2);
4841
5097
  console.log(
4842
5098
  has(p, "json") ? JSON.stringify({ plan: s.plan.file, title: s.plan.title, tasks: s.tasks, rulings: s.rulings, notesFile: s.notesFile }, null, 2) : formatPlanStatus(s)
4843
5099
  );
4844
5100
  return 0;
4845
5101
  }
4846
- if (sub === "note" && file && rest.length) {
4847
- console.log(`noted in ${addNote(file, rest.join(" "))}`);
5102
+ if (sub === "note" && file2 && rest.length) {
5103
+ console.log(`noted in ${addNote(file2, rest.join(" "))}`);
4848
5104
  return 0;
4849
5105
  }
4850
5106
  throw new UserError('usage: pitroom plan status PLAN.md [--json] | pitroom plan note PLAN.md "Task N: \u2026"');
4851
5107
  }
4852
5108
  function cmdCrew(p) {
4853
- const file = flag(p, "task-file");
4854
- const tasks = (file ? fs27.readFileSync(file, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
5109
+ const file2 = flag(p, "task-file");
5110
+ const tasks = (file2 ? fs29.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
4855
5111
  if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
4856
5112
  if (has(p, "write") && tasks.length > 1) {
4857
5113
  throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
@@ -5052,7 +5308,7 @@ function sinceMs(s) {
5052
5308
  if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
5053
5309
  return Date.now() - Number(m[1]) * 864e5;
5054
5310
  }
5055
- var readStdin = () => process.stdin.isTTY ? "" : fs27.readFileSync(0, "utf8");
5311
+ var readStdin = () => process.stdin.isTTY ? "" : fs29.readFileSync(0, "utf8");
5056
5312
  function cmdStatusline(p) {
5057
5313
  const input = readStdin();
5058
5314
  const lines = [];
@@ -5092,8 +5348,8 @@ function cmdModels(p) {
5092
5348
  return 0;
5093
5349
  }
5094
5350
  function cmdHookStart() {
5095
- const script = path23.join(path23.dirname(path23.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
5096
- if (fs27.existsSync(script)) spawnSync7(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
5351
+ const script = path25.join(path25.dirname(path25.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
5352
+ if (fs29.existsSync(script)) spawnSync7(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
5097
5353
  return 0;
5098
5354
  }
5099
5355
  function cmdInit(p) {
@@ -5133,8 +5389,8 @@ function cmdSavings(p) {
5133
5389
  est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
5134
5390
  const out = flag(p, "card");
5135
5391
  if (out) {
5136
- fs27.writeFileSync(out, card(t, period));
5137
- console.log(`card written to ${path23.resolve(out)}`);
5392
+ fs29.writeFileSync(out, card(t, period));
5393
+ console.log(`card written to ${path25.resolve(out)}`);
5138
5394
  }
5139
5395
  if (has(p, "badge")) console.log(`![pitroom](${badgeUrl(t)})`);
5140
5396
  return 0;
@@ -5154,7 +5410,7 @@ function cmdConfig(p) {
5154
5410
  console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
5155
5411
  return 0;
5156
5412
  }
5157
- console.log(`config file: ${configPath()}${fs27.existsSync(configPath()) ? "" : " (not present)"}`);
5413
+ console.log(`config file: ${configPath()}${fs29.existsSync(configPath()) ? "" : " (not present)"}`);
5158
5414
  for (const [key, s] of Object.entries(eff)) {
5159
5415
  const v = Array.isArray(s.value) ? s.value.join(", ") || "\u2014" : s.value && typeof s.value === "object" ? Object.entries(s.value).map(([k, m]) => `${k}=${m}`).join(", ") || "\u2014" : s.value === void 0 ? key === "model" ? "the worker's default" : "\u2014" : String(s.value);
5160
5416
  console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
@@ -5178,9 +5434,10 @@ function cmdClean(p) {
5178
5434
  const m = readMeta(id);
5179
5435
  if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
5180
5436
  recordRun(m);
5181
- fs27.rmSync(runDir(id), { recursive: true, force: true });
5437
+ fs29.rmSync(runDir(id), { recursive: true, force: true });
5182
5438
  }
5183
- console.log(`removed ${old.length} run(s); the history and the savings ledger are kept`);
5439
+ const snapshots = prune(Date.now(), 0);
5440
+ console.log(`removed ${old.length} run(s)${snapshots ? ` and ${snapshots} read snapshot${snapshots === 1 ? "" : "s"}` : ""}; the history and the savings ledger are kept`);
5184
5441
  return 0;
5185
5442
  }
5186
5443
  var cmdRevert = (p) => (console.log(revertRun(freshMeta(resolveRun(p.positional[0])))), 0);
@@ -5188,9 +5445,9 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
5188
5445
 
5189
5446
  // src/core/doctor.ts
5190
5447
  import { spawnSync as spawnSync8 } from "node:child_process";
5191
- import fs28 from "node:fs";
5448
+ import fs30 from "node:fs";
5192
5449
  import os10 from "node:os";
5193
- import path24 from "node:path";
5450
+ import path26 from "node:path";
5194
5451
  var MARK = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
5195
5452
  var NEXT = [
5196
5453
  { when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
@@ -5200,15 +5457,15 @@ var NEXT = [
5200
5457
  { when: /Gemini CLI is not signed in|IneligibleTierError/, command: "export GEMINI_API_KEY=\u2026", why: "a Google AI Studio key for the Gemini worker" }
5201
5458
  ];
5202
5459
  function firstNodeOnPath() {
5203
- for (const dir of (process.env.PATH ?? "").split(path24.delimiter).filter(Boolean)) {
5204
- const file = path24.join(dir, process.platform === "win32" ? "node.exe" : "node");
5460
+ for (const dir of (process.env.PATH ?? "").split(path26.delimiter).filter(Boolean)) {
5461
+ const file2 = path26.join(dir, process.platform === "win32" ? "node.exe" : "node");
5205
5462
  try {
5206
- if (!fs28.statSync(file).isFile()) continue;
5463
+ if (!fs30.statSync(file2).isFile()) continue;
5207
5464
  } catch {
5208
5465
  continue;
5209
5466
  }
5210
- const r = spawnSync8(file, ["-p", "process.versions.node"], { encoding: "utf8", timeout: 5e3 });
5211
- return r.status === 0 ? { path: file, version: r.stdout.trim() } : { path: file, version: "" };
5467
+ const r = spawnSync8(file2, ["-p", "process.versions.node"], { encoding: "utf8", timeout: 5e3 });
5468
+ return r.status === 0 ? { path: file2, version: r.stdout.trim() } : { path: file2, version: "" };
5212
5469
  }
5213
5470
  return void 0;
5214
5471
  }
@@ -5230,12 +5487,12 @@ function doctor5(probe) {
5230
5487
  const addAll = (list2) => list2.forEach((c) => add(c.level, c.message));
5231
5488
  add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
5232
5489
  const onPath = firstNodeOnPath();
5233
- if (onPath && onPath.version && tooOld(onPath.version) && path24.resolve(onPath.path) !== path24.resolve(process.execPath)) {
5490
+ if (onPath && onPath.version && tooOld(onPath.version) && path26.resolve(onPath.path) !== path26.resolve(process.execPath)) {
5234
5491
  add("warn", `the first \`node\` on PATH is v${onPath.version} (${onPath.path}), older than the 22.13 Pitroom needs: shells that agent apps start may use it (the pitroom command finds a newer Node itself; other tools may not)`);
5235
5492
  }
5236
5493
  add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
5237
5494
  const cfg = loadConfig();
5238
- add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs28.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
5495
+ add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs30.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
5239
5496
  for (const w of cfg.warnings) add("warn", w);
5240
5497
  if (process.platform !== "win32") {
5241
5498
  const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
@@ -5277,6 +5534,9 @@ function doctor5(probe) {
5277
5534
  add("ok", `cost: ${key} = ${mine}${cheaper ? `; you priced ${cheaper[0]} cheaper (${cheaper[1]}): is the dearer one needed?` : ""}`);
5278
5535
  }
5279
5536
  }
5537
+ for (const c of Object.values(activeCooldowns())) {
5538
+ add("warn", `${c.target} is cooling down ${untilText(c)} (${c.reason.slice(0, 80)}): runs skip it while a fallback is left; \`pitroom cooldown --clear\` tries it again`);
5539
+ }
5280
5540
  const byBackend = /* @__PURE__ */ new Map();
5281
5541
  for (const t of [...chain, ...tierTargets]) byBackend.set(t.backend, [...byBackend.get(t.backend) ?? [], t.model]);
5282
5542
  for (const [id, models] of byBackend) {
@@ -5380,19 +5640,19 @@ function skillChecks() {
5380
5640
  else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
5381
5641
  }
5382
5642
  if (geminiExtensionInstalled()) {
5383
- const linked = installedSkills().some((i) => i.base.includes(`${path24.sep}.agents${path24.sep}`) && i.names.length);
5643
+ const linked = installedSkills().some((i) => i.base.includes(`${path26.sep}.agents${path26.sep}`) && i.names.length);
5384
5644
  checks.push(
5385
5645
  linked ? { level: "warn", message: "Pitroom is installed as a Gemini CLI extension and linked into ~/.agents/skills: Gemini loads the skills twice; run `pitroom uninstall` or `gemini extensions uninstall pitroom`" } : { level: "ok", message: "Gemini CLI extension installed (skills + context)" }
5386
5646
  );
5387
5647
  }
5388
5648
  if (viaPlugin) {
5389
- const linked = installedSkills().some((i) => i.base.includes(`${path24.sep}.claude${path24.sep}`) && i.names.length);
5649
+ const linked = installedSkills().some((i) => i.base.includes(`${path26.sep}.claude${path26.sep}`) && i.names.length);
5390
5650
  checks.push(
5391
5651
  linked ? { level: "warn", message: "Pitroom is installed as a Claude Code plugin and linked into ~/.claude/skills: skills load twice; run `pitroom uninstall` or remove the plugin" } : { level: "ok", message: "Claude Code plugin installed (skills + session-start hook)" }
5392
5652
  );
5393
5653
  }
5394
5654
  const launcher = launcherPath();
5395
- if (fs28.existsSync(launcher)) {
5655
+ if (fs30.existsSync(launcher)) {
5396
5656
  const r = spawnSync8(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
5397
5657
  checks.push(
5398
5658
  r.status === 0 ? { level: "ok", message: `launcher ${launcher} \u2192 pitroom ${r.stdout.trim()}` } : { level: "fail", message: `launcher ${launcher} does not start: ${(r.stderr || r.stdout).trim().slice(0, 200)}` }
@@ -5404,12 +5664,12 @@ function skillChecks() {
5404
5664
  }
5405
5665
  function geminiExtensionInstalled() {
5406
5666
  const home2 = process.env.GEMINI_CLI_HOME ?? os10.homedir();
5407
- return fs28.existsSync(path24.join(home2, ".gemini", "extensions", "pitroom"));
5667
+ return fs30.existsSync(path26.join(home2, ".gemini", "extensions", "pitroom"));
5408
5668
  }
5409
5669
  function pluginInstalled() {
5410
5670
  try {
5411
- const f = path24.join(os10.homedir(), ".claude", "plugins", "installed_plugins.json");
5412
- const plugins = JSON.parse(fs28.readFileSync(f, "utf8")).plugins ?? {};
5671
+ const f = path26.join(os10.homedir(), ".claude", "plugins", "installed_plugins.json");
5672
+ const plugins = JSON.parse(fs30.readFileSync(f, "utf8")).plugins ?? {};
5413
5673
  return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
5414
5674
  } catch {
5415
5675
  return false;
@@ -5426,24 +5686,24 @@ function superpowersActive() {
5426
5686
  for (const p of openCodePlugins()) {
5427
5687
  if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
5428
5688
  }
5429
- for (const base2 of [path24.join(os10.homedir(), ".agents", "skills"), path24.join(os10.homedir(), ".claude", "skills")]) {
5430
- const dir = path24.join(base2, "using-superpowers");
5431
- if (fs28.existsSync(path24.join(dir, "SKILL.md"))) found.push(dir);
5689
+ for (const base2 of [path26.join(os10.homedir(), ".agents", "skills"), path26.join(os10.homedir(), ".claude", "skills")]) {
5690
+ const dir = path26.join(base2, "using-superpowers");
5691
+ if (fs30.existsSync(path26.join(dir, "SKILL.md"))) found.push(dir);
5432
5692
  }
5433
5693
  return found;
5434
5694
  }
5435
5695
  function claudePlugins() {
5436
5696
  try {
5437
- const f = path24.join(os10.homedir(), ".claude", "plugins", "installed_plugins.json");
5438
- return Object.keys(JSON.parse(fs28.readFileSync(f, "utf8")).plugins ?? {});
5697
+ const f = path26.join(os10.homedir(), ".claude", "plugins", "installed_plugins.json");
5698
+ return Object.keys(JSON.parse(fs30.readFileSync(f, "utf8")).plugins ?? {});
5439
5699
  } catch {
5440
5700
  return [];
5441
5701
  }
5442
5702
  }
5443
5703
  function claudePluginEnabled(key) {
5444
5704
  try {
5445
- const f = path24.join(os10.homedir(), ".claude", "settings.json");
5446
- return JSON.parse(fs28.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
5705
+ const f = path26.join(os10.homedir(), ".claude", "settings.json");
5706
+ return JSON.parse(fs30.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
5447
5707
  } catch {
5448
5708
  return true;
5449
5709
  }
@@ -5452,7 +5712,7 @@ function codexPlugins() {
5452
5712
  const plugins = /* @__PURE__ */ new Map();
5453
5713
  let text;
5454
5714
  try {
5455
- text = fs28.readFileSync(path24.join(process.env.CODEX_HOME ?? path24.join(os10.homedir(), ".codex"), "config.toml"), "utf8");
5715
+ text = fs30.readFileSync(path26.join(process.env.CODEX_HOME ?? path26.join(os10.homedir(), ".codex"), "config.toml"), "utf8");
5456
5716
  } catch {
5457
5717
  return plugins;
5458
5718
  }
@@ -5470,11 +5730,11 @@ function codexPlugins() {
5470
5730
  return plugins;
5471
5731
  }
5472
5732
  function openCodePlugins() {
5473
- const dir = path24.join(process.env.XDG_CONFIG_HOME ?? path24.join(os10.homedir(), ".config"), "opencode");
5733
+ const dir = path26.join(process.env.XDG_CONFIG_HOME ?? path26.join(os10.homedir(), ".config"), "opencode");
5474
5734
  const plugins = [];
5475
5735
  for (const name of ["opencode.json", "opencode.jsonc"]) {
5476
5736
  try {
5477
- const text = fs28.readFileSync(path24.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
5737
+ const text = fs30.readFileSync(path26.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
5478
5738
  const list2 = JSON.parse(text).plugin;
5479
5739
  if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
5480
5740
  } catch {
@@ -5482,7 +5742,7 @@ function openCodePlugins() {
5482
5742
  }
5483
5743
  for (const folder of ["plugin", "plugins"]) {
5484
5744
  try {
5485
- for (const f of fs28.readdirSync(path24.join(dir, folder))) plugins.push(path24.join(dir, folder, f));
5745
+ for (const f of fs30.readdirSync(path26.join(dir, folder))) plugins.push(path26.join(dir, folder, f));
5486
5746
  } catch {
5487
5747
  }
5488
5748
  }
@@ -5532,6 +5792,8 @@ Usage
5532
5792
  pitroom statusline [--then CMD] status-bar line: running workers, savings this week (after CMD's)
5533
5793
  pitroom hook-card PostToolUse hook: a card after each Bash \`pitroom\` command
5534
5794
  pitroom hook-start SessionStart hook: introduces Pitroom to the agent (Codex; Claude Code uses its plugin)
5795
+ pitroom cooldown [--clear] models that said "rate limited" and are skipped for a while (a quota used up);
5796
+ --clear tries them again
5535
5797
  pitroom doctor [--probe] check workers, models, permissions, skills
5536
5798
  pitroom config effective settings, where each comes from, config file path
5537
5799
  pitroom init [--model ID] [--fallback A,B] [--yes] [--force]
@@ -5551,6 +5813,8 @@ Run options
5551
5813
  -W, --worker T worker target "backend[:model]" (default: config "worker", else opencode)
5552
5814
  -m, --model M model for that worker (default: the worker CLI's own default)
5553
5815
  --tier NAME a worker from the config's "tiers" (e.g. cheap, standard, capable); -W wins
5816
+ --in-place a read run reads the directory itself, not a clean snapshot (default: a snapshot without secret-looking
5817
+ files when the directory has any; config "readIn": auto | snapshot | project)
5554
5818
  --audit have another worker re-check this read run's answer; --no-audit skips it (default: the config's "audit")
5555
5819
  --effort LEVEL reasoning effort for the worker: low, medium, high, xhigh, \u2026 (model#level)
5556
5820
  --plan PLAN with --step N: implement Task N of a plan (-i or -w); the task text is your notes
@@ -5581,6 +5845,7 @@ var COMMANDS = {
5581
5845
  crew: cmdCrew,
5582
5846
  review: cmdReview,
5583
5847
  audit: cmdAudit,
5848
+ cooldown: cmdCooldown,
5584
5849
  plan: cmdPlan,
5585
5850
  status: cmdStatus,
5586
5851
  wait: cmdWait,