pitroom 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pitroom.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import fs43 from "node:fs";
4
+ import fs44 from "node:fs";
5
5
 
6
6
  // src/core/errors.ts
7
7
  var UserError = class extends Error {
@@ -558,7 +558,7 @@ function denyCommandRules() {
558
558
  "command(eval)"
559
559
  ];
560
560
  }
561
- function workerSettings(workspace, workerHome3, reads = []) {
561
+ function workerSettings(workspace, workerHome3, reads = [], writes = false) {
562
562
  const real = (p) => {
563
563
  try {
564
564
  return fs6.realpathSync.native(p);
@@ -582,6 +582,8 @@ function workerSettings(workspace, workerHome3, reads = []) {
582
582
  allow: [
583
583
  "command(*)",
584
584
  ...both(path5.join(dataDir(workerHome3), "brain")).map((p) => `write_file(${rulePath(p)})`),
585
+ // agy 1.4 asks for edits in the workspace too, --mode accept-edits or not, and a headless run cannot ask
586
+ ...writes ? both(workspace).map((p) => `write_file(${rulePath(p)})`) : [],
585
587
  ...toolchains().flatMap(both).map((p) => `read_file(${rulePath(p)})`),
586
588
  // what the task names outside the workspace: a headless agy cannot ask, and would end the turn unanswered
587
589
  ...reads.map((r) => r.path).flatMap(both).map((p) => `read_file(${rulePath(p)})`)
@@ -707,12 +709,12 @@ function baseSettings() {
707
709
  }
708
710
  var rulePath = (p) => p.replace(/^[A-Za-z]:(?=[\\/])/, "").replace(/\\/g, "/");
709
711
  var HOMES_KEPT_DAYS = 14;
710
- function workerHome(workspace, reads = []) {
712
+ function workerHome(workspace, reads = [], writes = false) {
711
713
  const root = path5.join(home(), "antigravity-home");
712
714
  const id2 = crypto3.createHash("sha256").update(path5.resolve(workspace)).digest("hex").slice(0, 16);
713
715
  const dir = path5.join(root, id2);
714
716
  fs6.mkdirSync(dataDir(dir), { recursive: true });
715
- writeIfChanged(path5.join(dataDir(dir), "settings.json"), `${JSON.stringify(workerSettings(workspace, dir, reads), null, 2)}
717
+ writeIfChanged(path5.join(dataDir(dir), "settings.json"), `${JSON.stringify(workerSettings(workspace, dir, reads, writes), null, 2)}
716
718
  `);
717
719
  if (process.platform === "darwin") {
718
720
  fs6.mkdirSync(path5.join(dir, "Library"), { recursive: true });
@@ -769,13 +771,13 @@ function invocation(req) {
769
771
  if (model) args.push("--model", model);
770
772
  if (effort) args.push("--effort", effort);
771
773
  const { command, prefix } = resolveCommand(binary());
772
- const workerDir = workerHome(req.cwd, outsideReads(prompt, req.cwd, req.project?.root));
774
+ const workerDir = workerHome(req.cwd, outsideReads(prompt, req.cwd, req.project?.root), req.mode !== "read");
773
775
  const env = { HOME: workerDir };
774
776
  if (process.platform === "win32") env.USERPROFILE = workerDir;
775
777
  return { command, args: [...prefix, ...args], env };
776
778
  }
777
779
  function classify(message) {
778
- if (/RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|capacity/i.test(message)) return "rate-limited";
780
+ if (/usage limit|session limit|RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|capacity/i.test(message)) return "rate-limited";
779
781
  if (/not (signed|logged) in|\bsign.?in\b|\blog ?in\b|authenticat|PERMISSION_DENIED|oauth|credential|keyring|keychain|\b401\b|unauthori[sz]ed/i.test(message)) {
780
782
  return "auth";
781
783
  }
@@ -826,9 +828,10 @@ function doctor({ models, hasFallback }) {
826
828
  }
827
829
  const checks = [
828
830
  { level: "ok", message: `Antigravity CLI ${version.out.trim().split("\n")[0]} at ${binary()}` },
829
- {
831
+ // checked on macOS (agy 1.4.7, scripts/check-worker.mjs); Linux and Windows not run yet
832
+ process.platform === "darwin" ? { level: "ok", message: "agy has no read-only mode for headless runs, so its read runs always read a snapshot of the project; prefer -i for changes" } : {
830
833
  level: "warn",
831
- message: "the Antigravity worker is in beta (checked against agy 1.3.5). agy has no read-only mode for headless runs, so its read runs always read a snapshot of the project; prefer -i for changes"
834
+ message: `the Antigravity worker is in beta on ${process.platform === "win32" ? "Windows" : "Linux"} (checked on macOS only; node scripts/check-worker.mjs antigravity tries it here). agy has no read-only mode for headless runs, so its read runs always read a snapshot of the project; prefer -i for changes`
832
835
  }
833
836
  ];
834
837
  if (process.platform === "win32") {
@@ -1013,7 +1016,7 @@ function classify2(message) {
1013
1016
  if (/authentication_failed|failed to authenticate|oauth|not logged in|\/login|invalid api key|\b40[13]\b|unauthori[sz]ed/i.test(message)) {
1014
1017
  return "auth";
1015
1018
  }
1016
- if (/rate.?limit|too many requests|\b429\b|\b529\b|overloaded|usage limit|limit reached|quota|capacity/i.test(message)) return "rate-limited";
1019
+ if (/usage limit|session limit|limit reached|rate.?limit|too many requests|\b429\b|\b529\b|overloaded|quota|capacity/i.test(message)) return "rate-limited";
1017
1020
  if (/model[^.]*(not found|does not exist|invalid|not available)|invalid model|not_found_error/i.test(message)) return "model-unavailable";
1018
1021
  return "other";
1019
1022
  }
@@ -1214,7 +1217,7 @@ function classify3(message) {
1214
1217
  if (/not supported|model[^.]*(not found|does not exist|unknown|unavailable)|unsupported model|no such model/i.test(message)) {
1215
1218
  return "model-unavailable";
1216
1219
  }
1217
- if (/rate.?limit|too many requests|\b429\b|usage limit|quota|overloaded|\b50[23]\b|capacity/i.test(message)) return "rate-limited";
1220
+ if (/usage limit|session limit|rate.?limit|too many requests|\b429\b|quota|overloaded|\b50[23]\b|capacity/i.test(message)) return "rate-limited";
1218
1221
  if (/unauthori[sz]ed|\b40[13]\b|not logged in|log ?in|api.?key|credits?\b|billing|subscription/i.test(message)) return "auth";
1219
1222
  return "other";
1220
1223
  }
@@ -1452,7 +1455,7 @@ function classify4(message) {
1452
1455
  if (/IneligibleTier|no longer supported for Gemini|error authenticating|not logged in|please (log ?in|sign in)|credentials|api key|UNAUTHENTICATED|PERMISSION_DENIED|\b40[13]\b|unauthori[sz]ed/i.test(message)) {
1453
1456
  return "auth";
1454
1457
  }
1455
- if (/RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|exhausted your capacity|capacity/i.test(message)) return "rate-limited";
1458
+ if (/usage limit|session limit|RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|exhausted your capacity|capacity/i.test(message)) return "rate-limited";
1456
1459
  if (/model.{0,80}(not found|not available|no longer available|does not exist|unsupported|invalid)|\b404\b|NOT_FOUND|invalid model|is not supported/i.test(message)) return "model-unavailable";
1457
1460
  return "other";
1458
1461
  }
@@ -1896,7 +1899,7 @@ function majorVersion(output) {
1896
1899
  }
1897
1900
  function classify5(message) {
1898
1901
  if (/model not found|ModelNotFound|unknown model|no such model|deprecated/i.test(message)) return "model-unavailable";
1899
- if (/rate.?limit|too many requests|\b429\b|quota|overloaded|\b50[23]\b|unavailable|capacity/i.test(message)) return "rate-limited";
1902
+ if (/usage limit|session limit|rate.?limit|too many requests|\b429\b|quota|overloaded|\b50[23]\b|unavailable|capacity/i.test(message)) return "rate-limited";
1900
1903
  if (/provider\.auth|FreeTierError|free tier|unauthori[sz]ed|\b40[13]\b|api.?key|insufficient|credits?\b|billing|payment/i.test(message)) {
1901
1904
  return "auth";
1902
1905
  }
@@ -2241,7 +2244,7 @@ function invocation7(req) {
2241
2244
  }
2242
2245
  function classify6(message) {
2243
2246
  if (/\b40[13]\b|incorrect api key|invalid api key|unauthori[sz]ed|authenticat|not logged in|no auth/i.test(message)) return "auth";
2244
- if (/\b429\b|rate.?limit|too many requests|quota|capacity|overloaded|\b529\b/i.test(message)) return "rate-limited";
2247
+ if (/usage limit|session limit|\b429\b|rate.?limit|too many requests|quota|capacity|overloaded|\b529\b/i.test(message)) return "rate-limited";
2245
2248
  if (/model[^.]*(not found|does not exist|not available|not supported)|invalid model|model_not_found|\b404\b/i.test(message)) return "model-unavailable";
2246
2249
  return "other";
2247
2250
  }
@@ -2343,7 +2346,8 @@ var SCHEMA = {
2343
2346
  priceFeedUrl: "string",
2344
2347
  priceFeedHours: "number",
2345
2348
  budgetDaily: "number",
2346
- rankWorkers: "boolean"
2349
+ rankWorkers: "boolean",
2350
+ autoFallback: "boolean"
2347
2351
  };
2348
2352
  function configPath() {
2349
2353
  if (process.env.PITROOM_CONFIG) return path12.resolve(process.env.PITROOM_CONFIG);
@@ -2453,6 +2457,7 @@ function effective(flags = {}) {
2453
2457
  priceFeedUrl: setting(void 0, e.PITROOM_PRICE_FEED_URL?.trim() || void 0, c.priceFeedUrl?.trim() || void 0, "https://models.dev/api.json"),
2454
2458
  priceFeedHours: setting(void 0, positiveInt(e.PITROOM_PRICE_FEED_HOURS), positiveInt(c.priceFeedHours), 24),
2455
2459
  rankWorkers: setting(void 0, flag01(e.PITROOM_RANK_WORKERS), c.rankWorkers, false),
2460
+ autoFallback: setting(void 0, flag01(e.PITROOM_AUTO_FALLBACK), c.autoFallback, false),
2456
2461
  budgetDaily: setting(void 0, amount(e.PITROOM_BUDGET_DAILY), amount(c.budgetDaily), void 0)
2457
2462
  };
2458
2463
  }
@@ -2639,9 +2644,9 @@ function effortFlag(p) {
2639
2644
  import readline from "node:readline";
2640
2645
 
2641
2646
  // src/core/run.ts
2642
- import { spawn as spawn4, spawnSync as spawnSync9 } from "node:child_process";
2643
- import fs31 from "node:fs";
2644
- import path27 from "node:path";
2647
+ import { spawn as spawn4, spawnSync as spawnSync10 } from "node:child_process";
2648
+ import fs32 from "node:fs";
2649
+ import path28 from "node:path";
2645
2650
 
2646
2651
  // src/vcs/git.ts
2647
2652
  import { spawnSync as spawnSync8 } from "node:child_process";
@@ -2772,7 +2777,29 @@ function applyPatch(root, patchFile, reverse) {
2772
2777
  function reviewDiff(root, a, b) {
2773
2778
  return `${must(root, [...DIFF, "--stat", a, b]).trim()}
2774
2779
 
2775
- ${must(root, [...DIFF, "-U10", a, b])}`;
2780
+ ${numberDiff(must(root, [...DIFF, "-U10", a, b]))}`;
2781
+ }
2782
+ var NUMBERED = "Each line of a hunk starts with its line number in the file after the change (a removed line has none): cite those.";
2783
+ function numberDiff(diff) {
2784
+ let next = 0;
2785
+ let width = 0;
2786
+ let inHunk = false;
2787
+ return diff.split("\n").map((l) => {
2788
+ if (l.startsWith("diff --git")) {
2789
+ inHunk = false;
2790
+ return l;
2791
+ }
2792
+ const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(l);
2793
+ if (hunk) {
2794
+ inHunk = true;
2795
+ next = Number(hunk[1]);
2796
+ width = String(next + Number(hunk[2] ?? 1)).length;
2797
+ return l;
2798
+ }
2799
+ if (!inHunk || !l || !"+- ".includes(l[0])) return l;
2800
+ if (l[0] === "-") return `-${" ".repeat(width)}| ${l.slice(1)}`;
2801
+ return `${l[0]}${String(next++).padStart(width)}| ${l.slice(1)}`;
2802
+ }).join("\n");
2776
2803
  }
2777
2804
  function rangeDiff(root, a, b) {
2778
2805
  const range = `${a}..${b}`;
@@ -2787,7 +2814,9 @@ ${log2 || "(none)"}`,
2787
2814
  ${must(root, [...DIFF, "--stat", since2]).trim() || "(none)"}`,
2788
2815
  `## DIFF
2789
2816
 
2790
- ${must(root, [...DIFF, "-U10", since2])}`
2817
+ ${NUMBERED}
2818
+
2819
+ ${numberDiff(must(root, [...DIFF, "-U10", since2]))}`
2791
2820
  ].join("\n\n");
2792
2821
  }
2793
2822
  function commitOf(root, ref) {
@@ -2799,6 +2828,10 @@ function gitDir(dir) {
2799
2828
  return r.code === 0 ? r.stdout.trim() : void 0;
2800
2829
  }
2801
2830
 
2831
+ // src/core/chain.ts
2832
+ import fs15 from "node:fs";
2833
+ import path14 from "node:path";
2834
+
2802
2835
  // src/core/target.ts
2803
2836
  function parseTarget(spec, defaultBackend) {
2804
2837
  const s = spec.trim();
@@ -2857,17 +2890,52 @@ function resolveChain(flags = {}) {
2857
2890
  warnings2.push(`fallback ${s} skipped: ${e.message}`);
2858
2891
  }
2859
2892
  }
2893
+ if (!fallback.length && eff.autoFallback.value) fallback.push(...autoFallback(worker, withDefault));
2860
2894
  }
2861
2895
  return { worker, fallback, warnings: warnings2 };
2862
2896
  }
2897
+ var AUTO_MAX = 4;
2898
+ var bare = (t) => `${t.backend}:${(t.model ?? "").split("#")[0]}`;
2899
+ function installed(backend) {
2900
+ try {
2901
+ const bin = getBackend(backend).binary();
2902
+ return path14.isAbsolute(bin) && fs15.statSync(bin).isFile();
2903
+ } catch {
2904
+ return false;
2905
+ }
2906
+ }
2907
+ function autoFallback(worker, withDefault = (t) => t) {
2908
+ const eff = effective();
2909
+ const named = [
2910
+ ...Object.values(eff.tiers.value),
2911
+ ...Object.entries(eff.models.value).map(([backend, model]) => `${backend}:${model}`),
2912
+ eff.worker.value
2913
+ ];
2914
+ const costs = eff.costs.value;
2915
+ const out = [];
2916
+ for (const spec of named) {
2917
+ let t;
2918
+ try {
2919
+ t = withDefault(parseTarget(spec, worker.backend));
2920
+ getBackend(t.backend);
2921
+ } catch {
2922
+ continue;
2923
+ }
2924
+ if (bare(t) === bare(worker) || out.some((o) => bare(o) === bare(t)) || !installed(t.backend)) continue;
2925
+ out.push(t);
2926
+ }
2927
+ const cost = (t) => costs[bare(t)] ?? Infinity;
2928
+ return out.map((t, i) => ({ t, i })).sort((a, b) => cost(a.t) - cost(b.t) || a.i - b.i).slice(0, AUTO_MAX).map((x) => x.t);
2929
+ }
2863
2930
 
2864
2931
  // src/core/process.ts
2865
2932
  import { spawn } from "node:child_process";
2866
- import fs16 from "node:fs";
2933
+ import fs17 from "node:fs";
2867
2934
 
2868
2935
  // src/vcs/guard.ts
2869
- import fs15 from "node:fs";
2870
- import path14 from "node:path";
2936
+ import { spawnSync as spawnSync9 } from "node:child_process";
2937
+ import fs16 from "node:fs";
2938
+ import path15 from "node:path";
2871
2939
  var VERSION = 2;
2872
2940
  var WINDOWS = process.platform === "win32";
2873
2941
  var fwd = (p) => WINDOWS ? p.replace(/\\/g, "/") : p;
@@ -2944,39 +3012,122 @@ esac
2944
3012
  exec "$real" "$@"
2945
3013
  `;
2946
3014
  function shimDir() {
2947
- return path14.join(home(), "shim", `v${VERSION}`);
3015
+ return path15.join(home(), "shim", `v${VERSION}`);
3016
+ }
3017
+ function scoopTarget(exe) {
3018
+ try {
3019
+ const text = fs16.readFileSync(exe.replace(/\.exe$/i, ".shim"), "utf8");
3020
+ const target = /^\s*path\s*=\s*"?([^"\r\n]+?)"?\s*$/m.exec(text)?.[1];
3021
+ return target && fs16.statSync(target).isFile() ? target : void 0;
3022
+ } catch {
3023
+ return void 0;
3024
+ }
2948
3025
  }
2949
3026
  function findRealGit(skip) {
2950
3027
  const same2 = (a, b) => WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
2951
- for (const dir of (process.env.PATH ?? "").split(path14.delimiter).filter(Boolean)) {
2952
- if (same2(path14.resolve(dir), skip)) continue;
2953
- const p = path14.join(dir, WINDOWS ? "git.exe" : "git");
3028
+ for (const dir of (process.env.PATH ?? "").split(path15.delimiter).filter(Boolean)) {
3029
+ if (same2(path15.resolve(dir), skip)) continue;
3030
+ const p = path15.join(dir, WINDOWS ? "git.exe" : "git");
2954
3031
  try {
2955
- fs15.accessSync(p, fs15.constants.X_OK);
2956
- if (fs15.statSync(p).isFile()) return p;
3032
+ fs16.accessSync(p, fs16.constants.X_OK);
3033
+ if (fs16.statSync(p).isFile()) return WINDOWS && scoopTarget(p) || p;
2957
3034
  } catch {
2958
3035
  }
2959
3036
  }
2960
3037
  return void 0;
2961
3038
  }
2962
3039
  function findGitSh(realGit) {
2963
- let dir = path14.dirname(realGit);
3040
+ let dir = path15.dirname(realGit);
2964
3041
  for (let i = 0; i < 4; i++) {
2965
3042
  for (const rel of ["bin/sh.exe", "usr/bin/sh.exe"]) {
2966
- const p = path14.join(dir, rel);
2967
- if (fs15.existsSync(p)) return p;
3043
+ const p = path15.join(dir, rel);
3044
+ if (fs16.existsSync(p)) return p;
2968
3045
  }
2969
- const up = path14.dirname(dir);
3046
+ const up = path15.dirname(dir);
2970
3047
  if (up === dir) break;
2971
3048
  dir = up;
2972
3049
  }
2973
3050
  return void 0;
2974
3051
  }
2975
- var cmdShim = (sh) => `@echo off\r
2976
- set "MSYS2_ARG_CONV_EXCL=*"\r
2977
- "${sh}" "%~dp0git" %*\r
2978
- exit /b %errorlevel%\r
3052
+ var cmdShim = (sh) => [
3053
+ "@echo off",
3054
+ "setlocal EnableExtensions DisableDelayedExpansion",
3055
+ 'set "MSYS2_ARG_CONV_EXCL=*"',
3056
+ 'set "PITROOM_SELF=%~f0"',
3057
+ 'set q=^"',
3058
+ "setlocal EnableDelayedExpansion",
3059
+ 'set "line=!cmdcmdline!"',
3060
+ 'if not "!line: /d =!"=="!line!" goto plain',
3061
+ 'if not "!line: /s =!"=="!line!" goto plain',
3062
+ 'set "tail=!line:*/c ""%PITROOM_SELF%"=!"',
3063
+ 'if "!tail!"=="!line!" set "tail=!line:*/c "%PITROOM_SELF%=!"',
3064
+ 'if "!tail!"=="!line!" goto plain',
3065
+ 'if "!tail:~-1!"=="!q!" set "tail=!tail:~0,-1!"',
3066
+ `"${sh}" "%~dp0git" !tail!`,
3067
+ "exit /b !errorlevel!",
3068
+ ":plain",
3069
+ "endlocal",
3070
+ `"${sh}" "%~dp0git" %*`,
3071
+ "exit /b %errorlevel%",
3072
+ ""
3073
+ ].join("\r\n");
3074
+ var launcherSource = (sh) => `// pitroom git guard: git.exe launcher v${VERSION} (generated; edit src/vcs/guard.ts)
3075
+ using System;
3076
+ using System.Diagnostics;
3077
+ using System.IO;
3078
+
3079
+ static class PitroomGit {
3080
+ const string Sh = @"${sh.replace(/"/g, '""')}";
3081
+
3082
+ static int Main() {
3083
+ string line = Environment.CommandLine;
3084
+ // this program's own name, as Windows reads it: up to the closing quote when it starts with one, else to a space
3085
+ int i = 0;
3086
+ if (line.Length > 0 && line[0] == '"') { int q = line.IndexOf('"', 1); i = q < 0 ? line.Length : q + 1; }
3087
+ else { while (i < line.Length && line[i] != ' ' && line[i] != '\\t') i++; }
3088
+ string script = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "git");
3089
+ ProcessStartInfo start = new ProcessStartInfo(Sh, "\\"" + script + "\\"" + line.Substring(i));
3090
+ start.UseShellExecute = false;
3091
+ start.EnvironmentVariables["MSYS2_ARG_CONV_EXCL"] = "*";
3092
+ // Ctrl+C reaches git too: wait for it to end and pass its exit code on
3093
+ Console.CancelKeyPress += delegate (object o, ConsoleCancelEventArgs e) { e.Cancel = true; };
3094
+ using (Process git = Process.Start(start)) {
3095
+ git.WaitForExit();
3096
+ return git.ExitCode;
3097
+ }
3098
+ }
3099
+ }
2979
3100
  `;
3101
+ function findCsc() {
3102
+ const windir = process.env.WINDIR ?? process.env.SystemRoot ?? "C:\\Windows";
3103
+ for (const fw of ["Framework64", "Framework"]) {
3104
+ const p = path15.join(windir, "Microsoft.NET", fw, "v4.0.30319", "csc.exe");
3105
+ if (fs16.existsSync(p)) return p;
3106
+ }
3107
+ return void 0;
3108
+ }
3109
+ function buildLauncher(dir, sh) {
3110
+ const source = launcherSource(sh);
3111
+ const src = path15.join(dir, "git-launcher.cs");
3112
+ const exe = path15.join(dir, "git.exe");
3113
+ if (fs16.existsSync(exe) && fs16.existsSync(src) && fs16.readFileSync(src, "utf8") === source) return;
3114
+ const csc = findCsc();
3115
+ if (!csc) return;
3116
+ const tmp = path15.join(dir, `git.${process.pid}.tmp.exe`);
3117
+ const tmpSrc = `${src}.${process.pid}.tmp`;
3118
+ try {
3119
+ fs16.mkdirSync(dir, { recursive: true });
3120
+ fs16.writeFileSync(tmpSrc, source);
3121
+ const r = spawnSync9(csc, ["/nologo", "/optimize+", "/target:exe", `/out:${tmp}`, tmpSrc], { encoding: "utf8", timeout: 6e4, windowsHide: true });
3122
+ if (r.status !== 0 || !fs16.existsSync(tmp)) return;
3123
+ renameOver(tmp, exe);
3124
+ renameOver(tmpSrc, src);
3125
+ } catch {
3126
+ } finally {
3127
+ fs16.rmSync(tmp, { force: true });
3128
+ fs16.rmSync(tmpSrc, { force: true });
3129
+ }
3130
+ }
2980
3131
  var HOOK_VERSION = 1;
2981
3132
  var REF_HOOK = `#!/bin/sh
2982
3133
  # pitroom git guard (layer 2, v${HOOK_VERSION}): refuse ref updates made by workers.
@@ -2984,12 +3135,12 @@ var REF_HOOK = `#!/bin/sh
2984
3135
  echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
2985
3136
  exit 1
2986
3137
  `;
2987
- var hooksDir = () => path14.join(home(), "git-hooks", `v${HOOK_VERSION}`);
3138
+ var hooksDir = () => path15.join(home(), "git-hooks", `v${HOOK_VERSION}`);
2988
3139
  function writeIfChanged2(file2, content) {
2989
- if (fs15.existsSync(file2) && fs15.readFileSync(file2, "utf8") === content) return;
2990
- fs15.mkdirSync(path14.dirname(file2), { recursive: true });
3140
+ if (fs16.existsSync(file2) && fs16.readFileSync(file2, "utf8") === content) return;
3141
+ fs16.mkdirSync(path15.dirname(file2), { recursive: true });
2991
3142
  const tmp = `${file2}.${process.pid}.tmp`;
2992
- fs15.writeFileSync(tmp, content, { mode: 493 });
3143
+ fs16.writeFileSync(tmp, content, { mode: 493 });
2993
3144
  renameOver(tmp, file2);
2994
3145
  }
2995
3146
  function pathKey(env) {
@@ -3021,7 +3172,7 @@ function guardEnv(env) {
3021
3172
  GIT_PAGER: "cat",
3022
3173
  PAGER: "cat"
3023
3174
  };
3024
- writeIfChanged2(path14.join(hooksDir(), "reference-transaction"), REF_HOOK);
3175
+ writeIfChanged2(path15.join(hooksDir(), "reference-transaction"), REF_HOOK);
3025
3176
  env = withGitConfig(onePath(env), [
3026
3177
  ["core.hooksPath", fwd(hooksDir())],
3027
3178
  ["url.pitroom-push-blocked://.pushInsteadOf", ""]
@@ -3033,11 +3184,12 @@ function guardEnv(env) {
3033
3184
  if (WINDOWS) {
3034
3185
  sh = findGitSh(real);
3035
3186
  if (!sh) return { ...env, ...quiet };
3036
- writeIfChanged2(path14.join(dir, "git.cmd"), cmdShim(sh));
3187
+ writeIfChanged2(path15.join(dir, "git.cmd"), cmdShim(sh));
3188
+ buildLauncher(dir, sh);
3037
3189
  }
3038
- writeIfChanged2(path14.join(dir, "git"), SHIM);
3190
+ writeIfChanged2(path15.join(dir, "git"), SHIM);
3039
3191
  const key = pathKey(env);
3040
- return { ...env, ...quiet, PITROOM_REAL_GIT: fwd(real), [key]: `${dir}${path14.delimiter}${env[key] ?? ""}` };
3192
+ return { ...env, ...quiet, PITROOM_REAL_GIT: fwd(real), [key]: `${dir}${path15.delimiter}${env[key] ?? ""}` };
3041
3193
  }
3042
3194
  function shimReady() {
3043
3195
  const env = guardEnv({ ...process.env });
@@ -3046,8 +3198,8 @@ function shimReady() {
3046
3198
 
3047
3199
  // src/core/process.ts
3048
3200
  async function spawnWorker(inv, opts) {
3049
- const out = fs16.openSync(opts.stdoutFile, "w");
3050
- const err = fs16.openSync(opts.stderrFile, "w");
3201
+ const out = fs17.openSync(opts.stdoutFile, "w");
3202
+ const err = fs17.openSync(opts.stderrFile, "w");
3051
3203
  const res = { code: null, timedOut: false, stopped: false };
3052
3204
  const child = spawn(inv.command, inv.args, {
3053
3205
  cwd: opts.cwd,
@@ -3070,7 +3222,7 @@ async function spawnWorker(inv, opts) {
3070
3222
  }, opts.timeoutSec * 1e3);
3071
3223
  res.code = await new Promise((resolve2) => {
3072
3224
  child.on("error", (e) => {
3073
- res.spawnError = e.code === "ENOENT" ? fs16.existsSync(opts.cwd) ? `${inv.command} not found` : `the working directory ${opts.cwd} does not exist` : e.message;
3225
+ res.spawnError = e.code === "ENOENT" ? fs17.existsSync(opts.cwd) ? `${inv.command} not found` : `the working directory ${opts.cwd} does not exist` : e.message;
3074
3226
  resolve2(127);
3075
3227
  });
3076
3228
  child.on("close", (c) => resolve2(c));
@@ -3079,8 +3231,8 @@ async function spawnWorker(inv, opts) {
3079
3231
  if (killer) clearTimeout(killer);
3080
3232
  process.off("SIGINT", stop);
3081
3233
  process.off("SIGTERM", stop);
3082
- fs16.closeSync(out);
3083
- fs16.closeSync(err);
3234
+ fs17.closeSync(out);
3235
+ fs17.closeSync(err);
3084
3236
  return res;
3085
3237
  }
3086
3238
 
@@ -3112,12 +3264,13 @@ function parseAudit(text) {
3112
3264
  }
3113
3265
 
3114
3266
  // src/core/cooldown.ts
3115
- import fs17 from "node:fs";
3116
- import path15 from "node:path";
3267
+ import fs18 from "node:fs";
3268
+ import path16 from "node:path";
3117
3269
  var MAX_MS = 24 * 36e5;
3118
3270
  var OVERLOAD_MS = 20 * 6e4;
3271
+ var USAGE_MS = 36e5;
3119
3272
  var DAILY_MS = 4 * 36e5;
3120
- var file = () => path15.join(home(), "cooldowns.json");
3273
+ var file = () => path16.join(home(), "cooldowns.json");
3121
3274
  function cooldownKey(target, backend) {
3122
3275
  const model = (target.model ?? backend.defaultModel?.() ?? "").split("#")[0];
3123
3276
  return `${target.backend}:${model}`;
@@ -3133,23 +3286,54 @@ function retryAfterMs(message) {
3133
3286
  }
3134
3287
  return ms > 0 ? ms : void 0;
3135
3288
  }
3136
- function cooldownMs(message) {
3137
- const told = retryAfterMs(message);
3289
+ function wallClock(at, timeZone) {
3290
+ const p = Object.fromEntries(
3291
+ new Intl.DateTimeFormat("en-US", { timeZone, hourCycle: "h23", year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", second: "numeric" }).formatToParts(at).map((x) => [x.type, Number(x.value)])
3292
+ );
3293
+ return Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);
3294
+ }
3295
+ function retryAtMs(message, now = Date.now()) {
3296
+ const m = /(?:try again at|resets?(?: at)?)\s+(\d{1,2})(?::(\d{2}))?\s*([ap])\.?m\b\.?(?:\s*\(([A-Za-z_]+(?:\/[A-Za-z0-9_+-]+)*)\))?/i.exec(message) ?? /(?:try again at|resets?(?: at)?)\s+(\d{1,2}):(\d{2})()(?!\d)(?:\s*\(([A-Za-z_]+(?:\/[A-Za-z0-9_+-]+)*)\))?/i.exec(message);
3297
+ if (!m) return void 0;
3298
+ let hour = Number(m[1]);
3299
+ const minute = Number(m[2] ?? 0);
3300
+ const half = m[3]?.toLowerCase();
3301
+ if (minute > 59 || (half ? hour < 1 || hour > 12 : hour > 23)) return void 0;
3302
+ if (half === "p" && hour < 12) hour += 12;
3303
+ if (half === "a" && hour === 12) hour = 0;
3304
+ let zone = m[4];
3305
+ try {
3306
+ if (zone) new Intl.DateTimeFormat("en-US", { timeZone: zone });
3307
+ } catch {
3308
+ zone = void 0;
3309
+ }
3310
+ const wall = wallClock(now, zone);
3311
+ for (const day of [0, 1]) {
3312
+ const d = new Date(wall);
3313
+ const target = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + day, hour, minute);
3314
+ const at = target - (wallClock(target, zone) - target);
3315
+ if (at > now) return at - now;
3316
+ }
3317
+ return void 0;
3318
+ }
3319
+ function cooldownMs(message, now = Date.now()) {
3320
+ const told = retryAfterMs(message) ?? retryAtMs(message, now);
3138
3321
  if (told !== void 0) return Math.min(Math.max(told, 6e4), MAX_MS);
3139
- return /daily|per.?day|per day|exhausted your/i.test(message) ? DAILY_MS : OVERLOAD_MS;
3322
+ if (/daily|per.?day|per day|exhausted your/i.test(message)) return DAILY_MS;
3323
+ return /usage limit|session limit|quota|FreeUsageLimit/i.test(message) ? USAGE_MS : OVERLOAD_MS;
3140
3324
  }
3141
3325
  function read() {
3142
3326
  try {
3143
- const raw = JSON.parse(fs17.readFileSync(file(), "utf8"));
3327
+ const raw = JSON.parse(fs18.readFileSync(file(), "utf8"));
3144
3328
  return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
3145
3329
  } catch {
3146
3330
  return {};
3147
3331
  }
3148
3332
  }
3149
3333
  function write(all) {
3150
- fs17.mkdirSync(path15.dirname(file()), { recursive: true });
3334
+ fs18.mkdirSync(path16.dirname(file()), { recursive: true });
3151
3335
  const tmp = `${file()}.${process.pid}.tmp`;
3152
- fs17.writeFileSync(tmp, `${JSON.stringify(all, null, 2)}
3336
+ fs18.writeFileSync(tmp, `${JSON.stringify(all, null, 2)}
3153
3337
  `);
3154
3338
  renameOver(tmp, file());
3155
3339
  }
@@ -3160,7 +3344,7 @@ var activeCooldown = (key, now = Date.now()) => activeCooldowns(now)[key];
3160
3344
  function recordCooldown(key, targetName, message, now = Date.now()) {
3161
3345
  try {
3162
3346
  const all = activeCooldowns(now);
3163
- all[key] = { until: new Date(now + cooldownMs(message)).toISOString(), reason: message.replace(/\s+/g, " ").trim().slice(0, 160), target: targetName };
3347
+ all[key] = { until: new Date(now + cooldownMs(message, now)).toISOString(), reason: message.replace(/\s+/g, " ").trim().slice(0, 160), target: targetName };
3164
3348
  write(all);
3165
3349
  } catch {
3166
3350
  }
@@ -3179,15 +3363,15 @@ function untilText(c, now = Date.now()) {
3179
3363
  }
3180
3364
 
3181
3365
  // src/core/snapshot.ts
3182
- import fs18 from "node:fs";
3183
- import path16 from "node:path";
3366
+ import fs19 from "node:fs";
3367
+ import path17 from "node:path";
3184
3368
  import crypto5 from "node:crypto";
3185
3369
  var READ_IN = ["auto", "snapshot", "project"];
3186
3370
  var KEEP_MS = 24 * 36e5;
3187
3371
  var PRUNE_AGE_MS = 3 * 24 * 36e5;
3188
3372
  var KEEP_NEWEST = 6;
3189
3373
  var JUST_USED_MS = 10 * 6e4;
3190
- var snapshotsDir = () => path16.join(home(), "snapshots");
3374
+ var snapshotsDir = () => path17.join(home(), "snapshots");
3191
3375
  function wantSnapshot(readIn2, root, dir) {
3192
3376
  if (!root || readIn2 === "project") return void 0;
3193
3377
  const secrets = findSecretFiles(dir);
@@ -3196,8 +3380,8 @@ function wantSnapshot(readIn2, root, dir) {
3196
3380
  function readSnapshot(root, owner) {
3197
3381
  const drop2 = findSecretFilesInTree(root, root);
3198
3382
  const tree = snapshotTree(root, [], drop2);
3199
- const dest = path16.join(snapshotsDir(), owner ? `${tree}-${owner}` : tree);
3200
- if (fs18.existsSync(path16.join(dest, ".git"))) {
3383
+ const dest = path17.join(snapshotsDir(), owner ? `${tree}-${owner}` : tree);
3384
+ if (fs19.existsSync(path17.join(dest, ".git"))) {
3201
3385
  touch(dest);
3202
3386
  return { dir: dest, tree, created: false };
3203
3387
  }
@@ -3206,19 +3390,19 @@ function readSnapshot(root, owner) {
3206
3390
  return { dir: dest, tree, created: true };
3207
3391
  }
3208
3392
  function restoreSnapshot(root, dir, tree) {
3209
- const rel = path16.relative(snapshotsDir(), dir);
3210
- if (!rel || rel.startsWith("..") || path16.isAbsolute(rel)) throw new Error(`not a snapshot: ${dir}`);
3211
- if (!fs18.existsSync(path16.join(dir, ".git"))) materialize(root, tree, dir);
3393
+ const rel = path17.relative(snapshotsDir(), dir);
3394
+ if (!rel || rel.startsWith("..") || path17.isAbsolute(rel)) throw new Error(`not a snapshot: ${dir}`);
3395
+ if (!fs19.existsSync(path17.join(dir, ".git"))) materialize(root, tree, dir);
3212
3396
  }
3213
3397
  function materialize(root, tree, dest) {
3214
- const tmp = path16.join(snapshotsDir(), `.tmp-${process.pid}-${crypto5.randomBytes(3).toString("hex")}`);
3398
+ const tmp = path17.join(snapshotsDir(), `.tmp-${process.pid}-${crypto5.randomBytes(3).toString("hex")}`);
3215
3399
  try {
3216
3400
  createIsolatedCopy(root, tree, tmp);
3217
3401
  readOnly(tmp);
3218
3402
  try {
3219
- fs18.renameSync(tmp, dest);
3403
+ fs19.renameSync(tmp, dest);
3220
3404
  } catch (e) {
3221
- if (!fs18.existsSync(path16.join(dest, ".git"))) throw e;
3405
+ if (!fs19.existsSync(path17.join(dest, ".git"))) throw e;
3222
3406
  removeIsolatedCopy(tmp, snapshotsDir());
3223
3407
  }
3224
3408
  } catch (e) {
@@ -3232,17 +3416,17 @@ function materialize(root, tree, dest) {
3232
3416
  function touch(dir) {
3233
3417
  try {
3234
3418
  const now = /* @__PURE__ */ new Date();
3235
- fs18.utimesSync(dir, now, now);
3419
+ fs19.utimesSync(dir, now, now);
3236
3420
  } catch {
3237
3421
  }
3238
3422
  }
3239
3423
  function readOnly(dir) {
3240
3424
  const walk = (d) => {
3241
- for (const e of fs18.readdirSync(d, { withFileTypes: true })) {
3425
+ for (const e of fs19.readdirSync(d, { withFileTypes: true })) {
3242
3426
  if (e.name === ".git") continue;
3243
- const p = path16.join(d, e.name);
3427
+ const p = path17.join(d, e.name);
3244
3428
  if (e.isDirectory()) walk(p);
3245
- else if (e.isFile()) fs18.chmodSync(p, fs18.statSync(p).mode & ~146);
3429
+ else if (e.isFile()) fs19.chmodSync(p, fs19.statSync(p).mode & ~146);
3246
3430
  }
3247
3431
  };
3248
3432
  try {
@@ -3253,7 +3437,7 @@ function readOnly(dir) {
3253
3437
  function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWEST) {
3254
3438
  let names;
3255
3439
  try {
3256
- names = fs18.readdirSync(snapshotsDir());
3440
+ names = fs19.readdirSync(snapshotsDir());
3257
3441
  } catch {
3258
3442
  return 0;
3259
3443
  }
@@ -3261,14 +3445,14 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
3261
3445
  for (const id2 of listRunIds()) {
3262
3446
  try {
3263
3447
  const m = readMeta(id2);
3264
- if (m.snapshot?.dir && isActive(m.state)) inUse.add(path16.resolve(m.snapshot.dir));
3448
+ if (m.snapshot?.dir && isActive(m.state)) inUse.add(path17.resolve(m.snapshot.dir));
3265
3449
  } catch {
3266
3450
  }
3267
3451
  }
3268
3452
  const entries = names.map((n) => {
3269
- const dir = path16.join(snapshotsDir(), n);
3453
+ const dir = path17.join(snapshotsDir(), n);
3270
3454
  try {
3271
- return { n, dir, age: now - fs18.statSync(dir).mtimeMs };
3455
+ return { n, dir, age: now - fs19.statSync(dir).mtimeMs };
3272
3456
  } catch {
3273
3457
  return void 0;
3274
3458
  }
@@ -3276,7 +3460,7 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
3276
3460
  let removed = 0;
3277
3461
  const rank = { shared: 0, own: 0 };
3278
3462
  for (const e of entries) {
3279
- if (inUse.has(path16.resolve(e.dir))) continue;
3463
+ if (inUse.has(path17.resolve(e.dir))) continue;
3280
3464
  const half = e.n.startsWith(".tmp-");
3281
3465
  const old = e.age >= (half ? KEEP_MS : maxAgeMs);
3282
3466
  const surplus = !half && ++rank[e.n.includes("-") ? "own" : "shared"] > keepNewest && e.age >= JUST_USED_MS;
@@ -3292,10 +3476,10 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
3292
3476
  }
3293
3477
  function makeWritable(dir) {
3294
3478
  const walk = (d) => {
3295
- for (const e of fs18.readdirSync(d, { withFileTypes: true })) {
3296
- const p = path16.join(d, e.name);
3479
+ for (const e of fs19.readdirSync(d, { withFileTypes: true })) {
3480
+ const p = path17.join(d, e.name);
3297
3481
  if (e.isDirectory()) walk(p);
3298
- else if (e.isFile()) fs18.chmodSync(p, fs18.statSync(p).mode | 128);
3482
+ else if (e.isFile()) fs19.chmodSync(p, fs19.statSync(p).mode | 128);
3299
3483
  }
3300
3484
  };
3301
3485
  try {
@@ -3306,17 +3490,17 @@ function makeWritable(dir) {
3306
3490
  function backToProject(text, snapshotDir, root) {
3307
3491
  let real = snapshotDir;
3308
3492
  try {
3309
- real = fs18.realpathSync(snapshotDir);
3493
+ real = fs19.realpathSync(snapshotDir);
3310
3494
  } catch {
3311
3495
  }
3312
3496
  let out = text;
3313
- for (const dir of /* @__PURE__ */ new Set([real, snapshotDir])) out = out.split(`${dir}${path16.sep}`).join(`${root}${path16.sep}`).split(dir).join(root);
3497
+ for (const dir of /* @__PURE__ */ new Set([real, snapshotDir])) out = out.split(`${dir}${path17.sep}`).join(`${root}${path17.sep}`).split(dir).join(root);
3314
3498
  return out;
3315
3499
  }
3316
3500
 
3317
3501
  // src/core/audit.ts
3318
3502
  import crypto6 from "node:crypto";
3319
- import path17 from "node:path";
3503
+ import path18 from "node:path";
3320
3504
  var ANSWER_MAX = 8e3;
3321
3505
  function auditRate(meta) {
3322
3506
  return meta.auditRate ?? effective().audit.value;
@@ -3349,20 +3533,30 @@ function focusedRate(base2, task, record2) {
3349
3533
  return { rate: Math.min(rate2, 1), why };
3350
3534
  }
3351
3535
  function pickAuditor(meta) {
3536
+ return auditors(meta)[0];
3537
+ }
3538
+ function auditors(meta) {
3352
3539
  const eff = effective();
3353
3540
  const tiers = eff.tiers.value;
3354
3541
  const def = parseTarget(eff.worker.value, DEFAULT_BACKEND).backend;
3355
3542
  const ran = meta.ran ?? meta.worker;
3543
+ const bare2 = (t) => ({ backend: t.backend, model: t.model?.split("#")[0] || void 0 });
3356
3544
  const candidates = [tiers.audit, tiers.cheap, ...eff.fallback.value, eff.worker.value].filter((s) => !!s);
3357
- return candidates.find((c) => !sameTarget(parseTarget(c, def), ran));
3545
+ const out = [];
3546
+ for (const c of candidates) {
3547
+ const t = bare2(parseTarget(c, def));
3548
+ if (sameTarget(t, bare2(ran)) || out.some((o) => sameTarget(bare2(parseTarget(o, def)), t))) continue;
3549
+ out.push(c);
3550
+ }
3551
+ return out;
3358
3552
  }
3359
3553
  function relativeToAuditor(answer, meta) {
3360
3554
  let out = answer;
3361
3555
  const roots = [...new Set([meta.dir, meta.repoRoot].filter((r) => !!r))].sort((a, b) => b.length - a.length);
3362
3556
  for (const root of roots) {
3363
- const rel = path17.relative(meta.dir, root);
3364
- const prefix = rel ? `${rel.split(path17.sep).join("/")}/` : "";
3365
- out = out.split(`${root}${path17.sep}`).join(prefix).replace(new RegExp(`${root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w.-])`, "g"), rel || ".");
3557
+ const rel = path18.relative(meta.dir, root);
3558
+ const prefix = rel ? `${rel.split(path18.sep).join("/")}/` : "";
3559
+ out = out.split(`${root}${path18.sep}`).join(prefix).replace(new RegExp(`${root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w.-])`, "g"), rel || ".");
3366
3560
  }
3367
3561
  return out;
3368
3562
  }
@@ -3432,19 +3626,19 @@ function auditBadge(meta) {
3432
3626
  }
3433
3627
 
3434
3628
  // src/core/history.ts
3435
- import fs22 from "node:fs";
3629
+ import fs23 from "node:fs";
3436
3630
  import { createRequire } from "node:module";
3437
- import path20 from "node:path";
3631
+ import path21 from "node:path";
3438
3632
  import zlib from "node:zlib";
3439
3633
 
3440
3634
  // src/core/receipt.ts
3441
- import fs20 from "node:fs";
3442
- import path19 from "node:path";
3635
+ import fs21 from "node:fs";
3636
+ import path20 from "node:path";
3443
3637
 
3444
3638
  // src/core/prices.ts
3445
3639
  import { spawn as spawn2 } from "node:child_process";
3446
- import fs19 from "node:fs";
3447
- import path18 from "node:path";
3640
+ import fs20 from "node:fs";
3641
+ import path19 from "node:path";
3448
3642
  var DEFAULT_URL = "https://models.dev/api.json";
3449
3643
  var RETRY_AFTER_FAILURE_MS = 60 * 6e4;
3450
3644
  var MIN_MODELS = 100;
@@ -3457,8 +3651,8 @@ function catalogLabel() {
3457
3651
  return url;
3458
3652
  }
3459
3653
  }
3460
- var catalogFile = () => path18.join(home(), "prices.json");
3461
- var attemptFile = () => path18.join(home(), "prices.attempt");
3654
+ var catalogFile = () => path19.join(home(), "prices.json");
3655
+ var attemptFile = () => path19.join(home(), "prices.attempt");
3462
3656
  function trim(raw) {
3463
3657
  const out = {};
3464
3658
  if (!raw || typeof raw !== "object") return out;
@@ -3476,9 +3670,9 @@ var cached2;
3476
3670
  function readCatalog() {
3477
3671
  const file2 = catalogFile();
3478
3672
  try {
3479
- const { mtimeMs } = fs19.statSync(file2);
3673
+ const { mtimeMs } = fs20.statSync(file2);
3480
3674
  if (cached2?.file === file2 && cached2.mtimeMs === mtimeMs) return cached2.catalog;
3481
- const parsed = JSON.parse(fs19.readFileSync(file2, "utf8"));
3675
+ const parsed = JSON.parse(fs20.readFileSync(file2, "utf8"));
3482
3676
  const catalog6 = parsed && typeof parsed.models === "object" && parsed.models && Number.isFinite(Date.parse(parsed.fetchedAt)) ? parsed : void 0;
3483
3677
  cached2 = { file: file2, mtimeMs, catalog: catalog6 };
3484
3678
  return catalog6;
@@ -3488,7 +3682,7 @@ function readCatalog() {
3488
3682
  }
3489
3683
  var ageMs = (file2) => {
3490
3684
  try {
3491
- return Date.now() - fs19.statSync(file2).mtimeMs;
3685
+ return Date.now() - fs20.statSync(file2).mtimeMs;
3492
3686
  } catch {
3493
3687
  return Infinity;
3494
3688
  }
@@ -3507,17 +3701,17 @@ async function refreshCatalog() {
3507
3701
  const models = trim(await r.json());
3508
3702
  const n = Object.keys(models).length;
3509
3703
  if (n < MIN_MODELS) throw new Error(`only ${n} priced models in it (a catalog has thousands): not used`);
3510
- fs19.mkdirSync(home(), { recursive: true });
3704
+ fs20.mkdirSync(home(), { recursive: true });
3511
3705
  const tmp = `${catalogFile()}.${process.pid}.tmp`;
3512
3706
  const catalog6 = { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), source: url, models };
3513
- fs19.writeFileSync(tmp, JSON.stringify(catalog6));
3707
+ fs20.writeFileSync(tmp, JSON.stringify(catalog6));
3514
3708
  renameOver(tmp, catalogFile());
3515
- fs19.rmSync(attemptFile(), { force: true });
3709
+ fs20.rmSync(attemptFile(), { force: true });
3516
3710
  return { state: "updated", models: n };
3517
3711
  } catch (e) {
3518
3712
  try {
3519
- fs19.mkdirSync(home(), { recursive: true });
3520
- fs19.writeFileSync(attemptFile(), `${(/* @__PURE__ */ new Date()).toISOString()} ${e.message}
3713
+ fs20.mkdirSync(home(), { recursive: true });
3714
+ fs20.writeFileSync(attemptFile(), `${(/* @__PURE__ */ new Date()).toISOString()} ${e.message}
3521
3715
  `);
3522
3716
  } catch {
3523
3717
  }
@@ -3529,8 +3723,8 @@ function refreshInBackground() {
3529
3723
  if (!refreshDue()) return;
3530
3724
  const script = process.argv[1];
3531
3725
  if (!script) return;
3532
- fs19.mkdirSync(home(), { recursive: true });
3533
- fs19.writeFileSync(attemptFile(), `${(/* @__PURE__ */ new Date()).toISOString()} refresh started
3726
+ fs20.mkdirSync(home(), { recursive: true });
3727
+ fs20.writeFileSync(attemptFile(), `${(/* @__PURE__ */ new Date()).toISOString()} refresh started
3534
3728
  `);
3535
3729
  const child = spawn2(process.execPath, [script, "prices", "--refresh", "--quiet"], { detached: true, stdio: "ignore", env: process.env });
3536
3730
  child.on("error", () => void 0);
@@ -3560,7 +3754,7 @@ function catalogStatus() {
3560
3754
  const c = readCatalog();
3561
3755
  let lastFailure;
3562
3756
  try {
3563
- const t = fs19.readFileSync(attemptFile(), "utf8").trim();
3757
+ const t = fs20.readFileSync(attemptFile(), "utf8").trim();
3564
3758
  if (t && !t.endsWith("refresh started")) lastFailure = t;
3565
3759
  } catch {
3566
3760
  }
@@ -3611,12 +3805,21 @@ function workerPrice(backend, ...models) {
3611
3805
  }
3612
3806
  var costAt = (usage2, price) => (usage2.input * price.input + usage2.cacheRead * price.cachedInput + (usage2.output + usage2.reasoning) * price.output) / 1e6;
3613
3807
  var workerCostOf = (usage2) => usage2?.cost ?? usage2?.costEstimate;
3808
+ function addUsage(a, b) {
3809
+ const sum = (k) => (a[k] ?? 0) + (b[k] ?? 0);
3810
+ const out = { input: sum("input"), output: sum("output"), reasoning: sum("reasoning"), cacheRead: sum("cacheRead"), cacheWrite: sum("cacheWrite"), total: sum("total"), steps: sum("steps"), toolCalls: sum("toolCalls"), denied: sum("denied") };
3811
+ const ca = workerCostOf(a);
3812
+ const cb = workerCostOf(b);
3813
+ if (ca === void 0 && cb === void 0) return out;
3814
+ if (a.cost !== void 0 && b.cost !== void 0) return { ...out, cost: a.cost + b.cost };
3815
+ return { ...out, costEstimate: (ca ?? 0) + (cb ?? 0), costSource: a.costSource ?? b.costSource };
3816
+ }
3614
3817
  function savedUsd(usage2, returnedTokens, price = primaryPrice()) {
3615
3818
  const readingTheReport = returnedTokens * price.input / 1e6;
3616
3819
  return Math.max(0, costAt(usage2, price) - (workerCostOf(usage2) ?? 0) - readingTheReport);
3617
3820
  }
3618
3821
  function record(meta) {
3619
- if (!meta.usage?.steps || meta.auditOf) return;
3822
+ if (meta.auditOf || !meta.usage?.steps && meta.state !== "done") return false;
3620
3823
  const entry = {
3621
3824
  id: meta.id,
3622
3825
  at: meta.endedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
@@ -3624,22 +3827,49 @@ function record(meta) {
3624
3827
  state: meta.state,
3625
3828
  backend: (meta.ran ?? meta.worker).backend,
3626
3829
  model: meta.resolvedModel ?? (meta.ran ?? meta.worker).model,
3627
- tokens: meta.usage.total,
3830
+ tokens: meta.usage?.total ?? 0,
3628
3831
  returned: meta.returnedTokens ?? 0,
3629
3832
  workerCost: workerCostOf(meta.usage) ?? 0,
3630
- ...meta.usage.cost === void 0 && meta.usage.costEstimate !== void 0 ? { costEstimated: true, costSource: meta.usage.costSource } : {},
3833
+ ...meta.usage?.cost === void 0 && meta.usage?.costEstimate !== void 0 ? { costEstimated: true, costSource: meta.usage.costSource } : {},
3631
3834
  saved: meta.savedUsd ?? 0,
3632
3835
  price: primaryPrice().name
3633
3836
  };
3634
- fs20.mkdirSync(path19.dirname(ledgerFile()), { recursive: true });
3635
- fs20.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
3837
+ fs21.mkdirSync(path20.dirname(ledgerFile()), { recursive: true });
3838
+ fs21.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
3636
3839
  `);
3840
+ return true;
3841
+ }
3842
+ var SETTLING_MS = 10 * 6e4;
3843
+ function settleLedger(metas) {
3844
+ const known = new Set(readLedger().map((e) => e.id));
3845
+ let n = 0;
3846
+ for (const m of metas ?? finishedRuns()) {
3847
+ if (known.has(m.id) || !TERMINAL.includes(m.state)) continue;
3848
+ if (m.pid !== process.pid && isAlive(m.pid) && (!m.endedAt || Date.now() - Date.parse(m.endedAt) < SETTLING_MS)) continue;
3849
+ if (record(m)) {
3850
+ known.add(m.id);
3851
+ n++;
3852
+ }
3853
+ }
3854
+ return n;
3855
+ }
3856
+ function* finishedRuns() {
3857
+ for (const id2 of listRunIds()) {
3858
+ try {
3859
+ yield readMeta(id2);
3860
+ } catch {
3861
+ }
3862
+ }
3637
3863
  }
3638
3864
  function readLedger(sinceMs2) {
3639
- if (!fs20.existsSync(ledgerFile())) return [];
3640
- return fs20.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
3865
+ if (!fs21.existsSync(ledgerFile())) return [];
3866
+ const seen = /* @__PURE__ */ new Set();
3867
+ return fs21.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
3641
3868
  try {
3642
- return [JSON.parse(l)];
3869
+ const e = JSON.parse(l);
3870
+ if (seen.has(e.id)) return [];
3871
+ seen.add(e.id);
3872
+ return [e];
3643
3873
  } catch {
3644
3874
  return [];
3645
3875
  }
@@ -3712,7 +3942,7 @@ ${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.rou
3712
3942
  }
3713
3943
 
3714
3944
  // src/core/ui.ts
3715
- import fs21 from "node:fs";
3945
+ import fs22 from "node:fs";
3716
3946
  var WEEK_MS = 7 * 24 * 3600 * 1e3;
3717
3947
  var RECENT = 40;
3718
3948
  var RECENT_HOURS = 1;
@@ -3803,9 +4033,9 @@ function cardsFor(m) {
3803
4033
  const out = [];
3804
4034
  for (const [phase, text] of phases) {
3805
4035
  const marker = runFile(m.id, `card-${phase}`);
3806
- if (fs21.existsSync(marker)) continue;
4036
+ if (fs22.existsSync(marker)) continue;
3807
4037
  if (!(phase === "started" && !isActive(m.state))) out.push(text());
3808
- fs21.writeFileSync(marker, "");
4038
+ fs22.writeFileSync(marker, "");
3809
4039
  }
3810
4040
  return out;
3811
4041
  }
@@ -3816,10 +4046,10 @@ function hookCards(input) {
3816
4046
  const response = event.tool_response;
3817
4047
  const output = typeof response === "string" ? response : [response?.stdout, response?.stderr].filter(Boolean).join("\n");
3818
4048
  const ids = [...new Set(`${command}
3819
- ${output}`.match(RUN_ID) ?? [])].filter((id2) => fs21.existsSync(`${runsDir()}/${id2}`));
4049
+ ${output}`.match(RUN_ID) ?? [])].filter((id2) => fs22.existsSync(`${runsDir()}/${id2}`));
3820
4050
  const since2 = Date.now() - RECENT_HOURS * 36e5;
3821
4051
  for (const id2 of listRunIds().slice(-RECENT)) {
3822
- if (ids.includes(id2) || fs21.existsSync(runFile(id2, "card-ended"))) continue;
4052
+ if (ids.includes(id2) || fs22.existsSync(runFile(id2, "card-ended"))) continue;
3823
4053
  try {
3824
4054
  const m = readMeta(id2);
3825
4055
  if (!isActive(m.state) && Date.parse(m.endedAt ?? m.startedAt) > since2) ids.push(id2);
@@ -3841,7 +4071,7 @@ var SCHEMA_VERSION = 1;
3841
4071
  var PATCH_MAX = 1e6;
3842
4072
  var ANSWER_MAX2 = 2e5;
3843
4073
  var cached3;
3844
- var historyFile = () => path20.join(home(), "history.db");
4074
+ var historyFile = () => path21.join(home(), "history.db");
3845
4075
  function openDb() {
3846
4076
  const file2 = historyFile();
3847
4077
  if (cached3?.file === file2) return cached3.db;
@@ -3855,7 +4085,7 @@ function openDb() {
3855
4085
  } finally {
3856
4086
  process.emitWarning = emit;
3857
4087
  }
3858
- fs22.mkdirSync(path20.dirname(file2), { recursive: true });
4088
+ fs23.mkdirSync(path21.dirname(file2), { recursive: true });
3859
4089
  db = new DatabaseSync(file2);
3860
4090
  db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
3861
4091
  migrate(db);
@@ -3890,31 +4120,31 @@ function migrate(db) {
3890
4120
  function readRunFile(id2, name) {
3891
4121
  const f = runFile(id2, name);
3892
4122
  try {
3893
- if (fs22.existsSync(f)) return fs22.readFileSync(f, "utf8");
3894
- if (fs22.existsSync(`${f}.gz`)) return zlib.gunzipSync(fs22.readFileSync(`${f}.gz`)).toString("utf8");
4123
+ if (fs23.existsSync(f)) return fs23.readFileSync(f, "utf8");
4124
+ if (fs23.existsSync(`${f}.gz`)) return zlib.gunzipSync(fs23.readFileSync(`${f}.gz`)).toString("utf8");
3895
4125
  } catch {
3896
4126
  }
3897
4127
  return void 0;
3898
4128
  }
3899
4129
  function gzipFile(f) {
3900
- if (!fs22.existsSync(f)) return;
3901
- fs22.writeFileSync(`${f}.gz`, zlib.gzipSync(fs22.readFileSync(f)));
3902
- fs22.rmSync(f);
4130
+ if (!fs23.existsSync(f)) return;
4131
+ fs23.writeFileSync(`${f}.gz`, zlib.gzipSync(fs23.readFileSync(f)));
4132
+ fs23.rmSync(f);
3903
4133
  }
3904
4134
  function compactRun(meta) {
3905
4135
  if (!TERMINAL.includes(meta.state)) return;
3906
- const dir = path20.dirname(runFile(meta.id, "meta.json"));
4136
+ const dir = path21.dirname(runFile(meta.id, "meta.json"));
3907
4137
  let names;
3908
4138
  try {
3909
- names = fs22.readdirSync(dir);
4139
+ names = fs23.readdirSync(dir);
3910
4140
  } catch {
3911
4141
  return;
3912
4142
  }
3913
4143
  for (const n of names) {
3914
- if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(path20.join(dir, n));
4144
+ if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(path21.join(dir, n));
3915
4145
  else if (/^stderr(\.attempt-\d+)?\.log$/.test(n)) {
3916
- if (meta.state === "done" && !/attempt/.test(n)) fs22.rmSync(path20.join(dir, n), { force: true });
3917
- else gzipFile(path20.join(dir, n));
4146
+ if (meta.state === "done" && !/attempt/.test(n)) fs23.rmSync(path21.join(dir, n), { force: true });
4147
+ else gzipFile(path21.join(dir, n));
3918
4148
  }
3919
4149
  }
3920
4150
  }
@@ -4036,6 +4266,7 @@ function archivedId(ref) {
4036
4266
  }
4037
4267
  }
4038
4268
  var VERIFY_FAILED = "(r.state = 'done' AND json_extract(r.meta_json, '$.verifyResult.ok') = 0)";
4269
+ var COST_ESTIMATED = "(json_extract(r.meta_json, '$.usage.cost') IS NULL AND json_extract(r.meta_json, '$.usage.costEstimate') IS NOT NULL)";
4039
4270
  var ftsQuery = (text) => text.split(/\s+/).filter(Boolean).map((w) => `"${w.replace(/"/g, '""')}"*`).join(" ");
4040
4271
  function listHistory(q = {}) {
4041
4272
  const db = openDb();
@@ -4070,7 +4301,7 @@ function listHistory(q = {}) {
4070
4301
  try {
4071
4302
  const total = db.prepare(`SELECT COUNT(*) AS n FROM runs r ${base2}`).get(...args).n;
4072
4303
  const page2 = q.beforeId ? `${base2 ? `${base2} AND` : "WHERE"} r.id < ?` : base2;
4073
- const rows = db.prepare(`SELECT r.*, json_extract(r.meta_json, '$.audit.state') AS audit_state, json_extract(r.meta_json, '$.audit.verdict') AS audit_verdict, ${VERIFY_FAILED} AS verify_failed FROM runs r ${page2} ORDER BY r.id DESC LIMIT ?`).all(...args, ...q.beforeId ? [q.beforeId] : [], Math.min(Math.max(q.limit ?? 30, 1), 200));
4304
+ const rows = db.prepare(`SELECT r.*, json_extract(r.meta_json, '$.audit.state') AS audit_state, json_extract(r.meta_json, '$.audit.verdict') AS audit_verdict, ${VERIFY_FAILED} AS verify_failed, ${COST_ESTIMATED} AS cost_estimated FROM runs r ${page2} ORDER BY r.id DESC LIMIT ?`).all(...args, ...q.beforeId ? [q.beforeId] : [], Math.min(Math.max(q.limit ?? 30, 1), 200));
4074
4305
  return {
4075
4306
  total,
4076
4307
  rows: rows.map((r) => ({
@@ -4088,6 +4319,8 @@ function listHistory(q = {}) {
4088
4319
  seconds: r.seconds ?? void 0,
4089
4320
  steps: r.steps ?? void 0,
4090
4321
  tokens: r.tokens ?? void 0,
4322
+ cost: r.cost ?? void 0,
4323
+ ...r.cost_estimated ? { costEstimated: true } : {},
4091
4324
  saved: r.saved ?? void 0,
4092
4325
  files: r.files_changed ?? 0,
4093
4326
  applied: !!r.applied
@@ -4122,18 +4355,32 @@ function auditStats(db, since2) {
4122
4355
  }
4123
4356
  return { total, byWorker };
4124
4357
  }
4125
- function workerRecord(backend, model, sinceMs2) {
4358
+ var WORK_SQL = { research: "kind = 'research'", change: "kind LIKE 'change%'", review: "kind LIKE '%review'" };
4359
+ function workerRecord(backend, model, sinceMs2, work) {
4126
4360
  const db = openDb();
4127
4361
  if (!db) return void 0;
4128
4362
  const since2 = new Date(sinceMs2).toISOString();
4129
4363
  const fits = (m) => !model || m === model || !!m?.endsWith(`/${model}`) || model.endsWith(`/${m}`);
4130
4364
  const rec = { runs: 0, ok: 0, audited: 0, agreed: 0 };
4131
4365
  try {
4132
- for (const r of db.prepare(`SELECT model, SUM(NOT ${LIMITED}) runs, SUM(state='done') ok FROM runs WHERE backend = ? AND started_at >= ? AND ${NOT_AUDIT} AND ${ATTEMPTED} GROUP BY model`).all(backend, since2)) {
4366
+ for (const r of db.prepare(`SELECT model, SUM(NOT ${LIMITED}) runs, SUM(state='done') ok FROM runs WHERE backend = ? AND started_at >= ? AND ${NOT_AUDIT} AND ${ATTEMPTED} AND ${work ? WORK_SQL[work] : "1"} GROUP BY model`).all(backend, since2)) {
4133
4367
  if (!fits(r.model)) continue;
4134
4368
  rec.runs += r.runs ?? 0;
4135
4369
  rec.ok += r.ok ?? 0;
4136
4370
  }
4371
+ if (work === "review") return rec;
4372
+ if (work === "change") {
4373
+ const reviews = db.prepare(`SELECT r.model, json_extract(v.meta_json, '$.verdict.spec') spec, json_extract(v.meta_json, '$.verdict.quality') quality, COUNT(*) n
4374
+ FROM runs v JOIN runs r ON r.id = v.review_of
4375
+ WHERE r.backend = ? AND v.started_at >= ? AND v.state = 'done' AND r.kind LIKE 'change%' AND json_extract(v.meta_json, '$.reviewOf') IS NOT NULL
4376
+ GROUP BY r.model, spec, quality`).all(backend, since2);
4377
+ for (const r of reviews) {
4378
+ if (!fits(r.model) || !["approved", "needs-fixes"].includes(r.quality)) continue;
4379
+ rec.audited += r.n;
4380
+ if (r.spec === "pass" && r.quality === "approved") rec.agreed += r.n;
4381
+ }
4382
+ return rec;
4383
+ }
4137
4384
  const audits = db.prepare(`SELECT r.model, json_extract(a.meta_json, '$.auditVerdict') v, COUNT(*) n
4138
4385
  FROM runs a JOIN runs r ON r.id = json_extract(a.meta_json, '$.auditOf')
4139
4386
  WHERE r.backend = ? AND a.started_at >= ? AND a.state = 'done' AND json_extract(a.meta_json, '$.auditOf') IS NOT NULL
@@ -4217,7 +4464,7 @@ function historyStats(sinceMs2) {
4217
4464
  }
4218
4465
 
4219
4466
  // src/core/budget.ts
4220
- import fs23 from "node:fs";
4467
+ import fs24 from "node:fs";
4221
4468
  var startOfDay = (now = /* @__PURE__ */ new Date()) => new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
4222
4469
  function activeSpend(sinceMs2) {
4223
4470
  let total = 0;
@@ -4227,8 +4474,8 @@ function activeSpend(sinceMs2) {
4227
4474
  if (!isActive(m.state) || Date.parse(m.startedAt) < sinceMs2) continue;
4228
4475
  const target = m.ran ?? m.worker;
4229
4476
  const events = runFile(id2, "events.jsonl");
4230
- if (!fs23.existsSync(events)) continue;
4231
- const run2 = getBackend(target.backend).parse(fs23.readFileSync(events, "utf8"));
4477
+ if (!fs24.existsSync(events)) continue;
4478
+ const run2 = getBackend(target.backend).parse(fs24.readFileSync(events, "utf8"));
4232
4479
  if (run2.usage.cost !== void 0) total += run2.usage.cost;
4233
4480
  else {
4234
4481
  const price = workerPrice(target.backend, run2.model, target.model);
@@ -4279,7 +4526,7 @@ function scoreOf(r) {
4279
4526
  return r.audited >= MIN_AUDITS ? ok * (r.agreed / r.audited) : ok;
4280
4527
  }
4281
4528
  var pct = (x) => `${Math.round(x * 100)}%`;
4282
- function rankChain(chain, recordOf) {
4529
+ function rankChain(chain, recordOf, work) {
4283
4530
  const scored = chain.map((t, i) => ({ t, i, score: scoreOf(recordOf(t)) }));
4284
4531
  const slots = scored.filter((s) => s.score !== void 0);
4285
4532
  if (slots.length < 2) return { chain };
@@ -4290,22 +4537,22 @@ function rankChain(chain, recordOf) {
4290
4537
  const by = (t) => scored.find((s) => s.t === t).score;
4291
4538
  return {
4292
4539
  chain: out,
4293
- note: `ranked by record ("rankWorkers"): ${describeTarget(out[0])} (${pct(by(out[0]))}) goes before ${describeTarget(chain[0])} (${pct(by(chain[0]))})`
4540
+ note: `ranked by record${work ? ` on ${work}` : ""} ("rankWorkers"): ${describeTarget(out[0])} (${pct(by(out[0]))}) goes before ${describeTarget(chain[0])} (${pct(by(chain[0]))})`
4294
4541
  };
4295
4542
  }
4296
4543
 
4297
4544
  // src/core/notify.ts
4298
4545
  import { spawn as spawn3 } from "node:child_process";
4299
4546
  import crypto7 from "node:crypto";
4300
- import fs26 from "node:fs";
4301
- import path22 from "node:path";
4547
+ import fs27 from "node:fs";
4548
+ import path23 from "node:path";
4302
4549
 
4303
4550
  // src/core/report.ts
4304
- import fs25 from "node:fs";
4551
+ import fs26 from "node:fs";
4305
4552
 
4306
4553
  // src/core/plan.ts
4307
- import fs24 from "node:fs";
4308
- import path21 from "node:path";
4554
+ import fs25 from "node:fs";
4555
+ import path22 from "node:path";
4309
4556
  var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
4310
4557
  var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
4311
4558
  function structure(lines) {
@@ -4352,9 +4599,9 @@ function parsePlan(text, file2 = "") {
4352
4599
  return { file: file2, title: titleHeading?.text ?? "", header, constraints, tasks };
4353
4600
  }
4354
4601
  function loadPlan(file2) {
4355
- const abs = path21.resolve(file2);
4356
- if (!fs24.existsSync(abs) || !fs24.statSync(abs).isFile()) throw new UserError(`plan not found: ${file2}`);
4357
- const plan = parsePlan(fs24.readFileSync(abs, "utf8"), fs24.realpathSync(abs));
4602
+ const abs = path22.resolve(file2);
4603
+ if (!fs25.existsSync(abs) || !fs25.statSync(abs).isFile()) throw new UserError(`plan not found: ${file2}`);
4604
+ const plan = parsePlan(fs25.readFileSync(abs, "utf8"), fs25.realpathSync(abs));
4358
4605
  if (!plan.tasks.length) throw new UserError(`${file2} has no "Task N" headings (see pitroom-writing-plans)`);
4359
4606
  return plan;
4360
4607
  }
@@ -4373,7 +4620,7 @@ function brief(plan, task) {
4373
4620
  ].filter(Boolean).join("\n\n")}
4374
4621
  `;
4375
4622
  }
4376
- var planName = (file2) => path21.basename(file2).replace(/\.md$/i, "");
4623
+ var planName = (file2) => path22.basename(file2).replace(/\.md$/i, "");
4377
4624
 
4378
4625
  // src/core/style.ts
4379
4626
  var enabled = () => {
@@ -4433,7 +4680,7 @@ function duration(meta) {
4433
4680
  }
4434
4681
  function readSummary(meta) {
4435
4682
  const f = runFile(meta.id, "summary.md");
4436
- return fs25.existsSync(f) ? fs25.readFileSync(f, "utf8").trim() : archivedRun(meta.id)?.answer ?? "";
4683
+ return fs26.existsSync(f) ? fs26.readFileSync(f, "utf8").trim() : archivedRun(meta.id)?.answer ?? "";
4437
4684
  }
4438
4685
  function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
4439
4686
  const out = [];
@@ -4451,7 +4698,7 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
4451
4698
  const status = meta.taskStatus ? ` \xB7 STATUS ${meta.taskStatus}` : "";
4452
4699
  out.push(`plan: ${planName(meta.plan.file)} \xB7 Task ${meta.plan.step}: ${meta.plan.title}${status}`);
4453
4700
  }
4454
- for (const a of meta.attempts ?? []) out.push(a.skipped ? `skipped: ${a.target} (${a.error.slice(0, 160)})` : `fallback: ${a.target} failed (${a.error.slice(0, 160)})`);
4701
+ for (const a of meta.attempts ?? []) out.push(a.skipped ? `skipped: ${a.target} (${a.error.slice(0, 160)})` : a.partial ? `fallback: ${a.target} hit a limit part way and handed the task over (${a.error.slice(0, 160)})` : `fallback: ${a.target} failed (${a.error.slice(0, 160)})`);
4455
4702
  if (meta.error) out.push(`error: ${meta.error}`);
4456
4703
  for (const w of meta.warnings) out.push(`warning: ${w}`);
4457
4704
  if (meta.state === "timeout") {
@@ -4519,8 +4766,8 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
4519
4766
  }
4520
4767
  function live(meta) {
4521
4768
  const f = runFile(meta.id, "events.jsonl");
4522
- if (!fs25.existsSync(f)) return { steps: 0, toolCalls: 0 };
4523
- const p = getBackend((meta.ran ?? meta.worker).backend).parse(fs25.readFileSync(f, "utf8"));
4769
+ if (!fs26.existsSync(f)) return { steps: 0, toolCalls: 0 };
4770
+ const p = getBackend((meta.ran ?? meta.worker).backend).parse(fs26.readFileSync(f, "utf8"));
4524
4771
  return { steps: p.usage.steps, toolCalls: p.usage.toolCalls, last: p.lastActivity };
4525
4772
  }
4526
4773
  function progress(meta) {
@@ -4692,12 +4939,12 @@ function claimGroup(group) {
4692
4939
  try {
4693
4940
  const ids = groupIds(group).sort();
4694
4941
  if (ids.length < 2) return true;
4695
- const dir = path22.join(home(), "notified");
4696
- fs26.mkdirSync(dir, { recursive: true });
4697
- for (const f of fs26.readdirSync(dir)) {
4698
- if (Date.now() - fs26.statSync(path22.join(dir, f)).mtimeMs > 30 * 864e5) fs26.rmSync(path22.join(dir, f), { force: true });
4942
+ const dir = path23.join(home(), "notified");
4943
+ fs27.mkdirSync(dir, { recursive: true });
4944
+ for (const f of fs27.readdirSync(dir)) {
4945
+ if (Date.now() - fs27.statSync(path23.join(dir, f)).mtimeMs > 30 * 864e5) fs27.rmSync(path23.join(dir, f), { force: true });
4699
4946
  }
4700
- fs26.writeFileSync(path22.join(dir, crypto7.createHash("sha1").update(ids.join(",")).digest("hex").slice(0, 16)), "", { flag: "wx" });
4947
+ fs27.writeFileSync(path23.join(dir, crypto7.createHash("sha1").update(ids.join(",")).digest("hex").slice(0, 16)), "", { flag: "wx" });
4701
4948
  return true;
4702
4949
  } catch {
4703
4950
  return false;
@@ -4731,30 +4978,30 @@ function notifyEnded(meta) {
4731
4978
  }
4732
4979
 
4733
4980
  // src/core/templates.ts
4734
- import fs28 from "node:fs";
4735
- import path24 from "node:path";
4981
+ import fs29 from "node:fs";
4982
+ import path25 from "node:path";
4736
4983
 
4737
4984
  // src/core/install.ts
4738
- import fs27 from "node:fs";
4985
+ import fs28 from "node:fs";
4739
4986
  import os11 from "node:os";
4740
- import path23 from "node:path";
4987
+ import path24 from "node:path";
4741
4988
  import { fileURLToPath as fileURLToPath3 } from "node:url";
4742
4989
  var LEGACY = ["pitroom", "opencode-worker"];
4743
4990
  function packageRoot() {
4744
- return path23.resolve(path23.dirname(fileURLToPath3(import.meta.url)), "..");
4991
+ return path24.resolve(path24.dirname(fileURLToPath3(import.meta.url)), "..");
4745
4992
  }
4746
4993
  function skillNames(root = packageRoot()) {
4747
- const dir = path23.join(root, "skills");
4748
- if (!fs27.existsSync(dir)) return [];
4749
- return fs27.readdirSync(dir).filter((d) => fs27.existsSync(path23.join(dir, d, "SKILL.md"))).sort();
4994
+ const dir = path24.join(root, "skills");
4995
+ if (!fs28.existsSync(dir)) return [];
4996
+ return fs28.readdirSync(dir).filter((d) => fs28.existsSync(path24.join(dir, d, "SKILL.md"))).sort();
4750
4997
  }
4751
4998
  function skillTargets() {
4752
4999
  const home3 = os11.homedir();
4753
- const targets = [path23.join(home3, ".agents", "skills")];
4754
- if (fs27.existsSync(path23.join(home3, ".claude"))) targets.push(path23.join(home3, ".claude", "skills"));
5000
+ const targets = [path24.join(home3, ".agents", "skills")];
5001
+ if (fs28.existsSync(path24.join(home3, ".claude"))) targets.push(path24.join(home3, ".claude", "skills"));
4755
5002
  return targets;
4756
5003
  }
4757
- var launcherPath = () => path23.join(os11.homedir(), ".local", "bin", process.platform === "win32" ? "pitroom.cmd" : "pitroom");
5004
+ var launcherPath = () => path24.join(os11.homedir(), ".local", "bin", process.platform === "win32" ? "pitroom.cmd" : "pitroom");
4758
5005
  var LAUNCHER_MARK = "# pitroom launcher";
4759
5006
  function launcherScript(bundle) {
4760
5007
  if (process.platform === "win32") {
@@ -4784,7 +5031,7 @@ exit 127
4784
5031
  function isOurLauncher(file2, root) {
4785
5032
  if (linksInto(file2, root)) return true;
4786
5033
  try {
4787
- const s = fs27.readFileSync(file2, "utf8");
5034
+ const s = fs28.readFileSync(file2, "utf8");
4788
5035
  return s.includes(LAUNCHER_MARK) && s.includes(root);
4789
5036
  } catch {
4790
5037
  return false;
@@ -4792,64 +5039,64 @@ function isOurLauncher(file2, root) {
4792
5039
  }
4793
5040
  function isPitroomLauncher(file2) {
4794
5041
  try {
4795
- return fs27.readFileSync(file2, "utf8").includes(LAUNCHER_MARK);
5042
+ return fs28.readFileSync(file2, "utf8").includes(LAUNCHER_MARK);
4796
5043
  } catch {
4797
5044
  return false;
4798
5045
  }
4799
5046
  }
4800
5047
  function placeLauncher(bundle, root, force) {
4801
5048
  const dest = launcherPath();
4802
- fs27.mkdirSync(path23.dirname(dest), { recursive: true });
4803
- const exists = fs27.lstatSync(dest, { throwIfNoEntry: false });
5049
+ fs28.mkdirSync(path24.dirname(dest), { recursive: true });
5050
+ const exists = fs28.lstatSync(dest, { throwIfNoEntry: false });
4804
5051
  if (exists && !isOurLauncher(dest, root)) {
4805
5052
  if (!force) return `! ${dest} exists and is not Pitroom's launcher; kept (use --force to back it up and replace)`;
4806
- fs27.renameSync(dest, `${dest}.bak-${Date.now()}`);
5053
+ fs28.renameSync(dest, `${dest}.bak-${Date.now()}`);
4807
5054
  } else if (exists) {
4808
- fs27.rmSync(dest, { force: true });
5055
+ fs28.rmSync(dest, { force: true });
4809
5056
  }
4810
- fs27.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
5057
+ fs28.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
4811
5058
  return `\u2714 ${dest} \u2192 launcher for ${bundle} (Node 22.13+)`;
4812
5059
  }
4813
5060
  function linksInto(link, dir) {
4814
- const st = fs27.lstatSync(link, { throwIfNoEntry: false });
5061
+ const st = fs28.lstatSync(link, { throwIfNoEntry: false });
4815
5062
  if (!st?.isSymbolicLink()) return false;
4816
- const target = path23.resolve(path23.dirname(link), fs27.readlinkSync(link));
4817
- const rel = path23.relative(dir, target);
4818
- return !rel.startsWith("..") && !path23.isAbsolute(rel);
5063
+ const target = path24.resolve(path24.dirname(link), fs28.readlinkSync(link));
5064
+ const rel = path24.relative(dir, target);
5065
+ return !rel.startsWith("..") && !path24.isAbsolute(rel);
4819
5066
  }
4820
5067
  function place(src, dest, opts) {
4821
- fs27.mkdirSync(path23.dirname(dest), { recursive: true });
4822
- const st = fs27.lstatSync(dest, { throwIfNoEntry: false });
5068
+ fs28.mkdirSync(path24.dirname(dest), { recursive: true });
5069
+ const st = fs28.lstatSync(dest, { throwIfNoEntry: false });
4823
5070
  if (st && !st.isSymbolicLink()) {
4824
5071
  if (!opts.force) return `! ${dest} exists and is not a link; kept (use --force to back it up and replace)`;
4825
- fs27.renameSync(dest, `${dest}.bak-${Date.now()}`);
5072
+ fs28.renameSync(dest, `${dest}.bak-${Date.now()}`);
4826
5073
  } else if (st) {
4827
- fs27.unlinkSync(dest);
5074
+ fs28.unlinkSync(dest);
4828
5075
  }
4829
- if (opts.copy) fs27.cpSync(src, dest, { recursive: true });
4830
- else fs27.symlinkSync(src, dest, process.platform === "win32" ? "junction" : fs27.statSync(src).isDirectory() ? "dir" : "file");
5076
+ if (opts.copy) fs28.cpSync(src, dest, { recursive: true });
5077
+ else fs28.symlinkSync(src, dest, process.platform === "win32" ? "junction" : fs28.statSync(src).isDirectory() ? "dir" : "file");
4831
5078
  return `\u2714 ${dest} \u2192 ${opts.copy ? "copied" : src}`;
4832
5079
  }
4833
5080
  function install(opts) {
4834
5081
  const root = packageRoot();
4835
5082
  const skills2 = opts.skills === false ? [] : skillNames(root);
4836
- if (opts.skills !== false && !skills2.length) throw new Error(`no skills found under ${path23.join(root, "skills")}`);
5083
+ if (opts.skills !== false && !skills2.length) throw new Error(`no skills found under ${path24.join(root, "skills")}`);
4837
5084
  const out = [];
4838
5085
  for (const base2 of opts.skills === false ? [] : skillTargets()) {
4839
5086
  for (const legacy of LEGACY) {
4840
- const l = path23.join(base2, legacy);
5087
+ const l = path24.join(base2, legacy);
4841
5088
  if (!skills2.includes(legacy) && linksInto(l, root)) {
4842
- fs27.unlinkSync(l);
5089
+ fs28.unlinkSync(l);
4843
5090
  out.push(`\u2714 removed old link ${l}`);
4844
5091
  }
4845
5092
  }
4846
- for (const name of skills2) out.push(place(path23.join(root, "skills", name), path23.join(base2, name), opts));
5093
+ for (const name of skills2) out.push(place(path24.join(root, "skills", name), path24.join(base2, name), opts));
4847
5094
  }
4848
- const bundle = path23.join(root, "dist", "pitroom.mjs");
4849
- if (fs27.existsSync(bundle)) {
5095
+ const bundle = path24.join(root, "dist", "pitroom.mjs");
5096
+ if (fs28.existsSync(bundle)) {
4850
5097
  out.push(placeLauncher(bundle, root, opts.force));
4851
- const onPath = (process.env.PATH ?? "").split(path23.delimiter).some((d) => path23.resolve(d) === path23.dirname(launcherPath()));
4852
- if (!onPath) out.push(`! ${path23.dirname(launcherPath())} is not on PATH; add it, or run ${launcherPath()} directly`);
5098
+ const onPath = (process.env.PATH ?? "").split(path24.delimiter).some((d) => path24.resolve(d) === path24.dirname(launcherPath()));
5099
+ if (!onPath) out.push(`! ${path24.dirname(launcherPath())} is not on PATH; add it, or run ${launcherPath()} directly`);
4853
5100
  }
4854
5101
  return out;
4855
5102
  }
@@ -4857,24 +5104,24 @@ function uninstall() {
4857
5104
  const root = packageRoot();
4858
5105
  const out = [];
4859
5106
  for (const base2 of skillTargets()) {
4860
- if (!fs27.existsSync(base2)) continue;
4861
- for (const name of fs27.readdirSync(base2)) {
4862
- const l = path23.join(base2, name);
5107
+ if (!fs28.existsSync(base2)) continue;
5108
+ for (const name of fs28.readdirSync(base2)) {
5109
+ const l = path24.join(base2, name);
4863
5110
  if (linksInto(l, root)) {
4864
- fs27.unlinkSync(l);
5111
+ fs28.unlinkSync(l);
4865
5112
  out.push(`\u2714 removed ${l}`);
4866
5113
  }
4867
5114
  }
4868
5115
  }
4869
5116
  if (isOurLauncher(launcherPath(), root)) {
4870
- fs27.rmSync(launcherPath(), { force: true });
5117
+ fs28.rmSync(launcherPath(), { force: true });
4871
5118
  out.push(`\u2714 removed ${launcherPath()}`);
4872
5119
  }
4873
5120
  return out.length ? out : ["nothing to remove"];
4874
5121
  }
4875
5122
  function installedSkills() {
4876
5123
  const names = skillNames();
4877
- return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs27.existsSync(path23.join(base2, n, "SKILL.md"))) }));
5124
+ return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs28.existsSync(path24.join(base2, n, "SKILL.md"))) }));
4878
5125
  }
4879
5126
 
4880
5127
  // src/core/templates.ts
@@ -4885,9 +5132,9 @@ var FILES = {
4885
5132
  "code-reviewer": "pitroom-review/code-reviewer.md"
4886
5133
  };
4887
5134
  function loadTemplate(name, root = packageRoot()) {
4888
- const file2 = path24.join(root, "skills", FILES[name]);
4889
- if (!fs28.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
4890
- return fs28.readFileSync(file2, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
5135
+ const file2 = path25.join(root, "skills", FILES[name]);
5136
+ if (!fs29.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
5137
+ return fs29.readFileSync(file2, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
4891
5138
  }
4892
5139
  function fill(template, values) {
4893
5140
  const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
@@ -4897,10 +5144,10 @@ function fill(template, values) {
4897
5144
 
4898
5145
  // src/core/slots.ts
4899
5146
  import crypto8 from "node:crypto";
4900
- import fs29 from "node:fs";
4901
- import path25 from "node:path";
4902
- var slotsDir = () => path25.join(home(), "slots");
4903
- var locksDir = () => path25.join(home(), "locks");
5147
+ import fs30 from "node:fs";
5148
+ import path26 from "node:path";
5149
+ var slotsDir = () => path26.join(home(), "slots");
5150
+ var locksDir = () => path26.join(home(), "locks");
4904
5151
  var STARTUP_GRACE_MS = 3e4;
4905
5152
  function holderActive(runId, self) {
4906
5153
  if (!runId || runId === self) return false;
@@ -4914,23 +5161,23 @@ function holderActive(runId, self) {
4914
5161
  }
4915
5162
  }
4916
5163
  function tryClaim(file2, runId) {
4917
- fs29.mkdirSync(path25.dirname(file2), { recursive: true });
5164
+ fs30.mkdirSync(path26.dirname(file2), { recursive: true });
4918
5165
  try {
4919
- fs29.writeFileSync(file2, runId, { flag: "wx" });
5166
+ fs30.writeFileSync(file2, runId, { flag: "wx" });
4920
5167
  return true;
4921
5168
  } catch (e) {
4922
5169
  if (e.code !== "EEXIST") throw e;
4923
5170
  }
4924
5171
  let owner = "";
4925
5172
  try {
4926
- owner = fs29.readFileSync(file2, "utf8").trim();
5173
+ owner = fs30.readFileSync(file2, "utf8").trim();
4927
5174
  } catch {
4928
5175
  }
4929
5176
  if (owner === runId) return true;
4930
5177
  if (holderActive(owner, runId)) return false;
4931
- fs29.rmSync(file2, { force: true });
5178
+ fs30.rmSync(file2, { force: true });
4932
5179
  try {
4933
- fs29.writeFileSync(file2, runId, { flag: "wx" });
5180
+ fs30.writeFileSync(file2, runId, { flag: "wx" });
4934
5181
  return true;
4935
5182
  } catch {
4936
5183
  return false;
@@ -4939,25 +5186,25 @@ function tryClaim(file2, runId) {
4939
5186
  function releaseIfOwner(file2, runId) {
4940
5187
  if (!file2) return;
4941
5188
  try {
4942
- if (fs29.readFileSync(file2, "utf8").trim() === runId) fs29.rmSync(file2, { force: true });
5189
+ if (fs30.readFileSync(file2, "utf8").trim() === runId) fs30.rmSync(file2, { force: true });
4943
5190
  } catch {
4944
5191
  }
4945
5192
  }
4946
5193
  function tryAcquireSlot(runId, maxParallel) {
4947
5194
  for (let n = 0; n < Math.max(1, maxParallel); n++) {
4948
- const file2 = path25.join(slotsDir(), `slot-${n}`);
5195
+ const file2 = path26.join(slotsDir(), `slot-${n}`);
4949
5196
  if (tryClaim(file2, runId)) return file2;
4950
5197
  }
4951
5198
  return void 0;
4952
5199
  }
4953
5200
  var releaseSlot = (file2, runId) => releaseIfOwner(file2, runId);
4954
- var lockFile = (repoRoot2) => path25.join(locksDir(), `write-${crypto8.createHash("sha1").update(path25.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
5201
+ var lockFile = (repoRoot2) => path26.join(locksDir(), `write-${crypto8.createHash("sha1").update(path26.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
4955
5202
  function acquireWriteLock(repoRoot2, runId) {
4956
5203
  const file2 = lockFile(repoRoot2);
4957
5204
  if (tryClaim(file2, runId)) return;
4958
5205
  let owner = "";
4959
5206
  try {
4960
- owner = fs29.readFileSync(file2, "utf8").trim();
5207
+ owner = fs30.readFileSync(file2, "utf8").trim();
4961
5208
  } catch {
4962
5209
  }
4963
5210
  throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
@@ -5007,13 +5254,14 @@ ${task}`;
5007
5254
  }
5008
5255
 
5009
5256
  // src/core/refs.ts
5010
- import fs30 from "node:fs";
5011
- import path26 from "node:path";
5257
+ import fs31 from "node:fs";
5258
+ import path27 from "node:path";
5012
5259
  var FILE_EXTENSIONS = new Set(
5013
5260
  "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(" ")
5014
5261
  );
5015
5262
  var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
5016
5263
  var MAX_INDEXED = 6e4;
5264
+ var MAX_SKIPPED = 3e5;
5017
5265
  var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
5018
5266
  var REF = new RegExp(
5019
5267
  String.raw`(?<![\w/:.-])(\.{0,2}/?(?:[\w@.+-]+/)*(?:[\w@+-][\w@.+-]*\.[A-Za-z][A-Za-z0-9]{0,7}|${EXTENSIONLESS})):(\d+)(?:[-–](\d+))?`,
@@ -5059,26 +5307,30 @@ function extractRefs(answer) {
5059
5307
  return [...seen.values()];
5060
5308
  }
5061
5309
  function verifyRefs(refs, dirs) {
5062
- const roots = [...new Set(dirs.filter(Boolean).map((d) => path26.resolve(d)))];
5310
+ const roots = [...new Set(dirs.filter(Boolean).map((d) => path27.resolve(d)))];
5063
5311
  const invalid = [];
5064
5312
  const lineCache = /* @__PURE__ */ new Map();
5065
5313
  const load = (file2) => {
5066
5314
  if (!lineCache.has(file2)) {
5067
- const size = fs30.statSync(file2).size;
5068
- const lines = size > MAX_BYTES ? null : fs30.readFileSync(file2, "utf8").split("\n");
5315
+ const size = fs31.statSync(file2).size;
5316
+ const lines = size > MAX_BYTES ? null : fs31.readFileSync(file2, "utf8").split("\n");
5069
5317
  if (lines && lines[lines.length - 1] === "") lines.pop();
5070
5318
  lineCache.set(file2, lines);
5071
5319
  }
5072
5320
  return lineCache.get(file2);
5073
5321
  };
5074
5322
  let index;
5323
+ let skipped;
5075
5324
  const byName = () => index ??= indexFiles(roots);
5325
+ const inSkipped = () => skipped ??= indexFiles(roots, true);
5076
5326
  const candidates = (ref) => {
5077
5327
  const direct = resolve(ref.file, roots);
5078
5328
  if (direct) return [direct];
5079
- if (path26.isAbsolute(ref.file)) return [];
5329
+ if (path27.isAbsolute(ref.file)) return [];
5080
5330
  const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
5081
- return (byName().get(path26.basename(wanted)) ?? []).filter((f) => f.split(path26.sep).join("/").endsWith(`/${wanted}`) || path26.basename(f) === wanted).slice(0, 20);
5331
+ const fits = (f) => path27.basename(f) === wanted || foldersInOrder(f.split(path27.sep).join("/"), wanted);
5332
+ const own = (byName().get(path27.basename(wanted)) ?? []).filter(fits);
5333
+ return (own.length || !isFileName(wanted) ? own : (inSkipped().get(path27.basename(wanted)) ?? []).filter(fits)).slice(0, 20);
5082
5334
  };
5083
5335
  const check = (file2, ref) => {
5084
5336
  const lines = load(file2);
@@ -5093,7 +5345,7 @@ function verifyRefs(refs, dirs) {
5093
5345
  if (!files.length) {
5094
5346
  if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
5095
5347
  total++;
5096
- invalid.push({ ref: ref.text, reason: path26.isAbsolute(ref.file) && !inside2(ref.file, roots) ? "outside the project" : "file not found" });
5348
+ invalid.push({ ref: ref.text, reason: path27.isAbsolute(ref.file) && !inside2(ref.file, roots) ? "outside the project" : "file not found" });
5097
5349
  continue;
5098
5350
  }
5099
5351
  total++;
@@ -5114,39 +5366,54 @@ function enclosedBy(lines, ref) {
5114
5366
  return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
5115
5367
  }
5116
5368
  var isFileName = (name) => {
5117
- const base2 = path26.basename(name);
5369
+ const base2 = path27.basename(name);
5118
5370
  return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
5119
5371
  };
5120
- function indexFiles(roots) {
5372
+ function foldersInOrder(file2, wanted) {
5373
+ const want = wanted.split("/");
5374
+ const have = file2.split("/");
5375
+ if (have.at(-1) !== want.at(-1)) return false;
5376
+ let i = have.length - 2;
5377
+ for (let j = want.length - 2; j >= 0; j--) {
5378
+ while (i >= 0 && have[i] !== want[j]) i--;
5379
+ if (i < 0) return false;
5380
+ i--;
5381
+ }
5382
+ return true;
5383
+ }
5384
+ function indexFiles(roots, skipped = false) {
5121
5385
  const index = /* @__PURE__ */ new Map();
5386
+ const cap = skipped ? MAX_SKIPPED : MAX_INDEXED;
5122
5387
  let count = 0;
5123
- const walk = (dir) => {
5388
+ const walk = (dir, within) => {
5124
5389
  let entries;
5125
5390
  try {
5126
- entries = fs30.readdirSync(dir, { withFileTypes: true });
5391
+ entries = fs31.readdirSync(dir, { withFileTypes: true });
5127
5392
  } catch {
5128
5393
  return;
5129
5394
  }
5130
5395
  for (const e of entries) {
5131
- if (count >= MAX_INDEXED) return;
5396
+ if (count >= cap) return;
5132
5397
  if (e.isDirectory()) {
5133
- if (!SKIP_DIRS2.has(e.name)) walk(path26.join(dir, e.name));
5134
- } else if (e.isFile()) {
5398
+ if (e.name === ".git") continue;
5399
+ const skip = SKIP_DIRS2.has(e.name);
5400
+ if (skipped || !skip) walk(path27.join(dir, e.name), within || skip);
5401
+ } else if (e.isFile() && within === skipped) {
5135
5402
  count++;
5136
5403
  const list2 = index.get(e.name);
5137
- if (list2) list2.push(path26.join(dir, e.name));
5138
- else index.set(e.name, [path26.join(dir, e.name)]);
5404
+ if (list2) list2.push(path27.join(dir, e.name));
5405
+ else index.set(e.name, [path27.join(dir, e.name)]);
5139
5406
  }
5140
5407
  }
5141
5408
  };
5142
- for (const r of roots) walk(r);
5409
+ for (const r of roots) walk(r, false);
5143
5410
  return index;
5144
5411
  }
5145
5412
  function resolve(ref, roots) {
5146
- const candidates = path26.isAbsolute(ref) ? inside2(ref, roots) ? [ref] : [] : roots.map((r) => path26.join(r, ref));
5413
+ const candidates = path27.isAbsolute(ref) ? inside2(ref, roots) ? [ref] : [] : roots.map((r) => path27.join(r, ref));
5147
5414
  return candidates.find((c) => {
5148
5415
  try {
5149
- return fs30.statSync(c).isFile() && inside2(c, roots);
5416
+ return fs31.statSync(c).isFile() && inside2(c, roots);
5150
5417
  } catch {
5151
5418
  return false;
5152
5419
  }
@@ -5154,14 +5421,14 @@ function resolve(ref, roots) {
5154
5421
  }
5155
5422
  function inside2(file2, roots) {
5156
5423
  return roots.some((r) => {
5157
- const rel = path26.relative(r, path26.resolve(file2));
5158
- return rel !== "" && !rel.startsWith("..") && !path26.isAbsolute(rel);
5424
+ const rel = path27.relative(r, path27.resolve(file2));
5425
+ return rel !== "" && !rel.startsWith("..") && !path27.isAbsolute(rel);
5159
5426
  });
5160
5427
  }
5161
5428
  var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5162
5429
 
5163
5430
  // src/core/run.ts
5164
- var VERSION2 = true ? "0.25.0" : "0.0.0-dev";
5431
+ var VERSION2 = true ? "0.27.0" : "0.0.0-dev";
5165
5432
  function planWork(o) {
5166
5433
  if (!o.plan) return { task: o.task.trim(), tier: o.tier };
5167
5434
  if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
@@ -5193,19 +5460,21 @@ function prepareRun(o) {
5193
5460
  parent = readMeta(o.continueFrom);
5194
5461
  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");
5195
5462
  if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
5196
- if (parent.mode === "isolate" && (!parent.worktree || !fs31.existsSync(parent.worktree))) {
5463
+ if (parent.mode === "isolate" && (!parent.worktree || !fs32.existsSync(parent.worktree))) {
5197
5464
  throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
5198
5465
  }
5199
5466
  const ran = parent.ran ?? parent.worker;
5200
5467
  worker = o.model ? { ...ran, model: o.model } : ran;
5201
5468
  fallback = o.noFallback ? [] : parent.fallback.filter((t) => t.backend === ran.backend);
5202
5469
  } else {
5203
- const chain = resolveChain({ worker: o.worker, model: o.model, tier: work.tier, effort: o.effort, noFallback: o.noFallback });
5470
+ const chain = resolveChain({ worker: o.worker, model: o.model, tier: work.tier, effort: o.effort, noFallback: o.noFallback || !!o.fallbackTo });
5204
5471
  ({ worker, fallback } = chain);
5472
+ if (o.fallbackTo && !o.noFallback) fallback = o.fallbackTo.map((w) => resolveChain({ worker: w, noFallback: true }).worker);
5205
5473
  warnings2.push(...chain.warnings);
5206
5474
  if (effective().rankWorkers.value && !o.worker && !o.model && !work.tier && fallback.length) {
5207
5475
  const since2 = Date.now() - 30 * 864e5;
5208
- const ranked = rankChain([worker, ...fallback], (t) => workerRecord(t.backend, t.model, since2));
5476
+ const work2 = o.review ? "review" : o.mode === "read" ? "research" : "change";
5477
+ const ranked = rankChain([worker, ...fallback], (t) => workerRecord(t.backend, t.model, since2, work2), work2 === "change" ? "changes" : work2);
5209
5478
  [worker, ...fallback] = ranked.chain;
5210
5479
  if (ranked.note) warnings2.push(ranked.note);
5211
5480
  }
@@ -5221,22 +5490,22 @@ function prepareRun(o) {
5221
5490
  throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
5222
5491
  }
5223
5492
  const mode = parent?.mode ?? o.mode;
5224
- const dir = canonical(path27.resolve(parent?.dir ?? o.dir));
5225
- if (!fs31.existsSync(dir) || !fs31.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
5493
+ const dir = canonical(path28.resolve(parent?.dir ?? o.dir));
5494
+ if (!fs32.existsSync(dir) || !fs32.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
5226
5495
  const root = repoRoot(dir);
5227
5496
  if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
5228
5497
  if (mode === "write" && !root && !o.allowNonGit) {
5229
5498
  throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
5230
5499
  }
5231
- const files = o.files.map((f) => path27.resolve(f));
5232
- for (const f of files) if (!fs31.existsSync(f)) throw new UserError(`file not found: ${f}`);
5500
+ const files = o.files.map((f) => path28.resolve(f));
5501
+ for (const f of files) if (!fs32.existsSync(f)) throw new UserError(`file not found: ${f}`);
5233
5502
  const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
5234
5503
  const untrusted = [worker, ...fallback].map((t) => getBackend(t.backend)).find(readsOwnSnapshot);
5235
5504
  const snap = mode === "read" && !o.review && !o.inPlace && !parent ? wantSnapshot(readIn2, root, dir) : void 0;
5236
5505
  if (untrusted && mode === "read" && !parent && (o.review || o.inPlace || !root)) {
5237
5506
  warnings2.push(`this read run is not in a snapshot${root ? "" : " (not a git repository)"}, and ${untrusted.name} does not reliably stay read-only: it could change files here`);
5238
5507
  }
5239
- if (parent?.snapshot?.dir && !fs31.existsSync(parent.snapshot.dir)) {
5508
+ if (parent?.snapshot?.dir && !fs32.existsSync(parent.snapshot.dir)) {
5240
5509
  let restored = false;
5241
5510
  if (parent.snapshot.private && root && parent.snapshot.tree) {
5242
5511
  try {
@@ -5293,13 +5562,13 @@ function prepareRun(o) {
5293
5562
  try {
5294
5563
  acquireWriteLock(root, meta.id);
5295
5564
  } catch (e) {
5296
- fs31.rmSync(runDir(meta.id), { recursive: true, force: true });
5565
+ fs32.rmSync(runDir(meta.id), { recursive: true, force: true });
5297
5566
  throw e;
5298
5567
  }
5299
5568
  }
5300
- fs31.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
5569
+ fs32.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
5301
5570
  `);
5302
- if (work.brief) fs31.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
5571
+ if (work.brief) fs32.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
5303
5572
  return meta;
5304
5573
  }
5305
5574
  var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -5396,10 +5665,11 @@ async function execute(meta) {
5396
5665
  }
5397
5666
  }
5398
5667
  if (i === chain.length - 1) break;
5399
- (meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
5668
+ if (why.partial) handOver(meta, backend, target);
5669
+ (meta.attempts ??= []).push({ target: describeTarget(target), error: why.message, ...why.partial ? { partial: true } : {} });
5400
5670
  for (const f of ["events.jsonl", "stderr.log"]) {
5401
5671
  const from = runFile(meta.id, f);
5402
- if (fs31.existsSync(from)) fs31.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
5672
+ if (fs32.existsSync(from)) fs32.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
5403
5673
  }
5404
5674
  writeMeta(meta);
5405
5675
  }
@@ -5422,7 +5692,7 @@ function autoAudit(meta) {
5422
5692
  let rate2 = auditRate(meta);
5423
5693
  if (meta.auditRate === void 0 && effective().auditFocus.value) {
5424
5694
  const ran = meta.ran ?? meta.worker;
5425
- const focus = focusedRate(rate2, meta.task, workerRecord(ran.backend, meta.resolvedModel ?? ran.model, Date.now() - 30 * 864e5));
5695
+ const focus = focusedRate(rate2, meta.task, workerRecord(ran.backend, meta.resolvedModel ?? ran.model, Date.now() - 30 * 864e5, "research"));
5426
5696
  rate2 = focus.rate;
5427
5697
  if (focus.why.length) {
5428
5698
  meta.auditFocus = { rate: rate2, why: focus.why };
@@ -5436,7 +5706,7 @@ function autoAudit(meta) {
5436
5706
  }
5437
5707
  function startAudit(meta, worker) {
5438
5708
  const answer = read2(runFile(meta.id, "summary.md"));
5439
- const auditor = worker ?? pickAuditor(meta);
5709
+ const [auditor, ...others] = worker ? [worker] : auditors(meta);
5440
5710
  if (!auditor) return void 0;
5441
5711
  const a = prepareRun({
5442
5712
  mode: "read",
@@ -5448,8 +5718,9 @@ function startAudit(meta, worker) {
5448
5718
  timeoutSec: Math.min(meta.timeoutSec, 15 * 60),
5449
5719
  allowNonGit: true,
5450
5720
  web: false,
5451
- // never fall back to the worker being audited
5452
- noFallback: true,
5721
+ // the other auditors, never the worker being audited; one you named has none
5722
+ noFallback: !!worker,
5723
+ fallbackTo: others,
5453
5724
  group: meta.group,
5454
5725
  audit: { of: meta.id }
5455
5726
  });
@@ -5495,9 +5766,9 @@ function prepareTree(meta) {
5495
5766
  if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
5496
5767
  if (root && meta.mode === "isolate" && !meta.worktree) {
5497
5768
  meta.baseTree = snapshotTree(root);
5498
- meta.worktree = path27.join(worktreesDir(), meta.id);
5769
+ meta.worktree = path28.join(worktreesDir(), meta.id);
5499
5770
  createIsolatedCopy(root, meta.baseTree, meta.worktree);
5500
- meta.cwd = path27.join(meta.worktree, path27.relative(root, meta.dir));
5771
+ meta.cwd = path28.join(meta.worktree, path28.relative(root, meta.dir));
5501
5772
  const linked = linkIntoWorktree(root, meta.worktree, meta.link);
5502
5773
  if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
5503
5774
  }
@@ -5528,8 +5799,8 @@ function ownSnapshot(meta, root, backend, planned, attemptNo) {
5528
5799
  try {
5529
5800
  const s = readSnapshot(root, planned.own ? `${meta.id}-${attemptNo}` : meta.id);
5530
5801
  meta.snapshot = { dir: s.dir, tree: s.tree, left, private: true };
5531
- meta.cwd = path27.join(s.dir, path27.relative(root, meta.dir));
5532
- if (!fs31.existsSync(meta.cwd)) meta.cwd = s.dir;
5802
+ meta.cwd = path28.join(s.dir, path28.relative(root, meta.dir));
5803
+ if (!fs32.existsSync(meta.cwd)) meta.cwd = s.dir;
5533
5804
  planned.own = { snapshot: { ...meta.snapshot }, cwd: meta.cwd };
5534
5805
  const note = `${backend.name} reads a snapshot of your project of its own (it does not reliably stay read-only)`;
5535
5806
  if (!meta.warnings.some((w) => w.startsWith(note))) {
@@ -5561,8 +5832,8 @@ function sharedSnapshot(meta, root) {
5561
5832
  const s = readSnapshot(root);
5562
5833
  snapshot.dir = s.dir;
5563
5834
  snapshot.tree = s.tree;
5564
- meta.cwd = path27.join(s.dir, path27.relative(root, meta.dir));
5565
- if (!fs31.existsSync(meta.cwd)) meta.cwd = s.dir;
5835
+ meta.cwd = path28.join(s.dir, path28.relative(root, meta.dir));
5836
+ if (!fs32.existsSync(meta.cwd)) meta.cwd = s.dir;
5566
5837
  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`);
5567
5838
  } catch (e) {
5568
5839
  meta.snapshot = void 0;
@@ -5572,7 +5843,9 @@ function sharedSnapshot(meta, root) {
5572
5843
  function attempt(meta, backend, target) {
5573
5844
  const inv = backend.invocation({
5574
5845
  mode: meta.mode,
5575
- prompt: buildPrompt(meta.mode, meta.task, !!meta.parent, !!meta.auditOf),
5846
+ prompt: buildPrompt(meta.mode, meta.handoff ? `${meta.task}
5847
+
5848
+ ${meta.handoff}` : meta.task, !!meta.parent, !!meta.auditOf),
5576
5849
  cwd: meta.cwd,
5577
5850
  model: target.model,
5578
5851
  sessionId: meta.sessionId,
@@ -5588,12 +5861,31 @@ function attempt(meta, backend, target) {
5588
5861
  timeoutSec: meta.timeoutSec
5589
5862
  });
5590
5863
  }
5591
- var read2 = (f) => fs31.existsSync(f) ? fs31.readFileSync(f, "utf8") : "";
5864
+ var read2 = (f) => fs32.existsSync(f) ? fs32.readFileSync(f, "utf8") : "";
5592
5865
  function retryableFailure(meta, backend, res) {
5593
5866
  const run2 = backend.parse(read2(runFile(meta.id, "events.jsonl")));
5594
- if (run2.usage.steps > 0) return void 0;
5595
5867
  const f = backend.failure(run2, read2(runFile(meta.id, "stderr.log")), res.code);
5596
- return f && f.kind !== "other" ? f : void 0;
5868
+ if (!f || f.kind === "other") return void 0;
5869
+ if (run2.usage.steps === 0) return f;
5870
+ return f.kind === "rate-limited" ? { ...f, partial: true } : void 0;
5871
+ }
5872
+ function handOver(meta, backend, target) {
5873
+ const run2 = backend.parse(read2(runFile(meta.id, "events.jsonl")));
5874
+ const used = { ...run2.usage };
5875
+ if (used.cost === void 0) {
5876
+ const price = workerPrice(target.backend, run2.model, target.model);
5877
+ if (price) Object.assign(used, { costEstimate: costAt(used, price), costSource: price.source ?? "config" });
5878
+ }
5879
+ meta.priorUsage = meta.priorUsage ? addUsage(meta.priorUsage, used) : used;
5880
+ if (meta.mode === "read") return;
5881
+ let changed = [];
5882
+ try {
5883
+ const root = meta.mode === "isolate" ? meta.worktree : meta.repoRoot;
5884
+ if (meta.baseTree) changed = diffTrees(root, meta.baseTree, snapshotTree(root, meta.mode === "isolate" ? meta.link : [])).changes.map((c) => c.path);
5885
+ } catch {
5886
+ }
5887
+ const list2 = changed.length ? `: ${changed.slice(0, 30).join(", ")}${changed.length > 30 ? `, and ${changed.length - 30} more` : ""}` : "";
5888
+ meta.handoff = `Another worker started this task and stopped part way, at its usage limit. Its edits are already in the files${list2}. Do not start over: check what it changed against the task, keep what is right, fix what is not, and finish the task.`;
5597
5889
  }
5598
5890
  var HINTS = {
5599
5891
  "model-unavailable": "the model is not available; fix the worker config, pass --worker/--model, or configure fallback workers (see `pitroom doctor`)",
@@ -5608,12 +5900,12 @@ function finalize(meta, res) {
5608
5900
  if (meta.snapshot?.dir && meta.repoRoot) run2.finalText = backToProject(run2.finalText, meta.snapshot.dir, meta.repoRoot);
5609
5901
  meta.sessionId = run2.sessionId ?? meta.sessionId;
5610
5902
  meta.usage = run2.usage;
5611
- fs31.writeFileSync(runFile(meta.id, "summary.md"), `${run2.finalText}
5903
+ fs32.writeFileSync(runFile(meta.id, "summary.md"), `${run2.finalText}
5612
5904
  `);
5613
5905
  if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run2.finalText);
5614
5906
  if (meta.reviewOf) {
5615
5907
  meta.verdict = parseVerdict(run2.finalText);
5616
- if (meta.packageFile) fs31.rmSync(meta.packageFile, { force: true });
5908
+ if (meta.packageFile) fs32.rmSync(meta.packageFile, { force: true });
5617
5909
  }
5618
5910
  for (const w of backend.warnings?.(run2, read2(runFile(meta.id, "stderr.log"))) ?? []) {
5619
5911
  if (!meta.warnings.includes(w)) meta.warnings.push(w);
@@ -5659,6 +5951,7 @@ function finalize(meta, res) {
5659
5951
  meta.usage.costSource = price.source ?? "config";
5660
5952
  }
5661
5953
  }
5954
+ if (meta.priorUsage && meta.usage) meta.usage = addUsage(meta.priorUsage, meta.usage);
5662
5955
  meta.returnedTokens = estimateTokens(formatReport(meta, run2.finalText));
5663
5956
  meta.savedUsd = meta.auditOf ? 0 : savedUsd(meta.usage, meta.returnedTokens);
5664
5957
  writeMeta(meta);
@@ -5677,16 +5970,16 @@ function captureChanges(meta) {
5677
5970
  const d = diffTrees(target, meta.baseTree, meta.afterTree);
5678
5971
  meta.changes = d.changes;
5679
5972
  meta.stats = d.stats;
5680
- fs31.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
5973
+ fs32.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
5681
5974
  }
5682
5975
  function verifyPath() {
5683
- const extra = [path27.dirname(process.execPath), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
5684
- const parts = (process.env.PATH ?? "").split(path27.delimiter).filter(Boolean);
5685
- for (const d of process.platform === "win32" ? [path27.dirname(process.execPath)] : extra) if (!parts.includes(d)) parts.push(d);
5686
- return parts.join(path27.delimiter);
5976
+ const extra = [path28.dirname(process.execPath), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
5977
+ const parts = (process.env.PATH ?? "").split(path28.delimiter).filter(Boolean);
5978
+ for (const d of process.platform === "win32" ? [path28.dirname(process.execPath)] : extra) if (!parts.includes(d)) parts.push(d);
5979
+ return parts.join(path28.delimiter);
5687
5980
  }
5688
5981
  function runVerify(meta) {
5689
- const r = spawnSync9(meta.verify, {
5982
+ const r = spawnSync10(meta.verify, {
5690
5983
  cwd: meta.cwd,
5691
5984
  shell: true,
5692
5985
  encoding: "utf8",
@@ -5696,7 +5989,7 @@ function runVerify(meta) {
5696
5989
  env: { ...process.env, PATH: verifyPath(), PWD: meta.cwd, PITROOM_ACTIVE: "1" }
5697
5990
  });
5698
5991
  const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
5699
- fs31.writeFileSync(runFile(meta.id, "verify.log"), output);
5992
+ fs32.writeFileSync(runFile(meta.id, "verify.log"), output);
5700
5993
  const notFound = process.platform === "win32" && r.status === 1 && /is not recognized as an internal or external command/.test(output);
5701
5994
  return { ok: r.status === 0, code: notFound ? 127 : r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
5702
5995
  }
@@ -5751,12 +6044,12 @@ function discardRun(meta) {
5751
6044
  return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
5752
6045
  }
5753
6046
  function cleanupWorktree(meta) {
5754
- if (meta.worktree && fs31.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
6047
+ if (meta.worktree && fs32.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
5755
6048
  }
5756
6049
 
5757
6050
  // src/cli/mcp-extras.ts
5758
- import fs32 from "node:fs";
5759
- import path28 from "node:path";
6051
+ import fs33 from "node:fs";
6052
+ import path29 from "node:path";
5760
6053
 
5761
6054
  // src/cli/mcp-support.ts
5762
6055
  import { spawn as spawn5 } from "node:child_process";
@@ -6041,7 +6334,7 @@ function skillPrompts() {
6041
6334
  const root = packageRoot();
6042
6335
  for (const name of skillNames(root)) {
6043
6336
  try {
6044
- const text = fs32.readFileSync(path28.join(root, "skills", name, "SKILL.md"), "utf8");
6337
+ const text = fs33.readFileSync(path29.join(root, "skills", name, "SKILL.md"), "utf8");
6045
6338
  const front = /^---\n([\s\S]*?)\n---\n/.exec(text);
6046
6339
  const body = (front ? text.slice(front[0].length) : text).trim();
6047
6340
  const description = /^description:\s*(.+)$/m.exec(front?.[1] ?? "")?.[1]?.trim() ?? `The ${name} skill.`;
@@ -6103,6 +6396,7 @@ var TOPICS = {
6103
6396
  stats: ["since"],
6104
6397
  savings: ["since", "perModel"],
6105
6398
  models: ["worker", "all"],
6399
+ prices: ["worker"],
6106
6400
  cooldown: [],
6107
6401
  config: [],
6108
6402
  doctor: []
@@ -6128,6 +6422,8 @@ function infoArgs(topic, a) {
6128
6422
  return { args: ["history", "stats", ...since(a)] };
6129
6423
  case "savings":
6130
6424
  return { args: ["savings", ...since(a), ...bool(a, "perModel") ? ["--models"] : []] };
6425
+ case "prices":
6426
+ return { args: ["prices", ...worker ? ["--", worker] : []] };
6131
6427
  case "models":
6132
6428
  return { args: ["models", ...worker ? [worker] : [], ...bool(a, "all") ? ["--all"] : []] };
6133
6429
  case "doctor":
@@ -6210,7 +6506,7 @@ var TOOLS2 = [
6210
6506
  {
6211
6507
  name: "pitroom_info",
6212
6508
  title: "Pitroom information",
6213
- description: "Reports, changing nothing. topic: runs (latest runs; running, group), history (search earlier answers before asking again; text, state, model, since, limit), stats (success rate, time, tokens, audits per worker; since), savings (since, perModel), models (a backend's models and costs; worker, all), cooldown (rate-limited models being skipped), config, doctor (setup check).",
6509
+ description: "Reports, changing nothing. topic: runs (latest runs; running, group), history (search earlier answers before asking again; text, state, model, since, limit), stats (success rate, time, tokens, audits per worker; since), savings (since, perModel), models (a backend's models and costs; worker, all), prices (USD per 1M tokens: the primary the savings are measured against, and each worker in use or the one named; worker), cooldown (rate-limited models being skipped), config, doctor (setup check).",
6214
6510
  inputSchema: {
6215
6511
  type: "object",
6216
6512
  properties: {
@@ -6559,22 +6855,22 @@ async function serveMcp() {
6559
6855
 
6560
6856
  // src/cli/mcp-http.ts
6561
6857
  import crypto10 from "node:crypto";
6562
- import fs34 from "node:fs";
6858
+ import fs35 from "node:fs";
6563
6859
  import http2 from "node:http";
6564
- import path30 from "node:path";
6860
+ import path31 from "node:path";
6565
6861
 
6566
6862
  // src/core/dash.ts
6567
6863
  import { spawn as spawn6 } from "node:child_process";
6568
6864
  import crypto9 from "node:crypto";
6569
- import fs33 from "node:fs";
6865
+ import fs34 from "node:fs";
6570
6866
  import http from "node:http";
6571
- import path29 from "node:path";
6867
+ import path30 from "node:path";
6572
6868
  import { fileURLToPath as fileURLToPath4 } from "node:url";
6573
6869
 
6574
6870
  // src/core/file-diff.ts
6575
6871
  var PREVIEW_LINES = 300;
6576
6872
  function gitPath(value, prefix = true) {
6577
- let path38 = value;
6873
+ let path39 = value;
6578
6874
  if (value.startsWith('"') && value.endsWith('"')) {
6579
6875
  const bytes = [];
6580
6876
  const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
@@ -6582,9 +6878,9 @@ function gitPath(value, prefix = true) {
6582
6878
  if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
6583
6879
  else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
6584
6880
  }
6585
- path38 = new TextDecoder().decode(new Uint8Array(bytes));
6881
+ path39 = new TextDecoder().decode(new Uint8Array(bytes));
6586
6882
  }
6587
- return prefix ? path38.replace(/^[ab]\//, "") : path38;
6883
+ return prefix ? path39.replace(/^[ab]\//, "") : path39;
6588
6884
  }
6589
6885
  function headerPath(header) {
6590
6886
  const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
@@ -6753,6 +7049,7 @@ function toRun(m) {
6753
7049
  }
6754
7050
  function dashState(opts = {}) {
6755
7051
  const limit = Math.min(Math.max(opts.limit ?? 40, 1), 200);
7052
+ const scanned = [];
6756
7053
  const metas = [];
6757
7054
  const groups = /* @__PURE__ */ new Set();
6758
7055
  for (const id2 of listRunIds().slice(-SCAN).reverse()) {
@@ -6762,6 +7059,7 @@ function dashState(opts = {}) {
6762
7059
  } catch {
6763
7060
  continue;
6764
7061
  }
7062
+ scanned.push(m);
6765
7063
  if (m.group) groups.add(m.group);
6766
7064
  if (opts.group && m.group !== opts.group) continue;
6767
7065
  metas.push(m);
@@ -6769,6 +7067,7 @@ function dashState(opts = {}) {
6769
7067
  const whole = new Set(metas.slice(0, limit).flatMap((m) => m.group ? [m.group] : []));
6770
7068
  const runs = metas.filter((m, i) => i < limit || m.group && whole.has(m.group)).map(toRun);
6771
7069
  const now = Date.now();
7070
+ settleLedger(scanned);
6772
7071
  const ledger = readLedger();
6773
7072
  const within = (at, days2) => !!at && Date.parse(at) >= now - days2 * DAY_MS;
6774
7073
  const per = (f) => Object.fromEntries(Object.entries(PERIODS).map(([p, days2]) => [p, f(days2)]));
@@ -6862,11 +7161,11 @@ var assetsDir = () => fileURLToPath4(new URL("./ui/", import.meta.url));
6862
7161
  var assetCache = /* @__PURE__ */ new Map();
6863
7162
  function readAsset(route) {
6864
7163
  try {
6865
- const file2 = path29.join(assetsDir(), path29.basename(route));
6866
- const { mtimeMs } = fs33.statSync(file2);
7164
+ const file2 = path30.join(assetsDir(), path30.basename(route));
7165
+ const { mtimeMs } = fs34.statSync(file2);
6867
7166
  const hit = assetCache.get(route);
6868
7167
  if (hit && hit.mtimeMs === mtimeMs) return hit.data;
6869
- const data = fs33.readFileSync(file2);
7168
+ const data = fs34.readFileSync(file2);
6870
7169
  assetCache.set(route, { mtimeMs, data });
6871
7170
  return data;
6872
7171
  } catch {
@@ -6995,7 +7294,7 @@ function startDash(opts) {
6995
7294
  });
6996
7295
  });
6997
7296
  }
6998
- var registryFile = () => path29.join(home(), "dash.json");
7297
+ var registryFile = () => path30.join(home(), "dash.json");
6999
7298
  async function ping(port) {
7000
7299
  try {
7001
7300
  const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
@@ -7007,12 +7306,12 @@ async function ping(port) {
7007
7306
  async function runningDash() {
7008
7307
  let r;
7009
7308
  try {
7010
- r = JSON.parse(fs33.readFileSync(registryFile(), "utf8"));
7309
+ r = JSON.parse(fs34.readFileSync(registryFile(), "utf8"));
7011
7310
  } catch {
7012
7311
  return void 0;
7013
7312
  }
7014
7313
  if (!isAlive(r.pid)) {
7015
- fs33.rmSync(registryFile(), { force: true });
7314
+ fs34.rmSync(registryFile(), { force: true });
7016
7315
  return void 0;
7017
7316
  }
7018
7317
  return await ping(r.port) ? { ...r, url: `http://127.0.0.1:${r.port}/` } : void 0;
@@ -7029,7 +7328,7 @@ async function dashCommand(o) {
7029
7328
  if (o.stop) {
7030
7329
  if (!existing) throw new UserError("pitroom dash is not running");
7031
7330
  process.kill(existing.pid, "SIGTERM");
7032
- fs33.rmSync(registryFile(), { force: true });
7331
+ fs34.rmSync(registryFile(), { force: true });
7033
7332
  o.log(`stopped pitroom dash (${existing.url})`);
7034
7333
  return 0;
7035
7334
  }
@@ -7058,6 +7357,7 @@ async function dashCommand(o) {
7058
7357
  throw new UserError("pitroom dash did not start (is the port taken? try --port 0)");
7059
7358
  }
7060
7359
  importRuns();
7360
+ settleLedger();
7061
7361
  const explicit = o.port !== void 0;
7062
7362
  let dash;
7063
7363
  try {
@@ -7066,9 +7366,9 @@ async function dashCommand(o) {
7066
7366
  if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
7067
7367
  dash = await startDash({ port: 0, idleMs: o.idleMs });
7068
7368
  }
7069
- fs33.mkdirSync(home(), { recursive: true });
7369
+ fs34.mkdirSync(home(), { recursive: true });
7070
7370
  const tmp = `${registryFile()}.${process.pid}.tmp`;
7071
- fs33.writeFileSync(tmp, JSON.stringify({ pid: process.pid, port: dash.port }));
7371
+ fs34.writeFileSync(tmp, JSON.stringify({ pid: process.pid, port: dash.port }));
7072
7372
  renameOver(tmp, registryFile());
7073
7373
  const stop = () => void dash.close();
7074
7374
  process.once("SIGINT", stop);
@@ -7078,7 +7378,7 @@ async function dashCommand(o) {
7078
7378
  if (o.open) openBrowser(dash.url);
7079
7379
  await dash.closed;
7080
7380
  try {
7081
- if (JSON.parse(fs33.readFileSync(registryFile(), "utf8")).pid === process.pid) fs33.rmSync(registryFile(), { force: true });
7381
+ if (JSON.parse(fs34.readFileSync(registryFile(), "utf8")).pid === process.pid) fs34.rmSync(registryFile(), { force: true });
7082
7382
  } catch {
7083
7383
  }
7084
7384
  return 0;
@@ -7090,10 +7390,10 @@ var MAX_BODY = 4e6;
7090
7390
  var MAX_SESSIONS = 64;
7091
7391
  var IDLE_MS = 60 * 6e4;
7092
7392
  var PATH = "/mcp";
7093
- var tokenFile = () => path30.join(home(), "mcp-token");
7393
+ var tokenFile = () => path31.join(home(), "mcp-token");
7094
7394
  var readToken = (file2) => {
7095
7395
  try {
7096
- const kept = fs34.readFileSync(file2, "utf8").trim();
7396
+ const kept = fs35.readFileSync(file2, "utf8").trim();
7097
7397
  return kept.length >= 16 ? kept : void 0;
7098
7398
  } catch {
7099
7399
  return void 0;
@@ -7108,10 +7408,10 @@ function mcpToken() {
7108
7408
  const file2 = tokenFile();
7109
7409
  let token = readToken(file2);
7110
7410
  if (!token) {
7111
- fs34.mkdirSync(path30.dirname(file2), { recursive: true });
7411
+ fs35.mkdirSync(path31.dirname(file2), { recursive: true });
7112
7412
  token = crypto10.randomBytes(32).toString("hex");
7113
7413
  try {
7114
- fs34.writeFileSync(file2, `${token}
7414
+ fs35.writeFileSync(file2, `${token}
7115
7415
  `, { mode: 384, flag: "wx" });
7116
7416
  } catch (e) {
7117
7417
  if (e.code !== "EEXIST") throw e;
@@ -7120,7 +7420,7 @@ function mcpToken() {
7120
7420
  }
7121
7421
  }
7122
7422
  try {
7123
- fs34.chmodSync(file2, 384);
7423
+ fs35.chmodSync(file2, 384);
7124
7424
  } catch {
7125
7425
  }
7126
7426
  return { token, from: file2 };
@@ -7301,17 +7601,17 @@ async function serveMcpHttp(opts) {
7301
7601
  }
7302
7602
 
7303
7603
  // src/cli/commands.ts
7304
- import { spawnSync as spawnSync12 } from "node:child_process";
7305
- import fs41 from "node:fs";
7306
- import path36 from "node:path";
7604
+ import { spawnSync as spawnSync13 } from "node:child_process";
7605
+ import fs42 from "node:fs";
7606
+ import path37 from "node:path";
7307
7607
  import { fileURLToPath as fileURLToPath5 } from "node:url";
7308
7608
 
7309
7609
  // src/core/init.ts
7310
- import fs35 from "node:fs";
7311
- import path31 from "node:path";
7610
+ import fs36 from "node:fs";
7611
+ import path32 from "node:path";
7312
7612
  var FREE = /-free$/;
7313
7613
  var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
7314
- var isFound = (binary8) => path31.isAbsolute(binary8) && fs35.existsSync(binary8);
7614
+ var isFound = (binary8) => path32.isAbsolute(binary8) && fs36.existsSync(binary8);
7315
7615
  function planInit(opts = {}) {
7316
7616
  const file2 = configPath();
7317
7617
  const workers = backendIds().map((id2) => {
@@ -7357,15 +7657,15 @@ function planInit(opts = {}) {
7357
7657
  if (Object.keys(tiers).length > 1) config.tiers = tiers;
7358
7658
  if (!workers.some((w) => w.found)) blocked = "no worker CLI found: install OpenCode (https://opencode.ai), Codex CLI, Claude Code or Gemini CLI first";
7359
7659
  else if (!blocked && !Object.keys(config).length) notes.push("nothing to propose: the default model and the single worker need no config");
7360
- return { path: file2, exists: fs35.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
7660
+ return { path: file2, exists: fs36.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
7361
7661
  }
7362
7662
  function writeInit(plan, force) {
7363
7663
  if (plan.blocked) throw new UserError(plan.blocked);
7364
7664
  if (!Object.keys(plan.config).length) throw new UserError("nothing to write: the proposal is empty");
7365
- if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${path31.basename(plan.path)}.bak)`);
7366
- fs35.mkdirSync(path31.dirname(plan.path), { recursive: true });
7367
- if (plan.exists) fs35.copyFileSync(plan.path, `${plan.path}.bak`);
7368
- fs35.writeFileSync(plan.path, `${JSON.stringify(plan.config, null, 2)}
7665
+ if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${path32.basename(plan.path)}.bak)`);
7666
+ fs36.mkdirSync(path32.dirname(plan.path), { recursive: true });
7667
+ if (plan.exists) fs36.copyFileSync(plan.path, `${plan.path}.bak`);
7668
+ fs36.writeFileSync(plan.path, `${JSON.stringify(plan.config, null, 2)}
7369
7669
  `);
7370
7670
  return plan.path;
7371
7671
  }
@@ -7385,18 +7685,18 @@ function formatInit(plan, written) {
7385
7685
  }
7386
7686
 
7387
7687
  // src/core/mcp-install.ts
7388
- import { spawnSync as spawnSync10 } from "node:child_process";
7389
- import fs36 from "node:fs";
7688
+ import { spawnSync as spawnSync11 } from "node:child_process";
7689
+ import fs37 from "node:fs";
7390
7690
  import os12 from "node:os";
7391
- import path32 from "node:path";
7691
+ import path33 from "node:path";
7392
7692
  var CLIENT_IDS = ["claude-code", "codex", "gemini", "cursor", "claude-desktop"];
7393
7693
  var NAME = "pitroom";
7394
7694
  function mcpCommand() {
7395
7695
  const launcher = launcherPath();
7396
7696
  if (isPitroomLauncher(launcher)) return { command: launcher, args: ["mcp"] };
7397
- for (const dir of (process.env.PATH ?? "").split(path32.delimiter).filter(Boolean)) {
7398
- const p = path32.join(dir, process.platform === "win32" ? "pitroom.cmd" : "pitroom");
7399
- if (fs36.existsSync(p)) return { command: path32.resolve(p), args: ["mcp"] };
7697
+ for (const dir of (process.env.PATH ?? "").split(path33.delimiter).filter(Boolean)) {
7698
+ const p = path33.join(dir, process.platform === "win32" ? "pitroom.cmd" : "pitroom");
7699
+ if (fs37.existsSync(p)) return { command: path33.resolve(p), args: ["mcp"] };
7400
7700
  }
7401
7701
  return { command: process.execPath, args: [process.argv[1] ?? "pitroom", "mcp"] };
7402
7702
  }
@@ -7406,13 +7706,13 @@ var same = (a, b) => {
7406
7706
  };
7407
7707
  function run(backendId, args) {
7408
7708
  const { command, prefix } = resolveCommand(getBackend(backendId).binary());
7409
- const r = spawnSync10(command, [...prefix, ...args], { encoding: "utf8", timeout: 6e4, stdio: ["ignore", "pipe", "pipe"] });
7709
+ const r = spawnSync11(command, [...prefix, ...args], { encoding: "utf8", timeout: 6e4, stdio: ["ignore", "pipe", "pipe"] });
7410
7710
  return { ok: r.status === 0 && !r.error, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
7411
7711
  }
7412
7712
  var runnable = (backendId) => run(backendId, ["--version"]).ok;
7413
7713
  var readJson = (file2) => {
7414
7714
  try {
7415
- return JSON.parse(fs36.readFileSync(file2, "utf8"));
7715
+ return JSON.parse(fs37.readFileSync(file2, "utf8"));
7416
7716
  } catch {
7417
7717
  return void 0;
7418
7718
  }
@@ -7422,7 +7722,7 @@ var jsonState = (file2, c) => {
7422
7722
  return entry === void 0 ? "absent" : same(c, entry) ? "same" : "different";
7423
7723
  };
7424
7724
  var home2 = () => os12.homedir();
7425
- var geminiDir = () => path32.join(process.env.GEMINI_CLI_HOME ?? home2(), ".gemini");
7725
+ var geminiDir = () => path33.join(process.env.GEMINI_CLI_HOME ?? home2(), ".gemini");
7426
7726
  function cliClient(id2, backend, name, where, o) {
7427
7727
  return {
7428
7728
  id: id2,
@@ -7445,43 +7745,43 @@ function jsonClient(id2, name, file2, dir) {
7445
7745
  return {
7446
7746
  id: id2,
7447
7747
  name,
7448
- found: () => fs36.existsSync(dir()),
7748
+ found: () => fs37.existsSync(dir()),
7449
7749
  state: (c) => jsonState(file2(), c),
7450
7750
  add: (c) => {
7451
7751
  let config = {};
7452
- if (fs36.existsSync(file2())) {
7752
+ if (fs37.existsSync(file2())) {
7453
7753
  config = readJson(file2());
7454
7754
  if (!config || typeof config !== "object" || Array.isArray(config)) return { ok: false, message: `${rel()} is not valid JSON: left alone` };
7455
7755
  const backup = `${file2()}.bak-pitroom`;
7456
- if (!fs36.existsSync(backup)) fs36.copyFileSync(file2(), backup);
7756
+ if (!fs37.existsSync(backup)) fs37.copyFileSync(file2(), backup);
7457
7757
  }
7458
7758
  config.mcpServers = { ...config.mcpServers ?? {}, [NAME]: { command: c.command, args: c.args } };
7459
- fs36.mkdirSync(path32.dirname(file2()), { recursive: true });
7759
+ fs37.mkdirSync(path33.dirname(file2()), { recursive: true });
7460
7760
  const tmp = `${file2()}.${process.pid}.tmp`;
7461
- fs36.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
7761
+ fs37.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
7462
7762
  `);
7463
- fs36.renameSync(tmp, file2());
7763
+ fs37.renameSync(tmp, file2());
7464
7764
  return { ok: true, message: rel() };
7465
7765
  },
7466
7766
  remove: () => {
7467
7767
  const config = readJson(file2());
7468
7768
  if (!config?.mcpServers?.[NAME]) return { ok: true, message: rel() };
7469
7769
  delete config.mcpServers[NAME];
7470
- fs36.writeFileSync(file2(), `${JSON.stringify(config, null, 2)}
7770
+ fs37.writeFileSync(file2(), `${JSON.stringify(config, null, 2)}
7471
7771
  `);
7472
7772
  return { ok: true, message: rel() };
7473
7773
  }
7474
7774
  };
7475
7775
  }
7476
7776
  function desktopFile() {
7477
- if (process.platform === "darwin") return path32.join(home2(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
7478
- if (process.platform === "win32") return path32.join(process.env.APPDATA ?? path32.join(home2(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
7479
- return path32.join(process.env.XDG_CONFIG_HOME ?? path32.join(home2(), ".config"), "Claude", "claude_desktop_config.json");
7777
+ if (process.platform === "darwin") return path33.join(home2(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
7778
+ if (process.platform === "win32") return path33.join(process.env.APPDATA ?? path33.join(home2(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
7779
+ return path33.join(process.env.XDG_CONFIG_HOME ?? path33.join(home2(), ".config"), "Claude", "claude_desktop_config.json");
7480
7780
  }
7481
7781
  function codexState(c) {
7482
7782
  let text;
7483
7783
  try {
7484
- text = fs36.readFileSync(path32.join(process.env.CODEX_HOME ?? path32.join(home2(), ".codex"), "config.toml"), "utf8");
7784
+ text = fs37.readFileSync(path33.join(process.env.CODEX_HOME ?? path33.join(home2(), ".codex"), "config.toml"), "utf8");
7485
7785
  } catch {
7486
7786
  return "absent";
7487
7787
  }
@@ -7499,7 +7799,7 @@ function codexState(c) {
7499
7799
  function clients() {
7500
7800
  return [
7501
7801
  cliClient("claude-code", "claude", "Claude Code", "user settings (~/.claude.json)", {
7502
- state: (c) => jsonState(path32.join(home2(), ".claude.json"), c),
7802
+ state: (c) => jsonState(path33.join(home2(), ".claude.json"), c),
7503
7803
  add: (c) => ["mcp", "add", "--scope", "user", NAME, "--", c.command, ...c.args],
7504
7804
  remove: () => ["mcp", "remove", NAME, "--scope", "user"]
7505
7805
  }),
@@ -7509,12 +7809,12 @@ function clients() {
7509
7809
  remove: () => ["mcp", "remove", NAME]
7510
7810
  }),
7511
7811
  cliClient("gemini", "gemini", "Gemini CLI", "user settings (~/.gemini/settings.json)", {
7512
- state: (c) => jsonState(path32.join(geminiDir(), "settings.json"), c),
7812
+ state: (c) => jsonState(path33.join(geminiDir(), "settings.json"), c),
7513
7813
  add: (c) => ["mcp", "add", "--scope", "user", NAME, c.command, ...c.args],
7514
7814
  remove: () => ["mcp", "remove", "--scope", "user", NAME]
7515
7815
  }),
7516
- jsonClient("cursor", "Cursor", () => path32.join(home2(), ".cursor", "mcp.json"), () => path32.join(home2(), ".cursor")),
7517
- jsonClient("claude-desktop", "Claude Desktop", desktopFile, () => path32.dirname(desktopFile()))
7816
+ jsonClient("cursor", "Cursor", () => path33.join(home2(), ".cursor", "mcp.json"), () => path33.join(home2(), ".cursor")),
7817
+ jsonClient("claude-desktop", "Claude Desktop", desktopFile, () => path33.dirname(desktopFile()))
7518
7818
  ];
7519
7819
  }
7520
7820
  function attempt2(f) {
@@ -7585,16 +7885,16 @@ var parseClients = (value) => {
7585
7885
 
7586
7886
  // src/core/plan-status.ts
7587
7887
  import crypto11 from "node:crypto";
7588
- import fs37 from "node:fs";
7589
- import path33 from "node:path";
7888
+ import fs38 from "node:fs";
7889
+ import path34 from "node:path";
7590
7890
  function notesFile(plan) {
7591
7891
  const id2 = crypto11.createHash("sha1").update(plan.file).digest("hex").slice(0, 12);
7592
- return path33.join(home(), "plans", id2, "notes.md");
7892
+ return path34.join(home(), "plans", id2, "notes.md");
7593
7893
  }
7594
7894
  function readNotes(plan) {
7595
7895
  const f = notesFile(plan);
7596
- if (!fs37.existsSync(f)) return [];
7597
- return fs37.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
7896
+ if (!fs38.existsSync(f)) return [];
7897
+ return fs38.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
7598
7898
  const m = /^(\d{4}-\d\d-\d\d \d\d:\d\d) (.*)$/.exec(l);
7599
7899
  return m ? { at: m[1], text: m[2] } : { at: "", text: l };
7600
7900
  });
@@ -7602,11 +7902,11 @@ function readNotes(plan) {
7602
7902
  function addNote(planFile, text) {
7603
7903
  const plan = loadPlan(planFile);
7604
7904
  const f = notesFile(plan);
7605
- fs37.mkdirSync(path33.dirname(f), { recursive: true });
7606
- if (!fs37.existsSync(f)) fs37.writeFileSync(f, `# plan: ${plan.file}
7905
+ fs38.mkdirSync(path34.dirname(f), { recursive: true });
7906
+ if (!fs38.existsSync(f)) fs38.writeFileSync(f, `# plan: ${plan.file}
7607
7907
  `);
7608
7908
  const at = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
7609
- fs37.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
7909
+ fs38.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
7610
7910
  `);
7611
7911
  return f;
7612
7912
  }
@@ -7674,8 +7974,8 @@ ${rulings}`,
7674
7974
 
7675
7975
  // src/core/cache.ts
7676
7976
  import crypto12 from "node:crypto";
7677
- import fs38 from "node:fs";
7678
- import path34 from "node:path";
7977
+ import fs39 from "node:fs";
7978
+ import path35 from "node:path";
7679
7979
  function preferredReadsOwnSnapshot(o) {
7680
7980
  try {
7681
7981
  const preferred = effective().worker.value;
@@ -7688,20 +7988,20 @@ var SCAN2 = 500;
7688
7988
  function cacheKey(o) {
7689
7989
  if (o.verify || o.mode !== "read" || o.continueFrom || o.web || o.plan || o.review || o.audit || o.auditRate === 1) return void 0;
7690
7990
  if (!(effective().cacheDays.value > 0)) return void 0;
7691
- const dir = canonical(path34.resolve(o.dir));
7991
+ const dir = canonical(path35.resolve(o.dir));
7692
7992
  const root = repoRoot(dir);
7693
7993
  if (!root) return void 0;
7694
7994
  try {
7695
7995
  const tree = snapshotTree(root);
7696
7996
  const head = commitOf(root, "HEAD") ?? "";
7697
7997
  const files = o.files.map((f) => {
7698
- const abs = path34.resolve(f);
7699
- return [abs, crypto12.createHash("sha256").update(fs38.readFileSync(abs)).digest("hex")];
7998
+ const abs = path35.resolve(f);
7999
+ return [abs, crypto12.createHash("sha256").update(fs39.readFileSync(abs)).digest("hex")];
7700
8000
  });
7701
8001
  const question = o.task.replace(/\s+/g, " ").trim();
7702
8002
  const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
7703
8003
  const where = !o.inPlace && (preferredReadsOwnSnapshot(o) || wantSnapshot(readIn2, root, dir)) ? "snapshot" : "project";
7704
- const key = crypto12.createHash("sha256").update(JSON.stringify([question, path34.relative(root, dir), files, where])).digest("hex");
8004
+ const key = crypto12.createHash("sha256").update(JSON.stringify([question, path35.relative(root, dir), files, where])).digest("hex");
7705
8005
  return { key, state: `${head}:${tree}`, where };
7706
8006
  } catch {
7707
8007
  return void 0;
@@ -7744,13 +8044,13 @@ function ago(iso, now = Date.now()) {
7744
8044
  var cachedNote = (m) => `pitroom \u27F2 cached answer \xB7 the same question on the same code as run ${m.id} (${ago(m.endedAt ?? m.startedAt)}) \xB7 no worker ran \xB7 --fresh asks one again`;
7745
8045
 
7746
8046
  // src/core/eval.ts
7747
- import { spawnSync as spawnSync11 } from "node:child_process";
7748
- import fs39 from "node:fs";
8047
+ import { spawnSync as spawnSync12 } from "node:child_process";
8048
+ import fs40 from "node:fs";
7749
8049
  var KINDS = ["definition", "count", "set"];
7750
8050
  function loadEval(file2) {
7751
8051
  let raw;
7752
8052
  try {
7753
- raw = JSON.parse(fs39.readFileSync(file2, "utf8"));
8053
+ raw = JSON.parse(fs40.readFileSync(file2, "utf8"));
7754
8054
  } catch (e) {
7755
8055
  throw new UserError(`cannot read ${file2}: ${e.message}`);
7756
8056
  }
@@ -7777,7 +8077,7 @@ function loadEval(file2) {
7777
8077
  }
7778
8078
  var clean = (p) => p.replace(/\\/g, "/").replace(/^\.?\//, "");
7779
8079
  function gitGrep(root, flags, t) {
7780
- const r = spawnSync11("git", ["-C", root, "grep", ...flags, t.regex ? "-E" : "-F", ...t.word ? ["-w"] : [], "-e", t.grep, "--", t.dir ?? "."], {
8080
+ const r = spawnSync12("git", ["-C", root, "grep", ...flags, t.regex ? "-E" : "-F", ...t.word ? ["-w"] : [], "-e", t.grep, "--", t.dir ?? "."], {
7781
8081
  encoding: "utf8",
7782
8082
  maxBuffer: 64 * 1024 * 1024
7783
8083
  });
@@ -7873,13 +8173,16 @@ function formatEval(rows, group) {
7873
8173
 
7874
8174
  // src/core/review.ts
7875
8175
  import crypto13 from "node:crypto";
7876
- import fs40 from "node:fs";
7877
- import path35 from "node:path";
8176
+ import fs41 from "node:fs";
8177
+ import path36 from "node:path";
7878
8178
  var TEMPLATE2 = { task: "task-reviewer", fix: "re-review", range: "code-reviewer" };
7879
8179
  var section = (title, body) => `## ${title}
7880
8180
 
7881
8181
  ${body.trim() || "(empty)"}
7882
8182
  `;
8183
+ var numbered = (diff) => diff.trim() ? `${NUMBERED}
8184
+
8185
+ ${/^[+ -]\s*\d+\| /m.test(diff) ? diff : numberDiff(diff)}` : diff;
7883
8186
  function ancestors(m) {
7884
8187
  const out = [];
7885
8188
  for (let id2 = m.parent; id2; ) {
@@ -7892,7 +8195,7 @@ function ancestors(m) {
7892
8195
  function briefOf(m) {
7893
8196
  const root = [m, ...ancestors(m)].at(-1);
7894
8197
  const f = runFile(root.id, "brief.md");
7895
- return fs40.existsSync(f) ? fs40.readFileSync(f, "utf8") : root.task;
8198
+ return fs41.existsSync(f) ? fs41.readFileSync(f, "utf8") : root.task;
7896
8199
  }
7897
8200
  function latestReview(ids) {
7898
8201
  for (const id2 of listRunIds().reverse()) {
@@ -7906,7 +8209,7 @@ function latestReview(ids) {
7906
8209
  }
7907
8210
  function diffOf(m, from = m.baseTree) {
7908
8211
  const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
7909
- if (where && fs40.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
8212
+ if (where && fs41.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
7910
8213
  if (from !== m.baseTree) {
7911
8214
  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);
7912
8215
  }
@@ -7920,7 +8223,7 @@ function runReview(id2) {
7920
8223
  if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
7921
8224
  const chain = ancestors(m);
7922
8225
  const previous = latestReview(chain.map((a) => a.id));
7923
- const dir = m.mode === "isolate" && m.worktree && fs40.existsSync(m.worktree) ? m.cwd : m.dir;
8226
+ const dir = m.mode === "isolate" && m.worktree && fs41.existsSync(m.worktree) ? m.cwd : m.dir;
7924
8227
  const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
7925
8228
  const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
7926
8229
  if (!previous) {
@@ -7928,7 +8231,7 @@ function runReview(id2) {
7928
8231
  ...job,
7929
8232
  kind: "task",
7930
8233
  package: [`# Review package \xB7 ${title}
7931
- `, section("BRIEF", briefOf(m)), section("REPORT", readSummary(m)), section("DIFF", diffOf(m))].join("\n")
8234
+ `, section("BRIEF", briefOf(m)), section("REPORT", readSummary(m)), section("DIFF", numbered(diffOf(m)))].join("\n")
7932
8235
  };
7933
8236
  }
7934
8237
  const reviewed = chain.find((a) => a.id === previous.reviewOf);
@@ -7942,7 +8245,7 @@ function runReview(id2) {
7942
8245
  section("BRIEF", briefOf(m)),
7943
8246
  section("PREVIOUS FINDINGS", readSummary(previous)),
7944
8247
  section("FIX REPORT", readSummary(m)),
7945
- section("FIX DIFF", diffOf(m, from))
8248
+ section("FIX DIFF", numbered(diffOf(m, from)))
7946
8249
  ].join("\n")
7947
8250
  };
7948
8251
  }
@@ -7996,19 +8299,19 @@ function writePackage(job) {
7996
8299
  const root = repoRoot(job.dir);
7997
8300
  if (!g || !root) throw new UserError(`not a git repository: ${job.dir}`);
7998
8301
  const name = `review-${crypto13.randomBytes(4).toString("hex")}.md`;
7999
- const inside3 = !path35.relative(fs40.realpathSync(root), fs40.realpathSync(g)).startsWith("..");
8000
- const file2 = inside3 ? path35.join(g, "pitroom", name) : path35.join(root, ".pitroom", name);
8302
+ const inside3 = !path36.relative(fs41.realpathSync(root), fs41.realpathSync(g)).startsWith("..");
8303
+ const file2 = inside3 ? path36.join(g, "pitroom", name) : path36.join(root, ".pitroom", name);
8001
8304
  if (!inside3) ignoreInGit(root, "/.pitroom/");
8002
- fs40.mkdirSync(path35.dirname(file2), { recursive: true });
8003
- fs40.writeFileSync(file2, job.package);
8305
+ fs41.mkdirSync(path36.dirname(file2), { recursive: true });
8306
+ fs41.writeFileSync(file2, job.package);
8004
8307
  return file2;
8005
8308
  }
8006
8309
  function ignoreInGit(root, pattern) {
8007
- const exclude = path35.resolve(root, git(root, ["rev-parse", "--git-path", "info/exclude"]).stdout.trim());
8008
- const text = fs40.existsSync(exclude) ? fs40.readFileSync(exclude, "utf8") : "";
8310
+ const exclude = path36.resolve(root, git(root, ["rev-parse", "--git-path", "info/exclude"]).stdout.trim());
8311
+ const text = fs41.existsSync(exclude) ? fs41.readFileSync(exclude, "utf8") : "";
8009
8312
  if (text.split(/\r?\n/).includes(pattern)) return;
8010
- fs40.mkdirSync(path35.dirname(exclude), { recursive: true });
8011
- fs40.appendFileSync(exclude, `${text && !text.endsWith("\n") ? "\n" : ""}# Pitroom review packages in worktrees (removed after each review)
8313
+ fs41.mkdirSync(path36.dirname(exclude), { recursive: true });
8314
+ fs41.appendFileSync(exclude, `${text && !text.endsWith("\n") ? "\n" : ""}# Pitroom review packages in worktrees (removed after each review)
8012
8315
  ${pattern}
8013
8316
  `);
8014
8317
  }
@@ -8164,10 +8467,10 @@ async function cmdReview(p) {
8164
8467
  writeMeta(meta);
8165
8468
  }
8166
8469
  } catch (e) {
8167
- fs41.rmSync(packageFile, { force: true });
8470
+ fs42.rmSync(packageFile, { force: true });
8168
8471
  throw e;
8169
8472
  }
8170
- fs41.writeFileSync(runFile(meta.id, "package.md"), job.package);
8473
+ fs42.writeFileSync(runFile(meta.id, "package.md"), job.package);
8171
8474
  return launch(p, meta);
8172
8475
  }
8173
8476
  async function cmdAudit(p) {
@@ -8179,7 +8482,8 @@ async function cmdAudit(p) {
8179
8482
  if (m.mode !== "read") throw new UserError(`run ${m.id} changed files; an audit re-checks a read run's answer: pitroom review ${m.id} judges a change`);
8180
8483
  const answer = readSummary(m);
8181
8484
  if (!answer.trim()) throw new UserError(`run ${m.id} gave no answer to audit`);
8182
- const worker = flag(p, "worker") ?? (flag(p, "tier") ? void 0 : pickAuditor(m));
8485
+ const named = flag(p, "worker") ?? flag(p, "tier");
8486
+ const [worker, ...others] = named ? [flag(p, "worker")] : auditors(m);
8183
8487
  if (!worker && !flag(p, "tier")) {
8184
8488
  throw new UserError('no other worker to audit with: name one with -W, or configure tiers "audit" or "cheap", or fallback workers, that differ from the one that answered', 3);
8185
8489
  }
@@ -8189,7 +8493,8 @@ async function cmdAudit(p) {
8189
8493
  dir: m.dir,
8190
8494
  worker,
8191
8495
  group: flag(p, "group") ?? m.group,
8192
- noFallback: true,
8496
+ noFallback: !!named,
8497
+ fallbackTo: others,
8193
8498
  audit: { of: m.id },
8194
8499
  auditRate: void 0
8195
8500
  });
@@ -8207,33 +8512,37 @@ async function cmdPrices(p) {
8207
8512
  if (!has(p, "quiet")) console.log(`price catalog updated: ${r.models} priced models`);
8208
8513
  }
8209
8514
  const st = catalogStatus();
8210
- const used = (() => {
8515
+ const priced = (t) => {
8516
+ const backend = getBackend(t.backend);
8517
+ const model = (t.model ?? "").split("#")[0];
8518
+ const price = workerPrice(t.backend, model);
8519
+ return {
8520
+ worker: model ? `${t.backend}:${model}` : t.backend,
8521
+ reportsCost: backend.capabilities.reportsCost,
8522
+ price: price ? { name: price.name, source: price.source, input: price.input, output: price.output, cachedInput: price.cachedInput } : void 0
8523
+ };
8524
+ };
8525
+ const asked = p.positional.length > 0;
8526
+ const used = asked ? p.positional.map((spec) => priced(parseTarget(spec, resolveChain().worker.backend))) : (() => {
8211
8527
  try {
8212
8528
  const chain = resolveChain();
8213
- return [chain.worker, ...chain.fallback].map((t) => {
8214
- const backend = getBackend(t.backend);
8215
- const model = (t.model ?? "").split("#")[0];
8216
- const price = workerPrice(t.backend, model);
8217
- return {
8218
- worker: model ? `${t.backend}:${model}` : t.backend,
8219
- reportsCost: backend.capabilities.reportsCost,
8220
- price: price ? { name: price.name, source: price.source, input: price.input, output: price.output, cachedInput: price.cachedInput } : void 0
8221
- };
8222
- });
8529
+ return [chain.worker, ...chain.fallback].map(priced);
8223
8530
  } catch {
8224
8531
  return [];
8225
8532
  }
8226
8533
  })();
8227
- if (has(p, "json")) return console.log(JSON.stringify({ ...st, workers: used }, null, 2)), 0;
8534
+ const primary = primaryPrice();
8535
+ if (has(p, "json")) return console.log(JSON.stringify({ ...st, primary, workers: used }, null, 2)), 0;
8228
8536
  console.log(`price feed: ${st.enabled ? "on" : 'off ("priceFeed": true in the config turns it on: Pitroom then fetches one public file, see PRIVACY.md)'}`);
8229
8537
  console.log(`source: ${st.url}${st.enabled ? `, refreshed after a run when older than ${st.hours} h` : ""}`);
8230
8538
  console.log(st.models ? `catalog: ${st.models} priced models, updated ${st.ageHours < 1 ? "within the hour" : `${st.ageHours.toFixed(1)} h ago`}` : "catalog: none yet (pitroom prices --refresh fetches it)");
8231
8539
  if (st.lastFailure) console.log(`last try: failed: ${st.lastFailure.replace(/^\S+ /, "")}`);
8540
+ console.log(`primary: ${primary.name}: in ${primary.input} \xB7 out ${primary.output} \xB7 cached ${primary.cachedInput} USD/1M (what the savings are measured against)`);
8232
8541
  if (used.length) {
8233
- console.log("\nthe workers in use:");
8542
+ console.log(asked ? "\nthe workers asked about:" : "\nthe workers in use:");
8234
8543
  for (const u of used) {
8235
8544
  const what2 = u.reportsCost ? "reports its own cost" : u.price ? `in ${u.price.input} \xB7 out ${u.price.output} \xB7 cached ${u.price.cachedInput} USD/1M (${u.price.source === "catalog" ? `price catalog ${catalogLabel()}` : "your workerPrices"})` : "no price: its runs count as free in the savings";
8236
- console.log(` ${u.worker.padEnd(36)} ${what2}`);
8545
+ console.log(` ${u.worker.padEnd(Math.max(36, ...used.map((x) => x.worker.length)))} ${what2}`);
8237
8546
  }
8238
8547
  }
8239
8548
  return 0;
@@ -8276,7 +8585,7 @@ function cmdPlan(p) {
8276
8585
  }
8277
8586
  function cmdCrew(p) {
8278
8587
  const file2 = flag(p, "task-file");
8279
- const tasks = (file2 ? fs41.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
8588
+ const tasks = (file2 ? fs42.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
8280
8589
  if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
8281
8590
  if (has(p, "write") && tasks.length > 1) {
8282
8591
  throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
@@ -8303,7 +8612,7 @@ async function cmdEval(p) {
8303
8612
  if (has(p, "write") || has(p, "isolate") || has(p, "continue") || has(p, "plan")) throw new UserError("eval asks read questions: drop -w/-i/--continue/--plan");
8304
8613
  const questions = loadEval(file2);
8305
8614
  const base2 = runOptions(p, "eval");
8306
- const root = repoRoot(path36.resolve(base2.dir));
8615
+ const root = repoRoot(path37.resolve(base2.dir));
8307
8616
  const truths = new Map(questions.map((q) => [q.id, truthOf(q, root)]));
8308
8617
  const workers = [...new Set(p.flags.get("worker") ?? [effective().worker.value])];
8309
8618
  const group = flag(p, "group") ?? `eval-${(/* @__PURE__ */ new Date()).toISOString().slice(11, 19).replace(/:/g, "")}`;
@@ -8405,6 +8714,7 @@ function cmdHistory(p) {
8405
8714
  return 0;
8406
8715
  }
8407
8716
  importRuns();
8717
+ settleLedger();
8408
8718
  const since2 = sinceMs(flag(p, "since"));
8409
8719
  if (sub === "stats") {
8410
8720
  const s = historyStats(since2);
@@ -8424,14 +8734,16 @@ ${[fmt2(head2), ...rows2.map(fmt2)].join("\n")}`);
8424
8734
  const { rows, total } = listHistory({ text: p.positional.join(" "), model: flag(p, "model"), state: flag(p, "state"), group: flag(p, "group"), sinceMs: since2, limit });
8425
8735
  if (has(p, "json")) return console.log(JSON.stringify({ total, rows }, null, 2)), 0;
8426
8736
  if (!rows.length) return console.log(total ? "nothing on this page" : "no matching runs"), 0;
8427
- const body = rows.map((r) => [r.id, when(r.startedAt), r.verifyFailed ? "verify failed" : r.state, `${r.backend}${r.model ? ` (${r.model.split("/").pop()})` : ""}`, secs(r.seconds), r.task.length > 60 ? `${r.task.slice(0, 59)}\u2026` : r.task]);
8428
- const head = ["RUN", "WHEN", "STATE", "WORKER", "TIME", "TASK"];
8737
+ const cost = (r) => r.cost === void 0 ? "-" : r.cost === 0 ? "free" : `${r.costEstimated ? "~" : ""}${usd(r.cost)}`;
8738
+ const body = rows.map((r) => [r.id, when(r.startedAt), r.verifyFailed ? "verify failed" : r.state, `${r.backend}${r.model ? ` (${r.model.split("/").pop()})` : ""}`, secs(r.seconds), cost(r), r.task.length > 60 ? `${r.task.slice(0, 59)}\u2026` : r.task]);
8739
+ const head = ["RUN", "WHEN", "STATE", "WORKER", "TIME", "COST", "TASK"];
8429
8740
  const widths = head.map((h, i) => Math.max(h.length, ...body.map((r) => r[i].length)));
8430
8741
  const fmt = (r, header = false) => r.map((c, i) => {
8431
8742
  const cell2 = i === r.length - 1 ? c : c.padEnd(widths[i]);
8432
8743
  return header ? dim(cell2) : i === 2 ? stateColour(r[2], cell2) : cell2;
8433
8744
  }).join(" ");
8434
8745
  console.log([fmt(head, true), ...body.map((r) => fmt(r))].join("\n"));
8746
+ if (rows.some((r) => r.costEstimated)) console.log(dim("\n~ estimated from a price: the worker's CLI reported no cost"));
8435
8747
  if (total > rows.length) console.log(`
8436
8748
  ${rows.length} of ${total} \xB7 --limit N for more \xB7 pitroom show <run> for one`);
8437
8749
  return 0;
@@ -8503,13 +8815,13 @@ function sinceMs(s) {
8503
8815
  if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
8504
8816
  return Date.now() - Number(m[1]) * 864e5;
8505
8817
  }
8506
- var readStdin = () => process.stdin.isTTY ? "" : fs41.readFileSync(0, "utf8");
8818
+ var readStdin = () => process.stdin.isTTY ? "" : fs42.readFileSync(0, "utf8");
8507
8819
  function cmdStatusline(p) {
8508
8820
  const input = readStdin();
8509
8821
  const lines = [];
8510
8822
  const then = flag(p, "then");
8511
8823
  if (then) {
8512
- const r = process.platform === "win32" ? spawnSync12(then, { shell: true, input, encoding: "utf8", timeout: 5e3 }) : spawnSync12("/bin/sh", ["-c", then], { input, encoding: "utf8", timeout: 5e3 });
8824
+ const r = process.platform === "win32" ? spawnSync13(then, { shell: true, input, encoding: "utf8", timeout: 5e3 }) : spawnSync13("/bin/sh", ["-c", then], { input, encoding: "utf8", timeout: 5e3 });
8513
8825
  if (r.stdout?.trimEnd()) lines.push(r.stdout.trimEnd());
8514
8826
  }
8515
8827
  try {
@@ -8543,8 +8855,8 @@ function cmdModels(p) {
8543
8855
  return 0;
8544
8856
  }
8545
8857
  function cmdHookStart() {
8546
- const script = path36.join(path36.dirname(path36.dirname(fileURLToPath5(import.meta.url))), "hooks", "session-start.mjs");
8547
- if (fs41.existsSync(script)) spawnSync12(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
8858
+ const script = path37.join(path37.dirname(path37.dirname(fileURLToPath5(import.meta.url))), "hooks", "session-start.mjs");
8859
+ if (fs42.existsSync(script)) spawnSync13(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
8548
8860
  return 0;
8549
8861
  }
8550
8862
  function cmdInit(p) {
@@ -8556,19 +8868,21 @@ function cmdInit(p) {
8556
8868
  }
8557
8869
  function cmdSavings(p) {
8558
8870
  const since2 = flag(p, "since") ?? "all";
8871
+ settleLedger();
8559
8872
  const t = totals(readLedger(sinceMs(since2)));
8560
8873
  if (has(p, "models")) {
8561
8874
  const by = /* @__PURE__ */ new Map();
8562
8875
  for (const e of readLedger(sinceMs(since2))) {
8563
8876
  const key = `${e.backend ?? "?"} ${e.model ?? "(default model)"}`;
8564
- const a = by.get(key) ?? { runs: 0, tokens: 0, returned: 0, cost: 0, saved: 0 };
8565
- by.set(key, { runs: a.runs + 1, tokens: a.tokens + e.tokens, returned: a.returned + e.returned, cost: a.cost + e.workerCost, saved: a.saved + e.saved });
8877
+ const a = by.get(key) ?? { runs: 0, tokens: 0, returned: 0, cost: 0, estimated: false, saved: 0 };
8878
+ by.set(key, { runs: a.runs + 1, tokens: a.tokens + e.tokens, returned: a.returned + e.returned, cost: a.cost + e.workerCost, estimated: a.estimated || !!e.costEstimated, saved: a.saved + e.saved });
8566
8879
  }
8567
8880
  const width = Math.max(12, ...[...by.keys()].map((k) => k.length));
8568
8881
  console.log(`${"worker model".padEnd(width)} ${"runs".padStart(5)} ${"processed".padStart(10)} ${"returned".padStart(9)} ${"cost".padStart(8)} ${"saved".padStart(8)}`);
8569
8882
  for (const [key, a] of [...by].sort((x, y) => y[1].runs - x[1].runs)) {
8570
- console.log(`${key.padEnd(width)} ${String(a.runs).padStart(5)} ${compact(a.tokens).padStart(10)} ${compact(a.returned).padStart(9)} ${usd(a.cost).padStart(8)} ${usd(a.saved).padStart(8)}`);
8883
+ console.log(`${key.padEnd(width)} ${String(a.runs).padStart(5)} ${compact(a.tokens).padStart(10)} ${compact(a.returned).padStart(9)} ${`${a.estimated ? "~" : ""}${usd(a.cost)}`.padStart(8)} ${usd(a.saved).padStart(8)}`);
8571
8884
  }
8885
+ if ([...by.values()].some((a) => a.estimated)) console.log(dim("\n~ the cost is estimated from a price, in part or in all: the worker's CLI reported none"));
8572
8886
  return 0;
8573
8887
  }
8574
8888
  const period = since2 === "all" ? "all time" : `last ${since2.replace("d", " days")}`;
@@ -8584,8 +8898,8 @@ function cmdSavings(p) {
8584
8898
  est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
8585
8899
  const out = flag(p, "card");
8586
8900
  if (out) {
8587
- fs41.writeFileSync(out, card(t, period));
8588
- console.log(`card written to ${path36.resolve(out)}`);
8901
+ fs42.writeFileSync(out, card(t, period));
8902
+ console.log(`card written to ${path37.resolve(out)}`);
8589
8903
  }
8590
8904
  if (has(p, "badge")) console.log(`![pitroom](${badgeUrl(t)})`);
8591
8905
  return 0;
@@ -8627,7 +8941,7 @@ function cmdConfig(p) {
8627
8941
  console.log(JSON.stringify({ path: configPath(), settings: eff, warnings: warnings2 }, null, 2));
8628
8942
  return 0;
8629
8943
  }
8630
- console.log(`config file: ${configPath()}${fs41.existsSync(configPath()) ? "" : " (not present)"}`);
8944
+ console.log(`config file: ${configPath()}${fs42.existsSync(configPath()) ? "" : " (not present)"}`);
8631
8945
  for (const [key, s] of Object.entries(eff)) {
8632
8946
  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);
8633
8947
  console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
@@ -8651,7 +8965,7 @@ function cmdClean(p) {
8651
8965
  const m = readMeta(id2);
8652
8966
  if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
8653
8967
  recordRun(m);
8654
- fs41.rmSync(runDir(id2), { recursive: true, force: true });
8968
+ fs42.rmSync(runDir(id2), { recursive: true, force: true });
8655
8969
  }
8656
8970
  const snapshots = prune(Date.now(), 0);
8657
8971
  console.log(`removed ${old.length} run(s)${snapshots ? ` and ${snapshots} read snapshot${snapshots === 1 ? "" : "s"}` : ""}; the history and the savings ledger are kept`);
@@ -8661,10 +8975,10 @@ var cmdRevert = (p) => (console.log(revertRun(freshMeta(resolveRun(p.positional[
8661
8975
  var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positional[0])))), 0);
8662
8976
 
8663
8977
  // src/core/doctor.ts
8664
- import { spawnSync as spawnSync13 } from "node:child_process";
8665
- import fs42 from "node:fs";
8978
+ import { spawnSync as spawnSync14 } from "node:child_process";
8979
+ import fs43 from "node:fs";
8666
8980
  import os13 from "node:os";
8667
- import path37 from "node:path";
8981
+ import path38 from "node:path";
8668
8982
  var MARK2 = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
8669
8983
  var NEXT = [
8670
8984
  { when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
@@ -8687,14 +9001,14 @@ function usageLine() {
8687
9001
  return `${ids.length} run${ids.length === 1 ? "" : "s"} so far${when2} (pitroom savings shows what they saved)`;
8688
9002
  }
8689
9003
  function firstNodeOnPath() {
8690
- for (const dir of (process.env.PATH ?? "").split(path37.delimiter).filter(Boolean)) {
8691
- const file2 = path37.join(dir, process.platform === "win32" ? "node.exe" : "node");
9004
+ for (const dir of (process.env.PATH ?? "").split(path38.delimiter).filter(Boolean)) {
9005
+ const file2 = path38.join(dir, process.platform === "win32" ? "node.exe" : "node");
8692
9006
  try {
8693
- if (!fs42.statSync(file2).isFile()) continue;
9007
+ if (!fs43.statSync(file2).isFile()) continue;
8694
9008
  } catch {
8695
9009
  continue;
8696
9010
  }
8697
- const r = spawnSync13(file2, ["-p", "process.versions.node"], { encoding: "utf8", timeout: 5e3 });
9011
+ const r = spawnSync14(file2, ["-p", "process.versions.node"], { encoding: "utf8", timeout: 5e3 });
8698
9012
  return r.status === 0 ? { path: file2, version: r.stdout.trim() } : { path: file2, version: "" };
8699
9013
  }
8700
9014
  return void 0;
@@ -8718,14 +9032,16 @@ function doctor8(probe2) {
8718
9032
  const addAll = (list2) => list2.forEach((c) => add2(c.level, c.message));
8719
9033
  add2("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
8720
9034
  const onPath = firstNodeOnPath();
8721
- if (onPath && onPath.version && tooOld(onPath.version) && path37.resolve(onPath.path) !== path37.resolve(process.execPath)) {
9035
+ if (onPath && onPath.version && tooOld(onPath.version) && path38.resolve(onPath.path) !== path38.resolve(process.execPath)) {
8722
9036
  add2("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)`);
8723
9037
  }
8724
9038
  add2(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
8725
9039
  const cfg = loadConfig();
8726
- add2(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs42.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
9040
+ add2(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs43.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
8727
9041
  for (const w of cfg.warnings) add2("warn", w);
8728
- if (shimReady()) add2("ok", "git guard shim ready");
9042
+ if (shimReady() && process.platform === "win32" && !fs43.existsSync(path38.join(shimDir(), "git.exe"))) {
9043
+ add2("warn", "git guard shim ready as git.cmd only (no C# compiler of .NET Framework 4 to build git.exe): a program that starts git without a shell skips it, and PowerShell's unquoted & | < > reach git changed");
9044
+ } else if (shimReady()) add2("ok", "git guard shim ready");
8729
9045
  else if (process.platform === "win32") add2("warn", "git guard: the ref and push guard is on, but the git shim needs the sh.exe of Git for Windows next to git (not found), so a worker's `git add`, `restore` or `clean` is not blocked; prefer --isolate and check the patch before apply");
8730
9046
  else add2("warn", "git guard unavailable (git not on PATH)");
8731
9047
  section2("Worker chain");
@@ -8737,7 +9053,8 @@ function doctor8(probe2) {
8737
9053
  } catch (e) {
8738
9054
  add2("fail", e.message);
8739
9055
  }
8740
- if (chain.length) add2("ok", `worker chain: ${chain.map(describeTarget).join(" \u2192 ")}`);
9056
+ const auto = chain.length > 1 && !effective().fallback.value.length;
9057
+ if (chain.length) add2("ok", `worker chain: ${chain.map(describeTarget).join(" \u2192 ")}${auto ? ' (fallbacks from "autoFallback": the other workers your config names)' : ""}`);
8741
9058
  const rate2 = effective().audit.value;
8742
9059
  if (rate2 > 0 && chain.length) {
8743
9060
  const auditor = pickAuditor({ worker: chain[0], fallback: chain.slice(1) });
@@ -8856,12 +9173,12 @@ function liveProbe(target) {
8856
9173
  } catch (e) {
8857
9174
  return { level: "fail", message: e.message };
8858
9175
  }
8859
- const scratch = readsOwnSnapshot(backend) ? fs42.mkdtempSync(path37.join(os13.tmpdir(), "pitroom-probe-")) : void 0;
9176
+ const scratch = readsOwnSnapshot(backend) ? fs43.mkdtempSync(path38.join(os13.tmpdir(), "pitroom-probe-")) : void 0;
8860
9177
  const cwd = scratch ?? process.cwd();
8861
9178
  try {
8862
9179
  return probe(backend, target, cwd);
8863
9180
  } finally {
8864
- if (scratch) fs42.rmSync(scratch, { recursive: true, force: true });
9181
+ if (scratch) fs43.rmSync(scratch, { recursive: true, force: true });
8865
9182
  }
8866
9183
  }
8867
9184
  function probe(backend, target, cwd) {
@@ -8875,7 +9192,7 @@ function probe(backend, target, cwd) {
8875
9192
  title: "pitroom doctor probe"
8876
9193
  });
8877
9194
  const t0 = Date.now();
8878
- const r = spawnSync13(inv.command, inv.args, {
9195
+ const r = spawnSync14(inv.command, inv.args, {
8879
9196
  encoding: "utf8",
8880
9197
  timeout: 3e5,
8881
9198
  stdio: ["ignore", "pipe", "pipe"],
@@ -8906,20 +9223,20 @@ function skillChecks() {
8906
9223
  else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
8907
9224
  }
8908
9225
  if (geminiExtensionInstalled()) {
8909
- const linked = installedSkills().some((i) => i.base.includes(`${path37.sep}.agents${path37.sep}`) && i.names.length);
9226
+ const linked = installedSkills().some((i) => i.base.includes(`${path38.sep}.agents${path38.sep}`) && i.names.length);
8910
9227
  checks.push(
8911
9228
  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)" }
8912
9229
  );
8913
9230
  }
8914
9231
  if (viaPlugin) {
8915
- const linked = installedSkills().some((i) => i.base.includes(`${path37.sep}.claude${path37.sep}`) && i.names.length);
9232
+ const linked = installedSkills().some((i) => i.base.includes(`${path38.sep}.claude${path38.sep}`) && i.names.length);
8916
9233
  checks.push(
8917
9234
  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)" }
8918
9235
  );
8919
9236
  }
8920
9237
  const launcher = launcherPath();
8921
- if (fs42.existsSync(launcher)) {
8922
- const r = spawnSync13(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
9238
+ if (fs43.existsSync(launcher)) {
9239
+ const r = spawnSync14(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
8923
9240
  checks.push(
8924
9241
  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)}` }
8925
9242
  );
@@ -8930,12 +9247,12 @@ function skillChecks() {
8930
9247
  }
8931
9248
  function geminiExtensionInstalled() {
8932
9249
  const home3 = process.env.GEMINI_CLI_HOME ?? os13.homedir();
8933
- return fs42.existsSync(path37.join(home3, ".gemini", "extensions", "pitroom"));
9250
+ return fs43.existsSync(path38.join(home3, ".gemini", "extensions", "pitroom"));
8934
9251
  }
8935
9252
  function pluginInstalled() {
8936
9253
  try {
8937
- const f = path37.join(os13.homedir(), ".claude", "plugins", "installed_plugins.json");
8938
- const plugins = JSON.parse(fs42.readFileSync(f, "utf8")).plugins ?? {};
9254
+ const f = path38.join(os13.homedir(), ".claude", "plugins", "installed_plugins.json");
9255
+ const plugins = JSON.parse(fs43.readFileSync(f, "utf8")).plugins ?? {};
8939
9256
  return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
8940
9257
  } catch {
8941
9258
  return false;
@@ -8952,24 +9269,24 @@ function superpowersActive() {
8952
9269
  for (const p of openCodePlugins()) {
8953
9270
  if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
8954
9271
  }
8955
- for (const base2 of [path37.join(os13.homedir(), ".agents", "skills"), path37.join(os13.homedir(), ".claude", "skills")]) {
8956
- const dir = path37.join(base2, "using-superpowers");
8957
- if (fs42.existsSync(path37.join(dir, "SKILL.md"))) found.push(dir);
9272
+ for (const base2 of [path38.join(os13.homedir(), ".agents", "skills"), path38.join(os13.homedir(), ".claude", "skills")]) {
9273
+ const dir = path38.join(base2, "using-superpowers");
9274
+ if (fs43.existsSync(path38.join(dir, "SKILL.md"))) found.push(dir);
8958
9275
  }
8959
9276
  return found;
8960
9277
  }
8961
9278
  function claudePlugins() {
8962
9279
  try {
8963
- const f = path37.join(os13.homedir(), ".claude", "plugins", "installed_plugins.json");
8964
- return Object.keys(JSON.parse(fs42.readFileSync(f, "utf8")).plugins ?? {});
9280
+ const f = path38.join(os13.homedir(), ".claude", "plugins", "installed_plugins.json");
9281
+ return Object.keys(JSON.parse(fs43.readFileSync(f, "utf8")).plugins ?? {});
8965
9282
  } catch {
8966
9283
  return [];
8967
9284
  }
8968
9285
  }
8969
9286
  function claudePluginEnabled(key) {
8970
9287
  try {
8971
- const f = path37.join(os13.homedir(), ".claude", "settings.json");
8972
- return JSON.parse(fs42.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
9288
+ const f = path38.join(os13.homedir(), ".claude", "settings.json");
9289
+ return JSON.parse(fs43.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
8973
9290
  } catch {
8974
9291
  return true;
8975
9292
  }
@@ -8978,7 +9295,7 @@ function codexPlugins() {
8978
9295
  const plugins = /* @__PURE__ */ new Map();
8979
9296
  let text;
8980
9297
  try {
8981
- text = fs42.readFileSync(path37.join(process.env.CODEX_HOME ?? path37.join(os13.homedir(), ".codex"), "config.toml"), "utf8");
9298
+ text = fs43.readFileSync(path38.join(process.env.CODEX_HOME ?? path38.join(os13.homedir(), ".codex"), "config.toml"), "utf8");
8982
9299
  } catch {
8983
9300
  return plugins;
8984
9301
  }
@@ -8996,11 +9313,11 @@ function codexPlugins() {
8996
9313
  return plugins;
8997
9314
  }
8998
9315
  function openCodePlugins() {
8999
- const dir = path37.join(process.env.XDG_CONFIG_HOME ?? path37.join(os13.homedir(), ".config"), "opencode");
9316
+ const dir = path38.join(process.env.XDG_CONFIG_HOME ?? path38.join(os13.homedir(), ".config"), "opencode");
9000
9317
  const plugins = [];
9001
9318
  for (const name of ["opencode.json", "opencode.jsonc"]) {
9002
9319
  try {
9003
- const text = fs42.readFileSync(path37.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
9320
+ const text = fs43.readFileSync(path38.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
9004
9321
  const list2 = JSON.parse(text).plugin;
9005
9322
  if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
9006
9323
  } catch {
@@ -9008,7 +9325,7 @@ function openCodePlugins() {
9008
9325
  }
9009
9326
  for (const folder of ["plugin", "plugins"]) {
9010
9327
  try {
9011
- for (const f of fs42.readdirSync(path37.join(dir, folder))) plugins.push(path37.join(dir, folder, f));
9328
+ for (const f of fs43.readdirSync(path38.join(dir, folder))) plugins.push(path38.join(dir, folder, f));
9012
9329
  } catch {
9013
9330
  }
9014
9331
  }
@@ -9061,7 +9378,7 @@ Usage
9061
9378
  pitroom statusline [--then CMD] status-bar line: running workers, savings this week (after CMD's)
9062
9379
  pitroom hook-card PostToolUse hook: a card after each Bash \`pitroom\` command
9063
9380
  pitroom hook-start SessionStart hook: introduces Pitroom to the agent (Codex; Claude Code uses its plugin)
9064
- pitroom prices [--refresh] [--json] the price catalog (config "priceFeed") and the price each worker in use gets
9381
+ pitroom prices [WORKER\u2026] [--refresh] [--json] the price catalog (config "priceFeed"), the primary's price and the price each worker in use (or named) gets
9065
9382
  pitroom cooldown [--clear] models that said "rate limited" and are skipped for a while (a quota used up);
9066
9383
  --clear tries them again
9067
9384
  pitroom mcp [-d DIR] [--http [--port N]]
@@ -9135,7 +9452,7 @@ var COMMANDS = {
9135
9452
  if (port !== void 0 && !(/^\d+$/.test(port) && Number(port) <= 65535)) throw new UserError("--port takes a number from 0 to 65535 (0 picks a free one)");
9136
9453
  const dir = flag(p, "dir");
9137
9454
  if (dir !== void 0) {
9138
- if (!fs43.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) throw new UserError(`-d ${dir}: no such directory`);
9455
+ if (!fs44.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) throw new UserError(`-d ${dir}: no such directory`);
9139
9456
  process.chdir(dir);
9140
9457
  process.env.PWD = process.cwd();
9141
9458
  }