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