pitroom 0.10.1 → 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);
@@ -874,6 +874,8 @@ function doctor3({ models, hasFallback }) {
874
874
  const type = settings().security?.auth?.selectedType;
875
875
  if (key || vertex) {
876
876
  checks.push({ level: "ok", message: `Gemini CLI: ${key ? "an API key is set" : "Vertex AI is set up"}` });
877
+ } else if (type === "gemini-api-key" || type === "vertex-ai") {
878
+ checks.push({ level: "ok", message: `Gemini CLI: ${type === "vertex-ai" ? "Vertex AI" : "an API key"} is selected (kept by Gemini CLI)` });
877
879
  } else if (signedIn && type === "oauth-personal") {
878
880
  checks.push({
879
881
  level: "warn",
@@ -1424,25 +1426,25 @@ function configPath() {
1424
1426
  var cached;
1425
1427
  function loadConfig() {
1426
1428
  if (cached) return cached;
1427
- const file = configPath();
1429
+ const file2 = configPath();
1428
1430
  const config = {};
1429
1431
  const warnings = [];
1430
- if (fs7.existsSync(file)) {
1432
+ if (fs7.existsSync(file2)) {
1431
1433
  let raw;
1432
1434
  try {
1433
- raw = JSON.parse(fs7.readFileSync(file, "utf8"));
1435
+ raw = JSON.parse(fs7.readFileSync(file2, "utf8"));
1434
1436
  } catch (e) {
1435
- warnings.push(`${file}: invalid JSON (${e.message}); ignored`);
1437
+ warnings.push(`${file2}: invalid JSON (${e.message}); ignored`);
1436
1438
  }
1437
1439
  if (raw && typeof raw === "object" && !Array.isArray(raw)) {
1438
1440
  for (const [key, value] of Object.entries(raw)) {
1439
1441
  const type = SCHEMA[key];
1440
1442
  if (!type) {
1441
- warnings.push(`${file}: unknown key "${key}" ignored`);
1443
+ warnings.push(`${file2}: unknown key "${key}" ignored`);
1442
1444
  } else if (matches(value, type)) {
1443
1445
  config[key] = value;
1444
1446
  } else {
1445
- warnings.push(`${file}: "${key}" must be ${type}; ignored`);
1447
+ warnings.push(`${file2}: "${key}" must be ${type}; ignored`);
1446
1448
  }
1447
1449
  }
1448
1450
  }
@@ -1554,6 +1556,7 @@ var BOOL_FLAGS = {
1554
1556
  "--full": "full",
1555
1557
  "--badge": "badge",
1556
1558
  "--probe": "probe",
1559
+ "--clear": "clear",
1557
1560
  "--copy": "copy",
1558
1561
  "--all": "all",
1559
1562
  "--models": "models",
@@ -1621,8 +1624,8 @@ function exitCodeFor(meta) {
1621
1624
  }
1622
1625
  }
1623
1626
  function readTask(p) {
1624
- const file = flag(p, "task-file");
1625
- if (file) return fs8.readFileSync(file, "utf8");
1627
+ const file2 = flag(p, "task-file");
1628
+ if (file2) return fs8.readFileSync(file2, "utf8");
1626
1629
  const words = p.positional;
1627
1630
  if (words.length === 1 && words[0] === "-") return fs8.readFileSync(0, "utf8");
1628
1631
  return words.join(" ");
@@ -1654,12 +1657,12 @@ function runOptions(p, task) {
1654
1657
  };
1655
1658
  }
1656
1659
  function planStep(p) {
1657
- const file = flag(p, "plan");
1660
+ const file2 = flag(p, "plan");
1658
1661
  const step = flag(p, "step");
1659
- if (!file && !step) return void 0;
1660
- 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");
1661
1664
  if (!/^\d+$/.test(step)) throw new UserError(`--step takes a task number, not "${step}"`);
1662
- return { file, step: Number(step) };
1665
+ return { file: file2, step: Number(step) };
1663
1666
  }
1664
1667
  function effortFlag(p) {
1665
1668
  const v = flag(p, "effort");
@@ -1669,8 +1672,8 @@ function effortFlag(p) {
1669
1672
 
1670
1673
  // src/cli/commands.ts
1671
1674
  import { spawnSync as spawnSync7 } from "node:child_process";
1672
- import fs27 from "node:fs";
1673
- import path23 from "node:path";
1675
+ import fs28 from "node:fs";
1676
+ import path24 from "node:path";
1674
1677
  import { fileURLToPath as fileURLToPath4 } from "node:url";
1675
1678
 
1676
1679
  // src/core/init.ts
@@ -1723,7 +1726,7 @@ var FREE = /-free$/;
1723
1726
  var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
1724
1727
  var isFound = (binary5) => path8.isAbsolute(binary5) && fs9.existsSync(binary5);
1725
1728
  function planInit(opts = {}) {
1726
- const file = configPath();
1729
+ const file2 = configPath();
1727
1730
  const workers = backendIds().map((id) => {
1728
1731
  const b = getBackend(id);
1729
1732
  const binary5 = b.binary();
@@ -1767,7 +1770,7 @@ function planInit(opts = {}) {
1767
1770
  if (Object.keys(tiers).length > 1) config.tiers = tiers;
1768
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";
1769
1772
  else if (!blocked && !Object.keys(config).length) notes.push("nothing to propose: the default model and the single worker need no config");
1770
- 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 };
1771
1774
  }
1772
1775
  function writeInit(plan, force) {
1773
1776
  if (plan.blocked) throw new UserError(plan.blocked);
@@ -2137,8 +2140,8 @@ var ANSWER_MAX2 = 2e5;
2137
2140
  var cached2;
2138
2141
  var historyFile = () => path10.join(home(), "history.db");
2139
2142
  function openDb() {
2140
- const file = historyFile();
2141
- if (cached2?.file === file) return cached2.db;
2143
+ const file2 = historyFile();
2144
+ if (cached2?.file === file2) return cached2.db;
2142
2145
  let db;
2143
2146
  try {
2144
2147
  const emit = process.emitWarning;
@@ -2149,14 +2152,14 @@ function openDb() {
2149
2152
  } finally {
2150
2153
  process.emitWarning = emit;
2151
2154
  }
2152
- fs12.mkdirSync(path10.dirname(file), { recursive: true });
2153
- db = new DatabaseSync(file);
2155
+ fs12.mkdirSync(path10.dirname(file2), { recursive: true });
2156
+ db = new DatabaseSync(file2);
2154
2157
  db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
2155
2158
  migrate(db);
2156
2159
  } catch {
2157
2160
  db = void 0;
2158
2161
  }
2159
- cached2 = { file, db };
2162
+ cached2 = { file: file2, db };
2160
2163
  return db;
2161
2164
  }
2162
2165
  function migrate(db) {
@@ -2488,7 +2491,7 @@ function structure(lines) {
2488
2491
  });
2489
2492
  return { headings, rules, fenced };
2490
2493
  }
2491
- function parsePlan(text, file = "") {
2494
+ function parsePlan(text, file2 = "") {
2492
2495
  const lines = text.replace(/\r\n?/g, "\n").split("\n");
2493
2496
  const { headings, rules, fenced } = structure(lines);
2494
2497
  const after = (line) => Math.min(headings.find((h) => h.line > line)?.line ?? lines.length, rules.find((r) => r > line) ?? lines.length);
@@ -2507,13 +2510,13 @@ function parsePlan(text, file = "") {
2507
2510
  const tier = /^\s*[-*]?\s*\*\*Worker:\*\*\s*`?([\w-]+)`?/m.exec(unfenced)?.[1]?.toLowerCase();
2508
2511
  tasks.push({ step: Number(m[1]), title: m[2].trim(), text: body, ...tier ? { tier } : {} });
2509
2512
  }
2510
- return { file, title: titleHeading?.text ?? "", header, constraints, tasks };
2513
+ return { file: file2, title: titleHeading?.text ?? "", header, constraints, tasks };
2511
2514
  }
2512
- function loadPlan(file) {
2513
- const abs = path11.resolve(file);
2514
- 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}`);
2515
2518
  const plan = parsePlan(fs13.readFileSync(abs, "utf8"), fs13.realpathSync(abs));
2516
- 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)`);
2517
2520
  return plan;
2518
2521
  }
2519
2522
  function planTask(plan, step) {
@@ -2531,7 +2534,7 @@ function brief(plan, task) {
2531
2534
  ].filter(Boolean).join("\n\n")}
2532
2535
  `;
2533
2536
  }
2534
- var planName = (file) => path11.basename(file).replace(/\.md$/i, "");
2537
+ var planName = (file2) => path11.basename(file2).replace(/\.md$/i, "");
2535
2538
 
2536
2539
  // src/core/report.ts
2537
2540
  var ICON2 = {
@@ -2566,7 +2569,7 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
2566
2569
  const status = meta.taskStatus ? ` \xB7 STATUS ${meta.taskStatus}` : "";
2567
2570
  out.push(`plan: ${planName(meta.plan.file)} \xB7 Task ${meta.plan.step}: ${meta.plan.title}${status}`);
2568
2571
  }
2569
- 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)})`);
2570
2573
  if (meta.error) out.push(`error: ${meta.error}`);
2571
2574
  for (const w of meta.warnings) out.push(`warning: ${w}`);
2572
2575
  if (meta.state === "timeout") {
@@ -2787,10 +2790,10 @@ echo "pitroom: needs Node.js 22.13 or newer (set PITROOM_NODE to its path)" >&2
2787
2790
  exit 127
2788
2791
  `;
2789
2792
  }
2790
- function isOurLauncher(file, root) {
2791
- if (linksInto(file, root)) return true;
2793
+ function isOurLauncher(file2, root) {
2794
+ if (linksInto(file2, root)) return true;
2792
2795
  try {
2793
- const s = fs15.readFileSync(file, "utf8");
2796
+ const s = fs15.readFileSync(file2, "utf8");
2794
2797
  return s.includes(LAUNCHER_MARK) && s.includes(root);
2795
2798
  } catch {
2796
2799
  return false;
@@ -2965,17 +2968,84 @@ ${rulings}`,
2965
2968
  ].join("\n");
2966
2969
  }
2967
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
+
2968
3038
  // src/core/review.ts
2969
3039
  import crypto5 from "node:crypto";
2970
- import fs18 from "node:fs";
2971
- import path15 from "node:path";
3040
+ import fs19 from "node:fs";
3041
+ import path16 from "node:path";
2972
3042
 
2973
3043
  // src/vcs/git.ts
2974
3044
  import { spawnSync as spawnSync5 } from "node:child_process";
2975
3045
  import crypto4 from "node:crypto";
2976
- import fs17 from "node:fs";
3046
+ import fs18 from "node:fs";
2977
3047
  import os9 from "node:os";
2978
- import path14 from "node:path";
3048
+ import path15 from "node:path";
2979
3049
  var SAFE = [
2980
3050
  "-c",
2981
3051
  "color.ui=false",
@@ -3007,20 +3077,20 @@ function gitAvailable() {
3007
3077
  }
3008
3078
  function repoRoot(dir) {
3009
3079
  const r = git(dir, ["rev-parse", "--show-toplevel"]);
3010
- return r.code === 0 ? path14.resolve(r.stdout.trim()) : void 0;
3080
+ return r.code === 0 ? path15.resolve(r.stdout.trim()) : void 0;
3011
3081
  }
3012
3082
  function snapshotTree(root, exclude = []) {
3013
- const tmp = path14.join(os9.tmpdir(), `pitroom-index-${process.pid}-${crypto4.randomBytes(4).toString("hex")}`);
3014
- 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());
3015
3085
  const env = { ...process.env, GIT_INDEX_FILE: tmp };
3016
3086
  try {
3017
- if (fs17.existsSync(real)) fs17.copyFileSync(real, tmp);
3087
+ if (fs18.existsSync(real)) fs18.copyFileSync(real, tmp);
3018
3088
  else must(root, ["read-tree", "--empty"], env);
3019
3089
  must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`)], env);
3020
3090
  return must(root, ["write-tree"], env).trim();
3021
3091
  } finally {
3022
- fs17.rmSync(tmp, { force: true });
3023
- fs17.rmSync(`${tmp}.lock`, { force: true });
3092
+ fs18.rmSync(tmp, { force: true });
3093
+ fs18.rmSync(`${tmp}.lock`, { force: true });
3024
3094
  }
3025
3095
  }
3026
3096
  var DIFF = ["diff", "--no-ext-diff", "--no-textconv", "--no-renames"];
@@ -3040,27 +3110,27 @@ function diffTrees(root, a, b) {
3040
3110
  return { changes, stats: { files: changes.length, insertions, deletions }, patch };
3041
3111
  }
3042
3112
  function createIsolatedCopy(root, tree, dest) {
3043
- const objects = path14.join(path14.resolve(root, must(root, ["rev-parse", "--git-common-dir"]).trim()), "objects");
3044
- fs17.mkdirSync(path14.dirname(dest), { recursive: true });
3045
- must(path14.dirname(dest), ["init", "--quiet", dest]);
3046
- 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}
3047
3117
  `);
3048
3118
  must(dest, ["read-tree", tree]);
3049
3119
  must(dest, ["checkout-index", "--all", "--force"]);
3050
3120
  }
3051
3121
  function removeIsolatedCopy(dest, ownedBy) {
3052
- const rel = path14.relative(ownedBy, dest);
3053
- if (!rel || rel.startsWith("..") || path14.isAbsolute(rel)) throw new Error(`refusing to remove ${dest}`);
3054
- 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 });
3055
3125
  }
3056
3126
  function linkIntoWorktree(root, worktree, rels) {
3057
3127
  const linked = [];
3058
3128
  for (const rel of rels) {
3059
- const src = path14.join(root, rel);
3060
- const dst = path14.join(worktree, rel);
3061
- if (!fs17.existsSync(src) || fs17.existsSync(dst)) continue;
3062
- fs17.mkdirSync(path14.dirname(dst), { recursive: true });
3063
- 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);
3064
3134
  linked.push(rel);
3065
3135
  }
3066
3136
  return linked;
@@ -3119,7 +3189,7 @@ function ancestors(m) {
3119
3189
  function briefOf(m) {
3120
3190
  const root = [m, ...ancestors(m)].at(-1);
3121
3191
  const f = runFile(root.id, "brief.md");
3122
- return fs18.existsSync(f) ? fs18.readFileSync(f, "utf8") : root.task;
3192
+ return fs19.existsSync(f) ? fs19.readFileSync(f, "utf8") : root.task;
3123
3193
  }
3124
3194
  function latestReview(ids) {
3125
3195
  for (const id of listRunIds().reverse()) {
@@ -3133,7 +3203,7 @@ function latestReview(ids) {
3133
3203
  }
3134
3204
  function diffOf(m, from = m.baseTree) {
3135
3205
  const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
3136
- 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);
3137
3207
  if (from !== m.baseTree) {
3138
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);
3139
3209
  }
@@ -3147,7 +3217,7 @@ function runReview(id) {
3147
3217
  if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
3148
3218
  const chain = ancestors(m);
3149
3219
  const previous = latestReview(chain.map((a) => a.id));
3150
- 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;
3151
3221
  const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
3152
3222
  const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
3153
3223
  if (!previous) {
@@ -3221,15 +3291,15 @@ function pickReviewer(job) {
3221
3291
  function writePackage(job) {
3222
3292
  const g = gitDir(job.dir);
3223
3293
  if (!g) throw new UserError(`not a git repository: ${job.dir}`);
3224
- const file = path15.join(g, "pitroom", `review-${crypto5.randomBytes(4).toString("hex")}.md`);
3225
- fs18.mkdirSync(path15.dirname(file), { recursive: true });
3226
- fs18.writeFileSync(file, job.package);
3227
- 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;
3228
3298
  }
3229
3299
 
3230
3300
  // src/core/templates.ts
3231
- import fs19 from "node:fs";
3232
- import path16 from "node:path";
3301
+ import fs20 from "node:fs";
3302
+ import path17 from "node:path";
3233
3303
  var FILES = {
3234
3304
  implementer: "pitroom-driven-development/implementer-prompt.md",
3235
3305
  "task-reviewer": "pitroom-driven-development/task-reviewer-prompt.md",
@@ -3237,9 +3307,9 @@ var FILES = {
3237
3307
  "code-reviewer": "pitroom-review/code-reviewer.md"
3238
3308
  };
3239
3309
  function loadTemplate(name, root = packageRoot()) {
3240
- const file = path16.join(root, "skills", FILES[name]);
3241
- if (!fs19.existsSync(file)) throw new UserError(`template missing: ${file} (broken install? run pitroom doctor)`, 3);
3242
- 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*/, "");
3243
3313
  }
3244
3314
  function fill(template, values) {
3245
3315
  const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
@@ -3346,15 +3416,15 @@ function formatModels(t) {
3346
3416
  // src/core/dash.ts
3347
3417
  import { spawn } from "node:child_process";
3348
3418
  import crypto6 from "node:crypto";
3349
- import fs20 from "node:fs";
3419
+ import fs21 from "node:fs";
3350
3420
  import http from "node:http";
3351
- import path17 from "node:path";
3421
+ import path18 from "node:path";
3352
3422
  import { fileURLToPath as fileURLToPath3 } from "node:url";
3353
3423
 
3354
3424
  // src/core/file-diff.ts
3355
3425
  var PREVIEW_LINES = 300;
3356
3426
  function gitPath(value, prefix = true) {
3357
- let path25 = value;
3427
+ let path26 = value;
3358
3428
  if (value.startsWith('"') && value.endsWith('"')) {
3359
3429
  const bytes = [];
3360
3430
  const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
@@ -3362,9 +3432,9 @@ function gitPath(value, prefix = true) {
3362
3432
  if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
3363
3433
  else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
3364
3434
  }
3365
- path25 = new TextDecoder().decode(new Uint8Array(bytes));
3435
+ path26 = new TextDecoder().decode(new Uint8Array(bytes));
3366
3436
  }
3367
- return prefix ? path25.replace(/^[ab]\//, "") : path25;
3437
+ return prefix ? path26.replace(/^[ab]\//, "") : path26;
3368
3438
  }
3369
3439
  function headerPath(header) {
3370
3440
  const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
@@ -3373,7 +3443,7 @@ function headerPath(header) {
3373
3443
  }
3374
3444
  function fileDiffs(patch, changes, sourceTruncated = false) {
3375
3445
  const files = [];
3376
- let file;
3446
+ let file2;
3377
3447
  let oldLine = 0;
3378
3448
  let newLine = 0;
3379
3449
  let oldLeft = 0;
@@ -3381,25 +3451,25 @@ function fileDiffs(patch, changes, sourceTruncated = false) {
3381
3451
  let inHunk = false;
3382
3452
  let row = 0;
3383
3453
  const finishHunk = () => {
3384
- if (file && (oldLeft > 0 || newLeft > 0)) file.incomplete = true;
3454
+ if (file2 && (oldLeft > 0 || newLeft > 0)) file2.incomplete = true;
3385
3455
  oldLeft = newLeft = 0;
3386
3456
  inHunk = false;
3387
3457
  };
3388
3458
  const add = (line) => {
3389
- if (!file) return;
3390
- if (file.lines.length < PREVIEW_LINES) file.lines.push({ ...line, id: String(row) });
3391
- 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++;
3392
3462
  row++;
3393
3463
  };
3394
3464
  for (const line of patch.split("\n")) {
3395
3465
  if (line.startsWith("diff --git ")) {
3396
3466
  finishHunk();
3397
- file = { path: headerPath(line), status: "M", lines: [], additions: 0, deletions: 0, omittedLines: 0, binary: false, incomplete: false };
3398
- 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);
3399
3469
  row = 0;
3400
3470
  continue;
3401
3471
  }
3402
- if (!file) continue;
3472
+ if (!file2) continue;
3403
3473
  const hunk = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line);
3404
3474
  if (hunk) {
3405
3475
  finishHunk();
@@ -3413,11 +3483,11 @@ function fileDiffs(patch, changes, sourceTruncated = false) {
3413
3483
  add({ type: "meta", content: line });
3414
3484
  } else if (inHunk && (oldLeft > 0 || newLeft > 0)) {
3415
3485
  if (line.startsWith("+") && newLeft > 0) {
3416
- file.additions++;
3486
+ file2.additions++;
3417
3487
  newLeft--;
3418
3488
  add({ type: "added", newLine: newLine++, content: line.slice(1) });
3419
3489
  } else if (line.startsWith("-") && oldLeft > 0) {
3420
- file.deletions++;
3490
+ file2.deletions++;
3421
3491
  oldLeft--;
3422
3492
  add({ type: "removed", oldLine: oldLine++, content: line.slice(1) });
3423
3493
  } else if (line.startsWith(" ") && oldLeft > 0 && newLeft > 0) {
@@ -3425,25 +3495,25 @@ function fileDiffs(patch, changes, sourceTruncated = false) {
3425
3495
  newLeft--;
3426
3496
  add({ type: "context", oldLine: oldLine++, newLine: newLine++, content: line.slice(1) });
3427
3497
  } else {
3428
- file.incomplete = true;
3498
+ file2.incomplete = true;
3429
3499
  }
3430
3500
  } else if (!inHunk) {
3431
- if (line.startsWith("+++ ") && line !== "+++ /dev/null") file.path = gitPath(line.slice(4).replace(/\t$/, ""));
3432
- else if (line.startsWith("--- ") && line !== "--- /dev/null") file.path = gitPath(line.slice(4).replace(/\t$/, ""));
3433
- 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;
3434
3504
  else if (/^(new file mode|deleted file mode|old mode|new mode|rename from|rename to|similarity index) /.test(line)) {
3435
- if (line.startsWith("new file mode ")) file.status = "A";
3436
- 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";
3437
3507
  if (line.startsWith("rename to ")) {
3438
- file.path = gitPath(line.slice(10), false);
3439
- file.status = "R";
3508
+ file2.path = gitPath(line.slice(10), false);
3509
+ file2.status = "R";
3440
3510
  }
3441
3511
  add({ type: "meta", content: line });
3442
3512
  }
3443
3513
  }
3444
3514
  }
3445
3515
  finishHunk();
3446
- if (sourceTruncated && file) file.incomplete = true;
3516
+ if (sourceTruncated && file2) file2.incomplete = true;
3447
3517
  for (const f of files) {
3448
3518
  if (f.incomplete || f.binary) f.additions = f.deletions = void 0;
3449
3519
  }
@@ -3612,11 +3682,11 @@ var assetsDir = () => fileURLToPath3(new URL("./ui/", import.meta.url));
3612
3682
  var assetCache = /* @__PURE__ */ new Map();
3613
3683
  function readAsset(route) {
3614
3684
  try {
3615
- const file = path17.join(assetsDir(), path17.basename(route));
3616
- const { mtimeMs } = fs20.statSync(file);
3685
+ const file2 = path18.join(assetsDir(), path18.basename(route));
3686
+ const { mtimeMs } = fs21.statSync(file2);
3617
3687
  const hit = assetCache.get(route);
3618
3688
  if (hit && hit.mtimeMs === mtimeMs) return hit.data;
3619
- const data = fs20.readFileSync(file);
3689
+ const data = fs21.readFileSync(file2);
3620
3690
  assetCache.set(route, { mtimeMs, data });
3621
3691
  return data;
3622
3692
  } catch {
@@ -3696,7 +3766,7 @@ function startDash(opts) {
3696
3766
  });
3697
3767
  });
3698
3768
  }
3699
- var registryFile = () => path17.join(home(), "dash.json");
3769
+ var registryFile = () => path18.join(home(), "dash.json");
3700
3770
  async function ping(port) {
3701
3771
  try {
3702
3772
  const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
@@ -3707,11 +3777,11 @@ async function ping(port) {
3707
3777
  }
3708
3778
  async function runningDash() {
3709
3779
  try {
3710
- const r = JSON.parse(fs20.readFileSync(registryFile(), "utf8"));
3780
+ const r = JSON.parse(fs21.readFileSync(registryFile(), "utf8"));
3711
3781
  if (isAlive(r.pid) && await ping(r.port)) return { ...r, url: `http://127.0.0.1:${r.port}/` };
3712
3782
  } catch {
3713
3783
  }
3714
- fs20.rmSync(registryFile(), { force: true });
3784
+ fs21.rmSync(registryFile(), { force: true });
3715
3785
  return void 0;
3716
3786
  }
3717
3787
  function openBrowser(url) {
@@ -3726,7 +3796,7 @@ async function dashCommand(o) {
3726
3796
  if (o.stop) {
3727
3797
  if (!existing) throw new UserError("pitroom dash is not running");
3728
3798
  process.kill(existing.pid, "SIGTERM");
3729
- fs20.rmSync(registryFile(), { force: true });
3799
+ fs21.rmSync(registryFile(), { force: true });
3730
3800
  o.log(`stopped pitroom dash (${existing.url})`);
3731
3801
  return 0;
3732
3802
  }
@@ -3763,8 +3833,8 @@ async function dashCommand(o) {
3763
3833
  if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
3764
3834
  dash = await startDash({ port: 0, idleMs: o.idleMs });
3765
3835
  }
3766
- fs20.mkdirSync(home(), { recursive: true });
3767
- 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 }));
3768
3838
  const stop = () => void dash.close();
3769
3839
  process.once("SIGINT", stop);
3770
3840
  process.once("SIGTERM", stop);
@@ -3773,7 +3843,7 @@ async function dashCommand(o) {
3773
3843
  if (o.open) openBrowser(dash.url);
3774
3844
  await dash.closed;
3775
3845
  try {
3776
- 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 });
3777
3847
  } catch {
3778
3848
  }
3779
3849
  return 0;
@@ -3781,8 +3851,8 @@ async function dashCommand(o) {
3781
3851
 
3782
3852
  // src/core/run.ts
3783
3853
  import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
3784
- import fs26 from "node:fs";
3785
- import path22 from "node:path";
3854
+ import fs27 from "node:fs";
3855
+ import path23 from "node:path";
3786
3856
 
3787
3857
  // src/core/chain.ts
3788
3858
  function resolveChain(flags = {}) {
@@ -3832,11 +3902,11 @@ function resolveChain(flags = {}) {
3832
3902
 
3833
3903
  // src/core/process.ts
3834
3904
  import { spawn as spawn2 } from "node:child_process";
3835
- import fs22 from "node:fs";
3905
+ import fs23 from "node:fs";
3836
3906
 
3837
3907
  // src/vcs/guard.ts
3838
- import fs21 from "node:fs";
3839
- import path18 from "node:path";
3908
+ import fs22 from "node:fs";
3909
+ import path19 from "node:path";
3840
3910
  var VERSION = 1;
3841
3911
  var SHIM = `#!/bin/sh
3842
3912
  # pitroom git guard v${VERSION}. Blocks git commands that change history, refs, the
@@ -3911,15 +3981,15 @@ esac
3911
3981
  exec "$real" "$@"
3912
3982
  `;
3913
3983
  function shimDir() {
3914
- return path18.join(home(), "shim", `v${VERSION}`);
3984
+ return path19.join(home(), "shim", `v${VERSION}`);
3915
3985
  }
3916
3986
  function findRealGit(skip) {
3917
- for (const dir of (process.env.PATH ?? "").split(path18.delimiter).filter(Boolean)) {
3918
- if (path18.resolve(dir) === skip) continue;
3919
- 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");
3920
3990
  try {
3921
- fs21.accessSync(p, fs21.constants.X_OK);
3922
- if (fs21.statSync(p).isFile()) return p;
3991
+ fs22.accessSync(p, fs22.constants.X_OK);
3992
+ if (fs22.statSync(p).isFile()) return p;
3923
3993
  } catch {
3924
3994
  }
3925
3995
  }
@@ -3932,13 +4002,13 @@ var REF_HOOK = `#!/bin/sh
3932
4002
  echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
3933
4003
  exit 1
3934
4004
  `;
3935
- var hooksDir = () => path18.join(home(), "git-hooks", `v${HOOK_VERSION}`);
3936
- function writeIfChanged(file, content) {
3937
- if (fs21.existsSync(file) && fs21.readFileSync(file, "utf8") === content) return;
3938
- fs21.mkdirSync(path18.dirname(file), { recursive: true });
3939
- const tmp = `${file}.${process.pid}.tmp`;
3940
- fs21.writeFileSync(tmp, content, { mode: 493 });
3941
- 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);
3942
4012
  }
3943
4013
  function withGitConfig(env, entries) {
3944
4014
  const out = { ...env };
@@ -3960,7 +4030,7 @@ function guardEnv(env) {
3960
4030
  PAGER: "cat"
3961
4031
  };
3962
4032
  if (process.platform === "win32") return { ...env, ...quiet };
3963
- writeIfChanged(path18.join(hooksDir(), "reference-transaction"), REF_HOOK);
4033
+ writeIfChanged(path19.join(hooksDir(), "reference-transaction"), REF_HOOK);
3964
4034
  env = withGitConfig(env, [
3965
4035
  ["core.hooksPath", hooksDir()],
3966
4036
  ["url.pitroom-push-blocked://.pushInsteadOf", ""]
@@ -3968,14 +4038,14 @@ function guardEnv(env) {
3968
4038
  const dir = shimDir();
3969
4039
  const real = findRealGit(dir);
3970
4040
  if (!real) return { ...env, ...quiet };
3971
- writeIfChanged(path18.join(dir, "git"), SHIM);
3972
- 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 ?? ""}` };
3973
4043
  }
3974
4044
 
3975
4045
  // src/core/process.ts
3976
4046
  async function spawnWorker(inv, opts) {
3977
- const out = fs22.openSync(opts.stdoutFile, "w");
3978
- const err = fs22.openSync(opts.stderrFile, "w");
4047
+ const out = fs23.openSync(opts.stdoutFile, "w");
4048
+ const err = fs23.openSync(opts.stderrFile, "w");
3979
4049
  const res = { code: null, timedOut: false, stopped: false };
3980
4050
  const child = spawn2(inv.command, inv.args, {
3981
4051
  cwd: opts.cwd,
@@ -4007,8 +4077,8 @@ async function spawnWorker(inv, opts) {
4007
4077
  if (killer) clearTimeout(killer);
4008
4078
  process.off("SIGINT", stop);
4009
4079
  process.off("SIGTERM", stop);
4010
- fs22.closeSync(out);
4011
- fs22.closeSync(err);
4080
+ fs23.closeSync(out);
4081
+ fs23.closeSync(err);
4012
4082
  return res;
4013
4083
  }
4014
4084
 
@@ -4041,10 +4111,10 @@ function parseAudit(text) {
4041
4111
 
4042
4112
  // src/core/slots.ts
4043
4113
  import crypto7 from "node:crypto";
4044
- import fs23 from "node:fs";
4045
- import path19 from "node:path";
4046
- var slotsDir = () => path19.join(home(), "slots");
4047
- 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");
4048
4118
  var STARTUP_GRACE_MS = 3e4;
4049
4119
  function holderActive(runId, self) {
4050
4120
  if (!runId || runId === self) return false;
@@ -4057,51 +4127,51 @@ function holderActive(runId, self) {
4057
4127
  return false;
4058
4128
  }
4059
4129
  }
4060
- function tryClaim(file, runId) {
4061
- fs23.mkdirSync(path19.dirname(file), { recursive: true });
4130
+ function tryClaim(file2, runId) {
4131
+ fs24.mkdirSync(path20.dirname(file2), { recursive: true });
4062
4132
  try {
4063
- fs23.writeFileSync(file, runId, { flag: "wx" });
4133
+ fs24.writeFileSync(file2, runId, { flag: "wx" });
4064
4134
  return true;
4065
4135
  } catch (e) {
4066
4136
  if (e.code !== "EEXIST") throw e;
4067
4137
  }
4068
4138
  let owner = "";
4069
4139
  try {
4070
- owner = fs23.readFileSync(file, "utf8").trim();
4140
+ owner = fs24.readFileSync(file2, "utf8").trim();
4071
4141
  } catch {
4072
4142
  }
4073
4143
  if (owner === runId) return true;
4074
4144
  if (holderActive(owner, runId)) return false;
4075
- fs23.rmSync(file, { force: true });
4145
+ fs24.rmSync(file2, { force: true });
4076
4146
  try {
4077
- fs23.writeFileSync(file, runId, { flag: "wx" });
4147
+ fs24.writeFileSync(file2, runId, { flag: "wx" });
4078
4148
  return true;
4079
4149
  } catch {
4080
4150
  return false;
4081
4151
  }
4082
4152
  }
4083
- function releaseIfOwner(file, runId) {
4084
- if (!file) return;
4153
+ function releaseIfOwner(file2, runId) {
4154
+ if (!file2) return;
4085
4155
  try {
4086
- 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 });
4087
4157
  } catch {
4088
4158
  }
4089
4159
  }
4090
4160
  function tryAcquireSlot(runId, maxParallel) {
4091
4161
  for (let n = 0; n < Math.max(1, maxParallel); n++) {
4092
- const file = path19.join(slotsDir(), `slot-${n}`);
4093
- if (tryClaim(file, runId)) return file;
4162
+ const file2 = path20.join(slotsDir(), `slot-${n}`);
4163
+ if (tryClaim(file2, runId)) return file2;
4094
4164
  }
4095
4165
  return void 0;
4096
4166
  }
4097
- var releaseSlot = (file, runId) => releaseIfOwner(file, runId);
4098
- 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)}`);
4099
4169
  function acquireWriteLock(repoRoot2, runId) {
4100
- const file = lockFile(repoRoot2);
4101
- if (tryClaim(file, runId)) return;
4170
+ const file2 = lockFile(repoRoot2);
4171
+ if (tryClaim(file2, runId)) return;
4102
4172
  let owner = "";
4103
4173
  try {
4104
- owner = fs23.readFileSync(file, "utf8").trim();
4174
+ owner = fs24.readFileSync(file2, "utf8").trim();
4105
4175
  } catch {
4106
4176
  }
4107
4177
  throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
@@ -4148,8 +4218,8 @@ ${task}`;
4148
4218
  }
4149
4219
 
4150
4220
  // src/core/refs.ts
4151
- import fs24 from "node:fs";
4152
- import path20 from "node:path";
4221
+ import fs25 from "node:fs";
4222
+ import path21 from "node:path";
4153
4223
  var FILE_EXTENSIONS = new Set(
4154
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(" ")
4155
4225
  );
@@ -4200,29 +4270,29 @@ function extractRefs(answer) {
4200
4270
  return [...seen.values()];
4201
4271
  }
4202
4272
  function verifyRefs(refs, dirs) {
4203
- 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)))];
4204
4274
  const invalid = [];
4205
4275
  const lineCache = /* @__PURE__ */ new Map();
4206
- const load = (file) => {
4207
- if (!lineCache.has(file)) {
4208
- const size = fs24.statSync(file).size;
4209
- 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");
4210
4280
  if (lines && lines[lines.length - 1] === "") lines.pop();
4211
- lineCache.set(file, lines);
4281
+ lineCache.set(file2, lines);
4212
4282
  }
4213
- return lineCache.get(file);
4283
+ return lineCache.get(file2);
4214
4284
  };
4215
4285
  let index;
4216
4286
  const byName = () => index ??= indexFiles(roots);
4217
4287
  const candidates = (ref) => {
4218
4288
  const direct = resolve(ref.file, roots);
4219
4289
  if (direct) return [direct];
4220
- if (path20.isAbsolute(ref.file)) return [];
4290
+ if (path21.isAbsolute(ref.file)) return [];
4221
4291
  const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
4222
- 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);
4223
4293
  };
4224
- const check = (file, ref) => {
4225
- const lines = load(file);
4294
+ const check = (file2, ref) => {
4295
+ const lines = load(file2);
4226
4296
  if (!lines) return void 0;
4227
4297
  if (ref.end > lines.length) return `file has ${lines.length} lines`;
4228
4298
  if (ref.symbol && !mentions(lines, ref) && !enclosedBy(lines, ref)) return `\`${ref.symbol}\` not near line ${ref.start}`;
@@ -4234,7 +4304,7 @@ function verifyRefs(refs, dirs) {
4234
4304
  if (!files.length) {
4235
4305
  if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
4236
4306
  total++;
4237
- 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" });
4238
4308
  continue;
4239
4309
  }
4240
4310
  total++;
@@ -4255,7 +4325,7 @@ function enclosedBy(lines, ref) {
4255
4325
  return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
4256
4326
  }
4257
4327
  var isFileName = (name) => {
4258
- const base2 = path20.basename(name);
4328
+ const base2 = path21.basename(name);
4259
4329
  return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
4260
4330
  };
4261
4331
  function indexFiles(roots) {
@@ -4264,19 +4334,19 @@ function indexFiles(roots) {
4264
4334
  const walk = (dir) => {
4265
4335
  let entries;
4266
4336
  try {
4267
- entries = fs24.readdirSync(dir, { withFileTypes: true });
4337
+ entries = fs25.readdirSync(dir, { withFileTypes: true });
4268
4338
  } catch {
4269
4339
  return;
4270
4340
  }
4271
4341
  for (const e of entries) {
4272
4342
  if (count >= MAX_INDEXED) return;
4273
4343
  if (e.isDirectory()) {
4274
- 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));
4275
4345
  } else if (e.isFile()) {
4276
4346
  count++;
4277
4347
  const list2 = index.get(e.name);
4278
- if (list2) list2.push(path20.join(dir, e.name));
4279
- 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)]);
4280
4350
  }
4281
4351
  }
4282
4352
  };
@@ -4284,27 +4354,27 @@ function indexFiles(roots) {
4284
4354
  return index;
4285
4355
  }
4286
4356
  function resolve(ref, roots) {
4287
- 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));
4288
4358
  return candidates.find((c) => {
4289
4359
  try {
4290
- return fs24.statSync(c).isFile() && inside(c, roots);
4360
+ return fs25.statSync(c).isFile() && inside(c, roots);
4291
4361
  } catch {
4292
4362
  return false;
4293
4363
  }
4294
4364
  });
4295
4365
  }
4296
- function inside(file, roots) {
4366
+ function inside(file2, roots) {
4297
4367
  return roots.some((r) => {
4298
- const rel = path20.relative(r, path20.resolve(file));
4299
- return rel !== "" && !rel.startsWith("..") && !path20.isAbsolute(rel);
4368
+ const rel = path21.relative(r, path21.resolve(file2));
4369
+ return rel !== "" && !rel.startsWith("..") && !path21.isAbsolute(rel);
4300
4370
  });
4301
4371
  }
4302
4372
  var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4303
4373
 
4304
4374
  // src/core/secrets.ts
4305
4375
  import { execFileSync } from "node:child_process";
4306
- import fs25 from "node:fs";
4307
- import path21 from "node:path";
4376
+ import fs26 from "node:fs";
4377
+ import path22 from "node:path";
4308
4378
  var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
4309
4379
  var MAX_VISITED = 2e4;
4310
4380
  var MAX_DEPTH = 4;
@@ -4320,16 +4390,16 @@ function findSecretFiles(dir) {
4320
4390
  if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
4321
4391
  let entries;
4322
4392
  try {
4323
- entries = fs25.readdirSync(d, { withFileTypes: true });
4393
+ entries = fs26.readdirSync(d, { withFileTypes: true });
4324
4394
  } catch {
4325
4395
  return;
4326
4396
  }
4327
4397
  for (const e of entries) {
4328
4398
  visited++;
4329
4399
  if (e.isDirectory()) {
4330
- 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);
4331
4401
  } else if (e.isFile() && looksSecret(e.name)) {
4332
- found.push(path21.relative(dir, path21.join(d, e.name)));
4402
+ found.push(path22.relative(dir, path22.join(d, e.name)));
4333
4403
  }
4334
4404
  }
4335
4405
  };
@@ -4339,7 +4409,7 @@ function findSecretFiles(dir) {
4339
4409
  function findSecretFilesInTree(root, dir) {
4340
4410
  try {
4341
4411
  const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
4342
- 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();
4343
4413
  } catch {
4344
4414
  return [];
4345
4415
  }
@@ -4353,7 +4423,7 @@ function secretWarning(files, mode) {
4353
4423
  }
4354
4424
 
4355
4425
  // src/core/run.ts
4356
- var VERSION2 = true ? "0.10.1" : "0.0.0-dev";
4426
+ var VERSION2 = true ? "0.12.0" : "0.0.0-dev";
4357
4427
  function planWork(o) {
4358
4428
  if (!o.plan) return { task: o.task.trim(), tier: o.tier };
4359
4429
  if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
@@ -4385,7 +4455,7 @@ function prepareRun(o) {
4385
4455
  parent = readMeta(o.continueFrom);
4386
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");
4387
4457
  if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
4388
- if (parent.mode === "isolate" && (!parent.worktree || !fs26.existsSync(parent.worktree))) {
4458
+ if (parent.mode === "isolate" && (!parent.worktree || !fs27.existsSync(parent.worktree))) {
4389
4459
  throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
4390
4460
  }
4391
4461
  const ran = parent.ran ?? parent.worker;
@@ -4404,15 +4474,15 @@ function prepareRun(o) {
4404
4474
  throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
4405
4475
  }
4406
4476
  const mode = parent?.mode ?? o.mode;
4407
- const dir = path22.resolve(parent?.dir ?? o.dir);
4408
- 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}`);
4409
4479
  const root = repoRoot(dir);
4410
4480
  if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
4411
4481
  if (mode === "write" && !root && !o.allowNonGit) {
4412
4482
  throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
4413
4483
  }
4414
- const files = o.files.map((f) => path22.resolve(f));
4415
- 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}`);
4416
4486
  if (!parent && !process.env.PITROOM_NO_SECRET_WARNING) {
4417
4487
  const secrets = mode === "isolate" && root ? findSecretFilesInTree(root, dir) : findSecretFiles(dir);
4418
4488
  const note = secretWarning(secrets, mode);
@@ -4454,13 +4524,13 @@ function prepareRun(o) {
4454
4524
  try {
4455
4525
  acquireWriteLock(root, meta.id);
4456
4526
  } catch (e) {
4457
- fs26.rmSync(runDir(meta.id), { recursive: true, force: true });
4527
+ fs27.rmSync(runDir(meta.id), { recursive: true, force: true });
4458
4528
  throw e;
4459
4529
  }
4460
4530
  }
4461
- fs26.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
4531
+ fs27.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
4462
4532
  `);
4463
- 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);
4464
4534
  return meta;
4465
4535
  }
4466
4536
  var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -4517,12 +4587,19 @@ async function execute(meta) {
4517
4587
  for (let i = 0; i < chain.length; i++) {
4518
4588
  const target = chain[i];
4519
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
+ }
4520
4596
  meta.ran = target;
4521
4597
  writeMeta(meta);
4522
4598
  result = await attempt(meta, backend, target);
4523
4599
  if (result.timedOut || result.stopped || result.spawnError) break;
4524
4600
  const why = retryableFailure(meta, backend, result);
4525
4601
  if (!why) break;
4602
+ if (why.kind === "rate-limited") recordCooldown(cooldownKey(target, backend), describeTarget(target), why.message);
4526
4603
  if (!target.model && backend.defaultModel) {
4527
4604
  const failed = backend.defaultModel();
4528
4605
  for (let j = chain.length - 1; j > i; j--) {
@@ -4533,7 +4610,7 @@ async function execute(meta) {
4533
4610
  (meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
4534
4611
  for (const f of ["events.jsonl", "stderr.log"]) {
4535
4612
  const from = runFile(meta.id, f);
4536
- 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}.`)));
4537
4614
  }
4538
4615
  writeMeta(meta);
4539
4616
  }
@@ -4552,13 +4629,13 @@ async function execute(meta) {
4552
4629
  }
4553
4630
  function autoAudit(meta) {
4554
4631
  try {
4555
- 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;
4556
4633
  startAudit(meta);
4557
4634
  } catch {
4558
4635
  }
4559
4636
  }
4560
4637
  function startAudit(meta, worker) {
4561
- const answer = read(runFile(meta.id, "summary.md"));
4638
+ const answer = read2(runFile(meta.id, "summary.md"));
4562
4639
  const auditor = worker ?? pickAuditor(meta);
4563
4640
  if (!auditor) return void 0;
4564
4641
  const a = prepareRun({
@@ -4594,9 +4671,9 @@ function prepareTree(meta) {
4594
4671
  if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
4595
4672
  if (root && meta.mode === "isolate" && !meta.worktree) {
4596
4673
  meta.baseTree = snapshotTree(root);
4597
- meta.worktree = path22.join(worktreesDir(), meta.id);
4674
+ meta.worktree = path23.join(worktreesDir(), meta.id);
4598
4675
  createIsolatedCopy(root, meta.baseTree, meta.worktree);
4599
- meta.cwd = path22.join(meta.worktree, path22.relative(root, meta.dir));
4676
+ meta.cwd = path23.join(meta.worktree, path23.relative(root, meta.dir));
4600
4677
  const linked = linkIntoWorktree(root, meta.worktree, meta.link);
4601
4678
  if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
4602
4679
  }
@@ -4619,11 +4696,11 @@ function attempt(meta, backend, target) {
4619
4696
  timeoutSec: meta.timeoutSec
4620
4697
  });
4621
4698
  }
4622
- var read = (f) => fs26.existsSync(f) ? fs26.readFileSync(f, "utf8") : "";
4699
+ var read2 = (f) => fs27.existsSync(f) ? fs27.readFileSync(f, "utf8") : "";
4623
4700
  function retryableFailure(meta, backend, res) {
4624
- const run = backend.parse(read(runFile(meta.id, "events.jsonl")));
4701
+ const run = backend.parse(read2(runFile(meta.id, "events.jsonl")));
4625
4702
  if (run.usage.steps > 0) return void 0;
4626
- 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);
4627
4704
  return f && f.kind !== "other" ? f : void 0;
4628
4705
  }
4629
4706
  var HINTS = {
@@ -4635,18 +4712,18 @@ var HINTS = {
4635
4712
  function finalize(meta, res) {
4636
4713
  const ran = meta.ran ?? meta.worker;
4637
4714
  const backend = getBackend(ran.backend);
4638
- const run = backend.parse(read(runFile(meta.id, "events.jsonl")));
4715
+ const run = backend.parse(read2(runFile(meta.id, "events.jsonl")));
4639
4716
  meta.sessionId = run.sessionId ?? meta.sessionId;
4640
4717
  meta.usage = run.usage;
4641
- fs26.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
4718
+ fs27.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
4642
4719
  `);
4643
4720
  if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run.finalText);
4644
4721
  if (meta.reviewOf) {
4645
4722
  meta.verdict = parseVerdict(run.finalText);
4646
- if (meta.packageFile) fs26.rmSync(meta.packageFile, { force: true });
4723
+ if (meta.packageFile) fs27.rmSync(meta.packageFile, { force: true });
4647
4724
  }
4648
4725
  if (!res.timedOut && !res.stopped && !meta.error) {
4649
- 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);
4650
4727
  if (f) meta.error = HINTS[f.kind] ? `${f.message} (${HINTS[f.kind]})` : f.message;
4651
4728
  }
4652
4729
  try {
@@ -4691,7 +4768,7 @@ function captureChanges(meta) {
4691
4768
  const d = diffTrees(target, meta.baseTree, meta.afterTree);
4692
4769
  meta.changes = d.changes;
4693
4770
  meta.stats = d.stats;
4694
- fs26.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
4771
+ fs27.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
4695
4772
  }
4696
4773
  function runVerify(meta) {
4697
4774
  const r = spawnSync6(meta.verify, {
@@ -4704,7 +4781,7 @@ function runVerify(meta) {
4704
4781
  env: { ...process.env, PWD: meta.cwd, PITROOM_ACTIVE: "1" }
4705
4782
  });
4706
4783
  const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
4707
- fs26.writeFileSync(runFile(meta.id, "verify.log"), output);
4784
+ fs27.writeFileSync(runFile(meta.id, "verify.log"), output);
4708
4785
  return { ok: r.status === 0, code: r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
4709
4786
  }
4710
4787
  function applyRun(meta, allowDelete = false) {
@@ -4749,7 +4826,7 @@ function discardRun(meta) {
4749
4826
  return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
4750
4827
  }
4751
4828
  function cleanupWorktree(meta) {
4752
- if (meta.worktree && fs26.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
4829
+ if (meta.worktree && fs27.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
4753
4830
  }
4754
4831
 
4755
4832
  // src/cli/commands.ts
@@ -4799,10 +4876,10 @@ async function cmdReview(p) {
4799
4876
  writeMeta(meta);
4800
4877
  }
4801
4878
  } catch (e) {
4802
- fs27.rmSync(packageFile, { force: true });
4879
+ fs28.rmSync(packageFile, { force: true });
4803
4880
  throw e;
4804
4881
  }
4805
- fs27.writeFileSync(runFile(meta.id, "package.md"), job.package);
4882
+ fs28.writeFileSync(runFile(meta.id, "package.md"), job.package);
4806
4883
  return launch(p, meta);
4807
4884
  }
4808
4885
  async function cmdAudit(p) {
@@ -4832,24 +4909,45 @@ async function cmdAudit(p) {
4832
4909
  writeMeta(m);
4833
4910
  return launch(p, a);
4834
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
+ }
4835
4933
  function cmdPlan(p) {
4836
- const [sub, file, ...rest] = p.positional;
4837
- if (sub === "status" && file) {
4838
- const s = planStatus(file);
4934
+ const [sub, file2, ...rest] = p.positional;
4935
+ if (sub === "status" && file2) {
4936
+ const s = planStatus(file2);
4839
4937
  console.log(
4840
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)
4841
4939
  );
4842
4940
  return 0;
4843
4941
  }
4844
- if (sub === "note" && file && rest.length) {
4845
- console.log(`noted in ${addNote(file, rest.join(" "))}`);
4942
+ if (sub === "note" && file2 && rest.length) {
4943
+ console.log(`noted in ${addNote(file2, rest.join(" "))}`);
4846
4944
  return 0;
4847
4945
  }
4848
4946
  throw new UserError('usage: pitroom plan status PLAN.md [--json] | pitroom plan note PLAN.md "Task N: \u2026"');
4849
4947
  }
4850
4948
  function cmdCrew(p) {
4851
- const file = flag(p, "task-file");
4852
- 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);
4853
4951
  if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
4854
4952
  if (has(p, "write") && tasks.length > 1) {
4855
4953
  throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
@@ -5050,7 +5148,7 @@ function sinceMs(s) {
5050
5148
  if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
5051
5149
  return Date.now() - Number(m[1]) * 864e5;
5052
5150
  }
5053
- var readStdin = () => process.stdin.isTTY ? "" : fs27.readFileSync(0, "utf8");
5151
+ var readStdin = () => process.stdin.isTTY ? "" : fs28.readFileSync(0, "utf8");
5054
5152
  function cmdStatusline(p) {
5055
5153
  const input = readStdin();
5056
5154
  const lines = [];
@@ -5090,8 +5188,8 @@ function cmdModels(p) {
5090
5188
  return 0;
5091
5189
  }
5092
5190
  function cmdHookStart() {
5093
- const script = path23.join(path23.dirname(path23.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
5094
- 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 });
5095
5193
  return 0;
5096
5194
  }
5097
5195
  function cmdInit(p) {
@@ -5131,8 +5229,8 @@ function cmdSavings(p) {
5131
5229
  est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
5132
5230
  const out = flag(p, "card");
5133
5231
  if (out) {
5134
- fs27.writeFileSync(out, card(t, period));
5135
- console.log(`card written to ${path23.resolve(out)}`);
5232
+ fs28.writeFileSync(out, card(t, period));
5233
+ console.log(`card written to ${path24.resolve(out)}`);
5136
5234
  }
5137
5235
  if (has(p, "badge")) console.log(`![pitroom](${badgeUrl(t)})`);
5138
5236
  return 0;
@@ -5152,7 +5250,7 @@ function cmdConfig(p) {
5152
5250
  console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
5153
5251
  return 0;
5154
5252
  }
5155
- console.log(`config file: ${configPath()}${fs27.existsSync(configPath()) ? "" : " (not present)"}`);
5253
+ console.log(`config file: ${configPath()}${fs28.existsSync(configPath()) ? "" : " (not present)"}`);
5156
5254
  for (const [key, s] of Object.entries(eff)) {
5157
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);
5158
5256
  console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
@@ -5176,7 +5274,7 @@ function cmdClean(p) {
5176
5274
  const m = readMeta(id);
5177
5275
  if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
5178
5276
  recordRun(m);
5179
- fs27.rmSync(runDir(id), { recursive: true, force: true });
5277
+ fs28.rmSync(runDir(id), { recursive: true, force: true });
5180
5278
  }
5181
5279
  console.log(`removed ${old.length} run(s); the history and the savings ledger are kept`);
5182
5280
  return 0;
@@ -5186,9 +5284,9 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
5186
5284
 
5187
5285
  // src/core/doctor.ts
5188
5286
  import { spawnSync as spawnSync8 } from "node:child_process";
5189
- import fs28 from "node:fs";
5287
+ import fs29 from "node:fs";
5190
5288
  import os10 from "node:os";
5191
- import path24 from "node:path";
5289
+ import path25 from "node:path";
5192
5290
  var MARK = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
5193
5291
  var NEXT = [
5194
5292
  { when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
@@ -5198,15 +5296,15 @@ var NEXT = [
5198
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" }
5199
5297
  ];
5200
5298
  function firstNodeOnPath() {
5201
- for (const dir of (process.env.PATH ?? "").split(path24.delimiter).filter(Boolean)) {
5202
- 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");
5203
5301
  try {
5204
- if (!fs28.statSync(file).isFile()) continue;
5302
+ if (!fs29.statSync(file2).isFile()) continue;
5205
5303
  } catch {
5206
5304
  continue;
5207
5305
  }
5208
- const r = spawnSync8(file, ["-p", "process.versions.node"], { encoding: "utf8", timeout: 5e3 });
5209
- 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: "" };
5210
5308
  }
5211
5309
  return void 0;
5212
5310
  }
@@ -5228,12 +5326,12 @@ function doctor5(probe) {
5228
5326
  const addAll = (list2) => list2.forEach((c) => add(c.level, c.message));
5229
5327
  add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
5230
5328
  const onPath = firstNodeOnPath();
5231
- 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)) {
5232
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)`);
5233
5331
  }
5234
5332
  add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
5235
5333
  const cfg = loadConfig();
5236
- 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)"}`);
5237
5335
  for (const w of cfg.warnings) add("warn", w);
5238
5336
  if (process.platform !== "win32") {
5239
5337
  const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
@@ -5275,6 +5373,9 @@ function doctor5(probe) {
5275
5373
  add("ok", `cost: ${key} = ${mine}${cheaper ? `; you priced ${cheaper[0]} cheaper (${cheaper[1]}): is the dearer one needed?` : ""}`);
5276
5374
  }
5277
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
+ }
5278
5379
  const byBackend = /* @__PURE__ */ new Map();
5279
5380
  for (const t of [...chain, ...tierTargets]) byBackend.set(t.backend, [...byBackend.get(t.backend) ?? [], t.model]);
5280
5381
  for (const [id, models] of byBackend) {
@@ -5289,6 +5390,19 @@ function doctor5(probe) {
5289
5390
  addAll(backend.doctor({ models: [...new Set(models)], hasFallback: chain.length > 1 }));
5290
5391
  add("ok", `${backend.name}: read-only runs enforced by ${READ_ONLY_HOW[backend.capabilities.readOnly]}`);
5291
5392
  }
5393
+ const unused = allBackends().filter((b) => !byBackend.has(b.id));
5394
+ for (const backend of unused) {
5395
+ let found;
5396
+ try {
5397
+ found = backend.doctor({ models: [], hasFallback: true });
5398
+ } catch {
5399
+ continue;
5400
+ }
5401
+ if (!found.length || found[0].level === "fail") continue;
5402
+ section2(`${backend.name} (installed, not in your config)`);
5403
+ for (const c of found) add("ok", c.message);
5404
+ add("ok", `use it with -W ${backend.id}[:model], or name it in "fallback" or "tiers" in the config`);
5405
+ }
5292
5406
  section2("Skills and agents");
5293
5407
  addAll(skillChecks());
5294
5408
  if (probe && chain[0]) {
@@ -5364,14 +5478,20 @@ function skillChecks() {
5364
5478
  else if (names.length) checks.push({ level: "warn", message: `skills in ${base2}: only ${names.join(", ")} of ${all.length}; run \`pitroom install\`` });
5365
5479
  else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
5366
5480
  }
5481
+ if (geminiExtensionInstalled()) {
5482
+ const linked = installedSkills().some((i) => i.base.includes(`${path25.sep}.agents${path25.sep}`) && i.names.length);
5483
+ checks.push(
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)" }
5485
+ );
5486
+ }
5367
5487
  if (viaPlugin) {
5368
- 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);
5369
5489
  checks.push(
5370
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)" }
5371
5491
  );
5372
5492
  }
5373
5493
  const launcher = launcherPath();
5374
- if (fs28.existsSync(launcher)) {
5494
+ if (fs29.existsSync(launcher)) {
5375
5495
  const r = spawnSync8(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
5376
5496
  checks.push(
5377
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)}` }
@@ -5381,10 +5501,14 @@ function skillChecks() {
5381
5501
  }
5382
5502
  return checks;
5383
5503
  }
5504
+ function geminiExtensionInstalled() {
5505
+ const home2 = process.env.GEMINI_CLI_HOME ?? os10.homedir();
5506
+ return fs29.existsSync(path25.join(home2, ".gemini", "extensions", "pitroom"));
5507
+ }
5384
5508
  function pluginInstalled() {
5385
5509
  try {
5386
- const f = path24.join(os10.homedir(), ".claude", "plugins", "installed_plugins.json");
5387
- 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 ?? {};
5388
5512
  return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
5389
5513
  } catch {
5390
5514
  return false;
@@ -5401,24 +5525,24 @@ function superpowersActive() {
5401
5525
  for (const p of openCodePlugins()) {
5402
5526
  if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
5403
5527
  }
5404
- for (const base2 of [path24.join(os10.homedir(), ".agents", "skills"), path24.join(os10.homedir(), ".claude", "skills")]) {
5405
- const dir = path24.join(base2, "using-superpowers");
5406
- 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);
5407
5531
  }
5408
5532
  return found;
5409
5533
  }
5410
5534
  function claudePlugins() {
5411
5535
  try {
5412
- const f = path24.join(os10.homedir(), ".claude", "plugins", "installed_plugins.json");
5413
- 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 ?? {});
5414
5538
  } catch {
5415
5539
  return [];
5416
5540
  }
5417
5541
  }
5418
5542
  function claudePluginEnabled(key) {
5419
5543
  try {
5420
- const f = path24.join(os10.homedir(), ".claude", "settings.json");
5421
- 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;
5422
5546
  } catch {
5423
5547
  return true;
5424
5548
  }
@@ -5427,7 +5551,7 @@ function codexPlugins() {
5427
5551
  const plugins = /* @__PURE__ */ new Map();
5428
5552
  let text;
5429
5553
  try {
5430
- 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");
5431
5555
  } catch {
5432
5556
  return plugins;
5433
5557
  }
@@ -5445,11 +5569,11 @@ function codexPlugins() {
5445
5569
  return plugins;
5446
5570
  }
5447
5571
  function openCodePlugins() {
5448
- 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");
5449
5573
  const plugins = [];
5450
5574
  for (const name of ["opencode.json", "opencode.jsonc"]) {
5451
5575
  try {
5452
- 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, "");
5453
5577
  const list2 = JSON.parse(text).plugin;
5454
5578
  if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
5455
5579
  } catch {
@@ -5457,7 +5581,7 @@ function openCodePlugins() {
5457
5581
  }
5458
5582
  for (const folder of ["plugin", "plugins"]) {
5459
5583
  try {
5460
- 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));
5461
5585
  } catch {
5462
5586
  }
5463
5587
  }
@@ -5507,6 +5631,8 @@ Usage
5507
5631
  pitroom statusline [--then CMD] status-bar line: running workers, savings this week (after CMD's)
5508
5632
  pitroom hook-card PostToolUse hook: a card after each Bash \`pitroom\` command
5509
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
5510
5636
  pitroom doctor [--probe] check workers, models, permissions, skills
5511
5637
  pitroom config effective settings, where each comes from, config file path
5512
5638
  pitroom init [--model ID] [--fallback A,B] [--yes] [--force]
@@ -5556,6 +5682,7 @@ var COMMANDS = {
5556
5682
  crew: cmdCrew,
5557
5683
  review: cmdReview,
5558
5684
  audit: cmdAudit,
5685
+ cooldown: cmdCooldown,
5559
5686
  plan: cmdPlan,
5560
5687
  status: cmdStatus,
5561
5688
  wait: cmdWait,