pitroom 0.6.8 → 0.6.10

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
@@ -1391,8 +1391,8 @@ function effortFlag(p) {
1391
1391
 
1392
1392
  // src/cli/commands.ts
1393
1393
  import { spawnSync as spawnSync6 } from "node:child_process";
1394
- import fs24 from "node:fs";
1395
- import path20 from "node:path";
1394
+ import fs25 from "node:fs";
1395
+ import path21 from "node:path";
1396
1396
 
1397
1397
  // src/core/history.ts
1398
1398
  import fs10 from "node:fs";
@@ -3085,7 +3085,7 @@ async function dashCommand(o) {
3085
3085
  return 0;
3086
3086
  }
3087
3087
  if (o.detach) {
3088
- const child = spawn(process.execPath, [process.argv[1], "dash", "--serve", ...o.port !== void 0 ? ["--port", String(o.port)] : [], "--idle", "4h"], {
3088
+ const child = spawn(process.execPath, [process.argv[1], "dash", "--serve", ...o.port !== void 0 ? ["--port", String(o.port)] : [], "--idle", "1h"], {
3089
3089
  detached: true,
3090
3090
  stdio: "ignore",
3091
3091
  env: process.env
@@ -3130,8 +3130,8 @@ async function dashCommand(o) {
3130
3130
 
3131
3131
  // src/core/run.ts
3132
3132
  import { spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
3133
- import fs23 from "node:fs";
3134
- import path19 from "node:path";
3133
+ import fs24 from "node:fs";
3134
+ import path20 from "node:path";
3135
3135
 
3136
3136
  // src/core/chain.ts
3137
3137
  function resolveChain(flags = {}) {
@@ -3155,9 +3155,7 @@ function resolveChain(flags = {}) {
3155
3155
  worker = withDefault(worker);
3156
3156
  if (flags.effort) {
3157
3157
  const base2 = (worker.model ?? "").split("#")[0];
3158
- if (worker.backend === "opencode" && !base2) {
3159
- warnings.push("--effort needs a model for OpenCode (provider/model#variant): set one in the config or pass -m; ignored");
3160
- } else {
3158
+ if (!(worker.backend === "opencode" && !base2)) {
3161
3159
  worker = { ...worker, model: `${base2}#${flags.effort}` };
3162
3160
  }
3163
3161
  }
@@ -3494,6 +3492,11 @@ ${task}`;
3494
3492
  // src/core/refs.ts
3495
3493
  import fs22 from "node:fs";
3496
3494
  import path18 from "node:path";
3495
+ var FILE_EXTENSIONS = new Set(
3496
+ "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(" ")
3497
+ );
3498
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
3499
+ var MAX_INDEXED = 6e4;
3497
3500
  var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
3498
3501
  var REF = new RegExp(
3499
3502
  String.raw`(?<![\w/:.-])(\.{0,2}/?(?:[\w@.+-]+/)*(?:[\w@+-][\w@.+-]*\.[A-Za-z][A-Za-z0-9]{0,7}|${EXTENSIONLESS})):(\d+)(?:[-–](\d+))?`,
@@ -3551,23 +3554,36 @@ function verifyRefs(refs, dirs) {
3551
3554
  }
3552
3555
  return lineCache.get(file);
3553
3556
  };
3557
+ let index;
3558
+ const byName = () => index ??= indexFiles(roots);
3559
+ const candidates = (ref) => {
3560
+ const direct = resolve(ref.file, roots);
3561
+ if (direct) return [direct];
3562
+ if (path18.isAbsolute(ref.file)) return [];
3563
+ const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
3564
+ return (byName().get(path18.basename(wanted)) ?? []).filter((f) => f.endsWith(`/${wanted}`) || path18.basename(f) === wanted).slice(0, 20);
3565
+ };
3566
+ const check = (file, ref) => {
3567
+ const lines = load(file);
3568
+ if (!lines) return void 0;
3569
+ if (ref.end > lines.length) return `file has ${lines.length} lines`;
3570
+ if (ref.symbol && !mentions(lines, ref) && !enclosedBy(lines, ref)) return `\`${ref.symbol}\` not near line ${ref.start}`;
3571
+ return void 0;
3572
+ };
3573
+ let total = 0;
3554
3574
  for (const ref of refs) {
3555
- const file = resolve(ref.file, roots);
3556
- if (!file) {
3575
+ const files = candidates(ref);
3576
+ if (!files.length) {
3577
+ if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
3578
+ total++;
3557
3579
  invalid.push({ ref: ref.text, reason: path18.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
3558
3580
  continue;
3559
3581
  }
3560
- const lines = load(file);
3561
- if (!lines) continue;
3562
- if (ref.end > lines.length) {
3563
- invalid.push({ ref: ref.text, reason: `file has ${lines.length} lines` });
3564
- continue;
3565
- }
3566
- if (ref.symbol && !mentions(lines, ref) && !enclosedBy(lines, ref)) {
3567
- invalid.push({ ref: ref.text, reason: `\`${ref.symbol}\` not near line ${ref.start}` });
3568
- }
3582
+ total++;
3583
+ const reasons = files.map((f) => check(f, ref));
3584
+ if (!reasons.some((r) => r === void 0)) invalid.push({ ref: ref.text, reason: files.length > 1 ? `${reasons[0]} (${files.length} files match)` : reasons[0] });
3569
3585
  }
3570
- return { total: refs.length, valid: refs.length - invalid.length, invalid };
3586
+ return { total, valid: total - invalid.length, invalid };
3571
3587
  }
3572
3588
  function mentions(lines, ref) {
3573
3589
  const near = lines.slice(Math.max(0, ref.start - 1 - WINDOW), ref.end + WINDOW).join("\n");
@@ -3580,6 +3596,35 @@ function enclosedBy(lines, ref) {
3580
3596
  );
3581
3597
  return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
3582
3598
  }
3599
+ var isFileName = (name) => {
3600
+ const base2 = path18.basename(name);
3601
+ return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
3602
+ };
3603
+ function indexFiles(roots) {
3604
+ const index = /* @__PURE__ */ new Map();
3605
+ let count = 0;
3606
+ const walk = (dir) => {
3607
+ let entries;
3608
+ try {
3609
+ entries = fs22.readdirSync(dir, { withFileTypes: true });
3610
+ } catch {
3611
+ return;
3612
+ }
3613
+ for (const e of entries) {
3614
+ if (count >= MAX_INDEXED) return;
3615
+ if (e.isDirectory()) {
3616
+ if (!SKIP_DIRS.has(e.name)) walk(path18.join(dir, e.name));
3617
+ } else if (e.isFile()) {
3618
+ count++;
3619
+ const list2 = index.get(e.name);
3620
+ if (list2) list2.push(path18.join(dir, e.name));
3621
+ else index.set(e.name, [path18.join(dir, e.name)]);
3622
+ }
3623
+ }
3624
+ };
3625
+ for (const r of roots) walk(r);
3626
+ return index;
3627
+ }
3583
3628
  function resolve(ref, roots) {
3584
3629
  const candidates = path18.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path18.join(r, ref));
3585
3630
  return candidates.find((c) => {
@@ -3598,8 +3643,59 @@ function inside(file, roots) {
3598
3643
  }
3599
3644
  var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3600
3645
 
3646
+ // src/core/secrets.ts
3647
+ import { execFileSync } from "node:child_process";
3648
+ import fs23 from "node:fs";
3649
+ import path19 from "node:path";
3650
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
3651
+ var MAX_VISITED = 2e4;
3652
+ var MAX_DEPTH = 4;
3653
+ var TEMPLATE2 = /\.(example|sample|template|dist|defaults?|tpl)$/i;
3654
+ function looksSecret(name) {
3655
+ if (name === ".env" || name.startsWith(".env.") && !TEMPLATE2.test(name)) return true;
3656
+ return /\.(pem|p12|pfx)$/i.test(name) || /^id_(rsa|dsa|ecdsa|ed25519)$/.test(name) || name === ".netrc" || name === "credentials.json";
3657
+ }
3658
+ function findSecretFiles(dir) {
3659
+ const found = [];
3660
+ let visited = 0;
3661
+ const walk = (d, depth) => {
3662
+ if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
3663
+ let entries;
3664
+ try {
3665
+ entries = fs23.readdirSync(d, { withFileTypes: true });
3666
+ } catch {
3667
+ return;
3668
+ }
3669
+ for (const e of entries) {
3670
+ visited++;
3671
+ if (e.isDirectory()) {
3672
+ if (!SKIP_DIRS2.has(e.name)) walk(path19.join(d, e.name), depth + 1);
3673
+ } else if (e.isFile() && looksSecret(e.name)) {
3674
+ found.push(path19.relative(dir, path19.join(d, e.name)));
3675
+ }
3676
+ }
3677
+ };
3678
+ walk(dir, 0);
3679
+ return found.sort();
3680
+ }
3681
+ function findSecretFilesInTree(root, dir) {
3682
+ try {
3683
+ const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
3684
+ return out.split("\0").filter((f) => f && looksSecret(path19.basename(f)) && !path19.relative(dir, path19.join(root, f)).startsWith("..")).map((f) => path19.relative(dir, path19.join(root, f))).sort();
3685
+ } catch {
3686
+ return [];
3687
+ }
3688
+ }
3689
+ function secretWarning(files, mode) {
3690
+ if (!files.length) return void 0;
3691
+ const shown = files.slice(0, 3).join(", ") + (files.length > 3 ? `, +${files.length - 3} more` : "");
3692
+ const where = mode === "isolate" ? "would be copied into the isolated copy" : "sit in the directory the worker runs in";
3693
+ 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";
3694
+ 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`;
3695
+ }
3696
+
3601
3697
  // src/core/run.ts
3602
- var VERSION2 = true ? "0.6.8" : "0.0.0-dev";
3698
+ var VERSION2 = true ? "0.6.10" : "0.0.0-dev";
3603
3699
  function planWork(o) {
3604
3700
  if (!o.plan) return { task: o.task.trim(), tier: o.tier };
3605
3701
  if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
@@ -3631,7 +3727,7 @@ function prepareRun(o) {
3631
3727
  parent = readMeta(o.continueFrom);
3632
3728
  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");
3633
3729
  if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
3634
- if (parent.mode === "isolate" && (!parent.worktree || !fs23.existsSync(parent.worktree))) {
3730
+ if (parent.mode === "isolate" && (!parent.worktree || !fs24.existsSync(parent.worktree))) {
3635
3731
  throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
3636
3732
  }
3637
3733
  const ran = parent.ran ?? parent.worker;
@@ -3650,15 +3746,20 @@ function prepareRun(o) {
3650
3746
  throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
3651
3747
  }
3652
3748
  const mode = parent?.mode ?? o.mode;
3653
- const dir = path19.resolve(parent?.dir ?? o.dir);
3654
- if (!fs23.existsSync(dir) || !fs23.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
3749
+ const dir = path20.resolve(parent?.dir ?? o.dir);
3750
+ if (!fs24.existsSync(dir) || !fs24.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
3655
3751
  const root = repoRoot(dir);
3656
3752
  if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
3657
3753
  if (mode === "write" && !root && !o.allowNonGit) {
3658
3754
  throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
3659
3755
  }
3660
- const files = o.files.map((f) => path19.resolve(f));
3661
- for (const f of files) if (!fs23.existsSync(f)) throw new UserError(`file not found: ${f}`);
3756
+ const files = o.files.map((f) => path20.resolve(f));
3757
+ for (const f of files) if (!fs24.existsSync(f)) throw new UserError(`file not found: ${f}`);
3758
+ if (!parent && !process.env.PITROOM_NO_SECRET_WARNING) {
3759
+ const secrets = mode === "isolate" && root ? findSecretFilesInTree(root, dir) : findSecretFiles(dir);
3760
+ const note = secretWarning(secrets, mode);
3761
+ if (note) warnings.push(note);
3762
+ }
3662
3763
  const meta = {
3663
3764
  id: newRunId(),
3664
3765
  version: VERSION2,
@@ -3693,13 +3794,13 @@ function prepareRun(o) {
3693
3794
  try {
3694
3795
  acquireWriteLock(root, meta.id);
3695
3796
  } catch (e) {
3696
- fs23.rmSync(runDir(meta.id), { recursive: true, force: true });
3797
+ fs24.rmSync(runDir(meta.id), { recursive: true, force: true });
3697
3798
  throw e;
3698
3799
  }
3699
3800
  }
3700
- fs23.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
3801
+ fs24.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
3701
3802
  `);
3702
- if (work.brief) fs23.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
3803
+ if (work.brief) fs24.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
3703
3804
  return meta;
3704
3805
  }
3705
3806
  var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -3772,7 +3873,7 @@ async function execute(meta) {
3772
3873
  (meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
3773
3874
  for (const f of ["events.jsonl", "stderr.log"]) {
3774
3875
  const from = runFile(meta.id, f);
3775
- if (fs23.existsSync(from)) fs23.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
3876
+ if (fs24.existsSync(from)) fs24.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
3776
3877
  }
3777
3878
  writeMeta(meta);
3778
3879
  }
@@ -3792,9 +3893,9 @@ function prepareTree(meta) {
3792
3893
  if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
3793
3894
  if (root && meta.mode === "isolate" && !meta.worktree) {
3794
3895
  meta.baseTree = snapshotTree(root);
3795
- meta.worktree = path19.join(worktreesDir(), meta.id);
3896
+ meta.worktree = path20.join(worktreesDir(), meta.id);
3796
3897
  createIsolatedCopy(root, meta.baseTree, meta.worktree);
3797
- meta.cwd = path19.join(meta.worktree, path19.relative(root, meta.dir));
3898
+ meta.cwd = path20.join(meta.worktree, path20.relative(root, meta.dir));
3798
3899
  const linked = linkIntoWorktree(root, meta.worktree, meta.link);
3799
3900
  if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
3800
3901
  }
@@ -3817,7 +3918,7 @@ function attempt(meta, backend, target) {
3817
3918
  timeoutSec: meta.timeoutSec
3818
3919
  });
3819
3920
  }
3820
- var read = (f) => fs23.existsSync(f) ? fs23.readFileSync(f, "utf8") : "";
3921
+ var read = (f) => fs24.existsSync(f) ? fs24.readFileSync(f, "utf8") : "";
3821
3922
  function retryableFailure(meta, backend, res) {
3822
3923
  const run = backend.parse(read(runFile(meta.id, "events.jsonl")));
3823
3924
  if (run.usage.steps > 0) return void 0;
@@ -3836,12 +3937,12 @@ function finalize(meta, res) {
3836
3937
  const run = backend.parse(read(runFile(meta.id, "events.jsonl")));
3837
3938
  meta.sessionId = run.sessionId ?? meta.sessionId;
3838
3939
  meta.usage = run.usage;
3839
- fs23.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
3940
+ fs24.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
3840
3941
  `);
3841
3942
  if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run.finalText);
3842
3943
  if (meta.reviewOf) {
3843
3944
  meta.verdict = parseVerdict(run.finalText);
3844
- if (meta.packageFile) fs23.rmSync(meta.packageFile, { force: true });
3945
+ if (meta.packageFile) fs24.rmSync(meta.packageFile, { force: true });
3845
3946
  }
3846
3947
  if (!res.timedOut && !res.stopped && !meta.error) {
3847
3948
  const f = backend.failure(run, read(runFile(meta.id, "stderr.log")), res.code);
@@ -3883,7 +3984,7 @@ function captureChanges(meta) {
3883
3984
  const d = diffTrees(target, meta.baseTree, meta.afterTree);
3884
3985
  meta.changes = d.changes;
3885
3986
  meta.stats = d.stats;
3886
- fs23.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
3987
+ fs24.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
3887
3988
  }
3888
3989
  function runVerify(meta) {
3889
3990
  const r = spawnSync5(meta.verify, {
@@ -3896,7 +3997,7 @@ function runVerify(meta) {
3896
3997
  env: { ...process.env, PWD: meta.cwd, PITROOM_ACTIVE: "1" }
3897
3998
  });
3898
3999
  const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
3899
- fs23.writeFileSync(runFile(meta.id, "verify.log"), output);
4000
+ fs24.writeFileSync(runFile(meta.id, "verify.log"), output);
3900
4001
  return { ok: r.status === 0, code: r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
3901
4002
  }
3902
4003
  function applyRun(meta, allowDelete = false) {
@@ -3941,7 +4042,7 @@ function discardRun(meta) {
3941
4042
  return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
3942
4043
  }
3943
4044
  function cleanupWorktree(meta) {
3944
- if (meta.worktree && fs23.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
4045
+ if (meta.worktree && fs24.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
3945
4046
  }
3946
4047
 
3947
4048
  // src/cli/commands.ts
@@ -3951,7 +4052,8 @@ async function launch(p, meta) {
3951
4052
  console.log(
3952
4053
  has(p, "json") ? JSON.stringify(meta, null, 2) : `pitroom started ${meta.mode} run ${meta.id} in background${meta.group ? ` (group ${meta.group})` : ""}
3953
4054
  wait: pitroom wait ${meta.id}
3954
- status: pitroom status ${meta.id}`
4055
+ status: pitroom status ${meta.id}` + meta.warnings.map((w) => `
4056
+ warning: ${w}`).join("")
3955
4057
  );
3956
4058
  return 0;
3957
4059
  }
@@ -3990,10 +4092,10 @@ async function cmdReview(p) {
3990
4092
  writeMeta(meta);
3991
4093
  }
3992
4094
  } catch (e) {
3993
- fs24.rmSync(packageFile, { force: true });
4095
+ fs25.rmSync(packageFile, { force: true });
3994
4096
  throw e;
3995
4097
  }
3996
- fs24.writeFileSync(runFile(meta.id, "package.md"), job.package);
4098
+ fs25.writeFileSync(runFile(meta.id, "package.md"), job.package);
3997
4099
  return launch(p, meta);
3998
4100
  }
3999
4101
  function cmdPlan(p) {
@@ -4013,7 +4115,7 @@ function cmdPlan(p) {
4013
4115
  }
4014
4116
  function cmdCrew(p) {
4015
4117
  const file = flag(p, "task-file");
4016
- const tasks = (file ? fs24.readFileSync(file, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
4118
+ const tasks = (file ? fs25.readFileSync(file, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
4017
4119
  if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
4018
4120
  if (has(p, "write") && tasks.length > 1) {
4019
4121
  throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
@@ -4211,7 +4313,7 @@ function sinceMs(s) {
4211
4313
  if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
4212
4314
  return Date.now() - Number(m[1]) * 864e5;
4213
4315
  }
4214
- var readStdin = () => process.stdin.isTTY ? "" : fs24.readFileSync(0, "utf8");
4316
+ var readStdin = () => process.stdin.isTTY ? "" : fs25.readFileSync(0, "utf8");
4215
4317
  function cmdStatusline(p) {
4216
4318
  const input = readStdin();
4217
4319
  const lines = [];
@@ -4280,8 +4382,8 @@ function cmdSavings(p) {
4280
4382
  est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
4281
4383
  const out = flag(p, "card");
4282
4384
  if (out) {
4283
- fs24.writeFileSync(out, card(t, period));
4284
- console.log(`card written to ${path20.resolve(out)}`);
4385
+ fs25.writeFileSync(out, card(t, period));
4386
+ console.log(`card written to ${path21.resolve(out)}`);
4285
4387
  }
4286
4388
  if (has(p, "badge")) console.log(`![pitroom](${badgeUrl(t)})`);
4287
4389
  return 0;
@@ -4301,7 +4403,7 @@ function cmdConfig(p) {
4301
4403
  console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
4302
4404
  return 0;
4303
4405
  }
4304
- console.log(`config file: ${configPath()}${fs24.existsSync(configPath()) ? "" : " (not present)"}`);
4406
+ console.log(`config file: ${configPath()}${fs25.existsSync(configPath()) ? "" : " (not present)"}`);
4305
4407
  for (const [key, s] of Object.entries(eff)) {
4306
4408
  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);
4307
4409
  console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
@@ -4325,7 +4427,7 @@ function cmdClean(p) {
4325
4427
  const m = readMeta(id);
4326
4428
  if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
4327
4429
  recordRun(m);
4328
- fs24.rmSync(runDir(id), { recursive: true, force: true });
4430
+ fs25.rmSync(runDir(id), { recursive: true, force: true });
4329
4431
  }
4330
4432
  console.log(`removed ${old.length} run(s); the history and the savings ledger are kept`);
4331
4433
  return 0;
@@ -4335,9 +4437,9 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
4335
4437
 
4336
4438
  // src/core/doctor.ts
4337
4439
  import { spawnSync as spawnSync7 } from "node:child_process";
4338
- import fs25 from "node:fs";
4440
+ import fs26 from "node:fs";
4339
4441
  import os9 from "node:os";
4340
- import path21 from "node:path";
4442
+ import path22 from "node:path";
4341
4443
  var MARK = { ok: "\u2714", warn: "!", fail: "\u2718" };
4342
4444
  var READ_ONLY_HOW = {
4343
4445
  "permission-rules": "per-run permission rules",
@@ -4351,7 +4453,7 @@ function doctor4(probe) {
4351
4453
  add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
4352
4454
  add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
4353
4455
  const cfg = loadConfig();
4354
- add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs25.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
4456
+ add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs26.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
4355
4457
  for (const w of cfg.warnings) add("warn", w);
4356
4458
  if (process.platform !== "win32") {
4357
4459
  const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
@@ -4452,13 +4554,13 @@ function skillChecks() {
4452
4554
  else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
4453
4555
  }
4454
4556
  if (viaPlugin) {
4455
- const linked = installedSkills().some((i) => i.base.includes(`${path21.sep}.claude${path21.sep}`) && i.names.length);
4557
+ const linked = installedSkills().some((i) => i.base.includes(`${path22.sep}.claude${path22.sep}`) && i.names.length);
4456
4558
  checks.push(
4457
4559
  linked ? { level: "warn", message: "Pitroom is installed as a Claude Code plugin and linked into ~/.claude/skills: skills load twice; run `pitroom uninstall` or remove the plugin" } : { level: "ok", message: "Claude Code plugin installed (skills + session-start hook)" }
4458
4560
  );
4459
4561
  }
4460
4562
  const launcher = launcherPath();
4461
- if (fs25.existsSync(launcher)) {
4563
+ if (fs26.existsSync(launcher)) {
4462
4564
  const r = spawnSync7(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
4463
4565
  checks.push(
4464
4566
  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)}` }
@@ -4470,8 +4572,8 @@ function skillChecks() {
4470
4572
  }
4471
4573
  function pluginInstalled() {
4472
4574
  try {
4473
- const f = path21.join(os9.homedir(), ".claude", "plugins", "installed_plugins.json");
4474
- const plugins = JSON.parse(fs25.readFileSync(f, "utf8")).plugins ?? {};
4575
+ const f = path22.join(os9.homedir(), ".claude", "plugins", "installed_plugins.json");
4576
+ const plugins = JSON.parse(fs26.readFileSync(f, "utf8")).plugins ?? {};
4475
4577
  return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
4476
4578
  } catch {
4477
4579
  return false;
@@ -4488,24 +4590,24 @@ function superpowersActive() {
4488
4590
  for (const p of openCodePlugins()) {
4489
4591
  if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
4490
4592
  }
4491
- for (const base2 of [path21.join(os9.homedir(), ".agents", "skills"), path21.join(os9.homedir(), ".claude", "skills")]) {
4492
- const dir = path21.join(base2, "using-superpowers");
4493
- if (fs25.existsSync(path21.join(dir, "SKILL.md"))) found.push(dir);
4593
+ for (const base2 of [path22.join(os9.homedir(), ".agents", "skills"), path22.join(os9.homedir(), ".claude", "skills")]) {
4594
+ const dir = path22.join(base2, "using-superpowers");
4595
+ if (fs26.existsSync(path22.join(dir, "SKILL.md"))) found.push(dir);
4494
4596
  }
4495
4597
  return found;
4496
4598
  }
4497
4599
  function claudePlugins() {
4498
4600
  try {
4499
- const f = path21.join(os9.homedir(), ".claude", "plugins", "installed_plugins.json");
4500
- return Object.keys(JSON.parse(fs25.readFileSync(f, "utf8")).plugins ?? {});
4601
+ const f = path22.join(os9.homedir(), ".claude", "plugins", "installed_plugins.json");
4602
+ return Object.keys(JSON.parse(fs26.readFileSync(f, "utf8")).plugins ?? {});
4501
4603
  } catch {
4502
4604
  return [];
4503
4605
  }
4504
4606
  }
4505
4607
  function claudePluginEnabled(key) {
4506
4608
  try {
4507
- const f = path21.join(os9.homedir(), ".claude", "settings.json");
4508
- return JSON.parse(fs25.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
4609
+ const f = path22.join(os9.homedir(), ".claude", "settings.json");
4610
+ return JSON.parse(fs26.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
4509
4611
  } catch {
4510
4612
  return true;
4511
4613
  }
@@ -4514,7 +4616,7 @@ function codexPlugins() {
4514
4616
  const plugins = /* @__PURE__ */ new Map();
4515
4617
  let text;
4516
4618
  try {
4517
- text = fs25.readFileSync(path21.join(process.env.CODEX_HOME ?? path21.join(os9.homedir(), ".codex"), "config.toml"), "utf8");
4619
+ text = fs26.readFileSync(path22.join(process.env.CODEX_HOME ?? path22.join(os9.homedir(), ".codex"), "config.toml"), "utf8");
4518
4620
  } catch {
4519
4621
  return plugins;
4520
4622
  }
@@ -4532,11 +4634,11 @@ function codexPlugins() {
4532
4634
  return plugins;
4533
4635
  }
4534
4636
  function openCodePlugins() {
4535
- const dir = path21.join(process.env.XDG_CONFIG_HOME ?? path21.join(os9.homedir(), ".config"), "opencode");
4637
+ const dir = path22.join(process.env.XDG_CONFIG_HOME ?? path22.join(os9.homedir(), ".config"), "opencode");
4536
4638
  const plugins = [];
4537
4639
  for (const name of ["opencode.json", "opencode.jsonc"]) {
4538
4640
  try {
4539
- const text = fs25.readFileSync(path21.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
4641
+ const text = fs26.readFileSync(path22.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
4540
4642
  const list2 = JSON.parse(text).plugin;
4541
4643
  if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
4542
4644
  } catch {
@@ -4544,7 +4646,7 @@ function openCodePlugins() {
4544
4646
  }
4545
4647
  for (const folder of ["plugin", "plugins"]) {
4546
4648
  try {
4547
- for (const f of fs25.readdirSync(path21.join(dir, folder))) plugins.push(path21.join(dir, folder, f));
4649
+ for (const f of fs26.readdirSync(path22.join(dir, folder))) plugins.push(path22.join(dir, folder, f));
4548
4650
  } catch {
4549
4651
  }
4550
4652
  }