pitroom 0.12.0 → 0.13.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
@@ -1416,7 +1416,8 @@ var SCHEMA = {
1416
1416
  models: "record",
1417
1417
  tiers: "record",
1418
1418
  costs: "numbers",
1419
- audit: "number"
1419
+ audit: "number",
1420
+ readIn: "string"
1420
1421
  };
1421
1422
  function configPath() {
1422
1423
  if (process.env.PITROOM_CONFIG) return path7.resolve(process.env.PITROOM_CONFIG);
@@ -1474,6 +1475,7 @@ var rate = (v) => {
1474
1475
  const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
1475
1476
  return typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.min(n, 1) : void 0;
1476
1477
  };
1478
+ var readIn = (v) => v === "auto" || v === "snapshot" || v === "project" ? v : void 0;
1477
1479
  var list = (s) => s?.split(",").map((x) => x.trim()).filter(Boolean);
1478
1480
  function setting(flag2, env, conf, fallback) {
1479
1481
  if (flag2 !== void 0) return { value: flag2, source: "flag" };
@@ -1498,7 +1500,8 @@ function effective(flags = {}) {
1498
1500
  models: setting(void 0, void 0, c.models, {}),
1499
1501
  tiers: setting(void 0, void 0, c.tiers, {}),
1500
1502
  costs: setting(void 0, void 0, c.costs, {}),
1501
- audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0)
1503
+ audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0),
1504
+ readIn: setting(void 0, readIn(e.PITROOM_READ_IN), readIn(c.readIn), "auto")
1502
1505
  };
1503
1506
  }
1504
1507
 
@@ -1545,6 +1548,7 @@ var BOOL_FLAGS = {
1545
1548
  "-i": "isolate",
1546
1549
  "--isolate": "isolate",
1547
1550
  "--bg": "bg",
1551
+ "--in-place": "in-place",
1548
1552
  "--audit": "audit",
1549
1553
  "--no-audit": "no-audit",
1550
1554
  "--web": "web",
@@ -1639,6 +1643,7 @@ function runOptions(p, task) {
1639
1643
  return {
1640
1644
  mode: modes[0] ?? "read",
1641
1645
  auditRate: has(p, "no-audit") ? 0 : has(p, "audit") ? 1 : void 0,
1646
+ inPlace: has(p, "in-place"),
1642
1647
  task,
1643
1648
  dir: flag(p, "dir") ?? process.cwd(),
1644
1649
  files: p.flags.get("file") ?? [],
@@ -1672,8 +1677,8 @@ function effortFlag(p) {
1672
1677
 
1673
1678
  // src/cli/commands.ts
1674
1679
  import { spawnSync as spawnSync7 } from "node:child_process";
1675
- import fs28 from "node:fs";
1676
- import path24 from "node:path";
1680
+ import fs29 from "node:fs";
1681
+ import path25 from "node:path";
1677
1682
  import { fileURLToPath as fileURLToPath4 } from "node:url";
1678
1683
 
1679
1684
  // src/core/init.ts
@@ -2968,84 +2973,17 @@ ${rulings}`,
2968
2973
  ].join("\n");
2969
2974
  }
2970
2975
 
2971
- // src/core/cooldown.ts
2972
- import fs17 from "node:fs";
2973
- import path14 from "node:path";
2974
- var MAX_MS = 24 * 36e5;
2975
- var OVERLOAD_MS = 20 * 6e4;
2976
- var DAILY_MS = 4 * 36e5;
2977
- var file = () => path14.join(home(), "cooldowns.json");
2978
- function cooldownKey(target, backend) {
2979
- const model = (target.model ?? backend.defaultModel?.() ?? "").split("#")[0];
2980
- return `${target.backend}:${model}`;
2981
- }
2982
- function retryAfterMs(message) {
2983
- const compact3 = /(?:retry|try again|resets?|available again)[^0-9]{0,24}((?:\d+(?:\.\d+)?\s*[dhms]\s*)+)/i.exec(message)?.[1];
2984
- const words = /(?:retry|try again|resets?)[^0-9]{0,24}(\d+(?:\.\d+)?)\s*(second|minute|hour)s?/i.exec(message);
2985
- let ms = 0;
2986
- if (compact3) {
2987
- for (const [, n, unit] of compact3.matchAll(/(\d+(?:\.\d+)?)\s*([dhms])/gi)) ms += Number(n) * { d: 864e5, h: 36e5, m: 6e4, s: 1e3 }[unit.toLowerCase()];
2988
- } else if (words) {
2989
- ms = Number(words[1]) * { second: 1e3, minute: 6e4, hour: 36e5 }[words[2].toLowerCase()];
2990
- }
2991
- return ms > 0 ? ms : void 0;
2992
- }
2993
- function cooldownMs(message) {
2994
- const told = retryAfterMs(message);
2995
- if (told !== void 0) return Math.min(Math.max(told, 6e4), MAX_MS);
2996
- return /daily|per.?day|per day|exhausted your/i.test(message) ? DAILY_MS : OVERLOAD_MS;
2997
- }
2998
- function read() {
2999
- try {
3000
- const raw = JSON.parse(fs17.readFileSync(file(), "utf8"));
3001
- return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
3002
- } catch {
3003
- return {};
3004
- }
3005
- }
3006
- function write(all) {
3007
- fs17.mkdirSync(path14.dirname(file()), { recursive: true });
3008
- const tmp = `${file()}.${process.pid}.tmp`;
3009
- fs17.writeFileSync(tmp, `${JSON.stringify(all, null, 2)}
3010
- `);
3011
- fs17.renameSync(tmp, file());
3012
- }
3013
- function activeCooldowns(now = Date.now()) {
3014
- return Object.fromEntries(Object.entries(read()).filter(([, c]) => Date.parse(c.until) > now));
3015
- }
3016
- var activeCooldown = (key, now = Date.now()) => activeCooldowns(now)[key];
3017
- function recordCooldown(key, targetName, message, now = Date.now()) {
3018
- try {
3019
- const all = activeCooldowns(now);
3020
- all[key] = { until: new Date(now + cooldownMs(message)).toISOString(), reason: message.replace(/\s+/g, " ").trim().slice(0, 160), target: targetName };
3021
- write(all);
3022
- } catch {
3023
- }
3024
- }
3025
- function clearCooldowns(key) {
3026
- const all = read();
3027
- const keys = key ? Object.keys(all).filter((k) => k === key) : Object.keys(all);
3028
- for (const k of keys) delete all[k];
3029
- if (keys.length) write(all);
3030
- return keys.length;
3031
- }
3032
- function untilText(c, now = Date.now()) {
3033
- const d = new Date(c.until);
3034
- const time = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
3035
- return d.toDateString() === new Date(now).toDateString() ? `until ${time}` : `until ${d.toLocaleDateString()} ${time}`;
3036
- }
3037
-
3038
- // src/core/review.ts
3039
- import crypto5 from "node:crypto";
2976
+ // src/core/snapshot.ts
3040
2977
  import fs19 from "node:fs";
3041
2978
  import path16 from "node:path";
2979
+ import crypto5 from "node:crypto";
3042
2980
 
3043
2981
  // src/vcs/git.ts
3044
2982
  import { spawnSync as spawnSync5 } from "node:child_process";
3045
2983
  import crypto4 from "node:crypto";
3046
- import fs18 from "node:fs";
2984
+ import fs17 from "node:fs";
3047
2985
  import os9 from "node:os";
3048
- import path15 from "node:path";
2986
+ import path14 from "node:path";
3049
2987
  var SAFE = [
3050
2988
  "-c",
3051
2989
  "color.ui=false",
@@ -3077,20 +3015,24 @@ function gitAvailable() {
3077
3015
  }
3078
3016
  function repoRoot(dir) {
3079
3017
  const r = git(dir, ["rev-parse", "--show-toplevel"]);
3080
- return r.code === 0 ? path15.resolve(r.stdout.trim()) : void 0;
3018
+ return r.code === 0 ? path14.resolve(r.stdout.trim()) : void 0;
3081
3019
  }
3082
- function snapshotTree(root, exclude = []) {
3083
- const tmp = path15.join(os9.tmpdir(), `pitroom-index-${process.pid}-${crypto4.randomBytes(4).toString("hex")}`);
3084
- const real = path15.resolve(root, must(root, ["rev-parse", "--git-path", "index"]).trim());
3020
+ function snapshotTree(root, exclude = [], drop = []) {
3021
+ const tmp = path14.join(os9.tmpdir(), `pitroom-index-${process.pid}-${crypto4.randomBytes(4).toString("hex")}`);
3022
+ const real = path14.resolve(root, must(root, ["rev-parse", "--git-path", "index"]).trim());
3085
3023
  const env = { ...process.env, GIT_INDEX_FILE: tmp };
3086
3024
  try {
3087
- if (fs18.existsSync(real)) fs18.copyFileSync(real, tmp);
3025
+ if (fs17.existsSync(real)) fs17.copyFileSync(real, tmp);
3088
3026
  else must(root, ["read-tree", "--empty"], env);
3089
- must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`)], env);
3027
+ must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`), ...drop.map((p) => `:(top,literal,exclude)${p}`)], env);
3028
+ if (drop.length) {
3029
+ const r = spawnSync5("git", ["update-index", "--force-remove", "-z", "--stdin"], { cwd: root, env, input: `${drop.join("\0")}\0`, encoding: "utf8" });
3030
+ if (r.status !== 0) throw new Error(`git update-index failed: ${r.stderr.trim()}`);
3031
+ }
3090
3032
  return must(root, ["write-tree"], env).trim();
3091
3033
  } finally {
3092
- fs18.rmSync(tmp, { force: true });
3093
- fs18.rmSync(`${tmp}.lock`, { force: true });
3034
+ fs17.rmSync(tmp, { force: true });
3035
+ fs17.rmSync(`${tmp}.lock`, { force: true });
3094
3036
  }
3095
3037
  }
3096
3038
  var DIFF = ["diff", "--no-ext-diff", "--no-textconv", "--no-renames"];
@@ -3110,27 +3052,27 @@ function diffTrees(root, a, b) {
3110
3052
  return { changes, stats: { files: changes.length, insertions, deletions }, patch };
3111
3053
  }
3112
3054
  function createIsolatedCopy(root, tree, dest) {
3113
- const objects = path15.join(path15.resolve(root, must(root, ["rev-parse", "--git-common-dir"]).trim()), "objects");
3114
- fs18.mkdirSync(path15.dirname(dest), { recursive: true });
3115
- must(path15.dirname(dest), ["init", "--quiet", dest]);
3116
- fs18.writeFileSync(path15.join(dest, ".git", "objects", "info", "alternates"), `${objects}
3055
+ const objects = path14.join(path14.resolve(root, must(root, ["rev-parse", "--git-common-dir"]).trim()), "objects");
3056
+ fs17.mkdirSync(path14.dirname(dest), { recursive: true });
3057
+ must(path14.dirname(dest), ["init", "--quiet", dest]);
3058
+ fs17.writeFileSync(path14.join(dest, ".git", "objects", "info", "alternates"), `${objects}
3117
3059
  `);
3118
3060
  must(dest, ["read-tree", tree]);
3119
3061
  must(dest, ["checkout-index", "--all", "--force"]);
3120
3062
  }
