pitroom 0.11.0 → 0.12.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);
@@ -1426,25 +1426,25 @@ function configPath() {
1426
1426
  var cached;
1427
1427
  function loadConfig() {
1428
1428
  if (cached) return cached;
1429
- const file = configPath();
1429
+ const file2 = configPath();
1430
1430
  const config = {};
1431
1431
  const warnings = [];
1432
- if (fs7.existsSync(file)) {
1432
+ if (fs7.existsSync(file2)) {
1433
1433
  let raw;
1434
1434
  try {
1435
- raw = JSON.parse(fs7.readFileSync(file, "utf8"));
1435
+ raw = JSON.parse(fs7.readFileSync(file2, "utf8"));
1436
1436
  } catch (e) {
1437
- warnings.push(`${file}: invalid JSON (${e.message}); ignored`);
1437
+ warnings.push(`${file2}: invalid JSON (${e.message}); ignored`);
1438
1438
  }
1439
1439
  if (raw && typeof raw === "object" && !Array.isArray(raw)) {
1440
1440
  for (const [key, value] of Object.entries(raw)) {
1441
1441
  const type = SCHEMA[key];
1442
1442
  if (!type) {
1443
- warnings.push(`${file}: unknown key "${key}" ignored`);
1443
+ warnings.push(`${file2}: unknown key "${key}" ignored`);
1444
1444
  } else if (matches(value, type)) {
1445
1445
  config[key] = value;
1446
1446
  } else {
1447
- warnings.push(`${file}: "${key}" must be ${type}; ignored`);
1447
+ warnings.push(`${file2}: "${key}" must be ${type}; ignored`);
1448
1448
  }
1449
1449
  }
1450
1450
  }
@@ -1556,6 +1556,7 @@ var BOOL_FLAGS = {
1556
1556
  "--full": "full",
1557
1557
  "--badge": "badge",
1558
1558
  "--probe": "probe",
1559
+ "--clear": "clear",
1559
1560
  "--copy": "copy",
1560
1561
  "--all": "all",
1561
1562
  "--models": "models",
@@ -1623,8 +1624,8 @@ function exitCodeFor(meta) {
1623
1624
  }
1624
1625
  }
1625
1626
  function readTask(p) {
1626
- const file = flag(p, "task-file");
1627
- if (file) return fs8.readFileSync(file, "utf8");
1627
+ const file2 = flag(p, "task-file");
1628
+ if (file2) return fs8.readFileSync(file2, "utf8");
1628
1629
  const words = p.positional;
1629
1630
  if (words.length === 1 && words[0] === "-") return fs8.readFileSync(0, "utf8");
1630
1631
  return words.join(" ");
@@ -1656,12 +1657,12 @@ function runOptions(p, task) {
1656
1657
  };
1657
1658
  }
1658
1659
  function planStep(p) {
1659
- const file = flag(p, "plan");
1660
+ const file2 = flag(p, "plan");
1660
1661
  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");
1662
+ if (!file2 && !step) return void 0;
1663
+ if (!file2 || !step) throw new UserError("--plan and --step go together: pitroom run -i --plan PLAN.md --step N");
1663
1664
  if (!/^\d+$/.test(step)) throw new UserError(`--step takes a task number, not "${step}"`);
1664
- return { file, step: Number(step) };
1665
+ return { file: file2, step: Number(step) };
1665
1666
  }
1666
1667
  function effortFlag(p) {
1667
1668
  const v = flag(p, "effort");
@@ -1671,8 +1672,8 @@ function effortFlag(p) {
1671
1672
 
1672
1673
  // src/cli/commands.ts
1673
1674
  import { spawnSync as spawnSync7 } from "node:child_process";
1674
- import fs27 from "node:fs";
1675
- import path23 from "node:path";
1675
+ import fs28 from "node:fs";
1676
+ import path24 from "node:path";
1676
1677
  import { fileURLToPath as fileURLToPath4 } from "node:url";
1677
1678
 
1678
1679
  // src/core/init.ts
@@ -1725,7 +1726,7 @@ var FREE = /-free$/;
1725
1726
  var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
1726
1727
  var isFound = (binary5) => path8.isAbsolute(binary5) && fs9.existsSync(binary5);
1727
1728
  function planInit(opts = {}) {
1728
- const file = configPath();
1729
+ const file2 = configPath();
1729
1730
  const workers = backendIds().map((id) => {
1730
1731
  const b = getBackend(id);
1731
1732
  const binary5 = b.binary();
@@ -1769,7 +1770,7 @@ function planInit(opts = {}) {
1769
1770
  if (Object.keys(tiers).length > 1) config.tiers = tiers;
1770
1771
  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
1772
  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 };
1773
+ return { path: file2, exists: fs9.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
1773
1774
  }
1774
1775
  function writeInit(plan, force) {
1775
1776
  if (plan.blocked) throw new UserError(plan.blocked);
@@ -2139,8 +2140,8 @@ var ANSWER_MAX2 = 2e5;
2139
2140
  var cached2;
2140
2141
  var historyFile = () => path10.join(home(), "history.db");
2141
2142
  function openDb() {
2142
- const file = historyFile();
2143
- if (cached2?.file === file) return cached2.db;
2143
+ const file2 = historyFile();
2144
+ if (cached2?.file === file2) return cached2.db;
2144
2145
  let db;
2145
2146
  try {
2146
2147
  const emit = process.emitWarning;
@@ -2151,14 +2152,14 @@ function openDb() {
2151
2152
  } finally {
2152
2153
  process.emitWarning = emit;
2153
2154
  }
2154
- fs12.mkdirSync(path10.dirname(file), { recursive: true });
2155
- db = new DatabaseSync(file);
2155
+ fs12.mkdirSync(path10.dirname(file2), { recursive: true });
2156
+ db = new DatabaseSync(file2);
2156
2157
  db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
2157
2158
  migrate(db);
2158
2159
  } catch {
2159
2160
  db = void 0;
2160
2161
  }
2161
- cached2 = { file, db };
2162
+ cached2 = { file: file2, db };
2162
2163
  return db;
2163
2164
  }
2164
2165
  function migrate(db) {
@@ -2490,7 +2491,7 @@ function structure(lines) {
2490
2491
  });
2491
2492
  return { headings, rules, fenced };
2492
2493
  }
2493
- function parsePlan(text, file = "") {
2494
+ function parsePlan(text, file2 = "") {
2494
2495
  const lines = text.replace(/\r\n?/g, "\n").split("\n");
2495
2496
  const { headings, rules, fenced } = structure(lines);
2496
2497
  const after = (line) => Math.min(headings.find((h) => h.line > line)?.line ?? lines.length, rules.find((r) => r > line) ?? lines.length);
@@ -2509,13 +2510,13 @@ function parsePlan(text, file = "") {
2509
2510
  const tier = /^\s*[-*]?\s*\*\*Worker:\*\*\s*`?([\w-]+)`?/m.exec(unfenced)?.[1]?.toLowerCase();
2510
2511
  tasks.push({ step: Number(m[1]), title: m[2].trim(), text: body, ...tier ? { tier } : {} });
2511
2512
  }
2512
- return { file, title: titleHeading?.text ?? "", header, constraints, tasks };
2513
+ return { file: file2, title: titleHeading?.text ?? "", header, constraints, tasks };
2513
2514
  }
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}`);
2515
+ function loadPlan(file2) {
2516
+ const abs = path11.resolve(file2);
2517
+ if (!fs13.existsSync(abs) || !fs13.statSync(abs).isFile()) throw new UserError(`plan not found: ${file2}`);
2517
2518
  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)`);
2519
+ if (!plan.tasks.length) throw new UserError(`${file2} has no "Task N" headings (see pitroom-writing-plans)`);
2519
2520
  return plan;
2520
2521
  }
2521
2522
  function planTask(plan, step) {
@@ -2533,7 +2534,7 @@ function brief(plan, task) {
2533
2534
  ].filter(Boolean).join("\n\n")}
2534
2535
  `;
2535
2536
  }
2536
- var planName = (file) => path11.basename(file).replace(/\.md$/i, "");
2537
+ var planName = (file2) => path11.basename(file2).replace(/\.md$/i, "");
2537
2538
 
2538
2539
  // src/core/report.ts
2539
2540
  var ICON2 = {
@@ -2568,7 +2569,7 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
2568
2569
  const status = meta.taskStatus ? ` \xB7 STATUS ${meta.taskStatus}` : "";
2569
2570
  out.push(`plan: ${planName(meta.plan.file)} \xB7 Task ${meta.plan.step}: ${meta.plan.title}${status}`);
2570
2571
  }
2571
- for (const a of meta.attempts ?? []) out.push(`fallback: ${a.target} failed (${a.error.slice(0, 160)})`);
2572
+ 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
2573
  if (meta.error) out.push(`error: ${meta.error}`);
2573
2574
  for (const w of meta.warnings) out.push(`warning: ${w}`);
2574
2575
  if (meta.state === "timeout") {
@@ -2789,10 +2790,10 @@ echo "pitroom: needs Node.js 22.13 or newer (set PITROOM_NODE to its path)" >&2
2789
2790
  exit 127
2790
2791
  `;
2791
2792
  }
2792
- function isOurLauncher(file, root) {
2793
- if (linksInto(file, root)) return true;
2793
+ function isOurLauncher(file2, root) {
2794
+ if (linksInto(file2, root)) return true;
2794
2795
  try {
2795
- const s = fs15.readFileSync(file, "utf8");
2796
+ const s = fs15.readFileSync(file2, "utf8");
2796
2797
  return s.includes(LAUNCHER_MARK) && s.includes(root);
2797
2798
  } catch {
2798
2799
  return false;
@@ -2967,17 +2968,84 @@ ${rulings}`,
2967
2968
  ].join("\n");
2968
2969
  }
2969
2970
 
2971
+ // src/core/cooldown.ts
2972
+ import fs17 from "node:fs";
2973
+ import path14 from "node:path";
2974
+ var MAX_MS = 24 * 36e5;
2975
+ var OVERLOAD_MS = 20 * 6e4;
2976
+ var DAILY_MS = 4 * 36e5;
2977
+ var file = () => path14.join(home(), "cooldowns.json");
2978
+ function cooldownKey(target, backend) {
2979
+ const model = (target.model ?? backend.defaultModel?.() ?? "").split("#")[0];
2980
+ return `${target.backend}:${model}`;
2981
+ }
2982
+ function retryAfterMs(message) {
2983
+ const compact3 = /(?:retry|try again|resets?|available again)[^0-9]{0,24}((?:\d+(?:\.\d+)?\s*[dhms]\s*)+)/i.exec(message)?.[1];
2984
+ const words = /(?:retry|try again|resets?)[^0-9]{0,24}(\d+(?:\.\d+)?)\s*(second|minute|hour)s?/i.exec(message);
2985
+ let ms = 0;
2986
+ if (compact3) {
2987
+ 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()];
2988
+ } else if (words) {
2989
+ ms = Number(words[1]) * { second: 1e3, minute: 6e4, hour: 36e5 }[words[2].toLowerCase()];
2990
+ }
2991
+ return ms > 0 ? ms : void 0;
2992
+ }
2993
+ function cooldownMs(message) {
2994
+ const told = retryAfterMs(message);
2995
+ if (told !== void 0) return Math.min(Math.max(told, 6e4), MAX_MS);
2996
+ return /daily|per.?day|per day|exhausted your/i.test(message) ? DAILY_MS : OVERLOAD_MS;
2997
+ }
2998
+ function read() {
2999
+ try {
3000
+ const raw = JSON.parse(fs17.readFileSync(file(), "utf8"));
3001
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
3002
+ } catch {
3003
+ return {};
3004
+ }
3005
+ }
3006
+ function write(all) {
3007
+ fs17.mkdirSync(path14.dirname(file()), { recursive: true });
3008
+ const tmp = `${file()}.${process.pid}.tmp`;
3009
+ fs17.writeFileSync(tmp, `${JSON.stringify(all, null, 2)}
3010
+ `);
3011
+ fs17.renameSync(tmp, file());
3012
+ }
3013
+ function activeCooldowns(now = Date.now()) {
3014
+ return Object.fromEntries(Object.entries(read()).filter(([, c]) => Date.parse(c.until) > now));
3015
+ }
3016
+ var activeCooldown = (key, now = Date.now()) => activeCooldowns(now)[key];
3017
+ function recordCooldown(key, targetName, message, now = Date.now()) {
3018
+ try {
3019
+ const all = activeCooldowns(now);
3020
+ all[key] = { until: new Date(now + cooldownMs(message)).toISOString(), reason: message.replace(/\s+/g, " ").trim().slice(0, 160), target: targetName };
3021
+ write(all);
3022
+ } catch {
3023
+ }
3024
+ }
3025
+ function clearCooldowns(key) {
3026
+ const all = read();
3027
+ const keys = key ? Object.keys(all).filter((k) => k === key) : Object.keys(all);
3028
+ for (const k of keys) delete all[k];
3029
+ if (keys.length) write(all);
3030
+ return keys.length;
3031
+ }
3032
+ function untilText(c, now = Date.now()) {
3033
+ const d = new Date(c.until);
3034
+ const time = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
3035
+ return d.toDateString() === new Date(now).toDateString() ? `until ${time}` : `until ${d.toLocaleDateString()} ${time}`;
3036
+ }
3037
+
2970
3038
  // src/core/review.ts
2971
3039
  import crypto5 from "node:crypto";
2972
- import fs18 from "node:fs";
2973
- import path15 from "node:path";
3040
+ import fs19 from "node:fs";
3041
+ import path16 from "node:path";
2974
3042
 
2975
3043
  // src/vcs/git.ts
2976
3044
  import { spawnSync as spawnSync5 } from "node:child_process";
2977
3045
  import crypto4 from "node:crypto";
2978
- import fs17 from "node:fs";
3046
+ import fs18 from "node:fs";
2979
3047
  import os9 from "node:os";
2980
- import path14 from "node:path";
3048
+ import path15 from "node:path";
2981
3049
  var SAFE = [
2982
3050
  "-c",
2983
3051
  "color.ui=false",
@@ -3009,20 +3077,20 @@ function gitAvailable() {
3009
3077
  }
3010
3078
  function repoRoot(dir) {
3011
3079
  const r = git(dir, ["rev-parse", "--show-toplevel"]);
3012
- return r.code === 0 ? path14.resolve(r.stdout.trim()) : void 0;
3080
+ return r.code === 0 ? path15.resolve(r.stdout.trim()) : void 0;
3013
3081
  }
3014
3082
  function snapshotTree(root, exclude = []) {
3015
- const tmp = path14.join(os9.tmpdir(), `pitroom-index-${process.pid}-${crypto4.randomBytes(4).toString("hex")}`);
3016
- const real = path14.resolve(root, must(root, ["rev-parse", "--git-path", "index"]).trim());
3083
+ const tmp = path15.join(os9.tmpdir(), `pitroom-index-${process.pid}-${crypto4.randomBytes(4).toString("hex")}`);
3084
+ const real = path15.resolve(root, must(root, ["rev-parse", "--git-path", "index"]).trim());
3017
3085
  const env = { ...process.env, GIT_INDEX_FILE: tmp };
3018
3086
  try {
3019
- if (fs17.existsSync(real)) fs17.copyFileSync(real, tmp);
3087
+ if (fs18.existsSync(real)) fs18.copyFileSync(real, tmp);
3020
3088
  else must(root, ["read-tree", "--empty"], env);
3021
3089
  must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`)], env);
3022
3090
  return must(root, ["write-tree"], env).trim();
3023
3091
  } finally {
3024
- fs17.rmSync(tmp, { force: true });
3025
- fs17.rmSync(`${tmp}.lock`, { force: true });
3092
+ fs18.rmSync(tmp, { force: true });
3093
+ fs18.rmSync(`${tmp}.lock`, { force: true });
3026
3094
  }
3027
3095
  }
3028
3096
  var DIFF = ["diff", "--no-ext-diff", "--no-textconv", "--no-renames"];
@@ -3042,27 +3110,27 @@ function diffTrees(root, a, b) {
3042
3110
  return { changes, stats: { files: changes.length, insertions, deletions }, patch };
3043
3111
  }
3044
3112
  function createIsolatedCopy(root, tree, dest) {
3045
- const objects = path14.join(path14.resolve(root, must(root, ["rev-parse", "--git-common-dir"]).trim()), "objects");
3046
- fs17.mkdirSync(path14.dirname(dest), { recursive: true });
3047
- must(path14.dirname(dest), ["init", "--quiet", dest]);
3048
- fs17.writeFileSync(path14.join(dest, ".git", "objects", "info", "alternates"), `${objects}
3113
+ const objects = path15.join(path15.resolve(root, must(root, ["rev-parse", "--git-common-dir"]).trim()), "objects");
3114
+ fs18.mkdirSync(path15.dirname(dest), { recursive: true });
3115
+ must(path15.dirname(dest), ["init", "--quiet", dest]);
3116
+ fs18.writeFileSync(path15.join(dest, ".git", "objects", "info", "alternates"), `${objects}
3049
3117
  `);
3050
3118
  must(dest, ["read-tree", tree]);
3051
3119
  must(dest, ["checkout-index", "--all", "--force"]);
3052
3120
  }
3053
3121
  function removeIsolatedCopy(dest, ownedBy) {
3054
- const rel = path14.relative(ownedBy, dest);
3055
- if (!rel || rel.startsWith("..") || path14.isAbsolute(rel)) throw new Error(`refusing to remove ${dest}`);
3056
- fs17.rmSync(dest, { recursive: true, force: true });
3122
+ const rel = path15.relative(ownedBy, dest);
3123
+ if (!rel || rel.startsWith("..") || path15.isAbsolute(rel)) throw new Error(`refusing to remove ${dest}`);
3124
+ fs18.rmSync(dest, { recursive: true, force: true });
3057
3125
  }
3058
3126
  function linkIntoWorktree(root, worktree, rels) {
3059
3127
  const linked = [];
3060
3128
  for (const rel of rels) {
3061
- const src = path14.join(root, rel);
3062
- const dst = path14.join(worktree, rel);
3063
- if (!fs17.existsSync(src) || fs17.existsSync(dst)) continue;
3064
- fs17.mkdirSync(path14.dirname(dst), { recursive: true });
3065
- fs17.symlinkSync(src, dst, process.platform === "win32" ? "junction" : void 0);
3129
+ const src = path15.join(root, rel);
3130
+ const dst = path15.join(worktree, rel);
3131
+ if (!fs18.existsSync(src) || fs18.existsSync(dst)) continue;
3132
+ fs18.mkdirSync(path15.dirname(dst), { recursive: true });
3133
+ fs18.symlinkSync(src, dst, process.platform === "win32" ? "junction" : void 0);
3066
3134
  linked.push(rel);
3067
3135
  }
3068
3136
  return linked;
@@ -3121,7 +3189,7 @@ function ancestors(m) {
3121
3189
  function briefOf(m) {
3122
3190
  const root = [m, ...ancestors(m)].at(-1);
3123
3191
  const f = runFile(root.id, "brief.md");
3124
- return fs18.existsSync(f) ? fs18.readFileSync(f, "utf8") : root.task;
3192
+ return fs19.existsSync(f) ? fs19.readFileSync(f, "utf8") : root.task;
3125
3193
  }
3126
3194
  function latestReview(ids) {
3127
3195
  for (const id of listRunIds().reverse()) {
@@ -3135,7 +3203,7 @@ function latestReview(ids) {
3135
3203
  }
3136
3204
  function diffOf(m, from = m.baseTree) {
3137
3205
  const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
3138
- if (where && fs18.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
3206
+ if (where && fs19.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
3139
3207
  if (from !== m.baseTree) {
3140
3208
  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
3209
  }
@@ -3149,7 +3217,7 @@ function runReview(id) {
3149
3217
  if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
3150
3218
  const chain = ancestors(m);
3151
3219
  const previous = latestReview(chain.map((a) => a.id));
3152
- const dir = m.mode === "isolate" && m.worktree && fs18.existsSync(m.worktree) ? m.cwd : m.dir;
3220
+ const dir = m.mode === "isolate" && m.worktree && fs19.existsSync(m.worktree) ? m.cwd : m.dir;
3153
3221
  const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
3154
3222
  const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
3155
3223
  if (!previous) {
@@ -3223,15 +3291,15 @@ function pickReviewer(job) {
3223
3291
  function writePackage(job) {
3224
3292
  const g = gitDir(job.dir);
3225
3293
  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;
3294
+ const file2 = path16.join(g, "pitroom", `review-${crypto5.randomBytes(4).toString("hex")}.md`);
3295
+ fs19.mkdirSync(path16.dirname(file2), { recursive: true });
3296
+ fs19.writeFileSync(file2, job.package);
3297
+ return file2;
3230
3298
  }
3231
3299
 
3232
3300
  // src/core/templates.ts
3233
- import fs19 from "node:fs";
3234
- import path16 from "node:path";
3301
+ import fs20 from "node:fs";
3302
+ import path17 from "node:path";
3235
3303
  var FILES = {
3236
3304
  implementer: "pitroom-driven-development/implementer-prompt.md",
3237
3305
  "task-reviewer": "pitroom-driven-development/task-reviewer-prompt.md",
@@ -3239,9 +3307,9 @@ var FILES = {
3239
3307
  "code-reviewer": "pitroom-review/code-reviewer.md"
3240
3308
  };
3241
3309
  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*/, "");
3310
+ const file2 = path17.join(root, "skills", FILES[name]);
3311
+ if (!fs20.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
3312
+ return fs20.readFileSync(file2, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
3245
3313
  }
3246
3314
  function fill(template, values) {
3247
3315
  const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
@@ -3348,15 +3416,15 @@ function formatModels(t) {
3348
3416
  // src/core/dash.ts
3349
3417
  import { spawn } from "node:child_process";
3350
3418
  import crypto6 from "node:crypto";
3351
- import fs20 from "node:fs";
3419
+ import fs21 from "node:fs";
3352
3420
  import http from "node:http";
3353
- import path17 from "node:path";
3421
+ import path18 from "node:path";
3354
3422
  import { fileURLToPath as fileURLToPath3 } from "node:url";
3355
3423
 
3356
3424
  // src/core/file-diff.ts
3357
3425
  var PREVIEW_LINES = 300;
3358
3426
  function gitPath(value, prefix = true) {
3359
- let path25 = value;
3427
+ let path26 = value;
3360
3428
  if (value.startsWith('"') && value.endsWith('"')) {
3361
3429
  const bytes = [];
3362
3430
  const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
@@ -3364,9 +3432,9 @@ function gitPath(value, prefix = true) {
3364
3432
  if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
3365
3433
  else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
3366
3434
  }
3367
- path25 = new TextDecoder().decode(new Uint8Array(bytes));
3435
+ path26 = new TextDecoder().decode(new Uint8Array(bytes));
3368
3436
  }
3369
- return prefix ? path25.replace(/^[ab]\//, "") : path25;
3437
+ return prefix ? path26.replace(/^[ab]\//, "") : path26;
3370
3438
  }
3371
3439
  function headerPath(header) {
3372
3440
  const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
@@ -3375,7 +3443,7 @@ function headerPath(header) {
3375
3443
  }
3376
3444
  function fileDiffs(patch, changes, sourceTruncated = false) {
3377
3445
  const files = [];
3378
- let file;
3446
+ let file2;
3379
3447
  let oldLine = 0;
3380
3448
  let newLine = 0;
3381
3449
  let oldLeft = 0;
@@ -3383,25 +3451,25 @@ function fileDiffs(patch, changes, sourceTruncated = false) {
3383
3451
  let inHunk = false;
3384
3452
  let row = 0;
3385
3453
  const finishHunk = () => {
3386
- if (file && (oldLeft > 0 || newLeft > 0)) file.incomplete = true;
3454
+ if (file2 && (oldLeft > 0 || newLeft > 0)) file2.incomplete = true;
3387
3455
  oldLeft = newLeft = 0;
3388
3456
  inHunk = false;
3389
3457
  };
3390
3458
  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++;
3459
+ if (!file2) return;
3460
+ if (file2.lines.length < PREVIEW_LINES) file2.lines.push({ ...line, id: String(row) });
3461
+ else file2.omittedLines++;
3394
3462
  row++;
3395
3463
  };
3396
3464
  for (const line of patch.split("\n")) {
3397
3465
  if (line.startsWith("diff --git ")) {
3398
3466
  finishHunk();
3399
- file = { path: headerPath(line), status: "M", lines: [], additions: 0, deletions: 0, omittedLines: 0, binary: false, incomplete: false };
3400
- files.push(file);
3467
+ file2 = { path: headerPath(line), status: "M", lines: [], additions: 0, deletions: 0, omittedLines: 0, binary: false, incomplete: false };
3468
+ files.push(file2);
3401
3469
  row = 0;
3402
3470
  continue;
3403
3471
  }
3404
- if (!file) continue;
3472
+ if (!file2) continue;
3405
3473
  const hunk = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line);
3406
3474
  if (hunk) {
3407
3475
  finishHunk();
@@ -3415,11 +3483,11 @@ function fileDiffs(patch, changes, sourceTruncated = false) {
3415
3483
  add({ type: "meta", content: line });
3416
3484
  } else if (inHunk && (oldLeft > 0 || newLeft > 0)) {
3417
3485
  if (line.startsWith("+") && newLeft > 0) {
3418
- file.additions++;
3486
+ file2.additions++;
3419
3487
  newLeft--;
3420
3488
  add({ type: "added", newLine: newLine++, content: line.slice(1) });
3421
3489
  } else if (line.startsWith("-") && oldLeft > 0) {
3422
- file.deletions++;
3490
+ file2.deletions++;
3423
3491
  oldLeft--;
3424
3492
  add({ type: "removed", oldLine: oldLine++, content: line.slice(1) });
3425
3493
  } else if (line.startsWith(" ") && oldLeft > 0 && newLeft > 0) {
@@ -3427,25 +3495,25 @@ function fileDiffs(patch, changes, sourceTruncated = false) {
3427
3495
  newLeft--;
3428
3496
  add({ type: "context", oldLine: oldLine++, newLine: newLine++, content: line.slice(1) });
3429
3497
  } else {
3430
- file.incomplete = true;
3498
+ file2.incomplete = true;
3431
3499
  }
3432
3500
  } 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;
3501
+ if (line.startsWith("+++ ") && line !== "+++ /dev/null") file2.path = gitPath(line.slice(4).replace(/\t$/, ""));
3502
+ else if (line.startsWith("--- ") && line !== "--- /dev/null") file2.path = gitPath(line.slice(4).replace(/\t$/, ""));
3503
+ else if (/^(GIT binary patch|Binary files .* differ)$/.test(line)) file2.binary = true;
3436
3504
  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";
3505
+ if (line.startsWith("new file mode ")) file2.status = "A";
3506
+ if (line.startsWith("deleted file mode ")) file2.status = "D";
3439
3507
  if (line.startsWith("rename to ")) {
3440
- file.path = gitPath(line.slice(10), false);
3441
- file.status = "R";
3508
+ file2.path = gitPath(line.slice(10), false);
3509
+ file2.status = "R";
3442
3510
  }
3443
3511
  add({ type: "meta", content: line });
3444
3512
  }
3445
3513
  }
3446
3514
  }
3447
3515
  finishHunk();
3448
- if (sourceTruncated && file) file.incomplete = true;
3516
+ if (sourceTruncated && file2) file2.incomplete = true;
3449
3517
  for (const f of files) {
3450
3518
  if (f.incomplete || f.binary) f.additions = f.deletions = void 0;
3451
3519
  }
@@ -3614,11 +3682,11 @@ var assetsDir = () => fileURLToPath3(new URL("./ui/", import.meta.url));
3614
3682
  var assetCache = /* @__PURE__ */ new Map();
3615
3683
  function readAsset(route) {
3616
3684
  try {
3617
- const file = path17.join(assetsDir(), path17.basename(route));
3618
- const { mtimeMs } = fs20.statSync(file);
3685
+ const file2 = path18.join(assetsDir(), path18.basename(route));
3686
+ const { mtimeMs } = fs21.statSync(file2);
3619
3687
  const hit = assetCache.get(route);
3620
3688
  if (hit && hit.mtimeMs === mtimeMs) return hit.data;
3621
- const data = fs20.readFileSync(file);
3689
+ const data = fs21.readFileSync(file2);
3622
3690
  assetCache.set(route, { mtimeMs, data });
3623
3691
  return data;
3624
3692
  } catch {
@@ -3698,7 +3766,7 @@ function startDash(opts) {
3698
3766
  });
3699
3767
  });
3700
3768
  }
3701
- var registryFile = () => path17.join(home(), "dash.json");
3769
+ var registryFile = () => path18.join(home(), "dash.json");
3702
3770
  async function ping(port) {
3703
3771
  try {
3704
3772
  const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
@@ -3709,11 +3777,11 @@ async function ping(port) {
3709
3777
  }
3710
3778
  async function runningDash() {
3711
3779
  try {
3712
- const r = JSON.parse(fs20.readFileSync(registryFile(), "utf8"));
3780
+ const r = JSON.parse(fs21.readFileSync(registryFile(), "utf8"));
3713
3781
  if (isAlive(r.pid) && await ping(r.port)) return { ...r, url: `http://127.0.0.1:${r.port}/` };
3714
3782
  } catch {
3715
3783
  }
3716
- fs20.rmSync(registryFile(), { force: true });
3784
+ fs21.rmSync(registryFile(), { force: true });
3717
3785
  return void 0;
3718
3786
  }
3719
3787
  function openBrowser(url) {
@@ -3728,7 +3796,7 @@ async function dashCommand(o) {
3728
3796
  if (o.stop) {
3729
3797
  if (!existing) throw new UserError("pitroom dash is not running");
3730
3798
  process.kill(existing.pid, "SIGTERM");
3731
- fs20.rmSync(registryFile(), { force: true });
3799
+ fs21.rmSync(registryFile(), { force: true });
3732
3800
  o.log(`stopped pitroom dash (${existing.url})`);
3733
3801
  return 0;
3734
3802
  }
@@ -3765,8 +3833,8 @@ async function dashCommand(o) {
3765
3833
  if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
3766
3834
  dash = await startDash({ port: 0, idleMs: o.idleMs });
3767
3835
  }
3768
- fs20.mkdirSync(home(), { recursive: true });
3769
- fs20.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
3836
+ fs21.mkdirSync(home(), { recursive: true });
3837
+ fs21.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
3770
3838
  const stop = () => void dash.close();
3771
3839
  process.once("SIGINT", stop);
3772
3840
  process.once("SIGTERM", stop);
@@ -3775,7 +3843,7 @@ async function dashCommand(o) {
3775
3843
  if (o.open) openBrowser(dash.url);
3776
3844
  await dash.closed;
3777
3845
  try {
3778
- if (JSON.parse(fs20.readFileSync(registryFile(), "utf8")).pid === process.pid) fs20.rmSync(registryFile(), { force: true });
3846
+ if (JSON.parse(fs21.readFileSync(registryFile(), "utf8")).pid === process.pid) fs21.rmSync(registryFile(), { force: true });
3779
3847
  } catch {
3780
3848
  }
3781
3849
  return 0;
@@ -3783,8 +3851,8 @@ async function dashCommand(o) {
3783
3851
 
3784
3852
  // src/core/run.ts
3785
3853
  import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
3786
- import fs26 from "node:fs";
3787
- import path22 from "node:path";
3854
+ import fs27 from "node:fs";
3855
+ import path23 from "node:path";
3788
3856
 
3789
3857
  // src/core/chain.ts
3790
3858
  function resolveChain(flags = {}) {
@@ -3834,11 +3902,11 @@ function resolveChain(flags = {}) {
3834
3902
 
3835
3903
  // src/core/process.ts
3836
3904
  import { spawn as spawn2 } from "node:child_process";
3837
- import fs22 from "node:fs";
3905
+ import fs23 from "node:fs";
3838
3906
 
3839
3907
  // src/vcs/guard.ts
3840
- import fs21 from "node:fs";
3841
- import path18 from "node:path";
3908
+ import fs22 from "node:fs";
3909
+ import path19 from "node:path";
3842
3910
  var VERSION = 1;
3843
3911
  var SHIM = `#!/bin/sh
3844
3912
  # pitroom git guard v${VERSION}. Blocks git commands that change history, refs, the
@@ -3913,15 +3981,15 @@ esac
3913
3981
  exec "$real" "$@"
3914
3982
  `;
3915
3983
  function shimDir() {
3916
- return path18.join(home(), "shim", `v${VERSION}`);
3984
+ return path19.join(home(), "shim", `v${VERSION}`);
3917
3985
  }
3918
3986
  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");
3987
+ for (const dir of (process.env.PATH ?? "").split(path19.delimiter).filter(Boolean)) {
3988
+ if (path19.resolve(dir) === skip) continue;
3989
+ const p = path19.join(dir, "git");
3922
3990
  try {
3923
- fs21.accessSync(p, fs21.constants.X_OK);
3924
- if (fs21.statSync(p).isFile()) return p;
3991
+ fs22.accessSync(p, fs22.constants.X_OK);
3992
+ if (fs22.statSync(p).isFile()) return p;
3925
3993
  } catch {
3926
3994
  }
3927
3995
  }
@@ -3934,13 +4002,13 @@ var REF_HOOK = `#!/bin/sh
3934
4002
  echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
3935
4003
  exit 1
3936
4004
  `;
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);
4005
+ var hooksDir = () => path19.join(home(), "git-hooks", `v${HOOK_VERSION}`);
4006
+ function writeIfChanged(file2, content) {
4007
+ if (fs22.existsSync(file2) && fs22.readFileSync(file2, "utf8") === content) return;
4008
+ fs22.mkdirSync(path19.dirname(file2), { recursive: true });
4009
+ const tmp = `${file2}.${process.pid}.tmp`;
4010
+ fs22.writeFileSync(tmp, content, { mode: 493 });
4011
+ fs22.renameSync(tmp, file2);
3944
4012
  }
3945
4013
  function withGitConfig(env, entries) {
3946
4014
  const out = { ...env };
@@ -3962,7 +4030,7 @@ function guardEnv(env) {
3962
4030
  PAGER: "cat"
3963
4031
  };
3964
4032
  if (process.platform === "win32") return { ...env, ...quiet };
3965
- writeIfChanged(path18.join(hooksDir(), "reference-transaction"), REF_HOOK);
4033
+ writeIfChanged(path19.join(hooksDir(), "reference-transaction"), REF_HOOK);
3966
4034
  env = withGitConfig(env, [
3967
4035
  ["core.hooksPath", hooksDir()],
3968
4036
  ["url.pitroom-push-blocked://.pushInsteadOf", ""]
@@ -3970,14 +4038,14 @@ function guardEnv(env) {
3970
4038
  const dir = shimDir();
3971
4039
  const real = findRealGit(dir);
3972
4040
  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 ?? ""}` };
4041
+ writeIfChanged(path19.join(dir, "git"), SHIM);
4042
+ return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${path19.delimiter}${env.PATH ?? ""}` };
3975
4043
  }
3976
4044
 
3977
4045
  // src/core/process.ts
3978
4046
  async function spawnWorker(inv, opts) {
3979
- const out = fs22.openSync(opts.stdoutFile, "w");
3980
- const err = fs22.openSync(opts.stderrFile, "w");
4047
+ const out = fs23.openSync(opts.stdoutFile, "w");
4048
+ const err = fs23.openSync(opts.stderrFile, "w");
3981
4049
  const res = { code: null, timedOut: false, stopped: false };
3982
4050
  const child = spawn2(inv.command, inv.args, {
3983
4051
  cwd: opts.cwd,
@@ -4009,8 +4077,8 @@ async function spawnWorker(inv, opts) {
4009
4077
  if (killer) clearTimeout(killer);
4010
4078
  process.off("SIGINT", stop);
4011
4079
  process.off("SIGTERM", stop);
4012
- fs22.closeSync(out);
4013
- fs22.closeSync(err);
4080
+ fs23.closeSync(out);
4081
+ fs23.closeSync(err);
4014
4082
  return res;
4015
4083
  }
4016
4084
 
@@ -4043,10 +4111,10 @@ function parseAudit(text) {
4043
4111
 
4044
4112
  // src/core/slots.ts
4045
4113
  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");
4114
+ import fs24 from "node:fs";
4115
+ import path20 from "node:path";
4116
+ var slotsDir = () => path20.join(home(), "slots");
4117
+ var locksDir = () => path20.join(home(), "locks");
4050
4118
  var STARTUP_GRACE_MS = 3e4;
4051
4119
  function holderActive(runId, self) {
4052
4120
  if (!runId || runId === self) return false;
@@ -4059,51 +4127,51 @@ function holderActive(runId, self) {
4059
4127
  return false;
4060
4128
  }
4061
4129
  }
4062
- function tryClaim(file, runId) {
4063
- fs23.mkdirSync(path19.dirname(file), { recursive: true });
4130
+ function tryClaim(file2, runId) {
4131
+ fs24.mkdirSync(path20.dirname(file2), { recursive: true });
4064
4132
  try {
4065
- fs23.writeFileSync(file, runId, { flag: "wx" });
4133
+ fs24.writeFileSync(file2, runId, { flag: "wx" });
4066
4134
  return true;
4067
4135
  } catch (e) {
4068
4136
  if (e.code !== "EEXIST") throw e;
4069
4137
  }
4070
4138
  let owner = "";
4071
4139
  try {
4072
- owner = fs23.readFileSync(file, "utf8").trim();
4140
+ owner = fs24.readFileSync(file2, "utf8").trim();
4073
4141
  } catch {
4074
4142
  }
4075
4143
  if (owner === runId) return true;
4076
4144
  if (holderActive(owner, runId)) return false;
4077
- fs23.rmSync(file, { force: true });
4145
+ fs24.rmSync(file2, { force: true });
4078
4146
  try {
4079
- fs23.writeFileSync(file, runId, { flag: "wx" });
4147
+ fs24.writeFileSync(file2, runId, { flag: "wx" });
4080
4148
  return true;
4081
4149
  } catch {
4082
4150
  return false;
4083
4151
  }
4084
4152
  }
4085
- function releaseIfOwner(file, runId) {
4086
- if (!file) return;
4153
+ function releaseIfOwner(file2, runId) {
4154
+ if (!file2) return;
4087
4155
  try {
4088
- if (fs23.readFileSync(file, "utf8").trim() === runId) fs23.rmSync(file, { force: true });
4156
+ if (fs24.readFileSync(file2, "utf8").trim() === runId) fs24.rmSync(file2, { force: true });
4089
4157
  } catch {
4090
4158
  }
4091
4159
  }
4092
4160
  function tryAcquireSlot(runId, maxParallel) {
4093
4161
  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;
4162
+ const file2 = path20.join(slotsDir(), `slot-${n}`);
4163
+ if (tryClaim(file2, runId)) return file2;
4096
4164
  }
4097
4165
  return void 0;
4098
4166
  }
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)}`);
4167
+ var releaseSlot = (file2, runId) => releaseIfOwner(file2, runId);
4168
+ var lockFile = (repoRoot2) => path20.join(locksDir(), `write-${crypto7.createHash("sha1").update(path20.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
4101
4169
  function acquireWriteLock(repoRoot2, runId) {
4102
- const file = lockFile(repoRoot2);
4103
- if (tryClaim(file, runId)) return;
4170
+ const file2 = lockFile(repoRoot2);
4171
+ if (tryClaim(file2, runId)) return;
4104
4172
  let owner = "";
4105
4173
  try {
4106
- owner = fs23.readFileSync(file, "utf8").trim();
4174
+ owner = fs24.readFileSync(file2, "utf8").trim();
4107
4175
  } catch {
4108
4176
  }
4109
4177
  throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
@@ -4150,8 +4218,8 @@ ${task}`;
4150
4218
  }
4151
4219
 
4152
4220
  // src/core/refs.ts
4153
- import fs24 from "node:fs";
4154
- import path20 from "node:path";
4221
+ import fs25 from "node:fs";
4222
+ import path21 from "node:path";
4155
4223
  var FILE_EXTENSIONS = new Set(
4156
4224
  "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
4225
  );
@@ -4202,29 +4270,29 @@ function extractRefs(answer) {
4202
4270
  return [...seen.values()];
4203
4271
  }
4204
4272
  function verifyRefs(refs, dirs) {
4205
- const roots = [...new Set(dirs.filter(Boolean).map((d) => path20.resolve(d)))];
4273
+ const roots = [...new Set(dirs.filter(Boolean).map((d) => path21.resolve(d)))];
4206
4274
  const invalid = [];
4207
4275
  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");
4276
+ const load = (file2) => {
4277
+ if (!lineCache.has(file2)) {
4278
+ const size = fs25.statSync(file2).size;
4279
+ const lines = size > MAX_BYTES ? null : fs25.readFileSync(file2, "utf8").split("\n");
4212
4280
  if (lines && lines[lines.length - 1] === "") lines.pop();
4213
- lineCache.set(file, lines);
4281
+ lineCache.set(file2, lines);
4214
4282
  }
4215
- return lineCache.get(file);
4283
+ return lineCache.get(file2);
4216
4284
  };
4217
4285
  let index;
4218
4286
  const byName = () => index ??= indexFiles(roots);
4219
4287
  const candidates = (ref) => {
4220
4288
  const direct = resolve(ref.file, roots);
4221
4289
  if (direct) return [direct];
4222
- if (path20.isAbsolute(ref.file)) return [];
4290
+ if (path21.isAbsolute(ref.file)) return [];
4223
4291
  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);
4292
+ return (byName().get(path21.basename(wanted)) ?? []).filter((f) => f.endsWith(`/${wanted}`) || path21.basename(f) === wanted).slice(0, 20);
4225
4293
  };
4226
- const check = (file, ref) => {
4227
- const lines = load(file);
4294
+ const check = (file2, ref) => {
4295
+ const lines = load(file2);
4228
4296
  if (!lines) return void 0;
4229
4297
  if (ref.end > lines.length) return `file has ${lines.length} lines`;
4230
4298
  if (ref.symbol && !mentions(lines, ref) && !enclosedBy(lines, ref)) return `\`${ref.symbol}\` not near line ${ref.start}`;
@@ -4236,7 +4304,7 @@ function verifyRefs(refs, dirs) {
4236
4304
  if (!files.length) {
4237
4305
  if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
4238
4306
  total++;
4239
- invalid.push({ ref: ref.text, reason: path20.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
4307
+ invalid.push({ ref: ref.text, reason: path21.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
4240
4308
  continue;
4241
4309
  }
4242
4310
  total++;
@@ -4257,7 +4325,7 @@ function enclosedBy(lines, ref) {
4257
4325
  return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
4258
4326
  }
4259
4327
  var isFileName = (name) => {
4260
- const base2 = path20.basename(name);
4328
+ const base2 = path21.basename(name);
4261
4329
  return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
4262
4330
  };
4263
4331
  function indexFiles(roots) {
@@ -4266,19 +4334,19 @@ function indexFiles(roots) {
4266
4334
  const walk = (dir) => {
4267
4335
  let entries;
4268
4336
  try {
4269
- entries = fs24.readdirSync(dir, { withFileTypes: true });
4337
+ entries = fs25.readdirSync(dir, { withFileTypes: true });
4270
4338
  } catch {
4271
4339
  return;
4272
4340
  }
4273
4341
  for (const e of entries) {
4274
4342
  if (count >= MAX_INDEXED) return;
4275
4343
  if (e.isDirectory()) {
4276
- if (!SKIP_DIRS.has(e.name)) walk(path20.join(dir, e.name));
4344
+ if (!SKIP_DIRS.has(e.name)) walk(path21.join(dir, e.name));
4277
4345
  } else if (e.isFile()) {
4278
4346
  count++;
4279
4347
  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)]);
4348
+ if (list2) list2.push(path21.join(dir, e.name));
4349
+ else index.set(e.name, [path21.join(dir, e.name)]);
4282
4350
  }
4283
4351
  }
4284
4352
  };
@@ -4286,27 +4354,27 @@ function indexFiles(roots) {
4286
4354
  return index;
4287
4355
  }
4288
4356
  function resolve(ref, roots) {
4289
- const candidates = path20.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path20.join(r, ref));
4357
+ const candidates = path21.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path21.join(r, ref));
4290
4358
  return candidates.find((c) => {
4291
4359
  try {
4292
- return fs24.statSync(c).isFile() && inside(c, roots);
4360
+ return fs25.statSync(c).isFile() && inside(c, roots);
4293
4361
  } catch {
4294
4362
  return false;
4295
4363
  }
4296
4364
  });
4297
4365
  }
4298
- function inside(file, roots) {
4366
+ function inside(file2, roots) {
4299
4367
  return roots.some((r) => {
4300
- const rel = path20.relative(r, path20.resolve(file));
4301
- return rel !== "" && !rel.startsWith("..") && !path20.isAbsolute(rel);
4368
+ const rel = path21.relative(r, path21.resolve(file2));
4369
+ return rel !== "" && !rel.startsWith("..") && !path21.isAbsolute(rel);
4302
4370
  });
4303
4371
  }
4304
4372
  var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4305
4373
 
4306
4374
  // src/core/secrets.ts
4307
4375
  import { execFileSync } from "node:child_process";
4308
- import fs25 from "node:fs";
4309
- import path21 from "node:path";
4376
+ import fs26 from "node:fs";
4377
+ import path22 from "node:path";
4310
4378
  var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
4311
4379
  var MAX_VISITED = 2e4;
4312
4380
  var MAX_DEPTH = 4;
@@ -4322,16 +4390,16 @@ function findSecretFiles(dir) {
4322
4390
  if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
4323
4391
  let entries;
4324
4392
  try {
4325
- entries = fs25.readdirSync(d, { withFileTypes: true });
4393
+ entries = fs26.readdirSync(d, { withFileTypes: true });
4326
4394
  } catch {
4327
4395
  return;
4328
4396
  }
4329
4397
  for (const e of entries) {
4330
4398
  visited++;
4331
4399
  if (e.isDirectory()) {
4332
- if (!SKIP_DIRS2.has(e.name)) walk(path21.join(d, e.name), depth + 1);
4400
+ if (!SKIP_DIRS2.has(e.name)) walk(path22.join(d, e.name), depth + 1);
4333
4401
  } else if (e.isFile() && looksSecret(e.name)) {
4334
- found.push(path21.relative(dir, path21.join(d, e.name)));
4402
+ found.push(path22.relative(dir, path22.join(d, e.name)));
4335
4403
  }
4336
4404
  }
4337
4405
  };
@@ -4341,7 +4409,7 @@ function findSecretFiles(dir) {
4341
4409
  function findSecretFilesInTree(root, dir) {
4342
4410
  try {
4343
4411
  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();
4412
+ return out.split("\0").filter((f) => f && looksSecret(path22.basename(f)) && !path22.relative(dir, path22.join(root, f)).startsWith("..")).map((f) => path22.relative(dir, path22.join(root, f))).sort();
4345
4413
  } catch {
4346
4414
  return [];
4347
4415
  }
@@ -4355,7 +4423,7 @@ function secretWarning(files, mode) {
4355
4423
  }
4356
4424
 
4357
4425
  // src/core/run.ts
4358
- var VERSION2 = true ? "0.11.0" : "0.0.0-dev";
4426
+ var VERSION2 = true ? "0.12.0" : "0.0.0-dev";
4359
4427
  function planWork(o) {
4360
4428
  if (!o.plan) return { task: o.task.trim(), tier: o.tier };
4361
4429
  if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
@@ -4387,7 +4455,7 @@ function prepareRun(o) {
4387
4455
  parent = readMeta(o.continueFrom);
4388
4456
  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
4457
  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))) {
4458
+ if (parent.mode === "isolate" && (!parent.worktree || !fs27.existsSync(parent.worktree))) {
4391
4459
  throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
4392
4460
  }
4393
4461
  const ran = parent.ran ?? parent.worker;
@@ -4406,15 +4474,15 @@ function prepareRun(o) {
4406
4474
  throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
4407
4475
  }
4408
4476
  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}`);
4477
+ const dir = path23.resolve(parent?.dir ?? o.dir);
4478
+ if (!fs27.existsSync(dir) || !fs27.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
4411
4479
  const root = repoRoot(dir);
4412
4480
  if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
4413
4481
  if (mode === "write" && !root && !o.allowNonGit) {
4414
4482
  throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
4415
4483
  }
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}`);
4484
+ const files = o.files.map((f) => path23.resolve(f));
4485
+ for (const f of files) if (!fs27.existsSync(f)) throw new UserError(`file not found: ${f}`);
4418
4486
  if (!parent && !process.env.PITROOM_NO_SECRET_WARNING) {
4419
4487
  const secrets = mode === "isolate" && root ? findSecretFilesInTree(root, dir) : findSecretFiles(dir);
4420
4488
  const note = secretWarning(secrets, mode);
@@ -4456,13 +4524,13 @@ function prepareRun(o) {
4456
4524
  try {
4457
4525
  acquireWriteLock(root, meta.id);
4458
4526
  } catch (e) {
4459
- fs26.rmSync(runDir(meta.id), { recursive: true, force: true });
4527
+ fs27.rmSync(runDir(meta.id), { recursive: true, force: true });
4460
4528
  throw e;
4461
4529
  }
4462
4530
  }
4463
- fs26.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
4531
+ fs27.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
4464
4532
  `);
4465
- if (work.brief) fs26.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
4533
+ if (work.brief) fs27.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
4466
4534
  return meta;
4467
4535
  }
4468
4536
  var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -4519,12 +4587,19 @@ async function execute(meta) {
4519
4587
  for (let i = 0; i < chain.length; i++) {
4520
4588
  const target = chain[i];
4521
4589
  const backend = getBackend(target.backend);
4590
+ const cooling = activeCooldown(cooldownKey(target, backend));
4591
+ if (cooling && chain.slice(i + 1).some((t) => !activeCooldown(cooldownKey(t, getBackend(t.backend))))) {
4592
+ (meta.attempts ??= []).push({ target: describeTarget(target), error: `cooling down ${untilText(cooling)}: ${cooling.reason}`, skipped: true });
4593
+ writeMeta(meta);
4594
+ continue;
4595
+ }
4522
4596
  meta.ran = target;
4523
4597
  writeMeta(meta);
4524
4598
  result = await attempt(meta, backend, target);
4525
4599
  if (result.timedOut || result.stopped || result.spawnError) break;
4526
4600
  const why = retryableFailure(meta, backend, result);
4527
4601
  if (!why) break;
4602
+ if (why.kind === "rate-limited") recordCooldown(cooldownKey(target, backend), describeTarget(target), why.message);
4528
4603
  if (!target.model && backend.defaultModel) {
4529
4604
  const failed = backend.defaultModel();
4530
4605
  for (let j = chain.length - 1; j > i; j--) {
@@ -4535,7 +4610,7 @@ async function execute(meta) {
4535
4610
  (meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
4536
4611
  for (const f of ["events.jsonl", "stderr.log"]) {
4537
4612
  const from = runFile(meta.id, f);
4538
- if (fs26.existsSync(from)) fs26.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
4613
+ if (fs27.existsSync(from)) fs27.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
4539
4614
  }
4540
4615
  writeMeta(meta);
4541
4616
  }
@@ -4554,13 +4629,13 @@ async function execute(meta) {
4554
4629
  }
4555
4630
  function autoAudit(meta) {
4556
4631
  try {
4557
- if (!auditable(meta, read(runFile(meta.id, "summary.md"))) || !sampled(meta)) return;
4632
+ if (!auditable(meta, read2(runFile(meta.id, "summary.md"))) || !sampled(meta)) return;
4558
4633
  startAudit(meta);
4559
4634
  } catch {
4560
4635
  }
4561
4636
  }
4562
4637
  function startAudit(meta, worker) {
4563
- const answer = read(runFile(meta.id, "summary.md"));
4638
+ const answer = read2(runFile(meta.id, "summary.md"));
4564
4639
  const auditor = worker ?? pickAuditor(meta);
4565
4640
  if (!auditor) return void 0;
4566
4641
  const a = prepareRun({
@@ -4596,9 +4671,9 @@ function prepareTree(meta) {
4596
4671
  if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
4597
4672
  if (root && meta.mode === "isolate" && !meta.worktree) {
4598
4673
  meta.baseTree = snapshotTree(root);
4599
- meta.worktree = path22.join(worktreesDir(), meta.id);
4674
+ meta.worktree = path23.join(worktreesDir(), meta.id);
4600
4675
  createIsolatedCopy(root, meta.baseTree, meta.worktree);
4601
- meta.cwd = path22.join(meta.worktree, path22.relative(root, meta.dir));
4676
+ meta.cwd = path23.join(meta.worktree, path23.relative(root, meta.dir));
4602
4677
  const linked = linkIntoWorktree(root, meta.worktree, meta.link);
4603
4678
  if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
4604
4679
  }
@@ -4621,11 +4696,11 @@ function attempt(meta, backend, target) {
4621
4696
  timeoutSec: meta.timeoutSec
4622
4697
  });
4623
4698
  }
4624
- var read = (f) => fs26.existsSync(f) ? fs26.readFileSync(f, "utf8") : "";
4699
+ var read2 = (f) => fs27.existsSync(f) ? fs27.readFileSync(f, "utf8") : "";
4625
4700
  function retryableFailure(meta, backend, res) {
4626
- const run = backend.parse(read(runFile(meta.id, "events.jsonl")));
4701
+ const run = backend.parse(read2(runFile(meta.id, "events.jsonl")));
4627
4702
  if (run.usage.steps > 0) return void 0;
4628
- const f = backend.failure(run, read(runFile(meta.id, "stderr.log")), res.code);
4703
+ const f = backend.failure(run, read2(runFile(meta.id, "stderr.log")), res.code);
4629
4704
  return f && f.kind !== "other" ? f : void 0;
4630
4705
  }
4631
4706
  var HINTS = {
@@ -4637,18 +4712,18 @@ var HINTS = {
4637
4712
  function finalize(meta, res) {
4638
4713
  const ran = meta.ran ?? meta.worker;
4639
4714
  const backend = getBackend(ran.backend);
4640
- const run = backend.parse(read(runFile(meta.id, "events.jsonl")));
4715
+ const run = backend.parse(read2(runFile(meta.id, "events.jsonl")));
4641
4716
  meta.sessionId = run.sessionId ?? meta.sessionId;
4642
4717
  meta.usage = run.usage;
4643
- fs26.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
4718
+ fs27.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
4644
4719
  `);
4645
4720
  if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run.finalText);
4646
4721
  if (meta.reviewOf) {
4647
4722
  meta.verdict = parseVerdict(run.finalText);
4648
- if (meta.packageFile) fs26.rmSync(meta.packageFile, { force: true });
4723
+ if (meta.packageFile) fs27.rmSync(meta.packageFile, { force: true });
4649
4724
  }
4650
4725
  if (!res.timedOut && !res.stopped && !meta.error) {
4651
- const f = backend.failure(run, read(runFile(meta.id, "stderr.log")), res.code);
4726
+ const f = backend.failure(run, read2(runFile(meta.id, "stderr.log")), res.code);
4652
4727
  if (f) meta.error = HINTS[f.kind] ? `${f.message} (${HINTS[f.kind]})` : f.message;
4653
4728
  }
4654
4729
  try {
@@ -4693,7 +4768,7 @@ function captureChanges(meta) {
4693
4768
  const d = diffTrees(target, meta.baseTree, meta.afterTree);
4694
4769
  meta.changes = d.changes;
4695
4770
  meta.stats = d.stats;
4696
- fs26.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
4771
+ fs27.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
4697
4772
  }
4698
4773
  function runVerify(meta) {
4699
4774
  const r = spawnSync6(meta.verify, {
@@ -4706,7 +4781,7 @@ function runVerify(meta) {
4706
4781
  env: { ...process.env, PWD: meta.cwd, PITROOM_ACTIVE: "1" }
4707
4782
  });
4708
4783
  const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
4709
- fs26.writeFileSync(runFile(meta.id, "verify.log"), output);
4784
+ fs27.writeFileSync(runFile(meta.id, "verify.log"), output);
4710
4785
  return { ok: r.status === 0, code: r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
4711
4786
  }
4712
4787
  function applyRun(meta, allowDelete = false) {
@@ -4751,7 +4826,7 @@ function discardRun(meta) {
4751
4826
  return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
4752
4827
  }
4753
4828
  function cleanupWorktree(meta) {
4754
- if (meta.worktree && fs26.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
4829
+ if (meta.worktree && fs27.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
4755
4830
  }
4756
4831
 
4757
4832
  // src/cli/commands.ts
@@ -4801,10 +4876,10 @@ async function cmdReview(p) {
4801
4876
  writeMeta(meta);
4802
4877
  }
4803
4878
  } catch (e) {
4804
- fs27.rmSync(packageFile, { force: true });
4879
+ fs28.rmSync(packageFile, { force: true });
4805
4880
  throw e;
4806
4881
  }
4807
- fs27.writeFileSync(runFile(meta.id, "package.md"), job.package);
4882
+ fs28.writeFileSync(runFile(meta.id, "package.md"), job.package);
4808
4883
  return launch(p, meta);
4809
4884
  }
4810
4885
  async function cmdAudit(p) {
@@ -4834,24 +4909,45 @@ async function cmdAudit(p) {
4834
4909
  writeMeta(m);
4835
4910
  return launch(p, a);
4836
4911
  }
4912
+ function cmdCooldown(p) {
4913
+ if (has(p, "clear")) {
4914
+ const n = clearCooldowns();
4915
+ console.log(n ? `cleared ${n} cooldown${n === 1 ? "" : "s"}: those models are tried again` : "no cooldowns");
4916
+ return 0;
4917
+ }
4918
+ const all = activeCooldowns();
4919
+ if (has(p, "json")) {
4920
+ console.log(JSON.stringify(all, null, 2));
4921
+ return 0;
4922
+ }
4923
+ const rows = Object.values(all);
4924
+ if (!rows.length) {
4925
+ console.log("no model is cooling down");
4926
+ return 0;
4927
+ }
4928
+ console.log("Models left alone for now (runs go to the next worker; they are tried again after the time shown):");
4929
+ for (const c of rows) console.log(` ${c.target.padEnd(52)} ${untilText(c)} \xB7 ${c.reason.slice(0, 90)}`);
4930
+ console.log("\npitroom cooldown --clear try them again now");
4931
+ return 0;
4932
+ }
4837
4933
  function cmdPlan(p) {
4838
- const [sub, file, ...rest] = p.positional;
4839
- if (sub === "status" && file) {
4840
- const s = planStatus(file);
4934
+ const [sub, file2, ...rest] = p.positional;
4935
+ if (sub === "status" && file2) {
4936
+ const s = planStatus(file2);
4841
4937
  console.log(
4842
4938
  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
4939
  );
4844
4940
  return 0;
4845
4941
  }
4846
- if (sub === "note" && file && rest.length) {
4847
- console.log(`noted in ${addNote(file, rest.join(" "))}`);
4942
+ if (sub === "note" && file2 && rest.length) {
4943
+ console.log(`noted in ${addNote(file2, rest.join(" "))}`);
4848
4944
  return 0;
4849
4945
  }
4850
4946
  throw new UserError('usage: pitroom plan status PLAN.md [--json] | pitroom plan note PLAN.md "Task N: \u2026"');
4851
4947
  }
4852
4948
  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);
4949
+ const file2 = flag(p, "task-file");
4950
+ const tasks = (file2 ? fs28.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
4855
4951
  if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
4856
4952
  if (has(p, "write") && tasks.length > 1) {
4857
4953
  throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
@@ -5052,7 +5148,7 @@ function sinceMs(s) {
5052
5148
  if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
5053
5149
  return Date.now() - Number(m[1]) * 864e5;
5054
5150
  }
5055
- var readStdin = () => process.stdin.isTTY ? "" : fs27.readFileSync(0, "utf8");
5151
+ var readStdin = () => process.stdin.isTTY ? "" : fs28.readFileSync(0, "utf8");
5056
5152
  function cmdStatusline(p) {
5057
5153
  const input = readStdin();
5058
5154
  const lines = [];
@@ -5092,8 +5188,8 @@ function cmdModels(p) {
5092
5188
  return 0;
5093
5189
  }
5094
5190
  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 });
5191
+ const script = path24.join(path24.dirname(path24.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
5192
+ if (fs28.existsSync(script)) spawnSync7(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
5097
5193
  return 0;
5098
5194
  }
5099
5195
  function cmdInit(p) {
@@ -5133,8 +5229,8 @@ function cmdSavings(p) {
5133
5229
  est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
5134
5230
  const out = flag(p, "card");
5135
5231
  if (out) {
5136
- fs27.writeFileSync(out, card(t, period));
5137
- console.log(`card written to ${path23.resolve(out)}`);
5232
+ fs28.writeFileSync(out, card(t, period));
5233
+ console.log(`card written to ${path24.resolve(out)}`);
5138
5234
  }
5139
5235
  if (has(p, "badge")) console.log(`![pitroom](${badgeUrl(t)})`);
5140
5236
  return 0;
@@ -5154,7 +5250,7 @@ function cmdConfig(p) {
5154
5250
  console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
5155
5251
  return 0;
5156
5252
  }
5157
- console.log(`config file: ${configPath()}${fs27.existsSync(configPath()) ? "" : " (not present)"}`);
5253
+ console.log(`config file: ${configPath()}${fs28.existsSync(configPath()) ? "" : " (not present)"}`);
5158
5254
  for (const [key, s] of Object.entries(eff)) {
5159
5255
  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
5256
  console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
@@ -5178,7 +5274,7 @@ function cmdClean(p) {
5178
5274
  const m = readMeta(id);
5179
5275
  if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
5180
5276
  recordRun(m);
5181
- fs27.rmSync(runDir(id), { recursive: true, force: true });
5277
+ fs28.rmSync(runDir(id), { recursive: true, force: true });
5182
5278
  }
5183
5279
  console.log(`removed ${old.length} run(s); the history and the savings ledger are kept`);
5184
5280
  return 0;
@@ -5188,9 +5284,9 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
5188
5284
 
5189
5285
  // src/core/doctor.ts
5190
5286
  import { spawnSync as spawnSync8 } from "node:child_process";
5191
- import fs28 from "node:fs";
5287
+ import fs29 from "node:fs";
5192
5288
  import os10 from "node:os";
5193
- import path24 from "node:path";
5289
+ import path25 from "node:path";
5194
5290
  var MARK = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
5195
5291
  var NEXT = [
5196
5292
  { when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
@@ -5200,15 +5296,15 @@ var NEXT = [
5200
5296
  { 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
5297
  ];
5202
5298
  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");
5299
+ for (const dir of (process.env.PATH ?? "").split(path25.delimiter).filter(Boolean)) {
5300
+ const file2 = path25.join(dir, process.platform === "win32" ? "node.exe" : "node");
5205
5301
  try {
5206
- if (!fs28.statSync(file).isFile()) continue;
5302
+ if (!fs29.statSync(file2).isFile()) continue;
5207
5303
  } catch {
5208
5304
  continue;
5209
5305
  }
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: "" };
5306
+ const r = spawnSync8(file2, ["-p", "process.versions.node"], { encoding: "utf8", timeout: 5e3 });
5307
+ return r.status === 0 ? { path: file2, version: r.stdout.trim() } : { path: file2, version: "" };
5212
5308
  }
5213
5309
  return void 0;
5214
5310
  }
@@ -5230,12 +5326,12 @@ function doctor5(probe) {
5230
5326
  const addAll = (list2) => list2.forEach((c) => add(c.level, c.message));
5231
5327
  add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
5232
5328
  const onPath = firstNodeOnPath();
5233
- if (onPath && onPath.version && tooOld(onPath.version) && path24.resolve(onPath.path) !== path24.resolve(process.execPath)) {
5329
+ if (onPath && onPath.version && tooOld(onPath.version) && path25.resolve(onPath.path) !== path25.resolve(process.execPath)) {
5234
5330
  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
5331
  }
5236
5332
  add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
5237
5333
  const cfg = loadConfig();
5238
- add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs28.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
5334
+ add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs29.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
5239
5335
  for (const w of cfg.warnings) add("warn", w);
5240
5336
  if (process.platform !== "win32") {
5241
5337
  const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
@@ -5277,6 +5373,9 @@ function doctor5(probe) {
5277
5373
  add("ok", `cost: ${key} = ${mine}${cheaper ? `; you priced ${cheaper[0]} cheaper (${cheaper[1]}): is the dearer one needed?` : ""}`);
5278
5374
  }
5279
5375
  }
5376
+ for (const c of Object.values(activeCooldowns())) {
5377
+ 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`);
5378
+ }
5280
5379
  const byBackend = /* @__PURE__ */ new Map();
5281
5380
  for (const t of [...chain, ...tierTargets]) byBackend.set(t.backend, [...byBackend.get(t.backend) ?? [], t.model]);
5282
5381
  for (const [id, models] of byBackend) {
@@ -5380,19 +5479,19 @@ function skillChecks() {
5380
5479
  else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
5381
5480
  }
5382
5481
  if (geminiExtensionInstalled()) {
5383
- const linked = installedSkills().some((i) => i.base.includes(`${path24.sep}.agents${path24.sep}`) && i.names.length);
5482
+ const linked = installedSkills().some((i) => i.base.includes(`${path25.sep}.agents${path25.sep}`) && i.names.length);
5384
5483
  checks.push(
5385
5484
  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
5485
  );
5387
5486
  }
5388
5487
  if (viaPlugin) {
5389
- const linked = installedSkills().some((i) => i.base.includes(`${path24.sep}.claude${path24.sep}`) && i.names.length);
5488
+ const linked = installedSkills().some((i) => i.base.includes(`${path25.sep}.claude${path25.sep}`) && i.names.length);
5390
5489
  checks.push(
5391
5490
  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
5491
  );
5393
5492
  }
5394
5493
  const launcher = launcherPath();
5395
- if (fs28.existsSync(launcher)) {
5494
+ if (fs29.existsSync(launcher)) {
5396
5495
  const r = spawnSync8(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
5397
5496
  checks.push(
5398
5497
  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 +5503,12 @@ function skillChecks() {
5404
5503
  }
5405
5504
  function geminiExtensionInstalled() {
5406
5505
  const home2 = process.env.GEMINI_CLI_HOME ?? os10.homedir();
5407
- return fs28.existsSync(path24.join(home2, ".gemini", "extensions", "pitroom"));
5506
+ return fs29.existsSync(path25.join(home2, ".gemini", "extensions", "pitroom"));
5408
5507
  }
5409
5508
  function pluginInstalled() {
5410
5509
  try {
5411
- const f = path24.join(os10.homedir(), ".claude", "plugins", "installed_plugins.json");
5412
- const plugins = JSON.parse(fs28.readFileSync(f, "utf8")).plugins ?? {};
5510
+ const f = path25.join(os10.homedir(), ".claude", "plugins", "installed_plugins.json");
5511
+ const plugins = JSON.parse(fs29.readFileSync(f, "utf8")).plugins ?? {};
5413
5512
  return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
5414
5513
  } catch {
5415
5514
  return false;
@@ -5426,24 +5525,24 @@ function superpowersActive() {
5426
5525
  for (const p of openCodePlugins()) {
5427
5526
  if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
5428
5527
  }
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);
5528
+ for (const base2 of [path25.join(os10.homedir(), ".agents", "skills"), path25.join(os10.homedir(), ".claude", "skills")]) {
5529
+ const dir = path25.join(base2, "using-superpowers");
5530
+ if (fs29.existsSync(path25.join(dir, "SKILL.md"))) found.push(dir);
5432
5531
  }
5433
5532
  return found;
5434
5533
  }
5435
5534
  function claudePlugins() {
5436
5535
  try {
5437
- const f = path24.join(os10.homedir(), ".claude", "plugins", "installed_plugins.json");
5438
- return Object.keys(JSON.parse(fs28.readFileSync(f, "utf8")).plugins ?? {});
5536
+ const f = path25.join(os10.homedir(), ".claude", "plugins", "installed_plugins.json");
5537
+ return Object.keys(JSON.parse(fs29.readFileSync(f, "utf8")).plugins ?? {});
5439
5538
  } catch {
5440
5539
  return [];
5441
5540
  }
5442
5541
  }
5443
5542
  function claudePluginEnabled(key) {
5444
5543
  try {
5445
- const f = path24.join(os10.homedir(), ".claude", "settings.json");
5446
- return JSON.parse(fs28.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
5544
+ const f = path25.join(os10.homedir(), ".claude", "settings.json");
5545
+ return JSON.parse(fs29.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
5447
5546
  } catch {
5448
5547
  return true;
5449
5548
  }
@@ -5452,7 +5551,7 @@ function codexPlugins() {
5452
5551
  const plugins = /* @__PURE__ */ new Map();
5453
5552
  let text;
5454
5553
  try {
5455
- text = fs28.readFileSync(path24.join(process.env.CODEX_HOME ?? path24.join(os10.homedir(), ".codex"), "config.toml"), "utf8");
5554
+ text = fs29.readFileSync(path25.join(process.env.CODEX_HOME ?? path25.join(os10.homedir(), ".codex"), "config.toml"), "utf8");
5456
5555
  } catch {
5457
5556
  return plugins;
5458
5557
  }
@@ -5470,11 +5569,11 @@ function codexPlugins() {
5470
5569
  return plugins;
5471
5570
  }
5472
5571
  function openCodePlugins() {
5473
- const dir = path24.join(process.env.XDG_CONFIG_HOME ?? path24.join(os10.homedir(), ".config"), "opencode");
5572
+ const dir = path25.join(process.env.XDG_CONFIG_HOME ?? path25.join(os10.homedir(), ".config"), "opencode");
5474
5573
  const plugins = [];
5475
5574
  for (const name of ["opencode.json", "opencode.jsonc"]) {
5476
5575
  try {
5477
- const text = fs28.readFileSync(path24.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
5576
+ const text = fs29.readFileSync(path25.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
5478
5577
  const list2 = JSON.parse(text).plugin;
5479
5578
  if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
5480
5579
  } catch {
@@ -5482,7 +5581,7 @@ function openCodePlugins() {
5482
5581
  }
5483
5582
  for (const folder of ["plugin", "plugins"]) {
5484
5583
  try {
5485
- for (const f of fs28.readdirSync(path24.join(dir, folder))) plugins.push(path24.join(dir, folder, f));
5584
+ for (const f of fs29.readdirSync(path25.join(dir, folder))) plugins.push(path25.join(dir, folder, f));
5486
5585
  } catch {
5487
5586
  }
5488
5587
  }
@@ -5532,6 +5631,8 @@ Usage
5532
5631
  pitroom statusline [--then CMD] status-bar line: running workers, savings this week (after CMD's)
5533
5632
  pitroom hook-card PostToolUse hook: a card after each Bash \`pitroom\` command
5534
5633
  pitroom hook-start SessionStart hook: introduces Pitroom to the agent (Codex; Claude Code uses its plugin)
5634
+ pitroom cooldown [--clear] models that said "rate limited" and are skipped for a while (a quota used up);
5635
+ --clear tries them again
5535
5636
  pitroom doctor [--probe] check workers, models, permissions, skills
5536
5637
  pitroom config effective settings, where each comes from, config file path
5537
5638
  pitroom init [--model ID] [--fallback A,B] [--yes] [--force]
@@ -5581,6 +5682,7 @@ var COMMANDS = {
5581
5682
  crew: cmdCrew,
5582
5683
  review: cmdReview,
5583
5684
  audit: cmdAudit,
5685
+ cooldown: cmdCooldown,
5584
5686
  plan: cmdPlan,
5585
5687
  status: cmdStatus,
5586
5688
  wait: cmdWait,