pitroom 0.20.0 → 0.22.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 fs37 from "node:fs";
4
+ import fs38 from "node:fs";
5
5
 
6
6
  // src/core/errors.ts
7
7
  var UserError = class extends Error {
@@ -391,10 +391,10 @@ function parseEvents2(jsonl) {
391
391
  }
392
392
  case "turn.completed": {
393
393
  const u = e.usage ?? {};
394
- const cached3 = num2(u.cached_input_tokens);
394
+ const cached4 = num2(u.cached_input_tokens);
395
395
  const reasoning = num2(u.reasoning_output_tokens);
396
- usage2.input += Math.max(0, num2(u.input_tokens) - cached3);
397
- usage2.cacheRead += cached3;
396
+ usage2.input += Math.max(0, num2(u.input_tokens) - cached4);
397
+ usage2.cacheRead += cached4;
398
398
  usage2.cacheWrite += num2(u.cache_write_input_tokens);
399
399
  usage2.output += Math.max(0, num2(u.output_tokens) - reasoning);
400
400
  usage2.reasoning += reasoning;
@@ -768,9 +768,9 @@ function parseEvents3(jsonl) {
768
768
  if (e.severity === "error") error ??= String(e.message ?? "Gemini CLI error");
769
769
  } else if (e.type === "result") {
770
770
  const s = e.stats ?? {};
771
- const cached3 = num3(s.cached);
772
- usage2.cacheRead += cached3;
773
- usage2.input += s.input !== void 0 ? num3(s.input) : Math.max(0, num3(s.input_tokens) - cached3);
771
+ const cached4 = num3(s.cached);
772
+ usage2.cacheRead += cached4;
773
+ usage2.input += s.input !== void 0 ? num3(s.input) : Math.max(0, num3(s.input_tokens) - cached4);
774
774
  usage2.output += num3(s.output_tokens);
775
775
  usage2.total += num3(s.total_tokens) || num3(s.input_tokens) + num3(s.output_tokens);
776
776
  usage2.toolCalls = Math.max(usage2.toolCalls, num3(s.tool_calls));
@@ -843,6 +843,7 @@ function invocation3(req) {
843
843
  "none"
844
844
  ];
845
845
  for (const p of policies) args.push("--policy", path5.join(dir, p));
846
+ if (req.sessionId) args.push("--resume", req.sessionId);
846
847
  if (process.env.PITROOM_GEMINI_TRUST === "1") args.push("--skip-trust");
847
848
  const [model] = (req.model ?? "").split("#");
848
849
  if (model) args.push("--model", model);
@@ -929,7 +930,7 @@ function catalog3() {
929
930
  var gemini = {
930
931
  id: "gemini",
931
932
  name: "Gemini CLI",
932
- capabilities: { readOnly: "approval-mode", resume: "none", reportsCost: false, attachFiles: false },
933
+ capabilities: { readOnly: "approval-mode", resume: "by-id", reportsCost: false, attachFiles: false },
933
934
  binary: binary3,
934
935
  catalog: catalog3,
935
936
  invocation: invocation3,
@@ -1446,7 +1447,11 @@ var SCHEMA = {
1446
1447
  mcpDash: "boolean",
1447
1448
  notify: "boolean",
1448
1449
  notifyCommand: "string",
1449
- notifyAfter: "number"
1450
+ notifyAfter: "number",
1451
+ workerPrices: "record",
1452
+ priceFeed: "boolean",
1453
+ priceFeedUrl: "string",
1454
+ priceFeedHours: "number"
1450
1455
  };
1451
1456
  function configPath() {
1452
1457
  if (process.env.PITROOM_CONFIG) return path7.resolve(process.env.PITROOM_CONFIG);
@@ -1544,7 +1549,11 @@ function effective(flags = {}) {
1544
1549
  mcpDash: setting(void 0, flag01(e.PITROOM_MCP_DASH), c.mcpDash, true),
1545
1550
  notify: setting(void 0, flag01(e.PITROOM_NOTIFY), c.notify, false),
1546
1551
  notifyCommand: setting(void 0, e.PITROOM_NOTIFY_COMMAND?.trim() || void 0, c.notifyCommand?.trim() || void 0, void 0),
1547
- notifyAfter: setting(void 0, days(e.PITROOM_NOTIFY_AFTER), days(c.notifyAfter), 15)
1552
+ notifyAfter: setting(void 0, days(e.PITROOM_NOTIFY_AFTER), days(c.notifyAfter), 15),
1553
+ workerPrices: setting(void 0, void 0, c.workerPrices, {}),
1554
+ priceFeed: setting(void 0, flag01(e.PITROOM_PRICE_FEED), c.priceFeed, false),
1555
+ priceFeedUrl: setting(void 0, e.PITROOM_PRICE_FEED_URL?.trim() || void 0, c.priceFeedUrl?.trim() || void 0, "https://models.dev/api.json"),
1556
+ priceFeedHours: setting(void 0, positiveInt(e.PITROOM_PRICE_FEED_HOURS), positiveInt(c.priceFeedHours), 24)
1548
1557
  };
1549
1558
  }
1550
1559
 
@@ -1608,6 +1617,8 @@ var BOOL_FLAGS = {
1608
1617
  "--badge": "badge",
1609
1618
  "--probe": "probe",
1610
1619
  "--clear": "clear",
1620
+ "--refresh": "refresh",
1621
+ "--quiet": "quiet",
1611
1622
  "--copy": "copy",
1612
1623
  "--all": "all",
1613
1624
  "--models": "models",
@@ -1728,9 +1739,9 @@ function effortFlag(p) {
1728
1739
  import readline from "node:readline";
1729
1740
 
1730
1741
  // src/core/run.ts
1731
- import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
1732
- import fs26 from "node:fs";
1733
- import path22 from "node:path";
1742
+ import { spawn as spawn4, spawnSync as spawnSync6 } from "node:child_process";
1743
+ import fs27 from "node:fs";
1744
+ import path23 from "node:path";
1734
1745
 
1735
1746
  // src/vcs/git.ts
1736
1747
  import { spawnSync as spawnSync5 } from "node:child_process";
@@ -1944,7 +1955,9 @@ import fs12 from "node:fs";
1944
1955
  // src/vcs/guard.ts
1945
1956
  import fs11 from "node:fs";
1946
1957
  import path9 from "node:path";
1947
- var VERSION = 1;
1958
+ var VERSION = 2;
1959
+ var WINDOWS = process.platform === "win32";
1960
+ var fwd = (p) => WINDOWS ? p.replace(/\\/g, "/") : p;
1948
1961
  var SHIM = `#!/bin/sh
1949
1962
  # pitroom git guard v${VERSION}. Blocks git commands that change history, refs, the
1950
1963
  # index or discard work. The primary agent reviews and commits, not the worker.
@@ -2021,9 +2034,10 @@ function shimDir() {
2021
2034
  return path9.join(home(), "shim", `v${VERSION}`);
2022
2035
  }
2023
2036
  function findRealGit(skip) {
2037
+ const same2 = (a, b) => WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
2024
2038
  for (const dir of (process.env.PATH ?? "").split(path9.delimiter).filter(Boolean)) {
2025
- if (path9.resolve(dir) === skip) continue;
2026
- const p = path9.join(dir, "git");
2039
+ if (same2(path9.resolve(dir), skip)) continue;
2040
+ const p = path9.join(dir, WINDOWS ? "git.exe" : "git");
2027
2041
  try {
2028
2042
  fs11.accessSync(p, fs11.constants.X_OK);
2029
2043
  if (fs11.statSync(p).isFile()) return p;
@@ -2032,6 +2046,24 @@ function findRealGit(skip) {
2032
2046
  }
2033
2047
  return void 0;
2034
2048
  }
2049
+ function findGitSh(realGit) {
2050
+ let dir = path9.dirname(realGit);
2051
+ for (let i = 0; i < 4; i++) {
2052
+ for (const rel of ["bin/sh.exe", "usr/bin/sh.exe"]) {
2053
+ const p = path9.join(dir, rel);
2054
+ if (fs11.existsSync(p)) return p;
2055
+ }
2056
+ const up = path9.dirname(dir);
2057
+ if (up === dir) break;
2058
+ dir = up;
2059
+ }
2060
+ return void 0;
2061
+ }
2062
+ var cmdShim = (sh) => `@echo off\r
2063
+ set "MSYS2_ARG_CONV_EXCL=*"\r
2064
+ "${sh}" "%~dp0git" %*\r
2065
+ exit /b %errorlevel%\r
2066
+ `;
2035
2067
  var HOOK_VERSION = 1;
2036
2068
  var REF_HOOK = `#!/bin/sh
2037
2069
  # pitroom git guard (layer 2, v${HOOK_VERSION}): refuse ref updates made by workers.
@@ -2045,7 +2077,17 @@ function writeIfChanged(file2, content) {
2045
2077
  fs11.mkdirSync(path9.dirname(file2), { recursive: true });
2046
2078
  const tmp = `${file2}.${process.pid}.tmp`;
2047
2079
  fs11.writeFileSync(tmp, content, { mode: 493 });
2048
- fs11.renameSync(tmp, file2);
2080
+ renameOver(tmp, file2);
2081
+ }
2082
+ function pathKey(env) {
2083
+ const keys = Object.keys(env).filter((k) => k.toLowerCase() === "path");
2084
+ return keys.includes("PATH") ? "PATH" : keys[0] ?? "PATH";
2085
+ }
2086
+ function onePath(env) {
2087
+ const key = pathKey(env);
2088
+ const out = { ...env };
2089
+ for (const k of Object.keys(out)) if (k !== key && k.toLowerCase() === "path") delete out[k];
2090
+ return out;
2049
2091
  }
2050
2092
  function withGitConfig(env, entries) {
2051
2093
  const out = { ...env };
@@ -2066,17 +2108,27 @@ function guardEnv(env) {
2066
2108
  GIT_PAGER: "cat",
2067
2109
  PAGER: "cat"
2068
2110
  };
2069
- if (process.platform === "win32") return { ...env, ...quiet };
2070
2111
  writeIfChanged(path9.join(hooksDir(), "reference-transaction"), REF_HOOK);
2071
- env = withGitConfig(env, [
2072
- ["core.hooksPath", hooksDir()],
2112
+ env = withGitConfig(onePath(env), [
2113
+ ["core.hooksPath", fwd(hooksDir())],
2073
2114
  ["url.pitroom-push-blocked://.pushInsteadOf", ""]
2074
2115
  ]);
2075
2116
  const dir = shimDir();
2076
2117
  const real = findRealGit(dir);
2077
2118
  if (!real) return { ...env, ...quiet };
2119
+ let sh;
2120
+ if (WINDOWS) {
2121
+ sh = findGitSh(real);
2122
+ if (!sh) return { ...env, ...quiet };
2123
+ writeIfChanged(path9.join(dir, "git.cmd"), cmdShim(sh));
2124
+ }
2078
2125
  writeIfChanged(path9.join(dir, "git"), SHIM);
2079
- return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${path9.delimiter}${env.PATH ?? ""}` };
2126
+ const key = pathKey(env);
2127
+ return { ...env, ...quiet, PITROOM_REAL_GIT: fwd(real), [key]: `${dir}${path9.delimiter}${env[key] ?? ""}` };
2128
+ }
2129
+ function shimReady() {
2130
+ const env = guardEnv({ ...process.env });
2131
+ return Boolean(env[pathKey(env)]?.startsWith(shimDir()));
2080
2132
  }
2081
2133
 
2082
2134
  // src/core/process.ts
@@ -2482,20 +2534,157 @@ function auditBadge(meta) {
2482
2534
  }
2483
2535
 
2484
2536
  // src/core/notify.ts
2485
- import { spawn as spawn2 } from "node:child_process";
2537
+ import { spawn as spawn3 } from "node:child_process";
2486
2538
  import crypto5 from "node:crypto";
2487
- import fs21 from "node:fs";
2488
- import path17 from "node:path";
2539
+ import fs22 from "node:fs";
2540
+ import path18 from "node:path";
2489
2541
 
2490
2542
  // src/core/history.ts
2491
- import fs18 from "node:fs";
2543
+ import fs19 from "node:fs";
2492
2544
  import { createRequire } from "node:module";
2493
- import path15 from "node:path";
2545
+ import path16 from "node:path";
2494
2546
  import zlib from "node:zlib";
2495
2547
 
2496
2548
  // src/core/receipt.ts
2549
+ import fs17 from "node:fs";
2550
+ import path15 from "node:path";
2551
+
2552
+ // src/core/prices.ts
2553
+ import { spawn as spawn2 } from "node:child_process";
2497
2554
  import fs16 from "node:fs";
2498
2555
  import path14 from "node:path";
2556
+ var DEFAULT_URL = "https://models.dev/api.json";
2557
+ var RETRY_AFTER_FAILURE_MS = 60 * 6e4;
2558
+ var MIN_MODELS = 100;
2559
+ function catalogLabel() {
2560
+ const url = effective().priceFeedUrl.value;
2561
+ if (url === DEFAULT_URL) return "models.dev";
2562
+ try {
2563
+ return new URL(url).host;
2564
+ } catch {
2565
+ return url;
2566
+ }
2567
+ }
2568
+ var catalogFile = () => path14.join(home(), "prices.json");
2569
+ var attemptFile = () => path14.join(home(), "prices.attempt");
2570
+ function trim(raw) {
2571
+ const out = {};
2572
+ if (!raw || typeof raw !== "object") return out;
2573
+ for (const [provider, p] of Object.entries(raw)) {
2574
+ for (const [id, m] of Object.entries(p?.models ?? {})) {
2575
+ const c = m?.cost;
2576
+ const ok = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
2577
+ if (!c || !ok(c.input) || !ok(c.output)) continue;
2578
+ out[`${provider}/${id}`] = [c.input, c.output, ok(c.cache_read) ? c.cache_read : c.input / 10];
2579
+ }
2580
+ }
2581
+ return out;
2582
+ }
2583
+ var cached2;
2584
+ function readCatalog() {
2585
+ const file2 = catalogFile();
2586
+ try {
2587
+ const { mtimeMs } = fs16.statSync(file2);
2588
+ if (cached2?.file === file2 && cached2.mtimeMs === mtimeMs) return cached2.catalog;
2589
+ const parsed = JSON.parse(fs16.readFileSync(file2, "utf8"));
2590
+ const catalog5 = parsed && typeof parsed.models === "object" && parsed.models && Number.isFinite(Date.parse(parsed.fetchedAt)) ? parsed : void 0;
2591
+ cached2 = { file: file2, mtimeMs, catalog: catalog5 };
2592
+ return catalog5;
2593
+ } catch {
2594
+ return void 0;
2595
+ }
2596
+ }
2597
+ var ageMs = (file2) => {
2598
+ try {
2599
+ return Date.now() - fs16.statSync(file2).mtimeMs;
2600
+ } catch {
2601
+ return Infinity;
2602
+ }
2603
+ };
2604
+ function refreshDue() {
2605
+ const eff = effective();
2606
+ if (!eff.priceFeed.value) return false;
2607
+ if (ageMs(catalogFile()) < eff.priceFeedHours.value * 36e5) return false;
2608
+ return ageMs(attemptFile()) > RETRY_AFTER_FAILURE_MS;
2609
+ }
2610
+ async function refreshCatalog() {
2611
+ const url = effective().priceFeedUrl.value;
2612
+ try {
2613
+ const r = await fetch(url, { signal: AbortSignal.timeout(3e4), headers: { accept: "application/json" } });
2614
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
2615
+ const models = trim(await r.json());
2616
+ const n = Object.keys(models).length;
2617
+ if (n < MIN_MODELS) throw new Error(`only ${n} priced models in it (a catalog has thousands): not used`);
2618
+ fs16.mkdirSync(home(), { recursive: true });
2619
+ const tmp = `${catalogFile()}.${process.pid}.tmp`;
2620
+ const catalog5 = { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), source: url, models };
2621
+ fs16.writeFileSync(tmp, JSON.stringify(catalog5));
2622
+ renameOver(tmp, catalogFile());
2623
+ fs16.rmSync(attemptFile(), { force: true });
2624
+ return { state: "updated", models: n };
2625
+ } catch (e) {
2626
+ try {
2627
+ fs16.mkdirSync(home(), { recursive: true });
2628
+ fs16.writeFileSync(attemptFile(), `${(/* @__PURE__ */ new Date()).toISOString()} ${e.message}
2629
+ `);
2630
+ } catch {
2631
+ }
2632
+ return { state: "failed", error: e.message };
2633
+ }
2634
+ }
2635
+ function refreshInBackground() {
2636
+ try {
2637
+ if (!refreshDue()) return;
2638
+ const script = process.argv[1];
2639
+ if (!script) return;
2640
+ fs16.mkdirSync(home(), { recursive: true });
2641
+ fs16.writeFileSync(attemptFile(), `${(/* @__PURE__ */ new Date()).toISOString()} refresh started
2642
+ `);
2643
+ const child = spawn2(process.execPath, [script, "prices", "--refresh", "--quiet"], { detached: true, stdio: "ignore", env: process.env });
2644
+ child.on("error", () => void 0);
2645
+ child.unref();
2646
+ } catch {
2647
+ }
2648
+ }
2649
+ var OWN = { codex: ["openai"], claude: ["anthropic"], gemini: ["google"] };
2650
+ function catalogPrice(backend, ...models) {
2651
+ const catalog5 = readCatalog();
2652
+ if (!catalog5) return void 0;
2653
+ for (const raw of models) {
2654
+ const id = raw?.split("#")[0];
2655
+ if (!id) continue;
2656
+ const keys = [id, ...[...OWN[backend] ?? [], "anthropic", "openai", "google"].map((p) => `${p}/${id}`)];
2657
+ const any = keys.find((k) => catalog5.models[k]) ?? Object.keys(catalog5.models).filter((k) => k.endsWith(`/${id}`)).sort()[0];
2658
+ const triple = any ? catalog5.models[any] : void 0;
2659
+ if (any && triple && triple.length === 3 && triple.every((n) => Number.isFinite(n) && n >= 0)) {
2660
+ const [input, output, cachedInput] = triple;
2661
+ return { input, output, cachedInput, key: any };
2662
+ }
2663
+ }
2664
+ return void 0;
2665
+ }
2666
+ function catalogStatus() {
2667
+ const eff = effective();
2668
+ const c = readCatalog();
2669
+ let lastFailure;
2670
+ try {
2671
+ const t = fs16.readFileSync(attemptFile(), "utf8").trim();
2672
+ if (t && !t.endsWith("refresh started")) lastFailure = t;
2673
+ } catch {
2674
+ }
2675
+ return {
2676
+ enabled: eff.priceFeed.value,
2677
+ url: eff.priceFeedUrl.value,
2678
+ hours: eff.priceFeedHours.value,
2679
+ file: catalogFile(),
2680
+ models: c ? Object.keys(c.models).length : void 0,
2681
+ fetchedAt: c?.fetchedAt,
2682
+ ageHours: c ? (Date.now() - Date.parse(c.fetchedAt)) / 36e5 : void 0,
2683
+ lastFailure
2684
+ };
2685
+ }
2686
+
2687
+ // src/core/receipt.ts
2499
2688
  var PRESETS = {
2500
2689
  sonnet: { name: "Claude Sonnet", input: 3, output: 15, cachedInput: 0.3 },
2501
2690
  opus: { name: "Claude Opus", input: 5, output: 25, cachedInput: 0.5 },
@@ -2508,13 +2697,31 @@ function primaryPrice() {
2508
2697
  if (custom && custom.length >= 2 && custom.every((n) => Number.isFinite(n) && n >= 0)) {
2509
2698
  return { name: "custom", input: custom[0], output: custom[1], cachedInput: custom[2] ?? custom[0] / 10 };
2510
2699
  }
2511
- return PRESETS[eff.primary.value.toLowerCase()] ?? PRESETS.sonnet;
2700
+ const named = eff.primary.value;
2701
+ const preset = PRESETS[named.toLowerCase()];
2702
+ if (preset) return preset;
2703
+ const c = eff.priceFeed.value ? catalogPrice("", named) : void 0;
2704
+ return c ? { name: named, source: "catalog", input: c.input, output: c.output, cachedInput: c.cachedInput } : PRESETS.sonnet;
2512
2705
  }
2513
2706
  var estimateTokens = (text) => Math.ceil(text.length / 4);
2707
+ function parsePriceSpec(spec, name = "worker") {
2708
+ const parts = typeof spec === "string" ? spec.split(",").map((x) => Number(x.trim())) : [];
2709
+ if (parts.length < 2 || parts.length > 3 || !parts.every((n) => Number.isFinite(n) && n >= 0)) return void 0;
2710
+ return { name, source: "config", input: parts[0], output: parts[1], cachedInput: parts[2] ?? parts[0] / 10 };
2711
+ }
2712
+ function workerPrice(backend, ...models) {
2713
+ const prices = effective().workerPrices.value;
2714
+ for (const key of [...models.filter((m) => !!m).map((m) => `${backend}:${m.split("#")[0]}`), backend]) {
2715
+ if (prices[key] !== void 0) return parsePriceSpec(prices[key], key);
2716
+ }
2717
+ const c = effective().priceFeed.value ? catalogPrice(backend, ...models) : void 0;
2718
+ return c ? { name: c.key, source: "catalog", input: c.input, output: c.output, cachedInput: c.cachedInput } : void 0;
2719
+ }
2720
+ var costAt = (usage2, price) => (usage2.input * price.input + usage2.cacheRead * price.cachedInput + (usage2.output + usage2.reasoning) * price.output) / 1e6;
2721
+ var workerCostOf = (usage2) => usage2?.cost ?? usage2?.costEstimate;
2514
2722
  function savedUsd(usage2, returnedTokens, price = primaryPrice()) {
2515
- const wouldCost = (usage2.input * price.input + usage2.cacheRead * price.cachedInput + (usage2.output + usage2.reasoning) * price.output) / 1e6;
2516
2723
  const readingTheReport = returnedTokens * price.input / 1e6;
2517
- return Math.max(0, wouldCost - (usage2.cost ?? 0) - readingTheReport);
2724
+ return Math.max(0, costAt(usage2, price) - (workerCostOf(usage2) ?? 0) - readingTheReport);
2518
2725
  }
2519
2726
  function record(meta) {
2520
2727
  if (!meta.usage?.steps || meta.auditOf) return;
@@ -2527,17 +2734,18 @@ function record(meta) {
2527
2734
  model: meta.resolvedModel ?? (meta.ran ?? meta.worker).model,
2528
2735
  tokens: meta.usage.total,
2529
2736
  returned: meta.returnedTokens ?? 0,
2530
- workerCost: meta.usage.cost ?? 0,
2737
+ workerCost: workerCostOf(meta.usage) ?? 0,
2738
+ ...meta.usage.cost === void 0 && meta.usage.costEstimate !== void 0 ? { costEstimated: true, costSource: meta.usage.costSource } : {},
2531
2739
  saved: meta.savedUsd ?? 0,
2532
2740
  price: primaryPrice().name
2533
2741
  };
2534
- fs16.mkdirSync(path14.dirname(ledgerFile()), { recursive: true });
2535
- fs16.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
2742
+ fs17.mkdirSync(path15.dirname(ledgerFile()), { recursive: true });
2743
+ fs17.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
2536
2744
  `);
2537
2745
  }
2538
2746
  function readLedger(sinceMs2) {
2539
- if (!fs16.existsSync(ledgerFile())) return [];
2540
- return fs16.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
2747
+ if (!fs17.existsSync(ledgerFile())) return [];
2748
+ return fs17.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
2541
2749
  try {
2542
2750
  return [JSON.parse(l)];
2543
2751
  } catch {
@@ -2612,7 +2820,7 @@ ${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.rou
2612
2820
  }
2613
2821
 
2614
2822
  // src/core/ui.ts
2615
- import fs17 from "node:fs";
2823
+ import fs18 from "node:fs";
2616
2824
  var WEEK_MS = 7 * 24 * 3600 * 1e3;
2617
2825
  var RECENT = 40;
2618
2826
  var RECENT_HOURS = 1;
@@ -2703,9 +2911,9 @@ function cardsFor(m) {
2703
2911
  const out = [];
2704
2912
  for (const [phase, text] of phases) {
2705
2913
  const marker = runFile(m.id, `card-${phase}`);
2706
- if (fs17.existsSync(marker)) continue;
2914
+ if (fs18.existsSync(marker)) continue;
2707
2915
  if (!(phase === "started" && !isActive(m.state))) out.push(text());
2708
- fs17.writeFileSync(marker, "");
2916
+ fs18.writeFileSync(marker, "");
2709
2917
  }
2710
2918
  return out;
2711
2919
  }
@@ -2716,10 +2924,10 @@ function hookCards(input) {
2716
2924
  const response = event.tool_response;
2717
2925
  const output = typeof response === "string" ? response : [response?.stdout, response?.stderr].filter(Boolean).join("\n");
2718
2926
  const ids = [...new Set(`${command}
2719
- ${output}`.match(RUN_ID) ?? [])].filter((id) => fs17.existsSync(`${runsDir()}/${id}`));
2927
+ ${output}`.match(RUN_ID) ?? [])].filter((id) => fs18.existsSync(`${runsDir()}/${id}`));
2720
2928
  const since2 = Date.now() - RECENT_HOURS * 36e5;
2721
2929
  for (const id of listRunIds().slice(-RECENT)) {
2722
- if (ids.includes(id) || fs17.existsSync(runFile(id, "card-ended"))) continue;
2930
+ if (ids.includes(id) || fs18.existsSync(runFile(id, "card-ended"))) continue;
2723
2931
  try {
2724
2932
  const m = readMeta(id);
2725
2933
  if (!isActive(m.state) && Date.parse(m.endedAt ?? m.startedAt) > since2) ids.push(id);
@@ -2740,11 +2948,11 @@ ${output}`.match(RUN_ID) ?? [])].filter((id) => fs17.existsSync(`${runsDir()}/${
2740
2948
  var SCHEMA_VERSION = 1;
2741
2949
  var PATCH_MAX = 1e6;
2742
2950
  var ANSWER_MAX2 = 2e5;
2743
- var cached2;
2744
- var historyFile = () => path15.join(home(), "history.db");
2951
+ var cached3;
2952
+ var historyFile = () => path16.join(home(), "history.db");
2745
2953
  function openDb() {
2746
2954
  const file2 = historyFile();
2747
- if (cached2?.file === file2) return cached2.db;
2955
+ if (cached3?.file === file2) return cached3.db;
2748
2956
  let db;
2749
2957
  try {
2750
2958
  const emit = process.emitWarning;
@@ -2755,14 +2963,14 @@ function openDb() {
2755
2963
  } finally {
2756
2964
  process.emitWarning = emit;
2757
2965
  }
2758
- fs18.mkdirSync(path15.dirname(file2), { recursive: true });
2966
+ fs19.mkdirSync(path16.dirname(file2), { recursive: true });
2759
2967
  db = new DatabaseSync(file2);
2760
2968
  db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
2761
2969
  migrate(db);
2762
2970
  } catch {
2763
2971
  db = void 0;
2764
2972
  }
2765
- cached2 = { file: file2, db };
2973
+ cached3 = { file: file2, db };
2766
2974
  return db;
2767
2975
  }
2768
2976
  function migrate(db) {
@@ -2790,31 +2998,31 @@ function migrate(db) {
2790
2998
  function readRunFile(id, name) {
2791
2999
  const f = runFile(id, name);
2792
3000
  try {
2793
- if (fs18.existsSync(f)) return fs18.readFileSync(f, "utf8");
2794
- if (fs18.existsSync(`${f}.gz`)) return zlib.gunzipSync(fs18.readFileSync(`${f}.gz`)).toString("utf8");
3001
+ if (fs19.existsSync(f)) return fs19.readFileSync(f, "utf8");
3002
+ if (fs19.existsSync(`${f}.gz`)) return zlib.gunzipSync(fs19.readFileSync(`${f}.gz`)).toString("utf8");
2795
3003
  } catch {
2796
3004
  }
2797
3005
  return void 0;
2798
3006
  }
2799
3007
  function gzipFile(f) {
2800
- if (!fs18.existsSync(f)) return;
2801
- fs18.writeFileSync(`${f}.gz`, zlib.gzipSync(fs18.readFileSync(f)));
2802
- fs18.rmSync(f);
3008
+ if (!fs19.existsSync(f)) return;
3009
+ fs19.writeFileSync(`${f}.gz`, zlib.gzipSync(fs19.readFileSync(f)));
3010
+ fs19.rmSync(f);
2803
3011
  }
2804
3012
  function compactRun(meta) {
2805
3013
  if (!TERMINAL.includes(meta.state)) return;
2806
- const dir = path15.dirname(runFile(meta.id, "meta.json"));
3014
+ const dir = path16.dirname(runFile(meta.id, "meta.json"));
2807
3015
  let names;
2808
3016
  try {
2809
- names = fs18.readdirSync(dir);
3017
+ names = fs19.readdirSync(dir);
2810
3018
  } catch {
2811
3019
  return;
2812
3020
  }
2813
3021
  for (const n of names) {
2814
- if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(path15.join(dir, n));
3022
+ if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(path16.join(dir, n));
2815
3023
  else if (/^stderr(\.attempt-\d+)?\.log$/.test(n)) {
2816
- if (meta.state === "done" && !/attempt/.test(n)) fs18.rmSync(path15.join(dir, n), { force: true });
2817
- else gzipFile(path15.join(dir, n));
3024
+ if (meta.state === "done" && !/attempt/.test(n)) fs19.rmSync(path16.join(dir, n), { force: true });
3025
+ else gzipFile(path16.join(dir, n));
2818
3026
  }
2819
3027
  }
2820
3028
  }
@@ -2858,7 +3066,7 @@ function recordRun(meta) {
2858
3066
  tool_calls: u?.toolCalls ?? null,
2859
3067
  tokens: u?.total ?? null,
2860
3068
  returned_tokens: meta.returnedTokens ?? null,
2861
- cost: u?.cost ?? null,
3069
+ cost: u?.cost ?? u?.costEstimate ?? null,
2862
3070
  saved: meta.savedUsd ?? null,
2863
3071
  files_changed: meta.changes?.length ?? 0,
2864
3072
  applied: meta.applied ? 1 : 0,
@@ -3071,11 +3279,11 @@ function historyStats(sinceMs2) {
3071
3279
  }
3072
3280
 
3073
3281
  // src/core/report.ts
3074
- import fs20 from "node:fs";
3282
+ import fs21 from "node:fs";
3075
3283
 
3076
3284
  // src/core/plan.ts
3077
- import fs19 from "node:fs";
3078
- import path16 from "node:path";
3285
+ import fs20 from "node:fs";
3286
+ import path17 from "node:path";
3079
3287
  var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
3080
3288
  var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
3081
3289
  function structure(lines) {
@@ -3122,9 +3330,9 @@ function parsePlan(text, file2 = "") {
3122
3330
  return { file: file2, title: titleHeading?.text ?? "", header, constraints, tasks };
3123
3331
  }
3124
3332
  function loadPlan(file2) {
3125
- const abs = path16.resolve(file2);
3126
- if (!fs19.existsSync(abs) || !fs19.statSync(abs).isFile()) throw new UserError(`plan not found: ${file2}`);
3127
- const plan = parsePlan(fs19.readFileSync(abs, "utf8"), fs19.realpathSync(abs));
3333
+ const abs = path17.resolve(file2);
3334
+ if (!fs20.existsSync(abs) || !fs20.statSync(abs).isFile()) throw new UserError(`plan not found: ${file2}`);
3335
+ const plan = parsePlan(fs20.readFileSync(abs, "utf8"), fs20.realpathSync(abs));
3128
3336
  if (!plan.tasks.length) throw new UserError(`${file2} has no "Task N" headings (see pitroom-writing-plans)`);
3129
3337
  return plan;
3130
3338
  }
@@ -3143,7 +3351,7 @@ function brief(plan, task) {
3143
3351
  ].filter(Boolean).join("\n\n")}
3144
3352
  `;
3145
3353
  }
3146
- var planName = (file2) => path16.basename(file2).replace(/\.md$/i, "");
3354
+ var planName = (file2) => path17.basename(file2).replace(/\.md$/i, "");
3147
3355
 
3148
3356
  // src/core/style.ts
3149
3357
  var enabled = () => {
@@ -3203,7 +3411,7 @@ function duration(meta) {
3203
3411
  }
3204
3412
  function readSummary(meta) {
3205
3413
  const f = runFile(meta.id, "summary.md");
3206
- return fs20.existsSync(f) ? fs20.readFileSync(f, "utf8").trim() : archivedRun(meta.id)?.answer ?? "";
3414
+ return fs21.existsSync(f) ? fs21.readFileSync(f, "utf8").trim() : archivedRun(meta.id)?.answer ?? "";
3207
3415
  }
3208
3416
  function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
3209
3417
  const out = [];
@@ -3246,7 +3454,7 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
3246
3454
  if (u && u.steps) {
3247
3455
  const ratio = meta.returnedTokens ? `, ${Math.max(1, Math.round(u.total / meta.returnedTokens))}\xD7 compression` : "";
3248
3456
  out.push(
3249
- `\u2500\u2500 receipt: worker processed ${compact(u.total)} tokens in ${plural(u.steps, "step")} (${plural(u.toolCalls, "tool call")}${u.denied ? `, ${u.denied} blocked` : ""}) \xB7 worker cost ${u.cost === void 0 ? "n/a" : usd(u.cost)} \xB7 returned ~${compact(meta.returnedTokens ?? 0)} tokens${ratio}` + (meta.savedUsd !== void 0 ? ` \xB7 est. saved ${usd(meta.savedUsd)} vs ${primaryPrice().name}` : "")
3457
+ `\u2500\u2500 receipt: worker processed ${compact(u.total)} tokens in ${plural(u.steps, "step")} (${plural(u.toolCalls, "tool call")}${u.denied ? `, ${u.denied} blocked` : ""}) \xB7 worker cost ${u.cost !== void 0 ? usd(u.cost) : u.costEstimate !== void 0 ? `~${usd(u.costEstimate)} (estimated from ${u.costSource === "catalog" ? `${catalogLabel()} prices` : "your workerPrices"})` : "n/a"} \xB7 returned ~${compact(meta.returnedTokens ?? 0)} tokens${ratio}` + (meta.savedUsd !== void 0 ? ` \xB7 est. saved ${usd(meta.savedUsd)} vs ${primaryPrice().name}` : "")
3250
3458
  );
3251
3459
  }
3252
3460
  if (meta.changes) {
@@ -3289,8 +3497,8 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
3289
3497
  }
3290
3498
  function live(meta) {
3291
3499
  const f = runFile(meta.id, "events.jsonl");
3292
- if (!fs20.existsSync(f)) return { steps: 0, toolCalls: 0 };
3293
- const p = getBackend((meta.ran ?? meta.worker).backend).parse(fs20.readFileSync(f, "utf8"));
3500
+ if (!fs21.existsSync(f)) return { steps: 0, toolCalls: 0 };
3501
+ const p = getBackend((meta.ran ?? meta.worker).backend).parse(fs21.readFileSync(f, "utf8"));
3294
3502
  return { steps: p.usage.steps, toolCalls: p.usage.toolCalls, last: p.lastActivity };
3295
3503
  }
3296
3504
  function progress(meta) {
@@ -3450,12 +3658,12 @@ function claimGroup(group) {
3450
3658
  try {
3451
3659
  const ids = groupIds(group).sort();
3452
3660
  if (ids.length < 2) return true;
3453
- const dir = path17.join(home(), "notified");
3454
- fs21.mkdirSync(dir, { recursive: true });
3455
- for (const f of fs21.readdirSync(dir)) {
3456
- if (Date.now() - fs21.statSync(path17.join(dir, f)).mtimeMs > 30 * 864e5) fs21.rmSync(path17.join(dir, f), { force: true });
3661
+ const dir = path18.join(home(), "notified");
3662
+ fs22.mkdirSync(dir, { recursive: true });
3663
+ for (const f of fs22.readdirSync(dir)) {
3664
+ if (Date.now() - fs22.statSync(path18.join(dir, f)).mtimeMs > 30 * 864e5) fs22.rmSync(path18.join(dir, f), { force: true });
3457
3665
  }
3458
- fs21.writeFileSync(path17.join(dir, crypto5.createHash("sha1").update(ids.join(",")).digest("hex").slice(0, 16)), "", { flag: "wx" });
3666
+ fs22.writeFileSync(path18.join(dir, crypto5.createHash("sha1").update(ids.join(",")).digest("hex").slice(0, 16)), "", { flag: "wx" });
3459
3667
  return true;
3460
3668
  } catch {
3461
3669
  return false;
@@ -3467,12 +3675,12 @@ function send(notice, meta) {
3467
3675
  try {
3468
3676
  let child;
3469
3677
  if (command) {
3470
- child = spawn2(command, { shell: true, env, detached: true, stdio: "ignore" });
3678
+ child = spawn3(command, { shell: true, env, detached: true, stdio: "ignore" });
3471
3679
  } else if (process.platform === "darwin") {
3472
- child = spawn2("osascript", ["-e", "on run argv\ndisplay notification (item 2 of argv) with title (item 1 of argv)\nend run", notice.title, notice.body], { env, detached: true, stdio: "ignore" });
3680
+ child = spawn3("osascript", ["-e", "on run argv\ndisplay notification (item 2 of argv) with title (item 1 of argv)\nend run", notice.title, notice.body], { env, detached: true, stdio: "ignore" });
3473
3681
  } else if (process.platform === "linux") {
3474
3682
  const plain2 = (t) => t.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3475
- child = spawn2("notify-send", ["--", plain2(notice.title), plain2(notice.body)], { env, detached: true, stdio: "ignore" });
3683
+ child = spawn3("notify-send", ["--", plain2(notice.title), plain2(notice.body)], { env, detached: true, stdio: "ignore" });
3476
3684
  } else {
3477
3685
  return;
3478
3686
  }
@@ -3489,30 +3697,30 @@ function notifyEnded(meta) {
3489
3697
  }
3490
3698
 
3491
3699
  // src/core/templates.ts
3492
- import fs23 from "node:fs";
3493
- import path19 from "node:path";
3700
+ import fs24 from "node:fs";
3701
+ import path20 from "node:path";
3494
3702
 
3495
3703
  // src/core/install.ts
3496
- import fs22 from "node:fs";
3704
+ import fs23 from "node:fs";
3497
3705
  import os9 from "node:os";
3498
- import path18 from "node:path";
3706
+ import path19 from "node:path";
3499
3707
  import { fileURLToPath as fileURLToPath2 } from "node:url";
3500
3708
  var LEGACY = ["pitroom", "opencode-worker"];
3501
3709
  function packageRoot() {
3502
- return path18.resolve(path18.dirname(fileURLToPath2(import.meta.url)), "..");
3710
+ return path19.resolve(path19.dirname(fileURLToPath2(import.meta.url)), "..");
3503
3711
  }
3504
3712
  function skillNames(root = packageRoot()) {
3505
- const dir = path18.join(root, "skills");
3506
- if (!fs22.existsSync(dir)) return [];
3507
- return fs22.readdirSync(dir).filter((d) => fs22.existsSync(path18.join(dir, d, "SKILL.md"))).sort();
3713
+ const dir = path19.join(root, "skills");
3714
+ if (!fs23.existsSync(dir)) return [];
3715
+ return fs23.readdirSync(dir).filter((d) => fs23.existsSync(path19.join(dir, d, "SKILL.md"))).sort();
3508
3716
  }
3509
3717
  function skillTargets() {
3510
3718
  const home3 = os9.homedir();
3511
- const targets = [path18.join(home3, ".agents", "skills")];
3512
- if (fs22.existsSync(path18.join(home3, ".claude"))) targets.push(path18.join(home3, ".claude", "skills"));
3719
+ const targets = [path19.join(home3, ".agents", "skills")];
3720
+ if (fs23.existsSync(path19.join(home3, ".claude"))) targets.push(path19.join(home3, ".claude", "skills"));
3513
3721
  return targets;
3514
3722
  }
3515
- var launcherPath = () => path18.join(os9.homedir(), ".local", "bin", process.platform === "win32" ? "pitroom.cmd" : "pitroom");
3723
+ var launcherPath = () => path19.join(os9.homedir(), ".local", "bin", process.platform === "win32" ? "pitroom.cmd" : "pitroom");
3516
3724
  var LAUNCHER_MARK = "# pitroom launcher";
3517
3725
  function launcherScript(bundle) {
3518
3726
  if (process.platform === "win32") {
@@ -3542,7 +3750,7 @@ exit 127
3542
3750
  function isOurLauncher(file2, root) {
3543
3751
  if (linksInto(file2, root)) return true;
3544
3752
  try {
3545
- const s = fs22.readFileSync(file2, "utf8");
3753
+ const s = fs23.readFileSync(file2, "utf8");
3546
3754
  return s.includes(LAUNCHER_MARK) && s.includes(root);
3547
3755
  } catch {
3548
3756
  return false;
@@ -3550,64 +3758,64 @@ function isOurLauncher(file2, root) {
3550
3758
  }
3551
3759
  function isPitroomLauncher(file2) {
3552
3760
  try {
3553
- return fs22.readFileSync(file2, "utf8").includes(LAUNCHER_MARK);
3761
+ return fs23.readFileSync(file2, "utf8").includes(LAUNCHER_MARK);
3554
3762
  } catch {
3555
3763
  return false;
3556
3764
  }
3557
3765
  }
3558
3766
  function placeLauncher(bundle, root, force) {
3559
3767
  const dest = launcherPath();
3560
- fs22.mkdirSync(path18.dirname(dest), { recursive: true });
3561
- const exists = fs22.lstatSync(dest, { throwIfNoEntry: false });
3768
+ fs23.mkdirSync(path19.dirname(dest), { recursive: true });
3769
+ const exists = fs23.lstatSync(dest, { throwIfNoEntry: false });
3562
3770
  if (exists && !isOurLauncher(dest, root)) {
3563
3771
  if (!force) return `! ${dest} exists and is not Pitroom's launcher; kept (use --force to back it up and replace)`;
3564
- fs22.renameSync(dest, `${dest}.bak-${Date.now()}`);
3772
+ fs23.renameSync(dest, `${dest}.bak-${Date.now()}`);
3565
3773
  } else if (exists) {
3566
- fs22.rmSync(dest, { force: true });
3774
+ fs23.rmSync(dest, { force: true });
3567
3775
  }
3568
- fs22.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
3776
+ fs23.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
3569
3777
  return `\u2714 ${dest} \u2192 launcher for ${bundle} (Node 22.13+)`;
3570
3778
  }
3571
3779
  function linksInto(link, dir) {
3572
- const st = fs22.lstatSync(link, { throwIfNoEntry: false });
3780
+ const st = fs23.lstatSync(link, { throwIfNoEntry: false });
3573
3781
  if (!st?.isSymbolicLink()) return false;
3574
- const target = path18.resolve(path18.dirname(link), fs22.readlinkSync(link));
3575
- const rel = path18.relative(dir, target);
3576
- return !rel.startsWith("..") && !path18.isAbsolute(rel);
3782
+ const target = path19.resolve(path19.dirname(link), fs23.readlinkSync(link));
3783
+ const rel = path19.relative(dir, target);
3784
+ return !rel.startsWith("..") && !path19.isAbsolute(rel);
3577
3785
  }
3578
3786
  function place(src, dest, opts) {
3579
- fs22.mkdirSync(path18.dirname(dest), { recursive: true });
3580
- const st = fs22.lstatSync(dest, { throwIfNoEntry: false });
3787
+ fs23.mkdirSync(path19.dirname(dest), { recursive: true });
3788
+ const st = fs23.lstatSync(dest, { throwIfNoEntry: false });
3581
3789
  if (st && !st.isSymbolicLink()) {
3582
3790
  if (!opts.force) return `! ${dest} exists and is not a link; kept (use --force to back it up and replace)`;
3583
- fs22.renameSync(dest, `${dest}.bak-${Date.now()}`);
3791
+ fs23.renameSync(dest, `${dest}.bak-${Date.now()}`);
3584
3792
  } else if (st) {
3585
- fs22.unlinkSync(dest);
3793
+ fs23.unlinkSync(dest);
3586
3794
  }
3587
- if (opts.copy) fs22.cpSync(src, dest, { recursive: true });
3588
- else fs22.symlinkSync(src, dest, process.platform === "win32" ? "junction" : fs22.statSync(src).isDirectory() ? "dir" : "file");
3795
+ if (opts.copy) fs23.cpSync(src, dest, { recursive: true });
3796
+ else fs23.symlinkSync(src, dest, process.platform === "win32" ? "junction" : fs23.statSync(src).isDirectory() ? "dir" : "file");
3589
3797
  return `\u2714 ${dest} \u2192 ${opts.copy ? "copied" : src}`;
3590
3798
  }
3591
3799
  function install(opts) {
3592
3800
  const root = packageRoot();
3593
3801
  const skills2 = opts.skills === false ? [] : skillNames(root);
3594
- if (opts.skills !== false && !skills2.length) throw new Error(`no skills found under ${path18.join(root, "skills")}`);
3802
+ if (opts.skills !== false && !skills2.length) throw new Error(`no skills found under ${path19.join(root, "skills")}`);
3595
3803
  const out = [];
3596
3804
  for (const base2 of opts.skills === false ? [] : skillTargets()) {
3597
3805
  for (const legacy of LEGACY) {
3598
- const l = path18.join(base2, legacy);
3806
+ const l = path19.join(base2, legacy);
3599
3807
  if (!skills2.includes(legacy) && linksInto(l, root)) {
3600
- fs22.unlinkSync(l);
3808
+ fs23.unlinkSync(l);
3601
3809
  out.push(`\u2714 removed old link ${l}`);
3602
3810
  }
3603
3811
  }
3604
- for (const name of skills2) out.push(place(path18.join(root, "skills", name), path18.join(base2, name), opts));
3812
+ for (const name of skills2) out.push(place(path19.join(root, "skills", name), path19.join(base2, name), opts));
3605
3813
  }
3606
- const bundle = path18.join(root, "dist", "pitroom.mjs");
3607
- if (fs22.existsSync(bundle)) {
3814
+ const bundle = path19.join(root, "dist", "pitroom.mjs");
3815
+ if (fs23.existsSync(bundle)) {
3608
3816
  out.push(placeLauncher(bundle, root, opts.force));
3609
- const onPath = (process.env.PATH ?? "").split(path18.delimiter).some((d) => path18.resolve(d) === path18.dirname(launcherPath()));
3610
- if (!onPath) out.push(`! ${path18.dirname(launcherPath())} is not on PATH; add it, or run ${launcherPath()} directly`);
3817
+ const onPath = (process.env.PATH ?? "").split(path19.delimiter).some((d) => path19.resolve(d) === path19.dirname(launcherPath()));
3818
+ if (!onPath) out.push(`! ${path19.dirname(launcherPath())} is not on PATH; add it, or run ${launcherPath()} directly`);
3611
3819
  }
3612
3820
  return out;
3613
3821
  }
@@ -3615,24 +3823,24 @@ function uninstall() {
3615
3823
  const root = packageRoot();
3616
3824
  const out = [];
3617
3825
  for (const base2 of skillTargets()) {
3618
- if (!fs22.existsSync(base2)) continue;
3619
- for (const name of fs22.readdirSync(base2)) {
3620
- const l = path18.join(base2, name);
3826
+ if (!fs23.existsSync(base2)) continue;
3827
+ for (const name of fs23.readdirSync(base2)) {
3828
+ const l = path19.join(base2, name);
3621
3829
  if (linksInto(l, root)) {
3622
- fs22.unlinkSync(l);
3830
+ fs23.unlinkSync(l);
3623
3831
  out.push(`\u2714 removed ${l}`);
3624
3832
  }
3625
3833
  }
3626
3834
  }
3627
3835
  if (isOurLauncher(launcherPath(), root)) {
3628
- fs22.rmSync(launcherPath(), { force: true });
3836
+ fs23.rmSync(launcherPath(), { force: true });
3629
3837
  out.push(`\u2714 removed ${launcherPath()}`);
3630
3838
  }
3631
3839
  return out.length ? out : ["nothing to remove"];
3632
3840
  }
3633
3841
  function installedSkills() {
3634
3842
  const names = skillNames();
3635
- return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs22.existsSync(path18.join(base2, n, "SKILL.md"))) }));
3843
+ return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs23.existsSync(path19.join(base2, n, "SKILL.md"))) }));
3636
3844
  }
3637
3845
 
3638
3846
  // src/core/templates.ts
@@ -3643,9 +3851,9 @@ var FILES = {
3643
3851
  "code-reviewer": "pitroom-review/code-reviewer.md"
3644
3852
  };
3645
3853
  function loadTemplate(name, root = packageRoot()) {
3646
- const file2 = path19.join(root, "skills", FILES[name]);
3647
- if (!fs23.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
3648
- return fs23.readFileSync(file2, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
3854
+ const file2 = path20.join(root, "skills", FILES[name]);
3855
+ if (!fs24.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
3856
+ return fs24.readFileSync(file2, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
3649
3857
  }
3650
3858
  function fill(template, values) {
3651
3859
  const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
@@ -3655,10 +3863,10 @@ function fill(template, values) {
3655
3863
 
3656
3864
  // src/core/slots.ts
3657
3865
  import crypto6 from "node:crypto";
3658
- import fs24 from "node:fs";
3659
- import path20 from "node:path";
3660
- var slotsDir = () => path20.join(home(), "slots");
3661
- var locksDir = () => path20.join(home(), "locks");
3866
+ import fs25 from "node:fs";
3867
+ import path21 from "node:path";
3868
+ var slotsDir = () => path21.join(home(), "slots");
3869
+ var locksDir = () => path21.join(home(), "locks");
3662
3870
  var STARTUP_GRACE_MS = 3e4;
3663
3871
  function holderActive(runId, self) {
3664
3872
  if (!runId || runId === self) return false;
@@ -3672,23 +3880,23 @@ function holderActive(runId, self) {
3672
3880
  }
3673
3881
  }
3674
3882
  function tryClaim(file2, runId) {
3675
- fs24.mkdirSync(path20.dirname(file2), { recursive: true });
3883
+ fs25.mkdirSync(path21.dirname(file2), { recursive: true });
3676
3884
  try {
3677
- fs24.writeFileSync(file2, runId, { flag: "wx" });
3885
+ fs25.writeFileSync(file2, runId, { flag: "wx" });
3678
3886
  return true;
3679
3887
  } catch (e) {
3680
3888
  if (e.code !== "EEXIST") throw e;
3681
3889
  }
3682
3890
  let owner = "";
3683
3891
  try {
3684
- owner = fs24.readFileSync(file2, "utf8").trim();
3892
+ owner = fs25.readFileSync(file2, "utf8").trim();
3685
3893
  } catch {
3686
3894
  }
3687
3895
  if (owner === runId) return true;
3688
3896
  if (holderActive(owner, runId)) return false;
3689
- fs24.rmSync(file2, { force: true });
3897
+ fs25.rmSync(file2, { force: true });
3690
3898
  try {
3691
- fs24.writeFileSync(file2, runId, { flag: "wx" });
3899
+ fs25.writeFileSync(file2, runId, { flag: "wx" });
3692
3900
  return true;
3693
3901
  } catch {
3694
3902
  return false;
@@ -3697,25 +3905,25 @@ function tryClaim(file2, runId) {
3697
3905
  function releaseIfOwner(file2, runId) {
3698
3906
  if (!file2) return;
3699
3907
  try {
3700
- if (fs24.readFileSync(file2, "utf8").trim() === runId) fs24.rmSync(file2, { force: true });
3908
+ if (fs25.readFileSync(file2, "utf8").trim() === runId) fs25.rmSync(file2, { force: true });
3701
3909
  } catch {
3702
3910
  }
3703
3911
  }
3704
3912
  function tryAcquireSlot(runId, maxParallel) {
3705
3913
  for (let n = 0; n < Math.max(1, maxParallel); n++) {
3706
- const file2 = path20.join(slotsDir(), `slot-${n}`);
3914
+ const file2 = path21.join(slotsDir(), `slot-${n}`);
3707
3915
  if (tryClaim(file2, runId)) return file2;
3708
3916
  }
3709
3917
  return void 0;
3710
3918
  }
3711
3919
  var releaseSlot = (file2, runId) => releaseIfOwner(file2, runId);
3712
- var lockFile = (repoRoot2) => path20.join(locksDir(), `write-${crypto6.createHash("sha1").update(path20.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
3920
+ var lockFile = (repoRoot2) => path21.join(locksDir(), `write-${crypto6.createHash("sha1").update(path21.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
3713
3921
  function acquireWriteLock(repoRoot2, runId) {
3714
3922
  const file2 = lockFile(repoRoot2);
3715
3923
  if (tryClaim(file2, runId)) return;
3716
3924
  let owner = "";
3717
3925
  try {
3718
- owner = fs24.readFileSync(file2, "utf8").trim();
3926
+ owner = fs25.readFileSync(file2, "utf8").trim();
3719
3927
  } catch {
3720
3928
  }
3721
3929
  throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
@@ -3762,8 +3970,8 @@ ${task}`;
3762
3970
  }
3763
3971
 
3764
3972
  // src/core/refs.ts
3765
- import fs25 from "node:fs";
3766
- import path21 from "node:path";
3973
+ import fs26 from "node:fs";
3974
+ import path22 from "node:path";
3767
3975
  var FILE_EXTENSIONS = new Set(
3768
3976
  "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(" ")
3769
3977
  );
@@ -3814,13 +4022,13 @@ function extractRefs(answer) {
3814
4022
  return [...seen.values()];
3815
4023
  }
3816
4024
  function verifyRefs(refs, dirs) {
3817
- const roots = [...new Set(dirs.filter(Boolean).map((d) => path21.resolve(d)))];
4025
+ const roots = [...new Set(dirs.filter(Boolean).map((d) => path22.resolve(d)))];
3818
4026
  const invalid = [];
3819
4027
  const lineCache = /* @__PURE__ */ new Map();
3820
4028
  const load = (file2) => {
3821
4029
  if (!lineCache.has(file2)) {
3822
- const size = fs25.statSync(file2).size;
3823
- const lines = size > MAX_BYTES ? null : fs25.readFileSync(file2, "utf8").split("\n");
4030
+ const size = fs26.statSync(file2).size;
4031
+ const lines = size > MAX_BYTES ? null : fs26.readFileSync(file2, "utf8").split("\n");
3824
4032
  if (lines && lines[lines.length - 1] === "") lines.pop();
3825
4033
  lineCache.set(file2, lines);
3826
4034
  }
@@ -3831,9 +4039,9 @@ function verifyRefs(refs, dirs) {
3831
4039
  const candidates = (ref) => {
3832
4040
  const direct = resolve(ref.file, roots);
3833
4041
  if (direct) return [direct];
3834
- if (path21.isAbsolute(ref.file)) return [];
4042
+ if (path22.isAbsolute(ref.file)) return [];
3835
4043
  const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
3836
- return (byName().get(path21.basename(wanted)) ?? []).filter((f) => f.split(path21.sep).join("/").endsWith(`/${wanted}`) || path21.basename(f) === wanted).slice(0, 20);
4044
+ return (byName().get(path22.basename(wanted)) ?? []).filter((f) => f.split(path22.sep).join("/").endsWith(`/${wanted}`) || path22.basename(f) === wanted).slice(0, 20);
3837
4045
  };
3838
4046
  const check = (file2, ref) => {
3839
4047
  const lines = load(file2);
@@ -3848,7 +4056,7 @@ function verifyRefs(refs, dirs) {
3848
4056
  if (!files.length) {
3849
4057
  if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
3850
4058
  total++;
3851
- invalid.push({ ref: ref.text, reason: path21.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
4059
+ invalid.push({ ref: ref.text, reason: path22.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
3852
4060
  continue;
3853
4061
  }
3854
4062
  total++;
@@ -3869,7 +4077,7 @@ function enclosedBy(lines, ref) {
3869
4077
  return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
3870
4078
  }
3871
4079
  var isFileName = (name) => {
3872
- const base2 = path21.basename(name);
4080
+ const base2 = path22.basename(name);
3873
4081
  return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
3874
4082
  };
3875
4083
  function indexFiles(roots) {
@@ -3878,19 +4086,19 @@ function indexFiles(roots) {
3878
4086
  const walk = (dir) => {
3879
4087
  let entries;
3880
4088
  try {
3881
- entries = fs25.readdirSync(dir, { withFileTypes: true });
4089
+ entries = fs26.readdirSync(dir, { withFileTypes: true });
3882
4090
  } catch {
3883
4091
  return;
3884
4092
  }
3885
4093
  for (const e of entries) {
3886
4094
  if (count >= MAX_INDEXED) return;
3887
4095
  if (e.isDirectory()) {
3888
- if (!SKIP_DIRS2.has(e.name)) walk(path21.join(dir, e.name));
4096
+ if (!SKIP_DIRS2.has(e.name)) walk(path22.join(dir, e.name));
3889
4097
  } else if (e.isFile()) {
3890
4098
  count++;
3891
4099
  const list2 = index.get(e.name);
3892
- if (list2) list2.push(path21.join(dir, e.name));
3893
- else index.set(e.name, [path21.join(dir, e.name)]);
4100
+ if (list2) list2.push(path22.join(dir, e.name));
4101
+ else index.set(e.name, [path22.join(dir, e.name)]);
3894
4102
  }
3895
4103
  }
3896
4104
  };
@@ -3898,10 +4106,10 @@ function indexFiles(roots) {
3898
4106
  return index;
3899
4107
  }
3900
4108
  function resolve(ref, roots) {
3901
- const candidates = path21.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path21.join(r, ref));
4109
+ const candidates = path22.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path22.join(r, ref));
3902
4110
  return candidates.find((c) => {
3903
4111
  try {
3904
- return fs25.statSync(c).isFile() && inside(c, roots);
4112
+ return fs26.statSync(c).isFile() && inside(c, roots);
3905
4113
  } catch {
3906
4114
  return false;
3907
4115
  }
@@ -3909,14 +4117,14 @@ function resolve(ref, roots) {
3909
4117
  }
3910
4118
  function inside(file2, roots) {
3911
4119
  return roots.some((r) => {
3912
- const rel = path21.relative(r, path21.resolve(file2));
3913
- return rel !== "" && !rel.startsWith("..") && !path21.isAbsolute(rel);
4120
+ const rel = path22.relative(r, path22.resolve(file2));
4121
+ return rel !== "" && !rel.startsWith("..") && !path22.isAbsolute(rel);
3914
4122
  });
3915
4123
  }
3916
4124
  var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3917
4125
 
3918
4126
  // src/core/run.ts
3919
- var VERSION2 = true ? "0.20.0" : "0.0.0-dev";
4127
+ var VERSION2 = true ? "0.22.0" : "0.0.0-dev";
3920
4128
  function planWork(o) {
3921
4129
  if (!o.plan) return { task: o.task.trim(), tier: o.tier };
3922
4130
  if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
@@ -3948,7 +4156,7 @@ function prepareRun(o) {
3948
4156
  parent = readMeta(o.continueFrom);
3949
4157
  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");
3950
4158
  if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
3951
- if (parent.mode === "isolate" && (!parent.worktree || !fs26.existsSync(parent.worktree))) {
4159
+ if (parent.mode === "isolate" && (!parent.worktree || !fs27.existsSync(parent.worktree))) {
3952
4160
  throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
3953
4161
  }
3954
4162
  const ran = parent.ran ?? parent.worker;
@@ -3967,18 +4175,18 @@ function prepareRun(o) {
3967
4175
  throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
3968
4176
  }
3969
4177
  const mode = parent?.mode ?? o.mode;
3970
- const dir = canonical(path22.resolve(parent?.dir ?? o.dir));
3971
- if (!fs26.existsSync(dir) || !fs26.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
4178
+ const dir = canonical(path23.resolve(parent?.dir ?? o.dir));
4179
+ if (!fs27.existsSync(dir) || !fs27.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
3972
4180
  const root = repoRoot(dir);
3973
4181
  if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
3974
4182
  if (mode === "write" && !root && !o.allowNonGit) {
3975
4183
  throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
3976
4184
  }
3977
- const files = o.files.map((f) => path22.resolve(f));
3978
- for (const f of files) if (!fs26.existsSync(f)) throw new UserError(`file not found: ${f}`);
4185
+ const files = o.files.map((f) => path23.resolve(f));
4186
+ for (const f of files) if (!fs27.existsSync(f)) throw new UserError(`file not found: ${f}`);
3979
4187
  const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
3980
4188
  const snap = mode === "read" && !o.review && !o.inPlace && !parent ? wantSnapshot(readIn2, root, dir) : void 0;
3981
- if (parent?.snapshot?.dir && !fs26.existsSync(parent.snapshot.dir)) {
4189
+ if (parent?.snapshot?.dir && !fs27.existsSync(parent.snapshot.dir)) {
3982
4190
  throw new UserError(`the read snapshot of run ${parent.id} is gone (cleaned up); ask the question again as a new run`, 3);
3983
4191
  }
3984
4192
  if (!parent && !snap && !process.env.PITROOM_NO_SECRET_WARNING) {
@@ -4025,13 +4233,13 @@ function prepareRun(o) {
4025
4233
  try {
4026
4234
  acquireWriteLock(root, meta.id);
4027
4235
  } catch (e) {
4028
- fs26.rmSync(runDir(meta.id), { recursive: true, force: true });
4236
+ fs27.rmSync(runDir(meta.id), { recursive: true, force: true });
4029
4237
  throw e;
4030
4238
  }
4031
4239
  }
4032
- fs26.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
4240
+ fs27.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
4033
4241
  `);
4034
- if (work.brief) fs26.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
4242
+ if (work.brief) fs27.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
4035
4243
  return meta;
4036
4244
  }
4037
4245
  var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -4061,7 +4269,7 @@ function startInBackground(meta) {
4061
4269
  if (!script) throw new Error("cannot locate the pitroom executable");
4062
4270
  meta.background = true;
4063
4271
  writeMeta(meta);
4064
- const child = spawn3(process.execPath, [script, "__exec", meta.id], {
4272
+ const child = spawn4(process.execPath, [script, "__exec", meta.id], {
4065
4273
  detached: true,
4066
4274
  stdio: "ignore",
4067
4275
  cwd: meta.dir,
@@ -4113,7 +4321,7 @@ async function execute(meta) {
4113
4321
  (meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
4114
4322
  for (const f of ["events.jsonl", "stderr.log"]) {
4115
4323
  const from = runFile(meta.id, f);
4116
- if (fs26.existsSync(from)) fs26.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
4324
+ if (fs27.existsSync(from)) fs27.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
4117
4325
  }
4118
4326
  writeMeta(meta);
4119
4327
  }
@@ -4177,8 +4385,8 @@ function prepareTree(meta) {
4177
4385
  const s = readSnapshot(root);
4178
4386
  meta.snapshot.dir = s.dir;
4179
4387
  meta.snapshot.tree = s.tree;
4180
- meta.cwd = path22.join(s.dir, path22.relative(root, meta.dir));
4181
- if (!fs26.existsSync(meta.cwd)) meta.cwd = s.dir;
4388
+ meta.cwd = path23.join(s.dir, path23.relative(root, meta.dir));
4389
+ if (!fs27.existsSync(meta.cwd)) meta.cwd = s.dir;
4182
4390
  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`);
4183
4391
  } catch (e) {
4184
4392
  meta.snapshot = void 0;
@@ -4188,9 +4396,9 @@ function prepareTree(meta) {
4188
4396
  if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
4189
4397
  if (root && meta.mode === "isolate" && !meta.worktree) {
4190
4398
  meta.baseTree = snapshotTree(root);
4191
- meta.worktree = path22.join(worktreesDir(), meta.id);
4399
+ meta.worktree = path23.join(worktreesDir(), meta.id);
4192
4400
  createIsolatedCopy(root, meta.baseTree, meta.worktree);
4193
- meta.cwd = path22.join(meta.worktree, path22.relative(root, meta.dir));
4401
+ meta.cwd = path23.join(meta.worktree, path23.relative(root, meta.dir));
4194
4402
  const linked = linkIntoWorktree(root, meta.worktree, meta.link);
4195
4403
  if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
4196
4404
  }
@@ -4213,7 +4421,7 @@ function attempt(meta, backend, target) {
4213
4421
  timeoutSec: meta.timeoutSec
4214
4422
  });
4215
4423
  }
4216
- var read2 = (f) => fs26.existsSync(f) ? fs26.readFileSync(f, "utf8") : "";
4424
+ var read2 = (f) => fs27.existsSync(f) ? fs27.readFileSync(f, "utf8") : "";
4217
4425
  function retryableFailure(meta, backend, res) {
4218
4426
  const run2 = backend.parse(read2(runFile(meta.id, "events.jsonl")));
4219
4427
  if (run2.usage.steps > 0) return void 0;
@@ -4233,12 +4441,12 @@ function finalize(meta, res) {
4233
4441
  if (meta.snapshot?.dir && meta.repoRoot) run2.finalText = backToProject(run2.finalText, meta.snapshot.dir, meta.repoRoot);
4234
4442
  meta.sessionId = run2.sessionId ?? meta.sessionId;
4235
4443
  meta.usage = run2.usage;
4236
- fs26.writeFileSync(runFile(meta.id, "summary.md"), `${run2.finalText}
4444
+ fs27.writeFileSync(runFile(meta.id, "summary.md"), `${run2.finalText}
4237
4445
  `);
4238
4446
  if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run2.finalText);
4239
4447
  if (meta.reviewOf) {
4240
4448
  meta.verdict = parseVerdict(run2.finalText);
4241
- if (meta.packageFile) fs26.rmSync(meta.packageFile, { force: true });
4449
+ if (meta.packageFile) fs27.rmSync(meta.packageFile, { force: true });
4242
4450
  }
4243
4451
  if (!res.timedOut && !res.stopped && !meta.error) {
4244
4452
  const f = backend.failure(run2, read2(runFile(meta.id, "stderr.log")), res.code);
@@ -4273,6 +4481,13 @@ function finalize(meta, res) {
4273
4481
  if (meta.state === "done" && !run2.finalText) meta.warnings.push("worker finished without a written answer");
4274
4482
  if (meta.state === "done" && meta.verify) meta.verifyResult = runVerify(meta);
4275
4483
  meta.endedAt = (/* @__PURE__ */ new Date()).toISOString();
4484
+ if (meta.usage && meta.usage.cost === void 0) {
4485
+ const price = workerPrice(ran.backend, meta.resolvedModel ?? run2.model, ran.model);
4486
+ if (price) {
4487
+ meta.usage.costEstimate = costAt(meta.usage, price);
4488
+ meta.usage.costSource = price.source ?? "config";
4489
+ }
4490
+ }
4276
4491
  meta.returnedTokens = estimateTokens(formatReport(meta, run2.finalText));
4277
4492
  meta.savedUsd = meta.auditOf ? 0 : savedUsd(meta.usage, meta.returnedTokens);
4278
4493
  writeMeta(meta);
@@ -4280,6 +4495,7 @@ function finalize(meta, res) {
4280
4495
  record(meta);
4281
4496
  if (meta.auditOf) settleAudit(meta);
4282
4497
  notifyEnded(meta);
4498
+ refreshInBackground();
4283
4499
  return meta;
4284
4500
  }
4285
4501
  function captureChanges(meta) {
@@ -4290,13 +4506,13 @@ function captureChanges(meta) {
4290
4506
  const d = diffTrees(target, meta.baseTree, meta.afterTree);
4291
4507
  meta.changes = d.changes;
4292
4508
  meta.stats = d.stats;
4293
- fs26.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
4509
+ fs27.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
4294
4510
  }
4295
4511
  function verifyPath() {
4296
- const extra = [path22.dirname(process.execPath), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
4297
- const parts = (process.env.PATH ?? "").split(path22.delimiter).filter(Boolean);
4298
- for (const d of process.platform === "win32" ? [path22.dirname(process.execPath)] : extra) if (!parts.includes(d)) parts.push(d);
4299
- return parts.join(path22.delimiter);
4512
+ const extra = [path23.dirname(process.execPath), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
4513
+ const parts = (process.env.PATH ?? "").split(path23.delimiter).filter(Boolean);
4514
+ for (const d of process.platform === "win32" ? [path23.dirname(process.execPath)] : extra) if (!parts.includes(d)) parts.push(d);
4515
+ return parts.join(path23.delimiter);
4300
4516
  }
4301
4517
  function runVerify(meta) {
4302
4518
  const r = spawnSync6(meta.verify, {
@@ -4309,7 +4525,7 @@ function runVerify(meta) {
4309
4525
  env: { ...process.env, PATH: verifyPath(), PWD: meta.cwd, PITROOM_ACTIVE: "1" }
4310
4526
  });
4311
4527
  const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
4312
- fs26.writeFileSync(runFile(meta.id, "verify.log"), output);
4528
+ fs27.writeFileSync(runFile(meta.id, "verify.log"), output);
4313
4529
  const notFound = process.platform === "win32" && r.status === 1 && /is not recognized as an internal or external command/.test(output);
4314
4530
  return { ok: r.status === 0, code: notFound ? 127 : r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
4315
4531
  }
@@ -4364,15 +4580,15 @@ function discardRun(meta) {
4364
4580
  return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
4365
4581
  }
4366
4582
  function cleanupWorktree(meta) {
4367
- if (meta.worktree && fs26.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
4583
+ if (meta.worktree && fs27.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
4368
4584
  }
4369
4585
 
4370
4586
  // src/cli/mcp-extras.ts
4371
- import fs27 from "node:fs";
4372
- import path23 from "node:path";
4587
+ import fs28 from "node:fs";
4588
+ import path24 from "node:path";
4373
4589
 
4374
4590
  // src/cli/mcp-support.ts
4375
- import { spawn as spawn4 } from "node:child_process";
4591
+ import { spawn as spawn5 } from "node:child_process";
4376
4592
  var MAX_OUTPUT = 12e4;
4377
4593
  var DEFAULT_WAIT = 50;
4378
4594
  var MAX_WAIT = 540;
@@ -4440,7 +4656,7 @@ function pit(args, timeoutMs = 60 * 6e4, signal) {
4440
4656
  return new Promise((resolve2) => {
4441
4657
  const script = process.argv[1];
4442
4658
  if (!script) return resolve2({ code: 1, out: "", err: "cannot locate the pitroom executable" });
4443
- const child = spawn4(process.execPath, [script, ...args], { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
4659
+ const child = spawn5(process.execPath, [script, ...args], { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
4444
4660
  let out = "";
4445
4661
  let err = "";
4446
4662
  child.stdout.on("data", (d) => out += d);
@@ -4536,17 +4752,17 @@ async function startAndWait(start, seconds, ctx, task) {
4536
4752
  const started = await pit([...start, "--bg", "--json", ...task === void 0 ? [] : ["--", task]]);
4537
4753
  if (started.code !== 0) return asResult(started);
4538
4754
  let id;
4539
- let cached3 = false;
4755
+ let cached4 = false;
4540
4756
  try {
4541
- ({ id, cached: cached3 = false } = JSON.parse(started.out));
4757
+ ({ id, cached: cached4 = false } = JSON.parse(started.out));
4542
4758
  } catch {
4543
4759
  return { text: `could not read the run id from: ${started.out.slice(0, 200)}`, isError: true };
4544
4760
  }
4545
- const dash = cached3 ? Promise.resolve(void 0) : dashIfSlow(() => [id]);
4761
+ const dash = cached4 ? Promise.resolve(void 0) : dashIfSlow(() => [id]);
4546
4762
  const waited = await waitFor([id], seconds, ctx);
4547
4763
  const result = withLiveView(waited, waited.text.includes("Not finished yet") ? await dash : void 0);
4548
- if (cancelled(ctx) && !cached3) await stopQuietly(["stop", id]);
4549
- if (cached3) result.text = `Cached answer: the same question on the same code as run ${id}; no worker ran (fresh: true asks one).
4764
+ if (cancelled(ctx) && !cached4) await stopQuietly(["stop", id]);
4765
+ if (cached4) result.text = `Cached answer: the same question on the same code as run ${id}; no worker ran (fresh: true asks one).
4550
4766
 
4551
4767
  ${result.text}`;
4552
4768
  return result;
@@ -4654,7 +4870,7 @@ function skillPrompts() {
4654
4870
  const root = packageRoot();
4655
4871
  for (const name of skillNames(root)) {
4656
4872
  try {
4657
- const text = fs27.readFileSync(path23.join(root, "skills", name, "SKILL.md"), "utf8");
4873
+ const text = fs28.readFileSync(path24.join(root, "skills", name, "SKILL.md"), "utf8");
4658
4874
  const front = /^---\n([\s\S]*?)\n---\n/.exec(text);
4659
4875
  const body = (front ? text.slice(front[0].length) : text).trim();
4660
4876
  const description = /^description:\s*(.+)$/m.exec(front?.[1] ?? "")?.[1]?.trim() ?? `The ${name} skill.`;
@@ -5172,22 +5388,22 @@ async function serveMcp() {
5172
5388
 
5173
5389
  // src/cli/mcp-http.ts
5174
5390
  import crypto8 from "node:crypto";
5175
- import fs29 from "node:fs";
5391
+ import fs30 from "node:fs";
5176
5392
  import http2 from "node:http";
5177
- import path25 from "node:path";
5393
+ import path26 from "node:path";
5178
5394
 
5179
5395
  // src/core/dash.ts
5180
- import { spawn as spawn5 } from "node:child_process";
5396
+ import { spawn as spawn6 } from "node:child_process";
5181
5397
  import crypto7 from "node:crypto";
5182
- import fs28 from "node:fs";
5398
+ import fs29 from "node:fs";
5183
5399
  import http from "node:http";
5184
- import path24 from "node:path";
5400
+ import path25 from "node:path";
5185
5401
  import { fileURLToPath as fileURLToPath3 } from "node:url";
5186
5402
 
5187
5403
  // src/core/file-diff.ts
5188
5404
  var PREVIEW_LINES = 300;
5189
5405
  function gitPath(value, prefix = true) {
5190
- let path33 = value;
5406
+ let path34 = value;
5191
5407
  if (value.startsWith('"') && value.endsWith('"')) {
5192
5408
  const bytes = [];
5193
5409
  const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
@@ -5195,9 +5411,9 @@ function gitPath(value, prefix = true) {
5195
5411
  if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
5196
5412
  else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
5197
5413
  }
5198
- path33 = new TextDecoder().decode(new Uint8Array(bytes));
5414
+ path34 = new TextDecoder().decode(new Uint8Array(bytes));
5199
5415
  }
5200
- return prefix ? path33.replace(/^[ab]\//, "") : path33;
5416
+ return prefix ? path34.replace(/^[ab]\//, "") : path34;
5201
5417
  }
5202
5418
  function headerPath(header) {
5203
5419
  const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
@@ -5349,7 +5565,7 @@ function toRun(m) {
5349
5565
  }
5350
5566
  function dashState(opts = {}) {
5351
5567
  const limit = Math.min(Math.max(opts.limit ?? 40, 1), 200);
5352
- const runs = [];
5568
+ const metas = [];
5353
5569
  const groups = /* @__PURE__ */ new Set();
5354
5570
  for (const id of listRunIds().slice(-SCAN).reverse()) {
5355
5571
  let m;
@@ -5360,8 +5576,10 @@ function dashState(opts = {}) {
5360
5576
  }
5361
5577
  if (m.group) groups.add(m.group);
5362
5578
  if (opts.group && m.group !== opts.group) continue;
5363
- if (runs.length < limit) runs.push(toRun(m));
5579
+ metas.push(m);
5364
5580
  }
5581
+ const whole = new Set(metas.slice(0, limit).flatMap((m) => m.group ? [m.group] : []));
5582
+ const runs = metas.filter((m, i) => i < limit || m.group && whole.has(m.group)).map(toRun);
5365
5583
  return {
5366
5584
  price: primaryPrice().name,
5367
5585
  running: runs.filter((r) => r.state === "running" || r.state === "queued").length,
@@ -5422,7 +5640,8 @@ function runDetail(id) {
5422
5640
  toolCalls: u?.toolCalls,
5423
5641
  tokens: u?.total,
5424
5642
  returnedTokens: m.returnedTokens,
5425
- cost: u?.cost,
5643
+ cost: u?.cost ?? u?.costEstimate,
5644
+ costEstimated: u?.cost === void 0 && u?.costEstimate !== void 0 ? 1 : void 0,
5426
5645
  saved: m.savedUsd,
5427
5646
  group: m.group,
5428
5647
  directory: m.dir
@@ -5448,11 +5667,11 @@ var assetsDir = () => fileURLToPath3(new URL("./ui/", import.meta.url));
5448
5667
  var assetCache = /* @__PURE__ */ new Map();
5449
5668
  function readAsset(route) {
5450
5669
  try {
5451
- const file2 = path24.join(assetsDir(), path24.basename(route));
5452
- const { mtimeMs } = fs28.statSync(file2);
5670
+ const file2 = path25.join(assetsDir(), path25.basename(route));
5671
+ const { mtimeMs } = fs29.statSync(file2);
5453
5672
  const hit = assetCache.get(route);
5454
5673
  if (hit && hit.mtimeMs === mtimeMs) return hit.data;
5455
- const data = fs28.readFileSync(file2);
5674
+ const data = fs29.readFileSync(file2);
5456
5675
  assetCache.set(route, { mtimeMs, data });
5457
5676
  return data;
5458
5677
  } catch {
@@ -5558,7 +5777,7 @@ function startDash(opts) {
5558
5777
  });
5559
5778
  });
5560
5779
  }
5561
- var registryFile = () => path24.join(home(), "dash.json");
5780
+ var registryFile = () => path25.join(home(), "dash.json");
5562
5781
  async function ping(port) {
5563
5782
  try {
5564
5783
  const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
@@ -5570,12 +5789,12 @@ async function ping(port) {
5570
5789
  async function runningDash() {
5571
5790
  let r;
5572
5791
  try {
5573
- r = JSON.parse(fs28.readFileSync(registryFile(), "utf8"));
5792
+ r = JSON.parse(fs29.readFileSync(registryFile(), "utf8"));
5574
5793
  } catch {
5575
5794
  return void 0;
5576
5795
  }
5577
5796
  if (!isAlive(r.pid)) {
5578
- fs28.rmSync(registryFile(), { force: true });
5797
+ fs29.rmSync(registryFile(), { force: true });
5579
5798
  return void 0;
5580
5799
  }
5581
5800
  return await ping(r.port) ? { ...r, url: `http://127.0.0.1:${r.port}/` } : void 0;
@@ -5583,7 +5802,7 @@ async function runningDash() {
5583
5802
  function openBrowser(url) {
5584
5803
  const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
5585
5804
  try {
5586
- spawn5(cmd, args, { stdio: "ignore", detached: true }).on("error", () => void 0).unref();
5805
+ spawn6(cmd, args, { stdio: "ignore", detached: true }).on("error", () => void 0).unref();
5587
5806
  } catch {
5588
5807
  }
5589
5808
  }
@@ -5592,7 +5811,7 @@ async function dashCommand(o) {
5592
5811
  if (o.stop) {
5593
5812
  if (!existing) throw new UserError("pitroom dash is not running");
5594
5813
  process.kill(existing.pid, "SIGTERM");
5595
- fs28.rmSync(registryFile(), { force: true });
5814
+ fs29.rmSync(registryFile(), { force: true });
5596
5815
  o.log(`stopped pitroom dash (${existing.url})`);
5597
5816
  return 0;
5598
5817
  }
@@ -5602,7 +5821,7 @@ async function dashCommand(o) {
5602
5821
  return 0;
5603
5822
  }
5604
5823
  if (o.detach) {
5605
- const child = spawn5(process.execPath, [process.argv[1], "dash", "--serve", ...o.port !== void 0 ? ["--port", String(o.port)] : [], "--idle", "1h"], {
5824
+ const child = spawn6(process.execPath, [process.argv[1], "dash", "--serve", ...o.port !== void 0 ? ["--port", String(o.port)] : [], "--idle", "1h"], {
5606
5825
  detached: true,
5607
5826
  stdio: "ignore",
5608
5827
  env: process.env
@@ -5629,9 +5848,9 @@ async function dashCommand(o) {
5629
5848
  if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
5630
5849
  dash = await startDash({ port: 0, idleMs: o.idleMs });
5631
5850
  }
5632
- fs28.mkdirSync(home(), { recursive: true });
5851
+ fs29.mkdirSync(home(), { recursive: true });
5633
5852
  const tmp = `${registryFile()}.${process.pid}.tmp`;
5634
- fs28.writeFileSync(tmp, JSON.stringify({ pid: process.pid, port: dash.port }));
5853
+ fs29.writeFileSync(tmp, JSON.stringify({ pid: process.pid, port: dash.port }));
5635
5854
  renameOver(tmp, registryFile());
5636
5855
  const stop = () => void dash.close();
5637
5856
  process.once("SIGINT", stop);
@@ -5641,7 +5860,7 @@ async function dashCommand(o) {
5641
5860
  if (o.open) openBrowser(dash.url);
5642
5861
  await dash.closed;
5643
5862
  try {
5644
- if (JSON.parse(fs28.readFileSync(registryFile(), "utf8")).pid === process.pid) fs28.rmSync(registryFile(), { force: true });
5863
+ if (JSON.parse(fs29.readFileSync(registryFile(), "utf8")).pid === process.pid) fs29.rmSync(registryFile(), { force: true });
5645
5864
  } catch {
5646
5865
  }
5647
5866
  return 0;
@@ -5653,10 +5872,10 @@ var MAX_BODY = 4e6;
5653
5872
  var MAX_SESSIONS = 64;
5654
5873
  var IDLE_MS = 60 * 6e4;
5655
5874
  var PATH = "/mcp";
5656
- var tokenFile = () => path25.join(home(), "mcp-token");
5875
+ var tokenFile = () => path26.join(home(), "mcp-token");
5657
5876
  var readToken = (file2) => {
5658
5877
  try {
5659
- const kept = fs29.readFileSync(file2, "utf8").trim();
5878
+ const kept = fs30.readFileSync(file2, "utf8").trim();
5660
5879
  return kept.length >= 16 ? kept : void 0;
5661
5880
  } catch {
5662
5881
  return void 0;
@@ -5671,10 +5890,10 @@ function mcpToken() {
5671
5890
  const file2 = tokenFile();
5672
5891
  let token = readToken(file2);
5673
5892
  if (!token) {
5674
- fs29.mkdirSync(path25.dirname(file2), { recursive: true });
5893
+ fs30.mkdirSync(path26.dirname(file2), { recursive: true });
5675
5894
  token = crypto8.randomBytes(32).toString("hex");
5676
5895
  try {
5677
- fs29.writeFileSync(file2, `${token}
5896
+ fs30.writeFileSync(file2, `${token}
5678
5897
  `, { mode: 384, flag: "wx" });
5679
5898
  } catch (e) {
5680
5899
  if (e.code !== "EEXIST") throw e;
@@ -5683,7 +5902,7 @@ function mcpToken() {
5683
5902
  }
5684
5903
  }
5685
5904
  try {
5686
- fs29.chmodSync(file2, 384);
5905
+ fs30.chmodSync(file2, 384);
5687
5906
  } catch {
5688
5907
  }
5689
5908
  return { token, from: file2 };
@@ -5865,16 +6084,16 @@ async function serveMcpHttp(opts) {
5865
6084
 
5866
6085
  // src/cli/commands.ts
5867
6086
  import { spawnSync as spawnSync8 } from "node:child_process";
5868
- import fs35 from "node:fs";
5869
- import path31 from "node:path";
6087
+ import fs36 from "node:fs";
6088
+ import path32 from "node:path";
5870
6089
  import { fileURLToPath as fileURLToPath4 } from "node:url";
5871
6090
 
5872
6091
  // src/core/init.ts
5873
- import fs30 from "node:fs";
5874
- import path26 from "node:path";
6092
+ import fs31 from "node:fs";
6093
+ import path27 from "node:path";
5875
6094
  var FREE = /-free$/;
5876
6095
  var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
5877
- var isFound = (binary5) => path26.isAbsolute(binary5) && fs30.existsSync(binary5);
6096
+ var isFound = (binary5) => path27.isAbsolute(binary5) && fs31.existsSync(binary5);
5878
6097
  function planInit(opts = {}) {
5879
6098
  const file2 = configPath();
5880
6099
  const workers = backendIds().map((id) => {
@@ -5920,15 +6139,15 @@ function planInit(opts = {}) {
5920
6139
  if (Object.keys(tiers).length > 1) config.tiers = tiers;
5921
6140
  if (!workers.some((w) => w.found)) blocked = "no worker CLI found: install OpenCode (https://opencode.ai), Codex CLI, Claude Code or Gemini CLI first";
5922
6141
  else if (!blocked && !Object.keys(config).length) notes.push("nothing to propose: the default model and the single worker need no config");
5923
- return { path: file2, exists: fs30.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
6142
+ return { path: file2, exists: fs31.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
5924
6143
  }
5925
6144
  function writeInit(plan, force) {
5926
6145
  if (plan.blocked) throw new UserError(plan.blocked);
5927
6146
  if (!Object.keys(plan.config).length) throw new UserError("nothing to write: the proposal is empty");
5928
- if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${path26.basename(plan.path)}.bak)`);
5929
- fs30.mkdirSync(path26.dirname(plan.path), { recursive: true });
5930
- if (plan.exists) fs30.copyFileSync(plan.path, `${plan.path}.bak`);
5931
- fs30.writeFileSync(plan.path, `${JSON.stringify(plan.config, null, 2)}
6147
+ if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${path27.basename(plan.path)}.bak)`);
6148
+ fs31.mkdirSync(path27.dirname(plan.path), { recursive: true });
6149
+ if (plan.exists) fs31.copyFileSync(plan.path, `${plan.path}.bak`);
6150
+ fs31.writeFileSync(plan.path, `${JSON.stringify(plan.config, null, 2)}
5932
6151
  `);
5933
6152
  return plan.path;
5934
6153
  }
@@ -5949,17 +6168,17 @@ function formatInit(plan, written) {
5949
6168
 
5950
6169
  // src/core/mcp-install.ts
5951
6170
  import { spawnSync as spawnSync7 } from "node:child_process";
5952
- import fs31 from "node:fs";
6171
+ import fs32 from "node:fs";
5953
6172
  import os10 from "node:os";
5954
- import path27 from "node:path";
6173
+ import path28 from "node:path";
5955
6174
  var CLIENT_IDS = ["claude-code", "codex", "gemini", "cursor", "claude-desktop"];
5956
6175
  var NAME = "pitroom";
5957
6176
  function mcpCommand() {
5958
6177
  const launcher = launcherPath();
5959
6178
  if (isPitroomLauncher(launcher)) return { command: launcher, args: ["mcp"] };
5960
- for (const dir of (process.env.PATH ?? "").split(path27.delimiter).filter(Boolean)) {
5961
- const p = path27.join(dir, process.platform === "win32" ? "pitroom.cmd" : "pitroom");
5962
- if (fs31.existsSync(p)) return { command: path27.resolve(p), args: ["mcp"] };
6179
+ for (const dir of (process.env.PATH ?? "").split(path28.delimiter).filter(Boolean)) {
6180
+ const p = path28.join(dir, process.platform === "win32" ? "pitroom.cmd" : "pitroom");
6181
+ if (fs32.existsSync(p)) return { command: path28.resolve(p), args: ["mcp"] };
5963
6182
  }
5964
6183
  return { command: process.execPath, args: [process.argv[1] ?? "pitroom", "mcp"] };
5965
6184
  }
@@ -5975,7 +6194,7 @@ function run(backendId, args) {
5975
6194
  var runnable = (backendId) => run(backendId, ["--version"]).ok;
5976
6195
  var readJson = (file2) => {
5977
6196
  try {
5978
- return JSON.parse(fs31.readFileSync(file2, "utf8"));
6197
+ return JSON.parse(fs32.readFileSync(file2, "utf8"));
5979
6198
  } catch {
5980
6199
  return void 0;
5981
6200
  }
@@ -5985,7 +6204,7 @@ var jsonState = (file2, c) => {
5985
6204
  return entry === void 0 ? "absent" : same(c, entry) ? "same" : "different";
5986
6205
  };
5987
6206
  var home2 = () => os10.homedir();
5988
- var geminiDir = () => path27.join(process.env.GEMINI_CLI_HOME ?? home2(), ".gemini");
6207
+ var geminiDir = () => path28.join(process.env.GEMINI_CLI_HOME ?? home2(), ".gemini");
5989
6208
  function cliClient(id, backend, name, where, o) {
5990
6209
  return {
5991
6210
  id,
@@ -6008,43 +6227,43 @@ function jsonClient(id, name, file2, dir) {
6008
6227
  return {
6009
6228
  id,
6010
6229
  name,
6011
- found: () => fs31.existsSync(dir()),
6230
+ found: () => fs32.existsSync(dir()),
6012
6231
  state: (c) => jsonState(file2(), c),
6013
6232
  add: (c) => {
6014
6233
  let config = {};
6015
- if (fs31.existsSync(file2())) {
6234
+ if (fs32.existsSync(file2())) {
6016
6235
  config = readJson(file2());
6017
6236
  if (!config || typeof config !== "object" || Array.isArray(config)) return { ok: false, message: `${rel()} is not valid JSON: left alone` };
6018
6237
  const backup = `${file2()}.bak-pitroom`;
6019
- if (!fs31.existsSync(backup)) fs31.copyFileSync(file2(), backup);
6238
+ if (!fs32.existsSync(backup)) fs32.copyFileSync(file2(), backup);
6020
6239
  }
6021
6240
  config.mcpServers = { ...config.mcpServers ?? {}, [NAME]: { command: c.command, args: c.args } };
6022
- fs31.mkdirSync(path27.dirname(file2()), { recursive: true });
6241
+ fs32.mkdirSync(path28.dirname(file2()), { recursive: true });
6023
6242
  const tmp = `${file2()}.${process.pid}.tmp`;
6024
- fs31.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
6243
+ fs32.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
6025
6244
  `);
6026
- fs31.renameSync(tmp, file2());
6245
+ fs32.renameSync(tmp, file2());
6027
6246
  return { ok: true, message: rel() };
6028
6247
  },
6029
6248
  remove: () => {
6030
6249
  const config = readJson(file2());
6031
6250
  if (!config?.mcpServers?.[NAME]) return { ok: true, message: rel() };
6032
6251
  delete config.mcpServers[NAME];
6033
- fs31.writeFileSync(file2(), `${JSON.stringify(config, null, 2)}
6252
+ fs32.writeFileSync(file2(), `${JSON.stringify(config, null, 2)}
6034
6253
  `);
6035
6254
  return { ok: true, message: rel() };
6036
6255
  }
6037
6256
  };
6038
6257
  }
6039
6258
  function desktopFile() {
6040
- if (process.platform === "darwin") return path27.join(home2(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
6041
- if (process.platform === "win32") return path27.join(process.env.APPDATA ?? path27.join(home2(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
6042
- return path27.join(process.env.XDG_CONFIG_HOME ?? path27.join(home2(), ".config"), "Claude", "claude_desktop_config.json");
6259
+ if (process.platform === "darwin") return path28.join(home2(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
6260
+ if (process.platform === "win32") return path28.join(process.env.APPDATA ?? path28.join(home2(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
6261
+ return path28.join(process.env.XDG_CONFIG_HOME ?? path28.join(home2(), ".config"), "Claude", "claude_desktop_config.json");
6043
6262
  }
6044
6263
  function codexState(c) {
6045
6264
  let text;
6046
6265
  try {
6047
- text = fs31.readFileSync(path27.join(process.env.CODEX_HOME ?? path27.join(home2(), ".codex"), "config.toml"), "utf8");
6266
+ text = fs32.readFileSync(path28.join(process.env.CODEX_HOME ?? path28.join(home2(), ".codex"), "config.toml"), "utf8");
6048
6267
  } catch {
6049
6268
  return "absent";
6050
6269
  }
@@ -6062,7 +6281,7 @@ function codexState(c) {
6062
6281
  function clients() {
6063
6282
  return [
6064
6283
  cliClient("claude-code", "claude", "Claude Code", "user settings (~/.claude.json)", {
6065
- state: (c) => jsonState(path27.join(home2(), ".claude.json"), c),
6284
+ state: (c) => jsonState(path28.join(home2(), ".claude.json"), c),
6066
6285
  add: (c) => ["mcp", "add", "--scope", "user", NAME, "--", c.command, ...c.args],
6067
6286
  remove: () => ["mcp", "remove", NAME, "--scope", "user"]
6068
6287
  }),
@@ -6072,12 +6291,12 @@ function clients() {
6072
6291
  remove: () => ["mcp", "remove", NAME]
6073
6292
  }),
6074
6293
  cliClient("gemini", "gemini", "Gemini CLI", "user settings (~/.gemini/settings.json)", {
6075
- state: (c) => jsonState(path27.join(geminiDir(), "settings.json"), c),
6294
+ state: (c) => jsonState(path28.join(geminiDir(), "settings.json"), c),
6076
6295
  add: (c) => ["mcp", "add", "--scope", "user", NAME, c.command, ...c.args],
6077
6296
  remove: () => ["mcp", "remove", "--scope", "user", NAME]
6078
6297
  }),
6079
- jsonClient("cursor", "Cursor", () => path27.join(home2(), ".cursor", "mcp.json"), () => path27.join(home2(), ".cursor")),
6080
- jsonClient("claude-desktop", "Claude Desktop", desktopFile, () => path27.dirname(desktopFile()))
6298
+ jsonClient("cursor", "Cursor", () => path28.join(home2(), ".cursor", "mcp.json"), () => path28.join(home2(), ".cursor")),
6299
+ jsonClient("claude-desktop", "Claude Desktop", desktopFile, () => path28.dirname(desktopFile()))
6081
6300
  ];
6082
6301
  }
6083
6302
  function attempt2(f) {
@@ -6148,16 +6367,16 @@ var parseClients = (value) => {
6148
6367
 
6149
6368
  // src/core/plan-status.ts
6150
6369
  import crypto9 from "node:crypto";
6151
- import fs32 from "node:fs";
6152
- import path28 from "node:path";
6370
+ import fs33 from "node:fs";
6371
+ import path29 from "node:path";
6153
6372
  function notesFile(plan) {
6154
6373
  const id = crypto9.createHash("sha1").update(plan.file).digest("hex").slice(0, 12);
6155
- return path28.join(home(), "plans", id, "notes.md");
6374
+ return path29.join(home(), "plans", id, "notes.md");
6156
6375
  }
6157
6376
  function readNotes(plan) {
6158
6377
  const f = notesFile(plan);
6159
- if (!fs32.existsSync(f)) return [];
6160
- return fs32.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
6378
+ if (!fs33.existsSync(f)) return [];
6379
+ return fs33.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
6161
6380
  const m = /^(\d{4}-\d\d-\d\d \d\d:\d\d) (.*)$/.exec(l);
6162
6381
  return m ? { at: m[1], text: m[2] } : { at: "", text: l };
6163
6382
  });
@@ -6165,11 +6384,11 @@ function readNotes(plan) {
6165
6384
  function addNote(planFile, text) {
6166
6385
  const plan = loadPlan(planFile);
6167
6386
  const f = notesFile(plan);
6168
- fs32.mkdirSync(path28.dirname(f), { recursive: true });
6169
- if (!fs32.existsSync(f)) fs32.writeFileSync(f, `# plan: ${plan.file}
6387
+ fs33.mkdirSync(path29.dirname(f), { recursive: true });
6388
+ if (!fs33.existsSync(f)) fs33.writeFileSync(f, `# plan: ${plan.file}
6170
6389
  `);
6171
6390
  const at = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
6172
- fs32.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
6391
+ fs33.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
6173
6392
  `);
6174
6393
  return f;
6175
6394
  }
@@ -6237,26 +6456,26 @@ ${rulings}`,
6237
6456
 
6238
6457
  // src/core/cache.ts
6239
6458
  import crypto10 from "node:crypto";
6240
- import fs33 from "node:fs";
6241
- import path29 from "node:path";
6459
+ import fs34 from "node:fs";
6460
+ import path30 from "node:path";
6242
6461
  var SCAN2 = 500;
6243
6462
  function cacheKey(o) {
6244
6463
  if (o.verify || o.mode !== "read" || o.continueFrom || o.web || o.plan || o.review || o.audit || o.auditRate === 1) return void 0;
6245
6464
  if (!(effective().cacheDays.value > 0)) return void 0;
6246
- const dir = canonical(path29.resolve(o.dir));
6465
+ const dir = canonical(path30.resolve(o.dir));
6247
6466
  const root = repoRoot(dir);
6248
6467
  if (!root) return void 0;
6249
6468
  try {
6250
6469
  const tree = snapshotTree(root);
6251
6470
  const head = commitOf(root, "HEAD") ?? "";
6252
6471
  const files = o.files.map((f) => {
6253
- const abs = path29.resolve(f);
6254
- return [abs, crypto10.createHash("sha256").update(fs33.readFileSync(abs)).digest("hex")];
6472
+ const abs = path30.resolve(f);
6473
+ return [abs, crypto10.createHash("sha256").update(fs34.readFileSync(abs)).digest("hex")];
6255
6474
  });
6256
6475
  const question = o.task.replace(/\s+/g, " ").trim();
6257
6476
  const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
6258
6477
  const where = !o.inPlace && wantSnapshot(readIn2, root, dir) ? "snapshot" : "project";
6259
- const key = crypto10.createHash("sha256").update(JSON.stringify([question, path29.relative(root, dir), files, where])).digest("hex");
6478
+ const key = crypto10.createHash("sha256").update(JSON.stringify([question, path30.relative(root, dir), files, where])).digest("hex");
6260
6479
  return { key, state: `${head}:${tree}` };
6261
6480
  } catch {
6262
6481
  return void 0;
@@ -6300,8 +6519,8 @@ var cachedNote = (m) => `pitroom \u27F2 cached answer \xB7 the same question on
6300
6519
 
6301
6520
  // src/core/review.ts
6302
6521
  import crypto11 from "node:crypto";
6303
- import fs34 from "node:fs";
6304
- import path30 from "node:path";
6522
+ import fs35 from "node:fs";
6523
+ import path31 from "node:path";
6305
6524
  var TEMPLATE2 = { task: "task-reviewer", fix: "re-review", range: "code-reviewer" };
6306
6525
  var section = (title, body) => `## ${title}
6307
6526
 
@@ -6319,7 +6538,7 @@ function ancestors(m) {
6319
6538
  function briefOf(m) {
6320
6539
  const root = [m, ...ancestors(m)].at(-1);
6321
6540
  const f = runFile(root.id, "brief.md");
6322
- return fs34.existsSync(f) ? fs34.readFileSync(f, "utf8") : root.task;
6541
+ return fs35.existsSync(f) ? fs35.readFileSync(f, "utf8") : root.task;
6323
6542
  }
6324
6543
  function latestReview(ids) {
6325
6544
  for (const id of listRunIds().reverse()) {
@@ -6333,7 +6552,7 @@ function latestReview(ids) {
6333
6552
  }
6334
6553
  function diffOf(m, from = m.baseTree) {
6335
6554
  const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
6336
- if (where && fs34.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
6555
+ if (where && fs35.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
6337
6556
  if (from !== m.baseTree) {
6338
6557
  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);
6339
6558
  }
@@ -6347,7 +6566,7 @@ function runReview(id) {
6347
6566
  if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
6348
6567
  const chain = ancestors(m);
6349
6568
  const previous = latestReview(chain.map((a) => a.id));
6350
- const dir = m.mode === "isolate" && m.worktree && fs34.existsSync(m.worktree) ? m.cwd : m.dir;
6569
+ const dir = m.mode === "isolate" && m.worktree && fs35.existsSync(m.worktree) ? m.cwd : m.dir;
6351
6570
  const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
6352
6571
  const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
6353
6572
  if (!previous) {
@@ -6423,19 +6642,19 @@ function writePackage(job) {
6423
6642
  const root = repoRoot(job.dir);
6424
6643
  if (!g || !root) throw new UserError(`not a git repository: ${job.dir}`);
6425
6644
  const name = `review-${crypto11.randomBytes(4).toString("hex")}.md`;
6426
- const inside2 = !path30.relative(fs34.realpathSync(root), fs34.realpathSync(g)).startsWith("..");
6427
- const file2 = inside2 ? path30.join(g, "pitroom", name) : path30.join(root, ".pitroom", name);
6645
+ const inside2 = !path31.relative(fs35.realpathSync(root), fs35.realpathSync(g)).startsWith("..");
6646
+ const file2 = inside2 ? path31.join(g, "pitroom", name) : path31.join(root, ".pitroom", name);
6428
6647
  if (!inside2) ignoreInGit(root, "/.pitroom/");
6429
- fs34.mkdirSync(path30.dirname(file2), { recursive: true });
6430
- fs34.writeFileSync(file2, job.package);
6648
+ fs35.mkdirSync(path31.dirname(file2), { recursive: true });
6649
+ fs35.writeFileSync(file2, job.package);
6431
6650
  return file2;
6432
6651
  }
6433
6652
  function ignoreInGit(root, pattern) {
6434
- const exclude = path30.resolve(root, git(root, ["rev-parse", "--git-path", "info/exclude"]).stdout.trim());
6435
- const text = fs34.existsSync(exclude) ? fs34.readFileSync(exclude, "utf8") : "";
6653
+ const exclude = path31.resolve(root, git(root, ["rev-parse", "--git-path", "info/exclude"]).stdout.trim());
6654
+ const text = fs35.existsSync(exclude) ? fs35.readFileSync(exclude, "utf8") : "";
6436
6655
  if (text.split(/\r?\n/).includes(pattern)) return;
6437
- fs34.mkdirSync(path30.dirname(exclude), { recursive: true });
6438
- fs34.appendFileSync(exclude, `${text && !text.endsWith("\n") ? "\n" : ""}# Pitroom review packages in worktrees (removed after each review)
6656
+ fs35.mkdirSync(path31.dirname(exclude), { recursive: true });
6657
+ fs35.appendFileSync(exclude, `${text && !text.endsWith("\n") ? "\n" : ""}# Pitroom review packages in worktrees (removed after each review)
6439
6658
  ${pattern}
6440
6659
  `);
6441
6660
  }
@@ -6591,10 +6810,10 @@ async function cmdReview(p) {
6591
6810
  writeMeta(meta);
6592
6811
  }
6593
6812
  } catch (e) {
6594
- fs35.rmSync(packageFile, { force: true });
6813
+ fs36.rmSync(packageFile, { force: true });
6595
6814
  throw e;
6596
6815
  }
6597
- fs35.writeFileSync(runFile(meta.id, "package.md"), job.package);
6816
+ fs36.writeFileSync(runFile(meta.id, "package.md"), job.package);
6598
6817
  return launch(p, meta);
6599
6818
  }
6600
6819
  async function cmdAudit(p) {
@@ -6624,6 +6843,47 @@ async function cmdAudit(p) {
6624
6843
  writeMeta(m);
6625
6844
  return launch(p, a);
6626
6845
  }
6846
+ async function cmdPrices(p) {
6847
+ if (has(p, "refresh")) {
6848
+ const r = await refreshCatalog();
6849
+ if (r.state === "failed") {
6850
+ if (!has(p, "quiet")) console.error(`could not refresh the price catalog: ${r.error}`);
6851
+ return 1;
6852
+ }
6853
+ if (!has(p, "quiet")) console.log(`price catalog updated: ${r.models} priced models`);
6854
+ }
6855
+ const st = catalogStatus();
6856
+ const used = (() => {
6857
+ try {
6858
+ const chain = resolveChain();
6859
+ return [chain.worker, ...chain.fallback].map((t) => {
6860
+ const backend = getBackend(t.backend);
6861
+ const model = (t.model ?? "").split("#")[0];
6862
+ const price = workerPrice(t.backend, model);
6863
+ return {
6864
+ worker: model ? `${t.backend}:${model}` : t.backend,
6865
+ reportsCost: backend.capabilities.reportsCost,
6866
+ price: price ? { name: price.name, source: price.source, input: price.input, output: price.output, cachedInput: price.cachedInput } : void 0
6867
+ };
6868
+ });
6869
+ } catch {
6870
+ return [];
6871
+ }
6872
+ })();
6873
+ if (has(p, "json")) return console.log(JSON.stringify({ ...st, workers: used }, null, 2)), 0;
6874
+ 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)'}`);
6875
+ console.log(`source: ${st.url}${st.enabled ? `, refreshed after a run when older than ${st.hours} h` : ""}`);
6876
+ 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)");
6877
+ if (st.lastFailure) console.log(`last try: failed: ${st.lastFailure.replace(/^\S+ /, "")}`);
6878
+ if (used.length) {
6879
+ console.log("\nthe workers in use:");
6880
+ for (const u of used) {
6881
+ 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";
6882
+ console.log(` ${u.worker.padEnd(36)} ${what2}`);
6883
+ }
6884
+ }
6885
+ return 0;
6886
+ }
6627
6887
  function cmdCooldown(p) {
6628
6888
  if (has(p, "clear")) {
6629
6889
  const n = clearCooldowns();
@@ -6662,7 +6922,7 @@ function cmdPlan(p) {
6662
6922
  }
6663
6923
  function cmdCrew(p) {
6664
6924
  const file2 = flag(p, "task-file");
6665
- const tasks = (file2 ? fs35.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
6925
+ const tasks = (file2 ? fs36.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
6666
6926
  if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
6667
6927
  if (has(p, "write") && tasks.length > 1) {
6668
6928
  throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
@@ -6863,7 +7123,7 @@ function sinceMs(s) {
6863
7123
  if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
6864
7124
  return Date.now() - Number(m[1]) * 864e5;
6865
7125
  }
6866
- var readStdin = () => process.stdin.isTTY ? "" : fs35.readFileSync(0, "utf8");
7126
+ var readStdin = () => process.stdin.isTTY ? "" : fs36.readFileSync(0, "utf8");
6867
7127
  function cmdStatusline(p) {
6868
7128
  const input = readStdin();
6869
7129
  const lines = [];
@@ -6903,8 +7163,8 @@ function cmdModels(p) {
6903
7163
  return 0;
6904
7164
  }
6905
7165
  function cmdHookStart() {
6906
- const script = path31.join(path31.dirname(path31.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
6907
- if (fs35.existsSync(script)) spawnSync8(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
7166
+ const script = path32.join(path32.dirname(path32.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
7167
+ if (fs36.existsSync(script)) spawnSync8(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
6908
7168
  return 0;
6909
7169
  }
6910
7170
  function cmdInit(p) {
@@ -6944,8 +7204,8 @@ function cmdSavings(p) {
6944
7204
  est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
6945
7205
  const out = flag(p, "card");
6946
7206
  if (out) {
6947
- fs35.writeFileSync(out, card(t, period));
6948
- console.log(`card written to ${path31.resolve(out)}`);
7207
+ fs36.writeFileSync(out, card(t, period));
7208
+ console.log(`card written to ${path32.resolve(out)}`);
6949
7209
  }
6950
7210
  if (has(p, "badge")) console.log(`![pitroom](${badgeUrl(t)})`);
6951
7211
  return 0;
@@ -6987,7 +7247,7 @@ function cmdConfig(p) {
6987
7247
  console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
6988
7248
  return 0;
6989
7249
  }
6990
- console.log(`config file: ${configPath()}${fs35.existsSync(configPath()) ? "" : " (not present)"}`);
7250
+ console.log(`config file: ${configPath()}${fs36.existsSync(configPath()) ? "" : " (not present)"}`);
6991
7251
  for (const [key, s] of Object.entries(eff)) {
6992
7252
  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);
6993
7253
  console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
@@ -7011,7 +7271,7 @@ function cmdClean(p) {
7011
7271
  const m = readMeta(id);
7012
7272
  if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
7013
7273
  recordRun(m);
7014
- fs35.rmSync(runDir(id), { recursive: true, force: true });
7274
+ fs36.rmSync(runDir(id), { recursive: true, force: true });
7015
7275
  }
7016
7276
  const snapshots = prune(Date.now(), 0);
7017
7277
  console.log(`removed ${old.length} run(s)${snapshots ? ` and ${snapshots} read snapshot${snapshots === 1 ? "" : "s"}` : ""}; the history and the savings ledger are kept`);
@@ -7022,9 +7282,9 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
7022
7282
 
7023
7283
  // src/core/doctor.ts
7024
7284
  import { spawnSync as spawnSync9 } from "node:child_process";
7025
- import fs36 from "node:fs";
7285
+ import fs37 from "node:fs";
7026
7286
  import os11 from "node:os";
7027
- import path32 from "node:path";
7287
+ import path33 from "node:path";
7028
7288
  var MARK2 = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
7029
7289
  var NEXT = [
7030
7290
  { when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
@@ -7047,10 +7307,10 @@ function usageLine() {
7047
7307
  return `${ids.length} run${ids.length === 1 ? "" : "s"} so far${when2} (pitroom savings shows what they saved)`;
7048
7308
  }
7049
7309
  function firstNodeOnPath() {
7050
- for (const dir of (process.env.PATH ?? "").split(path32.delimiter).filter(Boolean)) {
7051
- const file2 = path32.join(dir, process.platform === "win32" ? "node.exe" : "node");
7310
+ for (const dir of (process.env.PATH ?? "").split(path33.delimiter).filter(Boolean)) {
7311
+ const file2 = path33.join(dir, process.platform === "win32" ? "node.exe" : "node");
7052
7312
  try {
7053
- if (!fs36.statSync(file2).isFile()) continue;
7313
+ if (!fs37.statSync(file2).isFile()) continue;
7054
7314
  } catch {
7055
7315
  continue;
7056
7316
  }
@@ -7077,19 +7337,16 @@ function doctor5(probe) {
7077
7337
  const addAll = (list2) => list2.forEach((c) => add(c.level, c.message));
7078
7338
  add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
7079
7339
  const onPath = firstNodeOnPath();
7080
- if (onPath && onPath.version && tooOld(onPath.version) && path32.resolve(onPath.path) !== path32.resolve(process.execPath)) {
7340
+ if (onPath && onPath.version && tooOld(onPath.version) && path33.resolve(onPath.path) !== path33.resolve(process.execPath)) {
7081
7341
  add("warn", `the first \`node\` on PATH is v${onPath.version} (${onPath.path}), older than the 22.13 Pitroom needs: shells that agent apps start may use it (the pitroom command finds a newer Node itself; other tools may not)`);
7082
7342
  }
7083
7343
  add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
7084
7344
  const cfg = loadConfig();
7085
- add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs36.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
7345
+ add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs37.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
7086
7346
  for (const w of cfg.warnings) add("warn", w);
7087
- if (process.platform === "win32") {
7088
- add("warn", "git guard: not available on Windows yet, so Pitroom does not block a worker's git history changes or pushes there (read-only runs are still limited by each worker CLI's own rules, and --isolate keeps edits in a copy); prefer --isolate and check the patch before apply");
7089
- } else {
7090
- const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
7091
- add(guarded ? "ok" : "warn", guarded ? "git guard shim ready" : "git guard unavailable (git not on PATH)");
7092
- }
7347
+ if (shimReady()) add("ok", "git guard shim ready");
7348
+ else if (process.platform === "win32") add("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");
7349
+ else add("warn", "git guard unavailable (git not on PATH)");
7093
7350
  section2("Worker chain");
7094
7351
  const chain = [];
7095
7352
  try {
@@ -7126,6 +7383,25 @@ function doctor5(probe) {
7126
7383
  add("ok", `cost: ${key} = ${mine}${cheaper ? `; you priced ${cheaper[0]} cheaper (${cheaper[1]}): is the dearer one needed?` : ""}`);
7127
7384
  }
7128
7385
  }
7386
+ const seenUnpriced = /* @__PURE__ */ new Set();
7387
+ for (const t of [...chain, ...tierTargets]) {
7388
+ let backend;
7389
+ try {
7390
+ backend = getBackend(t.backend);
7391
+ } catch {
7392
+ continue;
7393
+ }
7394
+ const model = (t.model ?? "").split("#")[0];
7395
+ const key = model ? `${t.backend}:${model}` : t.backend;
7396
+ if (backend.capabilities.reportsCost || seenUnpriced.has(key) || workerPrice(t.backend, model)) continue;
7397
+ seenUnpriced.add(key);
7398
+ add("warn", `${key} reports no cost, so its runs count as free in the savings: give its price in the config, "workerPrices": {"${key}": "<in>,<out>[,<cachedIn>]"} (USD per 1M tokens), or turn on "priceFeed": true to use the public price catalog`);
7399
+ }
7400
+ const feed = catalogStatus();
7401
+ if (feed.enabled) {
7402
+ if (feed.models) add(feed.lastFailure ? "warn" : "ok", `price catalog: ${feed.models} priced models from ${feed.url}, updated ${feed.ageHours < 1 ? "within the hour" : `${feed.ageHours.toFixed(1)} h ago`}${feed.lastFailure ? `; the last refresh failed (${feed.lastFailure.replace(/^\S+ /, "")})` : ""}`);
7403
+ else add("warn", `price feed is on but no catalog has been fetched yet${feed.lastFailure ? ` (the last try failed: ${feed.lastFailure.replace(/^\S+ /, "")})` : ""}: it is fetched after a run, or now with \`pitroom prices --refresh\``);
7404
+ }
7129
7405
  for (const c of Object.values(activeCooldowns())) {
7130
7406
  add("warn", `${c.target} is cooling down ${untilText(c)} (${c.reason.slice(0, 80)}): runs skip it while a fallback is left; \`pitroom cooldown --clear\` tries it again`);
7131
7407
  }
@@ -7239,19 +7515,19 @@ function skillChecks() {
7239
7515
  else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
7240
7516
  }
7241
7517
  if (geminiExtensionInstalled()) {
7242
- const linked = installedSkills().some((i) => i.base.includes(`${path32.sep}.agents${path32.sep}`) && i.names.length);
7518
+ const linked = installedSkills().some((i) => i.base.includes(`${path33.sep}.agents${path33.sep}`) && i.names.length);
7243
7519
  checks.push(
7244
7520
  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)" }
7245
7521
  );
7246
7522
  }
7247
7523
  if (viaPlugin) {
7248
- const linked = installedSkills().some((i) => i.base.includes(`${path32.sep}.claude${path32.sep}`) && i.names.length);
7524
+ const linked = installedSkills().some((i) => i.base.includes(`${path33.sep}.claude${path33.sep}`) && i.names.length);
7249
7525
  checks.push(
7250
7526
  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)" }
7251
7527
  );
7252
7528
  }
7253
7529
  const launcher = launcherPath();
7254
- if (fs36.existsSync(launcher)) {
7530
+ if (fs37.existsSync(launcher)) {
7255
7531
  const r = spawnSync9(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
7256
7532
  checks.push(
7257
7533
  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)}` }
@@ -7263,12 +7539,12 @@ function skillChecks() {
7263
7539
  }
7264
7540
  function geminiExtensionInstalled() {
7265
7541
  const home3 = process.env.GEMINI_CLI_HOME ?? os11.homedir();
7266
- return fs36.existsSync(path32.join(home3, ".gemini", "extensions", "pitroom"));
7542
+ return fs37.existsSync(path33.join(home3, ".gemini", "extensions", "pitroom"));
7267
7543
  }
7268
7544
  function pluginInstalled() {
7269
7545
  try {
7270
- const f = path32.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
7271
- const plugins = JSON.parse(fs36.readFileSync(f, "utf8")).plugins ?? {};
7546
+ const f = path33.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
7547
+ const plugins = JSON.parse(fs37.readFileSync(f, "utf8")).plugins ?? {};
7272
7548
  return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
7273
7549
  } catch {
7274
7550
  return false;
@@ -7285,24 +7561,24 @@ function superpowersActive() {
7285
7561
  for (const p of openCodePlugins()) {
7286
7562
  if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
7287
7563
  }
7288
- for (const base2 of [path32.join(os11.homedir(), ".agents", "skills"), path32.join(os11.homedir(), ".claude", "skills")]) {
7289
- const dir = path32.join(base2, "using-superpowers");
7290
- if (fs36.existsSync(path32.join(dir, "SKILL.md"))) found.push(dir);
7564
+ for (const base2 of [path33.join(os11.homedir(), ".agents", "skills"), path33.join(os11.homedir(), ".claude", "skills")]) {
7565
+ const dir = path33.join(base2, "using-superpowers");
7566
+ if (fs37.existsSync(path33.join(dir, "SKILL.md"))) found.push(dir);
7291
7567
  }
7292
7568
  return found;
7293
7569
  }
7294
7570
  function claudePlugins() {
7295
7571
  try {
7296
- const f = path32.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
7297
- return Object.keys(JSON.parse(fs36.readFileSync(f, "utf8")).plugins ?? {});
7572
+ const f = path33.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
7573
+ return Object.keys(JSON.parse(fs37.readFileSync(f, "utf8")).plugins ?? {});
7298
7574
  } catch {
7299
7575
  return [];
7300
7576
  }
7301
7577
  }
7302
7578
  function claudePluginEnabled(key) {
7303
7579
  try {
7304
- const f = path32.join(os11.homedir(), ".claude", "settings.json");
7305
- return JSON.parse(fs36.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
7580
+ const f = path33.join(os11.homedir(), ".claude", "settings.json");
7581
+ return JSON.parse(fs37.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
7306
7582
  } catch {
7307
7583
  return true;
7308
7584
  }
@@ -7311,7 +7587,7 @@ function codexPlugins() {
7311
7587
  const plugins = /* @__PURE__ */ new Map();
7312
7588
  let text;
7313
7589
  try {
7314
- text = fs36.readFileSync(path32.join(process.env.CODEX_HOME ?? path32.join(os11.homedir(), ".codex"), "config.toml"), "utf8");
7590
+ text = fs37.readFileSync(path33.join(process.env.CODEX_HOME ?? path33.join(os11.homedir(), ".codex"), "config.toml"), "utf8");
7315
7591
  } catch {
7316
7592
  return plugins;
7317
7593
  }
@@ -7329,11 +7605,11 @@ function codexPlugins() {
7329
7605
  return plugins;
7330
7606
  }
7331
7607
  function openCodePlugins() {
7332
- const dir = path32.join(process.env.XDG_CONFIG_HOME ?? path32.join(os11.homedir(), ".config"), "opencode");
7608
+ const dir = path33.join(process.env.XDG_CONFIG_HOME ?? path33.join(os11.homedir(), ".config"), "opencode");
7333
7609
  const plugins = [];
7334
7610
  for (const name of ["opencode.json", "opencode.jsonc"]) {
7335
7611
  try {
7336
- const text = fs36.readFileSync(path32.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
7612
+ const text = fs37.readFileSync(path33.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
7337
7613
  const list2 = JSON.parse(text).plugin;
7338
7614
  if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
7339
7615
  } catch {
@@ -7341,7 +7617,7 @@ function openCodePlugins() {
7341
7617
  }
7342
7618
  for (const folder of ["plugin", "plugins"]) {
7343
7619
  try {
7344
- for (const f of fs36.readdirSync(path32.join(dir, folder))) plugins.push(path32.join(dir, folder, f));
7620
+ for (const f of fs37.readdirSync(path33.join(dir, folder))) plugins.push(path33.join(dir, folder, f));
7345
7621
  } catch {
7346
7622
  }
7347
7623
  }
@@ -7391,6 +7667,7 @@ Usage
7391
7667
  pitroom statusline [--then CMD] status-bar line: running workers, savings this week (after CMD's)
7392
7668
  pitroom hook-card PostToolUse hook: a card after each Bash \`pitroom\` command
7393
7669
  pitroom hook-start SessionStart hook: introduces Pitroom to the agent (Codex; Claude Code uses its plugin)
7670
+ pitroom prices [--refresh] [--json] the price catalog (config "priceFeed") and the price each worker in use gets
7394
7671
  pitroom cooldown [--clear] models that said "rate limited" and are skipped for a while (a quota used up);
7395
7672
  --clear tries them again
7396
7673
  pitroom mcp [-d DIR] [--http [--port N]]
@@ -7444,21 +7721,24 @@ Exit codes: 0 ok \xB7 1 worker failed \xB7 2 usage \xB7 3 refused/setup \xB7 4 t
7444
7721
  Workers: ${backendIds().join(", ")} (targets: "opencode", "opencode:provider/model", or a bare model)
7445
7722
  Env: PITROOM_WORKER, PITROOM_MODEL, PITROOM_FALLBACK="t1,t2", PITROOM_TIMEOUT, PITROOM_MAX_PARALLEL,
7446
7723
  PITROOM_PRIMARY=sonnet|opus|haiku|gpt-5, PITROOM_PRICE="in,out", PITROOM_HOME, PITROOM_CONFIG, PITROOM_CACHE_DAYS,
7447
- PITROOM_<WORKER>_BIN
7448
- Config: ~/.config/pitroom/config.json (worker, fallback, models, tiers, timeout, primary, price, link, web, maxParallel, audit, readIn, cacheDays)`;
7724
+ PITROOM_NOTIFY, PITROOM_NOTIFY_COMMAND, PITROOM_NOTIFY_AFTER, PITROOM_MCP_DASH, PITROOM_PRICE_FEED, PITROOM_PRICE_FEED_URL,
7725
+ PITROOM_PRICE_FEED_HOURS, PITROOM_<WORKER>_BIN
7726
+ Config: ~/.config/pitroom/config.json (worker, fallback, models, tiers, timeout, primary, price, link, web, maxParallel, audit, readIn, cacheDays,
7727
+ countRateLimits, mcpDash, notify, notifyCommand, notifyAfter, workerPrices, priceFeed, priceFeedUrl, priceFeedHours)`;
7449
7728
  var COMMANDS = {
7450
7729
  run: cmdRun,
7451
7730
  crew: cmdCrew,
7452
7731
  review: cmdReview,
7453
7732
  audit: cmdAudit,
7454
7733
  cooldown: cmdCooldown,
7734
+ prices: cmdPrices,
7455
7735
  mcp: (p) => {
7456
7736
  const port = flag(p, "port");
7457
7737
  if (port !== void 0 && !has(p, "http")) throw new UserError("--port goes with --http (on stdio there is no port)");
7458
7738
  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)");
7459
7739
  const dir = flag(p, "dir");
7460
7740
  if (dir !== void 0) {
7461
- if (!fs37.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) throw new UserError(`-d ${dir}: no such directory`);
7741
+ if (!fs38.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) throw new UserError(`-d ${dir}: no such directory`);
7462
7742
  process.chdir(dir);
7463
7743
  process.env.PWD = process.cwd();
7464
7744
  }