3121
3063
  function removeIsolatedCopy(dest, ownedBy) {
3122
- const rel = path15.relative(ownedBy, dest);
3123
- if (!rel || rel.startsWith("..") || path15.isAbsolute(rel)) throw new Error(`refusing to remove ${dest}`);
3124
- fs18.rmSync(dest, { recursive: true, force: true });
3064
+ const rel = path14.relative(ownedBy, dest);
3065
+ if (!rel || rel.startsWith("..") || path14.isAbsolute(rel)) throw new Error(`refusing to remove ${dest}`);
3066
+ fs17.rmSync(dest, { recursive: true, force: true });
3125
3067
  }
3126
3068
  function linkIntoWorktree(root, worktree, rels) {
3127
3069
  const linked = [];
3128
3070
  for (const rel of rels) {
3129
- const src = path15.join(root, rel);
3130
- const dst = path15.join(worktree, rel);
3131
- if (!fs18.existsSync(src) || fs18.existsSync(dst)) continue;
3132
- fs18.mkdirSync(path15.dirname(dst), { recursive: true });
3133
- fs18.symlinkSync(src, dst, process.platform === "win32" ? "junction" : void 0);
3071
+ const src = path14.join(root, rel);
3072
+ const dst = path14.join(worktree, rel);
3073
+ if (!fs17.existsSync(src) || fs17.existsSync(dst)) continue;
3074
+ fs17.mkdirSync(path14.dirname(dst), { recursive: true });
3075
+ fs17.symlinkSync(src, dst, process.platform === "win32" ? "junction" : void 0);
3134
3076
  linked.push(rel);
3135
3077
  }
3136
3078
  return linked;
@@ -3171,8 +3113,255 @@ function gitDir(dir) {
3171
3113
  return r.code === 0 ? r.stdout.trim() : void 0;
3172
3114
  }
3173
3115
 
3116
+ // src/core/secrets.ts
3117
+ import { execFileSync } from "node:child_process";
3118
+ import fs18 from "node:fs";
3119
+ import path15 from "node:path";
3120
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
3121
+ var MAX_VISITED = 2e4;
3122
+ var MAX_DEPTH = 4;
3123
+ var TEMPLATE = /\.(example|sample|template|dist|defaults?|tpl)$/i;
3124
+ function looksSecret(name) {
3125
+ if (TEMPLATE.test(name)) return false;
3126
+ if (name === ".env" || name.startsWith(".env.") || /\.env$/i.test(name)) return true;
3127
+ return /\.(pem|p12|pfx|key|jks|keystore)$/i.test(name) || /^id_(rsa|dsa|ecdsa|ed25519)$/.test(name) || name === ".netrc" || name === "credentials.json" || /^secrets?\.(json|ya?ml|toml)$/i.test(name);
3128
+ }
3129
+ function findSecretFiles(dir) {
3130
+ const found = [];
3131
+ let visited = 0;
3132
+ const walk = (d, depth) => {
3133
+ if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
3134
+ let entries;
3135
+ try {
3136
+ entries = fs18.readdirSync(d, { withFileTypes: true });
3137
+ } catch {
3138
+ return;
3139
+ }
3140
+ for (const e of entries) {
3141
+ visited++;
3142
+ if (e.isDirectory()) {
3143
+ if (!SKIP_DIRS.has(e.name)) walk(path15.join(d, e.name), depth + 1);
3144
+ } else if (e.isFile() && looksSecret(e.name)) {
3145
+ found.push(path15.relative(dir, path15.join(d, e.name)));
3146
+ }
3147
+ }
3148
+ };
3149
+ walk(dir, 0);
3150
+ return found.sort();
3151
+ }
3152
+ function findSecretFilesInTree(root, dir) {
3153
+ try {
3154
+ const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
3155
+ return out.split("\0").filter((f) => f && looksSecret(path15.basename(f)) && !path15.relative(dir, path15.join(root, f)).startsWith("..")).map((f) => path15.relative(dir, path15.join(root, f))).sort();
3156
+ } catch {
3157
+ return [];
3158
+ }
3159
+ }
3160
+ function secretWarning(files, mode) {
3161
+ if (!files.length) return void 0;
3162
+ const shown = files.slice(0, 3).join(", ") + (files.length > 3 ? `, +${files.length - 3} more` : "");
3163
+ const where = mode === "isolate" ? "would be copied into the isolated copy" : "sit in the directory the worker runs in";
3164
+ 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";
3165
+ 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`;
3166
+ }
3167
+
3168
+ // src/core/snapshot.ts
3169
+ var READ_IN = ["auto", "snapshot", "project"];
3170
+ var KEEP_MS = 24 * 36e5;
3171
+ var PRUNE_AGE_MS = 3 * 24 * 36e5;
3172
+ var KEEP_NEWEST = 6;
3173
+ var JUST_USED_MS = 10 * 6e4;
3174
+ var snapshotsDir = () => path16.join(home(), "snapshots");
3175
+ function wantSnapshot(readIn2, root, dir) {
3176
+ if (!root || readIn2 === "project") return void 0;
3177
+ const secrets = findSecretFiles(dir);
3178
+ return readIn2 === "snapshot" || secrets.length ? { secrets } : void 0;
3179
+ }
3180
+ function readSnapshot(root) {
3181
+ const drop = findSecretFilesInTree(root, root);
3182
+ const tree = snapshotTree(root, [], drop);
3183
+ const dest = path16.join(snapshotsDir(), tree);
3184
+ if (fs19.existsSync(path16.join(dest, ".git"))) {
3185
+ touch(dest);
3186
+ return { dir: dest, tree, created: false };
3187
+ }
3188
+ const tmp = path16.join(snapshotsDir(), `.tmp-${process.pid}-${crypto5.randomBytes(3).toString("hex")}`);
3189
+ try {
3190
+ createIsolatedCopy(root, tree, tmp);
3191
+ readOnly(tmp);
3192
+ try {
3193
+ fs19.renameSync(tmp, dest);
3194
+ } catch (e) {
3195
+ if (!fs19.existsSync(path16.join(dest, ".git"))) throw e;
3196
+ removeIsolatedCopy(tmp, snapshotsDir());
3197
+ }
3198
+ } catch (e) {
3199
+ try {
3200
+ removeIsolatedCopy(tmp, snapshotsDir());
3201
+ } catch {
3202
+ }
3203
+ throw e;
3204
+ }
3205
+ prune();
3206
+ return { dir: dest, tree, created: true };
3207
+ }
3208
+ function touch(dir) {
3209
+ try {
3210
+ const now = /* @__PURE__ */ new Date();
3211
+ fs19.utimesSync(dir, now, now);
3212
+ } catch {
3213
+ }
3214
+ }
3215
+ function readOnly(dir) {
3216
+ const walk = (d) => {
3217
+ for (const e of fs19.readdirSync(d, { withFileTypes: true })) {
3218
+ if (e.name === ".git") continue;
3219
+ const p = path16.join(d, e.name);
3220
+ if (e.isDirectory()) walk(p);
3221
+ else if (e.isFile()) fs19.chmodSync(p, fs19.statSync(p).mode & ~146);
3222
+ }
3223
+ };
3224
+ try {
3225
+ walk(dir);
3226
+ } catch {
3227
+ }
3228
+ }
3229
+ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWEST) {
3230
+ let names;
3231
+ try {
3232
+ names = fs19.readdirSync(snapshotsDir());
3233
+ } catch {
3234
+ return 0;
3235
+ }
3236
+ const inUse = /* @__PURE__ */ new Set();
3237
+ for (const id of listRunIds()) {
3238
+ try {
3239
+ const m = readMeta(id);
3240
+ if (m.snapshot?.dir && isActive(m.state)) inUse.add(path16.resolve(m.snapshot.dir));
3241
+ } catch {
3242
+ }
3243
+ }
3244
+ const entries = names.map((n) => {
3245
+ const dir = path16.join(snapshotsDir(), n);
3246
+ try {
3247
+ return { n, dir, age: now - fs19.statSync(dir).mtimeMs };
3248
+ } catch {
3249
+ return void 0;
3250
+ }
3251
+ }).filter((e) => !!e).sort((x, y) => x.age - y.age);
3252
+ let removed = 0;
3253
+ let rank = 0;
3254
+ for (const e of entries) {
3255
+ if (inUse.has(path16.resolve(e.dir))) continue;
3256
+ const half = e.n.startsWith(".tmp-");
3257
+ const old = e.age >= (half ? KEEP_MS : maxAgeMs);
3258
+ const surplus = !half && ++rank > keepNewest && e.age >= JUST_USED_MS;
3259
+ if (!old && !surplus) continue;
3260
+ try {
3261
+ makeWritable(e.dir);
3262
+ removeIsolatedCopy(e.dir, snapshotsDir());
3263
+ removed++;
3264
+ } catch {
3265
+ }
3266
+ }
3267
+ return removed;
3268
+ }
3269
+ function makeWritable(dir) {
3270
+ const walk = (d) => {
3271
+ for (const e of fs19.readdirSync(d, { withFileTypes: true })) {
3272
+ const p = path16.join(d, e.name);
3273
+ if (e.isDirectory()) walk(p);
3274
+ else if (e.isFile()) fs19.chmodSync(p, fs19.statSync(p).mode | 128);
3275
+ }
3276
+ };
3277
+ try {
3278
+ walk(dir);
3279
+ } catch {
3280
+ }
3281
+ }
3282
+ function backToProject(text, snapshotDir, root) {
3283
+ let real = snapshotDir;
3284
+ try {
3285
+ real = fs19.realpathSync(snapshotDir);
3286
+ } catch {
3287
+ }
3288
+ let out = text;
3289
+ for (const dir of /* @__PURE__ */ new Set([real, snapshotDir])) out = out.split(`${dir}${path16.sep}`).join(`${root}${path16.sep}`).split(dir).join(root);
3290
+ return out;
3291
+ }
3292
+
3293
+ // src/core/cooldown.ts
3294
+ import fs20 from "node:fs";
3295
+ import path17 from "node:path";
3296
+ var MAX_MS = 24 * 36e5;
3297
+ var OVERLOAD_MS = 20 * 6e4;
3298
+ var DAILY_MS = 4 * 36e5;
3299
+ var file = () => path17.join(home(), "cooldowns.json");
3300
+ function cooldownKey(target, backend) {
3301
+ const model = (target.model ?? backend.defaultModel?.() ?? "").split("#")[0];
3302
+ return `${target.backend}:${model}`;
3303
+ }
3304
+ function retryAfterMs(message) {
3305
+ const compact3 = /(?:retry|try again|resets?|available again)[^0-9]{0,24}((?:\d+(?:\.\d+)?\s*[dhms]\s*)+)/i.exec(message)?.[1];
3306
+ const words = /(?:retry|try again|resets?)[^0-9]{0,24}(\d+(?:\.\d+)?)\s*(second|minute|hour)s?/i.exec(message);
3307
+ let ms = 0;
3308
+ if (compact3) {
3309
+ for (const [, n, unit] of compact3.matchAll(/(\d+(?:\.\d+)?)\s*([dhms])/gi)) ms += Number(n) * { d: 864e5, h: 36e5, m: 6e4, s: 1e3 }[unit.toLowerCase()];
3310
+ } else if (words) {
3311
+ ms = Number(words[1]) * { second: 1e3, minute: 6e4, hour: 36e5 }[words[2].toLowerCase()];
3312
+ }
3313
+ return ms > 0 ? ms : void 0;
3314
+ }
3315
+ function cooldownMs(message) {
3316
+ const told = retryAfterMs(message);
3317
+ if (told !== void 0) return Math.min(Math.max(told, 6e4), MAX_MS);
3318
+ return /daily|per.?day|per day|exhausted your/i.test(message) ? DAILY_MS : OVERLOAD_MS;
3319
+ }
3320
+ function read() {
3321
+ try {
3322
+ const raw = JSON.parse(fs20.readFileSync(file(), "utf8"));
3323
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
3324
+ } catch {
3325
+ return {};
3326
+ }
3327
+ }
3328
+ function write(all) {
3329
+ fs20.mkdirSync(path17.dirname(file()), { recursive: true });
3330
+ const tmp = `${file()}.${process.pid}.tmp`;
3331
+ fs20.writeFileSync(tmp, `${JSON.stringify(all, null, 2)}
3332
+ `);
3333
+ fs20.renameSync(tmp, file());
3334
+ }
3335
+ function activeCooldowns(now = Date.now()) {
3336
+ return Object.fromEntries(Object.entries(read()).filter(([, c]) => Date.parse(c.until) > now));
3337
+ }
3338
+ var activeCooldown = (key, now = Date.now()) => activeCooldowns(now)[key];
3339
+ function recordCooldown(key, targetName, message, now = Date.now()) {
3340
+ try {
3341
+ const all = activeCooldowns(now);
3342
+ all[key] = { until: new Date(now + cooldownMs(message)).toISOString(), reason: message.replace(/\s+/g, " ").trim().slice(0, 160), target: targetName };
3343
+ write(all);
3344
+ } catch {
3345
+ }
3346
+ }
3347
+ function clearCooldowns(key) {
3348
+ const all = read();
3349
+ const keys = key ? Object.keys(all).filter((k) => k === key) : Object.keys(all);
3350
+ for (const k of keys) delete all[k];
3351
+ if (keys.length) write(all);
3352
+ return keys.length;
3353
+ }
3354
+ function untilText(c, now = Date.now()) {
3355
+ const d = new Date(c.until);
3356
+ const time = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
3357
+ return d.toDateString() === new Date(now).toDateString() ? `until ${time}` : `until ${d.toLocaleDateString()} ${time}`;
3358
+ }
3359
+
3174
3360
  // src/core/review.ts
3175
- var TEMPLATE = { task: "task-reviewer", fix: "re-review", range: "code-reviewer" };
3361
+ import crypto6 from "node:crypto";
3362
+ import fs21 from "node:fs";
3363
+ import path18 from "node:path";
3364
+ var TEMPLATE2 = { task: "task-reviewer", fix: "re-review", range: "code-reviewer" };
3176
3365
  var section = (title, body) => `## ${title}
3177
3366
 
3178
3367
  ${body.trim() || "(empty)"}
@@ -3189,7 +3378,7 @@ function ancestors(m) {
3189
3378
  function briefOf(m) {
3190
3379
  const root = [m, ...ancestors(m)].at(-1);
3191
3380
  const f = runFile(root.id, "brief.md");
3192
- return fs19.existsSync(f) ? fs19.readFileSync(f, "utf8") : root.task;
3381
+ return fs21.existsSync(f) ? fs21.readFileSync(f, "utf8") : root.task;
3193
3382
  }
3194
3383
  function latestReview(ids) {
3195
3384
  for (const id of listRunIds().reverse()) {
@@ -3203,7 +3392,7 @@ function latestReview(ids) {
3203
3392
  }
3204
3393
  function diffOf(m, from = m.baseTree) {
3205
3394
  const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
3206
- if (where && fs19.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
3395
+ if (where && fs21.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
3207
3396
  if (from !== m.baseTree) {
3208
3397
  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);
3209
3398
  }
@@ -3217,7 +3406,7 @@ function runReview(id) {
3217
3406
  if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
3218
3407
  const chain = ancestors(m);
3219
3408
  const previous = latestReview(chain.map((a) => a.id));
3220
- const dir = m.mode === "isolate" && m.worktree && fs19.existsSync(m.worktree) ? m.cwd : m.dir;
3409
+ const dir = m.mode === "isolate" && m.worktree && fs21.existsSync(m.worktree) ? m.cwd : m.dir;
3221
3410
  const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
3222
3411
  const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
3223
3412
  if (!previous) {
@@ -3291,15 +3480,15 @@ function pickReviewer(job) {
3291
3480
  function writePackage(job) {
3292
3481
  const g = gitDir(job.dir);
3293
3482
  if (!g) throw new UserError(`not a git repository: ${job.dir}`);
3294
- const file2 = path16.join(g, "pitroom", `review-${crypto5.randomBytes(4).toString("hex")}.md`);
3295
- fs19.mkdirSync(path16.dirname(file2), { recursive: true });
3296
- fs19.writeFileSync(file2, job.package);
3483
+ const file2 = path18.join(g, "pitroom", `review-${crypto6.randomBytes(4).toString("hex")}.md`);
3484
+ fs21.mkdirSync(path18.dirname(file2), { recursive: true });
3485
+ fs21.writeFileSync(file2, job.package);
3297
3486
  return file2;
3298
3487
  }
3299
3488
 
3300
3489
  // src/core/templates.ts
3301
- import fs20 from "node:fs";
3302
- import path17 from "node:path";
3490
+ import fs22 from "node:fs";
3491
+ import path19 from "node:path";
3303
3492
  var FILES = {
3304
3493
  implementer: "pitroom-driven-development/implementer-prompt.md",
3305
3494
  "task-reviewer": "pitroom-driven-development/task-reviewer-prompt.md",
@@ -3307,9 +3496,9 @@ var FILES = {
3307
3496
  "code-reviewer": "pitroom-review/code-reviewer.md"
3308
3497
  };
3309
3498
  function loadTemplate(name, root = packageRoot()) {
3310
- const file2 = path17.join(root, "skills", FILES[name]);
3311
- if (!fs20.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
3312
- return fs20.readFileSync(file2, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
3499
+ const file2 = path19.join(root, "skills", FILES[name]);
3500
+ if (!fs22.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
3501
+ return fs22.readFileSync(file2, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
3313
3502
  }
3314
3503
  function fill(template, values) {
3315
3504
  const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
@@ -3415,16 +3604,16 @@ function formatModels(t) {
3415
3604
 
3416
3605
  // src/core/dash.ts
3417
3606
  import { spawn } from "node:child_process";
3418
- import crypto6 from "node:crypto";
3419
- import fs21 from "node:fs";
3607
+ import crypto7 from "node:crypto";
3608
+ import fs23 from "node:fs";
3420
3609
  import http from "node:http";
3421
- import path18 from "node:path";
3610
+ import path20 from "node:path";
3422
3611
  import { fileURLToPath as fileURLToPath3 } from "node:url";
3423
3612
 
3424
3613
  // src/core/file-diff.ts
3425
3614
  var PREVIEW_LINES = 300;
3426
3615
  function gitPath(value, prefix = true) {
3427
- let path26 = value;
3616
+ let path27 = value;
3428
3617
  if (value.startsWith('"') && value.endsWith('"')) {
3429
3618
  const bytes = [];
3430
3619
  const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
@@ -3432,9 +3621,9 @@ function gitPath(value, prefix = true) {
3432
3621
  if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
3433
3622
  else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
3434
3623
  }
3435
- path26 = new TextDecoder().decode(new Uint8Array(bytes));
3624
+ path27 = new TextDecoder().decode(new Uint8Array(bytes));
3436
3625
  }
3437
- return prefix ? path26.replace(/^[ab]\//, "") : path26;
3626
+ return prefix ? path27.replace(/^[ab]\//, "") : path27;
3438
3627
  }
3439
3628
  function headerPath(header) {
3440
3629
  const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
@@ -3675,32 +3864,32 @@ var HEADERS = {
3675
3864
  "x-content-type-options": "nosniff",
3676
3865
  "referrer-policy": "no-referrer",
3677
3866
  // Own files only, plus the hash of the one inline theme script; styles may be inline (the components position popups with them).
3678
- "content-security-policy": `default-src 'none'; script-src 'self' 'sha256-${crypto6.createHash("sha256").update(THEME_SCRIPT).digest("base64")}'; style-src 'self' 'unsafe-inline'; img-src data:; connect-src 'self'; base-uri 'none'; form-action 'none'`
3867
+ "content-security-policy": `default-src 'none'; script-src 'self' 'sha256-${crypto7.createHash("sha256").update(THEME_SCRIPT).digest("base64")}'; style-src 'self' 'unsafe-inline'; img-src data:; connect-src 'self'; base-uri 'none'; form-action 'none'`
3679
3868
  };
3680
3869
  var ASSETS = { "/assets/app.js": "text/javascript; charset=utf-8", "/assets/app.css": "text/css; charset=utf-8" };
3681
3870
  var assetsDir = () => fileURLToPath3(new URL("./ui/", import.meta.url));
3682
3871
  var assetCache = /* @__PURE__ */ new Map();
3683
3872
  function readAsset(route) {
3684
3873
  try {
3685
- const file2 = path18.join(assetsDir(), path18.basename(route));
3686
- const { mtimeMs } = fs21.statSync(file2);
3874
+ const file2 = path20.join(assetsDir(), path20.basename(route));
3875
+ const { mtimeMs } = fs23.statSync(file2);
3687
3876
  const hit = assetCache.get(route);
3688
3877
  if (hit && hit.mtimeMs === mtimeMs) return hit.data;
3689
- const data = fs21.readFileSync(file2);
3878
+ const data = fs23.readFileSync(file2);
3690
3879
  assetCache.set(route, { mtimeMs, data });
3691
3880
  return data;
3692
3881
  } catch {
3693
3882
  return void 0;
3694
3883
  }
3695
3884
  }
3696
- function handler(touch) {
3885
+ function handler(touch2) {
3697
3886
  const send = (res, code, type, body) => {
3698
3887
  res.writeHead(code, { ...HEADERS, "content-type": type });
3699
3888
  res.end(body);
3700
3889
  };
3701
3890
  const json = (res, code, value) => send(res, code, "application/json; charset=utf-8", JSON.stringify(value));
3702
3891
  return (req, res) => {
3703
- touch();
3892
+ touch2();
3704
3893
  if (!LOCAL_HOST.test(req.headers.host ?? "")) return json(res, 403, { error: "forbidden host" });
3705
3894
  if (req.method !== "GET" && req.method !== "HEAD") return json(res, 405, { error: "read-only" });
3706
3895
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -3766,7 +3955,7 @@ function startDash(opts) {
3766
3955
  });
3767
3956
  });
3768
3957
  }
3769
- var registryFile = () => path18.join(home(), "dash.json");
3958
+ var registryFile = () => path20.join(home(), "dash.json");
3770
3959
  async function ping(port) {
3771
3960
  try {
3772
3961
  const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
@@ -3777,11 +3966,11 @@ async function ping(port) {
3777
3966
  }
3778
3967
  async function runningDash() {
3779
3968
  try {
3780
- const r = JSON.parse(fs21.readFileSync(registryFile(), "utf8"));
3969
+ const r = JSON.parse(fs23.readFileSync(registryFile(), "utf8"));
3781
3970
  if (isAlive(r.pid) && await ping(r.port)) return { ...r, url: `http://127.0.0.1:${r.port}/` };
3782
3971
  } catch {
3783
3972
  }
3784
- fs21.rmSync(registryFile(), { force: true });
3973
+ fs23.rmSync(registryFile(), { force: true });
3785
3974
  return void 0;
3786
3975
  }
3787
3976
  function openBrowser(url) {
@@ -3796,7 +3985,7 @@ async function dashCommand(o) {
3796
3985
  if (o.stop) {
3797
3986
  if (!existing) throw new UserError("pitroom dash is not running");
3798
3987
  process.kill(existing.pid, "SIGTERM");
3799
- fs21.rmSync(registryFile(), { force: true });
3988
+ fs23.rmSync(registryFile(), { force: true });
3800
3989
  o.log(`stopped pitroom dash (${existing.url})`);
3801
3990
  return 0;
3802
3991
  }
@@ -3833,8 +4022,8 @@ async function dashCommand(o) {
3833
4022
  if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
3834
4023
  dash = await startDash({ port: 0, idleMs: o.idleMs });
3835
4024
  }
3836
- fs21.mkdirSync(home(), { recursive: true });
3837
- fs21.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
4025
+ fs23.mkdirSync(home(), { recursive: true });
4026
+ fs23.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
3838
4027
  const stop = () => void dash.close();
3839
4028
  process.once("SIGINT", stop);
3840
4029
  process.once("SIGTERM", stop);
@@ -3843,7 +4032,7 @@ async function dashCommand(o) {
3843
4032
  if (o.open) openBrowser(dash.url);
3844
4033
  await dash.closed;
3845
4034
  try {
3846
- if (JSON.parse(fs21.readFileSync(registryFile(), "utf8")).pid === process.pid) fs21.rmSync(registryFile(), { force: true });
4035
+ if (JSON.parse(fs23.readFileSync(registryFile(), "utf8")).pid === process.pid) fs23.rmSync(registryFile(), { force: true });
3847
4036
  } catch {
3848
4037
  }
3849
4038
  return 0;
@@ -3851,8 +4040,8 @@ async function dashCommand(o) {
3851
4040
 
3852
4041
  // src/core/run.ts
3853
4042
  import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
3854
- import fs27 from "node:fs";
3855
- import path23 from "node:path";
4043
+ import fs28 from "node:fs";
4044
+ import path24 from "node:path";
3856
4045
 
3857
4046
  // src/core/chain.ts
3858
4047
  function resolveChain(flags = {}) {
@@ -3902,11 +4091,11 @@ function resolveChain(flags = {}) {
3902
4091
 
3903
4092
  // src/core/process.ts
3904
4093
  import { spawn as spawn2 } from "node:child_process";
3905
- import fs23 from "node:fs";
4094
+ import fs25 from "node:fs";
3906
4095
 
3907
4096
  // src/vcs/guard.ts
3908
- import fs22 from "node:fs";
3909
- import path19 from "node:path";
4097
+ import fs24 from "node:fs";
4098
+ import path21 from "node:path";
3910
4099
  var VERSION = 1;
3911
4100
  var SHIM = `#!/bin/sh
3912
4101
  # pitroom git guard v${VERSION}. Blocks git commands that change history, refs, the
@@ -3981,15 +4170,15 @@ esac
3981
4170
  exec "$real" "$@"
3982
4171
  `;
3983
4172
  function shimDir() {
3984
- return path19.join(home(), "shim", `v${VERSION}`);
4173
+ return path21.join(home(), "shim", `v${VERSION}`);
3985
4174
  }
3986
4175
  function findRealGit(skip) {
3987
- for (const dir of (process.env.PATH ?? "").split(path19.delimiter).filter(Boolean)) {
3988
- if (path19.resolve(dir) === skip) continue;
3989
- const p = path19.join(dir, "git");
4176
+ for (const dir of (process.env.PATH ?? "").split(path21.delimiter).filter(Boolean)) {
4177
+ if (path21.resolve(dir) === skip) continue;
4178
+ const p = path21.join(dir, "git");
3990
4179
  try {
3991
- fs22.accessSync(p, fs22.constants.X_OK);
3992
- if (fs22.statSync(p).isFile()) return p;
4180
+ fs24.accessSync(p, fs24.constants.X_OK);
4181
+ if (fs24.statSync(p).isFile()) return p;
3993
4182
  } catch {
3994
4183
  }
3995
4184
  }
@@ -4002,13 +4191,13 @@ var REF_HOOK = `#!/bin/sh
4002
4191
  echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
4003
4192
  exit 1
4004
4193
  `;
4005
- var hooksDir = () => path19.join(home(), "git-hooks", `v${HOOK_VERSION}`);
4194
+ var hooksDir = () => path21.join(home(), "git-hooks", `v${HOOK_VERSION}`);
4006
4195
  function writeIfChanged(file2, content) {
4007
- if (fs22.existsSync(file2) && fs22.readFileSync(file2, "utf8") === content) return;
4008
- fs22.mkdirSync(path19.dirname(file2), { recursive: true });
4196
+ if (fs24.existsSync(file2) && fs24.readFileSync(file2, "utf8") === content) return;
4197
+ fs24.mkdirSync(path21.dirname(file2), { recursive: true });
4009
4198
  const tmp = `${file2}.${process.pid}.tmp`;
4010
- fs22.writeFileSync(tmp, content, { mode: 493 });
4011
- fs22.renameSync(tmp, file2);
4199
+ fs24.writeFileSync(tmp, content, { mode: 493 });
4200
+ fs24.renameSync(tmp, file2);
4012
4201
  }
4013
4202
  function withGitConfig(env, entries) {
4014
4203
  const out = { ...env };
@@ -4030,7 +4219,7 @@ function guardEnv(env) {
4030
4219
  PAGER: "cat"
4031
4220
  };
4032
4221
  if (process.platform === "win32") return { ...env, ...quiet };
4033
- writeIfChanged(path19.join(hooksDir(), "reference-transaction"), REF_HOOK);
4222
+ writeIfChanged(path21.join(hooksDir(), "reference-transaction"), REF_HOOK);
4034
4223
  env = withGitConfig(env, [
4035
4224
  ["core.hooksPath", hooksDir()],
4036
4225
  ["url.pitroom-push-blocked://.pushInsteadOf", ""]
@@ -4038,14 +4227,14 @@ function guardEnv(env) {
4038
4227
  const dir = shimDir();
4039
4228
  const real = findRealGit(dir);
4040
4229
  if (!real) return { ...env, ...quiet };
4041
- writeIfChanged(path19.join(dir, "git"), SHIM);
4042
- return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${path19.delimiter}${env.PATH ?? ""}` };
4230
+ writeIfChanged(path21.join(dir, "git"), SHIM);
4231
+ return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${path21.delimiter}${env.PATH ?? ""}` };
4043
4232
  }
4044
4233
 
4045
4234
  // src/core/process.ts
4046
4235
  async function spawnWorker(inv, opts) {
4047
- const out = fs23.openSync(opts.stdoutFile, "w");
4048
- const err = fs23.openSync(opts.stderrFile, "w");
4236
+ const out = fs25.openSync(opts.stdoutFile, "w");
4237
+ const err = fs25.openSync(opts.stderrFile, "w");
4049
4238
  const res = { code: null, timedOut: false, stopped: false };
4050
4239
  const child = spawn2(inv.command, inv.args, {
4051
4240
  cwd: opts.cwd,
@@ -4077,8 +4266,8 @@ async function spawnWorker(inv, opts) {
4077
4266
  if (killer) clearTimeout(killer);
4078
4267
  process.off("SIGINT", stop);
4079
4268
  process.off("SIGTERM", stop);
4080
- fs23.closeSync(out);
4081
- fs23.closeSync(err);
4269
+ fs25.closeSync(out);
4270
+ fs25.closeSync(err);
4082
4271
  return res;
4083
4272
  }
4084
4273
 
@@ -4110,11 +4299,11 @@ function parseAudit(text) {
4110
4299
  }
4111
4300
 
4112
4301
  // src/core/slots.ts
4113
- import crypto7 from "node:crypto";
4114
- import fs24 from "node:fs";
4115
- import path20 from "node:path";
4116
- var slotsDir = () => path20.join(home(), "slots");
4117
- var locksDir = () => path20.join(home(), "locks");
4302
+ import crypto8 from "node:crypto";
4303
+ import fs26 from "node:fs";
4304
+ import path22 from "node:path";
4305
+ var slotsDir = () => path22.join(home(), "slots");
4306
+ var locksDir = () => path22.join(home(), "locks");
4118
4307
  var STARTUP_GRACE_MS = 3e4;
4119
4308
  function holderActive(runId, self) {
4120
4309
  if (!runId || runId === self) return false;
@@ -4128,23 +4317,23 @@ function holderActive(runId, self) {
4128
4317
  }
4129
4318
  }
4130
4319
  function tryClaim(file2, runId) {
4131
- fs24.mkdirSync(path20.dirname(file2), { recursive: true });
4320
+ fs26.mkdirSync(path22.dirname(file2), { recursive: true });
4132
4321
  try {
4133
- fs24.writeFileSync(file2, runId, { flag: "wx" });
4322
+ fs26.writeFileSync(file2, runId, { flag: "wx" });
4134
4323
  return true;
4135
4324
  } catch (e) {
4136
4325
  if (e.code !== "EEXIST") throw e;
4137
4326
  }
4138
4327
  let owner = "";
4139
4328
  try {
4140
- owner = fs24.readFileSync(file2, "utf8").trim();
4329
+ owner = fs26.readFileSync(file2, "utf8").trim();
4141
4330
  } catch {
4142
4331
  }
4143
4332
  if (owner === runId) return true;
4144
4333
  if (holderActive(owner, runId)) return false;
4145
- fs24.rmSync(file2, { force: true });
4334
+ fs26.rmSync(file2, { force: true });
4146
4335
  try {
4147
- fs24.writeFileSync(file2, runId, { flag: "wx" });
4336
+ fs26.writeFileSync(file2, runId, { flag: "wx" });
4148
4337
  return true;
4149
4338
  } catch {
4150
4339
  return false;
@@ -4153,25 +4342,25 @@ function tryClaim(file2, runId) {
4153
4342
  function releaseIfOwner(file2, runId) {
4154
4343
  if (!file2) return;
4155
4344
  try {
4156
- if (fs24.readFileSync(file2, "utf8").trim() === runId) fs24.rmSync(file2, { force: true });
4345
+ if (fs26.readFileSync(file2, "utf8").trim() === runId) fs26.rmSync(file2, { force: true });
4157
4346
  } catch {
4158
4347
  }
4159
4348
  }
4160
4349
  function tryAcquireSlot(runId, maxParallel) {
4161
4350
  for (let n = 0; n < Math.max(1, maxParallel); n++) {
4162
- const file2 = path20.join(slotsDir(), `slot-${n}`);
4351
+ const file2 = path22.join(slotsDir(), `slot-${n}`);
4163
4352
  if (tryClaim(file2, runId)) return file2;
4164
4353
  }
4165
4354
  return void 0;
4166
4355
  }
4167
4356
  var releaseSlot = (file2, runId) => releaseIfOwner(file2, runId);
4168
- var lockFile = (repoRoot2) => path20.join(locksDir(), `write-${crypto7.createHash("sha1").update(path20.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
4357
+ var lockFile = (repoRoot2) => path22.join(locksDir(), `write-${crypto8.createHash("sha1").update(path22.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
4169
4358
  function acquireWriteLock(repoRoot2, runId) {
4170
4359
  const file2 = lockFile(repoRoot2);
4171
4360
  if (tryClaim(file2, runId)) return;
4172
4361
  let owner = "";
4173
4362
  try {
4174
- owner = fs24.readFileSync(file2, "utf8").trim();
4363
+ owner = fs26.readFileSync(file2, "utf8").trim();
4175
4364
  } catch {
4176
4365
  }
4177
4366
  throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
@@ -4218,12 +4407,12 @@ ${task}`;
4218
4407
  }
4219
4408
 
4220
4409
  // src/core/refs.ts
4221
- import fs25 from "node:fs";
4222
- import path21 from "node:path";
4410
+ import fs27 from "node:fs";
4411
+ import path23 from "node:path";
4223
4412
  var FILE_EXTENSIONS = new Set(
4224
4413
  "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(" ")
4225
4414
  );
4226
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
4415
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
4227
4416
  var MAX_INDEXED = 6e4;
4228
4417
  var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
4229
4418
  var REF = new RegExp(
@@ -4270,13 +4459,13 @@ function extractRefs(answer) {
4270
4459
  return [...seen.values()];
4271
4460
  }
4272
4461
  function verifyRefs(refs, dirs) {
4273
- const roots = [...new Set(dirs.filter(Boolean).map((d) => path21.resolve(d)))];
4462
+ const roots = [...new Set(dirs.filter(Boolean).map((d) => path23.resolve(d)))];
4274
4463
  const invalid = [];
4275
4464
  const lineCache = /* @__PURE__ */ new Map();
4276
4465
  const load = (file2) => {
4277
4466
  if (!lineCache.has(file2)) {
4278
- const size = fs25.statSync(file2).size;
4279
- const lines = size > MAX_BYTES ? null : fs25.readFileSync(file2, "utf8").split("\n");
4467
+ const size = fs27.statSync(file2).size;
4468
+ const lines = size > MAX_BYTES ? null : fs27.readFileSync(file2, "utf8").split("\n");
4280
4469
  if (lines && lines[lines.length - 1] === "") lines.pop();
4281
4470
  lineCache.set(file2, lines);
4282
4471
  }
@@ -4287,9 +4476,9 @@ function verifyRefs(refs, dirs) {
4287
4476
  const candidates = (ref) => {
4288
4477
  const direct = resolve(ref.file, roots);
4289
4478
  if (direct) return [direct];
4290
- if (path21.isAbsolute(ref.file)) return [];
4479
+ if (path23.isAbsolute(ref.file)) return [];
4291
4480
  const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
4292
- return (byName().get(path21.basename(wanted)) ?? []).filter((f) => f.endsWith(`/${wanted}`) || path21.basename(f) === wanted).slice(0, 20);
4481
+ return (byName().get(path23.basename(wanted)) ?? []).filter((f) => f.endsWith(`/${wanted}`) || path23.basename(f) === wanted).slice(0, 20);
4293
4482
  };
4294
4483
  const check = (file2, ref) => {
4295
4484
  const lines = load(file2);
@@ -4304,7 +4493,7 @@ function verifyRefs(refs, dirs) {
4304
4493
  if (!files.length) {
4305
4494
  if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
4306
4495
  total++;
4307
- invalid.push({ ref: ref.text, reason: path21.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
4496
+ invalid.push({ ref: ref.text, reason: path23.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
4308
4497
  continue;
4309
4498
  }
4310
4499
  total++;
@@ -4325,7 +4514,7 @@ function enclosedBy(lines, ref) {
4325
4514
  return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
4326
4515
  }
4327
4516
  var isFileName = (name) => {
4328
- const base2 = path21.basename(name);
4517
+ const base2 = path23.basename(name);
4329
4518
  return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
4330
4519
  };
4331
4520
  function indexFiles(roots) {
@@ -4334,19 +4523,19 @@ function indexFiles(roots) {
4334
4523
  const walk = (dir) => {
4335
4524
  let entries;
4336
4525
  try {
4337
- entries = fs25.readdirSync(dir, { withFileTypes: true });
4526
+ entries = fs27.readdirSync(dir, { withFileTypes: true });
4338
4527
  } catch {
4339
4528
  return;
4340
4529
  }
4341
4530
  for (const e of entries) {
4342
4531
  if (count >= MAX_INDEXED) return;
4343
4532
  if (e.isDirectory()) {
4344
- if (!SKIP_DIRS.has(e.name)) walk(path21.join(dir, e.name));
4533
+ if (!SKIP_DIRS2.has(e.name)) walk(path23.join(dir, e.name));
4345
4534
  } else if (e.isFile()) {
4346
4535
  count++;
4347
4536
  const list2 = index.get(e.name);
4348
- if (list2) list2.push(path21.join(dir, e.name));
4349
- else index.set(e.name, [path21.join(dir, e.name)]);
4537
+ if (list2) list2.push(path23.join(dir, e.name));
4538
+ else index.set(e.name, [path23.join(dir, e.name)]);
4350
4539
  }
4351
4540
  }
4352
4541
  };
@@ -4354,10 +4543,10 @@ function indexFiles(roots) {
4354
4543
  return index;
4355
4544
  }
4356
4545
  function resolve(ref, roots) {
4357
- const candidates = path21.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path21.join(r, ref));
4546
+ const candidates = path23.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path23.join(r, ref));
4358
4547
  return candidates.find((c) => {
4359
4548
  try {
4360
- return fs25.statSync(c).isFile() && inside(c, roots);
4549
+ return fs27.statSync(c).isFile() && inside(c, roots);
4361
4550
  } catch {
4362
4551
  return false;
4363
4552
  }
@@ -4365,65 +4554,14 @@ function resolve(ref, roots) {
4365
4554
  }
4366
4555
  function inside(file2, roots) {
4367
4556
  return roots.some((r) => {
4368
- const rel = path21.relative(r, path21.resolve(file2));
4369
- return rel !== "" && !rel.startsWith("..") && !path21.isAbsolute(rel);
4557
+ const rel = path23.relative(r, path23.resolve(file2));
4558
+ return rel !== "" && !rel.startsWith("..") && !path23.isAbsolute(rel);
4370
4559
  });
4371
4560
  }
4372
4561
  var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4373
4562
 
4374
- // src/core/secrets.ts
4375
- import { execFileSync } from "node:child_process";
4376
- import fs26 from "node:fs";
4377
- import path22 from "node:path";
4378
- var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
4379
- var MAX_VISITED = 2e4;
4380
- var MAX_DEPTH = 4;
4381
- var TEMPLATE2 = /\.(example|sample|template|dist|defaults?|tpl)$/i;
4382
- function looksSecret(name) {
4383
- if (name === ".env" || name.startsWith(".env.") && !TEMPLATE2.test(name)) return true;
4384
- return /\.(pem|p12|pfx)$/i.test(name) || /^id_(rsa|dsa|ecdsa|ed25519)$/.test(name) || name === ".netrc" || name === "credentials.json";
4385
- }
4386
- function findSecretFiles(dir) {
4387
- const found = [];
4388
- let visited = 0;
4389
- const walk = (d, depth) => {
4390
- if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
4391
- let entries;
4392
- try {
4393
- entries = fs26.readdirSync(d, { withFileTypes: true });
4394
- } catch {
4395
- return;
4396
- }
4397
- for (const e of entries) {
4398
- visited++;
4399
- if (e.isDirectory()) {
4400
- if (!SKIP_DIRS2.has(e.name)) walk(path22.join(d, e.name), depth + 1);
4401
- } else if (e.isFile() && looksSecret(e.name)) {
4402
- found.push(path22.relative(dir, path22.join(d, e.name)));
4403
- }
4404
- }
4405
- };
4406
- walk(dir, 0);
4407
- return found.sort();
4408
- }
4409
- function findSecretFilesInTree(root, dir) {
4410
- try {
4411
- const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
4412
- return out.split("\0").filter((f) => f && looksSecret(path22.basename(f)) && !path22.relative(dir, path22.join(root, f)).startsWith("..")).map((f) => path22.relative(dir, path22.join(root, f))).sort();
4413
- } catch {
4414
- return [];
4415
- }
4416
- }
4417
- function secretWarning(files, mode) {
4418
- if (!files.length) return void 0;
4419
- const shown = files.slice(0, 3).join(", ") + (files.length > 3 ? `, +${files.length - 3} more` : "");
4420
- const where = mode === "isolate" ? "would be copied into the isolated copy" : "sit in the directory the worker runs in";
4421
- 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";
4422
- 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`;
4423
- }
4424
-
4425
4563
  // src/core/run.ts
4426
- var VERSION2 = true ? "0.12.0" : "0.0.0-dev";
4564
+ var VERSION2 = true ? "0.13.0" : "0.0.0-dev";
4427
4565
  function planWork(o) {
4428
4566
  if (!o.plan) return { task: o.task.trim(), tier: o.tier };
4429
4567
  if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
@@ -4455,7 +4593,7 @@ function prepareRun(o) {
4455
4593
  parent = readMeta(o.continueFrom);
4456
4594
  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");
4457
4595
  if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
4458
- if (parent.mode === "isolate" && (!parent.worktree || !fs27.existsSync(parent.worktree))) {
4596
+ if (parent.mode === "isolate" && (!parent.worktree || !fs28.existsSync(parent.worktree))) {
4459
4597
  throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
4460
4598
  }
4461
4599
  const ran = parent.ran ?? parent.worker;
@@ -4474,16 +4612,21 @@ function prepareRun(o) {
4474
4612
  throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
4475
4613
  }
4476
4614
  const mode = parent?.mode ?? o.mode;
4477
- const dir = path23.resolve(parent?.dir ?? o.dir);
4478
- if (!fs27.existsSync(dir) || !fs27.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
4615
+ const dir = path24.resolve(parent?.dir ?? o.dir);
4616
+ if (!fs28.existsSync(dir) || !fs28.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
4479
4617
  const root = repoRoot(dir);
4480
4618
  if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
4481
4619
  if (mode === "write" && !root && !o.allowNonGit) {
4482
4620
  throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
4483
4621
  }
4484
- const files = o.files.map((f) => path23.resolve(f));
4485
- for (const f of files) if (!fs27.existsSync(f)) throw new UserError(`file not found: ${f}`);
4486
- if (!parent && !process.env.PITROOM_NO_SECRET_WARNING) {
4622
+ const files = o.files.map((f) => path24.resolve(f));
4623
+ for (const f of files) if (!fs28.existsSync(f)) throw new UserError(`file not found: ${f}`);
4624
+ const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
4625
+ const snap = mode === "read" && !o.review && !o.inPlace && !parent ? wantSnapshot(readIn2, root, dir) : void 0;
4626
+ if (parent?.snapshot?.dir && !fs28.existsSync(parent.snapshot.dir)) {
4627
+ throw new UserError(`the read snapshot of run ${parent.id} is gone (cleaned up); ask the question again as a new run`, 3);
4628
+ }
4629
+ if (!parent && !snap && !process.env.PITROOM_NO_SECRET_WARNING) {
4487
4630
  const secrets = mode === "isolate" && root ? findSecretFilesInTree(root, dir) : findSecretFiles(dir);
4488
4631
  const note = secretWarning(secrets, mode);
4489
4632
  if (note) warnings.push(note);
@@ -4515,6 +4658,8 @@ function prepareRun(o) {
4515
4658
  reviewOf: o.review?.of,
4516
4659
  auditOf: o.audit?.of,
4517
4660
  auditRate: o.auditRate,
4661
+ inPlace: o.inPlace || void 0,
4662
+ snapshot: parent?.snapshot ?? (snap ? { dir: "", tree: "", left: snap.secrets } : void 0),
4518
4663
  plan: work.plan ?? o.review?.plan ?? parent?.plan,
4519
4664
  reviewKind: o.review?.kind,
4520
4665
  packageFile: o.review?.packageFile
@@ -4524,13 +4669,13 @@ function prepareRun(o) {
4524
4669
  try {
4525
4670
  acquireWriteLock(root, meta.id);
4526
4671
  } catch (e) {
4527
- fs27.rmSync(runDir(meta.id), { recursive: true, force: true });
4672
+ fs28.rmSync(runDir(meta.id), { recursive: true, force: true });
4528
4673
  throw e;
4529
4674
  }
4530
4675
  }
4531
- fs27.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
4676
+ fs28.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
4532
4677
  `);
4533
- if (work.brief) fs27.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
4678
+ if (work.brief) fs28.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
4534
4679
  return meta;
4535
4680
  }
4536
4681
  var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -4610,7 +4755,7 @@ async function execute(meta) {
4610
4755
  (meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
4611
4756
  for (const f of ["events.jsonl", "stderr.log"]) {
4612
4757
  const from = runFile(meta.id, f);
4613
- if (fs27.existsSync(from)) fs27.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
4758
+ if (fs28.existsSync(from)) fs28.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
4614
4759
  }
4615
4760
  writeMeta(meta);
4616
4761
  }
@@ -4668,12 +4813,26 @@ function settleAudit(a) {
4668
4813
  }
4669
4814
  function prepareTree(meta) {
4670
4815
  const root = meta.repoRoot;
4816
+ if (root && meta.mode === "read" && meta.snapshot && !meta.snapshot.dir) {
4817
+ const shown = meta.snapshot.left.slice(0, 3).join(", ") + (meta.snapshot.left.length > 3 ? `, +${meta.snapshot.left.length - 3} more` : "");
4818
+ try {
4819
+ const s = readSnapshot(root);
4820
+ meta.snapshot.dir = s.dir;
4821
+ meta.snapshot.tree = s.tree;
4822
+ meta.cwd = path24.join(s.dir, path24.relative(root, meta.dir));
4823
+ if (!fs28.existsSync(meta.cwd)) meta.cwd = s.dir;
4824
+ 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`);
4825
+ } catch (e) {
4826
+ meta.snapshot = void 0;
4827
+ meta.warnings.push(`could not make a clean snapshot (${e.message.slice(0, 120)}): the worker reads the directory itself${shown ? `, where secret-looking files sit (${shown})` : ""}`);
4828
+ }
4829
+ }
4671
4830
  if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
4672
4831
  if (root && meta.mode === "isolate" && !meta.worktree) {
4673
4832
  meta.baseTree = snapshotTree(root);
4674
- meta.worktree = path23.join(worktreesDir(), meta.id);
4833
+ meta.worktree = path24.join(worktreesDir(), meta.id);
4675
4834
  createIsolatedCopy(root, meta.baseTree, meta.worktree);
4676
- meta.cwd = path23.join(meta.worktree, path23.relative(root, meta.dir));
4835
+ meta.cwd = path24.join(meta.worktree, path24.relative(root, meta.dir));
4677
4836
  const linked = linkIntoWorktree(root, meta.worktree, meta.link);
4678
4837
  if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
4679
4838
  }
@@ -4696,7 +4855,7 @@ function attempt(meta, backend, target) {
4696
4855
  timeoutSec: meta.timeoutSec
4697
4856
  });
4698
4857
  }
4699
- var read2 = (f) => fs27.existsSync(f) ? fs27.readFileSync(f, "utf8") : "";
4858
+ var read2 = (f) => fs28.existsSync(f) ? fs28.readFileSync(f, "utf8") : "";
4700
4859
  function retryableFailure(meta, backend, res) {
4701
4860
  const run = backend.parse(read2(runFile(meta.id, "events.jsonl")));
4702
4861
  if (run.usage.steps > 0) return void 0;
@@ -4713,14 +4872,15 @@ function finalize(meta, res) {
4713
4872
  const ran = meta.ran ?? meta.worker;
4714
4873
  const backend = getBackend(ran.backend);
4715
4874
  const run = backend.parse(read2(runFile(meta.id, "events.jsonl")));
4875
+ if (meta.snapshot?.dir && meta.repoRoot) run.finalText = backToProject(run.finalText, meta.snapshot.dir, meta.repoRoot);
4716
4876
  meta.sessionId = run.sessionId ?? meta.sessionId;
4717
4877
  meta.usage = run.usage;
4718
- fs27.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
4878
+ fs28.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
4719
4879
  `);
4720
4880
  if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run.finalText);
4721
4881
  if (meta.reviewOf) {
4722
4882
  meta.verdict = parseVerdict(run.finalText);
4723
- if (meta.packageFile) fs27.rmSync(meta.packageFile, { force: true });
4883
+ if (meta.packageFile) fs28.rmSync(meta.packageFile, { force: true });
4724
4884
  }
4725
4885
  if (!res.timedOut && !res.stopped && !meta.error) {
4726
4886
  const f = backend.failure(run, read2(runFile(meta.id, "stderr.log")), res.code);
@@ -4768,7 +4928,7 @@ function captureChanges(meta) {
4768
4928
  const d = diffTrees(target, meta.baseTree, meta.afterTree);
4769
4929
  meta.changes = d.changes;
4770
4930
  meta.stats = d.stats;
4771
- fs27.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
4931
+ fs28.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
4772
4932
  }
4773
4933
  function runVerify(meta) {
4774
4934
  const r = spawnSync6(meta.verify, {
@@ -4781,7 +4941,7 @@ function runVerify(meta) {
4781
4941
  env: { ...process.env, PWD: meta.cwd, PITROOM_ACTIVE: "1" }
4782
4942
  });
4783
4943
  const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
4784
- fs27.writeFileSync(runFile(meta.id, "verify.log"), output);
4944
+ fs28.writeFileSync(runFile(meta.id, "verify.log"), output);
4785
4945
  return { ok: r.status === 0, code: r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
4786
4946
  }
4787
4947
  function applyRun(meta, allowDelete = false) {
@@ -4826,7 +4986,7 @@ function discardRun(meta) {
4826
4986
  return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
4827
4987
  }
4828
4988
  function cleanupWorktree(meta) {
4829
- if (meta.worktree && fs27.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
4989
+ if (meta.worktree && fs28.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
4830
4990
  }
4831
4991
 
4832
4992
  // src/cli/commands.ts
@@ -4861,7 +5021,7 @@ async function cmdReview(p) {
4861
5021
  let meta;
4862
5022
  try {
4863
5023
  meta = prepareRun({
4864
- ...runOptions(p, fill(loadTemplate(TEMPLATE[job.kind]), { PACKAGE_FILE: packageFile })),
5024
+ ...runOptions(p, fill(loadTemplate(TEMPLATE2[job.kind]), { PACKAGE_FILE: packageFile })),
4865
5025
  mode: "read",
4866
5026
  dir: job.dir,
4867
5027
  worker: flag(p, "worker") ?? (flag(p, "tier") ? void 0 : pickReviewer(job)),
@@ -4876,10 +5036,10 @@ async function cmdReview(p) {
4876
5036
  writeMeta(meta);
4877
5037
  }
4878
5038
  } catch (e) {
4879
- fs28.rmSync(packageFile, { force: true });
5039
+ fs29.rmSync(packageFile, { force: true });
4880
5040
  throw e;
4881
5041
  }
4882
- fs28.writeFileSync(runFile(meta.id, "package.md"), job.package);
5042
+ fs29.writeFileSync(runFile(meta.id, "package.md"), job.package);
4883
5043
  return launch(p, meta);
4884
5044
  }
4885
5045
  async function cmdAudit(p) {
@@ -4947,7 +5107,7 @@ function cmdPlan(p) {
4947
5107
  }
4948
5108
  function cmdCrew(p) {
4949
5109
  const file2 = flag(p, "task-file");
4950
- const tasks = (file2 ? fs28.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
5110
+ const tasks = (file2 ? fs29.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
4951
5111
  if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
4952
5112
  if (has(p, "write") && tasks.length > 1) {
4953
5113
  throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
@@ -5148,7 +5308,7 @@ function sinceMs(s) {
5148
5308
  if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
5149
5309
  return Date.now() - Number(m[1]) * 864e5;
5150
5310
  }
5151
- var readStdin = () => process.stdin.isTTY ? "" : fs28.readFileSync(0, "utf8");
5311
+ var readStdin = () => process.stdin.isTTY ? "" : fs29.readFileSync(0, "utf8");
5152
5312
  function cmdStatusline(p) {
5153
5313
  const input = readStdin();
5154
5314
  const lines = [];
@@ -5188,8 +5348,8 @@ function cmdModels(p) {
5188
5348
  return 0;
5189
5349
  }
5190
5350
  function cmdHookStart() {
5191
- const script = path24.join(path24.dirname(path24.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
5192
- if (fs28.existsSync(script)) spawnSync7(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
5351
+ const script = path25.join(path25.dirname(path25.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
5352
+ if (fs29.existsSync(script)) spawnSync7(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
5193
5353
  return 0;
5194
5354
  }
5195
5355
  function cmdInit(p) {
@@ -5229,8 +5389,8 @@ function cmdSavings(p) {
5229
5389
  est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
5230
5390
  const out = flag(p, "card");
5231
5391
  if (out) {
5232
- fs28.writeFileSync(out, card(t, period));
5233
- console.log(`card written to ${path24.resolve(out)}`);
5392
+ fs29.writeFileSync(out, card(t, period));
5393
+ console.log(`card written to ${path25.resolve(out)}`);
5234
5394
  }
5235
5395
  if (has(p, "badge")) console.log(`![pitroom](${badgeUrl(t)})`);
5236
5396
  return 0;
@@ -5250,7 +5410,7 @@ function cmdConfig(p) {
5250
5410
  console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
5251
5411
  return 0;
5252
5412
  }
5253
- console.log(`config file: ${configPath()}${fs28.existsSync(configPath()) ? "" : " (not present)"}`);
5413
+ console.log(`config file: ${configPath()}${fs29.existsSync(configPath()) ? "" : " (not present)"}`);
5254
5414
  for (const [key, s] of Object.entries(eff)) {
5255
5415
  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);
5256
5416
  console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
@@ -5274,9 +5434,10 @@ function cmdClean(p) {
5274
5434
  const m = readMeta(id);
5275
5435
  if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
5276
5436
  recordRun(m);
5277
- fs28.rmSync(runDir(id), { recursive: true, force: true });
5437
+ fs29.rmSync(runDir(id), { recursive: true, force: true });
5278
5438
  }
5279
- console.log(`removed ${old.length} run(s); the history and the savings ledger are kept`);
5439
+ const snapshots = prune(Date.now(), 0);
5440
+ console.log(`removed ${old.length} run(s)${snapshots ? ` and ${snapshots} read snapshot${snapshots === 1 ? "" : "s"}` : ""}; the history and the savings ledger are kept`);
5280
5441
  return 0;
5281
5442
  }
5282
5443
  var cmdRevert = (p) => (console.log(revertRun(freshMeta(resolveRun(p.positional[0])))), 0);
@@ -5284,9 +5445,9 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
5284
5445
 
5285
5446
  // src/core/doctor.ts
5286
5447
  import { spawnSync as spawnSync8 } from "node:child_process";
5287
- import fs29 from "node:fs";
5448
+ import fs30 from "node:fs";
5288
5449
  import os10 from "node:os";
5289
- import path25 from "node:path";
5450
+ import path26 from "node:path";
5290
5451
  var MARK = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
5291
5452
  var NEXT = [
5292
5453
  { when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
@@ -5296,10 +5457,10 @@ var NEXT = [
5296
5457
  { when: /Gemini CLI is not signed in|IneligibleTierError/, command: "export GEMINI_API_KEY=\u2026", why: "a Google AI Studio key for the Gemini worker" }
5297
5458
  ];
5298
5459
  function firstNodeOnPath() {
5299
- for (const dir of (process.env.PATH ?? "").split(path25.delimiter).filter(Boolean)) {
5300
- const file2 = path25.join(dir, process.platform === "win32" ? "node.exe" : "node");
5460
+ for (const dir of (process.env.PATH ?? "").split(path26.delimiter).filter(Boolean)) {
5461
+ const file2 = path26.join(dir, process.platform === "win32" ? "node.exe" : "node");
5301
5462
  try {
5302
- if (!fs29.statSync(file2).isFile()) continue;
5463
+ if (!fs30.statSync(file2).isFile()) continue;
5303
5464
  } catch {
5304
5465
  continue;
5305
5466
  }
@@ -5326,12 +5487,12 @@ function doctor5(probe) {
5326
5487
  const addAll = (list2) => list2.forEach((c) => add(c.level, c.message));
5327
5488
  add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
5328
5489
  const onPath = firstNodeOnPath();
5329
- if (onPath && onPath.version && tooOld(onPath.version) && path25.resolve(onPath.path) !== path25.resolve(process.execPath)) {
5490
+ if (onPath && onPath.version && tooOld(onPath.version) && path26.resolve(onPath.path) !== path26.resolve(process.execPath)) {
5330
5491
  add("warn", `the first \`node\` on PATH is v${onPath.version} (${onPath.path}), older than the 22.13 Pitroom needs: shells that agent apps start may use it (the pitroom command finds a newer Node itself; other tools may not)`);
5331
5492
  }
5332
5493
  add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
5333
5494
  const cfg = loadConfig();
5334
- add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs29.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
5495
+ add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs30.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
5335
5496
  for (const w of cfg.warnings) add("warn", w);
5336
5497
  if (process.platform !== "win32") {
5337
5498
  const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
@@ -5479,19 +5640,19 @@ function skillChecks() {
5479
5640
  else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
5480
5641
  }
5481
5642
  if (geminiExtensionInstalled()) {
5482
- const linked = installedSkills().some((i) => i.base.includes(`${path25.sep}.agents${path25.sep}`) && i.names.length);
5643
+ const linked = installedSkills().some((i) => i.base.includes(`${path26.sep}.agents${path26.sep}`) && i.names.length);
5483
5644
  checks.push(
5484
5645
  linked ? { level: "warn", message: "Pitroom is installed as a Gemini CLI extension and linked into ~/.agents/skills: Gemini loads the skills twice; run `pitroom uninstall` or `gemini extensions uninstall pitroom`" } : { level: "ok", message: "Gemini CLI extension installed (skills + context)" }
5485
5646
  );
5486
5647
  }
5487
5648
  if (viaPlugin) {
5488
- const linked = installedSkills().some((i) => i.base.includes(`${path25.sep}.claude${path25.sep}`) && i.names.length);
5649
+ const linked = installedSkills().some((i) => i.base.includes(`${path26.sep}.claude${path26.sep}`) && i.names.length);
5489
5650
  checks.push(
5490
5651
  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)" }
5491
5652
  );
5492
5653
  }
5493
5654
  const launcher = launcherPath();
5494
- if (fs29.existsSync(launcher)) {
5655
+ if (fs30.existsSync(launcher)) {
5495
5656
  const r = spawnSync8(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
5496
5657
  checks.push(
5497
5658
  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)}` }
@@ -5503,12 +5664,12 @@ function skillChecks() {
5503
5664
  }
5504
5665
  function geminiExtensionInstalled() {
5505
5666
  const home2 = process.env.GEMINI_CLI_HOME ?? os10.homedir();
5506
- return fs29.existsSync(path25.join(home2, ".gemini", "extensions", "pitroom"));
5667
+ return fs30.existsSync(path26.join(home2, ".gemini", "extensions", "pitroom"));
5507
5668
  }
5508
5669
  function pluginInstalled() {
5509
5670
  try {
5510
- const f = path25.join(os10.homedir(), ".claude", "plugins", "installed_plugins.json");
5511
- const plugins = JSON.parse(fs29.readFileSync(f, "utf8")).plugins ?? {};
5671
+ const f = path26.join(os10.homedir(), ".claude", "plugins", "installed_plugins.json");
5672
+ const plugins = JSON.parse(fs30.readFileSync(f, "utf8")).plugins ?? {};
5512
5673
  return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
5513
5674
  } catch {
5514
5675
  return false;
@@ -5525,24 +5686,24 @@ function superpowersActive() {
5525
5686
  for (const p of openCodePlugins()) {
5526
5687
  if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
5527
5688
  }
5528
- for (const base2 of [path25.join(os10.homedir(), ".agents", "skills"), path25.join(os10.homedir(), ".claude", "skills")]) {
5529
- const dir = path25.join(base2, "using-superpowers");
5530
- if (fs29.existsSync(path25.join(dir, "SKILL.md"))) found.push(dir);
5689
+ for (const base2 of [path26.join(os10.homedir(), ".agents", "skills"), path26.join(os10.homedir(), ".claude", "skills")]) {
5690
+ const dir = path26.join(base2, "using-superpowers");
5691
+ if (fs30.existsSync(path26.join(dir, "SKILL.md"))) found.push(dir);
5531
5692
  }
5532
5693
  return found;
5533
5694
  }
5534
5695
  function claudePlugins() {
5535
5696
  try {
5536
- const f = path25.join(os10.homedir(), ".claude", "plugins", "installed_plugins.json");
5537
- return Object.keys(JSON.parse(fs29.readFileSync(f, "utf8")).plugins ?? {});
5697
+ const f = path26.join(os10.homedir(), ".claude", "plugins", "installed_plugins.json");
5698
+ return Object.keys(JSON.parse(fs30.readFileSync(f, "utf8")).plugins ?? {});
5538
5699
  } catch {
5539
5700
  return [];
5540
5701
  }
5541
5702
  }
5542
5703
  function claudePluginEnabled(key) {
5543
5704
  try {
5544
- const f = path25.join(os10.homedir(), ".claude", "settings.json");
5545
- return JSON.parse(fs29.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
5705
+ const f = path26.join(os10.homedir(), ".claude", "settings.json");
5706
+ return JSON.parse(fs30.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
5546
5707
  } catch {
5547
5708
  return true;
5548
5709
  }
@@ -5551,7 +5712,7 @@ function codexPlugins() {
5551
5712
  const plugins = /* @__PURE__ */ new Map();
5552
5713
  let text;
5553
5714
  try {
5554
- text = fs29.readFileSync(path25.join(process.env.CODEX_HOME ?? path25.join(os10.homedir(), ".codex"), "config.toml"), "utf8");
5715
+ text = fs30.readFileSync(path26.join(process.env.CODEX_HOME ?? path26.join(os10.homedir(), ".codex"), "config.toml"), "utf8");
5555
5716
  } catch {
5556
5717
  return plugins;
5557
5718
  }
@@ -5569,11 +5730,11 @@ function codexPlugins() {
5569
5730
  return plugins;
5570
5731
  }
5571
5732
  function openCodePlugins() {
5572
- const dir = path25.join(process.env.XDG_CONFIG_HOME ?? path25.join(os10.homedir(), ".config"), "opencode");
5733
+ const dir = path26.join(process.env.XDG_CONFIG_HOME ?? path26.join(os10.homedir(), ".config"), "opencode");
5573
5734
  const plugins = [];
5574
5735
  for (const name of ["opencode.json", "opencode.jsonc"]) {
5575
5736
  try {
5576
- const text = fs29.readFileSync(path25.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
5737
+ const text = fs30.readFileSync(path26.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
5577
5738
  const list2 = JSON.parse(text).plugin;
5578
5739
  if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
5579
5740
  } catch {
@@ -5581,7 +5742,7 @@ function openCodePlugins() {
5581
5742
  }
5582
5743
  for (const folder of ["plugin", "plugins"]) {
5583
5744
  try {
5584
- for (const f of fs29.readdirSync(path25.join(dir, folder))) plugins.push(path25.join(dir, folder, f));
5745
+ for (const f of fs30.readdirSync(path26.join(dir, folder))) plugins.push(path26.join(dir, folder, f));
5585
5746
  } catch {
5586
5747
  }
5587
5748
  }
@@ -5652,6 +5813,8 @@ Run options
5652
5813
  -W, --worker T worker target "backend[:model]" (default: config "worker", else opencode)
5653
5814
  -m, --model M model for that worker (default: the worker CLI's own default)
5654
5815
  --tier NAME a worker from the config's "tiers" (e.g. cheap, standard, capable); -W wins
5816
+ --in-place a read run reads the directory itself, not a clean snapshot (default: a snapshot without secret-looking
5817
+ files when the directory has any; config "readIn": auto | snapshot | project)
5655
5818
  --audit have another worker re-check this read run's answer; --no-audit skips it (default: the config's "audit")
5656
5819
  --effort LEVEL reasoning effort for the worker: low, medium, high, xhigh, \u2026 (model#level)
5657
5820
  --plan PLAN with --step N: implement Task N of a plan (-i or -w); the task text is your notes