pitroom 0.16.0 → 0.18.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 fs36 from "node:fs";
5
5
 
6
6
  // src/core/errors.ts
7
7
  var UserError = class extends Error {
@@ -555,16 +555,33 @@ var codex = {
555
555
 
556
556
  // src/backends/gemini/index.ts
557
557
  import { spawnSync as spawnSync3 } from "node:child_process";
558
- import fs5 from "node:fs";
558
+ import fs6 from "node:fs";
559
559
  import os5 from "node:os";
560
560
  import path5 from "node:path";
561
561
  import { fileURLToPath } from "node:url";
562
562
 
563
563
  // src/core/store.ts
564
- import fs4 from "node:fs";
564
+ import fs5 from "node:fs";
565
565
  import os4 from "node:os";
566
566
  import path4 from "node:path";
567
567
  import crypto from "node:crypto";
568
+
569
+ // src/core/fs-atomic.ts
570
+ import fs4 from "node:fs";
571
+ var BUSY = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
572
+ function renameOver(tmp, file2) {
573
+ for (let attempt3 = 0; ; attempt3++) {
574
+ try {
575
+ fs4.renameSync(tmp, file2);
576
+ return;
577
+ } catch (e) {
578
+ if (process.platform !== "win32" || !BUSY.has(e.code ?? "") || attempt3 >= 40) throw e;
579
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
580
+ }
581
+ }
582
+ }
583
+
584
+ // src/core/store.ts
568
585
  var TERMINAL = ["done", "failed", "timeout", "stopped"];
569
586
  var isActive = (s) => !TERMINAL.includes(s);
570
587
  function home() {
@@ -591,11 +608,11 @@ function useArchive(hooks) {
591
608
  archive = { meta: hooks.meta, id: hooks.id };
592
609
  }
593
610
  function writeMeta(meta) {
594
- fs4.mkdirSync(runDir(meta.id), { recursive: true });
611
+ fs5.mkdirSync(runDir(meta.id), { recursive: true });
595
612
  const file2 = runFile(meta.id, "meta.json");
596
613
  const tmp = `${file2}.${process.pid}.tmp`;
597
- fs4.writeFileSync(tmp, JSON.stringify(meta, null, 2));
598
- fs4.renameSync(tmp, file2);
614
+ fs5.writeFileSync(tmp, JSON.stringify(meta, null, 2));
615
+ renameOver(tmp, file2);
599
616
  if (TERMINAL.includes(meta.state)) {
600
617
  try {
601
618
  onFinished?.(meta);
@@ -605,11 +622,11 @@ function writeMeta(meta) {
605
622
  }
606
623
  function readMeta(id) {
607
624
  const file2 = runFile(id, "meta.json");
608
- if (!fs4.existsSync(file2)) {
625
+ if (!fs5.existsSync(file2)) {
609
626
  const kept = archive?.meta(id);
610
627
  if (kept) return upgrade(kept);
611
628
  }
612
- return upgrade(JSON.parse(fs4.readFileSync(file2, "utf8")));
629
+ return upgrade(JSON.parse(fs5.readFileSync(file2, "utf8")));
613
630
  }
614
631
  function upgrade(raw) {
615
632
  if (!raw.worker) raw.worker = raw.model ? { backend: "opencode", model: raw.model } : { backend: "opencode" };
@@ -622,8 +639,8 @@ function upgrade(raw) {
622
639
  return raw;
623
640
  }
624
641
  function listRunIds() {
625
- if (!fs4.existsSync(runsDir())) return [];
626
- return fs4.readdirSync(runsDir()).filter((d) => fs4.existsSync(runFile(d, "meta.json"))).sort();
642
+ if (!fs5.existsSync(runsDir())) return [];
643
+ return fs5.readdirSync(runsDir()).filter((d) => fs5.existsSync(runFile(d, "meta.json"))).sort();
627
644
  }
628
645
  function resolveRun(ref) {
629
646
  const ids = listRunIds();
@@ -649,13 +666,16 @@ function isAlive(pid) {
649
666
  return e.code === "EPERM";
650
667
  }
651
668
  }
669
+ var STOP_FILE = "stop-requested";
670
+ var requestStop = (id) => fs5.writeFileSync(runFile(id, STOP_FILE), "");
652
671
  function freshMeta(id) {
653
672
  const meta = readMeta(id);
654
673
  if (!TERMINAL.includes(meta.state) && meta.pid && !isAlive(meta.pid)) {
655
674
  const latest = readMeta(id);
656
675
  if (TERMINAL.includes(latest.state)) return latest;
657
- latest.state = latest.stopRequested ? "stopped" : "failed";
658
- if (!latest.stopRequested) latest.error ??= "worker process exited unexpectedly";
676
+ const stopped = latest.stopRequested || fs5.existsSync(runFile(id, STOP_FILE));
677
+ latest.state = stopped ? "stopped" : "failed";
678
+ if (!stopped) latest.error ??= "worker process exited unexpectedly";
659
679
  latest.endedAt ??= (/* @__PURE__ */ new Date()).toISOString();
660
680
  writeMeta(latest);
661
681
  return latest;
@@ -787,22 +807,22 @@ var policyDir = () => path5.resolve(path5.dirname(fileURLToPath(import.meta.url)
787
807
  function workerHome() {
788
808
  const root = path5.join(home(), "gemini-home");
789
809
  const dir = path5.join(root, ".gemini");
790
- const base2 = JSON.parse(fs5.readFileSync(path5.join(policyDir(), "worker-settings.json"), "utf8"));
810
+ const base2 = JSON.parse(fs6.readFileSync(path5.join(policyDir(), "worker-settings.json"), "utf8"));
791
811
  const auth = settings().security?.auth;
792
812
  const body = `${JSON.stringify(auth ? { ...base2, security: { ...base2.security, auth } } : base2, null, 2)}
793
813
  `;
794
814
  const file2 = path5.join(dir, "settings.json");
795
815
  try {
796
- if (fs5.readFileSync(file2, "utf8") === body) return root;
816
+ if (fs6.readFileSync(file2, "utf8") === body) return root;
797
817
  } catch {
798
818
  }
799
- fs5.mkdirSync(dir, { recursive: true });
800
- fs5.writeFileSync(file2, body);
819
+ fs6.mkdirSync(dir, { recursive: true });
820
+ fs6.writeFileSync(file2, body);
801
821
  for (const name of ["oauth_creds.json", "google_accounts.json"]) {
802
822
  const from = path5.join(geminiHome(), name);
803
823
  const to = path5.join(dir, name);
804
824
  try {
805
- if (fs5.existsSync(from) && !fs5.existsSync(to)) fs5.symlinkSync(from, to);
825
+ if (fs6.existsSync(from) && !fs6.existsSync(to)) fs6.symlinkSync(from, to);
806
826
  } catch {
807
827
  }
808
828
  }
@@ -829,7 +849,7 @@ function invocation3(req) {
829
849
  const { command, prefix } = resolveCommand(binary3());
830
850
  const env = { GEMINI_CLI_HOME: workerHome() };
831
851
  const trusted = path5.join(geminiHome(), "trustedFolders.json");
832
- if (fs5.existsSync(trusted)) env.GEMINI_CLI_TRUSTED_FOLDERS_PATH = trusted;
852
+ if (fs6.existsSync(trusted)) env.GEMINI_CLI_TRUSTED_FOLDERS_PATH = trusted;
833
853
  return { command, args: [...prefix, ...args], env };
834
854
  }
835
855
  function classify3(message) {
@@ -854,7 +874,7 @@ function geminiHome() {
854
874
  }
855
875
  function settings() {
856
876
  try {
857
- return JSON.parse(fs5.readFileSync(path5.join(geminiHome(), "settings.json"), "utf8"));
877
+ return JSON.parse(fs6.readFileSync(path5.join(geminiHome(), "settings.json"), "utf8"));
858
878
  } catch {
859
879
  return {};
860
880
  }
@@ -873,7 +893,7 @@ function doctor3({ models, hasFallback }) {
873
893
  const checks = [{ level: "ok", message: `Gemini CLI ${version.out.trim()} at ${binary3()}` }];
874
894
  const key = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
875
895
  const vertex = process.env.GOOGLE_GENAI_USE_VERTEXAI === "true" || process.env.GOOGLE_GENAI_USE_VERTEXAI === "1";
876
- const signedIn = fs5.existsSync(path5.join(geminiHome(), "oauth_creds.json"));
896
+ const signedIn = fs6.existsSync(path5.join(geminiHome(), "oauth_creds.json"));
877
897
  const type = settings().security?.auth?.selectedType;
878
898
  if (key || vertex) {
879
899
  checks.push({ level: "ok", message: `Gemini CLI: ${key ? "an API key is set" : "Vertex AI is set up"}` });
@@ -1032,7 +1052,7 @@ function describe2(input) {
1032
1052
  }
1033
1053
 
1034
1054
  // src/backends/opencode/profiles.ts
1035
- import fs6 from "node:fs";
1055
+ import fs7 from "node:fs";
1036
1056
  import os6 from "node:os";
1037
1057
  import path6 from "node:path";
1038
1058
  var AGENT = { read: "pitroom-read", write: "pitroom-write" };
@@ -1143,7 +1163,7 @@ function scratchDirs() {
1143
1163
  }
1144
1164
  function realpath(p) {
1145
1165
  try {
1146
- return fs6.realpathSync(p);
1166
+ return fs7.realpathSync(p);
1147
1167
  } catch {
1148
1168
  return p;
1149
1169
  }
@@ -1401,10 +1421,10 @@ function getBackend(id) {
1401
1421
  }
1402
1422
 
1403
1423
  // src/cli/args.ts
1404
- import fs8 from "node:fs";
1424
+ import fs9 from "node:fs";
1405
1425
 
1406
1426
  // src/core/config.ts
1407
- import fs7 from "node:fs";
1427
+ import fs8 from "node:fs";
1408
1428
  import os7 from "node:os";
1409
1429
  import path7 from "node:path";
1410
1430
  var SCHEMA = {
@@ -1420,7 +1440,8 @@ var SCHEMA = {
1420
1440
  tiers: "record",
1421
1441
  costs: "numbers",
1422
1442
  audit: "number",
1423
- readIn: "string"
1443
+ readIn: "string",
1444
+ cacheDays: "number"
1424
1445
  };
1425
1446
  function configPath() {
1426
1447
  if (process.env.PITROOM_CONFIG) return path7.resolve(process.env.PITROOM_CONFIG);
@@ -1433,10 +1454,10 @@ function loadConfig() {
1433
1454
  const file2 = configPath();
1434
1455
  const config = {};
1435
1456
  const warnings = [];
1436
- if (fs7.existsSync(file2)) {
1457
+ if (fs8.existsSync(file2)) {
1437
1458
  let raw;
1438
1459
  try {
1439
- raw = JSON.parse(fs7.readFileSync(file2, "utf8"));
1460
+ raw = JSON.parse(fs8.readFileSync(file2, "utf8"));
1440
1461
  } catch (e) {
1441
1462
  warnings.push(`${file2}: invalid JSON (${e.message}); ignored`);
1442
1463
  }
@@ -1478,6 +1499,10 @@ var rate = (v) => {
1478
1499
  const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
1479
1500
  return typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.min(n, 1) : void 0;
1480
1501
  };
1502
+ var days = (v) => {
1503
+ const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
1504
+ return typeof n === "number" && Number.isInteger(n) && n >= 0 ? n : void 0;
1505
+ };
1481
1506
  var readIn = (v) => v === "auto" || v === "snapshot" || v === "project" ? v : void 0;
1482
1507
  var list = (s) => s?.split(",").map((x) => x.trim()).filter(Boolean);
1483
1508
  function setting(flag2, env, conf, fallback) {
@@ -1504,7 +1529,8 @@ function effective(flags = {}) {
1504
1529
  tiers: setting(void 0, void 0, c.tiers, {}),
1505
1530
  costs: setting(void 0, void 0, c.costs, {}),
1506
1531
  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")
1532
+ readIn: setting(void 0, readIn(e.PITROOM_READ_IN), readIn(c.readIn), "auto"),
1533
+ cacheDays: setting(void 0, days(e.PITROOM_CACHE_DAYS), days(c.cacheDays), 7)
1508
1534
  };
1509
1535
  }
1510
1536
 
@@ -1581,6 +1607,7 @@ var BOOL_FLAGS = {
1581
1607
  "--stop": "stop",
1582
1608
  "--open": "open",
1583
1609
  "--serve": "serve",
1610
+ "--fresh": "fresh",
1584
1611
  "--http": "http",
1585
1612
  "-h": "help",
1586
1613
  "--help": "help",
@@ -1637,9 +1664,9 @@ function exitCodeFor(meta) {
1637
1664
  }
1638
1665
  function readTask(p) {
1639
1666
  const file2 = flag(p, "task-file");
1640
- if (file2) return fs8.readFileSync(file2, "utf8");
1667
+ if (file2) return fs9.readFileSync(file2, "utf8");
1641
1668
  const words = p.positional;
1642
- if (words.length === 1 && words[0] === "-") return fs8.readFileSync(0, "utf8");
1669
+ if (words.length === 1 && words[0] === "-") return fs9.readFileSync(0, "utf8");
1643
1670
  return words.join(" ");
1644
1671
  }
1645
1672
  function runOptions(p, task) {
@@ -1688,13 +1715,13 @@ import readline from "node:readline";
1688
1715
 
1689
1716
  // src/core/run.ts
1690
1717
  import { spawn as spawn2, spawnSync as spawnSync6 } from "node:child_process";
1691
- import fs24 from "node:fs";
1718
+ import fs25 from "node:fs";
1692
1719
  import path20 from "node:path";
1693
1720
 
1694
1721
  // src/vcs/git.ts
1695
1722
  import { spawnSync as spawnSync5 } from "node:child_process";
1696
1723
  import crypto2 from "node:crypto";
1697
- import fs9 from "node:fs";
1724
+ import fs10 from "node:fs";
1698
1725
  import os8 from "node:os";
1699
1726
  import path8 from "node:path";
1700
1727
  var SAFE = [
@@ -1726,26 +1753,33 @@ function must(cwd, args, env) {
1726
1753
  function gitAvailable() {
1727
1754
  return git(process.cwd(), ["--version"]).code === 0;
1728
1755
  }
1756
+ function canonical(dir) {
1757
+ try {
1758
+ return fs10.realpathSync.native(dir);
1759
+ } catch {
1760
+ return dir;
1761
+ }
1762
+ }
1729
1763
  function repoRoot(dir) {
1730
1764
  const r = git(dir, ["rev-parse", "--show-toplevel"]);
1731
- return r.code === 0 ? path8.resolve(r.stdout.trim()) : void 0;
1765
+ return r.code === 0 ? canonical(path8.resolve(r.stdout.trim())) : void 0;
1732
1766
  }
1733
- function snapshotTree(root, exclude = [], drop = []) {
1767
+ function snapshotTree(root, exclude = [], drop2 = []) {
1734
1768
  const tmp = path8.join(os8.tmpdir(), `pitroom-index-${process.pid}-${crypto2.randomBytes(4).toString("hex")}`);
1735
1769
  const real = path8.resolve(root, must(root, ["rev-parse", "--git-path", "index"]).trim());
1736
1770
  const env = { ...process.env, GIT_INDEX_FILE: tmp };
1737
1771
  try {
1738
- if (fs9.existsSync(real)) fs9.copyFileSync(real, tmp);
1772
+ if (fs10.existsSync(real)) fs10.copyFileSync(real, tmp);
1739
1773
  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" });
1774
+ must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`), ...drop2.map((p) => `:(top,literal,exclude)${p}`)], env);
1775
+ if (drop2.length) {
1776
+ const r = spawnSync5("git", ["update-index", "--force-remove", "-z", "--stdin"], { cwd: root, env, input: `${drop2.join("\0")}\0`, encoding: "utf8" });
1743
1777
  if (r.status !== 0) throw new Error(`git update-index failed: ${r.stderr.trim()}`);
1744
1778
  }
1745
1779
  return must(root, ["write-tree"], env).trim();
1746
1780
  } finally {
1747
- fs9.rmSync(tmp, { force: true });
1748
- fs9.rmSync(`${tmp}.lock`, { force: true });
1781
+ fs10.rmSync(tmp, { force: true });
1782
+ fs10.rmSync(`${tmp}.lock`, { force: true });
1749
1783
  }
1750
1784
  }
1751
1785
  var DIFF = ["diff", "--no-ext-diff", "--no-textconv", "--no-renames"];
@@ -1766,9 +1800,9 @@ function diffTrees(root, a, b) {
1766
1800
  }
1767
1801
  function createIsolatedCopy(root, tree, dest) {
1768
1802
  const objects = path8.join(path8.resolve(root, must(root, ["rev-parse", "--git-common-dir"]).trim()), "objects");
1769
- fs9.mkdirSync(path8.dirname(dest), { recursive: true });
1803
+ fs10.mkdirSync(path8.dirname(dest), { recursive: true });
1770
1804
  must(path8.dirname(dest), ["init", "--quiet", dest]);
1771
- fs9.writeFileSync(path8.join(dest, ".git", "objects", "info", "alternates"), `${objects}
1805
+ fs10.writeFileSync(path8.join(dest, ".git", "objects", "info", "alternates"), `${objects}
1772
1806
  `);
1773
1807
  must(dest, ["read-tree", tree]);
1774
1808
  must(dest, ["checkout-index", "--all", "--force"]);
@@ -1776,16 +1810,16 @@ function createIsolatedCopy(root, tree, dest) {
1776
1810
  function removeIsolatedCopy(dest, ownedBy) {
1777
1811
  const rel = path8.relative(ownedBy, dest);
1778
1812
  if (!rel || rel.startsWith("..") || path8.isAbsolute(rel)) throw new Error(`refusing to remove ${dest}`);
1779
- fs9.rmSync(dest, { recursive: true, force: true });
1813
+ fs10.rmSync(dest, { recursive: true, force: true });
1780
1814
  }
1781
1815
  function linkIntoWorktree(root, worktree, rels) {
1782
1816
  const linked = [];
1783
1817
  for (const rel of rels) {
1784
1818
  const src = path8.join(root, rel);
1785
1819
  const dst = path8.join(worktree, rel);
1786
- if (!fs9.existsSync(src) || fs9.existsSync(dst)) continue;
1787
- fs9.mkdirSync(path8.dirname(dst), { recursive: true });
1788
- fs9.symlinkSync(src, dst, process.platform === "win32" ? "junction" : void 0);
1820
+ if (!fs10.existsSync(src) || fs10.existsSync(dst)) continue;
1821
+ fs10.mkdirSync(path8.dirname(dst), { recursive: true });
1822
+ fs10.symlinkSync(src, dst, process.platform === "win32" ? "junction" : void 0);
1789
1823
  linked.push(rel);
1790
1824
  }
1791
1825
  return linked;
@@ -1804,6 +1838,7 @@ ${must(root, [...DIFF, "-U10", a, b])}`;
1804
1838
  }
1805
1839
  function rangeDiff(root, a, b) {
1806
1840
  const range = `${a}..${b}`;
1841
+ const since2 = git(root, ["merge-base", a, b]).code === 0 ? `${a}...${b}` : range;
1807
1842
  const log2 = must(root, ["log", "--oneline", "--no-decorate", range]).trim();
1808
1843
  return [
1809
1844
  `## COMMITS
@@ -1811,10 +1846,10 @@ function rangeDiff(root, a, b) {
1811
1846
  ${log2 || "(none)"}`,
1812
1847
  `## FILES CHANGED
1813
1848
 
1814
- ${must(root, [...DIFF, "--stat", range]).trim() || "(none)"}`,
1849
+ ${must(root, [...DIFF, "--stat", since2]).trim() || "(none)"}`,
1815
1850
  `## DIFF
1816
1851
 
1817
- ${must(root, [...DIFF, "-U10", range])}`
1852
+ ${must(root, [...DIFF, "-U10", since2])}`
1818
1853
  ].join("\n\n");
1819
1854
  }
1820
1855
  function commitOf(root, ref) {
@@ -1890,10 +1925,10 @@ function resolveChain(flags = {}) {
1890
1925
 
1891
1926
  // src/core/process.ts
1892
1927
  import { spawn } from "node:child_process";
1893
- import fs11 from "node:fs";
1928
+ import fs12 from "node:fs";
1894
1929
 
1895
1930
  // src/vcs/guard.ts
1896
- import fs10 from "node:fs";
1931
+ import fs11 from "node:fs";
1897
1932
  import path9 from "node:path";
1898
1933
  var VERSION = 1;
1899
1934
  var SHIM = `#!/bin/sh
@@ -1976,8 +2011,8 @@ function findRealGit(skip) {
1976
2011
  if (path9.resolve(dir) === skip) continue;
1977
2012
  const p = path9.join(dir, "git");
1978
2013
  try {
1979
- fs10.accessSync(p, fs10.constants.X_OK);
1980
- if (fs10.statSync(p).isFile()) return p;
2014
+ fs11.accessSync(p, fs11.constants.X_OK);
2015
+ if (fs11.statSync(p).isFile()) return p;
1981
2016
  } catch {
1982
2017
  }
1983
2018
  }
@@ -1992,11 +2027,11 @@ exit 1
1992
2027
  `;
1993
2028
  var hooksDir = () => path9.join(home(), "git-hooks", `v${HOOK_VERSION}`);
1994
2029
  function writeIfChanged(file2, content) {
1995
- if (fs10.existsSync(file2) && fs10.readFileSync(file2, "utf8") === content) return;
1996
- fs10.mkdirSync(path9.dirname(file2), { recursive: true });
2030
+ if (fs11.existsSync(file2) && fs11.readFileSync(file2, "utf8") === content) return;
2031
+ fs11.mkdirSync(path9.dirname(file2), { recursive: true });
1997
2032
  const tmp = `${file2}.${process.pid}.tmp`;
1998
- fs10.writeFileSync(tmp, content, { mode: 493 });
1999
- fs10.renameSync(tmp, file2);
2033
+ fs11.writeFileSync(tmp, content, { mode: 493 });
2034
+ fs11.renameSync(tmp, file2);
2000
2035
  }
2001
2036
  function withGitConfig(env, entries) {
2002
2037
  const out = { ...env };
@@ -2032,8 +2067,8 @@ function guardEnv(env) {
2032
2067
 
2033
2068
  // src/core/process.ts
2034
2069
  async function spawnWorker(inv, opts) {
2035
- const out = fs11.openSync(opts.stdoutFile, "w");
2036
- const err = fs11.openSync(opts.stderrFile, "w");
2070
+ const out = fs12.openSync(opts.stdoutFile, "w");
2071
+ const err = fs12.openSync(opts.stderrFile, "w");
2037
2072
  const res = { code: null, timedOut: false, stopped: false };
2038
2073
  const child = spawn(inv.command, inv.args, {
2039
2074
  cwd: opts.cwd,
@@ -2049,24 +2084,24 @@ async function spawnWorker(inv, opts) {
2049
2084
  process.once("SIGINT", stop);
2050
2085
  process.once("SIGTERM", stop);
2051
2086
  let killer;
2052
- const timer = setTimeout(() => {
2087
+ const timer2 = setTimeout(() => {
2053
2088
  res.timedOut = true;
2054
2089
  child.kill("SIGTERM");
2055
2090
  killer = setTimeout(() => child.kill("SIGKILL"), 1e4);
2056
2091
  }, opts.timeoutSec * 1e3);
2057
2092
  res.code = await new Promise((resolve2) => {
2058
2093
  child.on("error", (e) => {
2059
- res.spawnError = e.code === "ENOENT" ? `${inv.command} not found` : e.message;
2094
+ res.spawnError = e.code === "ENOENT" ? fs12.existsSync(opts.cwd) ? `${inv.command} not found` : `the working directory ${opts.cwd} does not exist` : e.message;
2060
2095
  resolve2(127);
2061
2096
  });
2062
2097
  child.on("close", (c) => resolve2(c));
2063
2098
  });
2064
- clearTimeout(timer);
2099
+ clearTimeout(timer2);
2065
2100
  if (killer) clearTimeout(killer);
2066
2101
  process.off("SIGINT", stop);
2067
2102
  process.off("SIGTERM", stop);
2068
- fs11.closeSync(out);
2069
- fs11.closeSync(err);
2103
+ fs12.closeSync(out);
2104
+ fs12.closeSync(err);
2070
2105
  return res;
2071
2106
  }
2072
2107
 
@@ -2098,7 +2133,7 @@ function parseAudit(text) {
2098
2133
  }
2099
2134
 
2100
2135
  // src/core/cooldown.ts
2101
- import fs12 from "node:fs";
2136
+ import fs13 from "node:fs";
2102
2137
  import path10 from "node:path";
2103
2138
  var MAX_MS = 24 * 36e5;
2104
2139
  var OVERLOAD_MS = 20 * 6e4;
@@ -2126,18 +2161,18 @@ function cooldownMs(message) {
2126
2161
  }
2127
2162
  function read() {
2128
2163
  try {
2129
- const raw = JSON.parse(fs12.readFileSync(file(), "utf8"));
2164
+ const raw = JSON.parse(fs13.readFileSync(file(), "utf8"));
2130
2165
  return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
2131
2166
  } catch {
2132
2167
  return {};
2133
2168
  }
2134
2169
  }
2135
2170
  function write(all) {
2136
- fs12.mkdirSync(path10.dirname(file()), { recursive: true });
2171
+ fs13.mkdirSync(path10.dirname(file()), { recursive: true });
2137
2172
  const tmp = `${file()}.${process.pid}.tmp`;
2138
- fs12.writeFileSync(tmp, `${JSON.stringify(all, null, 2)}
2173
+ fs13.writeFileSync(tmp, `${JSON.stringify(all, null, 2)}
2139
2174
  `);
2140
- fs12.renameSync(tmp, file());
2175
+ renameOver(tmp, file());
2141
2176
  }
2142
2177
  function activeCooldowns(now = Date.now()) {
2143
2178
  return Object.fromEntries(Object.entries(read()).filter(([, c]) => Date.parse(c.until) > now));
@@ -2165,13 +2200,13 @@ function untilText(c, now = Date.now()) {
2165
2200
  }
2166
2201
 
2167
2202
  // src/core/snapshot.ts
2168
- import fs14 from "node:fs";
2203
+ import fs15 from "node:fs";
2169
2204
  import path12 from "node:path";
2170
2205
  import crypto3 from "node:crypto";
2171
2206
 
2172
2207
  // src/core/secrets.ts
2173
2208
  import { execFileSync } from "node:child_process";
2174
- import fs13 from "node:fs";
2209
+ import fs14 from "node:fs";
2175
2210
  import path11 from "node:path";
2176
2211
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
2177
2212
  var MAX_VISITED = 2e4;
@@ -2189,7 +2224,7 @@ function findSecretFiles(dir) {
2189
2224
  if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
2190
2225
  let entries;
2191
2226
  try {
2192
- entries = fs13.readdirSync(d, { withFileTypes: true });
2227
+ entries = fs14.readdirSync(d, { withFileTypes: true });
2193
2228
  } catch {
2194
2229
  return;
2195
2230
  }
@@ -2198,7 +2233,7 @@ function findSecretFiles(dir) {
2198
2233
  if (e.isDirectory()) {
2199
2234
  if (!SKIP_DIRS.has(e.name)) walk(path11.join(d, e.name), depth + 1);
2200
2235
  } else if (e.isFile() && looksSecret(e.name)) {
2201
- found.push(path11.relative(dir, path11.join(d, e.name)));
2236
+ found.push(slash(path11.relative(dir, path11.join(d, e.name))));
2202
2237
  }
2203
2238
  }
2204
2239
  };
@@ -2208,7 +2243,7 @@ function findSecretFiles(dir) {
2208
2243
  function findSecretFilesInTree(root, dir) {
2209
2244
  try {
2210
2245
  const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
2211
- return out.split("\0").filter((f) => f && looksSecret(path11.basename(f)) && !path11.relative(dir, path11.join(root, f)).startsWith("..")).map((f) => path11.relative(dir, path11.join(root, f))).sort();
2246
+ return out.split("\0").filter((f) => f && looksSecret(path11.basename(f)) && !path11.relative(dir, path11.join(root, f)).startsWith("..")).map((f) => slash(path11.relative(dir, path11.join(root, f)))).sort();
2212
2247
  } catch {
2213
2248
  return [];
2214
2249
  }
@@ -2220,6 +2255,7 @@ function secretWarning(files, mode) {
2220
2255
  const fix = mode === "isolate" ? "add them to .gitignore, or" : "use an isolated copy (-i, git-ignored files are left out) or a clean checkout, or";
2221
2256
  return `secret-looking files ${where} (${shown}): the worker is told not to read them, but nothing stops it, and its model may be hosted by a third party; ${fix} set PITROOM_NO_SECRET_WARNING=1 to silence this`;
2222
2257
  }
2258
+ var slash = (p) => p.split(path11.sep).join("/");
2223
2259
 
2224
2260
  // src/core/snapshot.ts
2225
2261
  var READ_IN = ["auto", "snapshot", "project"];
@@ -2234,10 +2270,10 @@ function wantSnapshot(readIn2, root, dir) {
2234
2270
  return readIn2 === "snapshot" || secrets.length ? { secrets } : void 0;
2235
2271
  }
2236
2272
  function readSnapshot(root) {
2237
- const drop = findSecretFilesInTree(root, root);
2238
- const tree = snapshotTree(root, [], drop);
2273
+ const drop2 = findSecretFilesInTree(root, root);
2274
+ const tree = snapshotTree(root, [], drop2);
2239
2275
  const dest = path12.join(snapshotsDir(), tree);
2240
- if (fs14.existsSync(path12.join(dest, ".git"))) {
2276
+ if (fs15.existsSync(path12.join(dest, ".git"))) {
2241
2277
  touch(dest);
2242
2278
  return { dir: dest, tree, created: false };
2243
2279
  }
@@ -2246,9 +2282,9 @@ function readSnapshot(root) {
2246
2282
  createIsolatedCopy(root, tree, tmp);
2247
2283
  readOnly(tmp);
2248
2284
  try {
2249
- fs14.renameSync(tmp, dest);
2285
+ fs15.renameSync(tmp, dest);
2250
2286
  } catch (e) {
2251
- if (!fs14.existsSync(path12.join(dest, ".git"))) throw e;
2287
+ if (!fs15.existsSync(path12.join(dest, ".git"))) throw e;
2252
2288
  removeIsolatedCopy(tmp, snapshotsDir());
2253
2289
  }
2254
2290
  } catch (e) {
@@ -2264,17 +2300,17 @@ function readSnapshot(root) {
2264
2300
  function touch(dir) {
2265
2301
  try {
2266
2302
  const now = /* @__PURE__ */ new Date();
2267
- fs14.utimesSync(dir, now, now);
2303
+ fs15.utimesSync(dir, now, now);
2268
2304
  } catch {
2269
2305
  }
2270
2306
  }
2271
2307
  function readOnly(dir) {
2272
2308
  const walk = (d) => {
2273
- for (const e of fs14.readdirSync(d, { withFileTypes: true })) {
2309
+ for (const e of fs15.readdirSync(d, { withFileTypes: true })) {
2274
2310
  if (e.name === ".git") continue;
2275
2311
  const p = path12.join(d, e.name);
2276
2312
  if (e.isDirectory()) walk(p);
2277
- else if (e.isFile()) fs14.chmodSync(p, fs14.statSync(p).mode & ~146);
2313
+ else if (e.isFile()) fs15.chmodSync(p, fs15.statSync(p).mode & ~146);
2278
2314
  }
2279
2315
  };
2280
2316
  try {
@@ -2285,7 +2321,7 @@ function readOnly(dir) {
2285
2321
  function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWEST) {
2286
2322
  let names;
2287
2323
  try {
2288
- names = fs14.readdirSync(snapshotsDir());
2324
+ names = fs15.readdirSync(snapshotsDir());
2289
2325
  } catch {
2290
2326
  return 0;
2291
2327
  }
@@ -2300,7 +2336,7 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
2300
2336
  const entries = names.map((n) => {
2301
2337
  const dir = path12.join(snapshotsDir(), n);
2302
2338
  try {
2303
- return { n, dir, age: now - fs14.statSync(dir).mtimeMs };
2339
+ return { n, dir, age: now - fs15.statSync(dir).mtimeMs };
2304
2340
  } catch {
2305
2341
  return void 0;
2306
2342
  }
@@ -2324,10 +2360,10 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
2324
2360
  }
2325
2361
  function makeWritable(dir) {
2326
2362
  const walk = (d) => {
2327
- for (const e of fs14.readdirSync(d, { withFileTypes: true })) {
2363
+ for (const e of fs15.readdirSync(d, { withFileTypes: true })) {
2328
2364
  const p = path12.join(d, e.name);
2329
2365
  if (e.isDirectory()) walk(p);
2330
- else if (e.isFile()) fs14.chmodSync(p, fs14.statSync(p).mode | 128);
2366
+ else if (e.isFile()) fs15.chmodSync(p, fs15.statSync(p).mode | 128);
2331
2367
  }
2332
2368
  };
2333
2369
  try {
@@ -2338,7 +2374,7 @@ function makeWritable(dir) {
2338
2374
  function backToProject(text, snapshotDir, root) {
2339
2375
  let real = snapshotDir;
2340
2376
  try {
2341
- real = fs14.realpathSync(snapshotDir);
2377
+ real = fs15.realpathSync(snapshotDir);
2342
2378
  } catch {
2343
2379
  }
2344
2380
  let out = text;
@@ -2377,6 +2413,9 @@ function auditTask(meta, answer) {
2377
2413
  return [
2378
2414
  "You are auditing another worker's answer to a read-only question about this project. Do not take the answer on trust:",
2379
2415
  "check its key claims yourself against the files here (open the cited files and lines, search for what it says exists or is missing).",
2416
+ "Check the answer against the QUESTION, not only its own claims: every condition the question sets (a directory or scope,",
2417
+ 'what to leave out, "all", "only", "exactly", a count) must hold. Valid references do not make an answer right:',
2418
+ "for a list, look for items that are missing and items that do not belong; for a count, count again yourself.",
2380
2419
  "Do not edit anything and do not run anything that changes files.",
2381
2420
  "",
2382
2421
  "QUESTION:",
@@ -2391,7 +2430,7 @@ function auditTask(meta, answer) {
2391
2430
  "DISPUTED:",
2392
2431
  "- <the claim> \u2014 <what is actually true, with path:line>",
2393
2432
  "",
2394
- "AGREE: every key claim you checked holds. PARTIAL: some are wrong or could not be verified. DISAGREE: the main conclusion is wrong.",
2433
+ "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
2434
  'Under DISPUTED list only claims you checked and found wrong or unsupported; write "- (none)" when there are none.'
2396
2435
  ].join("\n");
2397
2436
  }
@@ -2416,7 +2455,7 @@ function auditBadge(meta) {
2416
2455
  }
2417
2456
 
2418
2457
  // src/core/plan.ts
2419
- import fs15 from "node:fs";
2458
+ import fs16 from "node:fs";
2420
2459
  import path13 from "node:path";
2421
2460
  var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
2422
2461
  var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
@@ -2465,8 +2504,8 @@ function parsePlan(text, file2 = "") {
2465
2504
  }
2466
2505
  function loadPlan(file2) {
2467
2506
  const abs = path13.resolve(file2);
2468
- if (!fs15.existsSync(abs) || !fs15.statSync(abs).isFile()) throw new UserError(`plan not found: ${file2}`);
2469
- const plan = parsePlan(fs15.readFileSync(abs, "utf8"), fs15.realpathSync(abs));
2507
+ if (!fs16.existsSync(abs) || !fs16.statSync(abs).isFile()) throw new UserError(`plan not found: ${file2}`);
2508
+ const plan = parsePlan(fs16.readFileSync(abs, "utf8"), fs16.realpathSync(abs));
2470
2509
  if (!plan.tasks.length) throw new UserError(`${file2} has no "Task N" headings (see pitroom-writing-plans)`);
2471
2510
  return plan;
2472
2511
  }
@@ -2488,11 +2527,11 @@ function brief(plan, task) {
2488
2527
  var planName = (file2) => path13.basename(file2).replace(/\.md$/i, "");
2489
2528
 
2490
2529
  // src/core/templates.ts
2491
- import fs17 from "node:fs";
2530
+ import fs18 from "node:fs";
2492
2531
  import path15 from "node:path";
2493
2532
 
2494
2533
  // src/core/install.ts
2495
- import fs16 from "node:fs";
2534
+ import fs17 from "node:fs";
2496
2535
  import os9 from "node:os";
2497
2536
  import path14 from "node:path";
2498
2537
  import { fileURLToPath as fileURLToPath2 } from "node:url";
@@ -2502,18 +2541,30 @@ function packageRoot() {
2502
2541
  }
2503
2542
  function skillNames(root = packageRoot()) {
2504
2543
  const dir = path14.join(root, "skills");
2505
- if (!fs16.existsSync(dir)) return [];
2506
- return fs16.readdirSync(dir).filter((d) => fs16.existsSync(path14.join(dir, d, "SKILL.md"))).sort();
2544
+ if (!fs17.existsSync(dir)) return [];
2545
+ return fs17.readdirSync(dir).filter((d) => fs17.existsSync(path14.join(dir, d, "SKILL.md"))).sort();
2507
2546
  }
2508
2547
  function skillTargets() {
2509
2548
  const home3 = os9.homedir();
2510
2549
  const targets = [path14.join(home3, ".agents", "skills")];
2511
- if (fs16.existsSync(path14.join(home3, ".claude"))) targets.push(path14.join(home3, ".claude", "skills"));
2550
+ if (fs17.existsSync(path14.join(home3, ".claude"))) targets.push(path14.join(home3, ".claude", "skills"));
2512
2551
  return targets;
2513
2552
  }
2514
- var launcherPath = () => path14.join(os9.homedir(), ".local", "bin", "pitroom");
2553
+ var launcherPath = () => path14.join(os9.homedir(), ".local", "bin", process.platform === "win32" ? "pitroom.cmd" : "pitroom");
2515
2554
  var LAUNCHER_MARK = "# pitroom launcher";
2516
2555
  function launcherScript(bundle) {
2556
+ if (process.platform === "win32") {
2557
+ const q2 = (v) => v.replace(/%/g, "%%");
2558
+ return `@echo off\r
2559
+ rem ${LAUNCHER_MARK} (created by \`pitroom install\`; \`pitroom uninstall\` removes it)\r
2560
+ if defined PITROOM_NODE (\r
2561
+ "%PITROOM_NODE%" "${q2(bundle)}" %*\r
2562
+ ) else (\r
2563
+ "${q2(process.execPath)}" "${q2(bundle)}" %*\r
2564
+ )\r
2565
+ exit /b %ERRORLEVEL%\r
2566
+ `;
2567
+ }
2517
2568
  const q = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
2518
2569
  return `#!/bin/sh
2519
2570
  ${LAUNCHER_MARK} (created by \`pitroom install\`; \`pitroom uninstall\` removes it)
@@ -2529,7 +2580,7 @@ exit 127
2529
2580
  function isOurLauncher(file2, root) {
2530
2581
  if (linksInto(file2, root)) return true;
2531
2582
  try {
2532
- const s = fs16.readFileSync(file2, "utf8");
2583
+ const s = fs17.readFileSync(file2, "utf8");
2533
2584
  return s.includes(LAUNCHER_MARK) && s.includes(root);
2534
2585
  } catch {
2535
2586
  return false;
@@ -2537,61 +2588,61 @@ function isOurLauncher(file2, root) {
2537
2588
  }
2538
2589
  function isPitroomLauncher(file2) {
2539
2590
  try {
2540
- return fs16.readFileSync(file2, "utf8").includes(LAUNCHER_MARK);
2591
+ return fs17.readFileSync(file2, "utf8").includes(LAUNCHER_MARK);
2541
2592
  } catch {
2542
2593
  return false;
2543
2594
  }
2544
2595
  }
2545
2596
  function placeLauncher(bundle, root, force) {
2546
2597
  const dest = launcherPath();
2547
- fs16.mkdirSync(path14.dirname(dest), { recursive: true });
2548
- const exists = fs16.lstatSync(dest, { throwIfNoEntry: false });
2598
+ fs17.mkdirSync(path14.dirname(dest), { recursive: true });
2599
+ const exists = fs17.lstatSync(dest, { throwIfNoEntry: false });
2549
2600
  if (exists && !isOurLauncher(dest, root)) {
2550
2601
  if (!force) return `! ${dest} exists and is not Pitroom's launcher; kept (use --force to back it up and replace)`;
2551
- fs16.renameSync(dest, `${dest}.bak-${Date.now()}`);
2602
+ fs17.renameSync(dest, `${dest}.bak-${Date.now()}`);
2552
2603
  } else if (exists) {
2553
- fs16.rmSync(dest, { force: true });
2604
+ fs17.rmSync(dest, { force: true });
2554
2605
  }
2555
- fs16.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
2606
+ fs17.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
2556
2607
  return `\u2714 ${dest} \u2192 launcher for ${bundle} (Node 22.13+)`;
2557
2608
  }
2558
2609
  function linksInto(link, dir) {
2559
- const st = fs16.lstatSync(link, { throwIfNoEntry: false });
2610
+ const st = fs17.lstatSync(link, { throwIfNoEntry: false });
2560
2611
  if (!st?.isSymbolicLink()) return false;
2561
- const target = path14.resolve(path14.dirname(link), fs16.readlinkSync(link));
2612
+ const target = path14.resolve(path14.dirname(link), fs17.readlinkSync(link));
2562
2613
  const rel = path14.relative(dir, target);
2563
2614
  return !rel.startsWith("..") && !path14.isAbsolute(rel);
2564
2615
  }
2565
2616
  function place(src, dest, opts) {
2566
- fs16.mkdirSync(path14.dirname(dest), { recursive: true });
2567
- const st = fs16.lstatSync(dest, { throwIfNoEntry: false });
2617
+ fs17.mkdirSync(path14.dirname(dest), { recursive: true });
2618
+ const st = fs17.lstatSync(dest, { throwIfNoEntry: false });
2568
2619
  if (st && !st.isSymbolicLink()) {
2569
2620
  if (!opts.force) return `! ${dest} exists and is not a link; kept (use --force to back it up and replace)`;
2570
- fs16.renameSync(dest, `${dest}.bak-${Date.now()}`);
2621
+ fs17.renameSync(dest, `${dest}.bak-${Date.now()}`);
2571
2622
  } else if (st) {
2572
- fs16.unlinkSync(dest);
2623
+ fs17.unlinkSync(dest);
2573
2624
  }
2574
- if (opts.copy) fs16.cpSync(src, dest, { recursive: true });
2575
- else fs16.symlinkSync(src, dest, process.platform === "win32" ? "junction" : fs16.statSync(src).isDirectory() ? "dir" : "file");
2625
+ if (opts.copy) fs17.cpSync(src, dest, { recursive: true });
2626
+ else fs17.symlinkSync(src, dest, process.platform === "win32" ? "junction" : fs17.statSync(src).isDirectory() ? "dir" : "file");
2576
2627
  return `\u2714 ${dest} \u2192 ${opts.copy ? "copied" : src}`;
2577
2628
  }
2578
2629
  function install(opts) {
2579
2630
  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")}`);
2631
+ const skills2 = opts.skills === false ? [] : skillNames(root);
2632
+ if (opts.skills !== false && !skills2.length) throw new Error(`no skills found under ${path14.join(root, "skills")}`);
2582
2633
  const out = [];
2583
2634
  for (const base2 of opts.skills === false ? [] : skillTargets()) {
2584
2635
  for (const legacy of LEGACY) {
2585
2636
  const l = path14.join(base2, legacy);
2586
- if (!skills.includes(legacy) && linksInto(l, root)) {
2587
- fs16.unlinkSync(l);
2637
+ if (!skills2.includes(legacy) && linksInto(l, root)) {
2638
+ fs17.unlinkSync(l);
2588
2639
  out.push(`\u2714 removed old link ${l}`);
2589
2640
  }
2590
2641
  }
2591
- for (const name of skills) out.push(place(path14.join(root, "skills", name), path14.join(base2, name), opts));
2642
+ for (const name of skills2) out.push(place(path14.join(root, "skills", name), path14.join(base2, name), opts));
2592
2643
  }
2593
2644
  const bundle = path14.join(root, "dist", "pitroom.mjs");
2594
- if (fs16.existsSync(bundle)) {
2645
+ if (fs17.existsSync(bundle)) {
2595
2646
  out.push(placeLauncher(bundle, root, opts.force));
2596
2647
  const onPath = (process.env.PATH ?? "").split(path14.delimiter).some((d) => path14.resolve(d) === path14.dirname(launcherPath()));
2597
2648
  if (!onPath) out.push(`! ${path14.dirname(launcherPath())} is not on PATH; add it, or run ${launcherPath()} directly`);
@@ -2602,24 +2653,24 @@ function uninstall() {
2602
2653
  const root = packageRoot();
2603
2654
  const out = [];
2604
2655
  for (const base2 of skillTargets()) {
2605
- if (!fs16.existsSync(base2)) continue;
2606
- for (const name of fs16.readdirSync(base2)) {
2656
+ if (!fs17.existsSync(base2)) continue;
2657
+ for (const name of fs17.readdirSync(base2)) {
2607
2658
  const l = path14.join(base2, name);
2608
2659
  if (linksInto(l, root)) {
2609
- fs16.unlinkSync(l);
2660
+ fs17.unlinkSync(l);
2610
2661
  out.push(`\u2714 removed ${l}`);
2611
2662
  }
2612
2663
  }
2613
2664
  }
2614
2665
  if (isOurLauncher(launcherPath(), root)) {
2615
- fs16.rmSync(launcherPath(), { force: true });
2666
+ fs17.rmSync(launcherPath(), { force: true });
2616
2667
  out.push(`\u2714 removed ${launcherPath()}`);
2617
2668
  }
2618
2669
  return out.length ? out : ["nothing to remove"];
2619
2670
  }
2620
2671
  function installedSkills() {
2621
2672
  const names = skillNames();
2622
- return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs16.existsSync(path14.join(base2, n, "SKILL.md"))) }));
2673
+ return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs17.existsSync(path14.join(base2, n, "SKILL.md"))) }));
2623
2674
  }
2624
2675
 
2625
2676
  // src/core/templates.ts
@@ -2631,8 +2682,8 @@ var FILES = {
2631
2682
  };
2632
2683
  function loadTemplate(name, root = packageRoot()) {
2633
2684
  const file2 = path15.join(root, "skills", FILES[name]);
2634
- if (!fs17.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
2635
- return fs17.readFileSync(file2, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
2685
+ if (!fs18.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
2686
+ return fs18.readFileSync(file2, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
2636
2687
  }
2637
2688
  function fill(template, values) {
2638
2689
  const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
@@ -2642,7 +2693,7 @@ function fill(template, values) {
2642
2693
 
2643
2694
  // src/core/slots.ts
2644
2695
  import crypto5 from "node:crypto";
2645
- import fs18 from "node:fs";
2696
+ import fs19 from "node:fs";
2646
2697
  import path16 from "node:path";
2647
2698
  var slotsDir = () => path16.join(home(), "slots");
2648
2699
  var locksDir = () => path16.join(home(), "locks");
@@ -2659,23 +2710,23 @@ function holderActive(runId, self) {
2659
2710
  }
2660
2711
  }
2661
2712
  function tryClaim(file2, runId) {
2662
- fs18.mkdirSync(path16.dirname(file2), { recursive: true });
2713
+ fs19.mkdirSync(path16.dirname(file2), { recursive: true });
2663
2714
  try {
2664
- fs18.writeFileSync(file2, runId, { flag: "wx" });
2715
+ fs19.writeFileSync(file2, runId, { flag: "wx" });
2665
2716
  return true;
2666
2717
  } catch (e) {
2667
2718
  if (e.code !== "EEXIST") throw e;
2668
2719
  }
2669
2720
  let owner = "";
2670
2721
  try {
2671
- owner = fs18.readFileSync(file2, "utf8").trim();
2722
+ owner = fs19.readFileSync(file2, "utf8").trim();
2672
2723
  } catch {
2673
2724
  }
2674
2725
  if (owner === runId) return true;
2675
2726
  if (holderActive(owner, runId)) return false;
2676
- fs18.rmSync(file2, { force: true });
2727
+ fs19.rmSync(file2, { force: true });
2677
2728
  try {
2678
- fs18.writeFileSync(file2, runId, { flag: "wx" });
2729
+ fs19.writeFileSync(file2, runId, { flag: "wx" });
2679
2730
  return true;
2680
2731
  } catch {
2681
2732
  return false;
@@ -2684,7 +2735,7 @@ function tryClaim(file2, runId) {
2684
2735
  function releaseIfOwner(file2, runId) {
2685
2736
  if (!file2) return;
2686
2737
  try {
2687
- if (fs18.readFileSync(file2, "utf8").trim() === runId) fs18.rmSync(file2, { force: true });
2738
+ if (fs19.readFileSync(file2, "utf8").trim() === runId) fs19.rmSync(file2, { force: true });
2688
2739
  } catch {
2689
2740
  }
2690
2741
  }
@@ -2702,7 +2753,7 @@ function acquireWriteLock(repoRoot2, runId) {
2702
2753
  if (tryClaim(file2, runId)) return;
2703
2754
  let owner = "";
2704
2755
  try {
2705
- owner = fs18.readFileSync(file2, "utf8").trim();
2756
+ owner = fs19.readFileSync(file2, "utf8").trim();
2706
2757
  } catch {
2707
2758
  }
2708
2759
  throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
@@ -2749,7 +2800,7 @@ ${task}`;
2749
2800
  }
2750
2801
 
2751
2802
  // src/core/receipt.ts
2752
- import fs19 from "node:fs";
2803
+ import fs20 from "node:fs";
2753
2804
  import path17 from "node:path";
2754
2805
  var PRESETS = {
2755
2806
  sonnet: { name: "Claude Sonnet", input: 3, output: 15, cachedInput: 0.3 },
@@ -2786,13 +2837,13 @@ function record(meta) {
2786
2837
  saved: meta.savedUsd ?? 0,
2787
2838
  price: primaryPrice().name
2788
2839
  };
2789
- fs19.mkdirSync(path17.dirname(ledgerFile()), { recursive: true });
2790
- fs19.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
2840
+ fs20.mkdirSync(path17.dirname(ledgerFile()), { recursive: true });
2841
+ fs20.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
2791
2842
  `);
2792
2843
  }
2793
2844
  function readLedger(sinceMs2) {
2794
- if (!fs19.existsSync(ledgerFile())) return [];
2795
- return fs19.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
2845
+ if (!fs20.existsSync(ledgerFile())) return [];
2846
+ return fs20.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
2796
2847
  try {
2797
2848
  return [JSON.parse(l)];
2798
2849
  } catch {
@@ -2867,7 +2918,7 @@ ${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.rou
2867
2918
  }
2868
2919
 
2869
2920
  // src/core/refs.ts
2870
- import fs20 from "node:fs";
2921
+ import fs21 from "node:fs";
2871
2922
  import path18 from "node:path";
2872
2923
  var FILE_EXTENSIONS = new Set(
2873
2924
  "ts tsx mts cts js jsx mjs cjs json jsonc json5 md mdx txt rst py pyi rb go rs java kt kts swift c h cc cpp cxx hpp hh cs fs php lua r jl sh bash zsh fish ps1 bat yml yaml toml ini cfg conf env html htm css scss sass less vue svelte astro sql graphql gql proto lock xml svg csv tsv gradle tf hcl dart ex exs erl hs ml scala clj vim el mk cmake dockerfile gitignore gitattributes editorconfig npmrc nvmrc".split(" ")
@@ -2924,8 +2975,8 @@ function verifyRefs(refs, dirs) {
2924
2975
  const lineCache = /* @__PURE__ */ new Map();
2925
2976
  const load = (file2) => {
2926
2977
  if (!lineCache.has(file2)) {
2927
- const size = fs20.statSync(file2).size;
2928
- const lines = size > MAX_BYTES ? null : fs20.readFileSync(file2, "utf8").split("\n");
2978
+ const size = fs21.statSync(file2).size;
2979
+ const lines = size > MAX_BYTES ? null : fs21.readFileSync(file2, "utf8").split("\n");
2929
2980
  if (lines && lines[lines.length - 1] === "") lines.pop();
2930
2981
  lineCache.set(file2, lines);
2931
2982
  }
@@ -2938,7 +2989,7 @@ function verifyRefs(refs, dirs) {
2938
2989
  if (direct) return [direct];
2939
2990
  if (path18.isAbsolute(ref.file)) return [];
2940
2991
  const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
2941
- return (byName().get(path18.basename(wanted)) ?? []).filter((f) => f.endsWith(`/${wanted}`) || path18.basename(f) === wanted).slice(0, 20);
2992
+ return (byName().get(path18.basename(wanted)) ?? []).filter((f) => f.split(path18.sep).join("/").endsWith(`/${wanted}`) || path18.basename(f) === wanted).slice(0, 20);
2942
2993
  };
2943
2994
  const check = (file2, ref) => {
2944
2995
  const lines = load(file2);
@@ -2983,7 +3034,7 @@ function indexFiles(roots) {
2983
3034
  const walk = (dir) => {
2984
3035
  let entries;
2985
3036
  try {
2986
- entries = fs20.readdirSync(dir, { withFileTypes: true });
3037
+ entries = fs21.readdirSync(dir, { withFileTypes: true });
2987
3038
  } catch {
2988
3039
  return;
2989
3040
  }
@@ -3006,7 +3057,7 @@ function resolve(ref, roots) {
3006
3057
  const candidates = path18.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path18.join(r, ref));
3007
3058
  return candidates.find((c) => {
3008
3059
  try {
3009
- return fs20.statSync(c).isFile() && inside(c, roots);
3060
+ return fs21.statSync(c).isFile() && inside(c, roots);
3010
3061
  } catch {
3011
3062
  return false;
3012
3063
  }
@@ -3021,13 +3072,13 @@ function inside(file2, roots) {
3021
3072
  var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3022
3073
 
3023
3074
  // src/core/history.ts
3024
- import fs22 from "node:fs";
3075
+ import fs23 from "node:fs";
3025
3076
  import { createRequire } from "node:module";
3026
3077
  import path19 from "node:path";
3027
3078
  import zlib from "node:zlib";
3028
3079
 
3029
3080
  // src/core/ui.ts
3030
- import fs21 from "node:fs";
3081
+ import fs22 from "node:fs";
3031
3082
  var WEEK_MS = 7 * 24 * 3600 * 1e3;
3032
3083
  var RECENT = 40;
3033
3084
  var RECENT_HOURS = 1;
@@ -3118,9 +3169,9 @@ function cardsFor(m) {
3118
3169
  const out = [];
3119
3170
  for (const [phase, text] of phases) {
3120
3171
  const marker = runFile(m.id, `card-${phase}`);
3121
- if (fs21.existsSync(marker)) continue;
3172
+ if (fs22.existsSync(marker)) continue;
3122
3173
  if (!(phase === "started" && !isActive(m.state))) out.push(text());
3123
- fs21.writeFileSync(marker, "");
3174
+ fs22.writeFileSync(marker, "");
3124
3175
  }
3125
3176
  return out;
3126
3177
  }
@@ -3131,10 +3182,10 @@ function hookCards(input) {
3131
3182
  const response = event.tool_response;
3132
3183
  const output = typeof response === "string" ? response : [response?.stdout, response?.stderr].filter(Boolean).join("\n");
3133
3184
  const ids = [...new Set(`${command}
3134
- ${output}`.match(RUN_ID) ?? [])].filter((id) => fs21.existsSync(`${runsDir()}/${id}`));
3185
+ ${output}`.match(RUN_ID) ?? [])].filter((id) => fs22.existsSync(`${runsDir()}/${id}`));
3135
3186
  const since2 = Date.now() - RECENT_HOURS * 36e5;
3136
3187
  for (const id of listRunIds().slice(-RECENT)) {
3137
- if (ids.includes(id) || fs21.existsSync(runFile(id, "card-ended"))) continue;
3188
+ if (ids.includes(id) || fs22.existsSync(runFile(id, "card-ended"))) continue;
3138
3189
  try {
3139
3190
  const m = readMeta(id);
3140
3191
  if (!isActive(m.state) && Date.parse(m.endedAt ?? m.startedAt) > since2) ids.push(id);
@@ -3170,7 +3221,7 @@ function openDb() {
3170
3221
  } finally {
3171
3222
  process.emitWarning = emit;
3172
3223
  }
3173
- fs22.mkdirSync(path19.dirname(file2), { recursive: true });
3224
+ fs23.mkdirSync(path19.dirname(file2), { recursive: true });
3174
3225
  db = new DatabaseSync(file2);
3175
3226
  db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
3176
3227
  migrate(db);
@@ -3205,30 +3256,30 @@ function migrate(db) {
3205
3256
  function readRunFile(id, name) {
3206
3257
  const f = runFile(id, name);
3207
3258
  try {
3208
- if (fs22.existsSync(f)) return fs22.readFileSync(f, "utf8");
3209
- if (fs22.existsSync(`${f}.gz`)) return zlib.gunzipSync(fs22.readFileSync(`${f}.gz`)).toString("utf8");
3259
+ if (fs23.existsSync(f)) return fs23.readFileSync(f, "utf8");
3260
+ if (fs23.existsSync(`${f}.gz`)) return zlib.gunzipSync(fs23.readFileSync(`${f}.gz`)).toString("utf8");
3210
3261
  } catch {
3211
3262
  }
3212
3263
  return void 0;
3213
3264
  }
3214
3265
  function gzipFile(f) {
3215
- if (!fs22.existsSync(f)) return;
3216
- fs22.writeFileSync(`${f}.gz`, zlib.gzipSync(fs22.readFileSync(f)));
3217
- fs22.rmSync(f);
3266
+ if (!fs23.existsSync(f)) return;
3267
+ fs23.writeFileSync(`${f}.gz`, zlib.gzipSync(fs23.readFileSync(f)));
3268
+ fs23.rmSync(f);
3218
3269
  }
3219
3270
  function compactRun(meta) {
3220
3271
  if (!TERMINAL.includes(meta.state)) return;
3221
3272
  const dir = path19.dirname(runFile(meta.id, "meta.json"));
3222
3273
  let names;
3223
3274
  try {
3224
- names = fs22.readdirSync(dir);
3275
+ names = fs23.readdirSync(dir);
3225
3276
  } catch {
3226
3277
  return;
3227
3278
  }
3228
3279
  for (const n of names) {
3229
3280
  if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(path19.join(dir, n));
3230
3281
  else if (/^stderr(\.attempt-\d+)?\.log$/.test(n)) {
3231
- if (meta.state === "done" && !/attempt/.test(n)) fs22.rmSync(path19.join(dir, n), { force: true });
3282
+ if (meta.state === "done" && !/attempt/.test(n)) fs23.rmSync(path19.join(dir, n), { force: true });
3232
3283
  else gzipFile(path19.join(dir, n));
3233
3284
  }
3234
3285
  }
@@ -3350,6 +3401,7 @@ function archivedId(ref) {
3350
3401
  return void 0;
3351
3402
  }
3352
3403
  }
3404
+ var VERIFY_FAILED = "(r.state = 'done' AND json_extract(r.meta_json, '$.verifyResult.ok') = 0)";
3353
3405
  var ftsQuery = (text) => text.split(/\s+/).filter(Boolean).map((w) => `"${w.replace(/"/g, '""')}"*`).join(" ");
3354
3406
  function listHistory(q = {}) {
3355
3407
  const db = openDb();
@@ -3369,7 +3421,7 @@ function listHistory(q = {}) {
3369
3421
  args.push(q.backend);
3370
3422
  }
3371
3423
  if (q.state) {
3372
- where.push(q.state === "problem" ? "r.state IN ('failed','timeout','stopped')" : "r.state = ?");
3424
+ where.push(q.state === "problem" ? `(r.state IN ('failed','timeout','stopped') OR ${VERIFY_FAILED})` : "r.state = ?");
3373
3425
  if (q.state !== "problem") args.push(q.state);
3374
3426
  }
3375
3427
  if (q.group) {
@@ -3383,8 +3435,8 @@ function listHistory(q = {}) {
3383
3435
  const base2 = where.length ? `WHERE ${where.join(" AND ")}` : "";
3384
3436
  try {
3385
3437
  const total = db.prepare(`SELECT COUNT(*) AS n FROM runs r ${base2}`).get(...args).n;
3386
- 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));
3438
+ const page2 = q.beforeId ? `${base2 ? `${base2} AND` : "WHERE"} r.id < ?` : base2;
3439
+ 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 ${page2} ORDER BY r.id DESC LIMIT ?`).all(...args, ...q.beforeId ? [q.beforeId] : [], Math.min(Math.max(q.limit ?? 30, 1), 200));
3388
3440
  return {
3389
3441
  total,
3390
3442
  rows: rows.map((r) => ({
@@ -3398,6 +3450,7 @@ function listHistory(q = {}) {
3398
3450
  group: r.grp ?? void 0,
3399
3451
  verdict: r.verdict ?? void 0,
3400
3452
  audit: auditBadge({ audit: r.audit_state ? { id: "", state: r.audit_state, verdict: r.audit_verdict ?? void 0 } : void 0 }),
3453
+ verifyFailed: r.verify_failed ? true : void 0,
3401
3454
  seconds: r.seconds ?? void 0,
3402
3455
  steps: r.steps ?? void 0,
3403
3456
  tokens: r.tokens ?? void 0,
@@ -3480,7 +3533,7 @@ function historyStats(sinceMs2) {
3480
3533
  }
3481
3534
 
3482
3535
  // src/core/report.ts
3483
- import fs23 from "node:fs";
3536
+ import fs24 from "node:fs";
3484
3537
 
3485
3538
  // src/core/style.ts
3486
3539
  var enabled = () => {
@@ -3540,11 +3593,13 @@ function duration(meta) {
3540
3593
  }
3541
3594
  function readSummary(meta) {
3542
3595
  const f = runFile(meta.id, "summary.md");
3543
- return fs23.existsSync(f) ? fs23.readFileSync(f, "utf8").trim() : archivedRun(meta.id)?.answer ?? "";
3596
+ return fs24.existsSync(f) ? fs24.readFileSync(f, "utf8").trim() : archivedRun(meta.id)?.answer ?? "";
3544
3597
  }
3545
3598
  function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
3546
3599
  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)}`);
3600
+ const verifyFailed = meta.state === "done" && meta.verifyResult && !meta.verifyResult.ok;
3601
+ const head = verifyFailed ? stateColour("failed", "\u26A0 done \xB7 verify failed") : stateColour(meta.state, `${ICON2[meta.state]} ${meta.state}`);
3602
+ out.push(`${bold("pitroom")} ${head} \xB7 ${meta.mode} \xB7 ${duration(meta)} \xB7 run ${dim(meta.id)}`);
3548
3603
  const ran = meta.ran ?? meta.worker;
3549
3604
  const ids = [`worker ${ran.backend}`, meta.resolvedModel && `model ${meta.resolvedModel}`, meta.sessionId && `session ${meta.sessionId}`];
3550
3605
  out.push(ids.filter(Boolean).join(" \xB7 "));
@@ -3610,7 +3665,9 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
3610
3665
  }
3611
3666
  if (meta.verifyResult) {
3612
3667
  const v = meta.verifyResult;
3613
- out.push(`\u2500\u2500 verify: \`${meta.verify}\` ${v.ok ? "\u2714 passed" : `\u2718 failed (exit ${v.code})`}`);
3668
+ const notFound = !v.ok && v.code === 127 && /not found|no such file|is not recognized as an internal or external command/i.test(v.tail);
3669
+ 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})`}`);
3670
+ 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
3671
  if (!v.ok && v.tail) out.push(...v.tail.split("\n").map((l) => ` ${l}`));
3615
3672
  }
3616
3673
  if (isActive(meta.state)) {
@@ -3622,8 +3679,8 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
3622
3679
  }
3623
3680
  function live(meta) {
3624
3681
  const f = runFile(meta.id, "events.jsonl");
3625
- if (!fs23.existsSync(f)) return { steps: 0, toolCalls: 0 };
3626
- const p = getBackend((meta.ran ?? meta.worker).backend).parse(fs23.readFileSync(f, "utf8"));
3682
+ if (!fs24.existsSync(f)) return { steps: 0, toolCalls: 0 };
3683
+ const p = getBackend((meta.ran ?? meta.worker).backend).parse(fs24.readFileSync(f, "utf8"));
3627
3684
  return { steps: p.usage.steps, toolCalls: p.usage.toolCalls, last: p.lastActivity };
3628
3685
  }
3629
3686
  function progress(meta) {
@@ -3634,7 +3691,7 @@ function progress(meta) {
3634
3691
  }
3635
3692
 
3636
3693
  // src/core/run.ts
3637
- var VERSION2 = true ? "0.16.0" : "0.0.0-dev";
3694
+ var VERSION2 = true ? "0.18.0" : "0.0.0-dev";
3638
3695
  function planWork(o) {
3639
3696
  if (!o.plan) return { task: o.task.trim(), tier: o.tier };
3640
3697
  if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
@@ -3666,7 +3723,7 @@ function prepareRun(o) {
3666
3723
  parent = readMeta(o.continueFrom);
3667
3724
  if (o.worker || o.tier || o.effort) throw new UserError("a follow-up runs on the same worker as its parent; drop --worker/--tier/--effort");
3668
3725
  if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
3669
- if (parent.mode === "isolate" && (!parent.worktree || !fs24.existsSync(parent.worktree))) {
3726
+ if (parent.mode === "isolate" && (!parent.worktree || !fs25.existsSync(parent.worktree))) {
3670
3727
  throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
3671
3728
  }
3672
3729
  const ran = parent.ran ?? parent.worker;
@@ -3685,18 +3742,18 @@ function prepareRun(o) {
3685
3742
  throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
3686
3743
  }
3687
3744
  const mode = parent?.mode ?? o.mode;
3688
- const dir = path20.resolve(parent?.dir ?? o.dir);
3689
- if (!fs24.existsSync(dir) || !fs24.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
3745
+ const dir = canonical(path20.resolve(parent?.dir ?? o.dir));
3746
+ if (!fs25.existsSync(dir) || !fs25.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
3690
3747
  const root = repoRoot(dir);
3691
3748
  if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
3692
3749
  if (mode === "write" && !root && !o.allowNonGit) {
3693
3750
  throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
3694
3751
  }
3695
3752
  const files = o.files.map((f) => path20.resolve(f));
3696
- for (const f of files) if (!fs24.existsSync(f)) throw new UserError(`file not found: ${f}`);
3753
+ for (const f of files) if (!fs25.existsSync(f)) throw new UserError(`file not found: ${f}`);
3697
3754
  const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
3698
3755
  const snap = mode === "read" && !o.review && !o.inPlace && !parent ? wantSnapshot(readIn2, root, dir) : void 0;
3699
- if (parent?.snapshot?.dir && !fs24.existsSync(parent.snapshot.dir)) {
3756
+ if (parent?.snapshot?.dir && !fs25.existsSync(parent.snapshot.dir)) {
3700
3757
  throw new UserError(`the read snapshot of run ${parent.id} is gone (cleaned up); ask the question again as a new run`, 3);
3701
3758
  }
3702
3759
  if (!parent && !snap && !process.env.PITROOM_NO_SECRET_WARNING) {
@@ -3732,6 +3789,7 @@ function prepareRun(o) {
3732
3789
  auditOf: o.audit?.of,
3733
3790
  auditRate: o.auditRate,
3734
3791
  inPlace: o.inPlace || void 0,
3792
+ cache: o.cache,
3735
3793
  snapshot: parent?.snapshot ?? (snap ? { dir: "", tree: "", left: snap.secrets } : void 0),
3736
3794
  plan: work.plan ?? o.review?.plan ?? parent?.plan,
3737
3795
  reviewKind: o.review?.kind,
@@ -3742,13 +3800,13 @@ function prepareRun(o) {
3742
3800
  try {
3743
3801
  acquireWriteLock(root, meta.id);
3744
3802
  } catch (e) {
3745
- fs24.rmSync(runDir(meta.id), { recursive: true, force: true });
3803
+ fs25.rmSync(runDir(meta.id), { recursive: true, force: true });
3746
3804
  throw e;
3747
3805
  }
3748
3806
  }
3749
- fs24.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
3807
+ fs25.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
3750
3808
  `);
3751
- if (work.brief) fs24.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
3809
+ if (work.brief) fs25.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
3752
3810
  return meta;
3753
3811
  }
3754
3812
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -3828,7 +3886,7 @@ async function execute(meta) {
3828
3886
  (meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
3829
3887
  for (const f of ["events.jsonl", "stderr.log"]) {
3830
3888
  const from = runFile(meta.id, f);
3831
- if (fs24.existsSync(from)) fs24.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
3889
+ if (fs25.existsSync(from)) fs25.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
3832
3890
  }
3833
3891
  writeMeta(meta);
3834
3892
  }
@@ -3893,7 +3951,7 @@ function prepareTree(meta) {
3893
3951
  meta.snapshot.dir = s.dir;
3894
3952
  meta.snapshot.tree = s.tree;
3895
3953
  meta.cwd = path20.join(s.dir, path20.relative(root, meta.dir));
3896
- if (!fs24.existsSync(meta.cwd)) meta.cwd = s.dir;
3954
+ if (!fs25.existsSync(meta.cwd)) meta.cwd = s.dir;
3897
3955
  if (shown) meta.warnings.push(`read in a clean snapshot of your project: the secret-looking files (${shown}) and git-ignored files are not in it; --in-place reads the directory itself`);
3898
3956
  } catch (e) {
3899
3957
  meta.snapshot = void 0;
@@ -3928,7 +3986,7 @@ function attempt(meta, backend, target) {
3928
3986
  timeoutSec: meta.timeoutSec
3929
3987
  });
3930
3988
  }
3931
- var read2 = (f) => fs24.existsSync(f) ? fs24.readFileSync(f, "utf8") : "";
3989
+ var read2 = (f) => fs25.existsSync(f) ? fs25.readFileSync(f, "utf8") : "";
3932
3990
  function retryableFailure(meta, backend, res) {
3933
3991
  const run2 = backend.parse(read2(runFile(meta.id, "events.jsonl")));
3934
3992
  if (run2.usage.steps > 0) return void 0;
@@ -3948,12 +4006,12 @@ function finalize(meta, res) {
3948
4006
  if (meta.snapshot?.dir && meta.repoRoot) run2.finalText = backToProject(run2.finalText, meta.snapshot.dir, meta.repoRoot);
3949
4007
  meta.sessionId = run2.sessionId ?? meta.sessionId;
3950
4008
  meta.usage = run2.usage;
3951
- fs24.writeFileSync(runFile(meta.id, "summary.md"), `${run2.finalText}
4009
+ fs25.writeFileSync(runFile(meta.id, "summary.md"), `${run2.finalText}
3952
4010
  `);
3953
4011
  if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run2.finalText);
3954
4012
  if (meta.reviewOf) {
3955
4013
  meta.verdict = parseVerdict(run2.finalText);
3956
- if (meta.packageFile) fs24.rmSync(meta.packageFile, { force: true });
4014
+ if (meta.packageFile) fs25.rmSync(meta.packageFile, { force: true });
3957
4015
  }
3958
4016
  if (!res.timedOut && !res.stopped && !meta.error) {
3959
4017
  const f = backend.failure(run2, read2(runFile(meta.id, "stderr.log")), res.code);
@@ -4001,7 +4059,13 @@ function captureChanges(meta) {
4001
4059
  const d = diffTrees(target, meta.baseTree, meta.afterTree);
4002
4060
  meta.changes = d.changes;
4003
4061
  meta.stats = d.stats;
4004
- fs24.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
4062
+ fs25.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
4063
+ }
4064
+ function verifyPath() {
4065
+ const extra = [path20.dirname(process.execPath), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
4066
+ const parts = (process.env.PATH ?? "").split(path20.delimiter).filter(Boolean);
4067
+ for (const d of process.platform === "win32" ? [path20.dirname(process.execPath)] : extra) if (!parts.includes(d)) parts.push(d);
4068
+ return parts.join(path20.delimiter);
4005
4069
  }
4006
4070
  function runVerify(meta) {
4007
4071
  const r = spawnSync6(meta.verify, {
@@ -4011,11 +4075,12 @@ function runVerify(meta) {
4011
4075
  timeout: 15 * 6e4,
4012
4076
  stdio: ["ignore", "pipe", "pipe"],
4013
4077
  maxBuffer: 64 * 1024 * 1024,
4014
- env: { ...process.env, PWD: meta.cwd, PITROOM_ACTIVE: "1" }
4078
+ env: { ...process.env, PATH: verifyPath(), PWD: meta.cwd, PITROOM_ACTIVE: "1" }
4015
4079
  });
4016
4080
  const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
4017
- fs24.writeFileSync(runFile(meta.id, "verify.log"), output);
4018
- return { ok: r.status === 0, code: r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
4081
+ fs25.writeFileSync(runFile(meta.id, "verify.log"), output);
4082
+ const notFound = process.platform === "win32" && r.status === 1 && /is not recognized as an internal or external command/.test(output);
4083
+ return { ok: r.status === 0, code: notFound ? 127 : r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
4019
4084
  }
4020
4085
  function applyRun(meta, allowDelete = false) {
4021
4086
  if (meta.mode !== "isolate") throw new UserError(`run ${meta.id} edited your tree directly (${meta.mode}); nothing to apply`);
@@ -4051,6 +4116,15 @@ ${res.message}`, 1);
4051
4116
  writeMeta(meta);
4052
4117
  return `reverted ${meta.changes.length} file(s) changed by ${meta.id}`;
4053
4118
  }
4119
+ function stopRun(id) {
4120
+ const meta = freshMeta(id);
4121
+ if (!isActive(meta.state) || !isAlive(meta.pid)) return void 0;
4122
+ requestStop(meta.id);
4123
+ meta.stopRequested = true;
4124
+ writeMeta(meta);
4125
+ process.kill(meta.pid, "SIGTERM");
4126
+ return meta;
4127
+ }
4054
4128
  function discardRun(meta) {
4055
4129
  if (meta.mode !== "isolate") throw new UserError("only --isolate runs have an isolated copy to discard");
4056
4130
  cleanupWorktree(meta);
@@ -4059,9 +4133,13 @@ function discardRun(meta) {
4059
4133
  return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
4060
4134
  }
4061
4135
  function cleanupWorktree(meta) {
4062
- if (meta.worktree && fs24.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
4136
+ if (meta.worktree && fs25.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
4063
4137
  }
4064
4138
 
4139
+ // src/cli/mcp-extras.ts
4140
+ import fs26 from "node:fs";
4141
+ import path21 from "node:path";
4142
+
4065
4143
  // src/cli/mcp-support.ts
4066
4144
  import { spawn as spawn3 } from "node:child_process";
4067
4145
 
@@ -4248,12 +4326,12 @@ function pit(args, timeoutMs = 60 * 6e4, signal) {
4248
4326
  child.stdout.on("data", (d) => out += d);
4249
4327
  child.stderr.on("data", (d) => err += d);
4250
4328
  const stop = () => child.kill("SIGTERM");
4251
- const timer = setTimeout(stop, timeoutMs);
4329
+ const timer2 = setTimeout(stop, timeoutMs);
4252
4330
  signal?.addEventListener("abort", stop, { once: true });
4253
4331
  if (signal?.aborted) stop();
4254
4332
  child.on("error", (e) => resolve2({ code: 1, out, err: err || String(e.message) }));
4255
4333
  child.on("close", (code) => {
4256
- clearTimeout(timer);
4334
+ clearTimeout(timer2);
4257
4335
  signal?.removeEventListener("abort", stop);
4258
4336
  resolve2({ code, out: strip(out).trim(), err: strip(err).trim() });
4259
4337
  });
@@ -4264,11 +4342,11 @@ function asResult(r, still) {
4264
4342
  if (r.code === 75 && still) return { text: clip6(`${text}
4265
4343
 
4266
4344
  ${still}`) };
4267
- return { text: clip6(text), isError: r.code === 2 || r.code === 3 || r.code === 1 && !r.out };
4345
+ return { text: clip6(text), isError: r.code === 2 || r.code === 3 || r.code === 5 || r.code === 6 || r.code === 1 && !r.out };
4268
4346
  }
4269
4347
  var plain = async (args, ctx, timeoutMs = 6e4) => asResult(await pit(args, timeoutMs, ctx.signal));
4270
4348
  function trackProgress(ctx, ids) {
4271
- const tick = () => {
4349
+ const tick2 = () => {
4272
4350
  try {
4273
4351
  const lines = ids().map((id) => freshMeta(id)).filter((m) => isActive(m.state)).map((m) => progress(m).replace(/^pitroom\s+/, ""));
4274
4352
  if (lines.length) ctx.progress(lines.slice(0, 3).join("\n") + (lines.length > 3 ? `
@@ -4276,11 +4354,11 @@ function trackProgress(ctx, ids) {
4276
4354
  } catch {
4277
4355
  }
4278
4356
  };
4279
- const first = setTimeout(tick, 800);
4280
- const timer = setInterval(tick, PROGRESS_EVERY_MS);
4357
+ const first = setTimeout(tick2, 800);
4358
+ const timer2 = setInterval(tick2, PROGRESS_EVERY_MS);
4281
4359
  return () => {
4282
4360
  clearTimeout(first);
4283
- clearInterval(timer);
4361
+ clearInterval(timer2);
4284
4362
  };
4285
4363
  }
4286
4364
  async function waitFor(ids, seconds, ctx, group) {
@@ -4305,13 +4383,17 @@ async function startAndWait(start, seconds, ctx, task) {
4305
4383
  const started = await pit([...start, "--bg", "--json", ...task === void 0 ? [] : ["--", task]]);
4306
4384
  if (started.code !== 0) return asResult(started);
4307
4385
  let id;
4386
+ let cached3 = false;
4308
4387
  try {
4309
- id = JSON.parse(started.out).id;
4388
+ ({ id, cached: cached3 = false } = JSON.parse(started.out));
4310
4389
  } catch {
4311
4390
  return { text: `could not read the run id from: ${started.out.slice(0, 200)}`, isError: true };
4312
4391
  }
4313
4392
  const result = await waitFor([id], seconds, ctx);
4314
- if (cancelled(ctx)) await stopQuietly(["stop", id]);
4393
+ if (cancelled(ctx) && !cached3) await stopQuietly(["stop", id]);
4394
+ if (cached3) result.text = `Cached answer: the same question on the same code as run ${id}; no worker ran (fresh: true asks one).
4395
+
4396
+ ${result.text}`;
4315
4397
  return result;
4316
4398
  }
4317
4399
  async function startCrew(flags, tasks, seconds, ctx) {
@@ -4337,6 +4419,7 @@ var RpcError = class extends Error {
4337
4419
  };
4338
4420
  var RUN = /^pitroom:\/\/run\/(\d{8}-\d{6}-[0-9a-f]{4})(\/patch)?$/;
4339
4421
  var LISTED = 30;
4422
+ var runOfUri = (uri) => RUN.exec(uri)?.[1];
4340
4423
  var oneLine6 = (text, n) => {
4341
4424
  const flat = text.replace(/\s+/g, " ").trim();
4342
4425
  return flat.length > n ? `${flat.slice(0, n - 1)}\u2026` : flat;
@@ -4399,16 +4482,50 @@ Call pitroom_run with mode "isolate" (add "verify" with the project's test comma
4399
4482
  title: "Split work across workers",
4400
4483
  description: "Independent tasks run in parallel as one group of workers.",
4401
4484
  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):
4485
+ text: (a) => `Run these independent tasks in parallel: call pitroom_run with "tasks" (one worker each):
4403
4486
 
4404
4487
  ${a.tasks}
4405
4488
 
4406
4489
  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
4490
  }
4408
4491
  ];
4409
- var listPrompts = () => ({ prompts: PROMPTS.map(({ name, title, description, arguments: args }) => ({ name, title, description, arguments: args })) });
4492
+ 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.';
4493
+ var skills;
4494
+ function skillPrompts() {
4495
+ if (skills) return skills;
4496
+ const found = [];
4497
+ const root = packageRoot();
4498
+ for (const name of skillNames(root)) {
4499
+ try {
4500
+ const text = fs26.readFileSync(path21.join(root, "skills", name, "SKILL.md"), "utf8");
4501
+ const front = /^---\n([\s\S]*?)\n---\n/.exec(text);
4502
+ const body = (front ? text.slice(front[0].length) : text).trim();
4503
+ const description = /^description:\s*(.+)$/m.exec(front?.[1] ?? "")?.[1]?.trim() ?? `The ${name} skill.`;
4504
+ const title = /^#\s+(.+)$/m.exec(body)?.[1]?.trim() ?? name;
4505
+ found.push({
4506
+ name,
4507
+ title,
4508
+ // the first sentence: prompts/list is read whole by some clients, so it stays short
4509
+ description: `Skill: ${description.split(/(?<=\.)\s/)[0]}`,
4510
+ arguments: [{ name: "task", description: "What you are about to do (optional)." }],
4511
+ text: (a) => `${TOOL_MAP}
4512
+
4513
+ ${body}${a.task ? `
4514
+
4515
+ ---
4516
+
4517
+ The task: ${a.task}` : ""}`
4518
+ });
4519
+ } catch {
4520
+ }
4521
+ }
4522
+ skills = found.filter((p) => !PROMPTS.some((b) => b.name === p.name));
4523
+ return skills;
4524
+ }
4525
+ var allPrompts = () => [...PROMPTS, ...skillPrompts()];
4526
+ var listPrompts = () => ({ prompts: allPrompts().map(({ name, title, description, arguments: args }) => ({ name, title, description, arguments: args })) });
4410
4527
  function getPrompt(name, given) {
4411
- const p = PROMPTS.find((x) => x.name === name);
4528
+ const p = allPrompts().find((x) => x.name === name);
4412
4529
  if (!p) throw new RpcError(-32602, `unknown prompt: ${String(name)}`);
4413
4530
  const args = {};
4414
4531
  const raw = given && typeof given === "object" && !Array.isArray(given) ? given : {};
@@ -4422,84 +4539,110 @@ function getPrompt(name, given) {
4422
4539
 
4423
4540
  // src/cli/mcp-tools.ts
4424
4541
  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." }
4542
+ worker: { type: "string", description: 'Worker "backend[:model]", e.g. "opencode", "claude:haiku". Default: configured.' },
4543
+ model: { type: "string", description: "Model for that worker." },
4544
+ tier: { type: "string", description: "cheap, standard or capable (from the config)." },
4545
+ effort: { type: "string", description: "low, medium, high, xhigh." },
4546
+ dir: { type: "string", description: "Project directory (default: the server's)." },
4547
+ files: { type: "array", items: { type: "string" }, description: "Files to attach." },
4548
+ verify: { type: "string", description: 'Command run afterwards, e.g. "npm test".' },
4549
+ link: { type: "array", items: { type: "string" }, description: 'Ignored dirs to link into an isolated copy, e.g. ["node_modules"].' },
4550
+ web: { type: "boolean", description: "Allow web tools." },
4551
+ group: { type: "string", description: "Group name for related runs." }
4435
4552
  };
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." };
4553
+ var WAIT_PROP = { waitSeconds: { type: "number", description: `Wait this long before returning "still running" (default ${DEFAULT_WAIT}, max ${MAX_WAIT}).` } };
4554
+ var RUN_ID2 = { type: "string", description: 'Run id, or "last".' };
4439
4555
  var READ_ONLY = { readOnlyHint: true, openWorldHint: false };
4556
+ var TOPICS = {
4557
+ runs: ["running", "group"],
4558
+ history: ["text", "state", "model", "since", "limit"],
4559
+ stats: ["since"],
4560
+ savings: ["since", "perModel"],
4561
+ models: ["worker", "all"],
4562
+ cooldown: [],
4563
+ config: [],
4564
+ doctor: []
4565
+ };
4566
+ function infoArgs(topic, a) {
4567
+ const allowed = TOPICS[topic];
4568
+ const stray = Object.keys(a).filter((k) => k !== "topic" && a[k] !== void 0 && a[k] !== null && !allowed.includes(k));
4569
+ 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"})`);
4570
+ const group = str(a, "group");
4571
+ const worker = str(a, "worker");
4572
+ switch (topic) {
4573
+ case "runs":
4574
+ return { args: group ? ["status", "-g", group] : ["ls", ...bool(a, "running") ? ["--running"] : []] };
4575
+ case "history": {
4576
+ const limit = a.limit;
4577
+ 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');
4578
+ const text = str(a, "text");
4579
+ const state = str(a, "state");
4580
+ const model = str(a, "model");
4581
+ return { args: ["history", ...since(a), ...state ? ["--state", state] : [], ...model ? ["--model", model] : [], ...limit ? ["--limit", String(limit)] : [], ...text ? ["--", text] : []] };
4582
+ }
4583
+ case "stats":
4584
+ return { args: ["history", "stats", ...since(a)] };
4585
+ case "savings":
4586
+ return { args: ["savings", ...since(a), ...bool(a, "perModel") ? ["--models"] : []] };
4587
+ case "models":
4588
+ return { args: ["models", ...worker ? [worker] : [], ...bool(a, "all") ? ["--all"] : []] };
4589
+ case "doctor":
4590
+ return { args: ["doctor"], timeoutMs: 12e4 };
4591
+ case "cooldown":
4592
+ case "config":
4593
+ return { args: [topic] };
4594
+ default:
4595
+ throw new Error(`topic "${topic}" has no command`);
4596
+ }
4597
+ }
4440
4598
  var TOOLS2 = [
4441
4599
  {
4442
4600
  name: "pitroom_run",
4443
4601
  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.`,
4602
+ 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
4603
  inputSchema: {
4446
4604
  type: "object",
4447
4605
  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." },
4606
+ task: { type: "string", description: "What to do, with the context the worker needs." },
4607
+ tasks: { type: "array", items: { type: "string" }, minItems: 1, maxItems: MAX_TASKS, description: "Or several independent tasks, one worker each." },
4608
+ mode: { type: "string", enum: ["read", "isolate", "write"] },
4609
+ continue: { type: "string", description: "A finished run to follow up in the same worker session." },
4610
+ inPlace: { type: "boolean", description: "Read: read the directory itself, not a snapshot without secret-looking files." },
4611
+ audit: { type: "boolean", description: "Read: have another worker re-check the answer (each one, with tasks)." },
4612
+ fresh: { type: "boolean", description: "Read: ask a worker even if this question was answered on the same code." },
4453
4613
  ...WORKER_PROPS,
4454
4614
  ...WAIT_PROP
4455
- },
4456
- required: ["task"]
4615
+ }
4457
4616
  },
4458
4617
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
4459
4618
  async call(a, ctx) {
4460
- const task = str(a, "task", true);
4619
+ const task = str(a, "task");
4620
+ const tasks = strs(a, "tasks").filter((t) => t.trim());
4621
+ if (!task && !tasks.length) throw new ToolError('give "task" (or "tasks" for parallel work)');
4622
+ if (task && tasks.length) throw new ToolError('give "task" or "tasks", not both');
4461
4623
  const mode = oneOf(a, "mode", ["read", "isolate", "write"], "read");
4462
4624
  const flags = mode === "isolate" ? ["-i"] : mode === "write" ? ["-w"] : [];
4463
- const follow = str(a, "continue");
4464
- if (follow) flags.push("--continue", follow);
4465
4625
  if (mode === "read") {
4466
4626
  if (bool(a, "inPlace")) flags.push("--in-place");
4467
4627
  if (bool(a, "audit")) flags.push("--audit");
4628
+ if (bool(a, "fresh")) flags.push("--fresh");
4629
+ }
4630
+ const follow = str(a, "continue");
4631
+ if (tasks.length) {
4632
+ if (tasks.length > MAX_TASKS) throw new ToolError(`at most ${MAX_TASKS} tasks at once: start the rest when these are done`);
4633
+ if (mode === "write") throw new ToolError('parallel workers never write in place: use mode "isolate" (each gets its own copy)');
4634
+ if (follow) throw new ToolError('"continue" follows up one run: give "task"');
4635
+ return startCrew([...flags, ...workerFlags(a)], tasks, waitSeconds(a), ctx);
4468
4636
  }
4637
+ if (follow) flags.push("--continue", follow);
4469
4638
  return startAndWait(["run", ...flags, ...workerFlags(a)], waitSeconds(a), ctx, task);
4470
4639
  }
4471
4640
  },
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
4641
  {
4496
4642
  name: "pitroom_wait",
4497
4643
  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
- },
4644
+ description: 'Wait for runs that came back "still running" and return their reports.',
4645
+ inputSchema: { type: "object", properties: { runs: { type: "array", items: { type: "string" } }, group: { type: "string" }, ...WAIT_PROP } },
4503
4646
  annotations: READ_ONLY,
4504
4647
  async call(a, ctx) {
4505
4648
  const runs = strs(a, "runs");
@@ -4508,126 +4651,56 @@ var TOOLS2 = [
4508
4651
  return waitFor(runs, waitSeconds(a), ctx, runs.length ? void 0 : group);
4509
4652
  }
4510
4653
  },
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
4654
  {
4536
4655
  name: "pitroom_show",
4537
4656
  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"] },
4657
+ description: "A run's report (live progress while it runs); patch: the exact diff; full: the untruncated answer.",
4658
+ inputSchema: { type: "object", properties: { run: RUN_ID2, patch: { type: "boolean" }, full: { type: "boolean" } } },
4540
4659
  annotations: READ_ONLY,
4541
4660
  async call(a, ctx) {
4542
- const run2 = str(a, "run", true);
4661
+ const run2 = str(a, "run") ?? "last";
4543
4662
  return plain(["show", run2, ...bool(a, "patch") ? ["--patch"] : bool(a, "full") ? ["--full"] : []], ctx);
4544
4663
  }
4545
4664
  },
4546
4665
  {
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.",
4666
+ name: "pitroom_info",
4667
+ title: "Pitroom information",
4668
+ 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
4669
  inputSchema: {
4551
4670
  type: "object",
4552
4671
  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
- }
4672
+ topic: { type: "string", enum: Object.keys(TOPICS) },
4673
+ text: { type: "string" },
4674
+ state: { type: "string" },
4675
+ model: { type: "string" },
4676
+ since: { type: "string", description: "7d, 30d, \u2026 or all." },
4677
+ limit: { type: "number" },
4678
+ running: { type: "boolean" },
4679
+ group: { type: "string" },
4680
+ worker: { type: "string" },
4681
+ all: { type: "boolean" },
4682
+ perModel: { type: "boolean" }
4683
+ },
4684
+ required: ["topic"]
4559
4685
  },
4560
4686
  annotations: READ_ONLY,
4561
4687
  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);
4688
+ if (str(a, "topic") === void 0) throw new ToolError(`"topic" is required: one of ${Object.keys(TOPICS).join(", ")}`);
4689
+ const topic = oneOf(a, "topic", Object.keys(TOPICS), "runs");
4690
+ const { args, timeoutMs } = infoArgs(topic, a);
4691
+ return plain(args, ctx, timeoutMs);
4619
4692
  }
4620
4693
  },
4621
4694
  {
4622
4695
  name: "pitroom_review",
4623
4696
  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.`,
4697
+ 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
4698
  inputSchema: {
4626
4699
  type: "object",
4627
4700
  properties: {
4628
- run: { ...RUN_ID2, description: "The run whose change to review (an isolated or in-place change)." },
4701
+ run: RUN_ID2,
4629
4702
  range: { type: "string", description: 'Or a commit range "A..B".' },
4630
- plan: { type: "string", description: "With range: the plan file the commits implement." },
4703
+ plan: { type: "string", description: "With range: the plan file it implements." },
4631
4704
  worker: WORKER_PROPS.worker,
4632
4705
  tier: WORKER_PROPS.tier,
4633
4706
  dir: WORKER_PROPS.dir,
@@ -4648,7 +4721,7 @@ var TOOLS2 = [
4648
4721
  {
4649
4722
  name: "pitroom_audit",
4650
4723
  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.",
4724
+ description: "Another worker re-checks a read run's answer: AGREE, PARTIAL or DISAGREE, with the disputed claims.",
4652
4725
  inputSchema: { type: "object", properties: { run: RUN_ID2, worker: WORKER_PROPS.worker, ...WAIT_PROP }, required: ["run"] },
4653
4726
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
4654
4727
  async call(a, ctx) {
@@ -4660,7 +4733,7 @@ var TOOLS2 = [
4660
4733
  {
4661
4734
  name: "pitroom_apply",
4662
4735
  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.",
4736
+ 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
4737
  inputSchema: { type: "object", properties: { run: RUN_ID2, group: { type: "string" }, allowDelete: { type: "boolean" } } },
4665
4738
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
4666
4739
  async call(a, ctx) {
@@ -4668,13 +4741,13 @@ var TOOLS2 = [
4668
4741
  const run2 = str(a, "run");
4669
4742
  if (!run2 && !group) throw new ToolError('give "run" or "group"');
4670
4743
  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);
4744
+ return plain(["apply", ...group ? ["-g", group] : [run2], ...bool(a, "allowDelete") ? ["--allow-delete"] : []], ctx, 12e4);
4672
4745
  }
4673
4746
  },
4674
4747
  {
4675
4748
  name: "pitroom_discard",
4676
4749
  title: "Discard an isolated change",
4677
- description: "Drop an isolated run's private copy (its patch is kept).",
4750
+ description: "Drop an isolated run's private copy (the patch is kept).",
4678
4751
  inputSchema: { type: "object", properties: { run: RUN_ID2 }, required: ["run"] },
4679
4752
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
4680
4753
  async call(a, ctx) {
@@ -4684,7 +4757,7 @@ var TOOLS2 = [
4684
4757
  {
4685
4758
  name: "pitroom_revert",
4686
4759
  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).',
4760
+ description: `Undo a mode "write" run's changes (refused if the files changed since).`,
4688
4761
  inputSchema: { type: "object", properties: { run: RUN_ID2 }, required: ["run"] },
4689
4762
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
4690
4763
  async call(a, ctx) {
@@ -4694,32 +4767,115 @@ var TOOLS2 = [
4694
4767
  {
4695
4768
  name: "pitroom_stop",
4696
4769
  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" } } },
4770
+ description: "Stop a running or queued run, or a group; cooldowns: try rate-limited models again now.",
4771
+ inputSchema: { type: "object", properties: { run: RUN_ID2, group: { type: "string" }, cooldowns: { type: "boolean" } } },
4699
4772
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
4700
4773
  async call(a, ctx) {
4701
4774
  const run2 = str(a, "run");
4702
4775
  const group = str(a, "group");
4703
- if (!run2 && !group) throw new ToolError('give "run" or "group"');
4776
+ if (bool(a, "cooldowns")) {
4777
+ if (run2 || group) throw new ToolError('"cooldowns" goes alone');
4778
+ return plain(["cooldown", "--clear"], ctx);
4779
+ }
4780
+ if (!run2 && !group) throw new ToolError('give "run" or "group" (or "cooldowns": true)');
4704
4781
  if (run2 && group) throw new ToolError('give "run" or "group", not both');
4705
4782
  return plain(["stop", ...run2 ? [run2] : ["-g", group]], ctx);
4706
4783
  }
4707
4784
  }
4708
4785
  ];
4709
4786
 
4787
+ // src/cli/mcp-watch.ts
4788
+ var EVERY_MS = 2e3;
4789
+ var MAX_SUBSCRIPTIONS = 100;
4790
+ var watched = /* @__PURE__ */ new Set();
4791
+ var timer;
4792
+ var signature = (uri) => {
4793
+ const id = runOfUri(uri);
4794
+ if (!id) return void 0;
4795
+ try {
4796
+ const m = freshMeta(id);
4797
+ return uri.endsWith("/patch") ? `${m.state}:${m.changes?.length ?? 0}` : m.state;
4798
+ } catch {
4799
+ return void 0;
4800
+ }
4801
+ };
4802
+ var newestRun = () => {
4803
+ try {
4804
+ return listRunIds().at(-1);
4805
+ } catch {
4806
+ return void 0;
4807
+ }
4808
+ };
4809
+ function subscribe(session, uri) {
4810
+ if (typeof uri !== "string") throw new RpcError(-32602, '"uri" is required');
4811
+ if (!runOfUri(uri)) throw new RpcError(-32602, `only runs can be subscribed to (pitroom://run/<id>): ${uri}`);
4812
+ if (!session.subscriptions.has(uri) && session.subscriptions.size >= MAX_SUBSCRIPTIONS) throw new RpcError(-32602, `at most ${MAX_SUBSCRIPTIONS} subscriptions: unsubscribe from finished runs first`);
4813
+ const sig = signature(uri);
4814
+ if (sig === void 0) throw new RpcError(-32002, `unknown resource: ${uri}`);
4815
+ session.subscriptions.set(uri, sig);
4816
+ }
4817
+ function unsubscribe(session, uri) {
4818
+ if (typeof uri !== "string") throw new RpcError(-32602, '"uri" is required');
4819
+ session.subscriptions.delete(uri);
4820
+ }
4821
+ function tick() {
4822
+ const listening = [...watched].filter((s) => s.push);
4823
+ if (!listening.length) return;
4824
+ const newest = newestRun();
4825
+ const seen = /* @__PURE__ */ new Map();
4826
+ for (const s of listening) {
4827
+ const push = s.push;
4828
+ if (newest !== s.newestRun) {
4829
+ s.newestRun = newest;
4830
+ push({ jsonrpc: "2.0", method: "notifications/resources/list_changed" });
4831
+ }
4832
+ for (const [uri, last] of s.subscriptions) {
4833
+ if (!seen.has(uri)) seen.set(uri, signature(uri));
4834
+ const now = seen.get(uri);
4835
+ if (now === last) continue;
4836
+ if (now === void 0) s.subscriptions.delete(uri);
4837
+ else s.subscriptions.set(uri, now);
4838
+ push({ jsonrpc: "2.0", method: "notifications/resources/updated", params: { uri } });
4839
+ }
4840
+ }
4841
+ }
4842
+ function watch2(session) {
4843
+ session.newestRun = newestRun();
4844
+ watched.add(session);
4845
+ if (!timer) {
4846
+ timer = setInterval(tick, EVERY_MS);
4847
+ timer.unref();
4848
+ }
4849
+ }
4850
+ function unwatch(session) {
4851
+ watched.delete(session);
4852
+ session.subscriptions.clear();
4853
+ if (!watched.size && timer) {
4854
+ clearInterval(timer);
4855
+ timer = void 0;
4856
+ }
4857
+ }
4858
+
4710
4859
  // src/cli/mcp.ts
4711
4860
  var PROTOCOLS = ["2025-06-18", "2025-03-26", "2024-11-05"];
4712
4861
  var INSTRUCTIONS = [
4713
4862
  "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.',
4863
+ '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
4864
  'A run can take minutes: pitroom_run waits up to waitSeconds, then returns the run id as "still running"; call pitroom_wait with it.',
4865
+ "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
4866
  "It is optional: for a small task you can do yourself, skip it."
4717
4867
  ].join(" ");
4718
4868
  var log = (msg) => void process.stderr.write(`pitroom mcp: ${msg}
4719
4869
  `);
4720
- var newSession = () => ({ inflight: /* @__PURE__ */ new Map() });
4870
+ function newSession() {
4871
+ const session = { inflight: /* @__PURE__ */ new Map(), subscriptions: /* @__PURE__ */ new Map() };
4872
+ watch2(session);
4873
+ return session;
4874
+ }
4721
4875
  function endSession(session) {
4722
4876
  for (const { abort } of session.inflight.values()) abort.abort("closed");
4877
+ unwatch(session);
4878
+ session.push = void 0;
4723
4879
  }
4724
4880
  function progressToken(params) {
4725
4881
  const meta = params && typeof params === "object" ? params._meta : void 0;
@@ -4737,13 +4893,13 @@ function contextFor(params, abort, notify) {
4737
4893
  }
4738
4894
  };
4739
4895
  }
4740
- async function dispatch(method, params, ctx) {
4896
+ async function dispatch(method, params, ctx, session) {
4741
4897
  switch (method) {
4742
4898
  case "initialize": {
4743
4899
  const asked = typeof params.protocolVersion === "string" ? params.protocolVersion : "";
4744
4900
  return {
4745
4901
  protocolVersion: PROTOCOLS.includes(asked) ? asked : PROTOCOLS[0],
4746
- capabilities: { tools: { listChanged: false }, resources: { listChanged: false, subscribe: false }, prompts: { listChanged: false } },
4902
+ capabilities: { tools: { listChanged: false }, resources: { listChanged: true, subscribe: true }, prompts: { listChanged: false } },
4747
4903
  serverInfo: { name: "pitroom", title: "Pitroom", version: VERSION2 },
4748
4904
  instructions: INSTRUCTIONS
4749
4905
  };
@@ -4772,6 +4928,12 @@ async function dispatch(method, params, ctx) {
4772
4928
  case "resources/read":
4773
4929
  if (typeof params.uri !== "string") throw new RpcError(-32602, '"uri" is required');
4774
4930
  return readResource(params.uri, ctx);
4931
+ case "resources/subscribe":
4932
+ subscribe(session, params.uri);
4933
+ return {};
4934
+ case "resources/unsubscribe":
4935
+ unsubscribe(session, params.uri);
4936
+ return {};
4775
4937
  case "prompts/list":
4776
4938
  return listPrompts();
4777
4939
  case "prompts/get":
@@ -4799,7 +4961,7 @@ async function handle(session, msg, notify) {
4799
4961
  const entry = { abort, ctx: contextFor(params, abort, notify) };
4800
4962
  if (isRequest) session.inflight.set(m.id, entry);
4801
4963
  try {
4802
- const result = await dispatch(m.method, params, entry.ctx);
4964
+ const result = await dispatch(m.method, params, entry.ctx, session);
4803
4965
  if (abort.signal.aborted && abort.signal.reason === "cancelled") return void 0;
4804
4966
  return isRequest ? { jsonrpc: "2.0", id: m.id, result } : void 0;
4805
4967
  } catch (e) {
@@ -4824,6 +4986,7 @@ async function serveMcp() {
4824
4986
  const send = (msg) => void process.stdout.write(`${JSON.stringify(msg)}
4825
4987
  `);
4826
4988
  const session = newSession();
4989
+ session.push = send;
4827
4990
  const rl = readline.createInterface({ input: process.stdin });
4828
4991
  const pending = /* @__PURE__ */ new Set();
4829
4992
  const handleLine = async (line) => {
@@ -4844,28 +5007,29 @@ async function serveMcp() {
4844
5007
  void p.finally(() => pending.delete(p));
4845
5008
  });
4846
5009
  await new Promise((resolve2) => rl.once("close", resolve2));
5010
+ unwatch(session);
4847
5011
  await Promise.allSettled([...pending]);
4848
5012
  return 0;
4849
5013
  }
4850
5014
 
4851
5015
  // src/cli/mcp-http.ts
4852
5016
  import crypto7 from "node:crypto";
4853
- import fs26 from "node:fs";
5017
+ import fs28 from "node:fs";
4854
5018
  import http2 from "node:http";
4855
- import path22 from "node:path";
5019
+ import path23 from "node:path";
4856
5020
 
4857
5021
  // src/core/dash.ts
4858
5022
  import { spawn as spawn4 } from "node:child_process";
4859
5023
  import crypto6 from "node:crypto";
4860
- import fs25 from "node:fs";
5024
+ import fs27 from "node:fs";
4861
5025
  import http from "node:http";
4862
- import path21 from "node:path";
5026
+ import path22 from "node:path";
4863
5027
  import { fileURLToPath as fileURLToPath3 } from "node:url";
4864
5028
 
4865
5029
  // src/core/file-diff.ts
4866
5030
  var PREVIEW_LINES = 300;
4867
5031
  function gitPath(value, prefix = true) {
4868
- let path29 = value;
5032
+ let path31 = value;
4869
5033
  if (value.startsWith('"') && value.endsWith('"')) {
4870
5034
  const bytes = [];
4871
5035
  const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
@@ -4873,9 +5037,9 @@ function gitPath(value, prefix = true) {
4873
5037
  if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
4874
5038
  else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
4875
5039
  }
4876
- path29 = new TextDecoder().decode(new Uint8Array(bytes));
5040
+ path31 = new TextDecoder().decode(new Uint8Array(bytes));
4877
5041
  }
4878
- return prefix ? path29.replace(/^[ab]\//, "") : path29;
5042
+ return prefix ? path31.replace(/^[ab]\//, "") : path31;
4879
5043
  }
4880
5044
  function headerPath(header) {
4881
5045
  const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
@@ -4977,9 +5141,9 @@ function fileDiffs(patch, changes, sourceTruncated = false) {
4977
5141
  // src/core/dash-page.ts
4978
5142
  var THEME_SCRIPT = "(function(){try{var t=localStorage.getItem('pitroom-theme');var d=t?t==='dark':matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.classList.toggle('dark',d)}catch(e){}})()";
4979
5143
  var ICON3 = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%23ff6a2b'/%3E%3Cpath d='M9 8h4v4H9zm8 0h4v4h-4zm-4 4h4v4h-4zm8 0h4v4h-4zM9 16h4v4H9zm8 0h4v4h-4zm-4 4h4v4h-4zm8 0h4v4h-4z' fill='%23fff'/%3E%3C/svg%3E";
4980
- var PAGE = `<!doctype html>
5144
+ var page = (token) => `<!doctype html>
4981
5145
  <html lang="en" class="dark"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
4982
- <meta name="color-scheme" content="dark light"><title>Pitroom</title><link rel="icon" href="${ICON3}">
5146
+ <meta name="color-scheme" content="dark light"><meta name="pitroom-token" content="${token}"><title>Pitroom</title><link rel="icon" href="${ICON3}">
4983
5147
  <script>${THEME_SCRIPT}</script><link rel="stylesheet" href="/assets/app.css"></head>
4984
5148
  <body><div id="root"></div><script type="module" src="/assets/app.js"></script></body></html>`;
4985
5149
  var MISSING = `<!doctype html><meta charset="utf-8"><title>Pitroom</title><body style="font:15px system-ui;padding:3rem;max-width:40rem;margin:auto"><h1>Pitroom dash</h1><p>The dashboard files are missing (dist/ui). Run <code>npm run build</code> in the Pitroom repository, or reinstall the package.</p></body>`;
@@ -5000,11 +5164,13 @@ function findings(m) {
5000
5164
  const parts = [v.critical && `${v.critical} critical`, v.important && `${v.important} important`, v.minor && `${v.minor} minor`].filter(Boolean);
5001
5165
  return parts.length ? parts.join(" \xB7 ") : "no findings";
5002
5166
  }
5167
+ var canDiscard = (m) => m.mode === "isolate" && !isActive(m.state) && !m.applied && !m.discarded;
5003
5168
  function toRun(m) {
5004
5169
  const l = m.state === "running" ? live(m) : void 0;
5005
5170
  return {
5006
5171
  id: m.id,
5007
5172
  state: m.state,
5173
+ verifyFailed: m.verifyResult && !m.verifyResult.ok ? true : void 0,
5008
5174
  kind: kind(m),
5009
5175
  // the model that really ran, once the CLI has said which; until then just the worker, never a guess
5010
5176
  worker: workerName(ranTarget(m)),
@@ -5019,7 +5185,8 @@ function toRun(m) {
5019
5185
  audit: auditBadge(m),
5020
5186
  changes: m.changes?.length || void 0,
5021
5187
  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)
5188
+ discardable: canDiscard(m) || void 0,
5189
+ 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
5190
  };
5024
5191
  }
5025
5192
  function dashState(opts = {}) {
@@ -5123,29 +5290,55 @@ var assetsDir = () => fileURLToPath3(new URL("./ui/", import.meta.url));
5123
5290
  var assetCache = /* @__PURE__ */ new Map();
5124
5291
  function readAsset(route) {
5125
5292
  try {
5126
- const file2 = path21.join(assetsDir(), path21.basename(route));
5127
- const { mtimeMs } = fs25.statSync(file2);
5293
+ const file2 = path22.join(assetsDir(), path22.basename(route));
5294
+ const { mtimeMs } = fs27.statSync(file2);
5128
5295
  const hit = assetCache.get(route);
5129
5296
  if (hit && hit.mtimeMs === mtimeMs) return hit.data;
5130
- const data = fs25.readFileSync(file2);
5297
+ const data = fs27.readFileSync(file2);
5131
5298
  assetCache.set(route, { mtimeMs, data });
5132
5299
  return data;
5133
5300
  } catch {
5134
5301
  return void 0;
5135
5302
  }
5136
5303
  }
5137
- function handler(touch2) {
5304
+ function handler(touch2, token) {
5138
5305
  const send = (res, code, type, body) => {
5139
5306
  res.writeHead(code, { ...HEADERS, "content-type": type });
5140
5307
  res.end(body);
5141
5308
  };
5142
5309
  const json = (res, code, value) => send(res, code, "application/json; charset=utf-8", JSON.stringify(value));
5310
+ const tokenBuf = Buffer.from(token);
5311
+ const act = (req, res, pathname) => {
5312
+ req.resume();
5313
+ const given = Buffer.from(String(req.headers["x-pitroom-token"] ?? ""));
5314
+ if (given.length !== tokenBuf.length || !crypto6.timingSafeEqual(given, tokenBuf)) return json(res, 403, { error: "forbidden" });
5315
+ if (req.headers.origin !== `http://${req.headers.host}`) return json(res, 403, { error: "forbidden origin" });
5316
+ const m = /^\/api\/run\/([^/]+)\/(stop|discard)$/.exec(pathname);
5317
+ if (!m) return json(res, 404, { error: "not found" });
5318
+ let meta;
5319
+ try {
5320
+ if (!RUN_ID3.test(m[1])) throw new Error("bad id");
5321
+ meta = freshMeta(m[1]);
5322
+ } catch {
5323
+ return json(res, 404, { error: "no such run" });
5324
+ }
5325
+ try {
5326
+ if (m[2] === "stop") {
5327
+ return stopRun(meta.id) ? json(res, 200, { ok: true, message: `stopping ${meta.id}` }) : json(res, 409, { error: "run is not active" });
5328
+ }
5329
+ if (!canDiscard(meta)) return json(res, 409, { error: "only a finished, not yet applied isolate run can be discarded" });
5330
+ return json(res, 200, { ok: true, message: discardRun(meta) });
5331
+ } catch (e) {
5332
+ return json(res, 409, { error: e.message });
5333
+ }
5334
+ };
5143
5335
  return (req, res) => {
5144
5336
  touch2();
5145
5337
  if (!LOCAL_HOST.test(req.headers.host ?? "")) return json(res, 403, { error: "forbidden host" });
5146
- if (req.method !== "GET" && req.method !== "HEAD") return json(res, 405, { error: "read-only" });
5147
5338
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
5148
- if (url.pathname === "/") return send(res, 200, "text/html; charset=utf-8", readAsset("/assets/app.js") ? PAGE : MISSING);
5339
+ if (req.method === "POST") return act(req, res, url.pathname);
5340
+ if (req.method !== "GET" && req.method !== "HEAD") return json(res, 405, { error: "read-only" });
5341
+ if (url.pathname === "/") return send(res, 200, "text/html; charset=utf-8", readAsset("/assets/app.js") ? page(token) : MISSING);
5149
5342
  const type = ASSETS[url.pathname];
5150
5343
  if (type) {
5151
5344
  const data = readAsset(url.pathname);
@@ -5160,21 +5353,21 @@ function handler(touch2) {
5160
5353
  }
5161
5354
  if (url.pathname === "/api/history") {
5162
5355
  const q = url.searchParams;
5163
- const days = Number(q.get("days") ?? 0);
5356
+ const days2 = Number(q.get("days") ?? 0);
5164
5357
  return json(res, 200, listHistory({
5165
5358
  text: (q.get("q") ?? "").slice(0, 200),
5166
5359
  model: q.get("model") || void 0,
5167
5360
  backend: q.get("backend") || void 0,
5168
5361
  state: q.get("state") || void 0,
5169
5362
  group: q.get("group") || void 0,
5170
- sinceMs: days > 0 ? Date.now() - days * 864e5 : void 0,
5363
+ sinceMs: days2 > 0 ? Date.now() - days2 * 864e5 : void 0,
5171
5364
  beforeId: RUN_ID3.test(q.get("before") ?? "") ? q.get("before") : void 0,
5172
5365
  limit: Number(q.get("limit") ?? 30) || 30
5173
5366
  }));
5174
5367
  }
5175
5368
  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 });
5369
+ const days2 = Number(url.searchParams.get("days") ?? 30);
5370
+ return json(res, 200, { ...historyStats(days2 > 0 ? Date.now() - days2 * 864e5 : void 0), price: primaryPrice().name });
5178
5371
  }
5179
5372
  const m = /^\/api\/run\/([^/]+)$/.exec(url.pathname);
5180
5373
  if (m) {
@@ -5186,28 +5379,28 @@ function handler(touch2) {
5186
5379
  }
5187
5380
  function startDash(opts) {
5188
5381
  let lastRequest = Date.now();
5189
- const server = http.createServer(handler(() => lastRequest = Date.now()));
5382
+ const server = http.createServer(handler(() => lastRequest = Date.now(), crypto6.randomBytes(24).toString("hex")));
5190
5383
  return new Promise((resolve2, reject) => {
5191
5384
  server.once("error", reject);
5192
5385
  server.listen(opts.port, "127.0.0.1", () => {
5193
5386
  const port = server.address().port;
5194
5387
  let done;
5195
5388
  const closed = new Promise((r) => done = r);
5196
- let timer;
5389
+ let timer2;
5197
5390
  const close = () => {
5198
- if (timer) clearInterval(timer);
5391
+ if (timer2) clearInterval(timer2);
5199
5392
  server.closeAllConnections?.();
5200
5393
  return new Promise((r) => server.close(() => (done(), r())));
5201
5394
  };
5202
5395
  if (opts.idleMs && opts.idleMs > 0) {
5203
- timer = setInterval(() => Date.now() - lastRequest > opts.idleMs && void close(), Math.min(6e4, opts.idleMs));
5204
- timer.unref();
5396
+ timer2 = setInterval(() => Date.now() - lastRequest > opts.idleMs && void close(), Math.min(6e4, opts.idleMs));
5397
+ timer2.unref();
5205
5398
  }
5206
5399
  resolve2({ port, url: `http://127.0.0.1:${port}/`, closed, close });
5207
5400
  });
5208
5401
  });
5209
5402
  }
5210
- var registryFile = () => path21.join(home(), "dash.json");
5403
+ var registryFile = () => path22.join(home(), "dash.json");
5211
5404
  async function ping(port) {
5212
5405
  try {
5213
5406
  const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
@@ -5218,11 +5411,11 @@ async function ping(port) {
5218
5411
  }
5219
5412
  async function runningDash() {
5220
5413
  try {
5221
- const r = JSON.parse(fs25.readFileSync(registryFile(), "utf8"));
5414
+ const r = JSON.parse(fs27.readFileSync(registryFile(), "utf8"));
5222
5415
  if (isAlive(r.pid) && await ping(r.port)) return { ...r, url: `http://127.0.0.1:${r.port}/` };
5223
5416
  } catch {
5224
5417
  }
5225
- fs25.rmSync(registryFile(), { force: true });
5418
+ fs27.rmSync(registryFile(), { force: true });
5226
5419
  return void 0;
5227
5420
  }
5228
5421
  function openBrowser(url) {
@@ -5237,7 +5430,7 @@ async function dashCommand(o) {
5237
5430
  if (o.stop) {
5238
5431
  if (!existing) throw new UserError("pitroom dash is not running");
5239
5432
  process.kill(existing.pid, "SIGTERM");
5240
- fs25.rmSync(registryFile(), { force: true });
5433
+ fs27.rmSync(registryFile(), { force: true });
5241
5434
  o.log(`stopped pitroom dash (${existing.url})`);
5242
5435
  return 0;
5243
5436
  }
@@ -5254,7 +5447,7 @@ async function dashCommand(o) {
5254
5447
  });
5255
5448
  child.on("error", () => void 0);
5256
5449
  child.unref();
5257
- for (let i = 0; i < 60; i++) {
5450
+ for (let i = 0; i < 200; i++) {
5258
5451
  await new Promise((r2) => setTimeout(r2, 100));
5259
5452
  const r = await runningDash();
5260
5453
  if (r && r.pid === child.pid) {
@@ -5274,8 +5467,8 @@ async function dashCommand(o) {
5274
5467
  if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
5275
5468
  dash = await startDash({ port: 0, idleMs: o.idleMs });
5276
5469
  }
5277
- fs25.mkdirSync(home(), { recursive: true });
5278
- fs25.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
5470
+ fs27.mkdirSync(home(), { recursive: true });
5471
+ fs27.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
5279
5472
  const stop = () => void dash.close();
5280
5473
  process.once("SIGINT", stop);
5281
5474
  process.once("SIGTERM", stop);
@@ -5284,7 +5477,7 @@ async function dashCommand(o) {
5284
5477
  if (o.open) openBrowser(dash.url);
5285
5478
  await dash.closed;
5286
5479
  try {
5287
- if (JSON.parse(fs25.readFileSync(registryFile(), "utf8")).pid === process.pid) fs25.rmSync(registryFile(), { force: true });
5480
+ if (JSON.parse(fs27.readFileSync(registryFile(), "utf8")).pid === process.pid) fs27.rmSync(registryFile(), { force: true });
5288
5481
  } catch {
5289
5482
  }
5290
5483
  return 0;
@@ -5296,10 +5489,10 @@ var MAX_BODY = 4e6;
5296
5489
  var MAX_SESSIONS = 64;
5297
5490
  var IDLE_MS = 60 * 6e4;
5298
5491
  var PATH = "/mcp";
5299
- var tokenFile = () => path22.join(home(), "mcp-token");
5492
+ var tokenFile = () => path23.join(home(), "mcp-token");
5300
5493
  var readToken = (file2) => {
5301
5494
  try {
5302
- const kept = fs26.readFileSync(file2, "utf8").trim();
5495
+ const kept = fs28.readFileSync(file2, "utf8").trim();
5303
5496
  return kept.length >= 16 ? kept : void 0;
5304
5497
  } catch {
5305
5498
  return void 0;
@@ -5314,10 +5507,10 @@ function mcpToken() {
5314
5507
  const file2 = tokenFile();
5315
5508
  let token = readToken(file2);
5316
5509
  if (!token) {
5317
- fs26.mkdirSync(path22.dirname(file2), { recursive: true });
5510
+ fs28.mkdirSync(path23.dirname(file2), { recursive: true });
5318
5511
  token = crypto7.randomBytes(32).toString("hex");
5319
5512
  try {
5320
- fs26.writeFileSync(file2, `${token}
5513
+ fs28.writeFileSync(file2, `${token}
5321
5514
  `, { mode: 384, flag: "wx" });
5322
5515
  } catch (e) {
5323
5516
  if (e.code !== "EEXIST") throw e;
@@ -5326,7 +5519,7 @@ function mcpToken() {
5326
5519
  }
5327
5520
  }
5328
5521
  try {
5329
- fs26.chmodSync(file2, 384);
5522
+ fs28.chmodSync(file2, 384);
5330
5523
  } catch {
5331
5524
  }
5332
5525
  return { token, from: file2 };
@@ -5337,6 +5530,15 @@ var sameSecret = (given, token) => {
5337
5530
  const b = crypto7.createHash("sha256").update(token).digest();
5338
5531
  return crypto7.timingSafeEqual(a, b);
5339
5532
  };
5533
+ var idleSince = (k) => Math.max(k.lastSeen, k.closeStream ? k.streamSeen ?? 0 : 0);
5534
+ function drop(sessions, id) {
5535
+ const k = sessions.get(id);
5536
+ if (!k) return;
5537
+ k.closeStream?.();
5538
+ endSession(k.session);
5539
+ sessions.delete(id);
5540
+ }
5541
+ var PING_MS = 25e3;
5340
5542
  function reply(res, status, body, headers = {}) {
5341
5543
  if (res.headersSent) return void res.end();
5342
5544
  const text = body === void 0 ? "" : JSON.stringify(body);
@@ -5362,9 +5564,9 @@ var wantsStream = (parsed) => {
5362
5564
  };
5363
5565
  function evictIdle(sessions) {
5364
5566
  let oldest;
5365
- for (const entry of sessions) if (!entry[1].session.inflight.size && (!oldest || entry[1].lastSeen < oldest[1].lastSeen)) oldest = entry;
5567
+ for (const entry of sessions) if (!entry[1].session.inflight.size && (!oldest || idleSince(entry[1]) < idleSince(oldest[1]))) oldest = entry;
5366
5568
  if (!oldest) return false;
5367
- sessions.delete(oldest[0]);
5569
+ drop(sessions, oldest[0]);
5368
5570
  return true;
5369
5571
  }
5370
5572
  function makeHandler(token, sessions) {
@@ -5378,13 +5580,44 @@ function makeHandler(token, sessions) {
5378
5580
  if (!auth || !sameSecret(auth, token)) return reply(res, 401, failure5(-32001, "a bearer token is needed"), { "WWW-Authenticate": "Bearer" });
5379
5581
  const sid = typeof req.headers["mcp-session-id"] === "string" ? req.headers["mcp-session-id"] : void 0;
5380
5582
  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);
5583
+ if (!sid || !sessions.has(sid)) return reply(res, 404, failure5(-32600, "no such session"));
5584
+ drop(sessions, sid);
5385
5585
  return reply(res, 204);
5386
5586
  }
5387
- if (req.method !== "POST") return reply(res, 405, failure5(-32600, "POST requests only"), { Allow: "POST, DELETE" });
5587
+ if (req.method === "GET") {
5588
+ 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"));
5589
+ if (!sid) return reply(res, 400, failure5(-32600, "Mcp-Session-Id is missing: initialize first"));
5590
+ const kept2 = sessions.get(sid);
5591
+ if (!kept2) return reply(res, 404, failure5(-32600, "no such session: initialize again"));
5592
+ kept2.closeStream?.();
5593
+ res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" });
5594
+ res.write(": open\n\n");
5595
+ const open = () => !res.writableEnded && !res.destroyed;
5596
+ const push = (msg) => {
5597
+ if (!open()) return;
5598
+ kept2.streamSeen = Date.now();
5599
+ res.write(`event: message
5600
+ data: ${JSON.stringify(msg)}
5601
+
5602
+ `);
5603
+ };
5604
+ const ping2 = setInterval(() => open() && res.write(": ping\n\n"), PING_MS);
5605
+ ping2.unref();
5606
+ res.on("error", () => {
5607
+ });
5608
+ kept2.streamSeen = Date.now();
5609
+ const close = () => {
5610
+ clearInterval(ping2);
5611
+ if (kept2.session.push === push) kept2.session.push = void 0;
5612
+ if (kept2.closeStream === close) kept2.closeStream = void 0;
5613
+ if (!res.writableEnded) res.end();
5614
+ };
5615
+ kept2.session.push = push;
5616
+ kept2.closeStream = close;
5617
+ res.on("close", close);
5618
+ return;
5619
+ }
5620
+ if (req.method !== "POST") return reply(res, 405, failure5(-32600, "GET, POST or DELETE"), { Allow: "GET, POST, DELETE" });
5388
5621
  const body = await readBody(req);
5389
5622
  if (body === "too-big") return reply(res, 413, failure5(-32600, `a request body is at most ${MAX_BODY} bytes`));
5390
5623
  let parsed;
@@ -5397,12 +5630,8 @@ function makeHandler(token, sessions) {
5397
5630
  let kept;
5398
5631
  const single = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
5399
5632
  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"));
5633
+ if (sid) drop(sessions, sid);
5634
+ 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
5635
  const id = crypto7.randomUUID();
5407
5636
  kept = { session: newSession(), lastSeen: Date.now() };
5408
5637
  sessions.set(id, kept);
@@ -5444,7 +5673,7 @@ async function serveMcpHttp(opts) {
5444
5673
  });
5445
5674
  });
5446
5675
  const sweep = setInterval(() => {
5447
- for (const [id, k] of sessions) if (Date.now() - k.lastSeen > IDLE_MS && !k.session.inflight.size) sessions.delete(id);
5676
+ for (const [id, k] of [...sessions]) if (Date.now() - idleSince(k) > IDLE_MS && !k.session.inflight.size) drop(sessions, id);
5448
5677
  }, 6e4);
5449
5678
  sweep.unref();
5450
5679
  const port = await new Promise((resolve2, reject) => {
@@ -5464,7 +5693,7 @@ async function serveMcpHttp(opts) {
5464
5693
  process.once("SIGTERM", stop);
5465
5694
  });
5466
5695
  clearInterval(sweep);
5467
- for (const k of sessions.values()) endSession(k.session);
5696
+ for (const id of [...sessions.keys()]) drop(sessions, id);
5468
5697
  server.closeAllConnections();
5469
5698
  await new Promise((resolve2) => server.close(() => resolve2()));
5470
5699
  return 0;
@@ -5472,16 +5701,16 @@ async function serveMcpHttp(opts) {
5472
5701
 
5473
5702
  // src/cli/commands.ts
5474
5703
  import { spawnSync as spawnSync8 } from "node:child_process";
5475
- import fs31 from "node:fs";
5476
- import path27 from "node:path";
5704
+ import fs34 from "node:fs";
5705
+ import path29 from "node:path";
5477
5706
  import { fileURLToPath as fileURLToPath4 } from "node:url";
5478
5707
 
5479
5708
  // src/core/init.ts
5480
- import fs27 from "node:fs";
5481
- import path23 from "node:path";
5709
+ import fs29 from "node:fs";
5710
+ import path24 from "node:path";
5482
5711
  var FREE = /-free$/;
5483
5712
  var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
5484
- var isFound = (binary5) => path23.isAbsolute(binary5) && fs27.existsSync(binary5);
5713
+ var isFound = (binary5) => path24.isAbsolute(binary5) && fs29.existsSync(binary5);
5485
5714
  function planInit(opts = {}) {
5486
5715
  const file2 = configPath();
5487
5716
  const workers = backendIds().map((id) => {
@@ -5527,15 +5756,15 @@ function planInit(opts = {}) {
5527
5756
  if (Object.keys(tiers).length > 1) config.tiers = tiers;
5528
5757
  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
5758
  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 };
5759
+ return { path: file2, exists: fs29.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
5531
5760
  }
5532
5761
  function writeInit(plan, force) {
5533
5762
  if (plan.blocked) throw new UserError(plan.blocked);
5534
5763
  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)}
5764
+ 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)`);
5765
+ fs29.mkdirSync(path24.dirname(plan.path), { recursive: true });
5766
+ if (plan.exists) fs29.copyFileSync(plan.path, `${plan.path}.bak`);
5767
+ fs29.writeFileSync(plan.path, `${JSON.stringify(plan.config, null, 2)}
5539
5768
  `);
5540
5769
  return plan.path;
5541
5770
  }
@@ -5556,17 +5785,17 @@ function formatInit(plan, written) {
5556
5785
 
5557
5786
  // src/core/mcp-install.ts
5558
5787
  import { spawnSync as spawnSync7 } from "node:child_process";
5559
- import fs28 from "node:fs";
5788
+ import fs30 from "node:fs";
5560
5789
  import os10 from "node:os";
5561
- import path24 from "node:path";
5790
+ import path25 from "node:path";
5562
5791
  var CLIENT_IDS = ["claude-code", "codex", "gemini", "cursor", "claude-desktop"];
5563
5792
  var NAME = "pitroom";
5564
5793
  function mcpCommand() {
5565
5794
  const launcher = launcherPath();
5566
5795
  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"] };
5796
+ for (const dir of (process.env.PATH ?? "").split(path25.delimiter).filter(Boolean)) {
5797
+ const p = path25.join(dir, process.platform === "win32" ? "pitroom.cmd" : "pitroom");
5798
+ if (fs30.existsSync(p)) return { command: path25.resolve(p), args: ["mcp"] };
5570
5799
  }
5571
5800
  return { command: process.execPath, args: [process.argv[1] ?? "pitroom", "mcp"] };
5572
5801
  }
@@ -5582,7 +5811,7 @@ function run(backendId, args) {
5582
5811
  var runnable = (backendId) => run(backendId, ["--version"]).ok;
5583
5812
  var readJson = (file2) => {
5584
5813
  try {
5585
- return JSON.parse(fs28.readFileSync(file2, "utf8"));
5814
+ return JSON.parse(fs30.readFileSync(file2, "utf8"));
5586
5815
  } catch {
5587
5816
  return void 0;
5588
5817
  }
@@ -5592,7 +5821,7 @@ var jsonState = (file2, c) => {
5592
5821
  return entry === void 0 ? "absent" : same(c, entry) ? "same" : "different";
5593
5822
  };
5594
5823
  var home2 = () => os10.homedir();
5595
- var geminiDir = () => path24.join(process.env.GEMINI_CLI_HOME ?? home2(), ".gemini");
5824
+ var geminiDir = () => path25.join(process.env.GEMINI_CLI_HOME ?? home2(), ".gemini");
5596
5825
  function cliClient(id, backend, name, where, o) {
5597
5826
  return {
5598
5827
  id,
@@ -5615,43 +5844,43 @@ function jsonClient(id, name, file2, dir) {
5615
5844
  return {
5616
5845
  id,
5617
5846
  name,
5618
- found: () => fs28.existsSync(dir()),
5847
+ found: () => fs30.existsSync(dir()),
5619
5848
  state: (c) => jsonState(file2(), c),
5620
5849
  add: (c) => {
5621
5850
  let config = {};
5622
- if (fs28.existsSync(file2())) {
5851
+ if (fs30.existsSync(file2())) {
5623
5852
  config = readJson(file2());
5624
5853
  if (!config || typeof config !== "object" || Array.isArray(config)) return { ok: false, message: `${rel()} is not valid JSON: left alone` };
5625
5854
  const backup = `${file2()}.bak-pitroom`;
5626
- if (!fs28.existsSync(backup)) fs28.copyFileSync(file2(), backup);
5855
+ if (!fs30.existsSync(backup)) fs30.copyFileSync(file2(), backup);
5627
5856
  }
5628
5857
  config.mcpServers = { ...config.mcpServers ?? {}, [NAME]: { command: c.command, args: c.args } };
5629
- fs28.mkdirSync(path24.dirname(file2()), { recursive: true });
5858
+ fs30.mkdirSync(path25.dirname(file2()), { recursive: true });
5630
5859
  const tmp = `${file2()}.${process.pid}.tmp`;
5631
- fs28.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
5860
+ fs30.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
5632
5861
  `);
5633
- fs28.renameSync(tmp, file2());
5862
+ fs30.renameSync(tmp, file2());
5634
5863
  return { ok: true, message: rel() };
5635
5864
  },
5636
5865
  remove: () => {
5637
5866
  const config = readJson(file2());
5638
5867
  if (!config?.mcpServers?.[NAME]) return { ok: true, message: rel() };
5639
5868
  delete config.mcpServers[NAME];
5640
- fs28.writeFileSync(file2(), `${JSON.stringify(config, null, 2)}
5869
+ fs30.writeFileSync(file2(), `${JSON.stringify(config, null, 2)}
5641
5870
  `);
5642
5871
  return { ok: true, message: rel() };
5643
5872
  }
5644
5873
  };
5645
5874
  }
5646
5875
  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");
5876
+ if (process.platform === "darwin") return path25.join(home2(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
5877
+ if (process.platform === "win32") return path25.join(process.env.APPDATA ?? path25.join(home2(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
5878
+ return path25.join(process.env.XDG_CONFIG_HOME ?? path25.join(home2(), ".config"), "Claude", "claude_desktop_config.json");
5650
5879
  }
5651
5880
  function codexState(c) {
5652
5881
  let text;
5653
5882
  try {
5654
- text = fs28.readFileSync(path24.join(process.env.CODEX_HOME ?? path24.join(home2(), ".codex"), "config.toml"), "utf8");
5883
+ text = fs30.readFileSync(path25.join(process.env.CODEX_HOME ?? path25.join(home2(), ".codex"), "config.toml"), "utf8");
5655
5884
  } catch {
5656
5885
  return "absent";
5657
5886
  }
@@ -5669,7 +5898,7 @@ function codexState(c) {
5669
5898
  function clients() {
5670
5899
  return [
5671
5900
  cliClient("claude-code", "claude", "Claude Code", "user settings (~/.claude.json)", {
5672
- state: (c) => jsonState(path24.join(home2(), ".claude.json"), c),
5901
+ state: (c) => jsonState(path25.join(home2(), ".claude.json"), c),
5673
5902
  add: (c) => ["mcp", "add", "--scope", "user", NAME, "--", c.command, ...c.args],
5674
5903
  remove: () => ["mcp", "remove", NAME, "--scope", "user"]
5675
5904
  }),
@@ -5679,12 +5908,12 @@ function clients() {
5679
5908
  remove: () => ["mcp", "remove", NAME]
5680
5909
  }),
5681
5910
  cliClient("gemini", "gemini", "Gemini CLI", "user settings (~/.gemini/settings.json)", {
5682
- state: (c) => jsonState(path24.join(geminiDir(), "settings.json"), c),
5911
+ state: (c) => jsonState(path25.join(geminiDir(), "settings.json"), c),
5683
5912
  add: (c) => ["mcp", "add", "--scope", "user", NAME, c.command, ...c.args],
5684
5913
  remove: () => ["mcp", "remove", "--scope", "user", NAME]
5685
5914
  }),
5686
- jsonClient("cursor", "Cursor", () => path24.join(home2(), ".cursor", "mcp.json"), () => path24.join(home2(), ".cursor")),
5687
- jsonClient("claude-desktop", "Claude Desktop", desktopFile, () => path24.dirname(desktopFile()))
5915
+ jsonClient("cursor", "Cursor", () => path25.join(home2(), ".cursor", "mcp.json"), () => path25.join(home2(), ".cursor")),
5916
+ jsonClient("claude-desktop", "Claude Desktop", desktopFile, () => path25.dirname(desktopFile()))
5688
5917
  ];
5689
5918
  }
5690
5919
  function attempt2(f) {
@@ -5755,16 +5984,16 @@ var parseClients = (value) => {
5755
5984
 
5756
5985
  // src/core/plan-status.ts
5757
5986
  import crypto8 from "node:crypto";
5758
- import fs29 from "node:fs";
5759
- import path25 from "node:path";
5987
+ import fs31 from "node:fs";
5988
+ import path26 from "node:path";
5760
5989
  function notesFile(plan) {
5761
5990
  const id = crypto8.createHash("sha1").update(plan.file).digest("hex").slice(0, 12);
5762
- return path25.join(home(), "plans", id, "notes.md");
5991
+ return path26.join(home(), "plans", id, "notes.md");
5763
5992
  }
5764
5993
  function readNotes(plan) {
5765
5994
  const f = notesFile(plan);
5766
- if (!fs29.existsSync(f)) return [];
5767
- return fs29.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
5995
+ if (!fs31.existsSync(f)) return [];
5996
+ return fs31.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
5768
5997
  const m = /^(\d{4}-\d\d-\d\d \d\d:\d\d) (.*)$/.exec(l);
5769
5998
  return m ? { at: m[1], text: m[2] } : { at: "", text: l };
5770
5999
  });
@@ -5772,11 +6001,11 @@ function readNotes(plan) {
5772
6001
  function addNote(planFile, text) {
5773
6002
  const plan = loadPlan(planFile);
5774
6003
  const f = notesFile(plan);
5775
- fs29.mkdirSync(path25.dirname(f), { recursive: true });
5776
- if (!fs29.existsSync(f)) fs29.writeFileSync(f, `# plan: ${plan.file}
6004
+ fs31.mkdirSync(path26.dirname(f), { recursive: true });
6005
+ if (!fs31.existsSync(f)) fs31.writeFileSync(f, `# plan: ${plan.file}
5777
6006
  `);
5778
6007
  const at = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
5779
- fs29.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
6008
+ fs31.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
5780
6009
  `);
5781
6010
  return f;
5782
6011
  }
@@ -5842,10 +6071,73 @@ ${rulings}`,
5842
6071
  ].join("\n");
5843
6072
  }
5844
6073
 
5845
- // src/core/review.ts
6074
+ // src/core/cache.ts
5846
6075
  import crypto9 from "node:crypto";
5847
- import fs30 from "node:fs";
5848
- import path26 from "node:path";
6076
+ import fs32 from "node:fs";
6077
+ import path27 from "node:path";
6078
+ var SCAN2 = 500;
6079
+ function cacheKey(o) {
6080
+ if (o.verify || o.mode !== "read" || o.continueFrom || o.web || o.plan || o.review || o.audit || o.auditRate === 1) return void 0;
6081
+ if (!(effective().cacheDays.value > 0)) return void 0;
6082
+ const dir = canonical(path27.resolve(o.dir));
6083
+ const root = repoRoot(dir);
6084
+ if (!root) return void 0;
6085
+ try {
6086
+ const tree = snapshotTree(root);
6087
+ const head = commitOf(root, "HEAD") ?? "";
6088
+ const files = o.files.map((f) => {
6089
+ const abs = path27.resolve(f);
6090
+ return [abs, crypto9.createHash("sha256").update(fs32.readFileSync(abs)).digest("hex")];
6091
+ });
6092
+ const question = o.task.replace(/\s+/g, " ").trim();
6093
+ const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
6094
+ const where = !o.inPlace && wantSnapshot(readIn2, root, dir) ? "snapshot" : "project";
6095
+ const key = crypto9.createHash("sha256").update(JSON.stringify([question, path27.relative(root, dir), files, where])).digest("hex");
6096
+ return { key, state: `${head}:${tree}` };
6097
+ } catch {
6098
+ return void 0;
6099
+ }
6100
+ }
6101
+ function heldUp(m) {
6102
+ if (m.state !== "done" || m.mode !== "read" || m.reviewOf || m.auditOf) return false;
6103
+ if (m.refs && m.refs.invalid.length) return false;
6104
+ if (m.warnings.some((w) => w.startsWith("READ-ONLY VIOLATION"))) return false;
6105
+ if (m.audit && (isActive(m.audit.state) || m.audit.verdict === "disagree" || m.audit.verdict === "partial")) return false;
6106
+ try {
6107
+ return readSummary(m).trim() !== "";
6108
+ } catch {
6109
+ return false;
6110
+ }
6111
+ }
6112
+ function findCached(k, now = Date.now()) {
6113
+ const maxAge = effective().cacheDays.value * 864e5;
6114
+ let best;
6115
+ for (const id of listRunIds().slice(-SCAN2)) {
6116
+ let m;
6117
+ try {
6118
+ m = readMeta(id);
6119
+ } catch {
6120
+ continue;
6121
+ }
6122
+ if (m.cache?.key !== k.key || m.cache.state !== k.state) continue;
6123
+ const at = Date.parse(m.endedAt ?? m.startedAt);
6124
+ if (now - at > maxAge || best && at <= best.at || !heldUp(m)) continue;
6125
+ best = { m, at };
6126
+ }
6127
+ return best?.m;
6128
+ }
6129
+ function ago(iso, now = Date.now()) {
6130
+ const s = Math.max(0, (now - Date.parse(iso)) / 1e3);
6131
+ if (s < 3600) return `${Math.max(1, Math.round(s / 60))} min ago`;
6132
+ if (s < 86400) return `${Math.round(s / 3600)} h ago`;
6133
+ return `${Math.round(s / 86400)} d ago`;
6134
+ }
6135
+ 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`;
6136
+
6137
+ // src/core/review.ts
6138
+ import crypto10 from "node:crypto";
6139
+ import fs33 from "node:fs";
6140
+ import path28 from "node:path";
5849
6141
  var TEMPLATE2 = { task: "task-reviewer", fix: "re-review", range: "code-reviewer" };
5850
6142
  var section = (title, body) => `## ${title}
5851
6143
 
@@ -5863,7 +6155,7 @@ function ancestors(m) {
5863
6155
  function briefOf(m) {
5864
6156
  const root = [m, ...ancestors(m)].at(-1);
5865
6157
  const f = runFile(root.id, "brief.md");
5866
- return fs30.existsSync(f) ? fs30.readFileSync(f, "utf8") : root.task;
6158
+ return fs33.existsSync(f) ? fs33.readFileSync(f, "utf8") : root.task;
5867
6159
  }
5868
6160
  function latestReview(ids) {
5869
6161
  for (const id of listRunIds().reverse()) {
@@ -5877,7 +6169,7 @@ function latestReview(ids) {
5877
6169
  }
5878
6170
  function diffOf(m, from = m.baseTree) {
5879
6171
  const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
5880
- if (where && fs30.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
6172
+ if (where && fs33.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
5881
6173
  if (from !== m.baseTree) {
5882
6174
  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
6175
  }
@@ -5891,7 +6183,7 @@ function runReview(id) {
5891
6183
  if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
5892
6184
  const chain = ancestors(m);
5893
6185
  const previous = latestReview(chain.map((a) => a.id));
5894
- const dir = m.mode === "isolate" && m.worktree && fs30.existsSync(m.worktree) ? m.cwd : m.dir;
6186
+ const dir = m.mode === "isolate" && m.worktree && fs33.existsSync(m.worktree) ? m.cwd : m.dir;
5895
6187
  const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
5896
6188
  const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
5897
6189
  if (!previous) {
@@ -5964,12 +6256,25 @@ function pickReviewer(job) {
5964
6256
  }
5965
6257
  function writePackage(job) {
5966
6258
  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);
6259
+ const root = repoRoot(job.dir);
6260
+ if (!g || !root) throw new UserError(`not a git repository: ${job.dir}`);
6261
+ const name = `review-${crypto10.randomBytes(4).toString("hex")}.md`;
6262
+ const inside2 = !path28.relative(fs33.realpathSync(root), fs33.realpathSync(g)).startsWith("..");
6263
+ const file2 = inside2 ? path28.join(g, "pitroom", name) : path28.join(root, ".pitroom", name);
6264
+ if (!inside2) ignoreInGit(root, "/.pitroom/");
6265
+ fs33.mkdirSync(path28.dirname(file2), { recursive: true });
6266
+ fs33.writeFileSync(file2, job.package);
5971
6267
  return file2;
5972
6268
  }
6269
+ function ignoreInGit(root, pattern) {
6270
+ const exclude = path28.resolve(root, git(root, ["rev-parse", "--git-path", "info/exclude"]).stdout.trim());
6271
+ const text = fs33.existsSync(exclude) ? fs33.readFileSync(exclude, "utf8") : "";
6272
+ if (text.split(/\r?\n/).includes(pattern)) return;
6273
+ fs33.mkdirSync(path28.dirname(exclude), { recursive: true });
6274
+ fs33.appendFileSync(exclude, `${text && !text.endsWith("\n") ? "\n" : ""}# Pitroom review packages in worktrees (removed after each review)
6275
+ ${pattern}
6276
+ `);
6277
+ }
5973
6278
 
5974
6279
  // src/core/models.ts
5975
6280
  var base = (model) => (model ?? "").split("#")[0];
@@ -6086,7 +6391,15 @@ warning: ${w}`).join("")
6086
6391
  async function cmdRun(p) {
6087
6392
  const opts = { ...runOptions(p, readTask(p)), plan: planStep(p) };
6088
6393
  if (!opts.task.trim() && !opts.plan) throw new UserError('no task given (pitroom "find where X is handled")');
6089
- return launch(p, prepareRun(opts));
6394
+ const cache = cacheKey(opts);
6395
+ const hit = cache && !has(p, "fresh") ? findCached(cache) : void 0;
6396
+ if (hit) {
6397
+ console.log(has(p, "json") ? JSON.stringify({ ...hit, cached: true }, null, 2) : `${cachedNote(hit)}
6398
+
6399
+ ${formatReport(hit)}`);
6400
+ return exitCodeFor(hit);
6401
+ }
6402
+ return launch(p, prepareRun({ ...opts, cache }));
6090
6403
  }
6091
6404
  async function cmdReview(p) {
6092
6405
  const range = flag(p, "range");
@@ -6114,10 +6427,10 @@ async function cmdReview(p) {
6114
6427
  writeMeta(meta);
6115
6428
  }
6116
6429
  } catch (e) {
6117
- fs31.rmSync(packageFile, { force: true });
6430
+ fs34.rmSync(packageFile, { force: true });
6118
6431
  throw e;
6119
6432
  }
6120
- fs31.writeFileSync(runFile(meta.id, "package.md"), job.package);
6433
+ fs34.writeFileSync(runFile(meta.id, "package.md"), job.package);
6121
6434
  return launch(p, meta);
6122
6435
  }
6123
6436
  async function cmdAudit(p) {
@@ -6185,7 +6498,7 @@ function cmdPlan(p) {
6185
6498
  }
6186
6499
  function cmdCrew(p) {
6187
6500
  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);
6501
+ const tasks = (file2 ? fs34.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
6189
6502
  if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
6190
6503
  if (has(p, "write") && tasks.length > 1) {
6191
6504
  throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
@@ -6306,7 +6619,7 @@ ${[fmt2(head2), ...rows2.map(fmt2)].join("\n")}`);
6306
6619
  const { rows, total } = listHistory({ text: p.positional.join(" "), model: flag(p, "model"), state: flag(p, "state"), group: flag(p, "group"), sinceMs: since2, limit });
6307
6620
  if (has(p, "json")) return console.log(JSON.stringify({ total, rows }, null, 2)), 0;
6308
6621
  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]);
6622
+ 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
6623
  const head = ["RUN", "WHEN", "STATE", "WORKER", "TIME", "TASK"];
6311
6624
  const widths = head.map((h, i) => Math.max(h.length, ...body.map((r) => r[i].length)));
6312
6625
  const fmt = (r, header = false) => r.map((c, i) => {
@@ -6371,11 +6684,8 @@ function cmdStop(p) {
6371
6684
  const ids = selectIds(p, () => [resolveRun(void 0)]);
6372
6685
  let stopped = 0;
6373
6686
  for (const id of ids) {
6374
- const meta = freshMeta(id);
6375
- if (!isActive(meta.state) || !isAlive(meta.pid)) continue;
6376
- meta.stopRequested = true;
6377
- writeMeta(meta);
6378
- process.kill(meta.pid, "SIGTERM");
6687
+ const meta = stopRun(id);
6688
+ if (!meta) continue;
6379
6689
  console.log(`stopping ${meta.id} (${meta.state})`);
6380
6690
  stopped++;
6381
6691
  }
@@ -6388,13 +6698,13 @@ function sinceMs(s) {
6388
6698
  if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
6389
6699
  return Date.now() - Number(m[1]) * 864e5;
6390
6700
  }
6391
- var readStdin = () => process.stdin.isTTY ? "" : fs31.readFileSync(0, "utf8");
6701
+ var readStdin = () => process.stdin.isTTY ? "" : fs34.readFileSync(0, "utf8");
6392
6702
  function cmdStatusline(p) {
6393
6703
  const input = readStdin();
6394
6704
  const lines = [];
6395
6705
  const then = flag(p, "then");
6396
6706
  if (then) {
6397
- const r = spawnSync8("/bin/sh", ["-c", then], { input, encoding: "utf8", timeout: 5e3 });
6707
+ const r = process.platform === "win32" ? spawnSync8(then, { shell: true, input, encoding: "utf8", timeout: 5e3 }) : spawnSync8("/bin/sh", ["-c", then], { input, encoding: "utf8", timeout: 5e3 });
6398
6708
  if (r.stdout?.trimEnd()) lines.push(r.stdout.trimEnd());
6399
6709
  }
6400
6710
  try {
@@ -6428,8 +6738,8 @@ function cmdModels(p) {
6428
6738
  return 0;
6429
6739
  }
6430
6740
  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 });
6741
+ const script = path29.join(path29.dirname(path29.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
6742
+ if (fs34.existsSync(script)) spawnSync8(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
6433
6743
  return 0;
6434
6744
  }
6435
6745
  function cmdInit(p) {
@@ -6469,8 +6779,8 @@ function cmdSavings(p) {
6469
6779
  est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
6470
6780
  const out = flag(p, "card");
6471
6781
  if (out) {
6472
- fs31.writeFileSync(out, card(t, period));
6473
- console.log(`card written to ${path27.resolve(out)}`);
6782
+ fs34.writeFileSync(out, card(t, period));
6783
+ console.log(`card written to ${path29.resolve(out)}`);
6474
6784
  }
6475
6785
  if (has(p, "badge")) console.log(`![pitroom](${badgeUrl(t)})`);
6476
6786
  return 0;
@@ -6512,7 +6822,7 @@ function cmdConfig(p) {
6512
6822
  console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
6513
6823
  return 0;
6514
6824
  }
6515
- console.log(`config file: ${configPath()}${fs31.existsSync(configPath()) ? "" : " (not present)"}`);
6825
+ console.log(`config file: ${configPath()}${fs34.existsSync(configPath()) ? "" : " (not present)"}`);
6516
6826
  for (const [key, s] of Object.entries(eff)) {
6517
6827
  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
6828
  console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
@@ -6521,22 +6831,22 @@ function cmdConfig(p) {
6521
6831
  return 0;
6522
6832
  }
6523
6833
  function cmdClean(p) {
6524
- const days = Number(flag(p, "days") ?? 14);
6525
- const cutoff = Date.now() - days * 864e5;
6834
+ const days2 = Number(flag(p, "days") ?? 14);
6835
+ const cutoff = Date.now() - days2 * 864e5;
6526
6836
  const old = listRunIds().filter((id) => {
6527
6837
  const m = freshMeta(id);
6528
6838
  const pendingPatch = m.mode === "isolate" && !m.applied && !m.discarded && m.changes?.length;
6529
6839
  return TERMINAL.includes(m.state) && Date.parse(m.startedAt) < cutoff && !pendingPatch;
6530
6840
  });
6531
6841
  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.`);
6842
+ console.log(`${old.length} run(s) older than ${days2} days would be removed (unapplied isolate patches are kept). Re-run with --yes.`);
6533
6843
  return 0;
6534
6844
  }
6535
6845
  for (const id of old) {
6536
6846
  const m = readMeta(id);
6537
6847
  if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
6538
6848
  recordRun(m);
6539
- fs31.rmSync(runDir(id), { recursive: true, force: true });
6849
+ fs34.rmSync(runDir(id), { recursive: true, force: true });
6540
6850
  }
6541
6851
  const snapshots = prune(Date.now(), 0);
6542
6852
  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 +6857,9 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
6547
6857
 
6548
6858
  // src/core/doctor.ts
6549
6859
  import { spawnSync as spawnSync9 } from "node:child_process";
6550
- import fs32 from "node:fs";
6860
+ import fs35 from "node:fs";
6551
6861
  import os11 from "node:os";
6552
- import path28 from "node:path";
6862
+ import path30 from "node:path";
6553
6863
  var MARK = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
6554
6864
  var NEXT = [
6555
6865
  { when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
@@ -6559,10 +6869,10 @@ var NEXT = [
6559
6869
  { 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
6870
  ];
6561
6871
  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");
6872
+ for (const dir of (process.env.PATH ?? "").split(path30.delimiter).filter(Boolean)) {
6873
+ const file2 = path30.join(dir, process.platform === "win32" ? "node.exe" : "node");
6564
6874
  try {
6565
- if (!fs32.statSync(file2).isFile()) continue;
6875
+ if (!fs35.statSync(file2).isFile()) continue;
6566
6876
  } catch {
6567
6877
  continue;
6568
6878
  }
@@ -6589,14 +6899,16 @@ function doctor5(probe) {
6589
6899
  const addAll = (list2) => list2.forEach((c) => add(c.level, c.message));
6590
6900
  add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
6591
6901
  const onPath = firstNodeOnPath();
6592
- if (onPath && onPath.version && tooOld(onPath.version) && path28.resolve(onPath.path) !== path28.resolve(process.execPath)) {
6902
+ if (onPath && onPath.version && tooOld(onPath.version) && path30.resolve(onPath.path) !== path30.resolve(process.execPath)) {
6593
6903
  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
6904
  }
6595
6905
  add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
6596
6906
  const cfg = loadConfig();
6597
- add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs32.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
6907
+ add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs35.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
6598
6908
  for (const w of cfg.warnings) add("warn", w);
6599
- if (process.platform !== "win32") {
6909
+ if (process.platform === "win32") {
6910
+ add("warn", "git guard: not available on Windows yet, so Pitroom does not block a worker's git history changes or pushes there (read-only runs are still limited by each worker CLI's own rules, and --isolate keeps edits in a copy); prefer --isolate and check the patch before apply");
6911
+ } else {
6600
6912
  const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
6601
6913
  add(guarded ? "ok" : "warn", guarded ? "git guard shim ready" : "git guard unavailable (git not on PATH)");
6602
6914
  }
@@ -6747,20 +7059,20 @@ function skillChecks() {
6747
7059
  else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
6748
7060
  }
6749
7061
  if (geminiExtensionInstalled()) {
6750
- const linked = installedSkills().some((i) => i.base.includes(`${path28.sep}.agents${path28.sep}`) && i.names.length);
7062
+ const linked = installedSkills().some((i) => i.base.includes(`${path30.sep}.agents${path30.sep}`) && i.names.length);
6751
7063
  checks.push(
6752
7064
  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
7065
  );
6754
7066
  }
6755
7067
  if (viaPlugin) {
6756
- const linked = installedSkills().some((i) => i.base.includes(`${path28.sep}.claude${path28.sep}`) && i.names.length);
7068
+ const linked = installedSkills().some((i) => i.base.includes(`${path30.sep}.claude${path30.sep}`) && i.names.length);
6757
7069
  checks.push(
6758
7070
  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
7071
  );
6760
7072
  }
6761
7073
  const launcher = launcherPath();
6762
- if (fs32.existsSync(launcher)) {
6763
- const r = spawnSync9(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
7074
+ if (fs35.existsSync(launcher)) {
7075
+ const r = spawnSync9(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
6764
7076
  checks.push(
6765
7077
  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)}` }
6766
7078
  );
@@ -6771,12 +7083,12 @@ function skillChecks() {
6771
7083
  }
6772
7084
  function geminiExtensionInstalled() {
6773
7085
  const home3 = process.env.GEMINI_CLI_HOME ?? os11.homedir();
6774
- return fs32.existsSync(path28.join(home3, ".gemini", "extensions", "pitroom"));
7086
+ return fs35.existsSync(path30.join(home3, ".gemini", "extensions", "pitroom"));
6775
7087
  }
6776
7088
  function pluginInstalled() {
6777
7089
  try {
6778
- const f = path28.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
6779
- const plugins = JSON.parse(fs32.readFileSync(f, "utf8")).plugins ?? {};
7090
+ const f = path30.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
7091
+ const plugins = JSON.parse(fs35.readFileSync(f, "utf8")).plugins ?? {};
6780
7092
  return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
6781
7093
  } catch {
6782
7094
  return false;
@@ -6793,24 +7105,24 @@ function superpowersActive() {
6793
7105
  for (const p of openCodePlugins()) {
6794
7106
  if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
6795
7107
  }
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);
7108
+ for (const base2 of [path30.join(os11.homedir(), ".agents", "skills"), path30.join(os11.homedir(), ".claude", "skills")]) {
7109
+ const dir = path30.join(base2, "using-superpowers");
7110
+ if (fs35.existsSync(path30.join(dir, "SKILL.md"))) found.push(dir);
6799
7111
  }
6800
7112
  return found;
6801
7113
  }
6802
7114
  function claudePlugins() {
6803
7115
  try {
6804
- const f = path28.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
6805
- return Object.keys(JSON.parse(fs32.readFileSync(f, "utf8")).plugins ?? {});
7116
+ const f = path30.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
7117
+ return Object.keys(JSON.parse(fs35.readFileSync(f, "utf8")).plugins ?? {});
6806
7118
  } catch {
6807
7119
  return [];
6808
7120
  }
6809
7121
  }
6810
7122
  function claudePluginEnabled(key) {
6811
7123
  try {
6812
- const f = path28.join(os11.homedir(), ".claude", "settings.json");
6813
- return JSON.parse(fs32.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
7124
+ const f = path30.join(os11.homedir(), ".claude", "settings.json");
7125
+ return JSON.parse(fs35.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
6814
7126
  } catch {
6815
7127
  return true;
6816
7128
  }
@@ -6819,7 +7131,7 @@ function codexPlugins() {
6819
7131
  const plugins = /* @__PURE__ */ new Map();
6820
7132
  let text;
6821
7133
  try {
6822
- text = fs32.readFileSync(path28.join(process.env.CODEX_HOME ?? path28.join(os11.homedir(), ".codex"), "config.toml"), "utf8");
7134
+ text = fs35.readFileSync(path30.join(process.env.CODEX_HOME ?? path30.join(os11.homedir(), ".codex"), "config.toml"), "utf8");
6823
7135
  } catch {
6824
7136
  return plugins;
6825
7137
  }
@@ -6837,11 +7149,11 @@ function codexPlugins() {
6837
7149
  return plugins;
6838
7150
  }
6839
7151
  function openCodePlugins() {
6840
- const dir = path28.join(process.env.XDG_CONFIG_HOME ?? path28.join(os11.homedir(), ".config"), "opencode");
7152
+ const dir = path30.join(process.env.XDG_CONFIG_HOME ?? path30.join(os11.homedir(), ".config"), "opencode");
6841
7153
  const plugins = [];
6842
7154
  for (const name of ["opencode.json", "opencode.jsonc"]) {
6843
7155
  try {
6844
- const text = fs32.readFileSync(path28.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
7156
+ const text = fs35.readFileSync(path30.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
6845
7157
  const list2 = JSON.parse(text).plugin;
6846
7158
  if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
6847
7159
  } catch {
@@ -6849,7 +7161,7 @@ function openCodePlugins() {
6849
7161
  }
6850
7162
  for (const folder of ["plugin", "plugins"]) {
6851
7163
  try {
6852
- for (const f of fs32.readdirSync(path28.join(dir, folder))) plugins.push(path28.join(dir, folder, f));
7164
+ for (const f of fs35.readdirSync(path30.join(dir, folder))) plugins.push(path30.join(dir, folder, f));
6853
7165
  } catch {
6854
7166
  }
6855
7167
  }
@@ -6929,6 +7241,7 @@ Run options
6929
7241
  --in-place a read run reads the directory itself, not a clean snapshot (default: a snapshot without secret-looking
6930
7242
  files when the directory has any; config "readIn": auto | snapshot | project)
6931
7243
  --audit have another worker re-check this read run's answer; --no-audit skips it (default: the config's "audit")
7244
+ --fresh ask a worker even when the same read question was answered on the same code (config "cacheDays", default 7)
6932
7245
  --effort LEVEL reasoning effort for the worker: low, medium, high, xhigh, \u2026 (model#level)
6933
7246
  --plan PLAN with --step N: implement Task N of a plan (-i or -w); the task text is your notes
6934
7247
  --step N the plan task for --plan
@@ -6950,9 +7263,9 @@ Exit codes: 0 ok \xB7 1 worker failed \xB7 2 usage \xB7 3 refused/setup \xB7 4 t
6950
7263
  5 read-only violation \xB7 6 verify failed \xB7 75 still running (wait again)
6951
7264
  Workers: ${backendIds().join(", ")} (targets: "opencode", "opencode:provider/model", or a bare model)
6952
7265
  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,
7266
+ PITROOM_PRIMARY=sonnet|opus|haiku|gpt-5, PITROOM_PRICE="in,out", PITROOM_HOME, PITROOM_CONFIG, PITROOM_CACHE_DAYS,
6954
7267
  PITROOM_<WORKER>_BIN
6955
- Config: ~/.config/pitroom/config.json (worker, fallback, models, tiers, timeout, primary, price, link, web, maxParallel)`;
7268
+ Config: ~/.config/pitroom/config.json (worker, fallback, models, tiers, timeout, primary, price, link, web, maxParallel, audit, readIn, cacheDays)`;
6956
7269
  var COMMANDS = {
6957
7270
  run: cmdRun,
6958
7271
  crew: cmdCrew,
@@ -6965,7 +7278,7 @@ var COMMANDS = {
6965
7278
  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
7279
  const dir = flag(p, "dir");
6967
7280
  if (dir !== void 0) {
6968
- if (!fs33.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) throw new UserError(`-d ${dir}: no such directory`);
7281
+ if (!fs36.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) throw new UserError(`-d ${dir}: no such directory`);
6969
7282
  process.chdir(dir);
6970
7283
  process.env.PWD = process.cwd();
6971
7284
  }