pitroom 0.16.0 → 0.17.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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import fs33 from "node:fs";
4
+ import fs35 from "node:fs";
5
5
 
6
6
  // src/core/errors.ts
7
7
  var UserError = class extends Error {
@@ -1420,7 +1420,8 @@ var SCHEMA = {
1420
1420
  tiers: "record",
1421
1421
  costs: "numbers",
1422
1422
  audit: "number",
1423
- readIn: "string"
1423
+ readIn: "string",
1424
+ cacheDays: "number"
1424
1425
  };
1425
1426
  function configPath() {
1426
1427
  if (process.env.PITROOM_CONFIG) return path7.resolve(process.env.PITROOM_CONFIG);
@@ -1478,6 +1479,10 @@ var rate = (v) => {
1478
1479
  const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
1479
1480
  return typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.min(n, 1) : void 0;
1480
1481
  };
1482
+ var days = (v) => {
1483
+ const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
1484
+ return typeof n === "number" && Number.isInteger(n) && n >= 0 ? n : void 0;
1485
+ };
1481
1486
  var readIn = (v) => v === "auto" || v === "snapshot" || v === "project" ? v : void 0;
1482
1487
  var list = (s) => s?.split(",").map((x) => x.trim()).filter(Boolean);
1483
1488
  function setting(flag2, env, conf, fallback) {
@@ -1504,7 +1509,8 @@ function effective(flags = {}) {
1504
1509
  tiers: setting(void 0, void 0, c.tiers, {}),
1505
1510
  costs: setting(void 0, void 0, c.costs, {}),
1506
1511
  audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0),
1507
- readIn: setting(void 0, readIn(e.PITROOM_READ_IN), readIn(c.readIn), "auto")
1512
+ readIn: setting(void 0, readIn(e.PITROOM_READ_IN), readIn(c.readIn), "auto"),
1513
+ cacheDays: setting(void 0, days(e.PITROOM_CACHE_DAYS), days(c.cacheDays), 7)
1508
1514
  };
1509
1515
  }
1510
1516
 
@@ -1581,6 +1587,7 @@ var BOOL_FLAGS = {
1581
1587
  "--stop": "stop",
1582
1588
  "--open": "open",
1583
1589
  "--serve": "serve",
1590
+ "--fresh": "fresh",
1584
1591
  "--http": "http",
1585
1592
  "-h": "help",
1586
1593
  "--help": "help",
@@ -1730,16 +1737,16 @@ function repoRoot(dir) {
1730
1737
  const r = git(dir, ["rev-parse", "--show-toplevel"]);
1731
1738
  return r.code === 0 ? path8.resolve(r.stdout.trim()) : void 0;
1732
1739
  }
1733
- function snapshotTree(root, exclude = [], drop = []) {
1740
+ function snapshotTree(root, exclude = [], drop2 = []) {
1734
1741
  const tmp = path8.join(os8.tmpdir(), `pitroom-index-${process.pid}-${crypto2.randomBytes(4).toString("hex")}`);
1735
1742
  const real = path8.resolve(root, must(root, ["rev-parse", "--git-path", "index"]).trim());
1736
1743
  const env = { ...process.env, GIT_INDEX_FILE: tmp };
1737
1744
  try {
1738
1745
  if (fs9.existsSync(real)) fs9.copyFileSync(real, tmp);
1739
1746
  else must(root, ["read-tree", "--empty"], env);
1740
- must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`), ...drop.map((p) => `:(top,literal,exclude)${p}`)], env);
1741
- if (drop.length) {
1742
- const r = spawnSync5("git", ["update-index", "--force-remove", "-z", "--stdin"], { cwd: root, env, input: `${drop.join("\0")}\0`, encoding: "utf8" });
1747
+ must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`), ...drop2.map((p) => `:(top,literal,exclude)${p}`)], env);
1748
+ if (drop2.length) {
1749
+ const r = spawnSync5("git", ["update-index", "--force-remove", "-z", "--stdin"], { cwd: root, env, input: `${drop2.join("\0")}\0`, encoding: "utf8" });
1743
1750
  if (r.status !== 0) throw new Error(`git update-index failed: ${r.stderr.trim()}`);
1744
1751
  }
1745
1752
  return must(root, ["write-tree"], env).trim();
@@ -1804,6 +1811,7 @@ ${must(root, [...DIFF, "-U10", a, b])}`;
1804
1811
  }
1805
1812
  function rangeDiff(root, a, b) {
1806
1813
  const range = `${a}..${b}`;
1814
+ const since2 = git(root, ["merge-base", a, b]).code === 0 ? `${a}...${b}` : range;
1807
1815
  const log2 = must(root, ["log", "--oneline", "--no-decorate", range]).trim();
1808
1816
  return [
1809
1817
  `## COMMITS
@@ -1811,10 +1819,10 @@ function rangeDiff(root, a, b) {
1811
1819
  ${log2 || "(none)"}`,
1812
1820
  `## FILES CHANGED
1813
1821
 
1814
- ${must(root, [...DIFF, "--stat", range]).trim() || "(none)"}`,
1822
+ ${must(root, [...DIFF, "--stat", since2]).trim() || "(none)"}`,
1815
1823
  `## DIFF
1816
1824
 
1817
- ${must(root, [...DIFF, "-U10", range])}`
1825
+ ${must(root, [...DIFF, "-U10", since2])}`
1818
1826
  ].join("\n\n");
1819
1827
  }
1820
1828
  function commitOf(root, ref) {
@@ -2049,7 +2057,7 @@ async function spawnWorker(inv, opts) {
2049
2057
  process.once("SIGINT", stop);
2050
2058
  process.once("SIGTERM", stop);
2051
2059
  let killer;
2052
- const timer = setTimeout(() => {
2060
+ const timer2 = setTimeout(() => {
2053
2061
  res.timedOut = true;
2054
2062
  child.kill("SIGTERM");
2055
2063
  killer = setTimeout(() => child.kill("SIGKILL"), 1e4);
@@ -2061,7 +2069,7 @@ async function spawnWorker(inv, opts) {
2061
2069
  });
2062
2070
  child.on("close", (c) => resolve2(c));
2063
2071
  });
2064
- clearTimeout(timer);
2072
+ clearTimeout(timer2);
2065
2073
  if (killer) clearTimeout(killer);
2066
2074
  process.off("SIGINT", stop);
2067
2075
  process.off("SIGTERM", stop);
@@ -2234,8 +2242,8 @@ function wantSnapshot(readIn2, root, dir) {
2234
2242
  return readIn2 === "snapshot" || secrets.length ? { secrets } : void 0;
2235
2243
  }
2236
2244
  function readSnapshot(root) {
2237
- const drop = findSecretFilesInTree(root, root);
2238
- const tree = snapshotTree(root, [], drop);
2245
+ const drop2 = findSecretFilesInTree(root, root);
2246
+ const tree = snapshotTree(root, [], drop2);
2239
2247
  const dest = path12.join(snapshotsDir(), tree);
2240
2248
  if (fs14.existsSync(path12.join(dest, ".git"))) {
2241
2249
  touch(dest);
@@ -2377,6 +2385,9 @@ function auditTask(meta, answer) {
2377
2385
  return [
2378
2386
  "You are auditing another worker's answer to a read-only question about this project. Do not take the answer on trust:",
2379
2387
  "check its key claims yourself against the files here (open the cited files and lines, search for what it says exists or is missing).",
2388
+ "Check the answer against the QUESTION, not only its own claims: every condition the question sets (a directory or scope,",
2389
+ 'what to leave out, "all", "only", "exactly", a count) must hold. Valid references do not make an answer right:',
2390
+ "for a list, look for items that are missing and items that do not belong; for a count, count again yourself.",
2380
2391
  "Do not edit anything and do not run anything that changes files.",
2381
2392
  "",
2382
2393
  "QUESTION:",
@@ -2391,7 +2402,7 @@ function auditTask(meta, answer) {
2391
2402
  "DISPUTED:",
2392
2403
  "- <the claim> \u2014 <what is actually true, with path:line>",
2393
2404
  "",
2394
- "AGREE: every key claim you checked holds. PARTIAL: some are wrong or could not be verified. DISAGREE: the main conclusion is wrong.",
2405
+ "AGREE: every key claim you checked holds and the answer meets every condition of the question (no extra or missing items, the right count). PARTIAL: some are wrong, unverified, or the answer goes beyond or falls short of the question's scope. DISAGREE: the main conclusion is wrong.",
2395
2406
  'Under DISPUTED list only claims you checked and found wrong or unsupported; write "- (none)" when there are none.'
2396
2407
  ].join("\n");
2397
2408
  }
@@ -2577,18 +2588,18 @@ function place(src, dest, opts) {
2577
2588
  }
2578
2589
  function install(opts) {
2579
2590
  const root = packageRoot();
2580
- const skills = opts.skills === false ? [] : skillNames(root);
2581
- if (opts.skills !== false && !skills.length) throw new Error(`no skills found under ${path14.join(root, "skills")}`);
2591
+ const skills2 = opts.skills === false ? [] : skillNames(root);
2592
+ if (opts.skills !== false && !skills2.length) throw new Error(`no skills found under ${path14.join(root, "skills")}`);
2582
2593
  const out = [];
2583
2594
  for (const base2 of opts.skills === false ? [] : skillTargets()) {
2584
2595
  for (const legacy of LEGACY) {
2585
2596
  const l = path14.join(base2, legacy);
2586
- if (!skills.includes(legacy) && linksInto(l, root)) {
2597
+ if (!skills2.includes(legacy) && linksInto(l, root)) {
2587
2598
  fs16.unlinkSync(l);
2588
2599
  out.push(`\u2714 removed old link ${l}`);
2589
2600
  }
2590
2601
  }
2591
- for (const name of skills) out.push(place(path14.join(root, "skills", name), path14.join(base2, name), opts));
2602
+ for (const name of skills2) out.push(place(path14.join(root, "skills", name), path14.join(base2, name), opts));
2592
2603
  }
2593
2604
  const bundle = path14.join(root, "dist", "pitroom.mjs");
2594
2605
  if (fs16.existsSync(bundle)) {
@@ -3350,6 +3361,7 @@ function archivedId(ref) {
3350
3361
  return void 0;
3351
3362
  }
3352
3363
  }
3364
+ var VERIFY_FAILED = "(r.state = 'done' AND json_extract(r.meta_json, '$.verifyResult.ok') = 0)";
3353
3365
  var ftsQuery = (text) => text.split(/\s+/).filter(Boolean).map((w) => `"${w.replace(/"/g, '""')}"*`).join(" ");
3354
3366
  function listHistory(q = {}) {
3355
3367
  const db = openDb();
@@ -3369,7 +3381,7 @@ function listHistory(q = {}) {
3369
3381
  args.push(q.backend);
3370
3382
  }
3371
3383
  if (q.state) {
3372
- where.push(q.state === "problem" ? "r.state IN ('failed','timeout','stopped')" : "r.state = ?");
3384
+ where.push(q.state === "problem" ? `(r.state IN ('failed','timeout','stopped') OR ${VERIFY_FAILED})` : "r.state = ?");
3373
3385
  if (q.state !== "problem") args.push(q.state);
3374
3386
  }
3375
3387
  if (q.group) {
@@ -3384,7 +3396,7 @@ function listHistory(q = {}) {
3384
3396
  try {
3385
3397
  const total = db.prepare(`SELECT COUNT(*) AS n FROM runs r ${base2}`).get(...args).n;
3386
3398
  const page = q.beforeId ? `${base2 ? `${base2} AND` : "WHERE"} r.id < ?` : base2;
3387
- const rows = db.prepare(`SELECT r.*, json_extract(r.meta_json, '$.audit.state') AS audit_state, json_extract(r.meta_json, '$.audit.verdict') AS audit_verdict FROM runs r ${page} ORDER BY r.id DESC LIMIT ?`).all(...args, ...q.beforeId ? [q.beforeId] : [], Math.min(Math.max(q.limit ?? 30, 1), 200));
3399
+ const rows = db.prepare(`SELECT r.*, json_extract(r.meta_json, '$.audit.state') AS audit_state, json_extract(r.meta_json, '$.audit.verdict') AS audit_verdict, ${VERIFY_FAILED} AS verify_failed FROM runs r ${page} ORDER BY r.id DESC LIMIT ?`).all(...args, ...q.beforeId ? [q.beforeId] : [], Math.min(Math.max(q.limit ?? 30, 1), 200));
3388
3400
  return {
3389
3401
  total,
3390
3402
  rows: rows.map((r) => ({
@@ -3398,6 +3410,7 @@ function listHistory(q = {}) {
3398
3410
  group: r.grp ?? void 0,
3399
3411
  verdict: r.verdict ?? void 0,
3400
3412
  audit: auditBadge({ audit: r.audit_state ? { id: "", state: r.audit_state, verdict: r.audit_verdict ?? void 0 } : void 0 }),
3413
+ verifyFailed: r.verify_failed ? true : void 0,
3401
3414
  seconds: r.seconds ?? void 0,
3402
3415
  steps: r.steps ?? void 0,
3403
3416
  tokens: r.tokens ?? void 0,
@@ -3544,7 +3557,9 @@ function readSummary(meta) {
3544
3557
  }
3545
3558
  function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
3546
3559
  const out = [];
3547
- out.push(`${bold("pitroom")} ${stateColour(meta.state, `${ICON2[meta.state]} ${meta.state}`)} \xB7 ${meta.mode} \xB7 ${duration(meta)} \xB7 run ${dim(meta.id)}`);
3560
+ const verifyFailed = meta.state === "done" && meta.verifyResult && !meta.verifyResult.ok;
3561
+ const head = verifyFailed ? stateColour("failed", "\u26A0 done \xB7 verify failed") : stateColour(meta.state, `${ICON2[meta.state]} ${meta.state}`);
3562
+ out.push(`${bold("pitroom")} ${head} \xB7 ${meta.mode} \xB7 ${duration(meta)} \xB7 run ${dim(meta.id)}`);
3548
3563
  const ran = meta.ran ?? meta.worker;
3549
3564
  const ids = [`worker ${ran.backend}`, meta.resolvedModel && `model ${meta.resolvedModel}`, meta.sessionId && `session ${meta.sessionId}`];
3550
3565
  out.push(ids.filter(Boolean).join(" \xB7 "));
@@ -3610,7 +3625,9 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
3610
3625
  }
3611
3626
  if (meta.verifyResult) {
3612
3627
  const v = meta.verifyResult;
3613
- out.push(`\u2500\u2500 verify: \`${meta.verify}\` ${v.ok ? "\u2714 passed" : `\u2718 failed (exit ${v.code})`}`);
3628
+ const notFound = !v.ok && v.code === 127 && /not found|no such file/i.test(v.tail);
3629
+ out.push(`\u2500\u2500 verify: \`${meta.verify}\` ${v.ok ? "\u2714 passed" : notFound ? "\u2718 could not run (exit 127: command not found)" : `\u2718 failed (exit ${v.code})`}`);
3630
+ if (notFound) out.push(" the command is not on the PATH Pitroom runs with (an app such as an MCP client may start it without your shell's PATH): give its full path, or set PATH in the command");
3614
3631
  if (!v.ok && v.tail) out.push(...v.tail.split("\n").map((l) => ` ${l}`));
3615
3632
  }
3616
3633
  if (isActive(meta.state)) {
@@ -3634,7 +3651,7 @@ function progress(meta) {
3634
3651
  }
3635
3652
 
3636
3653
  // src/core/run.ts
3637
- var VERSION2 = true ? "0.16.0" : "0.0.0-dev";
3654
+ var VERSION2 = true ? "0.17.0" : "0.0.0-dev";
3638
3655
  function planWork(o) {
3639
3656
  if (!o.plan) return { task: o.task.trim(), tier: o.tier };
3640
3657
  if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
@@ -3732,6 +3749,7 @@ function prepareRun(o) {
3732
3749
  auditOf: o.audit?.of,
3733
3750
  auditRate: o.auditRate,
3734
3751
  inPlace: o.inPlace || void 0,
3752
+ cache: o.cache,
3735
3753
  snapshot: parent?.snapshot ?? (snap ? { dir: "", tree: "", left: snap.secrets } : void 0),
3736
3754
  plan: work.plan ?? o.review?.plan ?? parent?.plan,
3737
3755
  reviewKind: o.review?.kind,
@@ -4003,6 +4021,12 @@ function captureChanges(meta) {
4003
4021
  meta.stats = d.stats;
4004
4022
  fs24.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
4005
4023
  }
4024
+ function verifyPath() {
4025
+ const extra = [path20.dirname(process.execPath), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
4026
+ const parts = (process.env.PATH ?? "").split(path20.delimiter).filter(Boolean);
4027
+ for (const d of process.platform === "win32" ? [path20.dirname(process.execPath)] : extra) if (!parts.includes(d)) parts.push(d);
4028
+ return parts.join(path20.delimiter);
4029
+ }
4006
4030
  function runVerify(meta) {
4007
4031
  const r = spawnSync6(meta.verify, {
4008
4032
  cwd: meta.cwd,
@@ -4011,7 +4035,7 @@ function runVerify(meta) {
4011
4035
  timeout: 15 * 6e4,
4012
4036
  stdio: ["ignore", "pipe", "pipe"],
4013
4037
  maxBuffer: 64 * 1024 * 1024,
4014
- env: { ...process.env, PWD: meta.cwd, PITROOM_ACTIVE: "1" }
4038
+ env: { ...process.env, PATH: verifyPath(), PWD: meta.cwd, PITROOM_ACTIVE: "1" }
4015
4039
  });
4016
4040
  const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
4017
4041
  fs24.writeFileSync(runFile(meta.id, "verify.log"), output);
@@ -4062,6 +4086,10 @@ function cleanupWorktree(meta) {
4062
4086
  if (meta.worktree && fs24.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
4063
4087
  }
4064
4088
 
4089
+ // src/cli/mcp-extras.ts
4090
+ import fs25 from "node:fs";
4091
+ import path21 from "node:path";
4092
+
4065
4093
  // src/cli/mcp-support.ts
4066
4094
  import { spawn as spawn3 } from "node:child_process";
4067
4095
 
@@ -4248,12 +4276,12 @@ function pit(args, timeoutMs = 60 * 6e4, signal) {
4248
4276
  child.stdout.on("data", (d) => out += d);
4249
4277
  child.stderr.on("data", (d) => err += d);
4250
4278
  const stop = () => child.kill("SIGTERM");
4251
- const timer = setTimeout(stop, timeoutMs);
4279
+ const timer2 = setTimeout(stop, timeoutMs);
4252
4280
  signal?.addEventListener("abort", stop, { once: true });
4253
4281
  if (signal?.aborted) stop();
4254
4282
  child.on("error", (e) => resolve2({ code: 1, out, err: err || String(e.message) }));
4255
4283
  child.on("close", (code) => {
4256
- clearTimeout(timer);
4284
+ clearTimeout(timer2);
4257
4285
  signal?.removeEventListener("abort", stop);
4258
4286
  resolve2({ code, out: strip(out).trim(), err: strip(err).trim() });
4259
4287
  });
@@ -4264,11 +4292,11 @@ function asResult(r, still) {
4264
4292
  if (r.code === 75 && still) return { text: clip6(`${text}
4265
4293
 
4266
4294
  ${still}`) };
4267
- return { text: clip6(text), isError: r.code === 2 || r.code === 3 || r.code === 1 && !r.out };
4295
+ return { text: clip6(text), isError: r.code === 2 || r.code === 3 || r.code === 5 || r.code === 6 || r.code === 1 && !r.out };
4268
4296
  }
4269
4297
  var plain = async (args, ctx, timeoutMs = 6e4) => asResult(await pit(args, timeoutMs, ctx.signal));
4270
4298
  function trackProgress(ctx, ids) {
4271
- const tick = () => {
4299
+ const tick2 = () => {
4272
4300
  try {
4273
4301
  const lines = ids().map((id) => freshMeta(id)).filter((m) => isActive(m.state)).map((m) => progress(m).replace(/^pitroom\s+/, ""));
4274
4302
  if (lines.length) ctx.progress(lines.slice(0, 3).join("\n") + (lines.length > 3 ? `
@@ -4276,11 +4304,11 @@ function trackProgress(ctx, ids) {
4276
4304
  } catch {
4277
4305
  }
4278
4306
  };
4279
- const first = setTimeout(tick, 800);
4280
- const timer = setInterval(tick, PROGRESS_EVERY_MS);
4307
+ const first = setTimeout(tick2, 800);
4308
+ const timer2 = setInterval(tick2, PROGRESS_EVERY_MS);
4281
4309
  return () => {
4282
4310
  clearTimeout(first);
4283
- clearInterval(timer);
4311
+ clearInterval(timer2);
4284
4312
  };
4285
4313
  }
4286
4314
  async function waitFor(ids, seconds, ctx, group) {
@@ -4305,13 +4333,17 @@ async function startAndWait(start, seconds, ctx, task) {
4305
4333
  const started = await pit([...start, "--bg", "--json", ...task === void 0 ? [] : ["--", task]]);
4306
4334
  if (started.code !== 0) return asResult(started);
4307
4335
  let id;
4336
+ let cached3 = false;
4308
4337
  try {
4309
- id = JSON.parse(started.out).id;
4338
+ ({ id, cached: cached3 = false } = JSON.parse(started.out));
4310
4339
  } catch {
4311
4340
  return { text: `could not read the run id from: ${started.out.slice(0, 200)}`, isError: true };
4312
4341
  }
4313
4342
  const result = await waitFor([id], seconds, ctx);
4314
- if (cancelled(ctx)) await stopQuietly(["stop", id]);
4343
+ if (cancelled(ctx) && !cached3) await stopQuietly(["stop", id]);
4344
+ if (cached3) result.text = `Cached answer: the same question on the same code as run ${id}; no worker ran (fresh: true asks one).
4345
+
4346
+ ${result.text}`;
4315
4347
  return result;
4316
4348
  }
4317
4349
  async function startCrew(flags, tasks, seconds, ctx) {
@@ -4337,6 +4369,7 @@ var RpcError = class extends Error {
4337
4369
  };
4338
4370
  var RUN = /^pitroom:\/\/run\/(\d{8}-\d{6}-[0-9a-f]{4})(\/patch)?$/;
4339
4371
  var LISTED = 30;
4372
+ var runOfUri = (uri) => RUN.exec(uri)?.[1];
4340
4373
  var oneLine6 = (text, n) => {
4341
4374
  const flat = text.replace(/\s+/g, " ").trim();
4342
4375
  return flat.length > n ? `${flat.slice(0, n - 1)}\u2026` : flat;
@@ -4399,16 +4432,50 @@ Call pitroom_run with mode "isolate" (add "verify" with the project's test comma
4399
4432
  title: "Split work across workers",
4400
4433
  description: "Independent tasks run in parallel as one group of workers.",
4401
4434
  arguments: [{ name: "tasks", description: "The tasks, one per line.", required: true }],
4402
- text: (a) => `Run these independent tasks in parallel with pitroom_crew (one worker each):
4435
+ text: (a) => `Run these independent tasks in parallel: call pitroom_run with "tasks" (one worker each):
4403
4436
 
4404
4437
  ${a.tasks}
4405
4438
 
4406
4439
  Make each task self-contained. Use mode "isolate" if they change files, then read every patch (pitroom_show, patch: true) and apply them with pitroom_apply (group). Only split work that does not depend on each other.`
4407
4440
  }
4408
4441
  ];
4409
- var listPrompts = () => ({ prompts: PROMPTS.map(({ name, title, description, arguments: args }) => ({ name, title, description, arguments: args })) });
4442
+ var TOOL_MAP = 'Through this MCP server the `pitroom` commands below are tools: `pitroom run` is pitroom_run (`-i` is mode "isolate", `-w` mode "write", `--continue` is continue, `--verify` is verify), `pitroom crew` is pitroom_run with tasks, `pitroom wait` pitroom_wait, `pitroom show`/`status` pitroom_show, `pitroom review` pitroom_review, `pitroom audit` pitroom_audit, `pitroom apply`/`discard`/`revert`/`stop` the tools of those names, `pitroom history`/`ls`/`models`/`doctor` pitroom_info. With a shell the commands work as well.';
4443
+ var skills;
4444
+ function skillPrompts() {
4445
+ if (skills) return skills;
4446
+ const found = [];
4447
+ const root = packageRoot();
4448
+ for (const name of skillNames(root)) {
4449
+ try {
4450
+ const text = fs25.readFileSync(path21.join(root, "skills", name, "SKILL.md"), "utf8");
4451
+ const front = /^---\n([\s\S]*?)\n---\n/.exec(text);
4452
+ const body = (front ? text.slice(front[0].length) : text).trim();
4453
+ const description = /^description:\s*(.+)$/m.exec(front?.[1] ?? "")?.[1]?.trim() ?? `The ${name} skill.`;
4454
+ const title = /^#\s+(.+)$/m.exec(body)?.[1]?.trim() ?? name;
4455
+ found.push({
4456
+ name,
4457
+ title,
4458
+ // the first sentence: prompts/list is read whole by some clients, so it stays short
4459
+ description: `Skill: ${description.split(/(?<=\.)\s/)[0]}`,
4460
+ arguments: [{ name: "task", description: "What you are about to do (optional)." }],
4461
+ text: (a) => `${TOOL_MAP}
4462
+
4463
+ ${body}${a.task ? `
4464
+
4465
+ ---
4466
+
4467
+ The task: ${a.task}` : ""}`
4468
+ });
4469
+ } catch {
4470
+ }
4471
+ }
4472
+ skills = found.filter((p) => !PROMPTS.some((b) => b.name === p.name));
4473
+ return skills;
4474
+ }
4475
+ var allPrompts = () => [...PROMPTS, ...skillPrompts()];
4476
+ var listPrompts = () => ({ prompts: allPrompts().map(({ name, title, description, arguments: args }) => ({ name, title, description, arguments: args })) });
4410
4477
  function getPrompt(name, given) {
4411
- const p = PROMPTS.find((x) => x.name === name);
4478
+ const p = allPrompts().find((x) => x.name === name);
4412
4479
  if (!p) throw new RpcError(-32602, `unknown prompt: ${String(name)}`);
4413
4480
  const args = {};
4414
4481
  const raw = given && typeof given === "object" && !Array.isArray(given) ? given : {};
@@ -4422,84 +4489,110 @@ function getPrompt(name, given) {
4422
4489
 
4423
4490
  // src/cli/mcp-tools.ts
4424
4491
  var WORKER_PROPS = {
4425
- worker: { type: "string", description: 'Worker target "backend[:model]", e.g. "opencode", "codex", "claude:haiku", "gemini:gemini-3.8-flash". Default: the configured worker.' },
4426
- model: { type: "string", description: "Model for the preferred worker." },
4427
- tier: { type: "string", description: "A worker tier from the config: cheap, standard, capable (an explicit worker wins)." },
4428
- effort: { type: "string", description: "Reasoning effort for the worker: low, medium, high, xhigh." },
4429
- dir: { type: "string", description: "Project directory (default: the directory the server was started in)." },
4430
- files: { type: "array", items: { type: "string" }, description: "Files to attach (paths)." },
4431
- verify: { type: "string", description: 'A command run after the worker finishes (e.g. "npm test"); a failing one is reported.' },
4432
- link: { type: "array", items: { type: "string" }, description: 'Ignored directories to link into an isolated copy so tests can run (e.g. ["node_modules"]).' },
4433
- web: { type: "boolean", description: "Let the worker use web tools." },
4434
- group: { type: "string", description: "A name to group related runs under." }
4492
+ worker: { type: "string", description: 'Worker "backend[:model]", e.g. "opencode", "claude:haiku". Default: configured.' },
4493
+ model: { type: "string", description: "Model for that worker." },
4494
+ tier: { type: "string", description: "cheap, standard or capable (from the config)." },
4495
+ effort: { type: "string", description: "low, medium, high, xhigh." },
4496
+ dir: { type: "string", description: "Project directory (default: the server's)." },
4497
+ files: { type: "array", items: { type: "string" }, description: "Files to attach." },
4498
+ verify: { type: "string", description: 'Command run afterwards, e.g. "npm test".' },
4499
+ link: { type: "array", items: { type: "string" }, description: 'Ignored dirs to link into an isolated copy, e.g. ["node_modules"].' },
4500
+ web: { type: "boolean", description: "Allow web tools." },
4501
+ group: { type: "string", description: "Group name for related runs." }
4435
4502
  };
4436
- var WAIT_PROP = { waitSeconds: { type: "number", description: `How long to wait for the result before returning "still running" (default ${DEFAULT_WAIT}, at most ${MAX_WAIT}).` } };
4437
- var RUN_ID2 = { type: "string", description: 'A run id as printed by a run, or "last".' };
4438
- var SINCE = { type: "string", description: "A window: 7d, 30d, \u2026 or all." };
4503
+ var WAIT_PROP = { waitSeconds: { type: "number", description: `Wait this long before returning "still running" (default ${DEFAULT_WAIT}, max ${MAX_WAIT}).` } };
4504
+ var RUN_ID2 = { type: "string", description: 'Run id, or "last".' };
4439
4505
  var READ_ONLY = { readOnlyHint: true, openWorldHint: false };
4506
+ var TOPICS = {
4507
+ runs: ["running", "group"],
4508
+ history: ["text", "state", "model", "since", "limit"],
4509
+ stats: ["since"],
4510
+ savings: ["since", "perModel"],
4511
+ models: ["worker", "all"],
4512
+ cooldown: [],
4513
+ config: [],
4514
+ doctor: []
4515
+ };
4516
+ function infoArgs(topic, a) {
4517
+ const allowed = TOPICS[topic];
4518
+ const stray = Object.keys(a).filter((k) => k !== "topic" && a[k] !== void 0 && a[k] !== null && !allowed.includes(k));
4519
+ if (stray.length) throw new ToolError(`${stray.map((k) => `"${k}"`).join(", ")} ${stray.length > 1 ? "do" : "does"} not go with topic "${topic}" (${allowed.length ? `it takes ${allowed.join(", ")}` : "it takes no options"})`);
4520
+ const group = str(a, "group");
4521
+ const worker = str(a, "worker");
4522
+ switch (topic) {
4523
+ case "runs":
4524
+ return { args: group ? ["status", "-g", group] : ["ls", ...bool(a, "running") ? ["--running"] : []] };
4525
+ case "history": {
4526
+ const limit = a.limit;
4527
+ if (limit !== void 0 && (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1 || limit > 200)) throw new ToolError('"limit" must be a whole number from 1 to 200');
4528
+ const text = str(a, "text");
4529
+ const state = str(a, "state");
4530
+ const model = str(a, "model");
4531
+ return { args: ["history", ...since(a), ...state ? ["--state", state] : [], ...model ? ["--model", model] : [], ...limit ? ["--limit", String(limit)] : [], ...text ? ["--", text] : []] };
4532
+ }
4533
+ case "stats":
4534
+ return { args: ["history", "stats", ...since(a)] };
4535
+ case "savings":
4536
+ return { args: ["savings", ...since(a), ...bool(a, "perModel") ? ["--models"] : []] };
4537
+ case "models":
4538
+ return { args: ["models", ...worker ? [worker] : [], ...bool(a, "all") ? ["--all"] : []] };
4539
+ case "doctor":
4540
+ return { args: ["doctor"], timeoutMs: 12e4 };
4541
+ case "cooldown":
4542
+ case "config":
4543
+ return { args: [topic] };
4544
+ default:
4545
+ throw new Error(`topic "${topic}" has no command`);
4546
+ }
4547
+ }
4440
4548
  var TOOLS2 = [
4441
4549
  {
4442
4550
  name: "pitroom_run",
4443
4551
  title: "Run a worker",
4444
- description: `Hand a bounded task to a cheaper worker agent. mode "read" (default) is read-only research that returns an answer whose file:line references are verified; "isolate" lets the worker edit a private copy and returns the exact diff (review it, then pitroom_apply or pitroom_discard); "write" edits the working tree in place and is undoable with pitroom_revert. Returns the worker's answer and a receipt, or "still running" with the run id. Progress is reported while it waits; cancelling the call stops the run.`,
4552
+ description: 'Hand a bounded task to a cheaper worker agent. mode "read" (default): read-only research, an answer with verified file:line references. "isolate": the worker edits a private copy and you get the exact diff (then pitroom_review, pitroom_apply or pitroom_discard). "write": edits the working tree (undo: pitroom_revert). Give "tasks" instead of "task" to run independent tasks in parallel (read or isolate). Returns the report(s) and a receipt (a read question asked before on the same code comes back cached), or "still running" for pitroom_wait. The using-pitroom skill (or prompt) says when and how to delegate. Cancelling the call stops the run.',
4445
4553
  inputSchema: {
4446
4554
  type: "object",
4447
4555
  properties: {
4448
- task: { type: "string", description: "What the worker should do, with the context it needs." },
4449
- mode: { type: "string", enum: ["read", "isolate", "write"], description: "read (default), isolate, or write." },
4450
- continue: { type: "string", description: "A finished run to follow up in the same worker session (the task is then the follow-up)." },
4451
- inPlace: { type: "boolean", description: "Read mode only: read the directory itself instead of a clean snapshot without secret-looking files." },
4452
- audit: { type: "boolean", description: "Read mode only: have another worker re-check the answer afterwards." },
4556
+ task: { type: "string", description: "What to do, with the context the worker needs." },
4557
+ tasks: { type: "array", items: { type: "string" }, minItems: 1, maxItems: MAX_TASKS, description: "Or several independent tasks, one worker each." },
4558
+ mode: { type: "string", enum: ["read", "isolate", "write"] },
4559
+ continue: { type: "string", description: "A finished run to follow up in the same worker session." },
4560
+ inPlace: { type: "boolean", description: "Read: read the directory itself, not a snapshot without secret-looking files." },
4561
+ audit: { type: "boolean", description: "Read: have another worker re-check the answer (each one, with tasks)." },
4562
+ fresh: { type: "boolean", description: "Read: ask a worker even if this question was answered on the same code." },
4453
4563
  ...WORKER_PROPS,
4454
4564
  ...WAIT_PROP
4455
- },
4456
- required: ["task"]
4565
+ }
4457
4566
  },
4458
4567
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
4459
4568
  async call(a, ctx) {
4460
- const task = str(a, "task", true);
4569
+ const task = str(a, "task");
4570
+ const tasks = strs(a, "tasks").filter((t) => t.trim());
4571
+ if (!task && !tasks.length) throw new ToolError('give "task" (or "tasks" for parallel work)');
4572
+ if (task && tasks.length) throw new ToolError('give "task" or "tasks", not both');
4461
4573
  const mode = oneOf(a, "mode", ["read", "isolate", "write"], "read");
4462
4574
  const flags = mode === "isolate" ? ["-i"] : mode === "write" ? ["-w"] : [];
4463
- const follow = str(a, "continue");
4464
- if (follow) flags.push("--continue", follow);
4465
4575
  if (mode === "read") {
4466
4576
  if (bool(a, "inPlace")) flags.push("--in-place");
4467
4577
  if (bool(a, "audit")) flags.push("--audit");
4578
+ if (bool(a, "fresh")) flags.push("--fresh");
4579
+ }
4580
+ const follow = str(a, "continue");
4581
+ if (tasks.length) {
4582
+ if (tasks.length > MAX_TASKS) throw new ToolError(`at most ${MAX_TASKS} tasks at once: start the rest when these are done`);
4583
+ if (mode === "write") throw new ToolError('parallel workers never write in place: use mode "isolate" (each gets its own copy)');
4584
+ if (follow) throw new ToolError('"continue" follows up one run: give "task"');
4585
+ return startCrew([...flags, ...workerFlags(a)], tasks, waitSeconds(a), ctx);
4468
4586
  }
4587
+ if (follow) flags.push("--continue", follow);
4469
4588
  return startAndWait(["run", ...flags, ...workerFlags(a)], waitSeconds(a), ctx, task);
4470
4589
  }
4471
4590
  },
4472
- {
4473
- name: "pitroom_crew",
4474
- title: "Run workers in parallel",
4475
- description: 'Start several independent tasks at once as one group of workers (read, or isolate: each edits its own copy) and wait for the group. Returns every report, or "still running" with the group name for pitroom_wait. Only for tasks that do not depend on each other. Cancelling the call stops the group.',
4476
- inputSchema: {
4477
- type: "object",
4478
- properties: {
4479
- tasks: { type: "array", items: { type: "string" }, minItems: 1, maxItems: MAX_TASKS, description: "One self-contained task per worker." },
4480
- mode: { type: "string", enum: ["read", "isolate"], description: "read (default) or isolate." },
4481
- ...WORKER_PROPS,
4482
- ...WAIT_PROP
4483
- },
4484
- required: ["tasks"]
4485
- },
4486
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
4487
- async call(a, ctx) {
4488
- const tasks = strs(a, "tasks").filter((t) => t.trim());
4489
- if (!tasks.length) throw new ToolError('"tasks" needs at least one task');
4490
- if (tasks.length > MAX_TASKS) throw new ToolError(`at most ${MAX_TASKS} tasks at once: start the rest when these are done`);
4491
- const mode = oneOf(a, "mode", ["read", "isolate"], "read");
4492
- return startCrew([...mode === "isolate" ? ["-i"] : [], ...workerFlags(a)], tasks, waitSeconds(a), ctx);
4493
- }
4494
- },
4495
4591
  {
4496
4592
  name: "pitroom_wait",
4497
4593
  title: "Wait for runs",
4498
- description: 'Wait for runs started earlier (a run that came back "still running") and return their reports.',
4499
- inputSchema: {
4500
- type: "object",
4501
- properties: { runs: { type: "array", items: { type: "string" }, description: "Run ids." }, group: { type: "string", description: "Or a group name." }, ...WAIT_PROP }
4502
- },
4594
+ description: 'Wait for runs that came back "still running" and return their reports.',
4595
+ inputSchema: { type: "object", properties: { runs: { type: "array", items: { type: "string" } }, group: { type: "string" }, ...WAIT_PROP } },
4503
4596
  annotations: READ_ONLY,
4504
4597
  async call(a, ctx) {
4505
4598
  const runs = strs(a, "runs");
@@ -4508,126 +4601,56 @@ var TOOLS2 = [
4508
4601
  return waitFor(runs, waitSeconds(a), ctx, runs.length ? void 0 : group);
4509
4602
  }
4510
4603
  },
4511
- {
4512
- name: "pitroom_status",
4513
- title: "Run status",
4514
- description: "The state and live progress of a run (default: the latest), or of a group.",
4515
- inputSchema: { type: "object", properties: { run: RUN_ID2, group: { type: "string", description: "A group name." } } },
4516
- annotations: READ_ONLY,
4517
- async call(a, ctx) {
4518
- const group = str(a, "group");
4519
- const run2 = str(a, "run");
4520
- if (group && run2) throw new ToolError('give "run" or "group", not both');
4521
- return plain(["status", ...group ? ["-g", group] : run2 ? [run2] : []], ctx);
4522
- }
4523
- },
4524
- {
4525
- name: "pitroom_list",
4526
- title: "List runs",
4527
- description: "The latest runs with their state, worker and task; with running only the active ones, or one group's.",
4528
- inputSchema: { type: "object", properties: { running: { type: "boolean", description: "Only runs that are queued or running." }, group: { type: "string", description: "A group name." } } },
4529
- annotations: READ_ONLY,
4530
- async call(a, ctx) {
4531
- const group = str(a, "group");
4532
- return plain(["ls", ...bool(a, "running") ? ["--running"] : [], ...group ? ["-g", group] : []], ctx);
4533
- }
4534
- },
4535
4604
  {
4536
4605
  name: "pitroom_show",
4537
4606
  title: "Show a run",
4538
- description: "A finished run's report again; with patch the exact diff of an isolated change, with full the untruncated answer.",
4539
- inputSchema: { type: "object", properties: { run: RUN_ID2, patch: { type: "boolean" }, full: { type: "boolean" } }, required: ["run"] },
4607
+ description: "A run's report (live progress while it runs); patch: the exact diff; full: the untruncated answer.",
4608
+ inputSchema: { type: "object", properties: { run: RUN_ID2, patch: { type: "boolean" }, full: { type: "boolean" } } },
4540
4609
  annotations: READ_ONLY,
4541
4610
  async call(a, ctx) {
4542
- const run2 = str(a, "run", true);
4611
+ const run2 = str(a, "run") ?? "last";
4543
4612
  return plain(["show", run2, ...bool(a, "patch") ? ["--patch"] : bool(a, "full") ? ["--full"] : []], ctx);
4544
4613
  }
4545
4614
  },
4546
4615
  {
4547
- name: "pitroom_history",
4548
- title: "Search past runs",
4549
- description: "Finished runs from the history, searchable: full-text over the task, the answer and the steps. Use it to find an earlier answer before asking a worker again.",
4616
+ name: "pitroom_info",
4617
+ title: "Pitroom information",
4618
+ description: "Reports, changing nothing. topic: runs (latest runs; running, group), history (search earlier answers before asking again; text, state, model, since, limit), stats (success rate, time, tokens, audits per worker; since), savings (since, perModel), models (a backend's models and costs; worker, all), cooldown (rate-limited models being skipped), config, doctor (setup check).",
4550
4619
  inputSchema: {
4551
4620
  type: "object",
4552
4621
  properties: {
4553
- text: { type: "string", description: "Words to search for." },
4554
- state: { type: "string", description: "done, problem, failed, \u2026" },
4555
- model: { type: "string", description: "Only runs of this model." },
4556
- since: SINCE,
4557
- limit: { type: "number", description: "How many runs (default 20)." }
4558
- }
4622
+ topic: { type: "string", enum: Object.keys(TOPICS) },
4623
+ text: { type: "string" },
4624
+ state: { type: "string" },
4625
+ model: { type: "string" },
4626
+ since: { type: "string", description: "7d, 30d, \u2026 or all." },
4627
+ limit: { type: "number" },
4628
+ running: { type: "boolean" },
4629
+ group: { type: "string" },
4630
+ worker: { type: "string" },
4631
+ all: { type: "boolean" },
4632
+ perModel: { type: "boolean" }
4633
+ },
4634
+ required: ["topic"]
4559
4635
  },
4560
4636
  annotations: READ_ONLY,
4561
4637
  async call(a, ctx) {
4562
- const limit = a.limit;
4563
- if (limit !== void 0 && (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1 || limit > 200)) throw new ToolError('"limit" must be a whole number from 1 to 200');
4564
- const text = str(a, "text");
4565
- const state = str(a, "state");
4566
- const model = str(a, "model");
4567
- return plain(["history", ...since(a), ...state ? ["--state", state] : [], ...model ? ["--model", model] : [], ...limit ? ["--limit", String(limit)] : [], ...text ? ["--", text] : []], ctx);
4568
- }
4569
- },
4570
- {
4571
- name: "pitroom_stats",
4572
- title: "Worker statistics",
4573
- description: "Runs, success rate, average time and tokens per worker and model, and how often their answers were confirmed by audits. Use it to pick a worker.",
4574
- inputSchema: { type: "object", properties: { since: SINCE } },
4575
- annotations: READ_ONLY,
4576
- async call(a, ctx) {
4577
- return plain(["history", "stats", ...since(a)], ctx);
4578
- }
4579
- },
4580
- {
4581
- name: "pitroom_savings",
4582
- title: "What the workers saved",
4583
- description: "The tokens and money workers took over from you, per model, from the receipts of the runs.",
4584
- inputSchema: { type: "object", properties: { since: SINCE, models: { type: "boolean", description: "Break it down per model." } } },
4585
- annotations: READ_ONLY,
4586
- async call(a, ctx) {
4587
- return plain(["savings", ...since(a), ...bool(a, "models") ? ["--models"] : []], ctx);
4588
- }
4589
- },
4590
- {
4591
- name: "pitroom_models",
4592
- title: "Worker models",
4593
- description: "The models a worker backend offers, with effort levels, the costs set in the config and recent usage. Use it to choose a model for pitroom_run.",
4594
- inputSchema: { type: "object", properties: { worker: { type: "string", description: "A backend: opencode, codex, claude, gemini. Default: the configured one." }, all: { type: "boolean", description: "Include models that are hidden by default." } } },
4595
- annotations: READ_ONLY,
4596
- async call(a, ctx) {
4597
- const worker = str(a, "worker");
4598
- return plain(["models", ...worker ? [worker] : [], ...bool(a, "all") ? ["--all"] : []], ctx);
4599
- }
4600
- },
4601
- {
4602
- name: "pitroom_cooldown",
4603
- title: "Rate-limited models",
4604
- description: 'The models that said "rate limited" and are being skipped for now (runs go to the next worker). With clear they are tried again immediately.',
4605
- inputSchema: { type: "object", properties: { clear: { type: "boolean", description: "Forget the cooldowns." } } },
4606
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
4607
- async call(a, ctx) {
4608
- return plain(["cooldown", ...bool(a, "clear") ? ["--clear"] : []], ctx);
4609
- }
4610
- },
4611
- {
4612
- name: "pitroom_doctor",
4613
- title: "Check the setup",
4614
- description: "Checks the setup without spending tokens: the workers found and their logins, the config, the skills, where the MCP server is registered. Use it when a run fails to start.",
4615
- inputSchema: { type: "object", properties: {} },
4616
- annotations: READ_ONLY,
4617
- async call(_a, ctx) {
4618
- return plain(["doctor"], ctx, 12e4);
4638
+ if (str(a, "topic") === void 0) throw new ToolError(`"topic" is required: one of ${Object.keys(TOPICS).join(", ")}`);
4639
+ const topic = oneOf(a, "topic", Object.keys(TOPICS), "runs");
4640
+ const { args, timeoutMs } = infoArgs(topic, a);
4641
+ return plain(args, ctx, timeoutMs);
4619
4642
  }
4620
4643
  },
4621
4644
  {
4622
4645
  name: "pitroom_review",
4623
4646
  title: "Review a change",
4624
- description: `A read-only reviewer (by default another worker than the implementer) judges one run's change, or a range of commits ("main..HEAD"). Returns findings with severities and a SPEC / QUALITY verdict.`,
4647
+ description: `A read-only reviewer (another worker by default) judges a run's change or a commit range ("main..HEAD"): findings with severities and a SPEC / QUALITY verdict.`,
4625
4648
  inputSchema: {
4626
4649
  type: "object",
4627
4650
  properties: {
4628
- run: { ...RUN_ID2, description: "The run whose change to review (an isolated or in-place change)." },
4651
+ run: RUN_ID2,
4629
4652
  range: { type: "string", description: 'Or a commit range "A..B".' },
4630
- plan: { type: "string", description: "With range: the plan file the commits implement." },
4653
+ plan: { type: "string", description: "With range: the plan file it implements." },
4631
4654
  worker: WORKER_PROPS.worker,
4632
4655
  tier: WORKER_PROPS.tier,
4633
4656
  dir: WORKER_PROPS.dir,
@@ -4648,7 +4671,7 @@ var TOOLS2 = [
4648
4671
  {
4649
4672
  name: "pitroom_audit",
4650
4673
  title: "Audit an answer",
4651
- description: "Another worker re-checks a finished read run's answer against the project and says AGREE, PARTIAL or DISAGREE with the claims it disputes.",
4674
+ description: "Another worker re-checks a read run's answer: AGREE, PARTIAL or DISAGREE, with the disputed claims.",
4652
4675
  inputSchema: { type: "object", properties: { run: RUN_ID2, worker: WORKER_PROPS.worker, ...WAIT_PROP }, required: ["run"] },
4653
4676
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
4654
4677
  async call(a, ctx) {
@@ -4660,7 +4683,7 @@ var TOOLS2 = [
4660
4683
  {
4661
4684
  name: "pitroom_apply",
4662
4685
  title: "Apply an isolated change",
4663
- description: "Land an isolated run's patch on the user's working tree (checked first; refused if it no longer applies cleanly), or every patch of a group in order. A patch that deletes files is refused unless allowDelete is true: only pass it after checking the deletions are what the user asked for.",
4686
+ description: "Land an isolated run's patch (or a group's, in order) on the working tree; checked first. A patch deleting files needs allowDelete: pass it only after checking the deletions were asked for.",
4664
4687
  inputSchema: { type: "object", properties: { run: RUN_ID2, group: { type: "string" }, allowDelete: { type: "boolean" } } },
4665
4688
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
4666
4689
  async call(a, ctx) {
@@ -4668,13 +4691,13 @@ var TOOLS2 = [
4668
4691
  const run2 = str(a, "run");
4669
4692
  if (!run2 && !group) throw new ToolError('give "run" or "group"');
4670
4693
  if (run2 && group) throw new ToolError('give "run" or "group", not both');
4671
- return plain(["apply", ...group && !run2 ? ["-g", group] : [run2], ...bool(a, "allowDelete") ? ["--allow-delete"] : []], ctx, 12e4);
4694
+ return plain(["apply", ...group ? ["-g", group] : [run2], ...bool(a, "allowDelete") ? ["--allow-delete"] : []], ctx, 12e4);
4672
4695
  }
4673
4696
  },
4674
4697
  {
4675
4698
  name: "pitroom_discard",
4676
4699
  title: "Discard an isolated change",
4677
- description: "Drop an isolated run's private copy (its patch is kept).",
4700
+ description: "Drop an isolated run's private copy (the patch is kept).",
4678
4701
  inputSchema: { type: "object", properties: { run: RUN_ID2 }, required: ["run"] },
4679
4702
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
4680
4703
  async call(a, ctx) {
@@ -4684,7 +4707,7 @@ var TOOLS2 = [
4684
4707
  {
4685
4708
  name: "pitroom_revert",
4686
4709
  title: "Undo an in-place change",
4687
- description: 'Undo what a mode "write" run changed in the working tree (checked first; refused when the files changed since).',
4710
+ description: `Undo a mode "write" run's changes (refused if the files changed since).`,
4688
4711
  inputSchema: { type: "object", properties: { run: RUN_ID2 }, required: ["run"] },
4689
4712
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
4690
4713
  async call(a, ctx) {
@@ -4694,32 +4717,115 @@ var TOOLS2 = [
4694
4717
  {
4695
4718
  name: "pitroom_stop",
4696
4719
  title: "Stop runs",
4697
- description: "Stop a running or queued run, or every run of a group.",
4698
- inputSchema: { type: "object", properties: { run: RUN_ID2, group: { type: "string" } } },
4720
+ description: "Stop a running or queued run, or a group; cooldowns: try rate-limited models again now.",
4721
+ inputSchema: { type: "object", properties: { run: RUN_ID2, group: { type: "string" }, cooldowns: { type: "boolean" } } },
4699
4722
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
4700
4723
  async call(a, ctx) {
4701
4724
  const run2 = str(a, "run");
4702
4725
  const group = str(a, "group");
4703
- if (!run2 && !group) throw new ToolError('give "run" or "group"');
4726
+ if (bool(a, "cooldowns")) {
4727
+ if (run2 || group) throw new ToolError('"cooldowns" goes alone');
4728
+ return plain(["cooldown", "--clear"], ctx);
4729
+ }
4730
+ if (!run2 && !group) throw new ToolError('give "run" or "group" (or "cooldowns": true)');
4704
4731
  if (run2 && group) throw new ToolError('give "run" or "group", not both');
4705
4732
  return plain(["stop", ...run2 ? [run2] : ["-g", group]], ctx);
4706
4733
  }
4707
4734
  }
4708
4735
  ];
4709
4736
 
4737
+ // src/cli/mcp-watch.ts
4738
+ var EVERY_MS = 2e3;
4739
+ var MAX_SUBSCRIPTIONS = 100;
4740
+ var watched = /* @__PURE__ */ new Set();
4741
+ var timer;
4742
+ var signature = (uri) => {
4743
+ const id = runOfUri(uri);
4744
+ if (!id) return void 0;
4745
+ try {
4746
+ const m = freshMeta(id);
4747
+ return uri.endsWith("/patch") ? `${m.state}:${m.changes?.length ?? 0}` : m.state;
4748
+ } catch {
4749
+ return void 0;
4750
+ }
4751
+ };
4752
+ var newestRun = () => {
4753
+ try {
4754
+ return listRunIds().at(-1);
4755
+ } catch {
4756
+ return void 0;
4757
+ }
4758
+ };
4759
+ function subscribe(session, uri) {
4760
+ if (typeof uri !== "string") throw new RpcError(-32602, '"uri" is required');
4761
+ if (!runOfUri(uri)) throw new RpcError(-32602, `only runs can be subscribed to (pitroom://run/<id>): ${uri}`);
4762
+ if (!session.subscriptions.has(uri) && session.subscriptions.size >= MAX_SUBSCRIPTIONS) throw new RpcError(-32602, `at most ${MAX_SUBSCRIPTIONS} subscriptions: unsubscribe from finished runs first`);
4763
+ const sig = signature(uri);
4764
+ if (sig === void 0) throw new RpcError(-32002, `unknown resource: ${uri}`);
4765
+ session.subscriptions.set(uri, sig);
4766
+ }
4767
+ function unsubscribe(session, uri) {
4768
+ if (typeof uri !== "string") throw new RpcError(-32602, '"uri" is required');
4769
+ session.subscriptions.delete(uri);
4770
+ }
4771
+ function tick() {
4772
+ const listening = [...watched].filter((s) => s.push);
4773
+ if (!listening.length) return;
4774
+ const newest = newestRun();
4775
+ const seen = /* @__PURE__ */ new Map();
4776
+ for (const s of listening) {
4777
+ const push = s.push;
4778
+ if (newest !== s.newestRun) {
4779
+ s.newestRun = newest;
4780
+ push({ jsonrpc: "2.0", method: "notifications/resources/list_changed" });
4781
+ }
4782
+ for (const [uri, last] of s.subscriptions) {
4783
+ if (!seen.has(uri)) seen.set(uri, signature(uri));
4784
+ const now = seen.get(uri);
4785
+ if (now === last) continue;
4786
+ if (now === void 0) s.subscriptions.delete(uri);
4787
+ else s.subscriptions.set(uri, now);
4788
+ push({ jsonrpc: "2.0", method: "notifications/resources/updated", params: { uri } });
4789
+ }
4790
+ }
4791
+ }
4792
+ function watch2(session) {
4793
+ session.newestRun = newestRun();
4794
+ watched.add(session);
4795
+ if (!timer) {
4796
+ timer = setInterval(tick, EVERY_MS);
4797
+ timer.unref();
4798
+ }
4799
+ }
4800
+ function unwatch(session) {
4801
+ watched.delete(session);
4802
+ session.subscriptions.clear();
4803
+ if (!watched.size && timer) {
4804
+ clearInterval(timer);
4805
+ timer = void 0;
4806
+ }
4807
+ }
4808
+
4710
4809
  // src/cli/mcp.ts
4711
4810
  var PROTOCOLS = ["2025-06-18", "2025-03-26", "2024-11-05"];
4712
4811
  var INSTRUCTIONS = [
4713
4812
  "Pitroom hands bounded work to cheaper worker agents and returns a verified answer, the exact diff and a cost receipt. You decide, verify and answer.",
4714
- 'Use pitroom_run with mode "read" for research and locating code, mode "isolate" for code changes (the worker edits a copy; check it with pitroom_review, then pitroom_apply or pitroom_discard); pitroom_crew for independent tasks in parallel.',
4813
+ 'Use pitroom_run with mode "read" for research and locating code, mode "isolate" for code changes (the worker edits a copy; check it with pitroom_review, then pitroom_apply or pitroom_discard); pitroom_run with "tasks" for independent tasks in parallel; pitroom_info (topic history) finds an earlier answer before you ask again.',
4715
4814
  'A run can take minutes: pitroom_run waits up to waitSeconds, then returns the run id as "still running"; call pitroom_wait with it.',
4815
+ "Pitroom's skills (using-pitroom, pitroom-research, pitroom-implement, pitroom-review, pitroom-crew, pitroom-debugging, pitroom-tdd and more) say when to delegate and how to check what comes back: if you have them, use the matching one alongside these tools; if not, the prompts of the same names hold them.",
4716
4816
  "It is optional: for a small task you can do yourself, skip it."
4717
4817
  ].join(" ");
4718
4818
  var log = (msg) => void process.stderr.write(`pitroom mcp: ${msg}
4719
4819
  `);
4720
- var newSession = () => ({ inflight: /* @__PURE__ */ new Map() });
4820
+ function newSession() {
4821
+ const session = { inflight: /* @__PURE__ */ new Map(), subscriptions: /* @__PURE__ */ new Map() };
4822
+ watch2(session);
4823
+ return session;
4824
+ }
4721
4825
  function endSession(session) {
4722
4826
  for (const { abort } of session.inflight.values()) abort.abort("closed");
4827
+ unwatch(session);
4828
+ session.push = void 0;
4723
4829
  }
4724
4830
  function progressToken(params) {
4725
4831
  const meta = params && typeof params === "object" ? params._meta : void 0;
@@ -4737,13 +4843,13 @@ function contextFor(params, abort, notify) {
4737
4843
  }
4738
4844
  };
4739
4845
  }
4740
- async function dispatch(method, params, ctx) {
4846
+ async function dispatch(method, params, ctx, session) {
4741
4847
  switch (method) {
4742
4848
  case "initialize": {
4743
4849
  const asked = typeof params.protocolVersion === "string" ? params.protocolVersion : "";
4744
4850
  return {
4745
4851
  protocolVersion: PROTOCOLS.includes(asked) ? asked : PROTOCOLS[0],
4746
- capabilities: { tools: { listChanged: false }, resources: { listChanged: false, subscribe: false }, prompts: { listChanged: false } },
4852
+ capabilities: { tools: { listChanged: false }, resources: { listChanged: true, subscribe: true }, prompts: { listChanged: false } },
4747
4853
  serverInfo: { name: "pitroom", title: "Pitroom", version: VERSION2 },
4748
4854
  instructions: INSTRUCTIONS
4749
4855
  };
@@ -4772,6 +4878,12 @@ async function dispatch(method, params, ctx) {
4772
4878
  case "resources/read":
4773
4879
  if (typeof params.uri !== "string") throw new RpcError(-32602, '"uri" is required');
4774
4880
  return readResource(params.uri, ctx);
4881
+ case "resources/subscribe":
4882
+ subscribe(session, params.uri);
4883
+ return {};
4884
+ case "resources/unsubscribe":
4885
+ unsubscribe(session, params.uri);
4886
+ return {};
4775
4887
  case "prompts/list":
4776
4888
  return listPrompts();
4777
4889
  case "prompts/get":
@@ -4799,7 +4911,7 @@ async function handle(session, msg, notify) {
4799
4911
  const entry = { abort, ctx: contextFor(params, abort, notify) };
4800
4912
  if (isRequest) session.inflight.set(m.id, entry);
4801
4913
  try {
4802
- const result = await dispatch(m.method, params, entry.ctx);
4914
+ const result = await dispatch(m.method, params, entry.ctx, session);
4803
4915
  if (abort.signal.aborted && abort.signal.reason === "cancelled") return void 0;
4804
4916
  return isRequest ? { jsonrpc: "2.0", id: m.id, result } : void 0;
4805
4917
  } catch (e) {
@@ -4824,6 +4936,7 @@ async function serveMcp() {
4824
4936
  const send = (msg) => void process.stdout.write(`${JSON.stringify(msg)}
4825
4937
  `);
4826
4938
  const session = newSession();
4939
+ session.push = send;
4827
4940
  const rl = readline.createInterface({ input: process.stdin });
4828
4941
  const pending = /* @__PURE__ */ new Set();
4829
4942
  const handleLine = async (line) => {
@@ -4844,28 +4957,29 @@ async function serveMcp() {
4844
4957
  void p.finally(() => pending.delete(p));
4845
4958
  });
4846
4959
  await new Promise((resolve2) => rl.once("close", resolve2));
4960
+ unwatch(session);
4847
4961
  await Promise.allSettled([...pending]);
4848
4962
  return 0;
4849
4963
  }
4850
4964
 
4851
4965
  // src/cli/mcp-http.ts
4852
4966
  import crypto7 from "node:crypto";
4853
- import fs26 from "node:fs";
4967
+ import fs27 from "node:fs";
4854
4968
  import http2 from "node:http";
4855
- import path22 from "node:path";
4969
+ import path23 from "node:path";
4856
4970
 
4857
4971
  // src/core/dash.ts
4858
4972
  import { spawn as spawn4 } from "node:child_process";
4859
4973
  import crypto6 from "node:crypto";
4860
- import fs25 from "node:fs";
4974
+ import fs26 from "node:fs";
4861
4975
  import http from "node:http";
4862
- import path21 from "node:path";
4976
+ import path22 from "node:path";
4863
4977
  import { fileURLToPath as fileURLToPath3 } from "node:url";
4864
4978
 
4865
4979
  // src/core/file-diff.ts
4866
4980
  var PREVIEW_LINES = 300;
4867
4981
  function gitPath(value, prefix = true) {
4868
- let path29 = value;
4982
+ let path31 = value;
4869
4983
  if (value.startsWith('"') && value.endsWith('"')) {
4870
4984
  const bytes = [];
4871
4985
  const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
@@ -4873,9 +4987,9 @@ function gitPath(value, prefix = true) {
4873
4987
  if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
4874
4988
  else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
4875
4989
  }
4876
- path29 = new TextDecoder().decode(new Uint8Array(bytes));
4990
+ path31 = new TextDecoder().decode(new Uint8Array(bytes));
4877
4991
  }
4878
- return prefix ? path29.replace(/^[ab]\//, "") : path29;
4992
+ return prefix ? path31.replace(/^[ab]\//, "") : path31;
4879
4993
  }
4880
4994
  function headerPath(header) {
4881
4995
  const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
@@ -5005,6 +5119,7 @@ function toRun(m) {
5005
5119
  return {
5006
5120
  id: m.id,
5007
5121
  state: m.state,
5122
+ verifyFailed: m.verifyResult && !m.verifyResult.ok ? true : void 0,
5008
5123
  kind: kind(m),
5009
5124
  // the model that really ran, once the CLI has said which; until then just the worker, never a guess
5010
5125
  worker: workerName(ranTarget(m)),
@@ -5019,7 +5134,7 @@ function toRun(m) {
5019
5134
  audit: auditBadge(m),
5020
5135
  changes: m.changes?.length || void 0,
5021
5136
  applied: m.applied || void 0,
5022
- note: l?.last ? oneLine7(String(l.last), 140) : isActive(m.state) ? "" : m.verdict ? findings(m) : m.auditOf && m.state === "done" ? m.auditDisputed?.length ? `${m.auditDisputed.length} disputed` : "nothing disputed" : headline(m, 200) || oneLine7(m.error ?? "", 200)
5137
+ note: l?.last ? oneLine7(String(l.last), 140) : isActive(m.state) ? "" : m.verifyResult && !m.verifyResult.ok ? `verify failed: ${m.verify ?? ""}` : m.verdict ? findings(m) : m.auditOf && m.state === "done" ? m.auditDisputed?.length ? `${m.auditDisputed.length} disputed` : "nothing disputed" : headline(m, 200) || oneLine7(m.error ?? "", 200)
5023
5138
  };
5024
5139
  }
5025
5140
  function dashState(opts = {}) {
@@ -5123,11 +5238,11 @@ var assetsDir = () => fileURLToPath3(new URL("./ui/", import.meta.url));
5123
5238
  var assetCache = /* @__PURE__ */ new Map();
5124
5239
  function readAsset(route) {
5125
5240
  try {
5126
- const file2 = path21.join(assetsDir(), path21.basename(route));
5127
- const { mtimeMs } = fs25.statSync(file2);
5241
+ const file2 = path22.join(assetsDir(), path22.basename(route));
5242
+ const { mtimeMs } = fs26.statSync(file2);
5128
5243
  const hit = assetCache.get(route);
5129
5244
  if (hit && hit.mtimeMs === mtimeMs) return hit.data;
5130
- const data = fs25.readFileSync(file2);
5245
+ const data = fs26.readFileSync(file2);
5131
5246
  assetCache.set(route, { mtimeMs, data });
5132
5247
  return data;
5133
5248
  } catch {
@@ -5160,21 +5275,21 @@ function handler(touch2) {
5160
5275
  }
5161
5276
  if (url.pathname === "/api/history") {
5162
5277
  const q = url.searchParams;
5163
- const days = Number(q.get("days") ?? 0);
5278
+ const days2 = Number(q.get("days") ?? 0);
5164
5279
  return json(res, 200, listHistory({
5165
5280
  text: (q.get("q") ?? "").slice(0, 200),
5166
5281
  model: q.get("model") || void 0,
5167
5282
  backend: q.get("backend") || void 0,
5168
5283
  state: q.get("state") || void 0,
5169
5284
  group: q.get("group") || void 0,
5170
- sinceMs: days > 0 ? Date.now() - days * 864e5 : void 0,
5285
+ sinceMs: days2 > 0 ? Date.now() - days2 * 864e5 : void 0,
5171
5286
  beforeId: RUN_ID3.test(q.get("before") ?? "") ? q.get("before") : void 0,
5172
5287
  limit: Number(q.get("limit") ?? 30) || 30
5173
5288
  }));
5174
5289
  }
5175
5290
  if (url.pathname === "/api/stats") {
5176
- const days = Number(url.searchParams.get("days") ?? 30);
5177
- return json(res, 200, { ...historyStats(days > 0 ? Date.now() - days * 864e5 : void 0), price: primaryPrice().name });
5291
+ const days2 = Number(url.searchParams.get("days") ?? 30);
5292
+ return json(res, 200, { ...historyStats(days2 > 0 ? Date.now() - days2 * 864e5 : void 0), price: primaryPrice().name });
5178
5293
  }
5179
5294
  const m = /^\/api\/run\/([^/]+)$/.exec(url.pathname);
5180
5295
  if (m) {
@@ -5193,21 +5308,21 @@ function startDash(opts) {
5193
5308
  const port = server.address().port;
5194
5309
  let done;
5195
5310
  const closed = new Promise((r) => done = r);
5196
- let timer;
5311
+ let timer2;
5197
5312
  const close = () => {
5198
- if (timer) clearInterval(timer);
5313
+ if (timer2) clearInterval(timer2);
5199
5314
  server.closeAllConnections?.();
5200
5315
  return new Promise((r) => server.close(() => (done(), r())));
5201
5316
  };
5202
5317
  if (opts.idleMs && opts.idleMs > 0) {
5203
- timer = setInterval(() => Date.now() - lastRequest > opts.idleMs && void close(), Math.min(6e4, opts.idleMs));
5204
- timer.unref();
5318
+ timer2 = setInterval(() => Date.now() - lastRequest > opts.idleMs && void close(), Math.min(6e4, opts.idleMs));
5319
+ timer2.unref();
5205
5320
  }
5206
5321
  resolve2({ port, url: `http://127.0.0.1:${port}/`, closed, close });
5207
5322
  });
5208
5323
  });
5209
5324
  }
5210
- var registryFile = () => path21.join(home(), "dash.json");
5325
+ var registryFile = () => path22.join(home(), "dash.json");
5211
5326
  async function ping(port) {
5212
5327
  try {
5213
5328
  const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
@@ -5218,11 +5333,11 @@ async function ping(port) {
5218
5333
  }
5219
5334
  async function runningDash() {
5220
5335
  try {
5221
- const r = JSON.parse(fs25.readFileSync(registryFile(), "utf8"));
5336
+ const r = JSON.parse(fs26.readFileSync(registryFile(), "utf8"));
5222
5337
  if (isAlive(r.pid) && await ping(r.port)) return { ...r, url: `http://127.0.0.1:${r.port}/` };
5223
5338
  } catch {
5224
5339
  }
5225
- fs25.rmSync(registryFile(), { force: true });
5340
+ fs26.rmSync(registryFile(), { force: true });
5226
5341
  return void 0;
5227
5342
  }
5228
5343
  function openBrowser(url) {
@@ -5237,7 +5352,7 @@ async function dashCommand(o) {
5237
5352
  if (o.stop) {
5238
5353
  if (!existing) throw new UserError("pitroom dash is not running");
5239
5354
  process.kill(existing.pid, "SIGTERM");
5240
- fs25.rmSync(registryFile(), { force: true });
5355
+ fs26.rmSync(registryFile(), { force: true });
5241
5356
  o.log(`stopped pitroom dash (${existing.url})`);
5242
5357
  return 0;
5243
5358
  }
@@ -5274,8 +5389,8 @@ async function dashCommand(o) {
5274
5389
  if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
5275
5390
  dash = await startDash({ port: 0, idleMs: o.idleMs });
5276
5391
  }
5277
- fs25.mkdirSync(home(), { recursive: true });
5278
- fs25.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
5392
+ fs26.mkdirSync(home(), { recursive: true });
5393
+ fs26.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
5279
5394
  const stop = () => void dash.close();
5280
5395
  process.once("SIGINT", stop);
5281
5396
  process.once("SIGTERM", stop);
@@ -5284,7 +5399,7 @@ async function dashCommand(o) {
5284
5399
  if (o.open) openBrowser(dash.url);
5285
5400
  await dash.closed;
5286
5401
  try {
5287
- if (JSON.parse(fs25.readFileSync(registryFile(), "utf8")).pid === process.pid) fs25.rmSync(registryFile(), { force: true });
5402
+ if (JSON.parse(fs26.readFileSync(registryFile(), "utf8")).pid === process.pid) fs26.rmSync(registryFile(), { force: true });
5288
5403
  } catch {
5289
5404
  }
5290
5405
  return 0;
@@ -5296,10 +5411,10 @@ var MAX_BODY = 4e6;
5296
5411
  var MAX_SESSIONS = 64;
5297
5412
  var IDLE_MS = 60 * 6e4;
5298
5413
  var PATH = "/mcp";
5299
- var tokenFile = () => path22.join(home(), "mcp-token");
5414
+ var tokenFile = () => path23.join(home(), "mcp-token");
5300
5415
  var readToken = (file2) => {
5301
5416
  try {
5302
- const kept = fs26.readFileSync(file2, "utf8").trim();
5417
+ const kept = fs27.readFileSync(file2, "utf8").trim();
5303
5418
  return kept.length >= 16 ? kept : void 0;
5304
5419
  } catch {
5305
5420
  return void 0;
@@ -5314,10 +5429,10 @@ function mcpToken() {
5314
5429
  const file2 = tokenFile();
5315
5430
  let token = readToken(file2);
5316
5431
  if (!token) {
5317
- fs26.mkdirSync(path22.dirname(file2), { recursive: true });
5432
+ fs27.mkdirSync(path23.dirname(file2), { recursive: true });
5318
5433
  token = crypto7.randomBytes(32).toString("hex");
5319
5434
  try {
5320
- fs26.writeFileSync(file2, `${token}
5435
+ fs27.writeFileSync(file2, `${token}
5321
5436
  `, { mode: 384, flag: "wx" });
5322
5437
  } catch (e) {
5323
5438
  if (e.code !== "EEXIST") throw e;
@@ -5326,7 +5441,7 @@ function mcpToken() {
5326
5441
  }
5327
5442
  }
5328
5443
  try {
5329
- fs26.chmodSync(file2, 384);
5444
+ fs27.chmodSync(file2, 384);
5330
5445
  } catch {
5331
5446
  }
5332
5447
  return { token, from: file2 };
@@ -5337,6 +5452,15 @@ var sameSecret = (given, token) => {
5337
5452
  const b = crypto7.createHash("sha256").update(token).digest();
5338
5453
  return crypto7.timingSafeEqual(a, b);
5339
5454
  };
5455
+ var idleSince = (k) => Math.max(k.lastSeen, k.closeStream ? k.streamSeen ?? 0 : 0);
5456
+ function drop(sessions, id) {
5457
+ const k = sessions.get(id);
5458
+ if (!k) return;
5459
+ k.closeStream?.();
5460
+ endSession(k.session);
5461
+ sessions.delete(id);
5462
+ }
5463
+ var PING_MS = 25e3;
5340
5464
  function reply(res, status, body, headers = {}) {
5341
5465
  if (res.headersSent) return void res.end();
5342
5466
  const text = body === void 0 ? "" : JSON.stringify(body);
@@ -5362,9 +5486,9 @@ var wantsStream = (parsed) => {
5362
5486
  };
5363
5487
  function evictIdle(sessions) {
5364
5488
  let oldest;
5365
- for (const entry of sessions) if (!entry[1].session.inflight.size && (!oldest || entry[1].lastSeen < oldest[1].lastSeen)) oldest = entry;
5489
+ for (const entry of sessions) if (!entry[1].session.inflight.size && (!oldest || idleSince(entry[1]) < idleSince(oldest[1]))) oldest = entry;
5366
5490
  if (!oldest) return false;
5367
- sessions.delete(oldest[0]);
5491
+ drop(sessions, oldest[0]);
5368
5492
  return true;
5369
5493
  }
5370
5494
  function makeHandler(token, sessions) {
@@ -5378,13 +5502,44 @@ function makeHandler(token, sessions) {
5378
5502
  if (!auth || !sameSecret(auth, token)) return reply(res, 401, failure5(-32001, "a bearer token is needed"), { "WWW-Authenticate": "Bearer" });
5379
5503
  const sid = typeof req.headers["mcp-session-id"] === "string" ? req.headers["mcp-session-id"] : void 0;
5380
5504
  if (req.method === "DELETE") {
5381
- const kept2 = sid ? sessions.get(sid) : void 0;
5382
- if (!kept2) return reply(res, 404, failure5(-32600, "no such session"));
5383
- endSession(kept2.session);
5384
- sessions.delete(sid);
5505
+ if (!sid || !sessions.has(sid)) return reply(res, 404, failure5(-32600, "no such session"));
5506
+ drop(sessions, sid);
5385
5507
  return reply(res, 204);
5386
5508
  }
5387
- if (req.method !== "POST") return reply(res, 405, failure5(-32600, "POST requests only"), { Allow: "POST, DELETE" });
5509
+ if (req.method === "GET") {
5510
+ if (!/text\/event-stream|\*\/\*/.test(req.headers.accept ?? "")) return reply(res, 406, failure5(-32600, "a GET opens the event stream: send Accept: text/event-stream"));
5511
+ if (!sid) return reply(res, 400, failure5(-32600, "Mcp-Session-Id is missing: initialize first"));
5512
+ const kept2 = sessions.get(sid);
5513
+ if (!kept2) return reply(res, 404, failure5(-32600, "no such session: initialize again"));
5514
+ kept2.closeStream?.();
5515
+ res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" });
5516
+ res.write(": open\n\n");
5517
+ const open = () => !res.writableEnded && !res.destroyed;
5518
+ const push = (msg) => {
5519
+ if (!open()) return;
5520
+ kept2.streamSeen = Date.now();
5521
+ res.write(`event: message
5522
+ data: ${JSON.stringify(msg)}
5523
+
5524
+ `);
5525
+ };
5526
+ const ping2 = setInterval(() => open() && res.write(": ping\n\n"), PING_MS);
5527
+ ping2.unref();
5528
+ res.on("error", () => {
5529
+ });
5530
+ kept2.streamSeen = Date.now();
5531
+ const close = () => {
5532
+ clearInterval(ping2);
5533
+ if (kept2.session.push === push) kept2.session.push = void 0;
5534
+ if (kept2.closeStream === close) kept2.closeStream = void 0;
5535
+ if (!res.writableEnded) res.end();
5536
+ };
5537
+ kept2.session.push = push;
5538
+ kept2.closeStream = close;
5539
+ res.on("close", close);
5540
+ return;
5541
+ }
5542
+ if (req.method !== "POST") return reply(res, 405, failure5(-32600, "GET, POST or DELETE"), { Allow: "GET, POST, DELETE" });
5388
5543
  const body = await readBody(req);
5389
5544
  if (body === "too-big") return reply(res, 413, failure5(-32600, `a request body is at most ${MAX_BODY} bytes`));
5390
5545
  let parsed;
@@ -5397,12 +5552,8 @@ function makeHandler(token, sessions) {
5397
5552
  let kept;
5398
5553
  const single = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
5399
5554
  if (single?.method === "initialize") {
5400
- const old = sid ? sessions.get(sid) : void 0;
5401
- if (old) {
5402
- endSession(old.session);
5403
- sessions.delete(sid);
5404
- }
5405
- if (sessions.size >= MAX_SESSIONS && !evictIdle(sessions)) return reply(res, 503, failure5(-32e3, "too many sessions with requests in flight"));
5555
+ if (sid) drop(sessions, sid);
5556
+ if (sessions.size >= MAX_SESSIONS && !evictIdle(sessions)) return reply(res, 503, failure5(-32e3, `${MAX_SESSIONS} sessions are busy with requests: try again when one ends`));
5406
5557
  const id = crypto7.randomUUID();
5407
5558
  kept = { session: newSession(), lastSeen: Date.now() };
5408
5559
  sessions.set(id, kept);
@@ -5444,7 +5595,7 @@ async function serveMcpHttp(opts) {
5444
5595
  });
5445
5596
  });
5446
5597
  const sweep = setInterval(() => {
5447
- for (const [id, k] of sessions) if (Date.now() - k.lastSeen > IDLE_MS && !k.session.inflight.size) sessions.delete(id);
5598
+ for (const [id, k] of [...sessions]) if (Date.now() - idleSince(k) > IDLE_MS && !k.session.inflight.size) drop(sessions, id);
5448
5599
  }, 6e4);
5449
5600
  sweep.unref();
5450
5601
  const port = await new Promise((resolve2, reject) => {
@@ -5464,7 +5615,7 @@ async function serveMcpHttp(opts) {
5464
5615
  process.once("SIGTERM", stop);
5465
5616
  });
5466
5617
  clearInterval(sweep);
5467
- for (const k of sessions.values()) endSession(k.session);
5618
+ for (const id of [...sessions.keys()]) drop(sessions, id);
5468
5619
  server.closeAllConnections();
5469
5620
  await new Promise((resolve2) => server.close(() => resolve2()));
5470
5621
  return 0;
@@ -5472,16 +5623,16 @@ async function serveMcpHttp(opts) {
5472
5623
 
5473
5624
  // src/cli/commands.ts
5474
5625
  import { spawnSync as spawnSync8 } from "node:child_process";
5475
- import fs31 from "node:fs";
5476
- import path27 from "node:path";
5626
+ import fs33 from "node:fs";
5627
+ import path29 from "node:path";
5477
5628
  import { fileURLToPath as fileURLToPath4 } from "node:url";
5478
5629
 
5479
5630
  // src/core/init.ts
5480
- import fs27 from "node:fs";
5481
- import path23 from "node:path";
5631
+ import fs28 from "node:fs";
5632
+ import path24 from "node:path";
5482
5633
  var FREE = /-free$/;
5483
5634
  var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
5484
- var isFound = (binary5) => path23.isAbsolute(binary5) && fs27.existsSync(binary5);
5635
+ var isFound = (binary5) => path24.isAbsolute(binary5) && fs28.existsSync(binary5);
5485
5636
  function planInit(opts = {}) {
5486
5637
  const file2 = configPath();
5487
5638
  const workers = backendIds().map((id) => {
@@ -5527,15 +5678,15 @@ function planInit(opts = {}) {
5527
5678
  if (Object.keys(tiers).length > 1) config.tiers = tiers;
5528
5679
  if (!workers.some((w) => w.found)) blocked = "no worker CLI found: install OpenCode (https://opencode.ai), Codex CLI, Claude Code or Gemini CLI first";
5529
5680
  else if (!blocked && !Object.keys(config).length) notes.push("nothing to propose: the default model and the single worker need no config");
5530
- return { path: file2, exists: fs27.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
5681
+ return { path: file2, exists: fs28.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
5531
5682
  }
5532
5683
  function writeInit(plan, force) {
5533
5684
  if (plan.blocked) throw new UserError(plan.blocked);
5534
5685
  if (!Object.keys(plan.config).length) throw new UserError("nothing to write: the proposal is empty");
5535
- if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${path23.basename(plan.path)}.bak)`);
5536
- fs27.mkdirSync(path23.dirname(plan.path), { recursive: true });
5537
- if (plan.exists) fs27.copyFileSync(plan.path, `${plan.path}.bak`);
5538
- fs27.writeFileSync(plan.path, `${JSON.stringify(plan.config, null, 2)}
5686
+ if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${path24.basename(plan.path)}.bak)`);
5687
+ fs28.mkdirSync(path24.dirname(plan.path), { recursive: true });
5688
+ if (plan.exists) fs28.copyFileSync(plan.path, `${plan.path}.bak`);
5689
+ fs28.writeFileSync(plan.path, `${JSON.stringify(plan.config, null, 2)}
5539
5690
  `);
5540
5691
  return plan.path;
5541
5692
  }
@@ -5556,17 +5707,17 @@ function formatInit(plan, written) {
5556
5707
 
5557
5708
  // src/core/mcp-install.ts
5558
5709
  import { spawnSync as spawnSync7 } from "node:child_process";
5559
- import fs28 from "node:fs";
5710
+ import fs29 from "node:fs";
5560
5711
  import os10 from "node:os";
5561
- import path24 from "node:path";
5712
+ import path25 from "node:path";
5562
5713
  var CLIENT_IDS = ["claude-code", "codex", "gemini", "cursor", "claude-desktop"];
5563
5714
  var NAME = "pitroom";
5564
5715
  function mcpCommand() {
5565
5716
  const launcher = launcherPath();
5566
5717
  if (isPitroomLauncher(launcher)) return { command: launcher, args: ["mcp"] };
5567
- for (const dir of (process.env.PATH ?? "").split(path24.delimiter).filter(Boolean)) {
5568
- const p = path24.join(dir, process.platform === "win32" ? "pitroom.cmd" : "pitroom");
5569
- if (fs28.existsSync(p)) return { command: path24.resolve(p), args: ["mcp"] };
5718
+ for (const dir of (process.env.PATH ?? "").split(path25.delimiter).filter(Boolean)) {
5719
+ const p = path25.join(dir, process.platform === "win32" ? "pitroom.cmd" : "pitroom");
5720
+ if (fs29.existsSync(p)) return { command: path25.resolve(p), args: ["mcp"] };
5570
5721
  }
5571
5722
  return { command: process.execPath, args: [process.argv[1] ?? "pitroom", "mcp"] };
5572
5723
  }
@@ -5582,7 +5733,7 @@ function run(backendId, args) {
5582
5733
  var runnable = (backendId) => run(backendId, ["--version"]).ok;
5583
5734
  var readJson = (file2) => {
5584
5735
  try {
5585
- return JSON.parse(fs28.readFileSync(file2, "utf8"));
5736
+ return JSON.parse(fs29.readFileSync(file2, "utf8"));
5586
5737
  } catch {
5587
5738
  return void 0;
5588
5739
  }
@@ -5592,7 +5743,7 @@ var jsonState = (file2, c) => {
5592
5743
  return entry === void 0 ? "absent" : same(c, entry) ? "same" : "different";
5593
5744
  };
5594
5745
  var home2 = () => os10.homedir();
5595
- var geminiDir = () => path24.join(process.env.GEMINI_CLI_HOME ?? home2(), ".gemini");
5746
+ var geminiDir = () => path25.join(process.env.GEMINI_CLI_HOME ?? home2(), ".gemini");
5596
5747
  function cliClient(id, backend, name, where, o) {
5597
5748
  return {
5598
5749
  id,
@@ -5615,43 +5766,43 @@ function jsonClient(id, name, file2, dir) {
5615
5766
  return {
5616
5767
  id,
5617
5768
  name,
5618
- found: () => fs28.existsSync(dir()),
5769
+ found: () => fs29.existsSync(dir()),
5619
5770
  state: (c) => jsonState(file2(), c),
5620
5771
  add: (c) => {
5621
5772
  let config = {};
5622
- if (fs28.existsSync(file2())) {
5773
+ if (fs29.existsSync(file2())) {
5623
5774
  config = readJson(file2());
5624
5775
  if (!config || typeof config !== "object" || Array.isArray(config)) return { ok: false, message: `${rel()} is not valid JSON: left alone` };
5625
5776
  const backup = `${file2()}.bak-pitroom`;
5626
- if (!fs28.existsSync(backup)) fs28.copyFileSync(file2(), backup);
5777
+ if (!fs29.existsSync(backup)) fs29.copyFileSync(file2(), backup);
5627
5778
  }
5628
5779
  config.mcpServers = { ...config.mcpServers ?? {}, [NAME]: { command: c.command, args: c.args } };
5629
- fs28.mkdirSync(path24.dirname(file2()), { recursive: true });
5780
+ fs29.mkdirSync(path25.dirname(file2()), { recursive: true });
5630
5781
  const tmp = `${file2()}.${process.pid}.tmp`;
5631
- fs28.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
5782
+ fs29.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
5632
5783
  `);
5633
- fs28.renameSync(tmp, file2());
5784
+ fs29.renameSync(tmp, file2());
5634
5785
  return { ok: true, message: rel() };
5635
5786
  },
5636
5787
  remove: () => {
5637
5788
  const config = readJson(file2());
5638
5789
  if (!config?.mcpServers?.[NAME]) return { ok: true, message: rel() };
5639
5790
  delete config.mcpServers[NAME];
5640
- fs28.writeFileSync(file2(), `${JSON.stringify(config, null, 2)}
5791
+ fs29.writeFileSync(file2(), `${JSON.stringify(config, null, 2)}
5641
5792
  `);
5642
5793
  return { ok: true, message: rel() };
5643
5794
  }
5644
5795
  };
5645
5796
  }
5646
5797
  function desktopFile() {
5647
- if (process.platform === "darwin") return path24.join(home2(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
5648
- if (process.platform === "win32") return path24.join(process.env.APPDATA ?? path24.join(home2(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
5649
- return path24.join(process.env.XDG_CONFIG_HOME ?? path24.join(home2(), ".config"), "Claude", "claude_desktop_config.json");
5798
+ if (process.platform === "darwin") return path25.join(home2(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
5799
+ if (process.platform === "win32") return path25.join(process.env.APPDATA ?? path25.join(home2(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
5800
+ return path25.join(process.env.XDG_CONFIG_HOME ?? path25.join(home2(), ".config"), "Claude", "claude_desktop_config.json");
5650
5801
  }
5651
5802
  function codexState(c) {
5652
5803
  let text;
5653
5804
  try {
5654
- text = fs28.readFileSync(path24.join(process.env.CODEX_HOME ?? path24.join(home2(), ".codex"), "config.toml"), "utf8");
5805
+ text = fs29.readFileSync(path25.join(process.env.CODEX_HOME ?? path25.join(home2(), ".codex"), "config.toml"), "utf8");
5655
5806
  } catch {
5656
5807
  return "absent";
5657
5808
  }
@@ -5669,7 +5820,7 @@ function codexState(c) {
5669
5820
  function clients() {
5670
5821
  return [
5671
5822
  cliClient("claude-code", "claude", "Claude Code", "user settings (~/.claude.json)", {
5672
- state: (c) => jsonState(path24.join(home2(), ".claude.json"), c),
5823
+ state: (c) => jsonState(path25.join(home2(), ".claude.json"), c),
5673
5824
  add: (c) => ["mcp", "add", "--scope", "user", NAME, "--", c.command, ...c.args],
5674
5825
  remove: () => ["mcp", "remove", NAME, "--scope", "user"]
5675
5826
  }),
@@ -5679,12 +5830,12 @@ function clients() {
5679
5830
  remove: () => ["mcp", "remove", NAME]
5680
5831
  }),
5681
5832
  cliClient("gemini", "gemini", "Gemini CLI", "user settings (~/.gemini/settings.json)", {
5682
- state: (c) => jsonState(path24.join(geminiDir(), "settings.json"), c),
5833
+ state: (c) => jsonState(path25.join(geminiDir(), "settings.json"), c),
5683
5834
  add: (c) => ["mcp", "add", "--scope", "user", NAME, c.command, ...c.args],
5684
5835
  remove: () => ["mcp", "remove", "--scope", "user", NAME]
5685
5836
  }),
5686
- jsonClient("cursor", "Cursor", () => path24.join(home2(), ".cursor", "mcp.json"), () => path24.join(home2(), ".cursor")),
5687
- jsonClient("claude-desktop", "Claude Desktop", desktopFile, () => path24.dirname(desktopFile()))
5837
+ jsonClient("cursor", "Cursor", () => path25.join(home2(), ".cursor", "mcp.json"), () => path25.join(home2(), ".cursor")),
5838
+ jsonClient("claude-desktop", "Claude Desktop", desktopFile, () => path25.dirname(desktopFile()))
5688
5839
  ];
5689
5840
  }
5690
5841
  function attempt2(f) {
@@ -5755,16 +5906,16 @@ var parseClients = (value) => {
5755
5906
 
5756
5907
  // src/core/plan-status.ts
5757
5908
  import crypto8 from "node:crypto";
5758
- import fs29 from "node:fs";
5759
- import path25 from "node:path";
5909
+ import fs30 from "node:fs";
5910
+ import path26 from "node:path";
5760
5911
  function notesFile(plan) {
5761
5912
  const id = crypto8.createHash("sha1").update(plan.file).digest("hex").slice(0, 12);
5762
- return path25.join(home(), "plans", id, "notes.md");
5913
+ return path26.join(home(), "plans", id, "notes.md");
5763
5914
  }
5764
5915
  function readNotes(plan) {
5765
5916
  const f = notesFile(plan);
5766
- if (!fs29.existsSync(f)) return [];
5767
- return fs29.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
5917
+ if (!fs30.existsSync(f)) return [];
5918
+ return fs30.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
5768
5919
  const m = /^(\d{4}-\d\d-\d\d \d\d:\d\d) (.*)$/.exec(l);
5769
5920
  return m ? { at: m[1], text: m[2] } : { at: "", text: l };
5770
5921
  });
@@ -5772,11 +5923,11 @@ function readNotes(plan) {
5772
5923
  function addNote(planFile, text) {
5773
5924
  const plan = loadPlan(planFile);
5774
5925
  const f = notesFile(plan);
5775
- fs29.mkdirSync(path25.dirname(f), { recursive: true });
5776
- if (!fs29.existsSync(f)) fs29.writeFileSync(f, `# plan: ${plan.file}
5926
+ fs30.mkdirSync(path26.dirname(f), { recursive: true });
5927
+ if (!fs30.existsSync(f)) fs30.writeFileSync(f, `# plan: ${plan.file}
5777
5928
  `);
5778
5929
  const at = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
5779
- fs29.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
5930
+ fs30.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
5780
5931
  `);
5781
5932
  return f;
5782
5933
  }
@@ -5842,10 +5993,73 @@ ${rulings}`,
5842
5993
  ].join("\n");
5843
5994
  }
5844
5995
 
5845
- // src/core/review.ts
5996
+ // src/core/cache.ts
5846
5997
  import crypto9 from "node:crypto";
5847
- import fs30 from "node:fs";
5848
- import path26 from "node:path";
5998
+ import fs31 from "node:fs";
5999
+ import path27 from "node:path";
6000
+ var SCAN2 = 500;
6001
+ function cacheKey(o) {
6002
+ if (o.verify || o.mode !== "read" || o.continueFrom || o.web || o.plan || o.review || o.audit || o.auditRate === 1) return void 0;
6003
+ if (!(effective().cacheDays.value > 0)) return void 0;
6004
+ const dir = path27.resolve(o.dir);
6005
+ const root = repoRoot(dir);
6006
+ if (!root) return void 0;
6007
+ try {
6008
+ const tree = snapshotTree(root);
6009
+ const head = commitOf(root, "HEAD") ?? "";
6010
+ const files = o.files.map((f) => {
6011
+ const abs = path27.resolve(f);
6012
+ return [abs, crypto9.createHash("sha256").update(fs31.readFileSync(abs)).digest("hex")];
6013
+ });
6014
+ const question = o.task.replace(/\s+/g, " ").trim();
6015
+ const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
6016
+ const where = !o.inPlace && wantSnapshot(readIn2, root, dir) ? "snapshot" : "project";
6017
+ const key = crypto9.createHash("sha256").update(JSON.stringify([question, path27.relative(root, dir), files, where])).digest("hex");
6018
+ return { key, state: `${head}:${tree}` };
6019
+ } catch {
6020
+ return void 0;
6021
+ }
6022
+ }
6023
+ function heldUp(m) {
6024
+ if (m.state !== "done" || m.mode !== "read" || m.reviewOf || m.auditOf) return false;
6025
+ if (m.refs && m.refs.invalid.length) return false;
6026
+ if (m.warnings.some((w) => w.startsWith("READ-ONLY VIOLATION"))) return false;
6027
+ if (m.audit && (isActive(m.audit.state) || m.audit.verdict === "disagree" || m.audit.verdict === "partial")) return false;
6028
+ try {
6029
+ return readSummary(m).trim() !== "";
6030
+ } catch {
6031
+ return false;
6032
+ }
6033
+ }
6034
+ function findCached(k, now = Date.now()) {
6035
+ const maxAge = effective().cacheDays.value * 864e5;
6036
+ let best;
6037
+ for (const id of listRunIds().slice(-SCAN2)) {
6038
+ let m;
6039
+ try {
6040
+ m = readMeta(id);
6041
+ } catch {
6042
+ continue;
6043
+ }
6044
+ if (m.cache?.key !== k.key || m.cache.state !== k.state) continue;
6045
+ const at = Date.parse(m.endedAt ?? m.startedAt);
6046
+ if (now - at > maxAge || best && at <= best.at || !heldUp(m)) continue;
6047
+ best = { m, at };
6048
+ }
6049
+ return best?.m;
6050
+ }
6051
+ function ago(iso, now = Date.now()) {
6052
+ const s = Math.max(0, (now - Date.parse(iso)) / 1e3);
6053
+ if (s < 3600) return `${Math.max(1, Math.round(s / 60))} min ago`;
6054
+ if (s < 86400) return `${Math.round(s / 3600)} h ago`;
6055
+ return `${Math.round(s / 86400)} d ago`;
6056
+ }
6057
+ var cachedNote = (m) => `pitroom \u27F2 cached answer \xB7 the same question on the same code as run ${m.id} (${ago(m.endedAt ?? m.startedAt)}) \xB7 no worker ran \xB7 --fresh asks one again`;
6058
+
6059
+ // src/core/review.ts
6060
+ import crypto10 from "node:crypto";
6061
+ import fs32 from "node:fs";
6062
+ import path28 from "node:path";
5849
6063
  var TEMPLATE2 = { task: "task-reviewer", fix: "re-review", range: "code-reviewer" };
5850
6064
  var section = (title, body) => `## ${title}
5851
6065
 
@@ -5863,7 +6077,7 @@ function ancestors(m) {
5863
6077
  function briefOf(m) {
5864
6078
  const root = [m, ...ancestors(m)].at(-1);
5865
6079
  const f = runFile(root.id, "brief.md");
5866
- return fs30.existsSync(f) ? fs30.readFileSync(f, "utf8") : root.task;
6080
+ return fs32.existsSync(f) ? fs32.readFileSync(f, "utf8") : root.task;
5867
6081
  }
5868
6082
  function latestReview(ids) {
5869
6083
  for (const id of listRunIds().reverse()) {
@@ -5877,7 +6091,7 @@ function latestReview(ids) {
5877
6091
  }
5878
6092
  function diffOf(m, from = m.baseTree) {
5879
6093
  const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
5880
- if (where && fs30.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
6094
+ if (where && fs32.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
5881
6095
  if (from !== m.baseTree) {
5882
6096
  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);
5883
6097
  }
@@ -5891,7 +6105,7 @@ function runReview(id) {
5891
6105
  if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
5892
6106
  const chain = ancestors(m);
5893
6107
  const previous = latestReview(chain.map((a) => a.id));
5894
- const dir = m.mode === "isolate" && m.worktree && fs30.existsSync(m.worktree) ? m.cwd : m.dir;
6108
+ const dir = m.mode === "isolate" && m.worktree && fs32.existsSync(m.worktree) ? m.cwd : m.dir;
5895
6109
  const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
5896
6110
  const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
5897
6111
  if (!previous) {
@@ -5964,12 +6178,25 @@ function pickReviewer(job) {
5964
6178
  }
5965
6179
  function writePackage(job) {
5966
6180
  const g = gitDir(job.dir);
5967
- if (!g) throw new UserError(`not a git repository: ${job.dir}`);
5968
- const file2 = path26.join(g, "pitroom", `review-${crypto9.randomBytes(4).toString("hex")}.md`);
5969
- fs30.mkdirSync(path26.dirname(file2), { recursive: true });
5970
- fs30.writeFileSync(file2, job.package);
6181
+ const root = repoRoot(job.dir);
6182
+ if (!g || !root) throw new UserError(`not a git repository: ${job.dir}`);
6183
+ const name = `review-${crypto10.randomBytes(4).toString("hex")}.md`;
6184
+ const inside2 = !path28.relative(fs32.realpathSync(root), fs32.realpathSync(g)).startsWith("..");
6185
+ const file2 = inside2 ? path28.join(g, "pitroom", name) : path28.join(root, ".pitroom", name);
6186
+ if (!inside2) ignoreInGit(root, "/.pitroom/");
6187
+ fs32.mkdirSync(path28.dirname(file2), { recursive: true });
6188
+ fs32.writeFileSync(file2, job.package);
5971
6189
  return file2;
5972
6190
  }
6191
+ function ignoreInGit(root, pattern) {
6192
+ const exclude = path28.resolve(root, git(root, ["rev-parse", "--git-path", "info/exclude"]).stdout.trim());
6193
+ const text = fs32.existsSync(exclude) ? fs32.readFileSync(exclude, "utf8") : "";
6194
+ if (text.split(/\r?\n/).includes(pattern)) return;
6195
+ fs32.mkdirSync(path28.dirname(exclude), { recursive: true });
6196
+ fs32.appendFileSync(exclude, `${text && !text.endsWith("\n") ? "\n" : ""}# Pitroom review packages in worktrees (removed after each review)
6197
+ ${pattern}
6198
+ `);
6199
+ }
5973
6200
 
5974
6201
  // src/core/models.ts
5975
6202
  var base = (model) => (model ?? "").split("#")[0];
@@ -6086,7 +6313,15 @@ warning: ${w}`).join("")
6086
6313
  async function cmdRun(p) {
6087
6314
  const opts = { ...runOptions(p, readTask(p)), plan: planStep(p) };
6088
6315
  if (!opts.task.trim() && !opts.plan) throw new UserError('no task given (pitroom "find where X is handled")');
6089
- return launch(p, prepareRun(opts));
6316
+ const cache = cacheKey(opts);
6317
+ const hit = cache && !has(p, "fresh") ? findCached(cache) : void 0;
6318
+ if (hit) {
6319
+ console.log(has(p, "json") ? JSON.stringify({ ...hit, cached: true }, null, 2) : `${cachedNote(hit)}
6320
+
6321
+ ${formatReport(hit)}`);
6322
+ return exitCodeFor(hit);
6323
+ }
6324
+ return launch(p, prepareRun({ ...opts, cache }));
6090
6325
  }
6091
6326
  async function cmdReview(p) {
6092
6327
  const range = flag(p, "range");
@@ -6114,10 +6349,10 @@ async function cmdReview(p) {
6114
6349
  writeMeta(meta);
6115
6350
  }
6116
6351
  } catch (e) {
6117
- fs31.rmSync(packageFile, { force: true });
6352
+ fs33.rmSync(packageFile, { force: true });
6118
6353
  throw e;
6119
6354
  }
6120
- fs31.writeFileSync(runFile(meta.id, "package.md"), job.package);
6355
+ fs33.writeFileSync(runFile(meta.id, "package.md"), job.package);
6121
6356
  return launch(p, meta);
6122
6357
  }
6123
6358
  async function cmdAudit(p) {
@@ -6185,7 +6420,7 @@ function cmdPlan(p) {
6185
6420
  }
6186
6421
  function cmdCrew(p) {
6187
6422
  const file2 = flag(p, "task-file");
6188
- const tasks = (file2 ? fs31.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
6423
+ const tasks = (file2 ? fs33.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
6189
6424
  if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
6190
6425
  if (has(p, "write") && tasks.length > 1) {
6191
6426
  throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
@@ -6306,7 +6541,7 @@ ${[fmt2(head2), ...rows2.map(fmt2)].join("\n")}`);
6306
6541
  const { rows, total } = listHistory({ text: p.positional.join(" "), model: flag(p, "model"), state: flag(p, "state"), group: flag(p, "group"), sinceMs: since2, limit });
6307
6542
  if (has(p, "json")) return console.log(JSON.stringify({ total, rows }, null, 2)), 0;
6308
6543
  if (!rows.length) return console.log(total ? "nothing on this page" : "no matching runs"), 0;
6309
- const body = rows.map((r) => [r.id, when(r.startedAt), r.state, `${r.backend}${r.model ? ` (${r.model.split("/").pop()})` : ""}`, secs(r.seconds), r.task.length > 60 ? `${r.task.slice(0, 59)}\u2026` : r.task]);
6544
+ const body = rows.map((r) => [r.id, when(r.startedAt), r.verifyFailed ? "verify failed" : r.state, `${r.backend}${r.model ? ` (${r.model.split("/").pop()})` : ""}`, secs(r.seconds), r.task.length > 60 ? `${r.task.slice(0, 59)}\u2026` : r.task]);
6310
6545
  const head = ["RUN", "WHEN", "STATE", "WORKER", "TIME", "TASK"];
6311
6546
  const widths = head.map((h, i) => Math.max(h.length, ...body.map((r) => r[i].length)));
6312
6547
  const fmt = (r, header = false) => r.map((c, i) => {
@@ -6388,7 +6623,7 @@ function sinceMs(s) {
6388
6623
  if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
6389
6624
  return Date.now() - Number(m[1]) * 864e5;
6390
6625
  }
6391
- var readStdin = () => process.stdin.isTTY ? "" : fs31.readFileSync(0, "utf8");
6626
+ var readStdin = () => process.stdin.isTTY ? "" : fs33.readFileSync(0, "utf8");
6392
6627
  function cmdStatusline(p) {
6393
6628
  const input = readStdin();
6394
6629
  const lines = [];
@@ -6428,8 +6663,8 @@ function cmdModels(p) {
6428
6663
  return 0;
6429
6664
  }
6430
6665
  function cmdHookStart() {
6431
- const script = path27.join(path27.dirname(path27.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
6432
- if (fs31.existsSync(script)) spawnSync8(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
6666
+ const script = path29.join(path29.dirname(path29.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
6667
+ if (fs33.existsSync(script)) spawnSync8(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
6433
6668
  return 0;
6434
6669
  }
6435
6670
  function cmdInit(p) {
@@ -6469,8 +6704,8 @@ function cmdSavings(p) {
6469
6704
  est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
6470
6705
  const out = flag(p, "card");
6471
6706
  if (out) {
6472
- fs31.writeFileSync(out, card(t, period));
6473
- console.log(`card written to ${path27.resolve(out)}`);
6707
+ fs33.writeFileSync(out, card(t, period));
6708
+ console.log(`card written to ${path29.resolve(out)}`);
6474
6709
  }
6475
6710
  if (has(p, "badge")) console.log(`![pitroom](${badgeUrl(t)})`);
6476
6711
  return 0;
@@ -6512,7 +6747,7 @@ function cmdConfig(p) {
6512
6747
  console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
6513
6748
  return 0;
6514
6749
  }
6515
- console.log(`config file: ${configPath()}${fs31.existsSync(configPath()) ? "" : " (not present)"}`);
6750
+ console.log(`config file: ${configPath()}${fs33.existsSync(configPath()) ? "" : " (not present)"}`);
6516
6751
  for (const [key, s] of Object.entries(eff)) {
6517
6752
  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);
6518
6753
  console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
@@ -6521,22 +6756,22 @@ function cmdConfig(p) {
6521
6756
  return 0;
6522
6757
  }
6523
6758
  function cmdClean(p) {
6524
- const days = Number(flag(p, "days") ?? 14);
6525
- const cutoff = Date.now() - days * 864e5;
6759
+ const days2 = Number(flag(p, "days") ?? 14);
6760
+ const cutoff = Date.now() - days2 * 864e5;
6526
6761
  const old = listRunIds().filter((id) => {
6527
6762
  const m = freshMeta(id);
6528
6763
  const pendingPatch = m.mode === "isolate" && !m.applied && !m.discarded && m.changes?.length;
6529
6764
  return TERMINAL.includes(m.state) && Date.parse(m.startedAt) < cutoff && !pendingPatch;
6530
6765
  });
6531
6766
  if (!has(p, "yes")) {
6532
- console.log(`${old.length} run(s) older than ${days} days would be removed (unapplied isolate patches are kept). Re-run with --yes.`);
6767
+ console.log(`${old.length} run(s) older than ${days2} days would be removed (unapplied isolate patches are kept). Re-run with --yes.`);
6533
6768
  return 0;
6534
6769
  }
6535
6770
  for (const id of old) {
6536
6771
  const m = readMeta(id);
6537
6772
  if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
6538
6773
  recordRun(m);
6539
- fs31.rmSync(runDir(id), { recursive: true, force: true });
6774
+ fs33.rmSync(runDir(id), { recursive: true, force: true });
6540
6775
  }
6541
6776
  const snapshots = prune(Date.now(), 0);
6542
6777
  console.log(`removed ${old.length} run(s)${snapshots ? ` and ${snapshots} read snapshot${snapshots === 1 ? "" : "s"}` : ""}; the history and the savings ledger are kept`);
@@ -6547,9 +6782,9 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
6547
6782
 
6548
6783
  // src/core/doctor.ts
6549
6784
  import { spawnSync as spawnSync9 } from "node:child_process";
6550
- import fs32 from "node:fs";
6785
+ import fs34 from "node:fs";
6551
6786
  import os11 from "node:os";
6552
- import path28 from "node:path";
6787
+ import path30 from "node:path";
6553
6788
  var MARK = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
6554
6789
  var NEXT = [
6555
6790
  { when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
@@ -6559,10 +6794,10 @@ var NEXT = [
6559
6794
  { when: /Gemini CLI is not signed in|IneligibleTierError/, command: "export GEMINI_API_KEY=\u2026", why: "a Google AI Studio key for the Gemini worker" }
6560
6795
  ];
6561
6796
  function firstNodeOnPath() {
6562
- for (const dir of (process.env.PATH ?? "").split(path28.delimiter).filter(Boolean)) {
6563
- const file2 = path28.join(dir, process.platform === "win32" ? "node.exe" : "node");
6797
+ for (const dir of (process.env.PATH ?? "").split(path30.delimiter).filter(Boolean)) {
6798
+ const file2 = path30.join(dir, process.platform === "win32" ? "node.exe" : "node");
6564
6799
  try {
6565
- if (!fs32.statSync(file2).isFile()) continue;
6800
+ if (!fs34.statSync(file2).isFile()) continue;
6566
6801
  } catch {
6567
6802
  continue;
6568
6803
  }
@@ -6589,12 +6824,12 @@ function doctor5(probe) {
6589
6824
  const addAll = (list2) => list2.forEach((c) => add(c.level, c.message));
6590
6825
  add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
6591
6826
  const onPath = firstNodeOnPath();
6592
- if (onPath && onPath.version && tooOld(onPath.version) && path28.resolve(onPath.path) !== path28.resolve(process.execPath)) {
6827
+ if (onPath && onPath.version && tooOld(onPath.version) && path30.resolve(onPath.path) !== path30.resolve(process.execPath)) {
6593
6828
  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)`);
6594
6829
  }
6595
6830
  add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
6596
6831
  const cfg = loadConfig();
6597
- add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs32.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
6832
+ add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs34.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
6598
6833
  for (const w of cfg.warnings) add("warn", w);
6599
6834
  if (process.platform !== "win32") {
6600
6835
  const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
@@ -6747,19 +6982,19 @@ function skillChecks() {
6747
6982
  else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
6748
6983
  }
6749
6984
  if (geminiExtensionInstalled()) {
6750
- const linked = installedSkills().some((i) => i.base.includes(`${path28.sep}.agents${path28.sep}`) && i.names.length);
6985
+ const linked = installedSkills().some((i) => i.base.includes(`${path30.sep}.agents${path30.sep}`) && i.names.length);
6751
6986
  checks.push(
6752
6987
  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)" }
6753
6988
  );
6754
6989
  }
6755
6990
  if (viaPlugin) {
6756
- const linked = installedSkills().some((i) => i.base.includes(`${path28.sep}.claude${path28.sep}`) && i.names.length);
6991
+ const linked = installedSkills().some((i) => i.base.includes(`${path30.sep}.claude${path30.sep}`) && i.names.length);
6757
6992
  checks.push(
6758
6993
  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)" }
6759
6994
  );
6760
6995
  }
6761
6996
  const launcher = launcherPath();
6762
- if (fs32.existsSync(launcher)) {
6997
+ if (fs34.existsSync(launcher)) {
6763
6998
  const r = spawnSync9(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
6764
6999
  checks.push(
6765
7000
  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)}` }
@@ -6771,12 +7006,12 @@ function skillChecks() {
6771
7006
  }
6772
7007
  function geminiExtensionInstalled() {
6773
7008
  const home3 = process.env.GEMINI_CLI_HOME ?? os11.homedir();
6774
- return fs32.existsSync(path28.join(home3, ".gemini", "extensions", "pitroom"));
7009
+ return fs34.existsSync(path30.join(home3, ".gemini", "extensions", "pitroom"));
6775
7010
  }
6776
7011
  function pluginInstalled() {
6777
7012
  try {
6778
- const f = path28.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
6779
- const plugins = JSON.parse(fs32.readFileSync(f, "utf8")).plugins ?? {};
7013
+ const f = path30.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
7014
+ const plugins = JSON.parse(fs34.readFileSync(f, "utf8")).plugins ?? {};
6780
7015
  return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
6781
7016
  } catch {
6782
7017
  return false;
@@ -6793,24 +7028,24 @@ function superpowersActive() {
6793
7028
  for (const p of openCodePlugins()) {
6794
7029
  if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
6795
7030
  }
6796
- for (const base2 of [path28.join(os11.homedir(), ".agents", "skills"), path28.join(os11.homedir(), ".claude", "skills")]) {
6797
- const dir = path28.join(base2, "using-superpowers");
6798
- if (fs32.existsSync(path28.join(dir, "SKILL.md"))) found.push(dir);
7031
+ for (const base2 of [path30.join(os11.homedir(), ".agents", "skills"), path30.join(os11.homedir(), ".claude", "skills")]) {
7032
+ const dir = path30.join(base2, "using-superpowers");
7033
+ if (fs34.existsSync(path30.join(dir, "SKILL.md"))) found.push(dir);
6799
7034
  }
6800
7035
  return found;
6801
7036
  }
6802
7037
  function claudePlugins() {
6803
7038
  try {
6804
- const f = path28.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
6805
- return Object.keys(JSON.parse(fs32.readFileSync(f, "utf8")).plugins ?? {});
7039
+ const f = path30.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
7040
+ return Object.keys(JSON.parse(fs34.readFileSync(f, "utf8")).plugins ?? {});
6806
7041
  } catch {
6807
7042
  return [];
6808
7043
  }
6809
7044
  }
6810
7045
  function claudePluginEnabled(key) {
6811
7046
  try {
6812
- const f = path28.join(os11.homedir(), ".claude", "settings.json");
6813
- return JSON.parse(fs32.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
7047
+ const f = path30.join(os11.homedir(), ".claude", "settings.json");
7048
+ return JSON.parse(fs34.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
6814
7049
  } catch {
6815
7050
  return true;
6816
7051
  }
@@ -6819,7 +7054,7 @@ function codexPlugins() {
6819
7054
  const plugins = /* @__PURE__ */ new Map();
6820
7055
  let text;
6821
7056
  try {
6822
- text = fs32.readFileSync(path28.join(process.env.CODEX_HOME ?? path28.join(os11.homedir(), ".codex"), "config.toml"), "utf8");
7057
+ text = fs34.readFileSync(path30.join(process.env.CODEX_HOME ?? path30.join(os11.homedir(), ".codex"), "config.toml"), "utf8");
6823
7058
  } catch {
6824
7059
  return plugins;
6825
7060
  }
@@ -6837,11 +7072,11 @@ function codexPlugins() {
6837
7072
  return plugins;
6838
7073
  }
6839
7074
  function openCodePlugins() {
6840
- const dir = path28.join(process.env.XDG_CONFIG_HOME ?? path28.join(os11.homedir(), ".config"), "opencode");
7075
+ const dir = path30.join(process.env.XDG_CONFIG_HOME ?? path30.join(os11.homedir(), ".config"), "opencode");
6841
7076
  const plugins = [];
6842
7077
  for (const name of ["opencode.json", "opencode.jsonc"]) {
6843
7078
  try {
6844
- const text = fs32.readFileSync(path28.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
7079
+ const text = fs34.readFileSync(path30.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
6845
7080
  const list2 = JSON.parse(text).plugin;
6846
7081
  if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
6847
7082
  } catch {
@@ -6849,7 +7084,7 @@ function openCodePlugins() {
6849
7084
  }
6850
7085
  for (const folder of ["plugin", "plugins"]) {
6851
7086
  try {
6852
- for (const f of fs32.readdirSync(path28.join(dir, folder))) plugins.push(path28.join(dir, folder, f));
7087
+ for (const f of fs34.readdirSync(path30.join(dir, folder))) plugins.push(path30.join(dir, folder, f));
6853
7088
  } catch {
6854
7089
  }
6855
7090
  }
@@ -6929,6 +7164,7 @@ Run options
6929
7164
  --in-place a read run reads the directory itself, not a clean snapshot (default: a snapshot without secret-looking
6930
7165
  files when the directory has any; config "readIn": auto | snapshot | project)
6931
7166
  --audit have another worker re-check this read run's answer; --no-audit skips it (default: the config's "audit")
7167
+ --fresh ask a worker even when the same read question was answered on the same code (config "cacheDays", default 7)
6932
7168
  --effort LEVEL reasoning effort for the worker: low, medium, high, xhigh, \u2026 (model#level)
6933
7169
  --plan PLAN with --step N: implement Task N of a plan (-i or -w); the task text is your notes
6934
7170
  --step N the plan task for --plan
@@ -6950,9 +7186,9 @@ Exit codes: 0 ok \xB7 1 worker failed \xB7 2 usage \xB7 3 refused/setup \xB7 4 t
6950
7186
  5 read-only violation \xB7 6 verify failed \xB7 75 still running (wait again)
6951
7187
  Workers: ${backendIds().join(", ")} (targets: "opencode", "opencode:provider/model", or a bare model)
6952
7188
  Env: PITROOM_WORKER, PITROOM_MODEL, PITROOM_FALLBACK="t1,t2", PITROOM_TIMEOUT, PITROOM_MAX_PARALLEL,
6953
- PITROOM_PRIMARY=sonnet|opus|haiku|gpt-5, PITROOM_PRICE="in,out", PITROOM_HOME, PITROOM_CONFIG,
7189
+ PITROOM_PRIMARY=sonnet|opus|haiku|gpt-5, PITROOM_PRICE="in,out", PITROOM_HOME, PITROOM_CONFIG, PITROOM_CACHE_DAYS,
6954
7190
  PITROOM_<WORKER>_BIN
6955
- Config: ~/.config/pitroom/config.json (worker, fallback, models, tiers, timeout, primary, price, link, web, maxParallel)`;
7191
+ Config: ~/.config/pitroom/config.json (worker, fallback, models, tiers, timeout, primary, price, link, web, maxParallel, audit, readIn, cacheDays)`;
6956
7192
  var COMMANDS = {
6957
7193
  run: cmdRun,
6958
7194
  crew: cmdCrew,
@@ -6965,7 +7201,7 @@ var COMMANDS = {
6965
7201
  if (port !== void 0 && !(/^\d+$/.test(port) && Number(port) <= 65535)) throw new UserError("--port takes a number from 0 to 65535 (0 picks a free one)");
6966
7202
  const dir = flag(p, "dir");
6967
7203
  if (dir !== void 0) {
6968
- if (!fs33.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) throw new UserError(`-d ${dir}: no such directory`);
7204
+ if (!fs35.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) throw new UserError(`-d ${dir}: no such directory`);
6969
7205
  process.chdir(dir);
6970
7206
  process.env.PWD = process.cwd();
6971
7207
  }