pitroom 0.17.0 → 0.19.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 fs35 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 = {
@@ -1434,10 +1454,10 @@ function loadConfig() {
1434
1454
  const file2 = configPath();
1435
1455
  const config = {};
1436
1456
  const warnings = [];
1437
- if (fs7.existsSync(file2)) {
1457
+ if (fs8.existsSync(file2)) {
1438
1458
  let raw;
1439
1459
  try {
1440
- raw = JSON.parse(fs7.readFileSync(file2, "utf8"));
1460
+ raw = JSON.parse(fs8.readFileSync(file2, "utf8"));
1441
1461
  } catch (e) {
1442
1462
  warnings.push(`${file2}: invalid JSON (${e.message}); ignored`);
1443
1463
  }
@@ -1644,9 +1664,9 @@ function exitCodeFor(meta) {
1644
1664
  }
1645
1665
  function readTask(p) {
1646
1666
  const file2 = flag(p, "task-file");
1647
- if (file2) return fs8.readFileSync(file2, "utf8");
1667
+ if (file2) return fs9.readFileSync(file2, "utf8");
1648
1668
  const words = p.positional;
1649
- if (words.length === 1 && words[0] === "-") return fs8.readFileSync(0, "utf8");
1669
+ if (words.length === 1 && words[0] === "-") return fs9.readFileSync(0, "utf8");
1650
1670
  return words.join(" ");
1651
1671
  }
1652
1672
  function runOptions(p, task) {
@@ -1695,13 +1715,13 @@ import readline from "node:readline";
1695
1715
 
1696
1716
  // src/core/run.ts
1697
1717
  import { spawn as spawn2, spawnSync as spawnSync6 } from "node:child_process";
1698
- import fs24 from "node:fs";
1718
+ import fs25 from "node:fs";
1699
1719
  import path20 from "node:path";
1700
1720
 
1701
1721
  // src/vcs/git.ts
1702
1722
  import { spawnSync as spawnSync5 } from "node:child_process";
1703
1723
  import crypto2 from "node:crypto";
1704
- import fs9 from "node:fs";
1724
+ import fs10 from "node:fs";
1705
1725
  import os8 from "node:os";
1706
1726
  import path8 from "node:path";
1707
1727
  var SAFE = [
@@ -1733,16 +1753,23 @@ function must(cwd, args, env) {
1733
1753
  function gitAvailable() {
1734
1754
  return git(process.cwd(), ["--version"]).code === 0;
1735
1755
  }
1756
+ function canonical(dir) {
1757
+ try {
1758
+ return fs10.realpathSync.native(dir);
1759
+ } catch {
1760
+ return dir;
1761
+ }
1762
+ }
1736
1763
  function repoRoot(dir) {
1737
1764
  const r = git(dir, ["rev-parse", "--show-toplevel"]);
1738
- return r.code === 0 ? path8.resolve(r.stdout.trim()) : void 0;
1765
+ return r.code === 0 ? canonical(path8.resolve(r.stdout.trim())) : void 0;
1739
1766
  }
1740
1767
  function snapshotTree(root, exclude = [], drop2 = []) {
1741
1768
  const tmp = path8.join(os8.tmpdir(), `pitroom-index-${process.pid}-${crypto2.randomBytes(4).toString("hex")}`);
1742
1769
  const real = path8.resolve(root, must(root, ["rev-parse", "--git-path", "index"]).trim());
1743
1770
  const env = { ...process.env, GIT_INDEX_FILE: tmp };
1744
1771
  try {
1745
- if (fs9.existsSync(real)) fs9.copyFileSync(real, tmp);
1772
+ if (fs10.existsSync(real)) fs10.copyFileSync(real, tmp);
1746
1773
  else must(root, ["read-tree", "--empty"], env);
1747
1774
  must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`), ...drop2.map((p) => `:(top,literal,exclude)${p}`)], env);
1748
1775
  if (drop2.length) {
@@ -1751,8 +1778,8 @@ function snapshotTree(root, exclude = [], drop2 = []) {
1751
1778
  }
1752
1779
  return must(root, ["write-tree"], env).trim();
1753
1780
  } finally {
1754
- fs9.rmSync(tmp, { force: true });
1755
- fs9.rmSync(`${tmp}.lock`, { force: true });
1781
+ fs10.rmSync(tmp, { force: true });
1782
+ fs10.rmSync(`${tmp}.lock`, { force: true });
1756
1783
  }
1757
1784
  }
1758
1785
  var DIFF = ["diff", "--no-ext-diff", "--no-textconv", "--no-renames"];
@@ -1773,9 +1800,9 @@ function diffTrees(root, a, b) {
1773
1800
  }
1774
1801
  function createIsolatedCopy(root, tree, dest) {
1775
1802
  const objects = path8.join(path8.resolve(root, must(root, ["rev-parse", "--git-common-dir"]).trim()), "objects");
1776
- fs9.mkdirSync(path8.dirname(dest), { recursive: true });
1803
+ fs10.mkdirSync(path8.dirname(dest), { recursive: true });
1777
1804
  must(path8.dirname(dest), ["init", "--quiet", dest]);
1778
- fs9.writeFileSync(path8.join(dest, ".git", "objects", "info", "alternates"), `${objects}
1805
+ fs10.writeFileSync(path8.join(dest, ".git", "objects", "info", "alternates"), `${objects}
1779
1806
  `);
1780
1807
  must(dest, ["read-tree", tree]);
1781
1808
  must(dest, ["checkout-index", "--all", "--force"]);
@@ -1783,16 +1810,16 @@ function createIsolatedCopy(root, tree, dest) {
1783
1810
  function removeIsolatedCopy(dest, ownedBy) {
1784
1811
  const rel = path8.relative(ownedBy, dest);
1785
1812
  if (!rel || rel.startsWith("..") || path8.isAbsolute(rel)) throw new Error(`refusing to remove ${dest}`);
1786
- fs9.rmSync(dest, { recursive: true, force: true });
1813
+ fs10.rmSync(dest, { recursive: true, force: true });
1787
1814
  }
1788
1815
  function linkIntoWorktree(root, worktree, rels) {
1789
1816
  const linked = [];
1790
1817
  for (const rel of rels) {
1791
1818
  const src = path8.join(root, rel);
1792
1819
  const dst = path8.join(worktree, rel);
1793
- if (!fs9.existsSync(src) || fs9.existsSync(dst)) continue;
1794
- fs9.mkdirSync(path8.dirname(dst), { recursive: true });
1795
- 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);
1796
1823
  linked.push(rel);
1797
1824
  }
1798
1825
  return linked;
@@ -1898,10 +1925,10 @@ function resolveChain(flags = {}) {
1898
1925
 
1899
1926
  // src/core/process.ts
1900
1927
  import { spawn } from "node:child_process";
1901
- import fs11 from "node:fs";
1928
+ import fs12 from "node:fs";
1902
1929
 
1903
1930
  // src/vcs/guard.ts
1904
- import fs10 from "node:fs";
1931
+ import fs11 from "node:fs";
1905
1932
  import path9 from "node:path";
1906
1933
  var VERSION = 1;
1907
1934
  var SHIM = `#!/bin/sh
@@ -1984,8 +2011,8 @@ function findRealGit(skip) {
1984
2011
  if (path9.resolve(dir) === skip) continue;
1985
2012
  const p = path9.join(dir, "git");
1986
2013
  try {
1987
- fs10.accessSync(p, fs10.constants.X_OK);
1988
- if (fs10.statSync(p).isFile()) return p;
2014
+ fs11.accessSync(p, fs11.constants.X_OK);
2015
+ if (fs11.statSync(p).isFile()) return p;
1989
2016
  } catch {
1990
2017
  }
1991
2018
  }
@@ -2000,11 +2027,11 @@ exit 1
2000
2027
  `;
2001
2028
  var hooksDir = () => path9.join(home(), "git-hooks", `v${HOOK_VERSION}`);
2002
2029
  function writeIfChanged(file2, content) {
2003
- if (fs10.existsSync(file2) && fs10.readFileSync(file2, "utf8") === content) return;
2004
- 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 });
2005
2032
  const tmp = `${file2}.${process.pid}.tmp`;
2006
- fs10.writeFileSync(tmp, content, { mode: 493 });
2007
- fs10.renameSync(tmp, file2);
2033
+ fs11.writeFileSync(tmp, content, { mode: 493 });
2034
+ fs11.renameSync(tmp, file2);
2008
2035
  }
2009
2036
  function withGitConfig(env, entries) {
2010
2037
  const out = { ...env };
@@ -2040,8 +2067,8 @@ function guardEnv(env) {
2040
2067
 
2041
2068
  // src/core/process.ts
2042
2069
  async function spawnWorker(inv, opts) {
2043
- const out = fs11.openSync(opts.stdoutFile, "w");
2044
- const err = fs11.openSync(opts.stderrFile, "w");
2070
+ const out = fs12.openSync(opts.stdoutFile, "w");
2071
+ const err = fs12.openSync(opts.stderrFile, "w");
2045
2072
  const res = { code: null, timedOut: false, stopped: false };
2046
2073
  const child = spawn(inv.command, inv.args, {
2047
2074
  cwd: opts.cwd,
@@ -2064,7 +2091,7 @@ async function spawnWorker(inv, opts) {
2064
2091
  }, opts.timeoutSec * 1e3);
2065
2092
  res.code = await new Promise((resolve2) => {
2066
2093
  child.on("error", (e) => {
2067
- 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;
2068
2095
  resolve2(127);
2069
2096
  });
2070
2097
  child.on("close", (c) => resolve2(c));
@@ -2073,8 +2100,8 @@ async function spawnWorker(inv, opts) {
2073
2100
  if (killer) clearTimeout(killer);
2074
2101
  process.off("SIGINT", stop);
2075
2102
  process.off("SIGTERM", stop);
2076
- fs11.closeSync(out);
2077
- fs11.closeSync(err);
2103
+ fs12.closeSync(out);
2104
+ fs12.closeSync(err);
2078
2105
  return res;
2079
2106
  }
2080
2107
 
@@ -2106,7 +2133,7 @@ function parseAudit(text) {
2106
2133
  }
2107
2134
 
2108
2135
  // src/core/cooldown.ts
2109
- import fs12 from "node:fs";
2136
+ import fs13 from "node:fs";
2110
2137
  import path10 from "node:path";
2111
2138
  var MAX_MS = 24 * 36e5;
2112
2139
  var OVERLOAD_MS = 20 * 6e4;
@@ -2134,18 +2161,18 @@ function cooldownMs(message) {
2134
2161
  }
2135
2162
  function read() {
2136
2163
  try {
2137
- const raw = JSON.parse(fs12.readFileSync(file(), "utf8"));
2164
+ const raw = JSON.parse(fs13.readFileSync(file(), "utf8"));
2138
2165
  return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
2139
2166
  } catch {
2140
2167
  return {};
2141
2168
  }
2142
2169
  }
2143
2170
  function write(all) {
2144
- fs12.mkdirSync(path10.dirname(file()), { recursive: true });
2171
+ fs13.mkdirSync(path10.dirname(file()), { recursive: true });
2145
2172
  const tmp = `${file()}.${process.pid}.tmp`;
2146
- fs12.writeFileSync(tmp, `${JSON.stringify(all, null, 2)}
2173
+ fs13.writeFileSync(tmp, `${JSON.stringify(all, null, 2)}
2147
2174
  `);
2148
- fs12.renameSync(tmp, file());
2175
+ renameOver(tmp, file());
2149
2176
  }
2150
2177
  function activeCooldowns(now = Date.now()) {
2151
2178
  return Object.fromEntries(Object.entries(read()).filter(([, c]) => Date.parse(c.until) > now));
@@ -2173,13 +2200,13 @@ function untilText(c, now = Date.now()) {
2173
2200
  }
2174
2201
 
2175
2202
  // src/core/snapshot.ts
2176
- import fs14 from "node:fs";
2203
+ import fs15 from "node:fs";
2177
2204
  import path12 from "node:path";
2178
2205
  import crypto3 from "node:crypto";
2179
2206
 
2180
2207
  // src/core/secrets.ts
2181
2208
  import { execFileSync } from "node:child_process";
2182
- import fs13 from "node:fs";
2209
+ import fs14 from "node:fs";
2183
2210
  import path11 from "node:path";
2184
2211
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
2185
2212
  var MAX_VISITED = 2e4;
@@ -2197,7 +2224,7 @@ function findSecretFiles(dir) {
2197
2224
  if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
2198
2225
  let entries;
2199
2226
  try {
2200
- entries = fs13.readdirSync(d, { withFileTypes: true });
2227
+ entries = fs14.readdirSync(d, { withFileTypes: true });
2201
2228
  } catch {
2202
2229
  return;
2203
2230
  }
@@ -2206,7 +2233,7 @@ function findSecretFiles(dir) {
2206
2233
  if (e.isDirectory()) {
2207
2234
  if (!SKIP_DIRS.has(e.name)) walk(path11.join(d, e.name), depth + 1);
2208
2235
  } else if (e.isFile() && looksSecret(e.name)) {
2209
- found.push(path11.relative(dir, path11.join(d, e.name)));
2236
+ found.push(slash(path11.relative(dir, path11.join(d, e.name))));
2210
2237
  }
2211
2238
  }
2212
2239
  };
@@ -2216,7 +2243,7 @@ function findSecretFiles(dir) {
2216
2243
  function findSecretFilesInTree(root, dir) {
2217
2244
  try {
2218
2245
  const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
2219
- 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();
2220
2247
  } catch {
2221
2248
  return [];
2222
2249
  }
@@ -2228,6 +2255,7 @@ function secretWarning(files, mode) {
2228
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";
2229
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`;
2230
2257
  }
2258
+ var slash = (p) => p.split(path11.sep).join("/");
2231
2259
 
2232
2260
  // src/core/snapshot.ts
2233
2261
  var READ_IN = ["auto", "snapshot", "project"];
@@ -2245,7 +2273,7 @@ function readSnapshot(root) {
2245
2273
  const drop2 = findSecretFilesInTree(root, root);
2246
2274
  const tree = snapshotTree(root, [], drop2);
2247
2275
  const dest = path12.join(snapshotsDir(), tree);
2248
- if (fs14.existsSync(path12.join(dest, ".git"))) {
2276
+ if (fs15.existsSync(path12.join(dest, ".git"))) {
2249
2277
  touch(dest);
2250
2278
  return { dir: dest, tree, created: false };
2251
2279
  }
@@ -2254,9 +2282,9 @@ function readSnapshot(root) {
2254
2282
  createIsolatedCopy(root, tree, tmp);
2255
2283
  readOnly(tmp);
2256
2284
  try {
2257
- fs14.renameSync(tmp, dest);
2285
+ fs15.renameSync(tmp, dest);
2258
2286
  } catch (e) {
2259
- if (!fs14.existsSync(path12.join(dest, ".git"))) throw e;
2287
+ if (!fs15.existsSync(path12.join(dest, ".git"))) throw e;
2260
2288
  removeIsolatedCopy(tmp, snapshotsDir());
2261
2289
  }
2262
2290
  } catch (e) {
@@ -2272,17 +2300,17 @@ function readSnapshot(root) {
2272
2300
  function touch(dir) {
2273
2301
  try {
2274
2302
  const now = /* @__PURE__ */ new Date();
2275
- fs14.utimesSync(dir, now, now);
2303
+ fs15.utimesSync(dir, now, now);
2276
2304
  } catch {
2277
2305
  }
2278
2306
  }
2279
2307
  function readOnly(dir) {
2280
2308
  const walk = (d) => {
2281
- for (const e of fs14.readdirSync(d, { withFileTypes: true })) {
2309
+ for (const e of fs15.readdirSync(d, { withFileTypes: true })) {
2282
2310
  if (e.name === ".git") continue;
2283
2311
  const p = path12.join(d, e.name);
2284
2312
  if (e.isDirectory()) walk(p);
2285
- 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);
2286
2314
  }
2287
2315
  };
2288
2316
  try {
@@ -2293,7 +2321,7 @@ function readOnly(dir) {
2293
2321
  function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWEST) {
2294
2322
  let names;
2295
2323
  try {
2296
- names = fs14.readdirSync(snapshotsDir());
2324
+ names = fs15.readdirSync(snapshotsDir());
2297
2325
  } catch {
2298
2326
  return 0;
2299
2327
  }
@@ -2308,7 +2336,7 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
2308
2336
  const entries = names.map((n) => {
2309
2337
  const dir = path12.join(snapshotsDir(), n);
2310
2338
  try {
2311
- return { n, dir, age: now - fs14.statSync(dir).mtimeMs };
2339
+ return { n, dir, age: now - fs15.statSync(dir).mtimeMs };
2312
2340
  } catch {
2313
2341
  return void 0;
2314
2342
  }
@@ -2332,10 +2360,10 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
2332
2360
  }
2333
2361
  function makeWritable(dir) {
2334
2362
  const walk = (d) => {
2335
- for (const e of fs14.readdirSync(d, { withFileTypes: true })) {
2363
+ for (const e of fs15.readdirSync(d, { withFileTypes: true })) {
2336
2364
  const p = path12.join(d, e.name);
2337
2365
  if (e.isDirectory()) walk(p);
2338
- 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);
2339
2367
  }
2340
2368
  };
2341
2369
  try {
@@ -2346,7 +2374,7 @@ function makeWritable(dir) {
2346
2374
  function backToProject(text, snapshotDir, root) {
2347
2375
  let real = snapshotDir;
2348
2376
  try {
2349
- real = fs14.realpathSync(snapshotDir);
2377
+ real = fs15.realpathSync(snapshotDir);
2350
2378
  } catch {
2351
2379
  }
2352
2380
  let out = text;
@@ -2427,7 +2455,7 @@ function auditBadge(meta) {
2427
2455
  }
2428
2456
 
2429
2457
  // src/core/plan.ts
2430
- import fs15 from "node:fs";
2458
+ import fs16 from "node:fs";
2431
2459
  import path13 from "node:path";
2432
2460
  var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
2433
2461
  var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
@@ -2476,8 +2504,8 @@ function parsePlan(text, file2 = "") {
2476
2504
  }
2477
2505
  function loadPlan(file2) {
2478
2506
  const abs = path13.resolve(file2);
2479
- if (!fs15.existsSync(abs) || !fs15.statSync(abs).isFile()) throw new UserError(`plan not found: ${file2}`);
2480
- 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));
2481
2509
  if (!plan.tasks.length) throw new UserError(`${file2} has no "Task N" headings (see pitroom-writing-plans)`);
2482
2510
  return plan;
2483
2511
  }
@@ -2499,11 +2527,11 @@ function brief(plan, task) {
2499
2527
  var planName = (file2) => path13.basename(file2).replace(/\.md$/i, "");
2500
2528
 
2501
2529
  // src/core/templates.ts
2502
- import fs17 from "node:fs";
2530
+ import fs18 from "node:fs";
2503
2531
  import path15 from "node:path";
2504
2532
 
2505
2533
  // src/core/install.ts
2506
- import fs16 from "node:fs";
2534
+ import fs17 from "node:fs";
2507
2535
  import os9 from "node:os";
2508
2536
  import path14 from "node:path";
2509
2537
  import { fileURLToPath as fileURLToPath2 } from "node:url";
@@ -2513,18 +2541,30 @@ function packageRoot() {
2513
2541
  }
2514
2542
  function skillNames(root = packageRoot()) {
2515
2543
  const dir = path14.join(root, "skills");
2516
- if (!fs16.existsSync(dir)) return [];
2517
- 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();
2518
2546
  }
2519
2547
  function skillTargets() {
2520
2548
  const home3 = os9.homedir();
2521
2549
  const targets = [path14.join(home3, ".agents", "skills")];
2522
- 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"));
2523
2551
  return targets;
2524
2552
  }
2525
- var launcherPath = () => path14.join(os9.homedir(), ".local", "bin", "pitroom");
2553
+ var launcherPath = () => path14.join(os9.homedir(), ".local", "bin", process.platform === "win32" ? "pitroom.cmd" : "pitroom");
2526
2554
  var LAUNCHER_MARK = "# pitroom launcher";
2527
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
+ }
2528
2568
  const q = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
2529
2569
  return `#!/bin/sh
2530
2570
  ${LAUNCHER_MARK} (created by \`pitroom install\`; \`pitroom uninstall\` removes it)
@@ -2540,7 +2580,7 @@ exit 127
2540
2580
  function isOurLauncher(file2, root) {
2541
2581
  if (linksInto(file2, root)) return true;
2542
2582
  try {
2543
- const s = fs16.readFileSync(file2, "utf8");
2583
+ const s = fs17.readFileSync(file2, "utf8");
2544
2584
  return s.includes(LAUNCHER_MARK) && s.includes(root);
2545
2585
  } catch {
2546
2586
  return false;
@@ -2548,42 +2588,42 @@ function isOurLauncher(file2, root) {
2548
2588
  }
2549
2589
  function isPitroomLauncher(file2) {
2550
2590
  try {
2551
- return fs16.readFileSync(file2, "utf8").includes(LAUNCHER_MARK);
2591
+ return fs17.readFileSync(file2, "utf8").includes(LAUNCHER_MARK);
2552
2592
  } catch {
2553
2593
  return false;
2554
2594
  }
2555
2595
  }
2556
2596
  function placeLauncher(bundle, root, force) {
2557
2597
  const dest = launcherPath();
2558
- fs16.mkdirSync(path14.dirname(dest), { recursive: true });
2559
- const exists = fs16.lstatSync(dest, { throwIfNoEntry: false });
2598
+ fs17.mkdirSync(path14.dirname(dest), { recursive: true });
2599
+ const exists = fs17.lstatSync(dest, { throwIfNoEntry: false });
2560
2600
  if (exists && !isOurLauncher(dest, root)) {
2561
2601
  if (!force) return `! ${dest} exists and is not Pitroom's launcher; kept (use --force to back it up and replace)`;
2562
- fs16.renameSync(dest, `${dest}.bak-${Date.now()}`);
2602
+ fs17.renameSync(dest, `${dest}.bak-${Date.now()}`);
2563
2603
  } else if (exists) {
2564
- fs16.rmSync(dest, { force: true });
2604
+ fs17.rmSync(dest, { force: true });
2565
2605
  }
2566
- fs16.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
2606
+ fs17.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
2567
2607
  return `\u2714 ${dest} \u2192 launcher for ${bundle} (Node 22.13+)`;
2568
2608
  }
2569
2609
  function linksInto(link, dir) {
2570
- const st = fs16.lstatSync(link, { throwIfNoEntry: false });
2610
+ const st = fs17.lstatSync(link, { throwIfNoEntry: false });
2571
2611
  if (!st?.isSymbolicLink()) return false;
2572
- const target = path14.resolve(path14.dirname(link), fs16.readlinkSync(link));
2612
+ const target = path14.resolve(path14.dirname(link), fs17.readlinkSync(link));
2573
2613
  const rel = path14.relative(dir, target);
2574
2614
  return !rel.startsWith("..") && !path14.isAbsolute(rel);
2575
2615
  }
2576
2616
  function place(src, dest, opts) {
2577
- fs16.mkdirSync(path14.dirname(dest), { recursive: true });
2578
- const st = fs16.lstatSync(dest, { throwIfNoEntry: false });
2617
+ fs17.mkdirSync(path14.dirname(dest), { recursive: true });
2618
+ const st = fs17.lstatSync(dest, { throwIfNoEntry: false });
2579
2619
  if (st && !st.isSymbolicLink()) {
2580
2620
  if (!opts.force) return `! ${dest} exists and is not a link; kept (use --force to back it up and replace)`;
2581
- fs16.renameSync(dest, `${dest}.bak-${Date.now()}`);
2621
+ fs17.renameSync(dest, `${dest}.bak-${Date.now()}`);
2582
2622
  } else if (st) {
2583
- fs16.unlinkSync(dest);
2623
+ fs17.unlinkSync(dest);
2584
2624
  }
2585
- if (opts.copy) fs16.cpSync(src, dest, { recursive: true });
2586
- 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");
2587
2627
  return `\u2714 ${dest} \u2192 ${opts.copy ? "copied" : src}`;
2588
2628
  }
2589
2629
  function install(opts) {
@@ -2595,14 +2635,14 @@ function install(opts) {
2595
2635
  for (const legacy of LEGACY) {
2596
2636
  const l = path14.join(base2, legacy);
2597
2637
  if (!skills2.includes(legacy) && linksInto(l, root)) {
2598
- fs16.unlinkSync(l);
2638
+ fs17.unlinkSync(l);
2599
2639
  out.push(`\u2714 removed old link ${l}`);
2600
2640
  }
2601
2641
  }
2602
2642
  for (const name of skills2) out.push(place(path14.join(root, "skills", name), path14.join(base2, name), opts));
2603
2643
  }
2604
2644
  const bundle = path14.join(root, "dist", "pitroom.mjs");
2605
- if (fs16.existsSync(bundle)) {
2645
+ if (fs17.existsSync(bundle)) {
2606
2646
  out.push(placeLauncher(bundle, root, opts.force));
2607
2647
  const onPath = (process.env.PATH ?? "").split(path14.delimiter).some((d) => path14.resolve(d) === path14.dirname(launcherPath()));
2608
2648
  if (!onPath) out.push(`! ${path14.dirname(launcherPath())} is not on PATH; add it, or run ${launcherPath()} directly`);
@@ -2613,24 +2653,24 @@ function uninstall() {
2613
2653
  const root = packageRoot();
2614
2654
  const out = [];
2615
2655
  for (const base2 of skillTargets()) {
2616
- if (!fs16.existsSync(base2)) continue;
2617
- for (const name of fs16.readdirSync(base2)) {
2656
+ if (!fs17.existsSync(base2)) continue;
2657
+ for (const name of fs17.readdirSync(base2)) {
2618
2658
  const l = path14.join(base2, name);
2619
2659
  if (linksInto(l, root)) {
2620
- fs16.unlinkSync(l);
2660
+ fs17.unlinkSync(l);
2621
2661
  out.push(`\u2714 removed ${l}`);
2622
2662
  }
2623
2663
  }
2624
2664
  }
2625
2665
  if (isOurLauncher(launcherPath(), root)) {
2626
- fs16.rmSync(launcherPath(), { force: true });
2666
+ fs17.rmSync(launcherPath(), { force: true });
2627
2667
  out.push(`\u2714 removed ${launcherPath()}`);
2628
2668
  }
2629
2669
  return out.length ? out : ["nothing to remove"];
2630
2670
  }
2631
2671
  function installedSkills() {
2632
2672
  const names = skillNames();
2633
- 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"))) }));
2634
2674
  }
2635
2675
 
2636
2676
  // src/core/templates.ts
@@ -2642,8 +2682,8 @@ var FILES = {
2642
2682
  };
2643
2683
  function loadTemplate(name, root = packageRoot()) {
2644
2684
  const file2 = path15.join(root, "skills", FILES[name]);
2645
- if (!fs17.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
2646
- 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*/, "");
2647
2687
  }
2648
2688
  function fill(template, values) {
2649
2689
  const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
@@ -2653,7 +2693,7 @@ function fill(template, values) {
2653
2693
 
2654
2694
  // src/core/slots.ts
2655
2695
  import crypto5 from "node:crypto";
2656
- import fs18 from "node:fs";
2696
+ import fs19 from "node:fs";
2657
2697
  import path16 from "node:path";
2658
2698
  var slotsDir = () => path16.join(home(), "slots");
2659
2699
  var locksDir = () => path16.join(home(), "locks");
@@ -2670,23 +2710,23 @@ function holderActive(runId, self) {
2670
2710
  }
2671
2711
  }
2672
2712
  function tryClaim(file2, runId) {
2673
- fs18.mkdirSync(path16.dirname(file2), { recursive: true });
2713
+ fs19.mkdirSync(path16.dirname(file2), { recursive: true });
2674
2714
  try {
2675
- fs18.writeFileSync(file2, runId, { flag: "wx" });
2715
+ fs19.writeFileSync(file2, runId, { flag: "wx" });
2676
2716
  return true;
2677
2717
  } catch (e) {
2678
2718
  if (e.code !== "EEXIST") throw e;
2679
2719
  }
2680
2720
  let owner = "";
2681
2721
  try {
2682
- owner = fs18.readFileSync(file2, "utf8").trim();
2722
+ owner = fs19.readFileSync(file2, "utf8").trim();
2683
2723
  } catch {
2684
2724
  }
2685
2725
  if (owner === runId) return true;
2686
2726
  if (holderActive(owner, runId)) return false;
2687
- fs18.rmSync(file2, { force: true });
2727
+ fs19.rmSync(file2, { force: true });
2688
2728
  try {
2689
- fs18.writeFileSync(file2, runId, { flag: "wx" });
2729
+ fs19.writeFileSync(file2, runId, { flag: "wx" });
2690
2730
  return true;
2691
2731
  } catch {
2692
2732
  return false;
@@ -2695,7 +2735,7 @@ function tryClaim(file2, runId) {
2695
2735
  function releaseIfOwner(file2, runId) {
2696
2736
  if (!file2) return;
2697
2737
  try {
2698
- 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 });
2699
2739
  } catch {
2700
2740
  }
2701
2741
  }
@@ -2713,7 +2753,7 @@ function acquireWriteLock(repoRoot2, runId) {
2713
2753
  if (tryClaim(file2, runId)) return;
2714
2754
  let owner = "";
2715
2755
  try {
2716
- owner = fs18.readFileSync(file2, "utf8").trim();
2756
+ owner = fs19.readFileSync(file2, "utf8").trim();
2717
2757
  } catch {
2718
2758
  }
2719
2759
  throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
@@ -2760,7 +2800,7 @@ ${task}`;
2760
2800
  }
2761
2801
 
2762
2802
  // src/core/receipt.ts
2763
- import fs19 from "node:fs";
2803
+ import fs20 from "node:fs";
2764
2804
  import path17 from "node:path";
2765
2805
  var PRESETS = {
2766
2806
  sonnet: { name: "Claude Sonnet", input: 3, output: 15, cachedInput: 0.3 },
@@ -2797,13 +2837,13 @@ function record(meta) {
2797
2837
  saved: meta.savedUsd ?? 0,
2798
2838
  price: primaryPrice().name
2799
2839
  };
2800
- fs19.mkdirSync(path17.dirname(ledgerFile()), { recursive: true });
2801
- fs19.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
2840
+ fs20.mkdirSync(path17.dirname(ledgerFile()), { recursive: true });
2841
+ fs20.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
2802
2842
  `);
2803
2843
  }
2804
2844
  function readLedger(sinceMs2) {
2805
- if (!fs19.existsSync(ledgerFile())) return [];
2806
- 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) => {
2807
2847
  try {
2808
2848
  return [JSON.parse(l)];
2809
2849
  } catch {
@@ -2878,7 +2918,7 @@ ${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.rou
2878
2918
  }
2879
2919
 
2880
2920
  // src/core/refs.ts
2881
- import fs20 from "node:fs";
2921
+ import fs21 from "node:fs";
2882
2922
  import path18 from "node:path";
2883
2923
  var FILE_EXTENSIONS = new Set(
2884
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(" ")
@@ -2935,8 +2975,8 @@ function verifyRefs(refs, dirs) {
2935
2975
  const lineCache = /* @__PURE__ */ new Map();
2936
2976
  const load = (file2) => {
2937
2977
  if (!lineCache.has(file2)) {
2938
- const size = fs20.statSync(file2).size;
2939
- 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");
2940
2980
  if (lines && lines[lines.length - 1] === "") lines.pop();
2941
2981
  lineCache.set(file2, lines);
2942
2982
  }
@@ -2949,7 +2989,7 @@ function verifyRefs(refs, dirs) {
2949
2989
  if (direct) return [direct];
2950
2990
  if (path18.isAbsolute(ref.file)) return [];
2951
2991
  const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
2952
- 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);
2953
2993
  };
2954
2994
  const check = (file2, ref) => {
2955
2995
  const lines = load(file2);
@@ -2994,7 +3034,7 @@ function indexFiles(roots) {
2994
3034
  const walk = (dir) => {
2995
3035
  let entries;
2996
3036
  try {
2997
- entries = fs20.readdirSync(dir, { withFileTypes: true });
3037
+ entries = fs21.readdirSync(dir, { withFileTypes: true });
2998
3038
  } catch {
2999
3039
  return;
3000
3040
  }
@@ -3017,7 +3057,7 @@ function resolve(ref, roots) {
3017
3057
  const candidates = path18.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path18.join(r, ref));
3018
3058
  return candidates.find((c) => {
3019
3059
  try {
3020
- return fs20.statSync(c).isFile() && inside(c, roots);
3060
+ return fs21.statSync(c).isFile() && inside(c, roots);
3021
3061
  } catch {
3022
3062
  return false;
3023
3063
  }
@@ -3032,13 +3072,13 @@ function inside(file2, roots) {
3032
3072
  var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3033
3073
 
3034
3074
  // src/core/history.ts
3035
- import fs22 from "node:fs";
3075
+ import fs23 from "node:fs";
3036
3076
  import { createRequire } from "node:module";
3037
3077
  import path19 from "node:path";
3038
3078
  import zlib from "node:zlib";
3039
3079
 
3040
3080
  // src/core/ui.ts
3041
- import fs21 from "node:fs";
3081
+ import fs22 from "node:fs";
3042
3082
  var WEEK_MS = 7 * 24 * 3600 * 1e3;
3043
3083
  var RECENT = 40;
3044
3084
  var RECENT_HOURS = 1;
@@ -3129,9 +3169,9 @@ function cardsFor(m) {
3129
3169
  const out = [];
3130
3170
  for (const [phase, text] of phases) {
3131
3171
  const marker = runFile(m.id, `card-${phase}`);
3132
- if (fs21.existsSync(marker)) continue;
3172
+ if (fs22.existsSync(marker)) continue;
3133
3173
  if (!(phase === "started" && !isActive(m.state))) out.push(text());
3134
- fs21.writeFileSync(marker, "");
3174
+ fs22.writeFileSync(marker, "");
3135
3175
  }
3136
3176
  return out;
3137
3177
  }
@@ -3142,10 +3182,10 @@ function hookCards(input) {
3142
3182
  const response = event.tool_response;
3143
3183
  const output = typeof response === "string" ? response : [response?.stdout, response?.stderr].filter(Boolean).join("\n");
3144
3184
  const ids = [...new Set(`${command}
3145
- ${output}`.match(RUN_ID) ?? [])].filter((id) => fs21.existsSync(`${runsDir()}/${id}`));
3185
+ ${output}`.match(RUN_ID) ?? [])].filter((id) => fs22.existsSync(`${runsDir()}/${id}`));
3146
3186
  const since2 = Date.now() - RECENT_HOURS * 36e5;
3147
3187
  for (const id of listRunIds().slice(-RECENT)) {
3148
- if (ids.includes(id) || fs21.existsSync(runFile(id, "card-ended"))) continue;
3188
+ if (ids.includes(id) || fs22.existsSync(runFile(id, "card-ended"))) continue;
3149
3189
  try {
3150
3190
  const m = readMeta(id);
3151
3191
  if (!isActive(m.state) && Date.parse(m.endedAt ?? m.startedAt) > since2) ids.push(id);
@@ -3181,7 +3221,7 @@ function openDb() {
3181
3221
  } finally {
3182
3222
  process.emitWarning = emit;
3183
3223
  }
3184
- fs22.mkdirSync(path19.dirname(file2), { recursive: true });
3224
+ fs23.mkdirSync(path19.dirname(file2), { recursive: true });
3185
3225
  db = new DatabaseSync(file2);
3186
3226
  db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
3187
3227
  migrate(db);
@@ -3216,30 +3256,30 @@ function migrate(db) {
3216
3256
  function readRunFile(id, name) {
3217
3257
  const f = runFile(id, name);
3218
3258
  try {
3219
- if (fs22.existsSync(f)) return fs22.readFileSync(f, "utf8");
3220
- 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");
3221
3261
  } catch {
3222
3262
  }
3223
3263
  return void 0;
3224
3264
  }
3225
3265
  function gzipFile(f) {
3226
- if (!fs22.existsSync(f)) return;
3227
- fs22.writeFileSync(`${f}.gz`, zlib.gzipSync(fs22.readFileSync(f)));
3228
- fs22.rmSync(f);
3266
+ if (!fs23.existsSync(f)) return;
3267
+ fs23.writeFileSync(`${f}.gz`, zlib.gzipSync(fs23.readFileSync(f)));
3268
+ fs23.rmSync(f);
3229
3269
  }
3230
3270
  function compactRun(meta) {
3231
3271
  if (!TERMINAL.includes(meta.state)) return;
3232
3272
  const dir = path19.dirname(runFile(meta.id, "meta.json"));
3233
3273
  let names;
3234
3274
  try {
3235
- names = fs22.readdirSync(dir);
3275
+ names = fs23.readdirSync(dir);
3236
3276
  } catch {
3237
3277
  return;
3238
3278
  }
3239
3279
  for (const n of names) {
3240
3280
  if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(path19.join(dir, n));
3241
3281
  else if (/^stderr(\.attempt-\d+)?\.log$/.test(n)) {
3242
- 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 });
3243
3283
  else gzipFile(path19.join(dir, n));
3244
3284
  }
3245
3285
  }
@@ -3395,8 +3435,8 @@ function listHistory(q = {}) {
3395
3435
  const base2 = where.length ? `WHERE ${where.join(" AND ")}` : "";
3396
3436
  try {
3397
3437
  const total = db.prepare(`SELECT COUNT(*) AS n FROM runs r ${base2}`).get(...args).n;
3398
- const page = q.beforeId ? `${base2 ? `${base2} AND` : "WHERE"} r.id < ?` : base2;
3399
- const rows = db.prepare(`SELECT r.*, json_extract(r.meta_json, '$.audit.state') AS audit_state, json_extract(r.meta_json, '$.audit.verdict') AS audit_verdict, ${VERIFY_FAILED} AS verify_failed FROM runs r ${page} ORDER BY r.id DESC LIMIT ?`).all(...args, ...q.beforeId ? [q.beforeId] : [], Math.min(Math.max(q.limit ?? 30, 1), 200));
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));
3400
3440
  return {
3401
3441
  total,
3402
3442
  rows: rows.map((r) => ({
@@ -3493,7 +3533,7 @@ function historyStats(sinceMs2) {
3493
3533
  }
3494
3534
 
3495
3535
  // src/core/report.ts
3496
- import fs23 from "node:fs";
3536
+ import fs24 from "node:fs";
3497
3537
 
3498
3538
  // src/core/style.ts
3499
3539
  var enabled = () => {
@@ -3553,7 +3593,7 @@ function duration(meta) {
3553
3593
  }
3554
3594
  function readSummary(meta) {
3555
3595
  const f = runFile(meta.id, "summary.md");
3556
- 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 ?? "";
3557
3597
  }
3558
3598
  function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
3559
3599
  const out = [];
@@ -3625,7 +3665,7 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
3625
3665
  }
3626
3666
  if (meta.verifyResult) {
3627
3667
  const v = meta.verifyResult;
3628
- const notFound = !v.ok && v.code === 127 && /not found|no such file/i.test(v.tail);
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);
3629
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})`}`);
3630
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");
3631
3671
  if (!v.ok && v.tail) out.push(...v.tail.split("\n").map((l) => ` ${l}`));
@@ -3639,8 +3679,8 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
3639
3679
  }
3640
3680
  function live(meta) {
3641
3681
  const f = runFile(meta.id, "events.jsonl");
3642
- if (!fs23.existsSync(f)) return { steps: 0, toolCalls: 0 };
3643
- 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"));
3644
3684
  return { steps: p.usage.steps, toolCalls: p.usage.toolCalls, last: p.lastActivity };
3645
3685
  }
3646
3686
  function progress(meta) {
@@ -3651,7 +3691,7 @@ function progress(meta) {
3651
3691
  }
3652
3692
 
3653
3693
  // src/core/run.ts
3654
- var VERSION2 = true ? "0.17.0" : "0.0.0-dev";
3694
+ var VERSION2 = true ? "0.19.0" : "0.0.0-dev";
3655
3695
  function planWork(o) {
3656
3696
  if (!o.plan) return { task: o.task.trim(), tier: o.tier };
3657
3697
  if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
@@ -3683,7 +3723,7 @@ function prepareRun(o) {
3683
3723
  parent = readMeta(o.continueFrom);
3684
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");
3685
3725
  if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
3686
- if (parent.mode === "isolate" && (!parent.worktree || !fs24.existsSync(parent.worktree))) {
3726
+ if (parent.mode === "isolate" && (!parent.worktree || !fs25.existsSync(parent.worktree))) {
3687
3727
  throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
3688
3728
  }
3689
3729
  const ran = parent.ran ?? parent.worker;
@@ -3702,18 +3742,18 @@ function prepareRun(o) {
3702
3742
  throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
3703
3743
  }
3704
3744
  const mode = parent?.mode ?? o.mode;
3705
- const dir = path20.resolve(parent?.dir ?? o.dir);
3706
- 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}`);
3707
3747
  const root = repoRoot(dir);
3708
3748
  if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
3709
3749
  if (mode === "write" && !root && !o.allowNonGit) {
3710
3750
  throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
3711
3751
  }
3712
3752
  const files = o.files.map((f) => path20.resolve(f));
3713
- 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}`);
3714
3754
  const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
3715
3755
  const snap = mode === "read" && !o.review && !o.inPlace && !parent ? wantSnapshot(readIn2, root, dir) : void 0;
3716
- if (parent?.snapshot?.dir && !fs24.existsSync(parent.snapshot.dir)) {
3756
+ if (parent?.snapshot?.dir && !fs25.existsSync(parent.snapshot.dir)) {
3717
3757
  throw new UserError(`the read snapshot of run ${parent.id} is gone (cleaned up); ask the question again as a new run`, 3);
3718
3758
  }
3719
3759
  if (!parent && !snap && !process.env.PITROOM_NO_SECRET_WARNING) {
@@ -3760,13 +3800,13 @@ function prepareRun(o) {
3760
3800
  try {
3761
3801
  acquireWriteLock(root, meta.id);
3762
3802
  } catch (e) {
3763
- fs24.rmSync(runDir(meta.id), { recursive: true, force: true });
3803
+ fs25.rmSync(runDir(meta.id), { recursive: true, force: true });
3764
3804
  throw e;
3765
3805
  }
3766
3806
  }
3767
- fs24.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
3807
+ fs25.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
3768
3808
  `);
3769
- 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);
3770
3810
  return meta;
3771
3811
  }
3772
3812
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -3846,7 +3886,7 @@ async function execute(meta) {
3846
3886
  (meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
3847
3887
  for (const f of ["events.jsonl", "stderr.log"]) {
3848
3888
  const from = runFile(meta.id, f);
3849
- 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}.`)));
3850
3890
  }
3851
3891
  writeMeta(meta);
3852
3892
  }
@@ -3911,7 +3951,7 @@ function prepareTree(meta) {
3911
3951
  meta.snapshot.dir = s.dir;
3912
3952
  meta.snapshot.tree = s.tree;
3913
3953
  meta.cwd = path20.join(s.dir, path20.relative(root, meta.dir));
3914
- if (!fs24.existsSync(meta.cwd)) meta.cwd = s.dir;
3954
+ if (!fs25.existsSync(meta.cwd)) meta.cwd = s.dir;
3915
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`);
3916
3956
  } catch (e) {
3917
3957
  meta.snapshot = void 0;
@@ -3946,7 +3986,7 @@ function attempt(meta, backend, target) {
3946
3986
  timeoutSec: meta.timeoutSec
3947
3987
  });
3948
3988
  }
3949
- var read2 = (f) => fs24.existsSync(f) ? fs24.readFileSync(f, "utf8") : "";
3989
+ var read2 = (f) => fs25.existsSync(f) ? fs25.readFileSync(f, "utf8") : "";
3950
3990
  function retryableFailure(meta, backend, res) {
3951
3991
  const run2 = backend.parse(read2(runFile(meta.id, "events.jsonl")));
3952
3992
  if (run2.usage.steps > 0) return void 0;
@@ -3966,12 +4006,12 @@ function finalize(meta, res) {
3966
4006
  if (meta.snapshot?.dir && meta.repoRoot) run2.finalText = backToProject(run2.finalText, meta.snapshot.dir, meta.repoRoot);
3967
4007
  meta.sessionId = run2.sessionId ?? meta.sessionId;
3968
4008
  meta.usage = run2.usage;
3969
- fs24.writeFileSync(runFile(meta.id, "summary.md"), `${run2.finalText}
4009
+ fs25.writeFileSync(runFile(meta.id, "summary.md"), `${run2.finalText}
3970
4010
  `);
3971
4011
  if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run2.finalText);
3972
4012
  if (meta.reviewOf) {
3973
4013
  meta.verdict = parseVerdict(run2.finalText);
3974
- if (meta.packageFile) fs24.rmSync(meta.packageFile, { force: true });
4014
+ if (meta.packageFile) fs25.rmSync(meta.packageFile, { force: true });
3975
4015
  }
3976
4016
  if (!res.timedOut && !res.stopped && !meta.error) {
3977
4017
  const f = backend.failure(run2, read2(runFile(meta.id, "stderr.log")), res.code);
@@ -4019,7 +4059,7 @@ function captureChanges(meta) {
4019
4059
  const d = diffTrees(target, meta.baseTree, meta.afterTree);
4020
4060
  meta.changes = d.changes;
4021
4061
  meta.stats = d.stats;
4022
- fs24.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
4062
+ fs25.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
4023
4063
  }
4024
4064
  function verifyPath() {
4025
4065
  const extra = [path20.dirname(process.execPath), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
@@ -4038,8 +4078,9 @@ function runVerify(meta) {
4038
4078
  env: { ...process.env, PATH: verifyPath(), PWD: meta.cwd, PITROOM_ACTIVE: "1" }
4039
4079
  });
4040
4080
  const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
4041
- fs24.writeFileSync(runFile(meta.id, "verify.log"), output);
4042
- 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") };
4043
4084
  }
4044
4085
  function applyRun(meta, allowDelete = false) {
4045
4086
  if (meta.mode !== "isolate") throw new UserError(`run ${meta.id} edited your tree directly (${meta.mode}); nothing to apply`);
@@ -4075,6 +4116,15 @@ ${res.message}`, 1);
4075
4116
  writeMeta(meta);
4076
4117
  return `reverted ${meta.changes.length} file(s) changed by ${meta.id}`;
4077
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
+ }
4078
4128
  function discardRun(meta) {
4079
4129
  if (meta.mode !== "isolate") throw new UserError("only --isolate runs have an isolated copy to discard");
4080
4130
  cleanupWorktree(meta);
@@ -4083,11 +4133,11 @@ function discardRun(meta) {
4083
4133
  return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
4084
4134
  }
4085
4135
  function cleanupWorktree(meta) {
4086
- if (meta.worktree && fs24.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
4136
+ if (meta.worktree && fs25.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
4087
4137
  }
4088
4138
 
4089
4139
  // src/cli/mcp-extras.ts
4090
- import fs25 from "node:fs";
4140
+ import fs26 from "node:fs";
4091
4141
  import path21 from "node:path";
4092
4142
 
4093
4143
  // src/cli/mcp-support.ts
@@ -4447,7 +4497,7 @@ function skillPrompts() {
4447
4497
  const root = packageRoot();
4448
4498
  for (const name of skillNames(root)) {
4449
4499
  try {
4450
- const text = fs25.readFileSync(path21.join(root, "skills", name, "SKILL.md"), "utf8");
4500
+ const text = fs26.readFileSync(path21.join(root, "skills", name, "SKILL.md"), "utf8");
4451
4501
  const front = /^---\n([\s\S]*?)\n---\n/.exec(text);
4452
4502
  const body = (front ? text.slice(front[0].length) : text).trim();
4453
4503
  const description = /^description:\s*(.+)$/m.exec(front?.[1] ?? "")?.[1]?.trim() ?? `The ${name} skill.`;
@@ -4964,14 +5014,14 @@ async function serveMcp() {
4964
5014
 
4965
5015
  // src/cli/mcp-http.ts
4966
5016
  import crypto7 from "node:crypto";
4967
- import fs27 from "node:fs";
5017
+ import fs28 from "node:fs";
4968
5018
  import http2 from "node:http";
4969
5019
  import path23 from "node:path";
4970
5020
 
4971
5021
  // src/core/dash.ts
4972
5022
  import { spawn as spawn4 } from "node:child_process";
4973
5023
  import crypto6 from "node:crypto";
4974
- import fs26 from "node:fs";
5024
+ import fs27 from "node:fs";
4975
5025
  import http from "node:http";
4976
5026
  import path22 from "node:path";
4977
5027
  import { fileURLToPath as fileURLToPath3 } from "node:url";
@@ -5091,9 +5141,9 @@ function fileDiffs(patch, changes, sourceTruncated = false) {
5091
5141
  // src/core/dash-page.ts
5092
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){}})()";
5093
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";
5094
- var PAGE = `<!doctype html>
5144
+ var page = (token) => `<!doctype html>
5095
5145
  <html lang="en" class="dark"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
5096
- <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}">
5097
5147
  <script>${THEME_SCRIPT}</script><link rel="stylesheet" href="/assets/app.css"></head>
5098
5148
  <body><div id="root"></div><script type="module" src="/assets/app.js"></script></body></html>`;
5099
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>`;
@@ -5114,6 +5164,7 @@ function findings(m) {
5114
5164
  const parts = [v.critical && `${v.critical} critical`, v.important && `${v.important} important`, v.minor && `${v.minor} minor`].filter(Boolean);
5115
5165
  return parts.length ? parts.join(" \xB7 ") : "no findings";
5116
5166
  }
5167
+ var canDiscard = (m) => m.mode === "isolate" && !isActive(m.state) && !m.applied && !m.discarded;
5117
5168
  function toRun(m) {
5118
5169
  const l = m.state === "running" ? live(m) : void 0;
5119
5170
  return {
@@ -5134,6 +5185,7 @@ function toRun(m) {
5134
5185
  audit: auditBadge(m),
5135
5186
  changes: m.changes?.length || void 0,
5136
5187
  applied: m.applied || void 0,
5188
+ discardable: canDiscard(m) || void 0,
5137
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)
5138
5190
  };
5139
5191
  }
@@ -5239,28 +5291,54 @@ var assetCache = /* @__PURE__ */ new Map();
5239
5291
  function readAsset(route) {
5240
5292
  try {
5241
5293
  const file2 = path22.join(assetsDir(), path22.basename(route));
5242
- const { mtimeMs } = fs26.statSync(file2);
5294
+ const { mtimeMs } = fs27.statSync(file2);
5243
5295
  const hit = assetCache.get(route);
5244
5296
  if (hit && hit.mtimeMs === mtimeMs) return hit.data;
5245
- const data = fs26.readFileSync(file2);
5297
+ const data = fs27.readFileSync(file2);
5246
5298
  assetCache.set(route, { mtimeMs, data });
5247
5299
  return data;
5248
5300
  } catch {
5249
5301
  return void 0;
5250
5302
  }
5251
5303
  }
5252
- function handler(touch2) {
5304
+ function handler(touch2, token) {
5253
5305
  const send = (res, code, type, body) => {
5254
5306
  res.writeHead(code, { ...HEADERS, "content-type": type });
5255
5307
  res.end(body);
5256
5308
  };
5257
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
+ };
5258
5335
  return (req, res) => {
5259
5336
  touch2();
5260
5337
  if (!LOCAL_HOST.test(req.headers.host ?? "")) return json(res, 403, { error: "forbidden host" });
5261
- if (req.method !== "GET" && req.method !== "HEAD") return json(res, 405, { error: "read-only" });
5262
5338
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
5263
- 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);
5264
5342
  const type = ASSETS[url.pathname];
5265
5343
  if (type) {
5266
5344
  const data = readAsset(url.pathname);
@@ -5301,7 +5379,7 @@ function handler(touch2) {
5301
5379
  }
5302
5380
  function startDash(opts) {
5303
5381
  let lastRequest = Date.now();
5304
- const server = http.createServer(handler(() => lastRequest = Date.now()));
5382
+ const server = http.createServer(handler(() => lastRequest = Date.now(), crypto6.randomBytes(24).toString("hex")));
5305
5383
  return new Promise((resolve2, reject) => {
5306
5384
  server.once("error", reject);
5307
5385
  server.listen(opts.port, "127.0.0.1", () => {
@@ -5332,13 +5410,17 @@ async function ping(port) {
5332
5410
  }
5333
5411
  }
5334
5412
  async function runningDash() {
5413
+ let r;
5335
5414
  try {
5336
- const r = JSON.parse(fs26.readFileSync(registryFile(), "utf8"));
5337
- if (isAlive(r.pid) && await ping(r.port)) return { ...r, url: `http://127.0.0.1:${r.port}/` };
5415
+ r = JSON.parse(fs27.readFileSync(registryFile(), "utf8"));
5338
5416
  } catch {
5417
+ return void 0;
5339
5418
  }
5340
- fs26.rmSync(registryFile(), { force: true });
5341
- return void 0;
5419
+ if (!isAlive(r.pid)) {
5420
+ fs27.rmSync(registryFile(), { force: true });
5421
+ return void 0;
5422
+ }
5423
+ return await ping(r.port) ? { ...r, url: `http://127.0.0.1:${r.port}/` } : void 0;
5342
5424
  }
5343
5425
  function openBrowser(url) {
5344
5426
  const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
@@ -5352,7 +5434,7 @@ async function dashCommand(o) {
5352
5434
  if (o.stop) {
5353
5435
  if (!existing) throw new UserError("pitroom dash is not running");
5354
5436
  process.kill(existing.pid, "SIGTERM");
5355
- fs26.rmSync(registryFile(), { force: true });
5437
+ fs27.rmSync(registryFile(), { force: true });
5356
5438
  o.log(`stopped pitroom dash (${existing.url})`);
5357
5439
  return 0;
5358
5440
  }
@@ -5369,7 +5451,7 @@ async function dashCommand(o) {
5369
5451
  });
5370
5452
  child.on("error", () => void 0);
5371
5453
  child.unref();
5372
- for (let i = 0; i < 60; i++) {
5454
+ for (let i = 0; i < 200; i++) {
5373
5455
  await new Promise((r2) => setTimeout(r2, 100));
5374
5456
  const r = await runningDash();
5375
5457
  if (r && r.pid === child.pid) {
@@ -5389,8 +5471,10 @@ async function dashCommand(o) {
5389
5471
  if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
5390
5472
  dash = await startDash({ port: 0, idleMs: o.idleMs });
5391
5473
  }
5392
- fs26.mkdirSync(home(), { recursive: true });
5393
- fs26.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
5474
+ fs27.mkdirSync(home(), { recursive: true });
5475
+ const tmp = `${registryFile()}.${process.pid}.tmp`;
5476
+ fs27.writeFileSync(tmp, JSON.stringify({ pid: process.pid, port: dash.port }));
5477
+ renameOver(tmp, registryFile());
5394
5478
  const stop = () => void dash.close();
5395
5479
  process.once("SIGINT", stop);
5396
5480
  process.once("SIGTERM", stop);
@@ -5399,7 +5483,7 @@ async function dashCommand(o) {
5399
5483
  if (o.open) openBrowser(dash.url);
5400
5484
  await dash.closed;
5401
5485
  try {
5402
- if (JSON.parse(fs26.readFileSync(registryFile(), "utf8")).pid === process.pid) fs26.rmSync(registryFile(), { force: true });
5486
+ if (JSON.parse(fs27.readFileSync(registryFile(), "utf8")).pid === process.pid) fs27.rmSync(registryFile(), { force: true });
5403
5487
  } catch {
5404
5488
  }
5405
5489
  return 0;
@@ -5414,7 +5498,7 @@ var PATH = "/mcp";
5414
5498
  var tokenFile = () => path23.join(home(), "mcp-token");
5415
5499
  var readToken = (file2) => {
5416
5500
  try {
5417
- const kept = fs27.readFileSync(file2, "utf8").trim();
5501
+ const kept = fs28.readFileSync(file2, "utf8").trim();
5418
5502
  return kept.length >= 16 ? kept : void 0;
5419
5503
  } catch {
5420
5504
  return void 0;
@@ -5429,10 +5513,10 @@ function mcpToken() {
5429
5513
  const file2 = tokenFile();
5430
5514
  let token = readToken(file2);
5431
5515
  if (!token) {
5432
- fs27.mkdirSync(path23.dirname(file2), { recursive: true });
5516
+ fs28.mkdirSync(path23.dirname(file2), { recursive: true });
5433
5517
  token = crypto7.randomBytes(32).toString("hex");
5434
5518
  try {
5435
- fs27.writeFileSync(file2, `${token}
5519
+ fs28.writeFileSync(file2, `${token}
5436
5520
  `, { mode: 384, flag: "wx" });
5437
5521
  } catch (e) {
5438
5522
  if (e.code !== "EEXIST") throw e;
@@ -5441,7 +5525,7 @@ function mcpToken() {
5441
5525
  }
5442
5526
  }
5443
5527
  try {
5444
- fs27.chmodSync(file2, 384);
5528
+ fs28.chmodSync(file2, 384);
5445
5529
  } catch {
5446
5530
  }
5447
5531
  return { token, from: file2 };
@@ -5623,16 +5707,16 @@ async function serveMcpHttp(opts) {
5623
5707
 
5624
5708
  // src/cli/commands.ts
5625
5709
  import { spawnSync as spawnSync8 } from "node:child_process";
5626
- import fs33 from "node:fs";
5710
+ import fs34 from "node:fs";
5627
5711
  import path29 from "node:path";
5628
5712
  import { fileURLToPath as fileURLToPath4 } from "node:url";
5629
5713
 
5630
5714
  // src/core/init.ts
5631
- import fs28 from "node:fs";
5715
+ import fs29 from "node:fs";
5632
5716
  import path24 from "node:path";
5633
5717
  var FREE = /-free$/;
5634
5718
  var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
5635
- var isFound = (binary5) => path24.isAbsolute(binary5) && fs28.existsSync(binary5);
5719
+ var isFound = (binary5) => path24.isAbsolute(binary5) && fs29.existsSync(binary5);
5636
5720
  function planInit(opts = {}) {
5637
5721
  const file2 = configPath();
5638
5722
  const workers = backendIds().map((id) => {
@@ -5678,15 +5762,15 @@ function planInit(opts = {}) {
5678
5762
  if (Object.keys(tiers).length > 1) config.tiers = tiers;
5679
5763
  if (!workers.some((w) => w.found)) blocked = "no worker CLI found: install OpenCode (https://opencode.ai), Codex CLI, Claude Code or Gemini CLI first";
5680
5764
  else if (!blocked && !Object.keys(config).length) notes.push("nothing to propose: the default model and the single worker need no config");
5681
- return { path: file2, exists: fs28.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
5765
+ return { path: file2, exists: fs29.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
5682
5766
  }
5683
5767
  function writeInit(plan, force) {
5684
5768
  if (plan.blocked) throw new UserError(plan.blocked);
5685
5769
  if (!Object.keys(plan.config).length) throw new UserError("nothing to write: the proposal is empty");
5686
5770
  if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${path24.basename(plan.path)}.bak)`);
5687
- fs28.mkdirSync(path24.dirname(plan.path), { recursive: true });
5688
- if (plan.exists) fs28.copyFileSync(plan.path, `${plan.path}.bak`);
5689
- fs28.writeFileSync(plan.path, `${JSON.stringify(plan.config, null, 2)}
5771
+ fs29.mkdirSync(path24.dirname(plan.path), { recursive: true });
5772
+ if (plan.exists) fs29.copyFileSync(plan.path, `${plan.path}.bak`);
5773
+ fs29.writeFileSync(plan.path, `${JSON.stringify(plan.config, null, 2)}
5690
5774
  `);
5691
5775
  return plan.path;
5692
5776
  }
@@ -5707,7 +5791,7 @@ function formatInit(plan, written) {
5707
5791
 
5708
5792
  // src/core/mcp-install.ts
5709
5793
  import { spawnSync as spawnSync7 } from "node:child_process";
5710
- import fs29 from "node:fs";
5794
+ import fs30 from "node:fs";
5711
5795
  import os10 from "node:os";
5712
5796
  import path25 from "node:path";
5713
5797
  var CLIENT_IDS = ["claude-code", "codex", "gemini", "cursor", "claude-desktop"];
@@ -5717,7 +5801,7 @@ function mcpCommand() {
5717
5801
  if (isPitroomLauncher(launcher)) return { command: launcher, args: ["mcp"] };
5718
5802
  for (const dir of (process.env.PATH ?? "").split(path25.delimiter).filter(Boolean)) {
5719
5803
  const p = path25.join(dir, process.platform === "win32" ? "pitroom.cmd" : "pitroom");
5720
- if (fs29.existsSync(p)) return { command: path25.resolve(p), args: ["mcp"] };
5804
+ if (fs30.existsSync(p)) return { command: path25.resolve(p), args: ["mcp"] };
5721
5805
  }
5722
5806
  return { command: process.execPath, args: [process.argv[1] ?? "pitroom", "mcp"] };
5723
5807
  }
@@ -5733,7 +5817,7 @@ function run(backendId, args) {
5733
5817
  var runnable = (backendId) => run(backendId, ["--version"]).ok;
5734
5818
  var readJson = (file2) => {
5735
5819
  try {
5736
- return JSON.parse(fs29.readFileSync(file2, "utf8"));
5820
+ return JSON.parse(fs30.readFileSync(file2, "utf8"));
5737
5821
  } catch {
5738
5822
  return void 0;
5739
5823
  }
@@ -5766,29 +5850,29 @@ function jsonClient(id, name, file2, dir) {
5766
5850
  return {
5767
5851
  id,
5768
5852
  name,
5769
- found: () => fs29.existsSync(dir()),
5853
+ found: () => fs30.existsSync(dir()),
5770
5854
  state: (c) => jsonState(file2(), c),
5771
5855
  add: (c) => {
5772
5856
  let config = {};
5773
- if (fs29.existsSync(file2())) {
5857
+ if (fs30.existsSync(file2())) {
5774
5858
  config = readJson(file2());
5775
5859
  if (!config || typeof config !== "object" || Array.isArray(config)) return { ok: false, message: `${rel()} is not valid JSON: left alone` };
5776
5860
  const backup = `${file2()}.bak-pitroom`;
5777
- if (!fs29.existsSync(backup)) fs29.copyFileSync(file2(), backup);
5861
+ if (!fs30.existsSync(backup)) fs30.copyFileSync(file2(), backup);
5778
5862
  }
5779
5863
  config.mcpServers = { ...config.mcpServers ?? {}, [NAME]: { command: c.command, args: c.args } };
5780
- fs29.mkdirSync(path25.dirname(file2()), { recursive: true });
5864
+ fs30.mkdirSync(path25.dirname(file2()), { recursive: true });
5781
5865
  const tmp = `${file2()}.${process.pid}.tmp`;
5782
- fs29.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
5866
+ fs30.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
5783
5867
  `);
5784
- fs29.renameSync(tmp, file2());
5868
+ fs30.renameSync(tmp, file2());
5785
5869
  return { ok: true, message: rel() };
5786
5870
  },
5787
5871
  remove: () => {
5788
5872
  const config = readJson(file2());
5789
5873
  if (!config?.mcpServers?.[NAME]) return { ok: true, message: rel() };
5790
5874
  delete config.mcpServers[NAME];
5791
- fs29.writeFileSync(file2(), `${JSON.stringify(config, null, 2)}
5875
+ fs30.writeFileSync(file2(), `${JSON.stringify(config, null, 2)}
5792
5876
  `);
5793
5877
  return { ok: true, message: rel() };
5794
5878
  }
@@ -5802,7 +5886,7 @@ function desktopFile() {
5802
5886
  function codexState(c) {
5803
5887
  let text;
5804
5888
  try {
5805
- text = fs29.readFileSync(path25.join(process.env.CODEX_HOME ?? path25.join(home2(), ".codex"), "config.toml"), "utf8");
5889
+ text = fs30.readFileSync(path25.join(process.env.CODEX_HOME ?? path25.join(home2(), ".codex"), "config.toml"), "utf8");
5806
5890
  } catch {
5807
5891
  return "absent";
5808
5892
  }
@@ -5906,7 +5990,7 @@ var parseClients = (value) => {
5906
5990
 
5907
5991
  // src/core/plan-status.ts
5908
5992
  import crypto8 from "node:crypto";
5909
- import fs30 from "node:fs";
5993
+ import fs31 from "node:fs";
5910
5994
  import path26 from "node:path";
5911
5995
  function notesFile(plan) {
5912
5996
  const id = crypto8.createHash("sha1").update(plan.file).digest("hex").slice(0, 12);
@@ -5914,8 +5998,8 @@ function notesFile(plan) {
5914
5998
  }
5915
5999
  function readNotes(plan) {
5916
6000
  const f = notesFile(plan);
5917
- if (!fs30.existsSync(f)) return [];
5918
- return fs30.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
6001
+ if (!fs31.existsSync(f)) return [];
6002
+ return fs31.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
5919
6003
  const m = /^(\d{4}-\d\d-\d\d \d\d:\d\d) (.*)$/.exec(l);
5920
6004
  return m ? { at: m[1], text: m[2] } : { at: "", text: l };
5921
6005
  });
@@ -5923,11 +6007,11 @@ function readNotes(plan) {
5923
6007
  function addNote(planFile, text) {
5924
6008
  const plan = loadPlan(planFile);
5925
6009
  const f = notesFile(plan);
5926
- fs30.mkdirSync(path26.dirname(f), { recursive: true });
5927
- if (!fs30.existsSync(f)) fs30.writeFileSync(f, `# plan: ${plan.file}
6010
+ fs31.mkdirSync(path26.dirname(f), { recursive: true });
6011
+ if (!fs31.existsSync(f)) fs31.writeFileSync(f, `# plan: ${plan.file}
5928
6012
  `);
5929
6013
  const at = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
5930
- fs30.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
6014
+ fs31.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
5931
6015
  `);
5932
6016
  return f;
5933
6017
  }
@@ -5995,13 +6079,13 @@ ${rulings}`,
5995
6079
 
5996
6080
  // src/core/cache.ts
5997
6081
  import crypto9 from "node:crypto";
5998
- import fs31 from "node:fs";
6082
+ import fs32 from "node:fs";
5999
6083
  import path27 from "node:path";
6000
6084
  var SCAN2 = 500;
6001
6085
  function cacheKey(o) {
6002
6086
  if (o.verify || o.mode !== "read" || o.continueFrom || o.web || o.plan || o.review || o.audit || o.auditRate === 1) return void 0;
6003
6087
  if (!(effective().cacheDays.value > 0)) return void 0;
6004
- const dir = path27.resolve(o.dir);
6088
+ const dir = canonical(path27.resolve(o.dir));
6005
6089
  const root = repoRoot(dir);
6006
6090
  if (!root) return void 0;
6007
6091
  try {
@@ -6009,7 +6093,7 @@ function cacheKey(o) {
6009
6093
  const head = commitOf(root, "HEAD") ?? "";
6010
6094
  const files = o.files.map((f) => {
6011
6095
  const abs = path27.resolve(f);
6012
- return [abs, crypto9.createHash("sha256").update(fs31.readFileSync(abs)).digest("hex")];
6096
+ return [abs, crypto9.createHash("sha256").update(fs32.readFileSync(abs)).digest("hex")];
6013
6097
  });
6014
6098
  const question = o.task.replace(/\s+/g, " ").trim();
6015
6099
  const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
@@ -6058,7 +6142,7 @@ var cachedNote = (m) => `pitroom \u27F2 cached answer \xB7 the same question on
6058
6142
 
6059
6143
  // src/core/review.ts
6060
6144
  import crypto10 from "node:crypto";
6061
- import fs32 from "node:fs";
6145
+ import fs33 from "node:fs";
6062
6146
  import path28 from "node:path";
6063
6147
  var TEMPLATE2 = { task: "task-reviewer", fix: "re-review", range: "code-reviewer" };
6064
6148
  var section = (title, body) => `## ${title}
@@ -6077,7 +6161,7 @@ function ancestors(m) {
6077
6161
  function briefOf(m) {
6078
6162
  const root = [m, ...ancestors(m)].at(-1);
6079
6163
  const f = runFile(root.id, "brief.md");
6080
- return fs32.existsSync(f) ? fs32.readFileSync(f, "utf8") : root.task;
6164
+ return fs33.existsSync(f) ? fs33.readFileSync(f, "utf8") : root.task;
6081
6165
  }
6082
6166
  function latestReview(ids) {
6083
6167
  for (const id of listRunIds().reverse()) {
@@ -6091,7 +6175,7 @@ function latestReview(ids) {
6091
6175
  }
6092
6176
  function diffOf(m, from = m.baseTree) {
6093
6177
  const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
6094
- if (where && fs32.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
6178
+ if (where && fs33.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
6095
6179
  if (from !== m.baseTree) {
6096
6180
  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);
6097
6181
  }
@@ -6105,7 +6189,7 @@ function runReview(id) {
6105
6189
  if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
6106
6190
  const chain = ancestors(m);
6107
6191
  const previous = latestReview(chain.map((a) => a.id));
6108
- const dir = m.mode === "isolate" && m.worktree && fs32.existsSync(m.worktree) ? m.cwd : m.dir;
6192
+ const dir = m.mode === "isolate" && m.worktree && fs33.existsSync(m.worktree) ? m.cwd : m.dir;
6109
6193
  const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
6110
6194
  const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
6111
6195
  if (!previous) {
@@ -6181,19 +6265,19 @@ function writePackage(job) {
6181
6265
  const root = repoRoot(job.dir);
6182
6266
  if (!g || !root) throw new UserError(`not a git repository: ${job.dir}`);
6183
6267
  const name = `review-${crypto10.randomBytes(4).toString("hex")}.md`;
6184
- const inside2 = !path28.relative(fs32.realpathSync(root), fs32.realpathSync(g)).startsWith("..");
6268
+ const inside2 = !path28.relative(fs33.realpathSync(root), fs33.realpathSync(g)).startsWith("..");
6185
6269
  const file2 = inside2 ? path28.join(g, "pitroom", name) : path28.join(root, ".pitroom", name);
6186
6270
  if (!inside2) ignoreInGit(root, "/.pitroom/");
6187
- fs32.mkdirSync(path28.dirname(file2), { recursive: true });
6188
- fs32.writeFileSync(file2, job.package);
6271
+ fs33.mkdirSync(path28.dirname(file2), { recursive: true });
6272
+ fs33.writeFileSync(file2, job.package);
6189
6273
  return file2;
6190
6274
  }
6191
6275
  function ignoreInGit(root, pattern) {
6192
6276
  const exclude = path28.resolve(root, git(root, ["rev-parse", "--git-path", "info/exclude"]).stdout.trim());
6193
- const text = fs32.existsSync(exclude) ? fs32.readFileSync(exclude, "utf8") : "";
6277
+ const text = fs33.existsSync(exclude) ? fs33.readFileSync(exclude, "utf8") : "";
6194
6278
  if (text.split(/\r?\n/).includes(pattern)) return;
6195
- fs32.mkdirSync(path28.dirname(exclude), { recursive: true });
6196
- fs32.appendFileSync(exclude, `${text && !text.endsWith("\n") ? "\n" : ""}# Pitroom review packages in worktrees (removed after each review)
6279
+ fs33.mkdirSync(path28.dirname(exclude), { recursive: true });
6280
+ fs33.appendFileSync(exclude, `${text && !text.endsWith("\n") ? "\n" : ""}# Pitroom review packages in worktrees (removed after each review)
6197
6281
  ${pattern}
6198
6282
  `);
6199
6283
  }
@@ -6349,10 +6433,10 @@ async function cmdReview(p) {
6349
6433
  writeMeta(meta);
6350
6434
  }
6351
6435
  } catch (e) {
6352
- fs33.rmSync(packageFile, { force: true });
6436
+ fs34.rmSync(packageFile, { force: true });
6353
6437
  throw e;
6354
6438
  }
6355
- fs33.writeFileSync(runFile(meta.id, "package.md"), job.package);
6439
+ fs34.writeFileSync(runFile(meta.id, "package.md"), job.package);
6356
6440
  return launch(p, meta);
6357
6441
  }
6358
6442
  async function cmdAudit(p) {
@@ -6420,7 +6504,7 @@ function cmdPlan(p) {
6420
6504
  }
6421
6505
  function cmdCrew(p) {
6422
6506
  const file2 = flag(p, "task-file");
6423
- const tasks = (file2 ? fs33.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
6507
+ const tasks = (file2 ? fs34.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
6424
6508
  if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
6425
6509
  if (has(p, "write") && tasks.length > 1) {
6426
6510
  throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
@@ -6606,11 +6690,8 @@ function cmdStop(p) {
6606
6690
  const ids = selectIds(p, () => [resolveRun(void 0)]);
6607
6691
  let stopped = 0;
6608
6692
  for (const id of ids) {
6609
- const meta = freshMeta(id);
6610
- if (!isActive(meta.state) || !isAlive(meta.pid)) continue;
6611
- meta.stopRequested = true;
6612
- writeMeta(meta);
6613
- process.kill(meta.pid, "SIGTERM");
6693
+ const meta = stopRun(id);
6694
+ if (!meta) continue;
6614
6695
  console.log(`stopping ${meta.id} (${meta.state})`);
6615
6696
  stopped++;
6616
6697
  }
@@ -6623,13 +6704,13 @@ function sinceMs(s) {
6623
6704
  if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
6624
6705
  return Date.now() - Number(m[1]) * 864e5;
6625
6706
  }
6626
- var readStdin = () => process.stdin.isTTY ? "" : fs33.readFileSync(0, "utf8");
6707
+ var readStdin = () => process.stdin.isTTY ? "" : fs34.readFileSync(0, "utf8");
6627
6708
  function cmdStatusline(p) {
6628
6709
  const input = readStdin();
6629
6710
  const lines = [];
6630
6711
  const then = flag(p, "then");
6631
6712
  if (then) {
6632
- const r = spawnSync8("/bin/sh", ["-c", then], { input, encoding: "utf8", timeout: 5e3 });
6713
+ const r = process.platform === "win32" ? spawnSync8(then, { shell: true, input, encoding: "utf8", timeout: 5e3 }) : spawnSync8("/bin/sh", ["-c", then], { input, encoding: "utf8", timeout: 5e3 });
6633
6714
  if (r.stdout?.trimEnd()) lines.push(r.stdout.trimEnd());
6634
6715
  }
6635
6716
  try {
@@ -6664,7 +6745,7 @@ function cmdModels(p) {
6664
6745
  }
6665
6746
  function cmdHookStart() {
6666
6747
  const script = path29.join(path29.dirname(path29.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
6667
- if (fs33.existsSync(script)) spawnSync8(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
6748
+ if (fs34.existsSync(script)) spawnSync8(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
6668
6749
  return 0;
6669
6750
  }
6670
6751
  function cmdInit(p) {
@@ -6704,7 +6785,7 @@ function cmdSavings(p) {
6704
6785
  est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
6705
6786
  const out = flag(p, "card");
6706
6787
  if (out) {
6707
- fs33.writeFileSync(out, card(t, period));
6788
+ fs34.writeFileSync(out, card(t, period));
6708
6789
  console.log(`card written to ${path29.resolve(out)}`);
6709
6790
  }
6710
6791
  if (has(p, "badge")) console.log(`![pitroom](${badgeUrl(t)})`);
@@ -6747,7 +6828,7 @@ function cmdConfig(p) {
6747
6828
  console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
6748
6829
  return 0;
6749
6830
  }
6750
- console.log(`config file: ${configPath()}${fs33.existsSync(configPath()) ? "" : " (not present)"}`);
6831
+ console.log(`config file: ${configPath()}${fs34.existsSync(configPath()) ? "" : " (not present)"}`);
6751
6832
  for (const [key, s] of Object.entries(eff)) {
6752
6833
  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);
6753
6834
  console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
@@ -6771,7 +6852,7 @@ function cmdClean(p) {
6771
6852
  const m = readMeta(id);
6772
6853
  if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
6773
6854
  recordRun(m);
6774
- fs33.rmSync(runDir(id), { recursive: true, force: true });
6855
+ fs34.rmSync(runDir(id), { recursive: true, force: true });
6775
6856
  }
6776
6857
  const snapshots = prune(Date.now(), 0);
6777
6858
  console.log(`removed ${old.length} run(s)${snapshots ? ` and ${snapshots} read snapshot${snapshots === 1 ? "" : "s"}` : ""}; the history and the savings ledger are kept`);
@@ -6782,7 +6863,7 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
6782
6863
 
6783
6864
  // src/core/doctor.ts
6784
6865
  import { spawnSync as spawnSync9 } from "node:child_process";
6785
- import fs34 from "node:fs";
6866
+ import fs35 from "node:fs";
6786
6867
  import os11 from "node:os";
6787
6868
  import path30 from "node:path";
6788
6869
  var MARK = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
@@ -6793,11 +6874,24 @@ var NEXT = [
6793
6874
  { when: /not logged in/, command: "claude auth login", why: "sign in the Claude Code worker (Codex: codex login)" },
6794
6875
  { when: /Gemini CLI is not signed in|IneligibleTierError/, command: "export GEMINI_API_KEY=\u2026", why: "a Google AI Studio key for the Gemini worker" }
6795
6876
  ];
6877
+ function usageLine() {
6878
+ const ids = listRunIds();
6879
+ if (!ids.length) {
6880
+ return 'no runs yet. Ask your agent to use Pitroom (for example "have a worker find where X is defined"), or try it yourself: pitroom run "where is <something> defined?"';
6881
+ }
6882
+ let when2 = "";
6883
+ try {
6884
+ const hours = Math.max(0, Math.round((Date.now() - Date.parse(readMeta(ids[ids.length - 1]).startedAt)) / 36e5));
6885
+ when2 = hours < 1 ? ", the last one within the hour" : hours < 48 ? `, the last one ${hours} h ago` : `, the last one ${Math.round(hours / 24)} days ago`;
6886
+ } catch {
6887
+ }
6888
+ return `${ids.length} run${ids.length === 1 ? "" : "s"} so far${when2} (pitroom savings shows what they saved)`;
6889
+ }
6796
6890
  function firstNodeOnPath() {
6797
6891
  for (const dir of (process.env.PATH ?? "").split(path30.delimiter).filter(Boolean)) {
6798
6892
  const file2 = path30.join(dir, process.platform === "win32" ? "node.exe" : "node");
6799
6893
  try {
6800
- if (!fs34.statSync(file2).isFile()) continue;
6894
+ if (!fs35.statSync(file2).isFile()) continue;
6801
6895
  } catch {
6802
6896
  continue;
6803
6897
  }
@@ -6829,9 +6923,11 @@ function doctor5(probe) {
6829
6923
  }
6830
6924
  add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
6831
6925
  const cfg = loadConfig();
6832
- add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs34.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
6926
+ add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs35.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
6833
6927
  for (const w of cfg.warnings) add("warn", w);
6834
- if (process.platform !== "win32") {
6928
+ if (process.platform === "win32") {
6929
+ 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");
6930
+ } else {
6835
6931
  const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
6836
6932
  add(guarded ? "ok" : "warn", guarded ? "git guard shim ready" : "git guard unavailable (git not on PATH)");
6837
6933
  }
@@ -6908,6 +7004,8 @@ function doctor5(probe) {
6908
7004
  for (const m of stale) add("warn", `${m.name} runs a different command for pitroom mcp than this install's: \`pitroom install --mcp --no-skills\` updates it`);
6909
7005
  section2("Skills and agents");
6910
7006
  addAll(skillChecks());
7007
+ section2("Use");
7008
+ add("ok", usageLine());
6911
7009
  if (probe && chain[0]) {
6912
7010
  section2("Live probe");
6913
7011
  const p = liveProbe(chain[0]);
@@ -6994,8 +7092,8 @@ function skillChecks() {
6994
7092
  );
6995
7093
  }
6996
7094
  const launcher = launcherPath();
6997
- if (fs34.existsSync(launcher)) {
6998
- const r = spawnSync9(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
7095
+ if (fs35.existsSync(launcher)) {
7096
+ const r = spawnSync9(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
6999
7097
  checks.push(
7000
7098
  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)}` }
7001
7099
  );
@@ -7006,12 +7104,12 @@ function skillChecks() {
7006
7104
  }
7007
7105
  function geminiExtensionInstalled() {
7008
7106
  const home3 = process.env.GEMINI_CLI_HOME ?? os11.homedir();
7009
- return fs34.existsSync(path30.join(home3, ".gemini", "extensions", "pitroom"));
7107
+ return fs35.existsSync(path30.join(home3, ".gemini", "extensions", "pitroom"));
7010
7108
  }
7011
7109
  function pluginInstalled() {
7012
7110
  try {
7013
7111
  const f = path30.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
7014
- const plugins = JSON.parse(fs34.readFileSync(f, "utf8")).plugins ?? {};
7112
+ const plugins = JSON.parse(fs35.readFileSync(f, "utf8")).plugins ?? {};
7015
7113
  return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
7016
7114
  } catch {
7017
7115
  return false;
@@ -7030,14 +7128,14 @@ function superpowersActive() {
7030
7128
  }
7031
7129
  for (const base2 of [path30.join(os11.homedir(), ".agents", "skills"), path30.join(os11.homedir(), ".claude", "skills")]) {
7032
7130
  const dir = path30.join(base2, "using-superpowers");
7033
- if (fs34.existsSync(path30.join(dir, "SKILL.md"))) found.push(dir);
7131
+ if (fs35.existsSync(path30.join(dir, "SKILL.md"))) found.push(dir);
7034
7132
  }
7035
7133
  return found;
7036
7134
  }
7037
7135
  function claudePlugins() {
7038
7136
  try {
7039
7137
  const f = path30.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
7040
- return Object.keys(JSON.parse(fs34.readFileSync(f, "utf8")).plugins ?? {});
7138
+ return Object.keys(JSON.parse(fs35.readFileSync(f, "utf8")).plugins ?? {});
7041
7139
  } catch {
7042
7140
  return [];
7043
7141
  }
@@ -7045,7 +7143,7 @@ function claudePlugins() {
7045
7143
  function claudePluginEnabled(key) {
7046
7144
  try {
7047
7145
  const f = path30.join(os11.homedir(), ".claude", "settings.json");
7048
- return JSON.parse(fs34.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
7146
+ return JSON.parse(fs35.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
7049
7147
  } catch {
7050
7148
  return true;
7051
7149
  }
@@ -7054,7 +7152,7 @@ function codexPlugins() {
7054
7152
  const plugins = /* @__PURE__ */ new Map();
7055
7153
  let text;
7056
7154
  try {
7057
- text = fs34.readFileSync(path30.join(process.env.CODEX_HOME ?? path30.join(os11.homedir(), ".codex"), "config.toml"), "utf8");
7155
+ text = fs35.readFileSync(path30.join(process.env.CODEX_HOME ?? path30.join(os11.homedir(), ".codex"), "config.toml"), "utf8");
7058
7156
  } catch {
7059
7157
  return plugins;
7060
7158
  }
@@ -7076,7 +7174,7 @@ function openCodePlugins() {
7076
7174
  const plugins = [];
7077
7175
  for (const name of ["opencode.json", "opencode.jsonc"]) {
7078
7176
  try {
7079
- const text = fs34.readFileSync(path30.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
7177
+ const text = fs35.readFileSync(path30.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
7080
7178
  const list2 = JSON.parse(text).plugin;
7081
7179
  if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
7082
7180
  } catch {
@@ -7084,7 +7182,7 @@ function openCodePlugins() {
7084
7182
  }
7085
7183
  for (const folder of ["plugin", "plugins"]) {
7086
7184
  try {
7087
- for (const f of fs34.readdirSync(path30.join(dir, folder))) plugins.push(path30.join(dir, folder, f));
7185
+ for (const f of fs35.readdirSync(path30.join(dir, folder))) plugins.push(path30.join(dir, folder, f));
7088
7186
  } catch {
7089
7187
  }
7090
7188
  }
@@ -7201,7 +7299,7 @@ var COMMANDS = {
7201
7299
  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)");
7202
7300
  const dir = flag(p, "dir");
7203
7301
  if (dir !== void 0) {
7204
- if (!fs35.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) throw new UserError(`-d ${dir}: no such directory`);
7302
+ if (!fs36.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) throw new UserError(`-d ${dir}: no such directory`);
7205
7303
  process.chdir(dir);
7206
7304
  process.env.PWD = process.cwd();
7207
7305
  }