pitroom 0.6.5 → 0.6.7

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
@@ -57,6 +57,9 @@ function resolveCommand(bin) {
57
57
  return { command: bin, prefix: [] };
58
58
  }
59
59
 
60
+ // src/backends/types.ts
61
+ var MAX_STEPS = 400;
62
+
60
63
  // src/backends/claude/events.ts
61
64
  var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
62
65
  var DENIED = /permission|not allowed|denied|blocked|disallowed/i;
@@ -75,6 +78,8 @@ function parseEvents(jsonl) {
75
78
  const tools = {};
76
79
  const edits = [];
77
80
  const pendingEdits = /* @__PURE__ */ new Map();
81
+ const timeline = [];
82
+ const stepOf = /* @__PURE__ */ new Map();
78
83
  const steps = /* @__PURE__ */ new Set();
79
84
  let sessionId;
80
85
  let model;
@@ -105,18 +110,26 @@ function parseEvents(jsonl) {
105
110
  if (c.type === "text" && String(c.text ?? "").trim()) {
106
111
  lastText = String(c.text).trim();
107
112
  lastActivity = `says: ${oneLine(lastText)}`;
113
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip(lastText, 600) });
108
114
  } else if (c.type === "tool_use") {
109
115
  usage2.toolCalls++;
110
116
  tools[c.name] = (tools[c.name] ?? 0) + 1;
111
117
  const target = c.input?.file_path ?? c.input?.path ?? c.input?.pattern ?? c.input?.command ?? c.input?.url;
112
118
  lastActivity = `${c.name} ${target ? oneLine(String(target), 60) : ""}`.trim();
113
119
  if (EDIT_TOOLS.has(c.name) && c.input?.file_path) pendingEdits.set(c.id, String(c.input.file_path));
120
+ if (timeline.length < MAX_STEPS) {
121
+ const step = { kind: EDIT_TOOLS.has(c.name) ? "edit" : c.name === "Bash" ? "shell" : "tool", name: c.name, text: clip(String(target ?? ""), 240) };
122
+ timeline.push(step);
123
+ stepOf.set(String(c.id), step);
124
+ }
114
125
  }
115
126
  }
116
127
  } else if (e.type === "user") {
117
128
  for (const c of e.message?.content ?? []) {
118
129
  if (c?.type !== "tool_result") continue;
119
130
  const text = typeof c.content === "string" ? c.content : JSON.stringify(c.content ?? "");
131
+ const step = stepOf.get(String(c.tool_use_id));
132
+ if (step) step.ok = !c.is_error;
120
133
  if (c.is_error) {
121
134
  if (DENIED.test(text)) usage2.denied++;
122
135
  } else if (pendingEdits.has(c.tool_use_id)) {
@@ -142,8 +155,9 @@ function parseEvents(jsonl) {
142
155
  }
143
156
  }
144
157
  usage2.steps = steps.size;
145
- return { sessionId, model, finalText: finalText ?? (error ? "" : lastText), usage: usage2, tools, edits, lastActivity, error };
158
+ return { sessionId, model, finalText: finalText ?? (error ? "" : lastText), usage: usage2, tools, edits, lastActivity, timeline, error };
146
159
  }
160
+ var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
147
161
  function num(v) {
148
162
  return typeof v === "number" && Number.isFinite(v) ? v : 0;
149
163
  }
@@ -329,6 +343,7 @@ function parseEvents2(jsonl) {
329
343
  };
330
344
  const tools = {};
331
345
  const edits = [];
346
+ const timeline = [];
332
347
  let sessionId;
333
348
  let finalText = "";
334
349
  let lastActivity;
@@ -356,23 +371,26 @@ function parseEvents2(jsonl) {
356
371
  if (it.type === "agent_message" && String(it.text ?? "").trim()) {
357
372
  finalText = String(it.text).trim();
358
373
  lastActivity = `says: ${oneLine2(finalText)}`;
374
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip2(finalText, 600) });
359
375
  } else if (it.type === "command_execution") {
360
376
  if (it.status === "declined" || /operation not permitted|sandbox/i.test(String(it.aggregated_output ?? ""))) usage2.denied++;
361
377
  lastActivity = `shell ${oneLine2(String(it.command ?? ""), 60)}`;
378
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "shell", name: "shell", text: clip2(String(it.command ?? "").replace(/^\/bin\/\w+ -lc /, ""), 240), ok: it.exit_code === 0 });
362
379
  } else if (it.type === "file_change") {
363
380
  const paths = (it.changes ?? []).map((c) => String(c.path));
364
381
  if (it.status !== "failed" && it.status !== "declined") edits.push(...paths);
365
382
  else usage2.denied++;
366
383
  lastActivity = `edit ${oneLine2(paths.join(", "), 60)}`;
384
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "edit", name: "edit", text: clip2(paths.join(", "), 240), ok: it.status !== "failed" && it.status !== "declined" });
367
385
  }
368
386
  break;
369
387
  }
370
388
  case "turn.completed": {
371
389
  const u = e.usage ?? {};
372
- const cached2 = num2(u.cached_input_tokens);
390
+ const cached3 = num2(u.cached_input_tokens);
373
391
  const reasoning = num2(u.reasoning_output_tokens);
374
- usage2.input += Math.max(0, num2(u.input_tokens) - cached2);
375
- usage2.cacheRead += cached2;
392
+ usage2.input += Math.max(0, num2(u.input_tokens) - cached3);
393
+ usage2.cacheRead += cached3;
376
394
  usage2.cacheWrite += num2(u.cache_write_input_tokens);
377
395
  usage2.output += Math.max(0, num2(u.output_tokens) - reasoning);
378
396
  usage2.reasoning += reasoning;
@@ -387,7 +405,7 @@ function parseEvents2(jsonl) {
387
405
  break;
388
406
  }
389
407
  }
390
- return { sessionId, finalText, usage: usage2, tools, edits, lastActivity, error };
408
+ return { sessionId, finalText, usage: usage2, tools, edits, lastActivity, timeline, error };
391
409
  }
392
410
  function describe(raw) {
393
411
  const s = String(raw ?? "unknown Codex error");
@@ -407,6 +425,7 @@ function oneLine2(s, max = 80) {
407
425
  const t = s.replace(/\s+/g, " ").trim();
408
426
  return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
409
427
  }
428
+ var clip2 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
410
429
 
411
430
  // src/backends/codex/index.ts
412
431
  var binary2 = () => findBinary("codex", "PITROOM_CODEX_BIN");
@@ -551,6 +570,7 @@ function parseEvents3(ndjson) {
551
570
  };
552
571
  const tools = {};
553
572
  const edits = [];
573
+ const timeline = [];
554
574
  const textByMessage = /* @__PURE__ */ new Map();
555
575
  let sessionId;
556
576
  let lastActivity;
@@ -573,6 +593,7 @@ function parseEvents3(ndjson) {
573
593
  if (!textByMessage.has(key)) textByMessage.set(key, []);
574
594
  textByMessage.get(key).push(text);
575
595
  lastActivity = `says: ${oneLine3(text)}`;
596
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip3(text.trim(), 600), at: stamp(e.timestamp) });
576
597
  break;
577
598
  }
578
599
  case "tool_use": {
@@ -585,6 +606,11 @@ function parseEvents3(ndjson) {
585
606
  edits.push(String(st.input?.filePath ?? st.input?.path ?? name));
586
607
  }
587
608
  lastActivity = `${name} ${describe2(st.input)}`.trim();
609
+ if (timeline.length < MAX_STEPS) {
610
+ const kind2 = EDIT_TOOLS2.has(name) ? "edit" : /^(bash|shell)$/.test(name) ? "shell" : "tool";
611
+ const text = kind2 === "shell" ? String(st.input?.command ?? "") : String(st.input?.filePath ?? st.input?.path ?? st.input?.pattern ?? st.input?.url ?? "") || describe2(st.input);
612
+ timeline.push({ kind: kind2, name, text: clip3(text, 240), ok: st.status === "completed" ? true : st.status === "error" ? false : void 0, at: stamp(e.timestamp) });
613
+ }
588
614
  break;
589
615
  }
590
616
  case "step_finish": {
@@ -607,7 +633,7 @@ function parseEvents3(ndjson) {
607
633
  }
608
634
  const groups = [...textByMessage.values()];
609
635
  const finalText = (groups[groups.length - 1] ?? []).join("\n").trim();
610
- return { sessionId, finalText, usage: usage2, tools, edits, lastActivity, error };
636
+ return { sessionId, finalText, usage: usage2, tools, edits, lastActivity, timeline, error };
611
637
  }
612
638
  function describeError(err) {
613
639
  if (!err || typeof err !== "object") return "unknown OpenCode error";
@@ -620,6 +646,8 @@ function describeError(err) {
620
646
  const tags = [err.type, inner, err.status && `HTTP ${err.status}`].filter(Boolean);
621
647
  return tags.length ? `${message} [${tags.join(", ")}]` : message;
622
648
  }
649
+ var clip3 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
650
+ var stamp = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
623
651
  function num3(v) {
624
652
  return typeof v === "number" && Number.isFinite(v) ? v : 0;
625
653
  }
@@ -1121,8 +1149,14 @@ var runDir = (id) => path6.join(runsDir(), id);
1121
1149
  var runFile = (id, name) => path6.join(runDir(id), name);
1122
1150
  function newRunId(now = /* @__PURE__ */ new Date()) {
1123
1151
  const p = (n) => String(n).padStart(2, "0");
1124
- const stamp = `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
1125
- return `${stamp}-${crypto.randomBytes(2).toString("hex")}`;
1152
+ const stamp2 = `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
1153
+ return `${stamp2}-${crypto.randomBytes(2).toString("hex")}`;
1154
+ }
1155
+ var onFinished;
1156
+ var archive;
1157
+ function useArchive(hooks) {
1158
+ onFinished = hooks.onFinished;
1159
+ archive = { meta: hooks.meta, id: hooks.id };
1126
1160
  }
1127
1161
  function writeMeta(meta) {
1128
1162
  fs6.mkdirSync(runDir(meta.id), { recursive: true });
@@ -1130,9 +1164,20 @@ function writeMeta(meta) {
1130
1164
  const tmp = `${file}.${process.pid}.tmp`;
1131
1165
  fs6.writeFileSync(tmp, JSON.stringify(meta, null, 2));
1132
1166
  fs6.renameSync(tmp, file);
1167
+ if (TERMINAL.includes(meta.state)) {
1168
+ try {
1169
+ onFinished?.(meta);
1170
+ } catch {
1171
+ }
1172
+ }
1133
1173
  }
1134
1174
  function readMeta(id) {
1135
- return upgrade(JSON.parse(fs6.readFileSync(runFile(id, "meta.json"), "utf8")));
1175
+ const file = runFile(id, "meta.json");
1176
+ if (!fs6.existsSync(file)) {
1177
+ const kept = archive?.meta(id);
1178
+ if (kept) return upgrade(kept);
1179
+ }
1180
+ return upgrade(JSON.parse(fs6.readFileSync(file, "utf8")));
1136
1181
  }
1137
1182
  function upgrade(raw) {
1138
1183
  if (!raw.worker) raw.worker = raw.model ? { backend: "opencode", model: raw.model } : { backend: "opencode" };
@@ -1150,12 +1195,18 @@ function listRunIds() {
1150
1195
  }
1151
1196
  function resolveRun(ref) {
1152
1197
  const ids = listRunIds();
1153
- if (!ids.length) throw new UserError("no runs yet");
1154
- if (!ref || ref === "latest" || ref === "last") return ids[ids.length - 1];
1198
+ if (!ref || ref === "latest" || ref === "last") {
1199
+ if (!ids.length) throw new UserError("no runs yet");
1200
+ return ids[ids.length - 1];
1201
+ }
1155
1202
  if (ids.includes(ref)) return ref;
1156
1203
  const hits = ids.filter((id) => id.startsWith(ref) || id.endsWith(ref));
1157
1204
  if (hits.length === 1) return hits[0];
1158
- throw new UserError(hits.length ? `ambiguous run "${ref}": ${hits.join(", ")}` : `unknown run "${ref}"`);
1205
+ if (!hits.length) {
1206
+ const kept = archive?.id(ref);
1207
+ if (kept) return kept;
1208
+ }
1209
+ throw new UserError(hits.length ? `ambiguous run "${ref}": ${hits.join(", ")}` : ids.length ? `unknown run "${ref}"` : "no runs yet");
1159
1210
  }
1160
1211
  function isAlive(pid) {
1161
1212
  if (!pid) return false;
@@ -1204,6 +1255,10 @@ var VALUE_FLAGS = {
1204
1255
  "--interval": "interval",
1205
1256
  "--range": "range",
1206
1257
  "--then": "then",
1258
+ "--port": "port",
1259
+ "--idle": "idle",
1260
+ "--state": "state",
1261
+ "--limit": "limit",
1207
1262
  "--plan": "plan",
1208
1263
  "--step": "step"
1209
1264
  };
@@ -1233,6 +1288,10 @@ var BOOL_FLAGS = {
1233
1288
  "--any": "any",
1234
1289
  "--brief": "brief",
1235
1290
  "--running": "running",
1291
+ "--detach": "detach",
1292
+ "--stop": "stop",
1293
+ "--open": "open",
1294
+ "--serve": "serve",
1236
1295
  "-h": "help",
1237
1296
  "--help": "help",
1238
1297
  "-v": "version",
@@ -1332,87 +1391,21 @@ function effortFlag(p) {
1332
1391
 
1333
1392
  // src/cli/commands.ts
1334
1393
  import { spawnSync as spawnSync6 } from "node:child_process";
1335
- import fs22 from "node:fs";
1336
- import path18 from "node:path";
1394
+ import fs24 from "node:fs";
1395
+ import path20 from "node:path";
1337
1396
 
1338
- // src/core/report.ts
1397
+ // src/core/history.ts
1339
1398
  import fs10 from "node:fs";
1399
+ import { createRequire } from "node:module";
1400
+ import path8 from "node:path";
1401
+ import zlib from "node:zlib";
1340
1402
 
1341
- // src/core/plan.ts
1342
- import fs8 from "node:fs";
1343
- import path7 from "node:path";
1344
- var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
1345
- var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
1346
- function structure(lines) {
1347
- const headings = [];
1348
- const rules = [];
1349
- const fenced = [];
1350
- let open;
1351
- lines.forEach((l, i) => {
1352
- const f = FENCE.exec(l);
1353
- if (f) {
1354
- fenced[i] = true;
1355
- const mark = f[1];
1356
- if (!open) open = mark;
1357
- else if (mark[0] === open[0] && mark.length >= open.length && !f[2].trim()) open = void 0;
1358
- return;
1359
- }
1360
- fenced[i] = !!open;
1361
- if (open) return;
1362
- const h = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/.exec(l);
1363
- if (h) headings.push({ line: i, level: h[1].length, text: h[2] });
1364
- else if (RULE.test(l)) rules.push(i);
1365
- });
1366
- return { headings, rules, fenced };
1367
- }
1368
- function parsePlan(text, file = "") {
1369
- const lines = text.replace(/\r\n?/g, "\n").split("\n");
1370
- const { headings, rules, fenced } = structure(lines);
1371
- const after = (line) => Math.min(headings.find((h) => h.line > line)?.line ?? lines.length, rules.find((r) => r > line) ?? lines.length);
1372
- const titleHeading = headings.find((h) => h.level === 1);
1373
- const start = titleHeading ? titleHeading.line + 1 : 0;
1374
- const header = lines.slice(start, after(start - 1)).filter((l) => !/^\s*>/.test(l)).join("\n").trim();
1375
- const gc = headings.find((h) => /^global constraints\b/i.test(h.text));
1376
- const constraints = gc ? lines.slice(gc.line + 1, after(gc.line)).join("\n").trim() : "";
1377
- const tasks = [];
1378
- for (const h of headings) {
1379
- const m = /^Task\s+(\d+)\b\s*[:.)\-–—]?\s*(.*)$/i.exec(h.text);
1380
- if (!m) continue;
1381
- const end = headings.find((o) => o.line > h.line && o.level <= h.level)?.line ?? lines.length;
1382
- const body = lines.slice(h.line, end).join("\n").replace(/(\n\s*(?:---|\*\*\*|___)\s*)+$/, "").trimEnd();
1383
- const unfenced = lines.slice(h.line, end).filter((_, j) => !fenced[h.line + j]).join("\n");
1384
- const tier = /^\s*[-*]?\s*\*\*Worker:\*\*\s*`?([\w-]+)`?/m.exec(unfenced)?.[1]?.toLowerCase();
1385
- tasks.push({ step: Number(m[1]), title: m[2].trim(), text: body, ...tier ? { tier } : {} });
1386
- }
1387
- return { file, title: titleHeading?.text ?? "", header, constraints, tasks };
1388
- }
1389
- function loadPlan(file) {
1390
- const abs = path7.resolve(file);
1391
- if (!fs8.existsSync(abs) || !fs8.statSync(abs).isFile()) throw new UserError(`plan not found: ${file}`);
1392
- const plan = parsePlan(fs8.readFileSync(abs, "utf8"), fs8.realpathSync(abs));
1393
- if (!plan.tasks.length) throw new UserError(`${file} has no "Task N" headings (see pitroom-writing-plans)`);
1394
- return plan;
1395
- }
1396
- function planTask(plan, step) {
1397
- const t = plan.tasks.find((x) => x.step === step);
1398
- if (!t) throw new UserError(`no Task ${step} in ${plan.file}; it has ${plan.tasks.map((x) => `Task ${x.step}`).join(", ")}`);
1399
- return t;
1400
- }
1401
- function brief(plan, task) {
1402
- return `${[
1403
- `# ${plan.title || planName(plan.file)}`,
1404
- plan.header,
1405
- "## Global Constraints",
1406
- plan.constraints || "(none stated in the plan)",
1407
- task.text
1408
- ].filter(Boolean).join("\n\n")}
1409
- `;
1410
- }
1411
- var planName = (file) => path7.basename(file).replace(/\.md$/i, "");
1403
+ // src/core/ui.ts
1404
+ import fs9 from "node:fs";
1412
1405
 
1413
1406
  // src/core/receipt.ts
1414
- import fs9 from "node:fs";
1415
- import path8 from "node:path";
1407
+ import fs8 from "node:fs";
1408
+ import path7 from "node:path";
1416
1409
  var PRESETS = {
1417
1410
  sonnet: { name: "Claude Sonnet", input: 3, output: 15, cachedInput: 0.3 },
1418
1411
  opus: { name: "Claude Opus", input: 5, output: 25, cachedInput: 0.5 },
@@ -1448,13 +1441,13 @@ function record(meta) {
1448
1441
  saved: meta.savedUsd ?? 0,
1449
1442
  price: primaryPrice().name
1450
1443
  };
1451
- fs9.mkdirSync(path8.dirname(ledgerFile()), { recursive: true });
1452
- fs9.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
1444
+ fs8.mkdirSync(path7.dirname(ledgerFile()), { recursive: true });
1445
+ fs8.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
1453
1446
  `);
1454
1447
  }
1455
1448
  function readLedger(sinceMs2) {
1456
- if (!fs9.existsSync(ledgerFile())) return [];
1457
- return fs9.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
1449
+ if (!fs8.existsSync(ledgerFile())) return [];
1450
+ return fs8.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
1458
1451
  try {
1459
1452
  return [JSON.parse(l)];
1460
1453
  } catch {
@@ -1475,45 +1468,530 @@ function totals(entries) {
1475
1468
  );
1476
1469
  return { ...t, ratio: t.returned ? t.tokens / t.returned : 0 };
1477
1470
  }
1478
- var usd = (n) => n >= 100 ? `$${n.toFixed(0)}` : n >= 1 ? `$${n.toFixed(2)}` : `$${n.toFixed(3)}`;
1479
- function compact(n) {
1480
- const fmt = (v) => v.toFixed(v >= 100 ? 0 : 1).replace(/\.0$/, "");
1481
- if (n >= 1e6) return `${fmt(n / 1e6)}M`;
1482
- if (n >= 1e3) return `${fmt(n / 1e3)}k`;
1483
- return String(Math.round(n));
1471
+ var usd = (n) => n >= 100 ? `$${n.toFixed(0)}` : n >= 1 ? `$${n.toFixed(2)}` : `$${n.toFixed(3)}`;
1472
+ function compact(n) {
1473
+ const fmt = (v) => v.toFixed(v >= 100 ? 0 : 1).replace(/\.0$/, "");
1474
+ if (n >= 1e6) return `${fmt(n / 1e6)}M`;
1475
+ if (n >= 1e3) return `${fmt(n / 1e3)}k`;
1476
+ return String(Math.round(n));
1477
+ }
1478
+ function badgeUrl(t) {
1479
+ const msg = encodeURIComponent(`saved ${usd(t.saved)} \xB7 ${compact(t.tokens)} tokens offloaded`).replace(/-/g, "--");
1480
+ return `https://img.shields.io/badge/pitroom-${msg}-7c3aed`;
1481
+ }
1482
+ var esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
1483
+ function card(t, period) {
1484
+ const W = 720, H = 316, M = 32, GAP = 12;
1485
+ const tile = (i, value, label) => {
1486
+ const w = (W - 2 * M - 2 * GAP) / 3, x = M + i * (w + GAP);
1487
+ return `<rect x="${x}" y="196" width="${w}" height="72" rx="14" fill="#fff" fill-opacity=".035" stroke="#fff" stroke-opacity=".1"/>
1488
+ <text x="${x + 18}" y="224" class="lab">${esc(label.toUpperCase())}</text><text x="${x + 18}" y="254" class="val">${esc(value)}</text>`;
1489
+ };
1490
+ const when2 = period.toUpperCase();
1491
+ const pill = 22 + when2.length * 7.4;
1492
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" role="img" aria-label="pitroom saved ${esc(usd(t.saved))}">
1493
+ <style>
1494
+ text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Inter,Helvetica,Arial,sans-serif;fill:#f4f4f5}
1495
+ .name{font-size:20px;font-weight:650;letter-spacing:-.01em}
1496
+ .sub{font-size:12.5px;fill:#a1a1aa}
1497
+ .pill{font-size:11px;font-weight:600;letter-spacing:.1em;fill:#a1a1aa}
1498
+ .lab{font-size:11px;font-weight:500;letter-spacing:.09em;fill:#a1a1aa}
1499
+ .big{font-size:60px;font-weight:700;letter-spacing:-.03em;fill:#3ddc97}
1500
+ .val{font-size:26px;font-weight:650;letter-spacing:-.02em}
1501
+ .foot{font-size:11.5px;fill:#71717a}
1502
+ </style>
1503
+ <defs>
1504
+ <radialGradient id="o" cx="1" cy="0" r=".7"><stop offset="0" stop-color="#ff6a2b" stop-opacity=".24"/><stop offset="1" stop-color="#ff6a2b" stop-opacity="0"/></radialGradient>
1505
+ <radialGradient id="b" cx="0" cy="0" r=".7"><stop offset="0" stop-color="#38bdf8" stop-opacity=".16"/><stop offset="1" stop-color="#38bdf8" stop-opacity="0"/></radialGradient>
1506
+ <linearGradient id="m" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#ff6a2b"/><stop offset="1" stop-color="#ffa86b"/></linearGradient>
1507
+ <clipPath id="c"><rect width="${W}" height="${H}" rx="20"/></clipPath>
1508
+ </defs>
1509
+ <g clip-path="url(#c)"><rect width="${W}" height="${H}" fill="#0b0b0f"/><rect width="${W}" height="${H}" fill="url(#o)"/><rect width="${W}" height="${H}" fill="url(#b)"/></g>
1510
+ <rect x=".5" y=".5" width="${W - 1}" height="${H - 1}" rx="19.5" fill="none" stroke="#fff" stroke-opacity=".1"/>
1511
+ <rect x="${M}" y="28" width="38" height="38" rx="11" fill="url(#m)"/>
1512
+ <path d="M4 4h4v4H4zm8 0h4v4h-4zM8 8h4v4H8zm8 0h4v4h-4zM4 12h4v4H4zm8 0h4v4h-4zm-4 4h4v4H8zm8 0h4v4h-4z" fill="#fff" transform="translate(${M + 7} 35) scale(1)"/>
1513
+ <text x="${M + 52}" y="45" class="name">Pitroom</text><text x="${M + 52}" y="63" class="sub">Your agent's pit crew</text>
1514
+ <rect x="${W - M - pill}" y="34" width="${pill}" height="26" rx="13" fill="#fff" fill-opacity=".04" stroke="#fff" stroke-opacity=".14"/>
1515
+ <text x="${W - M - pill / 2}" y="51" class="pill" text-anchor="middle">${esc(when2)}</text>
1516
+ <text x="${M}" y="112" class="lab">SAVED ON YOUR MAIN CODING AGENT</text>
1517
+ <text x="${M}" y="168" class="big">${esc(usd(t.saved))}</text>
1518
+ ${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.round(t.ratio)}\xD7` : "\u2014", "Context compression")}${tile(2, String(t.runs), "Delegated tasks")}
1519
+ <text x="${M}" y="296" class="foot">Estimated against ${esc(primaryPrice().name)} pricing \xB7 npx pitroom</text>
1520
+ </svg>
1521
+ `;
1522
+ }
1523
+
1524
+ // src/core/ui.ts
1525
+ var WEEK_MS = 7 * 24 * 3600 * 1e3;
1526
+ var RECENT = 40;
1527
+ var RECENT_HOURS = 1;
1528
+ var MAX_CARDS = 5;
1529
+ function activeRuns() {
1530
+ const out = [];
1531
+ for (const id of listRunIds().slice(-RECENT)) {
1532
+ try {
1533
+ const m = readMeta(id);
1534
+ if (isActive(m.state) && (m.state === "queued" || isAlive(m.pid))) out.push(m);
1535
+ } catch {
1536
+ }
1537
+ }
1538
+ return out;
1539
+ }
1540
+ function statusLine() {
1541
+ const running = activeRuns();
1542
+ const saved = totals(readLedger(Date.now() - WEEK_MS)).saved;
1543
+ if (!running.length && !saved) return "";
1544
+ const parts = ["\u{1F3C1} pitroom"];
1545
+ if (running.length) {
1546
+ const who = [...new Set(running.map((m) => (m.ran ?? m.worker).backend))].join(", ");
1547
+ parts.push(`${running.length} running (${who})`);
1548
+ }
1549
+ if (saved) parts.push(`~${usd(saved)} saved this week`);
1550
+ return parts.join(" \xB7 ");
1551
+ }
1552
+ var RUN_ID = /\b\d{8}-\d{6}-[0-9a-f]{4}\b/g;
1553
+ var PITROOM_CALL = /(^|[\s;&|(`])(\S*\/)?pitroom(\.mjs)?(\s|$)/;
1554
+ var ICON = { done: "\u2714", failed: "\u2718", timeout: "\u23F1", stopped: "\u25A0" };
1555
+ function kind(m) {
1556
+ if (m.reviewOf) return m.reviewKind === "range" ? "branch review" : m.reviewKind === "fix" ? "re-review" : "review";
1557
+ return m.mode === "read" ? "research" : m.mode === "isolate" ? "change (isolated copy)" : "change";
1558
+ }
1559
+ var SUBJECT_MAX = 48;
1560
+ function short(text) {
1561
+ const s = text.replace(/`/g, "").replace(/^#+\s*/, "").replace(/\s+/g, " ").trim();
1562
+ if (s.length <= SUBJECT_MAX) return s;
1563
+ const cut = s.slice(0, SUBJECT_MAX);
1564
+ const space = cut.lastIndexOf(" ");
1565
+ return `${(space > SUBJECT_MAX / 2 ? cut.slice(0, space) : cut).replace(/[\s,.;:·-]+$/, "")}\u2026`;
1566
+ }
1567
+ var MODEL_MAX = 24;
1568
+ function workerName(t) {
1569
+ if (!t.model) return t.backend;
1570
+ const model = (t.model.split("/").pop() ?? t.model).replace(/-(contributor-)?free$/, "").replace(/-\d+\.\d+$/, "");
1571
+ return `${t.backend} (${model.length > MODEL_MAX ? `${model.slice(0, MODEL_MAX - 1)}\u2026` : model})`;
1572
+ }
1573
+ function what(m) {
1574
+ if (m.plan) return short(`Task ${m.plan.step}: ${m.plan.title}`);
1575
+ if (m.reviewOf) return `of ${m.reviewOf.replace(/\b([0-9a-f]{9})[0-9a-f]{31}\b/g, "$1")}`;
1576
+ const line = m.task.split("\n").find((l) => l.trim()) ?? "";
1577
+ const brief2 = /^You are implementing Task (\d+)\b.*?\bplan\s+(\S+)/i.exec(line);
1578
+ if (brief2) return short(`Task ${brief2[1]} \xB7 ${brief2[2].split("/").pop().replace(/\.md\W*$/, "")}`);
1579
+ return short(line);
1580
+ }
1581
+ function elapsed(m) {
1582
+ const s = Math.round((Date.parse(m.endedAt ?? "") - Date.parse(m.startedAt)) / 1e3);
1583
+ if (!Number.isFinite(s) || s < 0) return "";
1584
+ return s >= 60 ? `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s` : `${s}s`;
1585
+ }
1586
+ var startedCard = (m) => `\u{1F3C1} Pitroom \u25B6 ${kind(m)} on ${workerName(m.worker)} \xB7 ${what(m)} (${m.id})`;
1587
+ function endedCard(m) {
1588
+ const v = m.verdict;
1589
+ const bits = [
1590
+ `\u{1F3C1} Pitroom ${ICON[m.state] ?? "\u2022"} ${kind(m)} ${m.state} on ${workerName(m.ran ?? m.worker)}`,
1591
+ what(m),
1592
+ elapsed(m),
1593
+ v && `SPEC ${v.spec.toUpperCase()} \xB7 QUALITY ${v.quality.toUpperCase()}`,
1594
+ m.changes?.length ? `${m.changes.length} file${m.changes.length === 1 ? "" : "s"} changed` : "",
1595
+ m.savedUsd ? `~${usd(m.savedUsd)} saved` : ""
1596
+ ];
1597
+ return `${bits.filter(Boolean).join(" \xB7 ")} (${m.id})`;
1598
+ }
1599
+ function cardsFor(m) {
1600
+ const phases = [["started", () => startedCard(m)]];
1601
+ if (!isActive(m.state)) phases.push(["ended", () => endedCard(m)]);
1602
+ if (m.applied) {
1603
+ phases.push(["applied", () => `\u{1F3C1} Pitroom \u2935 applied ${m.changes?.length ?? 0} file(s) from ${m.id} to your tree`]);
1604
+ }
1605
+ const out = [];
1606
+ for (const [phase, text] of phases) {
1607
+ const marker = runFile(m.id, `card-${phase}`);
1608
+ if (fs9.existsSync(marker)) continue;
1609
+ if (!(phase === "started" && !isActive(m.state))) out.push(text());
1610
+ fs9.writeFileSync(marker, "");
1611
+ }
1612
+ return out;
1613
+ }
1614
+ function hookCards(input) {
1615
+ const event = JSON.parse(input);
1616
+ const command = event.tool_input?.command ?? "";
1617
+ if (event.tool_name !== "Bash" || !PITROOM_CALL.test(command)) return "";
1618
+ const response = event.tool_response;
1619
+ const output = typeof response === "string" ? response : [response?.stdout, response?.stderr].filter(Boolean).join("\n");
1620
+ const ids = [...new Set(`${command}
1621
+ ${output}`.match(RUN_ID) ?? [])].filter((id) => fs9.existsSync(`${runsDir()}/${id}`));
1622
+ const since = Date.now() - RECENT_HOURS * 36e5;
1623
+ for (const id of listRunIds().slice(-RECENT)) {
1624
+ if (ids.includes(id) || fs9.existsSync(runFile(id, "card-ended"))) continue;
1625
+ try {
1626
+ const m = readMeta(id);
1627
+ if (!isActive(m.state) && Date.parse(m.endedAt ?? m.startedAt) > since) ids.push(id);
1628
+ } catch {
1629
+ }
1630
+ }
1631
+ const cards = [];
1632
+ for (const id of ids.slice(0, MAX_CARDS)) {
1633
+ try {
1634
+ cards.push(...cardsFor(readMeta(id)));
1635
+ } catch {
1636
+ }
1637
+ }
1638
+ return cards.join("\n");
1639
+ }
1640
+
1641
+ // src/core/history.ts
1642
+ var SCHEMA_VERSION = 1;
1643
+ var PATCH_MAX = 1e6;
1644
+ var ANSWER_MAX = 2e5;
1645
+ var cached2;
1646
+ var historyFile = () => path8.join(home(), "history.db");
1647
+ function openDb() {
1648
+ const file = historyFile();
1649
+ if (cached2?.file === file) return cached2.db;
1650
+ let db;
1651
+ try {
1652
+ const emit = process.emitWarning;
1653
+ process.emitWarning = ((w, ...rest) => /SQLite/i.test(String(w?.message ?? w)) ? void 0 : emit.call(process, w, ...rest));
1654
+ let DatabaseSync;
1655
+ try {
1656
+ ({ DatabaseSync } = createRequire(import.meta.url)("node:sqlite"));
1657
+ } finally {
1658
+ process.emitWarning = emit;
1659
+ }
1660
+ fs10.mkdirSync(path8.dirname(file), { recursive: true });
1661
+ db = new DatabaseSync(file);
1662
+ db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
1663
+ migrate(db);
1664
+ } catch {
1665
+ db = void 0;
1666
+ }
1667
+ cached2 = { file, db };
1668
+ return db;
1669
+ }
1670
+ function migrate(db) {
1671
+ const v = db.prepare("PRAGMA user_version").get()?.user_version ?? 0;
1672
+ if (v >= SCHEMA_VERSION) return;
1673
+ db.exec(`
1674
+ CREATE TABLE IF NOT EXISTS runs (
1675
+ id TEXT PRIMARY KEY, started_at TEXT NOT NULL, ended_at TEXT, state TEXT NOT NULL, mode TEXT, kind TEXT,
1676
+ backend TEXT, model TEXT, task TEXT, grp TEXT, dir TEXT, review_of TEXT, verdict TEXT,
1677
+ seconds INTEGER, steps INTEGER, tool_calls INTEGER, tokens INTEGER, returned_tokens INTEGER,
1678
+ cost REAL, saved REAL, files_changed INTEGER, applied INTEGER, error TEXT,
1679
+ answer TEXT, patch TEXT, meta_json TEXT NOT NULL
1680
+ );
1681
+ CREATE INDEX IF NOT EXISTS runs_started ON runs(started_at);
1682
+ CREATE INDEX IF NOT EXISTS runs_model ON runs(backend, model);
1683
+ CREATE INDEX IF NOT EXISTS runs_grp ON runs(grp);
1684
+ CREATE TABLE IF NOT EXISTS steps (
1685
+ run_id TEXT NOT NULL, n INTEGER NOT NULL, kind TEXT, name TEXT, text TEXT, ok INTEGER, at INTEGER,
1686
+ PRIMARY KEY (run_id, n)
1687
+ ) WITHOUT ROWID;
1688
+ CREATE VIRTUAL TABLE IF NOT EXISTS runs_fts USING fts5(id UNINDEXED, task, answer, steps);
1689
+ PRAGMA user_version = ${SCHEMA_VERSION};
1690
+ `);
1691
+ }
1692
+ function readRunFile(id, name) {
1693
+ const f = runFile(id, name);
1694
+ try {
1695
+ if (fs10.existsSync(f)) return fs10.readFileSync(f, "utf8");
1696
+ if (fs10.existsSync(`${f}.gz`)) return zlib.gunzipSync(fs10.readFileSync(`${f}.gz`)).toString("utf8");
1697
+ } catch {
1698
+ }
1699
+ return void 0;
1700
+ }
1701
+ function gzipFile(f) {
1702
+ if (!fs10.existsSync(f)) return;
1703
+ fs10.writeFileSync(`${f}.gz`, zlib.gzipSync(fs10.readFileSync(f)));
1704
+ fs10.rmSync(f);
1705
+ }
1706
+ function compactRun(meta) {
1707
+ if (!TERMINAL.includes(meta.state)) return;
1708
+ const dir = path8.dirname(runFile(meta.id, "meta.json"));
1709
+ let names;
1710
+ try {
1711
+ names = fs10.readdirSync(dir);
1712
+ } catch {
1713
+ return;
1714
+ }
1715
+ for (const n of names) {
1716
+ if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(path8.join(dir, n));
1717
+ else if (/^stderr(\.attempt-\d+)?\.log$/.test(n)) {
1718
+ if (meta.state === "done" && !/attempt/.test(n)) fs10.rmSync(path8.join(dir, n), { force: true });
1719
+ else gzipFile(path8.join(dir, n));
1720
+ }
1721
+ }
1722
+ }
1723
+ var clip4 = (s, n) => s && s.length > n ? s.slice(0, n) : s;
1724
+ function timelineOf(meta) {
1725
+ const raw = readRunFile(meta.id, "events.jsonl");
1726
+ if (!raw) return [];
1727
+ try {
1728
+ return getBackend((meta.ran ?? meta.worker).backend).parse(raw).timeline ?? [];
1729
+ } catch {
1730
+ return [];
1731
+ }
1732
+ }
1733
+ function recordRun(meta) {
1734
+ if (!TERMINAL.includes(meta.state)) return;
1735
+ const db = openDb();
1736
+ if (!db) return;
1737
+ try {
1738
+ const u = meta.usage;
1739
+ const known = db.prepare("SELECT answer IS NOT NULL AS has FROM runs WHERE id = ?").get(meta.id);
1740
+ const answer = known?.has ? void 0 : clip4(readRunFile(meta.id, "summary.md")?.trim(), ANSWER_MAX);
1741
+ const patch = known?.has ? void 0 : clip4(readRunFile(meta.id, "changes.patch"), PATCH_MAX);
1742
+ const seconds = meta.endedAt ? Math.max(0, Math.round((Date.parse(meta.endedAt) - Date.parse(meta.startedAt)) / 1e3)) : null;
1743
+ const t = meta.ran ?? meta.worker;
1744
+ const fields = {
1745
+ id: meta.id,
1746
+ started_at: meta.startedAt,
1747
+ ended_at: meta.endedAt ?? null,
1748
+ state: meta.state,
1749
+ mode: meta.mode,
1750
+ kind: kind(meta),
1751
+ backend: t.backend,
1752
+ model: meta.resolvedModel ?? t.model ?? null,
1753
+ task: clip4(meta.task, 2e4) ?? "",
1754
+ grp: meta.group ?? null,
1755
+ dir: meta.dir,
1756
+ review_of: meta.reviewOf ?? null,
1757
+ verdict: meta.verdict ? `SPEC ${meta.verdict.spec.toUpperCase()} \xB7 QUALITY ${meta.verdict.quality.toUpperCase()}` : null,
1758
+ seconds,
1759
+ steps: u?.steps ?? null,
1760
+ tool_calls: u?.toolCalls ?? null,
1761
+ tokens: u?.total ?? null,
1762
+ returned_tokens: meta.returnedTokens ?? null,
1763
+ cost: u?.cost ?? null,
1764
+ saved: meta.savedUsd ?? null,
1765
+ files_changed: meta.changes?.length ?? 0,
1766
+ applied: meta.applied ? 1 : 0,
1767
+ error: clip4(meta.error, 2e3) ?? null,
1768
+ meta_json: JSON.stringify(meta)
1769
+ };
1770
+ db.exec("BEGIN IMMEDIATE");
1771
+ try {
1772
+ const cols = Object.keys(fields);
1773
+ db.prepare(`INSERT INTO runs (${cols.join(",")}) VALUES (${cols.map(() => "?").join(",")})
1774
+ ON CONFLICT(id) DO UPDATE SET ${cols.filter((c) => c !== "id").map((c) => `${c}=excluded.${c}`).join(",")}`).run(...Object.values(fields));
1775
+ if (!known?.has) {
1776
+ const steps = timelineOf(meta);
1777
+ db.prepare("UPDATE runs SET answer = ?, patch = ? WHERE id = ?").run(answer ?? "", patch ?? null, meta.id);
1778
+ db.prepare("DELETE FROM steps WHERE run_id = ?").run(meta.id);
1779
+ const ins = db.prepare("INSERT INTO steps (run_id, n, kind, name, text, ok, at) VALUES (?,?,?,?,?,?,?)");
1780
+ steps.forEach((s, n) => ins.run(meta.id, n, s.kind, s.name ?? null, s.text, s.ok === void 0 ? null : s.ok ? 1 : 0, s.at ?? null));
1781
+ db.prepare("DELETE FROM runs_fts WHERE id = ?").run(meta.id);
1782
+ db.prepare("INSERT INTO runs_fts (id, task, answer, steps) VALUES (?,?,?,?)").run(meta.id, fields.task, answer ?? "", steps.map((s) => s.text).join("\n"));
1783
+ }
1784
+ db.exec("COMMIT");
1785
+ } catch (e) {
1786
+ db.exec("ROLLBACK");
1787
+ throw e;
1788
+ }
1789
+ compactRun(meta);
1790
+ } catch {
1791
+ }
1792
+ }
1793
+ function importRuns() {
1794
+ const db = openDb();
1795
+ if (!db) return { imported: 0, known: 0 };
1796
+ const have = new Set(db.prepare("SELECT id FROM runs").all().map((r) => r.id));
1797
+ let imported = 0;
1798
+ for (const id of listRunIds()) {
1799
+ if (have.has(id)) continue;
1800
+ try {
1801
+ const m = readMeta(id);
1802
+ if (!TERMINAL.includes(m.state)) continue;
1803
+ recordRun(m);
1804
+ imported++;
1805
+ } catch {
1806
+ }
1807
+ }
1808
+ return { imported, known: have.size };
1809
+ }
1810
+ function archivedRun(id) {
1811
+ const db = openDb();
1812
+ if (!db) return void 0;
1813
+ try {
1814
+ const r = db.prepare("SELECT meta_json, answer, patch, started_at FROM runs WHERE id = ?").get(id);
1815
+ if (!r) return void 0;
1816
+ const start = Date.parse(r.started_at);
1817
+ const steps = db.prepare("SELECT kind, name, text, ok, at FROM steps WHERE run_id = ? ORDER BY n").all(id).map((s) => ({
1818
+ kind: s.kind,
1819
+ name: s.name ?? void 0,
1820
+ text: s.text,
1821
+ ok: s.ok === null ? void 0 : !!s.ok,
1822
+ at: s.at ?? void 0,
1823
+ t: s.at && Number.isFinite(start) ? Math.max(0, Math.round((s.at - start) / 1e3)) : void 0
1824
+ }));
1825
+ return { meta: JSON.parse(r.meta_json), answer: r.answer ?? "", patch: r.patch ?? void 0, steps };
1826
+ } catch {
1827
+ return void 0;
1828
+ }
1829
+ }
1830
+ function archivedId(ref) {
1831
+ const db = openDb();
1832
+ if (!db || !/^[\w-]{3,}$/.test(ref)) return void 0;
1833
+ try {
1834
+ const rows = db.prepare("SELECT id FROM runs WHERE id = ? OR id LIKE ? OR id LIKE ? ORDER BY id").all(ref, `${ref}%`, `%${ref}`);
1835
+ return rows.length === 1 ? rows[0].id : rows.find((r) => r.id === ref)?.id;
1836
+ } catch {
1837
+ return void 0;
1838
+ }
1839
+ }
1840
+ var ftsQuery = (text) => text.split(/\s+/).filter(Boolean).map((w) => `"${w.replace(/"/g, '""')}"*`).join(" ");
1841
+ function listHistory(q = {}) {
1842
+ const db = openDb();
1843
+ if (!db) return { rows: [], total: 0 };
1844
+ const where = [];
1845
+ const args = [];
1846
+ if (q.text?.trim()) {
1847
+ where.push("r.id IN (SELECT id FROM runs_fts WHERE runs_fts MATCH ?)");
1848
+ args.push(ftsQuery(q.text));
1849
+ }
1850
+ if (q.model) {
1851
+ where.push("r.model = ?");
1852
+ args.push(q.model);
1853
+ }
1854
+ if (q.backend) {
1855
+ where.push("r.backend = ?");
1856
+ args.push(q.backend);
1857
+ }
1858
+ if (q.state) {
1859
+ where.push(q.state === "problem" ? "r.state IN ('failed','timeout','stopped')" : "r.state = ?");
1860
+ if (q.state !== "problem") args.push(q.state);
1861
+ }
1862
+ if (q.group) {
1863
+ where.push("r.grp = ?");
1864
+ args.push(q.group);
1865
+ }
1866
+ if (q.sinceMs) {
1867
+ where.push("r.started_at >= ?");
1868
+ args.push(new Date(q.sinceMs).toISOString());
1869
+ }
1870
+ const base2 = where.length ? `WHERE ${where.join(" AND ")}` : "";
1871
+ try {
1872
+ const total = db.prepare(`SELECT COUNT(*) AS n FROM runs r ${base2}`).get(...args).n;
1873
+ const page = q.beforeId ? `${base2 ? `${base2} AND` : "WHERE"} r.id < ?` : base2;
1874
+ const rows = db.prepare(`SELECT r.* FROM runs r ${page} ORDER BY r.id DESC LIMIT ?`).all(...args, ...q.beforeId ? [q.beforeId] : [], Math.min(Math.max(q.limit ?? 30, 1), 200));
1875
+ return {
1876
+ total,
1877
+ rows: rows.map((r) => ({
1878
+ id: r.id,
1879
+ startedAt: r.started_at,
1880
+ state: r.state,
1881
+ kind: r.kind,
1882
+ backend: r.backend,
1883
+ model: r.model ?? void 0,
1884
+ task: r.review_of ? `of ${r.review_of.replace(/\b([0-9a-f]{9})[0-9a-f]{31}\b/g, "$1")}` : r.task.split("\n").find((l) => l.trim()) ?? "",
1885
+ group: r.grp ?? void 0,
1886
+ verdict: r.verdict ?? void 0,
1887
+ seconds: r.seconds ?? void 0,
1888
+ steps: r.steps ?? void 0,
1889
+ tokens: r.tokens ?? void 0,
1890
+ saved: r.saved ?? void 0,
1891
+ files: r.files_changed ?? 0,
1892
+ applied: !!r.applied
1893
+ }))
1894
+ };
1895
+ } catch {
1896
+ return { rows: [], total: 0 };
1897
+ }
1898
+ }
1899
+ function historyStats(sinceMs2) {
1900
+ const empty = { totals: { runs: 0, ok: 0, failed: 0, seconds: 0, tokens: 0, saved: 0 }, byWorker: [], byDay: [] };
1901
+ const db = openDb();
1902
+ if (!db) return empty;
1903
+ const since = sinceMs2 ? new Date(sinceMs2).toISOString() : "";
1904
+ try {
1905
+ const t = db.prepare("SELECT COUNT(*) runs, COALESCE(SUM(state='done'),0) ok, COALESCE(SUM(seconds),0) seconds, COALESCE(SUM(tokens),0) tokens, COALESCE(SUM(saved),0) saved FROM runs WHERE started_at >= ?").get(since);
1906
+ const w = db.prepare("SELECT backend, model, COUNT(*) runs, SUM(state='done') ok, AVG(seconds) avg_s, AVG(tokens) avg_t, COALESCE(SUM(saved),0) saved FROM runs WHERE started_at >= ? GROUP BY backend, model ORDER BY runs DESC LIMIT 40").all(since);
1907
+ const d = db.prepare("SELECT substr(started_at,1,10) day, COUNT(*) runs, SUM(state='done') ok, COALESCE(SUM(saved),0) saved FROM runs WHERE started_at >= ? GROUP BY day ORDER BY day DESC LIMIT 60").all(since);
1908
+ return {
1909
+ totals: { runs: t.runs, ok: t.ok, failed: t.runs - t.ok, seconds: t.seconds, tokens: t.tokens, saved: t.saved },
1910
+ byWorker: w.map((r) => ({ backend: r.backend, model: r.model ?? void 0, runs: r.runs, ok: r.ok ?? 0, avgSeconds: r.avg_s, avgTokens: r.avg_t, saved: r.saved })),
1911
+ byDay: d.reverse().map((r) => ({ day: r.day, runs: r.runs, ok: r.ok ?? 0, saved: r.saved }))
1912
+ };
1913
+ } catch {
1914
+ return empty;
1915
+ }
1916
+ }
1917
+
1918
+ // src/core/report.ts
1919
+ import fs12 from "node:fs";
1920
+
1921
+ // src/core/plan.ts
1922
+ import fs11 from "node:fs";
1923
+ import path9 from "node:path";
1924
+ var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
1925
+ var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
1926
+ function structure(lines) {
1927
+ const headings = [];
1928
+ const rules = [];
1929
+ const fenced = [];
1930
+ let open;
1931
+ lines.forEach((l, i) => {
1932
+ const f = FENCE.exec(l);
1933
+ if (f) {
1934
+ fenced[i] = true;
1935
+ const mark = f[1];
1936
+ if (!open) open = mark;
1937
+ else if (mark[0] === open[0] && mark.length >= open.length && !f[2].trim()) open = void 0;
1938
+ return;
1939
+ }
1940
+ fenced[i] = !!open;
1941
+ if (open) return;
1942
+ const h = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/.exec(l);
1943
+ if (h) headings.push({ line: i, level: h[1].length, text: h[2] });
1944
+ else if (RULE.test(l)) rules.push(i);
1945
+ });
1946
+ return { headings, rules, fenced };
1947
+ }
1948
+ function parsePlan(text, file = "") {
1949
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
1950
+ const { headings, rules, fenced } = structure(lines);
1951
+ const after = (line) => Math.min(headings.find((h) => h.line > line)?.line ?? lines.length, rules.find((r) => r > line) ?? lines.length);
1952
+ const titleHeading = headings.find((h) => h.level === 1);
1953
+ const start = titleHeading ? titleHeading.line + 1 : 0;
1954
+ const header = lines.slice(start, after(start - 1)).filter((l) => !/^\s*>/.test(l)).join("\n").trim();
1955
+ const gc = headings.find((h) => /^global constraints\b/i.test(h.text));
1956
+ const constraints = gc ? lines.slice(gc.line + 1, after(gc.line)).join("\n").trim() : "";
1957
+ const tasks = [];
1958
+ for (const h of headings) {
1959
+ const m = /^Task\s+(\d+)\b\s*[:.)\-–—]?\s*(.*)$/i.exec(h.text);
1960
+ if (!m) continue;
1961
+ const end = headings.find((o) => o.line > h.line && o.level <= h.level)?.line ?? lines.length;
1962
+ const body = lines.slice(h.line, end).join("\n").replace(/(\n\s*(?:---|\*\*\*|___)\s*)+$/, "").trimEnd();
1963
+ const unfenced = lines.slice(h.line, end).filter((_, j) => !fenced[h.line + j]).join("\n");
1964
+ const tier = /^\s*[-*]?\s*\*\*Worker:\*\*\s*`?([\w-]+)`?/m.exec(unfenced)?.[1]?.toLowerCase();
1965
+ tasks.push({ step: Number(m[1]), title: m[2].trim(), text: body, ...tier ? { tier } : {} });
1966
+ }
1967
+ return { file, title: titleHeading?.text ?? "", header, constraints, tasks };
1484
1968
  }
1485
- function badgeUrl(t) {
1486
- const msg = encodeURIComponent(`saved ${usd(t.saved)} \xB7 ${compact(t.tokens)} tokens offloaded`).replace(/-/g, "--");
1487
- return `https://img.shields.io/badge/pitroom-${msg}-7c3aed`;
1969
+ function loadPlan(file) {
1970
+ const abs = path9.resolve(file);
1971
+ if (!fs11.existsSync(abs) || !fs11.statSync(abs).isFile()) throw new UserError(`plan not found: ${file}`);
1972
+ const plan = parsePlan(fs11.readFileSync(abs, "utf8"), fs11.realpathSync(abs));
1973
+ if (!plan.tasks.length) throw new UserError(`${file} has no "Task N" headings (see pitroom-writing-plans)`);
1974
+ return plan;
1488
1975
  }
1489
- var esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
1490
- function card(t, period) {
1491
- const stat = (x, value, label) => `<text x="${x}" y="148" class="v">${esc(value)}</text><text x="${x}" y="170" class="l">${esc(label)}</text>`;
1492
- return `<svg xmlns="http://www.w3.org/2000/svg" width="600" height="220" viewBox="0 0 600 220" role="img" aria-label="pitroom saved ${esc(usd(t.saved))}">
1493
- <style>
1494
- text{font-family:ui-sans-serif,-apple-system,Segoe UI,Helvetica,Arial,sans-serif;fill:#ede9fe}
1495
- .h{font-size:15px;font-weight:600;fill:#a78bfa;letter-spacing:.08em}
1496
- .big{font-size:54px;font-weight:800;fill:#fff}
1497
- .sub{font-size:15px;fill:#c4b5fd}
1498
- .v{font-size:22px;font-weight:700;fill:#fff}
1499
- .l{font-size:12px;fill:#a78bfa}
1500
- .f{font-size:11px;fill:#8b5cf6}
1501
- </style>
1502
- <defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#1e1b4b"/><stop offset="1" stop-color="#3b0764"/></linearGradient></defs>
1503
- <rect width="600" height="220" rx="18" fill="url(#g)"/>
1504
- <text x="32" y="42" class="h">PITROOM \xB7 ${esc(period.toUpperCase())}</text>
1505
- <text x="32" y="98" class="big">${esc(usd(t.saved))}</text>
1506
- <text x="${Math.min(60 + usd(t.saved).length * 30, 330)}" y="98" class="sub">saved on my main coding agent</text>
1507
- ${stat(32, compact(t.tokens), "tokens offloaded")}
1508
- ${stat(200, t.ratio ? `${Math.round(t.ratio)}\xD7` : "\u2014", "context compression")}
1509
- ${stat(380, String(t.runs), "delegated tasks")}
1510
- <text x="32" y="202" class="f">estimated vs. ${esc(primaryPrice().name)} pricing \xB7 npx pitroom</text>
1511
- </svg>
1976
+ function planTask(plan, step) {
1977
+ const t = plan.tasks.find((x) => x.step === step);
1978
+ if (!t) throw new UserError(`no Task ${step} in ${plan.file}; it has ${plan.tasks.map((x) => `Task ${x.step}`).join(", ")}`);
1979
+ return t;
1980
+ }
1981
+ function brief(plan, task) {
1982
+ return `${[
1983
+ `# ${plan.title || planName(plan.file)}`,
1984
+ plan.header,
1985
+ "## Global Constraints",
1986
+ plan.constraints || "(none stated in the plan)",
1987
+ task.text
1988
+ ].filter(Boolean).join("\n\n")}
1512
1989
  `;
1513
1990
  }
1991
+ var planName = (file) => path9.basename(file).replace(/\.md$/i, "");
1514
1992
 
1515
1993
  // src/core/report.ts
1516
- var ICON = {
1994
+ var ICON2 = {
1517
1995
  queued: "\u22EF",
1518
1996
  running: "\u2026",
1519
1997
  done: "\u2714",
@@ -1529,11 +2007,11 @@ function duration(meta) {
1529
2007
  }
1530
2008
  function readSummary(meta) {
1531
2009
  const f = runFile(meta.id, "summary.md");
1532
- return fs10.existsSync(f) ? fs10.readFileSync(f, "utf8").trim() : "";
2010
+ return fs12.existsSync(f) ? fs12.readFileSync(f, "utf8").trim() : archivedRun(meta.id)?.answer ?? "";
1533
2011
  }
1534
2012
  function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
1535
2013
  const out = [];
1536
- out.push(`pitroom ${ICON[meta.state]} ${meta.state} \xB7 ${meta.mode} \xB7 ${duration(meta)} \xB7 run ${meta.id}`);
2014
+ out.push(`pitroom ${ICON2[meta.state]} ${meta.state} \xB7 ${meta.mode} \xB7 ${duration(meta)} \xB7 run ${meta.id}`);
1537
2015
  const ran = meta.ran ?? meta.worker;
1538
2016
  const ids = [`worker ${ran.backend}`, meta.resolvedModel && `model ${meta.resolvedModel}`, meta.sessionId && `session ${meta.sessionId}`];
1539
2017
  out.push(ids.filter(Boolean).join(" \xB7 "));
@@ -1604,8 +2082,8 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
1604
2082
  }
1605
2083
  function live(meta) {
1606
2084
  const f = runFile(meta.id, "events.jsonl");
1607
- if (!fs10.existsSync(f)) return { steps: 0, toolCalls: 0 };
1608
- const p = getBackend((meta.ran ?? meta.worker).backend).parse(fs10.readFileSync(f, "utf8"));
2085
+ if (!fs12.existsSync(f)) return { steps: 0, toolCalls: 0 };
2086
+ const p = getBackend((meta.ran ?? meta.worker).backend).parse(fs12.readFileSync(f, "utf8"));
1609
2087
  return { steps: p.usage.steps, toolCalls: p.usage.toolCalls, last: p.lastActivity };
1610
2088
  }
1611
2089
  function progress(meta) {
@@ -1676,22 +2154,30 @@ async function waitMany(ids, opts) {
1676
2154
  }
1677
2155
  async function watch(select, opts) {
1678
2156
  const seen = /* @__PURE__ */ new Map();
1679
- const emit = (e) => opts.write(`${JSON.stringify(e)}
2157
+ const line = (s) => opts.write(`${s}
1680
2158
  `);
2159
+ const emit = (e, m) => {
2160
+ if (!opts.brief) return opts.write(`${JSON.stringify(e)}
2161
+ `);
2162
+ if (e.event === "started" && m) line(startedCard(m));
2163
+ else if (e.event === "fallback" && m) line(`\u{1F3C1} Pitroom \u21BB ${m.id} fell back from ${String(e.failed)}: ${String(e.reason)}`);
2164
+ else if (m && TERMINAL.includes(m.state)) line(endedCard(m));
2165
+ else if (e.event === "all-done") line(`\u{1F3C1} Pitroom: ${String(e.runs)} run${e.runs === 1 ? "" : "s"} finished (${String(e.ok)} ok${e.failed ? `, ${String(e.failed)} not ok` : ""})`);
2166
+ };
1681
2167
  const deadline = opts.timeoutMs > 0 ? Date.now() + opts.timeoutMs : Infinity;
1682
2168
  for (; ; ) {
1683
2169
  const metas = select().map((id) => freshMeta(id));
1684
- if (opts.json) {
2170
+ if (opts.json || opts.brief) {
1685
2171
  for (const m of metas) {
1686
2172
  const prev = seen.get(m.id);
1687
2173
  const l = m.state === "running" ? live(m) : { steps: m.usage?.steps ?? 0, toolCalls: m.usage?.toolCalls ?? 0 };
1688
2174
  const attempts = m.attempts?.length ?? 0;
1689
2175
  const base2 = { run: m.id, mode: m.mode, task: oneLine4(m.task, 60) };
1690
- if (!prev) emit({ event: m.state === "queued" ? "queued" : "started", ...base2 });
1691
- else if (prev.state === "queued" && m.state === "running") emit({ event: "started", ...base2 });
2176
+ if (!prev) emit({ event: m.state === "queued" ? "queued" : "started", ...base2 }, m);
2177
+ else if (prev.state === "queued" && m.state === "running") emit({ event: "started", ...base2 }, m);
1692
2178
  if (attempts > (prev?.attempts ?? 0)) {
1693
2179
  const a = m.attempts[attempts - 1];
1694
- emit({ event: "fallback", run: m.id, failed: a.target, reason: oneLine4(a.error, 120) });
2180
+ emit({ event: "fallback", run: m.id, failed: a.target, reason: oneLine4(a.error, 120) }, m);
1695
2181
  }
1696
2182
  if (m.state === "running" && l.steps > (prev?.steps ?? 0)) {
1697
2183
  emit({ event: "progress", run: m.id, steps: l.steps, tools: l.toolCalls, last: "last" in l && l.last ? oneLine4(String(l.last), 80) : void 0 });
@@ -1706,7 +2192,7 @@ async function watch(select, opts) {
1706
2192
  error: m.error ? oneLine4(m.error, 160) : void 0,
1707
2193
  refs: m.refs ? `${m.refs.valid}/${m.refs.total}` : void 0,
1708
2194
  changes: m.changes?.length || void 0
1709
- });
2195
+ }, m);
1710
2196
  }
1711
2197
  seen.set(m.id, { state: m.state, steps: l.steps, attempts });
1712
2198
  }
@@ -1718,7 +2204,7 @@ ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \xB7 Ctrl-C to stop watchin
1718
2204
  }
1719
2205
  const allDone = metas.length > 0 && metas.every((m) => TERMINAL.includes(m.state));
1720
2206
  if (allDone || metas.length === 0 && seen.size === 0) {
1721
- if (opts.json) {
2207
+ if (opts.json || opts.brief) {
1722
2208
  const ok = metas.filter((m) => m.state === "done").length;
1723
2209
  emit({ event: "all-done", runs: metas.length, ok, failed: metas.length - ok });
1724
2210
  }
@@ -1730,44 +2216,44 @@ ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \xB7 Ctrl-C to stop watchin
1730
2216
  }
1731
2217
 
1732
2218
  // src/core/install.ts
1733
- import fs11 from "node:fs";
2219
+ import fs13 from "node:fs";
1734
2220
  import os7 from "node:os";
1735
- import path9 from "node:path";
2221
+ import path10 from "node:path";
1736
2222
  import { fileURLToPath } from "node:url";
1737
2223
  var LEGACY = ["pitroom", "opencode-worker"];
1738
2224
  function packageRoot() {
1739
- return path9.resolve(path9.dirname(fileURLToPath(import.meta.url)), "..");
2225
+ return path10.resolve(path10.dirname(fileURLToPath(import.meta.url)), "..");
1740
2226
  }
1741
2227
  function skillNames(root = packageRoot()) {
1742
- const dir = path9.join(root, "skills");
1743
- if (!fs11.existsSync(dir)) return [];
1744
- return fs11.readdirSync(dir).filter((d) => fs11.existsSync(path9.join(dir, d, "SKILL.md"))).sort();
2228
+ const dir = path10.join(root, "skills");
2229
+ if (!fs13.existsSync(dir)) return [];
2230
+ return fs13.readdirSync(dir).filter((d) => fs13.existsSync(path10.join(dir, d, "SKILL.md"))).sort();
1745
2231
  }
1746
2232
  function skillTargets() {
1747
2233
  const home2 = os7.homedir();
1748
- const targets = [path9.join(home2, ".agents", "skills")];
1749
- if (fs11.existsSync(path9.join(home2, ".claude"))) targets.push(path9.join(home2, ".claude", "skills"));
2234
+ const targets = [path10.join(home2, ".agents", "skills")];
2235
+ if (fs13.existsSync(path10.join(home2, ".claude"))) targets.push(path10.join(home2, ".claude", "skills"));
1750
2236
  return targets;
1751
2237
  }
1752
- var launcherPath = () => path9.join(os7.homedir(), ".local", "bin", "pitroom");
2238
+ var launcherPath = () => path10.join(os7.homedir(), ".local", "bin", "pitroom");
1753
2239
  var LAUNCHER_MARK = "# pitroom launcher";
1754
2240
  function launcherScript(bundle) {
1755
2241
  const q = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
1756
2242
  return `#!/bin/sh
1757
2243
  ${LAUNCHER_MARK} (created by \`pitroom install\`; \`pitroom uninstall\` removes it)
1758
2244
  cli=${q(bundle)}
1759
- ok() { [ -n "$1" ] && [ -x "$1" ] && "$1" -e 'process.exit(+process.versions.node.split(".")[0] >= 18 ? 0 : 1)' 2>/dev/null; }
2245
+ ok() { [ -n "$1" ] && [ -x "$1" ] && "$1" -e 'const [a,b]=process.versions.node.split(".").map(Number);process.exit(a>22||(a===22&&b>=13)?0:1)' 2>/dev/null; }
1760
2246
  for n in "\${PITROOM_NODE:-}" ${q(process.execPath)} "$(command -v node 2>/dev/null)" "$HOME"/.nvm/versions/node/*/bin/node /opt/homebrew/bin/node /usr/local/bin/node; do
1761
2247
  if ok "$n"; then exec "$n" "$cli" "$@"; fi
1762
2248
  done
1763
- echo "pitroom: needs Node.js 18 or newer (set PITROOM_NODE to its path)" >&2
2249
+ echo "pitroom: needs Node.js 22.13 or newer (set PITROOM_NODE to its path)" >&2
1764
2250
  exit 127
1765
2251
  `;
1766
2252
  }
1767
2253
  function isOurLauncher(file, root) {
1768
2254
  if (linksInto(file, root)) return true;
1769
2255
  try {
1770
- const s = fs11.readFileSync(file, "utf8");
2256
+ const s = fs13.readFileSync(file, "utf8");
1771
2257
  return s.includes(LAUNCHER_MARK) && s.includes(root);
1772
2258
  } catch {
1773
2259
  return false;
@@ -1775,57 +2261,57 @@ function isOurLauncher(file, root) {
1775
2261
  }
1776
2262
  function placeLauncher(bundle, root, force) {
1777
2263
  const dest = launcherPath();
1778
- fs11.mkdirSync(path9.dirname(dest), { recursive: true });
1779
- const exists = fs11.lstatSync(dest, { throwIfNoEntry: false });
2264
+ fs13.mkdirSync(path10.dirname(dest), { recursive: true });
2265
+ const exists = fs13.lstatSync(dest, { throwIfNoEntry: false });
1780
2266
  if (exists && !isOurLauncher(dest, root)) {
1781
2267
  if (!force) return `! ${dest} exists and is not Pitroom's launcher; kept (use --force to back it up and replace)`;
1782
- fs11.renameSync(dest, `${dest}.bak-${Date.now()}`);
2268
+ fs13.renameSync(dest, `${dest}.bak-${Date.now()}`);
1783
2269
  } else if (exists) {
1784
- fs11.rmSync(dest, { force: true });
2270
+ fs13.rmSync(dest, { force: true });
1785
2271
  }
1786
- fs11.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
1787
- return `\u2714 ${dest} \u2192 launcher for ${bundle} (Node 18+)`;
2272
+ fs13.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
2273
+ return `\u2714 ${dest} \u2192 launcher for ${bundle} (Node 22.13+)`;
1788
2274
  }
1789
2275
  function linksInto(link, dir) {
1790
- const st = fs11.lstatSync(link, { throwIfNoEntry: false });
2276
+ const st = fs13.lstatSync(link, { throwIfNoEntry: false });
1791
2277
  if (!st?.isSymbolicLink()) return false;
1792
- const target = path9.resolve(path9.dirname(link), fs11.readlinkSync(link));
1793
- const rel = path9.relative(dir, target);
1794
- return !rel.startsWith("..") && !path9.isAbsolute(rel);
2278
+ const target = path10.resolve(path10.dirname(link), fs13.readlinkSync(link));
2279
+ const rel = path10.relative(dir, target);
2280
+ return !rel.startsWith("..") && !path10.isAbsolute(rel);
1795
2281
  }
1796
2282
  function place(src, dest, opts) {
1797
- fs11.mkdirSync(path9.dirname(dest), { recursive: true });
1798
- const st = fs11.lstatSync(dest, { throwIfNoEntry: false });
2283
+ fs13.mkdirSync(path10.dirname(dest), { recursive: true });
2284
+ const st = fs13.lstatSync(dest, { throwIfNoEntry: false });
1799
2285
  if (st && !st.isSymbolicLink()) {
1800
2286
  if (!opts.force) return `! ${dest} exists and is not a link; kept (use --force to back it up and replace)`;
1801
- fs11.renameSync(dest, `${dest}.bak-${Date.now()}`);
2287
+ fs13.renameSync(dest, `${dest}.bak-${Date.now()}`);
1802
2288
  } else if (st) {
1803
- fs11.unlinkSync(dest);
2289
+ fs13.unlinkSync(dest);
1804
2290
  }
1805
- if (opts.copy) fs11.cpSync(src, dest, { recursive: true });
1806
- else fs11.symlinkSync(src, dest, process.platform === "win32" ? "junction" : fs11.statSync(src).isDirectory() ? "dir" : "file");
2291
+ if (opts.copy) fs13.cpSync(src, dest, { recursive: true });
2292
+ else fs13.symlinkSync(src, dest, process.platform === "win32" ? "junction" : fs13.statSync(src).isDirectory() ? "dir" : "file");
1807
2293
  return `\u2714 ${dest} \u2192 ${opts.copy ? "copied" : src}`;
1808
2294
  }
1809
2295
  function install(opts) {
1810
2296
  const root = packageRoot();
1811
2297
  const skills = skillNames(root);
1812
- if (!skills.length) throw new Error(`no skills found under ${path9.join(root, "skills")}`);
2298
+ if (!skills.length) throw new Error(`no skills found under ${path10.join(root, "skills")}`);
1813
2299
  const out = [];
1814
2300
  for (const base2 of skillTargets()) {
1815
2301
  for (const legacy of LEGACY) {
1816
- const l = path9.join(base2, legacy);
2302
+ const l = path10.join(base2, legacy);
1817
2303
  if (!skills.includes(legacy) && linksInto(l, root)) {
1818
- fs11.unlinkSync(l);
2304
+ fs13.unlinkSync(l);
1819
2305
  out.push(`\u2714 removed old link ${l}`);
1820
2306
  }
1821
2307
  }
1822
- for (const name of skills) out.push(place(path9.join(root, "skills", name), path9.join(base2, name), opts));
2308
+ for (const name of skills) out.push(place(path10.join(root, "skills", name), path10.join(base2, name), opts));
1823
2309
  }
1824
- const bundle = path9.join(root, "dist", "pitroom.mjs");
1825
- if (fs11.existsSync(bundle)) {
2310
+ const bundle = path10.join(root, "dist", "pitroom.mjs");
2311
+ if (fs13.existsSync(bundle)) {
1826
2312
  out.push(placeLauncher(bundle, root, opts.force));
1827
- const onPath = (process.env.PATH ?? "").split(path9.delimiter).some((d) => path9.resolve(d) === path9.dirname(launcherPath()));
1828
- if (!onPath) out.push(`! ${path9.dirname(launcherPath())} is not on PATH; add it, or run ${launcherPath()} directly`);
2313
+ const onPath = (process.env.PATH ?? "").split(path10.delimiter).some((d) => path10.resolve(d) === path10.dirname(launcherPath()));
2314
+ if (!onPath) out.push(`! ${path10.dirname(launcherPath())} is not on PATH; add it, or run ${launcherPath()} directly`);
1829
2315
  }
1830
2316
  return out;
1831
2317
  }
@@ -1833,38 +2319,38 @@ function uninstall() {
1833
2319
  const root = packageRoot();
1834
2320
  const out = [];
1835
2321
  for (const base2 of skillTargets()) {
1836
- if (!fs11.existsSync(base2)) continue;
1837
- for (const name of fs11.readdirSync(base2)) {
1838
- const l = path9.join(base2, name);
2322
+ if (!fs13.existsSync(base2)) continue;
2323
+ for (const name of fs13.readdirSync(base2)) {
2324
+ const l = path10.join(base2, name);
1839
2325
  if (linksInto(l, root)) {
1840
- fs11.unlinkSync(l);
2326
+ fs13.unlinkSync(l);
1841
2327
  out.push(`\u2714 removed ${l}`);
1842
2328
  }
1843
2329
  }
1844
2330
  }
1845
2331
  if (isOurLauncher(launcherPath(), root)) {
1846
- fs11.rmSync(launcherPath(), { force: true });
2332
+ fs13.rmSync(launcherPath(), { force: true });
1847
2333
  out.push(`\u2714 removed ${launcherPath()}`);
1848
2334
  }
1849
2335
  return out.length ? out : ["nothing to remove"];
1850
2336
  }
1851
2337
  function installedSkills() {
1852
2338
  const names = skillNames();
1853
- return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs11.existsSync(path9.join(base2, n, "SKILL.md"))) }));
2339
+ return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs13.existsSync(path10.join(base2, n, "SKILL.md"))) }));
1854
2340
  }
1855
2341
 
1856
2342
  // src/core/plan-status.ts
1857
2343
  import crypto2 from "node:crypto";
1858
- import fs12 from "node:fs";
1859
- import path10 from "node:path";
2344
+ import fs14 from "node:fs";
2345
+ import path11 from "node:path";
1860
2346
  function notesFile(plan) {
1861
2347
  const id = crypto2.createHash("sha1").update(plan.file).digest("hex").slice(0, 12);
1862
- return path10.join(home(), "plans", id, "notes.md");
2348
+ return path11.join(home(), "plans", id, "notes.md");
1863
2349
  }
1864
2350
  function readNotes(plan) {
1865
2351
  const f = notesFile(plan);
1866
- if (!fs12.existsSync(f)) return [];
1867
- return fs12.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
2352
+ if (!fs14.existsSync(f)) return [];
2353
+ return fs14.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
1868
2354
  const m = /^(\d{4}-\d\d-\d\d \d\d:\d\d) (.*)$/.exec(l);
1869
2355
  return m ? { at: m[1], text: m[2] } : { at: "", text: l };
1870
2356
  });
@@ -1872,11 +2358,11 @@ function readNotes(plan) {
1872
2358
  function addNote(planFile, text) {
1873
2359
  const plan = loadPlan(planFile);
1874
2360
  const f = notesFile(plan);
1875
- fs12.mkdirSync(path10.dirname(f), { recursive: true });
1876
- if (!fs12.existsSync(f)) fs12.writeFileSync(f, `# plan: ${plan.file}
2361
+ fs14.mkdirSync(path11.dirname(f), { recursive: true });
2362
+ if (!fs14.existsSync(f)) fs14.writeFileSync(f, `# plan: ${plan.file}
1877
2363
  `);
1878
2364
  const at = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
1879
- fs12.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
2365
+ fs14.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
1880
2366
  `);
1881
2367
  return f;
1882
2368
  }
@@ -1944,15 +2430,15 @@ ${rulings}`,
1944
2430
 
1945
2431
  // src/core/review.ts
1946
2432
  import crypto4 from "node:crypto";
1947
- import fs14 from "node:fs";
1948
- import path12 from "node:path";
2433
+ import fs16 from "node:fs";
2434
+ import path13 from "node:path";
1949
2435
 
1950
2436
  // src/vcs/git.ts
1951
2437
  import { spawnSync as spawnSync4 } from "node:child_process";
1952
2438
  import crypto3 from "node:crypto";
1953
- import fs13 from "node:fs";
2439
+ import fs15 from "node:fs";
1954
2440
  import os8 from "node:os";
1955
- import path11 from "node:path";
2441
+ import path12 from "node:path";
1956
2442
  var SAFE = [
1957
2443
  "-c",
1958
2444
  "color.ui=false",
@@ -1984,20 +2470,20 @@ function gitAvailable() {
1984
2470
  }
1985
2471
  function repoRoot(dir) {
1986
2472
  const r = git(dir, ["rev-parse", "--show-toplevel"]);
1987
- return r.code === 0 ? path11.resolve(r.stdout.trim()) : void 0;
2473
+ return r.code === 0 ? path12.resolve(r.stdout.trim()) : void 0;
1988
2474
  }
1989
2475
  function snapshotTree(root, exclude = []) {
1990
- const tmp = path11.join(os8.tmpdir(), `pitroom-index-${process.pid}-${crypto3.randomBytes(4).toString("hex")}`);
1991
- const real = path11.resolve(root, must(root, ["rev-parse", "--git-path", "index"]).trim());
2476
+ const tmp = path12.join(os8.tmpdir(), `pitroom-index-${process.pid}-${crypto3.randomBytes(4).toString("hex")}`);
2477
+ const real = path12.resolve(root, must(root, ["rev-parse", "--git-path", "index"]).trim());
1992
2478
  const env = { ...process.env, GIT_INDEX_FILE: tmp };
1993
2479
  try {
1994
- if (fs13.existsSync(real)) fs13.copyFileSync(real, tmp);
2480
+ if (fs15.existsSync(real)) fs15.copyFileSync(real, tmp);
1995
2481
  else must(root, ["read-tree", "--empty"], env);
1996
2482
  must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`)], env);
1997
2483
  return must(root, ["write-tree"], env).trim();
1998
2484
  } finally {
1999
- fs13.rmSync(tmp, { force: true });
2000
- fs13.rmSync(`${tmp}.lock`, { force: true });
2485
+ fs15.rmSync(tmp, { force: true });
2486
+ fs15.rmSync(`${tmp}.lock`, { force: true });
2001
2487
  }
2002
2488
  }
2003
2489
  var DIFF = ["diff", "--no-ext-diff", "--no-textconv", "--no-renames"];
@@ -2017,27 +2503,27 @@ function diffTrees(root, a, b) {
2017
2503
  return { changes, stats: { files: changes.length, insertions, deletions }, patch };
2018
2504
  }
2019
2505
  function createIsolatedCopy(root, tree, dest) {
2020
- const objects = path11.join(path11.resolve(root, must(root, ["rev-parse", "--git-common-dir"]).trim()), "objects");
2021
- fs13.mkdirSync(path11.dirname(dest), { recursive: true });
2022
- must(path11.dirname(dest), ["init", "--quiet", dest]);
2023
- fs13.writeFileSync(path11.join(dest, ".git", "objects", "info", "alternates"), `${objects}
2506
+ const objects = path12.join(path12.resolve(root, must(root, ["rev-parse", "--git-common-dir"]).trim()), "objects");
2507
+ fs15.mkdirSync(path12.dirname(dest), { recursive: true });
2508
+ must(path12.dirname(dest), ["init", "--quiet", dest]);
2509
+ fs15.writeFileSync(path12.join(dest, ".git", "objects", "info", "alternates"), `${objects}
2024
2510
  `);
2025
2511
  must(dest, ["read-tree", tree]);
2026
2512
  must(dest, ["checkout-index", "--all", "--force"]);
2027
2513
  }
2028
2514
  function removeIsolatedCopy(dest, ownedBy) {
2029
- const rel = path11.relative(ownedBy, dest);
2030
- if (!rel || rel.startsWith("..") || path11.isAbsolute(rel)) throw new Error(`refusing to remove ${dest}`);
2031
- fs13.rmSync(dest, { recursive: true, force: true });
2515
+ const rel = path12.relative(ownedBy, dest);
2516
+ if (!rel || rel.startsWith("..") || path12.isAbsolute(rel)) throw new Error(`refusing to remove ${dest}`);
2517
+ fs15.rmSync(dest, { recursive: true, force: true });
2032
2518
  }
2033
2519
  function linkIntoWorktree(root, worktree, rels) {
2034
2520
  const linked = [];
2035
2521
  for (const rel of rels) {
2036
- const src = path11.join(root, rel);
2037
- const dst = path11.join(worktree, rel);
2038
- if (!fs13.existsSync(src) || fs13.existsSync(dst)) continue;
2039
- fs13.mkdirSync(path11.dirname(dst), { recursive: true });
2040
- fs13.symlinkSync(src, dst, process.platform === "win32" ? "junction" : void 0);
2522
+ const src = path12.join(root, rel);
2523
+ const dst = path12.join(worktree, rel);
2524
+ if (!fs15.existsSync(src) || fs15.existsSync(dst)) continue;
2525
+ fs15.mkdirSync(path12.dirname(dst), { recursive: true });
2526
+ fs15.symlinkSync(src, dst, process.platform === "win32" ? "junction" : void 0);
2041
2527
  linked.push(rel);
2042
2528
  }
2043
2529
  return linked;
@@ -2096,7 +2582,7 @@ function ancestors(m) {
2096
2582
  function briefOf(m) {
2097
2583
  const root = [m, ...ancestors(m)].at(-1);
2098
2584
  const f = runFile(root.id, "brief.md");
2099
- return fs14.existsSync(f) ? fs14.readFileSync(f, "utf8") : root.task;
2585
+ return fs16.existsSync(f) ? fs16.readFileSync(f, "utf8") : root.task;
2100
2586
  }
2101
2587
  function latestReview(ids) {
2102
2588
  for (const id of listRunIds().reverse()) {
@@ -2110,11 +2596,11 @@ function latestReview(ids) {
2110
2596
  }
2111
2597
  function diffOf(m, from = m.baseTree) {
2112
2598
  const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
2113
- if (where && fs14.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
2599
+ if (where && fs16.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
2114
2600
  if (from !== m.baseTree) {
2115
2601
  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);
2116
2602
  }
2117
- return fs14.readFileSync(runFile(m.id, "changes.patch"), "utf8");
2603
+ return readRunFile(m.id, "changes.patch") ?? archivedRun(m.id)?.patch ?? "";
2118
2604
  }
2119
2605
  function runReview(id) {
2120
2606
  const m = freshMeta(id);
@@ -2124,7 +2610,7 @@ function runReview(id) {
2124
2610
  if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
2125
2611
  const chain = ancestors(m);
2126
2612
  const previous = latestReview(chain.map((a) => a.id));
2127
- const dir = m.mode === "isolate" && m.worktree && fs14.existsSync(m.worktree) ? m.cwd : m.dir;
2613
+ const dir = m.mode === "isolate" && m.worktree && fs16.existsSync(m.worktree) ? m.cwd : m.dir;
2128
2614
  const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
2129
2615
  const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
2130
2616
  if (!previous) {
@@ -2198,15 +2684,15 @@ function pickReviewer(job) {
2198
2684
  function writePackage(job) {
2199
2685
  const g = gitDir(job.dir);
2200
2686
  if (!g) throw new UserError(`not a git repository: ${job.dir}`);
2201
- const file = path12.join(g, "pitroom", `review-${crypto4.randomBytes(4).toString("hex")}.md`);
2202
- fs14.mkdirSync(path12.dirname(file), { recursive: true });
2203
- fs14.writeFileSync(file, job.package);
2687
+ const file = path13.join(g, "pitroom", `review-${crypto4.randomBytes(4).toString("hex")}.md`);
2688
+ fs16.mkdirSync(path13.dirname(file), { recursive: true });
2689
+ fs16.writeFileSync(file, job.package);
2204
2690
  return file;
2205
2691
  }
2206
2692
 
2207
2693
  // src/core/templates.ts
2208
- import fs15 from "node:fs";
2209
- import path13 from "node:path";
2694
+ import fs17 from "node:fs";
2695
+ import path14 from "node:path";
2210
2696
  var FILES = {
2211
2697
  implementer: "pitroom-driven-development/implementer-prompt.md",
2212
2698
  "task-reviewer": "pitroom-driven-development/task-reviewer-prompt.md",
@@ -2214,9 +2700,9 @@ var FILES = {
2214
2700
  "code-reviewer": "pitroom-review/code-reviewer.md"
2215
2701
  };
2216
2702
  function loadTemplate(name, root = packageRoot()) {
2217
- const file = path13.join(root, "skills", FILES[name]);
2218
- if (!fs15.existsSync(file)) throw new UserError(`template missing: ${file} (broken install? run pitroom doctor)`, 3);
2219
- return fs15.readFileSync(file, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
2703
+ const file = path14.join(root, "skills", FILES[name]);
2704
+ if (!fs17.existsSync(file)) throw new UserError(`template missing: ${file} (broken install? run pitroom doctor)`, 3);
2705
+ return fs17.readFileSync(file, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
2220
2706
  }
2221
2707
  function fill(template, values) {
2222
2708
  const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
@@ -2320,133 +2806,332 @@ function formatModels(t) {
2320
2806
  return out.join("\n");
2321
2807
  }
2322
2808
 
2323
- // src/core/ui.ts
2324
- import fs16 from "node:fs";
2325
- var WEEK_MS = 7 * 24 * 3600 * 1e3;
2326
- var RECENT = 40;
2327
- var RECENT_HOURS = 1;
2328
- var MAX_CARDS = 5;
2329
- function activeRuns() {
2330
- const out = [];
2331
- for (const id of listRunIds().slice(-RECENT)) {
2809
+ // src/core/dash.ts
2810
+ import { spawn } from "node:child_process";
2811
+ import crypto5 from "node:crypto";
2812
+ import fs18 from "node:fs";
2813
+ import http from "node:http";
2814
+ import path15 from "node:path";
2815
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
2816
+
2817
+ // src/core/dash-page.ts
2818
+ var THEME_SCRIPT = "(function(){try{var t=localStorage.getItem('pitroom-theme');var d=t?t==='dark':matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.classList.toggle('dark',d)}catch(e){}})()";
2819
+ var ICON3 = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%23ff6a2b'/%3E%3Cpath d='M9 8h4v4H9zm8 0h4v4h-4zm-4 4h4v4h-4zm8 0h4v4h-4zM9 16h4v4H9zm8 0h4v4h-4zm-4 4h4v4h-4zm8 0h4v4h-4z' fill='%23fff'/%3E%3C/svg%3E";
2820
+ var PAGE = `<!doctype html>
2821
+ <html lang="en" class="dark"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
2822
+ <meta name="color-scheme" content="dark light"><title>Pitroom</title><link rel="icon" href="${ICON3}">
2823
+ <script>${THEME_SCRIPT}</script><link rel="stylesheet" href="/assets/app.css"></head>
2824
+ <body><div id="root"></div><script type="module" src="/assets/app.js"></script></body></html>`;
2825
+ var MISSING = `<!doctype html><meta charset="utf-8"><title>Pitroom</title><body style="font:15px system-ui;padding:3rem;max-width:40rem;margin:auto"><h1>Pitroom dash</h1><p>The dashboard files are missing (dist/ui). Run <code>npm run build</code> in the Pitroom repository, or reinstall the package.</p></body>`;
2826
+
2827
+ // src/core/dash.ts
2828
+ var DEFAULT_PORT = 7878;
2829
+ var WEEK_MS2 = 7 * 24 * 3600 * 1e3;
2830
+ var SCAN = 400;
2831
+ var RUN_ID2 = /^\d{8}-\d{6}-[0-9a-f]{4}$/;
2832
+ var LOCAL_HOST = /^(127\.0\.0\.1|localhost|\[::1\]):\d+$/i;
2833
+ var oneLine5 = (s, max) => {
2834
+ const t = s.replace(/\s+/g, " ").trim();
2835
+ return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
2836
+ };
2837
+ function findings(m) {
2838
+ const v = m.verdict;
2839
+ if (!v) return "";
2840
+ const parts = [v.critical && `${v.critical} critical`, v.important && `${v.important} important`, v.minor && `${v.minor} minor`].filter(Boolean);
2841
+ return parts.length ? parts.join(" \xB7 ") : "no findings";
2842
+ }
2843
+ function toRun(m) {
2844
+ const l = m.state === "running" ? live(m) : void 0;
2845
+ return {
2846
+ id: m.id,
2847
+ state: m.state,
2848
+ kind: kind(m),
2849
+ worker: workerName(m.ran ?? m.worker),
2850
+ task: what(m),
2851
+ group: m.group,
2852
+ startedAt: m.startedAt,
2853
+ time: elapsed(m),
2854
+ steps: l ? l.steps : m.usage?.steps ?? 0,
2855
+ tokens: m.usage?.total,
2856
+ saved: m.savedUsd || void 0,
2857
+ verdict: m.verdict ? `SPEC ${m.verdict.spec.toUpperCase()} \xB7 QUALITY ${m.verdict.quality.toUpperCase()}` : void 0,
2858
+ changes: m.changes?.length || void 0,
2859
+ applied: m.applied || void 0,
2860
+ note: l?.last ? oneLine5(String(l.last), 140) : isActive(m.state) ? "" : m.verdict ? findings(m) : headline(m, 200) || oneLine5(m.error ?? "", 200)
2861
+ };
2862
+ }
2863
+ function dashState(opts = {}) {
2864
+ const limit = Math.min(Math.max(opts.limit ?? 40, 1), 200);
2865
+ const runs = [];
2866
+ const groups = /* @__PURE__ */ new Set();
2867
+ for (const id of listRunIds().slice(-SCAN).reverse()) {
2868
+ let m;
2332
2869
  try {
2333
- const m = readMeta(id);
2334
- if (isActive(m.state) && (m.state === "queued" || isAlive(m.pid))) out.push(m);
2870
+ m = freshMeta(id);
2335
2871
  } catch {
2872
+ continue;
2336
2873
  }
2874
+ if (m.group) groups.add(m.group);
2875
+ if (opts.group && m.group !== opts.group) continue;
2876
+ if (runs.length < limit) runs.push(toRun(m));
2337
2877
  }
2338
- return out;
2878
+ return {
2879
+ price: primaryPrice().name,
2880
+ running: runs.filter((r) => r.state === "running" || r.state === "queued").length,
2881
+ saved: totals(readLedger(Date.now() - WEEK_MS2)).saved,
2882
+ groups: [...groups].sort(),
2883
+ runs
2884
+ };
2339
2885
  }
2340
- function statusLine() {
2341
- const running = activeRuns();
2342
- const saved = totals(readLedger(Date.now() - WEEK_MS)).saved;
2343
- if (!running.length && !saved) return "";
2344
- const parts = ["\u{1F3C1} pitroom"];
2345
- if (running.length) {
2346
- const who = [...new Set(running.map((m) => (m.ran ?? m.worker).backend))].join(", ");
2347
- parts.push(`${running.length} running (${who})`);
2886
+ var TASK_MAX = 6e3;
2887
+ var PATCH_LINES = 300;
2888
+ function readFile(id, name) {
2889
+ const f = runFile(id, name);
2890
+ return fs18.existsSync(f) ? fs18.readFileSync(f, "utf8") : "";
2891
+ }
2892
+ function runDetail(id) {
2893
+ if (!RUN_ID2.test(id)) return void 0;
2894
+ let m;
2895
+ try {
2896
+ m = freshMeta(id);
2897
+ } catch {
2898
+ return void 0;
2348
2899
  }
2349
- if (saved) parts.push(`~${usd(saved)} saved this week`);
2350
- return parts.join(" \xB7 ");
2900
+ const start = Date.parse(m.startedAt);
2901
+ const kept = isActive(m.state) ? void 0 : archivedRun(id);
2902
+ let steps = kept?.steps ?? [];
2903
+ if (!steps.length) try {
2904
+ const events = readRunFile(id, "events.jsonl");
2905
+ steps = (events ? getBackend((m.ran ?? m.worker).backend).parse(events).timeline ?? [] : []).map((s) => ({
2906
+ ...s,
2907
+ t: s.at && Number.isFinite(start) ? Math.max(0, Math.round((s.at - start) / 1e3)) : void 0
2908
+ }));
2909
+ } catch {
2910
+ }
2911
+ const rel = (t) => [m.cwd, m.dir].filter(Boolean).reduce((x, base2) => x.split(`${base2}/`).join(""), t);
2912
+ steps = steps.map((x) => ({ ...x, text: rel(x.text) }));
2913
+ const answer = isActive(m.state) ? "" : readSummary(m).replace(/^SUMMARY:\s*SPEC[^\n]*\n+/i, "").replace(/^DETAILS:[ \t]*\n+/i, "").trim();
2914
+ const lastSay = steps.at(-1);
2915
+ if (lastSay?.kind === "say" && answer && (answer.includes(lastSay.text.slice(0, 80)) || lastSay.text.includes(answer.slice(0, 80)))) steps.pop();
2916
+ const patch = kept?.patch ?? readFile(id, "changes.patch");
2917
+ const patchLines = patch.split("\n");
2918
+ const u = m.usage;
2919
+ return {
2920
+ id,
2921
+ state: m.state,
2922
+ task: (m.reviewOf ? `Review of ${m.reviewOf.replace(/\b([0-9a-f]{9})[0-9a-f]{31}\b/g, "$1")}` : m.task).slice(0, TASK_MAX),
2923
+ steps,
2924
+ answer,
2925
+ changes: m.changes ?? [],
2926
+ patch: patch ? `${patchLines.slice(0, PATCH_LINES).join("\n")}${patchLines.length > PATCH_LINES ? `
2927
+ \u2026 ${patchLines.length - PATCH_LINES} more lines (pitroom show ${id} --patch)` : ""}` : void 0,
2928
+ info: {
2929
+ worker: (m.ran ?? m.worker).backend,
2930
+ // the model has its own line
2931
+ model: m.resolvedModel,
2932
+ mode: m.mode,
2933
+ started: m.startedAt,
2934
+ time: elapsed(m),
2935
+ steps: u?.steps,
2936
+ toolCalls: u?.toolCalls,
2937
+ tokens: u?.total,
2938
+ returnedTokens: m.returnedTokens,
2939
+ cost: u?.cost,
2940
+ saved: m.savedUsd,
2941
+ group: m.group,
2942
+ directory: m.dir
2943
+ },
2944
+ attempts: m.attempts ?? [],
2945
+ warnings: m.warnings ?? [],
2946
+ refs: m.refs ? { valid: m.refs.valid, total: m.refs.total, invalid: m.refs.invalid.map((r) => `${r.ref} (${r.reason})`) } : void 0,
2947
+ verify: m.verifyResult && m.verify ? { command: m.verify, ok: m.verifyResult.ok, tail: m.verifyResult.tail } : void 0,
2948
+ error: m.error,
2949
+ report: isActive(m.state) ? progress(m) : formatReport(m, void 0, 120)
2950
+ };
2351
2951
  }
2352
- var RUN_ID = /\b\d{8}-\d{6}-[0-9a-f]{4}\b/g;
2353
- var PITROOM_CALL = /(^|[\s;&|(`])(\S*\/)?pitroom(\.mjs)?(\s|$)/;
2354
- var ICON2 = { done: "\u2714", failed: "\u2718", timeout: "\u23F1", stopped: "\u25A0" };
2355
- function kind(m) {
2356
- if (m.reviewOf) return m.reviewKind === "range" ? "branch review" : m.reviewKind === "fix" ? "re-review" : "review";
2357
- return m.mode === "read" ? "research" : m.mode === "isolate" ? "change (isolated copy)" : "change";
2952
+ var HEADERS = {
2953
+ "cache-control": "no-store",
2954
+ "x-content-type-options": "nosniff",
2955
+ "referrer-policy": "no-referrer",
2956
+ // Own files only, plus the hash of the one inline theme script; styles may be inline (the components position popups with them).
2957
+ "content-security-policy": `default-src 'none'; script-src 'self' 'sha256-${crypto5.createHash("sha256").update(THEME_SCRIPT).digest("base64")}'; style-src 'self' 'unsafe-inline'; img-src data:; connect-src 'self'; base-uri 'none'; form-action 'none'`
2958
+ };
2959
+ var ASSETS = { "/assets/app.js": "text/javascript; charset=utf-8", "/assets/app.css": "text/css; charset=utf-8" };
2960
+ var assetsDir = () => fileURLToPath2(new URL("./ui/", import.meta.url));
2961
+ var assetCache = /* @__PURE__ */ new Map();
2962
+ function readAsset(route) {
2963
+ try {
2964
+ const file = path15.join(assetsDir(), path15.basename(route));
2965
+ const { mtimeMs } = fs18.statSync(file);
2966
+ const hit = assetCache.get(route);
2967
+ if (hit && hit.mtimeMs === mtimeMs) return hit.data;
2968
+ const data = fs18.readFileSync(file);
2969
+ assetCache.set(route, { mtimeMs, data });
2970
+ return data;
2971
+ } catch {
2972
+ return void 0;
2973
+ }
2358
2974
  }
2359
- var SUBJECT_MAX = 48;
2360
- function short(text) {
2361
- const s = text.replace(/`/g, "").replace(/^#+\s*/, "").replace(/\s+/g, " ").trim();
2362
- if (s.length <= SUBJECT_MAX) return s;
2363
- const cut = s.slice(0, SUBJECT_MAX);
2364
- const space = cut.lastIndexOf(" ");
2365
- return `${(space > SUBJECT_MAX / 2 ? cut.slice(0, space) : cut).replace(/[\s,.;:·-]+$/, "")}\u2026`;
2975
+ function handler(touch) {
2976
+ const send = (res, code, type, body) => {
2977
+ res.writeHead(code, { ...HEADERS, "content-type": type });
2978
+ res.end(body);
2979
+ };
2980
+ const json = (res, code, value) => send(res, code, "application/json; charset=utf-8", JSON.stringify(value));
2981
+ return (req, res) => {
2982
+ touch();
2983
+ if (!LOCAL_HOST.test(req.headers.host ?? "")) return json(res, 403, { error: "forbidden host" });
2984
+ if (req.method !== "GET" && req.method !== "HEAD") return json(res, 405, { error: "read-only" });
2985
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
2986
+ if (url.pathname === "/") return send(res, 200, "text/html; charset=utf-8", readAsset("/assets/app.js") ? PAGE : MISSING);
2987
+ const type = ASSETS[url.pathname];
2988
+ if (type) {
2989
+ const data = readAsset(url.pathname);
2990
+ if (!data) return json(res, 404, { error: "dashboard files missing" });
2991
+ res.writeHead(200, { ...HEADERS, "content-type": type });
2992
+ return void res.end(req.method === "HEAD" ? void 0 : data);
2993
+ }
2994
+ if (url.pathname === "/api/state") {
2995
+ const group = url.searchParams.get("group") ?? void 0;
2996
+ const limit = Number(url.searchParams.get("limit") ?? 40);
2997
+ return json(res, 200, dashState({ group: group && group.length <= 100 ? group : void 0, limit: Number.isFinite(limit) ? limit : 40 }));
2998
+ }
2999
+ if (url.pathname === "/api/history") {
3000
+ const q = url.searchParams;
3001
+ const days = Number(q.get("days") ?? 0);
3002
+ return json(res, 200, listHistory({
3003
+ text: (q.get("q") ?? "").slice(0, 200),
3004
+ model: q.get("model") || void 0,
3005
+ backend: q.get("backend") || void 0,
3006
+ state: q.get("state") || void 0,
3007
+ group: q.get("group") || void 0,
3008
+ sinceMs: days > 0 ? Date.now() - days * 864e5 : void 0,
3009
+ beforeId: RUN_ID2.test(q.get("before") ?? "") ? q.get("before") : void 0,
3010
+ limit: Number(q.get("limit") ?? 30) || 30
3011
+ }));
3012
+ }
3013
+ if (url.pathname === "/api/stats") {
3014
+ const days = Number(url.searchParams.get("days") ?? 30);
3015
+ return json(res, 200, { ...historyStats(days > 0 ? Date.now() - days * 864e5 : void 0), price: primaryPrice().name });
3016
+ }
3017
+ const m = /^\/api\/run\/([^/]+)$/.exec(url.pathname);
3018
+ if (m) {
3019
+ const d = runDetail(m[1]);
3020
+ return d ? json(res, 200, d) : json(res, 404, { error: "no such run" });
3021
+ }
3022
+ return json(res, 404, { error: "not found" });
3023
+ };
2366
3024
  }
2367
- var MODEL_MAX = 24;
2368
- function workerName(t) {
2369
- if (!t.model) return t.backend;
2370
- const model = (t.model.split("/").pop() ?? t.model).replace(/-(contributor-)?free$/, "").replace(/-\d+\.\d+$/, "");
2371
- return `${t.backend} (${model.length > MODEL_MAX ? `${model.slice(0, MODEL_MAX - 1)}\u2026` : model})`;
3025
+ function startDash(opts) {
3026
+ let lastRequest = Date.now();
3027
+ const server = http.createServer(handler(() => lastRequest = Date.now()));
3028
+ return new Promise((resolve2, reject) => {
3029
+ server.once("error", reject);
3030
+ server.listen(opts.port, "127.0.0.1", () => {
3031
+ const port = server.address().port;
3032
+ let done;
3033
+ const closed = new Promise((r) => done = r);
3034
+ let timer;
3035
+ const close = () => {
3036
+ if (timer) clearInterval(timer);
3037
+ server.closeAllConnections?.();
3038
+ return new Promise((r) => server.close(() => (done(), r())));
3039
+ };
3040
+ if (opts.idleMs && opts.idleMs > 0) {
3041
+ timer = setInterval(() => Date.now() - lastRequest > opts.idleMs && void close(), Math.min(6e4, opts.idleMs));
3042
+ timer.unref();
3043
+ }
3044
+ resolve2({ port, url: `http://127.0.0.1:${port}/`, closed, close });
3045
+ });
3046
+ });
2372
3047
  }
2373
- function what(m) {
2374
- if (m.plan) return short(`Task ${m.plan.step}: ${m.plan.title}`);
2375
- if (m.reviewOf) return `of ${m.reviewOf}`;
2376
- const line = m.task.split("\n").find((l) => l.trim()) ?? "";
2377
- const brief2 = /^You are implementing Task (\d+)\b.*?\bplan\s+(\S+)/i.exec(line);
2378
- if (brief2) return short(`Task ${brief2[1]} \xB7 ${brief2[2].split("/").pop().replace(/\.md\W*$/, "")}`);
2379
- return short(line);
3048
+ var registryFile = () => path15.join(home(), "dash.json");
3049
+ async function ping(port) {
3050
+ try {
3051
+ const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
3052
+ return r.ok;
3053
+ } catch {
3054
+ return false;
3055
+ }
2380
3056
  }
2381
- function duration2(m) {
2382
- const s = Math.round((Date.parse(m.endedAt ?? "") - Date.parse(m.startedAt)) / 1e3);
2383
- if (!Number.isFinite(s) || s < 0) return "";
2384
- return s >= 60 ? `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s` : `${s}s`;
3057
+ async function runningDash() {
3058
+ try {
3059
+ const r = JSON.parse(fs18.readFileSync(registryFile(), "utf8"));
3060
+ if (isAlive(r.pid) && await ping(r.port)) return { ...r, url: `http://127.0.0.1:${r.port}/` };
3061
+ } catch {
3062
+ }
3063
+ fs18.rmSync(registryFile(), { force: true });
3064
+ return void 0;
2385
3065
  }
2386
- function cardsFor(m) {
2387
- const phases = [
2388
- ["started", () => `\u{1F3C1} Pitroom \u25B6 ${kind(m)} on ${workerName(m.worker)} \xB7 ${what(m)} (${m.id})`]
2389
- ];
2390
- if (!isActive(m.state)) {
2391
- phases.push([
2392
- "ended",
2393
- () => {
2394
- const v = m.verdict;
2395
- const bits = [
2396
- `\u{1F3C1} Pitroom ${ICON2[m.state] ?? "\u2022"} ${kind(m)} ${m.state} on ${workerName(m.ran ?? m.worker)}`,
2397
- what(m),
2398
- duration2(m),
2399
- v && `SPEC ${v.spec.toUpperCase()} \xB7 QUALITY ${v.quality.toUpperCase()}`,
2400
- m.changes?.length ? `${m.changes.length} file${m.changes.length === 1 ? "" : "s"} changed` : "",
2401
- m.savedUsd ? `~${usd(m.savedUsd)} saved` : ""
2402
- ];
2403
- return `${bits.filter(Boolean).join(" \xB7 ")} (${m.id})`;
2404
- }
2405
- ]);
3066
+ function openBrowser(url) {
3067
+ const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
3068
+ try {
3069
+ spawn(cmd, args, { stdio: "ignore", detached: true }).on("error", () => void 0).unref();
3070
+ } catch {
2406
3071
  }
2407
- if (m.applied) {
2408
- phases.push(["applied", () => `\u{1F3C1} Pitroom \u2935 applied ${m.changes?.length ?? 0} file(s) from ${m.id} to your tree`]);
3072
+ }
3073
+ async function dashCommand(o) {
3074
+ const existing = await runningDash();
3075
+ if (o.stop) {
3076
+ if (!existing) throw new UserError("pitroom dash is not running");
3077
+ process.kill(existing.pid, "SIGTERM");
3078
+ fs18.rmSync(registryFile(), { force: true });
3079
+ o.log(`stopped pitroom dash (${existing.url})`);
3080
+ return 0;
2409
3081
  }
2410
- const out = [];
2411
- for (const [phase, text] of phases) {
2412
- const marker = runFile(m.id, `card-${phase}`);
2413
- if (fs16.existsSync(marker)) continue;
2414
- if (!(phase === "started" && !isActive(m.state))) out.push(text());
2415
- fs16.writeFileSync(marker, "");
3082
+ if (existing && !o.serve) {
3083
+ o.log(existing.url);
3084
+ if (o.open) openBrowser(existing.url);
3085
+ return 0;
2416
3086
  }
2417
- return out;
2418
- }
2419
- function hookCards(input) {
2420
- const event = JSON.parse(input);
2421
- const command = event.tool_input?.command ?? "";
2422
- if (event.tool_name !== "Bash" || !PITROOM_CALL.test(command)) return "";
2423
- const response = event.tool_response;
2424
- const output = typeof response === "string" ? response : [response?.stdout, response?.stderr].filter(Boolean).join("\n");
2425
- const ids = [...new Set(`${command}
2426
- ${output}`.match(RUN_ID) ?? [])].filter((id) => fs16.existsSync(`${runsDir()}/${id}`));
2427
- const since = Date.now() - RECENT_HOURS * 36e5;
2428
- for (const id of listRunIds().slice(-RECENT)) {
2429
- if (ids.includes(id) || fs16.existsSync(runFile(id, "card-ended"))) continue;
2430
- try {
2431
- const m = readMeta(id);
2432
- if (!isActive(m.state) && Date.parse(m.endedAt ?? m.startedAt) > since) ids.push(id);
2433
- } catch {
3087
+ if (o.detach) {
3088
+ const child = spawn(process.execPath, [process.argv[1], "dash", "--serve", ...o.port !== void 0 ? ["--port", String(o.port)] : [], "--idle", "4h"], {
3089
+ detached: true,
3090
+ stdio: "ignore",
3091
+ env: process.env
3092
+ });
3093
+ child.on("error", () => void 0);
3094
+ child.unref();
3095
+ for (let i = 0; i < 60; i++) {
3096
+ await new Promise((r2) => setTimeout(r2, 100));
3097
+ const r = await runningDash();
3098
+ if (r && r.pid === child.pid) {
3099
+ o.log(r.url);
3100
+ if (o.open) openBrowser(r.url);
3101
+ return 0;
3102
+ }
2434
3103
  }
3104
+ throw new UserError("pitroom dash did not start (is the port taken? try --port 0)");
2435
3105
  }
2436
- const cards = [];
2437
- for (const id of ids.slice(0, MAX_CARDS)) {
2438
- try {
2439
- cards.push(...cardsFor(readMeta(id)));
2440
- } catch {
2441
- }
3106
+ importRuns();
3107
+ const explicit = o.port !== void 0;
3108
+ let dash;
3109
+ try {
3110
+ dash = await startDash({ port: o.port ?? DEFAULT_PORT, idleMs: o.idleMs });
3111
+ } catch (e) {
3112
+ if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
3113
+ dash = await startDash({ port: 0, idleMs: o.idleMs });
2442
3114
  }
2443
- return cards.join("\n");
3115
+ fs18.mkdirSync(home(), { recursive: true });
3116
+ fs18.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
3117
+ const stop = () => void dash.close();
3118
+ process.once("SIGINT", stop);
3119
+ process.once("SIGTERM", stop);
3120
+ if (!o.serve) o.log(`${dash.url}
3121
+ (read-only, this machine only; Ctrl-C stops it)`);
3122
+ if (o.open) openBrowser(dash.url);
3123
+ await dash.closed;
3124
+ try {
3125
+ if (JSON.parse(fs18.readFileSync(registryFile(), "utf8")).pid === process.pid) fs18.rmSync(registryFile(), { force: true });
3126
+ } catch {
3127
+ }
3128
+ return 0;
2444
3129
  }
2445
3130
 
2446
3131
  // src/core/run.ts
2447
- import { spawn as spawn2, spawnSync as spawnSync5 } from "node:child_process";
2448
- import fs21 from "node:fs";
2449
- import path17 from "node:path";
3132
+ import { spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
3133
+ import fs23 from "node:fs";
3134
+ import path19 from "node:path";
2450
3135
 
2451
3136
  // src/core/chain.ts
2452
3137
  function resolveChain(flags = {}) {
@@ -2497,12 +3182,12 @@ function resolveChain(flags = {}) {
2497
3182
  }
2498
3183
 
2499
3184
  // src/core/process.ts
2500
- import { spawn } from "node:child_process";
2501
- import fs18 from "node:fs";
3185
+ import { spawn as spawn2 } from "node:child_process";
3186
+ import fs20 from "node:fs";
2502
3187
 
2503
3188
  // src/vcs/guard.ts
2504
- import fs17 from "node:fs";
2505
- import path14 from "node:path";
3189
+ import fs19 from "node:fs";
3190
+ import path16 from "node:path";
2506
3191
  var VERSION = 1;
2507
3192
  var SHIM = `#!/bin/sh
2508
3193
  # pitroom git guard v${VERSION}. Blocks git commands that change history, refs, the
@@ -2577,15 +3262,15 @@ esac
2577
3262
  exec "$real" "$@"
2578
3263
  `;
2579
3264
  function shimDir() {
2580
- return path14.join(home(), "shim", `v${VERSION}`);
3265
+ return path16.join(home(), "shim", `v${VERSION}`);
2581
3266
  }
2582
3267
  function findRealGit(skip) {
2583
- for (const dir of (process.env.PATH ?? "").split(path14.delimiter).filter(Boolean)) {
2584
- if (path14.resolve(dir) === skip) continue;
2585
- const p = path14.join(dir, "git");
3268
+ for (const dir of (process.env.PATH ?? "").split(path16.delimiter).filter(Boolean)) {
3269
+ if (path16.resolve(dir) === skip) continue;
3270
+ const p = path16.join(dir, "git");
2586
3271
  try {
2587
- fs17.accessSync(p, fs17.constants.X_OK);
2588
- if (fs17.statSync(p).isFile()) return p;
3272
+ fs19.accessSync(p, fs19.constants.X_OK);
3273
+ if (fs19.statSync(p).isFile()) return p;
2589
3274
  } catch {
2590
3275
  }
2591
3276
  }
@@ -2598,13 +3283,13 @@ var REF_HOOK = `#!/bin/sh
2598
3283
  echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
2599
3284
  exit 1
2600
3285
  `;
2601
- var hooksDir = () => path14.join(home(), "git-hooks", `v${HOOK_VERSION}`);
3286
+ var hooksDir = () => path16.join(home(), "git-hooks", `v${HOOK_VERSION}`);
2602
3287
  function writeIfChanged(file, content) {
2603
- if (fs17.existsSync(file) && fs17.readFileSync(file, "utf8") === content) return;
2604
- fs17.mkdirSync(path14.dirname(file), { recursive: true });
3288
+ if (fs19.existsSync(file) && fs19.readFileSync(file, "utf8") === content) return;
3289
+ fs19.mkdirSync(path16.dirname(file), { recursive: true });
2605
3290
  const tmp = `${file}.${process.pid}.tmp`;
2606
- fs17.writeFileSync(tmp, content, { mode: 493 });
2607
- fs17.renameSync(tmp, file);
3291
+ fs19.writeFileSync(tmp, content, { mode: 493 });
3292
+ fs19.renameSync(tmp, file);
2608
3293
  }
2609
3294
  function withGitConfig(env, entries) {
2610
3295
  const out = { ...env };
@@ -2626,7 +3311,7 @@ function guardEnv(env) {
2626
3311
  PAGER: "cat"
2627
3312
  };
2628
3313
  if (process.platform === "win32") return { ...env, ...quiet };
2629
- writeIfChanged(path14.join(hooksDir(), "reference-transaction"), REF_HOOK);
3314
+ writeIfChanged(path16.join(hooksDir(), "reference-transaction"), REF_HOOK);
2630
3315
  env = withGitConfig(env, [
2631
3316
  ["core.hooksPath", hooksDir()],
2632
3317
  ["url.pitroom-push-blocked://.pushInsteadOf", ""]
@@ -2634,16 +3319,16 @@ function guardEnv(env) {
2634
3319
  const dir = shimDir();
2635
3320
  const real = findRealGit(dir);
2636
3321
  if (!real) return { ...env, ...quiet };
2637
- writeIfChanged(path14.join(dir, "git"), SHIM);
2638
- return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${path14.delimiter}${env.PATH ?? ""}` };
3322
+ writeIfChanged(path16.join(dir, "git"), SHIM);
3323
+ return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${path16.delimiter}${env.PATH ?? ""}` };
2639
3324
  }
2640
3325
 
2641
3326
  // src/core/process.ts
2642
3327
  async function spawnWorker(inv, opts) {
2643
- const out = fs18.openSync(opts.stdoutFile, "w");
2644
- const err = fs18.openSync(opts.stderrFile, "w");
3328
+ const out = fs20.openSync(opts.stdoutFile, "w");
3329
+ const err = fs20.openSync(opts.stderrFile, "w");
2645
3330
  const res = { code: null, timedOut: false, stopped: false };
2646
- const child = spawn(inv.command, inv.args, {
3331
+ const child = spawn2(inv.command, inv.args, {
2647
3332
  cwd: opts.cwd,
2648
3333
  stdio: ["ignore", out, err],
2649
3334
  // OpenCode v2 takes its workspace from $PWD, not the process cwd: without this a worker
@@ -2673,8 +3358,8 @@ async function spawnWorker(inv, opts) {
2673
3358
  if (killer) clearTimeout(killer);
2674
3359
  process.off("SIGINT", stop);
2675
3360
  process.off("SIGTERM", stop);
2676
- fs18.closeSync(out);
2677
- fs18.closeSync(err);
3361
+ fs20.closeSync(out);
3362
+ fs20.closeSync(err);
2678
3363
  return res;
2679
3364
  }
2680
3365
 
@@ -2699,11 +3384,11 @@ function parseVerdict(text) {
2699
3384
  }
2700
3385
 
2701
3386
  // src/core/slots.ts
2702
- import crypto5 from "node:crypto";
2703
- import fs19 from "node:fs";
2704
- import path15 from "node:path";
2705
- var slotsDir = () => path15.join(home(), "slots");
2706
- var locksDir = () => path15.join(home(), "locks");
3387
+ import crypto6 from "node:crypto";
3388
+ import fs21 from "node:fs";
3389
+ import path17 from "node:path";
3390
+ var slotsDir = () => path17.join(home(), "slots");
3391
+ var locksDir = () => path17.join(home(), "locks");
2707
3392
  var STARTUP_GRACE_MS = 3e4;
2708
3393
  function holderActive(runId, self) {
2709
3394
  if (!runId || runId === self) return false;
@@ -2717,23 +3402,23 @@ function holderActive(runId, self) {
2717
3402
  }
2718
3403
  }
2719
3404
  function tryClaim(file, runId) {
2720
- fs19.mkdirSync(path15.dirname(file), { recursive: true });
3405
+ fs21.mkdirSync(path17.dirname(file), { recursive: true });
2721
3406
  try {
2722
- fs19.writeFileSync(file, runId, { flag: "wx" });
3407
+ fs21.writeFileSync(file, runId, { flag: "wx" });
2723
3408
  return true;
2724
3409
  } catch (e) {
2725
3410
  if (e.code !== "EEXIST") throw e;
2726
3411
  }
2727
3412
  let owner = "";
2728
3413
  try {
2729
- owner = fs19.readFileSync(file, "utf8").trim();
3414
+ owner = fs21.readFileSync(file, "utf8").trim();
2730
3415
  } catch {
2731
3416
  }
2732
3417
  if (owner === runId) return true;
2733
3418
  if (holderActive(owner, runId)) return false;
2734
- fs19.rmSync(file, { force: true });
3419
+ fs21.rmSync(file, { force: true });
2735
3420
  try {
2736
- fs19.writeFileSync(file, runId, { flag: "wx" });
3421
+ fs21.writeFileSync(file, runId, { flag: "wx" });
2737
3422
  return true;
2738
3423
  } catch {
2739
3424
  return false;
@@ -2742,25 +3427,25 @@ function tryClaim(file, runId) {
2742
3427
  function releaseIfOwner(file, runId) {
2743
3428
  if (!file) return;
2744
3429
  try {
2745
- if (fs19.readFileSync(file, "utf8").trim() === runId) fs19.rmSync(file, { force: true });
3430
+ if (fs21.readFileSync(file, "utf8").trim() === runId) fs21.rmSync(file, { force: true });
2746
3431
  } catch {
2747
3432
  }
2748
3433
  }
2749
3434
  function tryAcquireSlot(runId, maxParallel) {
2750
3435
  for (let n = 0; n < Math.max(1, maxParallel); n++) {
2751
- const file = path15.join(slotsDir(), `slot-${n}`);
3436
+ const file = path17.join(slotsDir(), `slot-${n}`);
2752
3437
  if (tryClaim(file, runId)) return file;
2753
3438
  }
2754
3439
  return void 0;
2755
3440
  }
2756
3441
  var releaseSlot = (file, runId) => releaseIfOwner(file, runId);
2757
- var lockFile = (repoRoot2) => path15.join(locksDir(), `write-${crypto5.createHash("sha1").update(path15.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
3442
+ var lockFile = (repoRoot2) => path17.join(locksDir(), `write-${crypto6.createHash("sha1").update(path17.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
2758
3443
  function acquireWriteLock(repoRoot2, runId) {
2759
3444
  const file = lockFile(repoRoot2);
2760
3445
  if (tryClaim(file, runId)) return;
2761
3446
  let owner = "";
2762
3447
  try {
2763
- owner = fs19.readFileSync(file, "utf8").trim();
3448
+ owner = fs21.readFileSync(file, "utf8").trim();
2764
3449
  } catch {
2765
3450
  }
2766
3451
  throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
@@ -2807,8 +3492,8 @@ ${task}`;
2807
3492
  }
2808
3493
 
2809
3494
  // src/core/refs.ts
2810
- import fs20 from "node:fs";
2811
- import path16 from "node:path";
3495
+ import fs22 from "node:fs";
3496
+ import path18 from "node:path";
2812
3497
  var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
2813
3498
  var REF = new RegExp(
2814
3499
  String.raw`(?<![\w/:.-])(\.{0,2}/?(?:[\w@.+-]+/)*(?:[\w@+-][\w@.+-]*\.[A-Za-z][A-Za-z0-9]{0,7}|${EXTENSIONLESS})):(\d+)(?:[-–](\d+))?`,
@@ -2854,13 +3539,13 @@ function extractRefs(answer) {
2854
3539
  return [...seen.values()];
2855
3540
  }
2856
3541
  function verifyRefs(refs, dirs) {
2857
- const roots = [...new Set(dirs.filter(Boolean).map((d) => path16.resolve(d)))];
3542
+ const roots = [...new Set(dirs.filter(Boolean).map((d) => path18.resolve(d)))];
2858
3543
  const invalid = [];
2859
3544
  const lineCache = /* @__PURE__ */ new Map();
2860
3545
  const load = (file) => {
2861
3546
  if (!lineCache.has(file)) {
2862
- const size = fs20.statSync(file).size;
2863
- const lines = size > MAX_BYTES ? null : fs20.readFileSync(file, "utf8").split("\n");
3547
+ const size = fs22.statSync(file).size;
3548
+ const lines = size > MAX_BYTES ? null : fs22.readFileSync(file, "utf8").split("\n");
2864
3549
  if (lines && lines[lines.length - 1] === "") lines.pop();
2865
3550
  lineCache.set(file, lines);
2866
3551
  }
@@ -2869,7 +3554,7 @@ function verifyRefs(refs, dirs) {
2869
3554
  for (const ref of refs) {
2870
3555
  const file = resolve(ref.file, roots);
2871
3556
  if (!file) {
2872
- invalid.push({ ref: ref.text, reason: path16.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
3557
+ invalid.push({ ref: ref.text, reason: path18.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
2873
3558
  continue;
2874
3559
  }
2875
3560
  const lines = load(file);
@@ -2896,10 +3581,10 @@ function enclosedBy(lines, ref) {
2896
3581
  return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
2897
3582
  }
2898
3583
  function resolve(ref, roots) {
2899
- const candidates = path16.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path16.join(r, ref));
3584
+ const candidates = path18.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path18.join(r, ref));
2900
3585
  return candidates.find((c) => {
2901
3586
  try {
2902
- return fs20.statSync(c).isFile() && inside(c, roots);
3587
+ return fs22.statSync(c).isFile() && inside(c, roots);
2903
3588
  } catch {
2904
3589
  return false;
2905
3590
  }
@@ -2907,14 +3592,14 @@ function resolve(ref, roots) {
2907
3592
  }
2908
3593
  function inside(file, roots) {
2909
3594
  return roots.some((r) => {
2910
- const rel = path16.relative(r, path16.resolve(file));
2911
- return rel !== "" && !rel.startsWith("..") && !path16.isAbsolute(rel);
3595
+ const rel = path18.relative(r, path18.resolve(file));
3596
+ return rel !== "" && !rel.startsWith("..") && !path18.isAbsolute(rel);
2912
3597
  });
2913
3598
  }
2914
3599
  var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2915
3600
 
2916
3601
  // src/core/run.ts
2917
- var VERSION2 = true ? "0.6.5" : "0.0.0-dev";
3602
+ var VERSION2 = true ? "0.6.7" : "0.0.0-dev";
2918
3603
  function planWork(o) {
2919
3604
  if (!o.plan) return { task: o.task.trim(), tier: o.tier };
2920
3605
  if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
@@ -2946,7 +3631,7 @@ function prepareRun(o) {
2946
3631
  parent = readMeta(o.continueFrom);
2947
3632
  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");
2948
3633
  if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
2949
- if (parent.mode === "isolate" && (!parent.worktree || !fs21.existsSync(parent.worktree))) {
3634
+ if (parent.mode === "isolate" && (!parent.worktree || !fs23.existsSync(parent.worktree))) {
2950
3635
  throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
2951
3636
  }
2952
3637
  const ran = parent.ran ?? parent.worker;
@@ -2965,15 +3650,15 @@ function prepareRun(o) {
2965
3650
  throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
2966
3651
  }
2967
3652
  const mode = parent?.mode ?? o.mode;
2968
- const dir = path17.resolve(parent?.dir ?? o.dir);
2969
- if (!fs21.existsSync(dir) || !fs21.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
3653
+ const dir = path19.resolve(parent?.dir ?? o.dir);
3654
+ if (!fs23.existsSync(dir) || !fs23.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
2970
3655
  const root = repoRoot(dir);
2971
3656
  if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
2972
3657
  if (mode === "write" && !root && !o.allowNonGit) {
2973
3658
  throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
2974
3659
  }
2975
- const files = o.files.map((f) => path17.resolve(f));
2976
- for (const f of files) if (!fs21.existsSync(f)) throw new UserError(`file not found: ${f}`);
3660
+ const files = o.files.map((f) => path19.resolve(f));
3661
+ for (const f of files) if (!fs23.existsSync(f)) throw new UserError(`file not found: ${f}`);
2977
3662
  const meta = {
2978
3663
  id: newRunId(),
2979
3664
  version: VERSION2,
@@ -3008,13 +3693,13 @@ function prepareRun(o) {
3008
3693
  try {
3009
3694
  acquireWriteLock(root, meta.id);
3010
3695
  } catch (e) {
3011
- fs21.rmSync(runDir(meta.id), { recursive: true, force: true });
3696
+ fs23.rmSync(runDir(meta.id), { recursive: true, force: true });
3012
3697
  throw e;
3013
3698
  }
3014
3699
  }
3015
- fs21.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
3700
+ fs23.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
3016
3701
  `);
3017
- if (work.brief) fs21.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
3702
+ if (work.brief) fs23.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
3018
3703
  return meta;
3019
3704
  }
3020
3705
  var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -3042,7 +3727,7 @@ async function waitForSlot(meta) {
3042
3727
  function startInBackground(meta) {
3043
3728
  const script = process.argv[1];
3044
3729
  if (!script) throw new Error("cannot locate the pitroom executable");
3045
- const child = spawn2(process.execPath, [script, "__exec", meta.id], {
3730
+ const child = spawn3(process.execPath, [script, "__exec", meta.id], {
3046
3731
  detached: true,
3047
3732
  stdio: "ignore",
3048
3733
  cwd: meta.dir,
@@ -3087,7 +3772,7 @@ async function execute(meta) {
3087
3772
  (meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
3088
3773
  for (const f of ["events.jsonl", "stderr.log"]) {
3089
3774
  const from = runFile(meta.id, f);
3090
- if (fs21.existsSync(from)) fs21.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
3775
+ if (fs23.existsSync(from)) fs23.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
3091
3776
  }
3092
3777
  writeMeta(meta);
3093
3778
  }
@@ -3107,9 +3792,9 @@ function prepareTree(meta) {
3107
3792
  if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
3108
3793
  if (root && meta.mode === "isolate" && !meta.worktree) {
3109
3794
  meta.baseTree = snapshotTree(root);
3110
- meta.worktree = path17.join(worktreesDir(), meta.id);
3795
+ meta.worktree = path19.join(worktreesDir(), meta.id);
3111
3796
  createIsolatedCopy(root, meta.baseTree, meta.worktree);
3112
- meta.cwd = path17.join(meta.worktree, path17.relative(root, meta.dir));
3797
+ meta.cwd = path19.join(meta.worktree, path19.relative(root, meta.dir));
3113
3798
  const linked = linkIntoWorktree(root, meta.worktree, meta.link);
3114
3799
  if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
3115
3800
  }
@@ -3132,7 +3817,7 @@ function attempt(meta, backend, target) {
3132
3817
  timeoutSec: meta.timeoutSec
3133
3818
  });
3134
3819
  }
3135
- var read = (f) => fs21.existsSync(f) ? fs21.readFileSync(f, "utf8") : "";
3820
+ var read = (f) => fs23.existsSync(f) ? fs23.readFileSync(f, "utf8") : "";
3136
3821
  function retryableFailure(meta, backend, res) {
3137
3822
  const run = backend.parse(read(runFile(meta.id, "events.jsonl")));
3138
3823
  if (run.usage.steps > 0) return void 0;
@@ -3151,12 +3836,12 @@ function finalize(meta, res) {
3151
3836
  const run = backend.parse(read(runFile(meta.id, "events.jsonl")));
3152
3837
  meta.sessionId = run.sessionId ?? meta.sessionId;
3153
3838
  meta.usage = run.usage;
3154
- fs21.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
3839
+ fs23.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
3155
3840
  `);
3156
3841
  if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run.finalText);
3157
3842
  if (meta.reviewOf) {
3158
3843
  meta.verdict = parseVerdict(run.finalText);
3159
- if (meta.packageFile) fs21.rmSync(meta.packageFile, { force: true });
3844
+ if (meta.packageFile) fs23.rmSync(meta.packageFile, { force: true });
3160
3845
  }
3161
3846
  if (!res.timedOut && !res.stopped && !meta.error) {
3162
3847
  const f = backend.failure(run, read(runFile(meta.id, "stderr.log")), res.code);
@@ -3198,7 +3883,7 @@ function captureChanges(meta) {
3198
3883
  const d = diffTrees(target, meta.baseTree, meta.afterTree);
3199
3884
  meta.changes = d.changes;
3200
3885
  meta.stats = d.stats;
3201
- fs21.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
3886
+ fs23.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
3202
3887
  }
3203
3888
  function runVerify(meta) {
3204
3889
  const r = spawnSync5(meta.verify, {
@@ -3211,7 +3896,7 @@ function runVerify(meta) {
3211
3896
  env: { ...process.env, PWD: meta.cwd, PITROOM_ACTIVE: "1" }
3212
3897
  });
3213
3898
  const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
3214
- fs21.writeFileSync(runFile(meta.id, "verify.log"), output);
3899
+ fs23.writeFileSync(runFile(meta.id, "verify.log"), output);
3215
3900
  return { ok: r.status === 0, code: r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
3216
3901
  }
3217
3902
  function applyRun(meta, allowDelete = false) {
@@ -3256,7 +3941,7 @@ function discardRun(meta) {
3256
3941
  return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
3257
3942
  }
3258
3943
  function cleanupWorktree(meta) {
3259
- if (meta.worktree && fs21.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
3944
+ if (meta.worktree && fs23.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
3260
3945
  }
3261
3946
 
3262
3947
  // src/cli/commands.ts
@@ -3305,10 +3990,10 @@ async function cmdReview(p) {
3305
3990
  writeMeta(meta);
3306
3991
  }
3307
3992
  } catch (e) {
3308
- fs22.rmSync(packageFile, { force: true });
3993
+ fs24.rmSync(packageFile, { force: true });
3309
3994
  throw e;
3310
3995
  }
3311
- fs22.writeFileSync(runFile(meta.id, "package.md"), job.package);
3996
+ fs24.writeFileSync(runFile(meta.id, "package.md"), job.package);
3312
3997
  return launch(p, meta);
3313
3998
  }
3314
3999
  function cmdPlan(p) {
@@ -3328,7 +4013,7 @@ function cmdPlan(p) {
3328
4013
  }
3329
4014
  function cmdCrew(p) {
3330
4015
  const file = flag(p, "task-file");
3331
- const tasks = (file ? fs22.readFileSync(file, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
4016
+ const tasks = (file ? fs24.readFileSync(file, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
3332
4017
  if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
3333
4018
  if (has(p, "write") && tasks.length > 1) {
3334
4019
  throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
@@ -3396,8 +4081,10 @@ async function cmdWatch(p) {
3396
4081
  console.log(group ? `no runs in group "${group}"` : "nothing is running");
3397
4082
  return 0;
3398
4083
  }
4084
+ const brief2 = has(p, "brief");
3399
4085
  const { metas, timedOut } = await watch(group ? () => groupIds(group) : () => initial, {
3400
- json: has(p, "json") || !process.stdout.isTTY,
4086
+ json: !brief2 && (has(p, "json") || !process.stdout.isTTY),
4087
+ brief: brief2,
3401
4088
  intervalMs: parseDuration(flag(p, "interval") ?? "2") * 1e3,
3402
4089
  timeoutMs: flag(p, "timeout") ? parseDuration(flag(p, "timeout")) * 1e3 : 0,
3403
4090
  write: (s) => process.stdout.write(s)
@@ -3405,11 +4092,62 @@ async function cmdWatch(p) {
3405
4092
  if (timedOut) return 75;
3406
4093
  return metas.some((m) => m.state !== "done") ? 1 : 0;
3407
4094
  }
4095
+ async function cmdDash(p) {
4096
+ const port = flag(p, "port");
4097
+ 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)");
4098
+ return dashCommand({
4099
+ port: port === void 0 ? void 0 : Number(port),
4100
+ idleMs: flag(p, "idle") ? parseDuration(flag(p, "idle")) * 1e3 : void 0,
4101
+ detach: has(p, "detach"),
4102
+ stop: has(p, "stop"),
4103
+ open: has(p, "open"),
4104
+ serve: has(p, "serve"),
4105
+ log: (s) => console.log(s)
4106
+ });
4107
+ }
4108
+ var when = (iso) => new Date(iso).toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
4109
+ var secs = (s) => s == null ? "-" : s >= 3600 ? `${Math.floor(s / 3600)}h${String(Math.floor(s % 3600 / 60)).padStart(2, "0")}m` : s >= 60 ? `${Math.floor(s / 60)}m${String(Math.round(s % 60)).padStart(2, "0")}s` : `${Math.round(s)}s`;
4110
+ function cmdHistory(p) {
4111
+ if (!openDb()) throw new UserError("the history needs Node.js 22.13+ (node:sqlite)", 3);
4112
+ const sub = p.positional[0];
4113
+ if (sub === "import") {
4114
+ const r = importRuns();
4115
+ console.log(`imported ${r.imported} run(s); ${r.known} were already in the history (${historyFile()})`);
4116
+ return 0;
4117
+ }
4118
+ importRuns();
4119
+ const since = sinceMs(flag(p, "since"));
4120
+ if (sub === "stats") {
4121
+ const s = historyStats(since);
4122
+ if (has(p, "json")) return console.log(JSON.stringify(s, null, 2)), 0;
4123
+ const t = s.totals;
4124
+ console.log(`${t.runs} runs \xB7 ${t.ok} ok \xB7 ${t.failed} not ok \xB7 ${secs(t.seconds)} of worker time \xB7 ${(t.tokens / 1e6).toFixed(1)}M tokens \xB7 ~${usd(t.saved)} saved`);
4125
+ const rows2 = s.byWorker.map((w) => [`${w.backend}${w.model ? `:${w.model}` : ""}`, String(w.runs), `${Math.round(100 * w.ok / w.runs)}%`, secs(w.avgSeconds), w.avgTokens ? `${Math.round(w.avgTokens / 1e3)}k` : "-", `~${usd(w.saved)}`]);
4126
+ const head2 = ["WORKER", "RUNS", "OK", "AVG TIME", "AVG TOKENS", "SAVED"];
4127
+ const widths2 = head2.map((h, i) => Math.max(h.length, ...rows2.map((r) => r[i].length)));
4128
+ const fmt2 = (r) => r.map((c, i) => i === 0 ? c.padEnd(widths2[i]) : c.padStart(widths2[i])).join(" ");
4129
+ if (rows2.length) console.log(`
4130
+ ${[fmt2(head2), ...rows2.map(fmt2)].join("\n")}`);
4131
+ return 0;
4132
+ }
4133
+ const limit = flag(p, "limit") ? Number(flag(p, "limit")) : 20;
4134
+ const { rows, total } = listHistory({ text: p.positional.join(" "), model: flag(p, "model"), state: flag(p, "state"), group: flag(p, "group"), sinceMs: since, limit });
4135
+ if (has(p, "json")) return console.log(JSON.stringify({ total, rows }, null, 2)), 0;
4136
+ if (!rows.length) return console.log(total ? "nothing on this page" : "no matching runs"), 0;
4137
+ const body = rows.map((r) => [r.id, when(r.startedAt), r.state, `${r.backend}${r.model ? ` (${r.model.split("/").pop()})` : ""}`, secs(r.seconds), r.task.length > 60 ? `${r.task.slice(0, 59)}\u2026` : r.task]);
4138
+ const head = ["RUN", "WHEN", "STATE", "WORKER", "TIME", "TASK"];
4139
+ const widths = head.map((h, i) => Math.max(h.length, ...body.map((r) => r[i].length)));
4140
+ const fmt = (r) => r.map((c, i) => i === r.length - 1 ? c : c.padEnd(widths[i])).join(" ");
4141
+ console.log([fmt(head), ...body.map(fmt)].join("\n"));
4142
+ if (total > rows.length) console.log(`
4143
+ ${rows.length} of ${total} \xB7 --limit N for more \xB7 pitroom show <run> for one`);
4144
+ return 0;
4145
+ }
3408
4146
  function cmdShow(p) {
3409
4147
  const meta = freshMeta(resolveRun(p.positional[0]));
3410
4148
  const dump = (name) => {
3411
- const f = runFile(meta.id, name);
3412
- process.stdout.write(fs22.existsSync(f) ? fs22.readFileSync(f, "utf8") : `(no ${name})
4149
+ const text = readRunFile(meta.id, name) ?? (name === "changes.patch" ? archivedRun(meta.id)?.patch : void 0);
4150
+ process.stdout.write(text ?? `(no ${name}${name === "events.jsonl" ? ": the raw stream was removed, the history keeps the steps" : ""})
3413
4151
  `);
3414
4152
  };
3415
4153
  if (has(p, "patch")) dump("changes.patch");
@@ -3473,7 +4211,7 @@ function sinceMs(s) {
3473
4211
  if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
3474
4212
  return Date.now() - Number(m[1]) * 864e5;
3475
4213
  }
3476
- var readStdin = () => process.stdin.isTTY ? "" : fs22.readFileSync(0, "utf8");
4214
+ var readStdin = () => process.stdin.isTTY ? "" : fs24.readFileSync(0, "utf8");
3477
4215
  function cmdStatusline(p) {
3478
4216
  const input = readStdin();
3479
4217
  const lines = [];
@@ -3542,8 +4280,8 @@ function cmdSavings(p) {
3542
4280
  est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
3543
4281
  const out = flag(p, "card");
3544
4282
  if (out) {
3545
- fs22.writeFileSync(out, card(t, period));
3546
- console.log(`card written to ${path18.resolve(out)}`);
4283
+ fs24.writeFileSync(out, card(t, period));
4284
+ console.log(`card written to ${path20.resolve(out)}`);
3547
4285
  }
3548
4286
  if (has(p, "badge")) console.log(`![pitroom](${badgeUrl(t)})`);
3549
4287
  return 0;
@@ -3563,7 +4301,7 @@ function cmdConfig(p) {
3563
4301
  console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
3564
4302
  return 0;
3565
4303
  }
3566
- console.log(`config file: ${configPath()}${fs22.existsSync(configPath()) ? "" : " (not present)"}`);
4304
+ console.log(`config file: ${configPath()}${fs24.existsSync(configPath()) ? "" : " (not present)"}`);
3567
4305
  for (const [key, s] of Object.entries(eff)) {
3568
4306
  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);
3569
4307
  console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
@@ -3586,9 +4324,10 @@ function cmdClean(p) {
3586
4324
  for (const id of old) {
3587
4325
  const m = readMeta(id);
3588
4326
  if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
3589
- fs22.rmSync(runDir(id), { recursive: true, force: true });
4327
+ recordRun(m);
4328
+ fs24.rmSync(runDir(id), { recursive: true, force: true });
3590
4329
  }
3591
- console.log(`removed ${old.length} run(s); the savings ledger is kept`);
4330
+ console.log(`removed ${old.length} run(s); the history and the savings ledger are kept`);
3592
4331
  return 0;
3593
4332
  }
3594
4333
  var cmdRevert = (p) => (console.log(revertRun(freshMeta(resolveRun(p.positional[0])))), 0);
@@ -3596,9 +4335,9 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
3596
4335
 
3597
4336
  // src/core/doctor.ts
3598
4337
  import { spawnSync as spawnSync7 } from "node:child_process";
3599
- import fs23 from "node:fs";
4338
+ import fs25 from "node:fs";
3600
4339
  import os9 from "node:os";
3601
- import path19 from "node:path";
4340
+ import path21 from "node:path";
3602
4341
  var MARK = { ok: "\u2714", warn: "!", fail: "\u2718" };
3603
4342
  var READ_ONLY_HOW = {
3604
4343
  "permission-rules": "per-run permission rules",
@@ -3612,7 +4351,7 @@ function doctor4(probe) {
3612
4351
  add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
3613
4352
  add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
3614
4353
  const cfg = loadConfig();
3615
- add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs23.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
4354
+ add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs25.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
3616
4355
  for (const w of cfg.warnings) add("warn", w);
3617
4356
  if (process.platform !== "win32") {
3618
4357
  const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
@@ -3690,11 +4429,11 @@ function liveProbe(target) {
3690
4429
  maxBuffer: 64 * 1024 * 1024,
3691
4430
  env: guardEnv({ ...process.env, ...inv.env, PWD: process.cwd(), PITROOM_ACTIVE: "1" })
3692
4431
  });
3693
- const secs = ((Date.now() - t0) / 1e3).toFixed(1);
4432
+ const secs2 = ((Date.now() - t0) / 1e3).toFixed(1);
3694
4433
  const run = backend.parse(r.stdout ?? "");
3695
- if (run.finalText.includes("PONG")) return { level: "ok", message: `live probe (${describeTarget(target)}) answered in ${secs}s` };
4434
+ if (run.finalText.includes("PONG")) return { level: "ok", message: `live probe (${describeTarget(target)}) answered in ${secs2}s` };
3696
4435
  const why = backend.failure(run, r.stderr ?? "", r.status)?.message ?? run.finalText.slice(0, 200);
3697
- return { level: "fail", message: `live probe (${describeTarget(target)}) failed after ${secs}s: ${why}` };
4436
+ return { level: "fail", message: `live probe (${describeTarget(target)}) failed after ${secs2}s: ${why}` };
3698
4437
  }
3699
4438
  function skillChecks() {
3700
4439
  const checks = [];
@@ -3713,13 +4452,13 @@ function skillChecks() {
3713
4452
  else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
3714
4453
  }
3715
4454
  if (viaPlugin) {
3716
- const linked = installedSkills().some((i) => i.base.includes(`${path19.sep}.claude${path19.sep}`) && i.names.length);
4455
+ const linked = installedSkills().some((i) => i.base.includes(`${path21.sep}.claude${path21.sep}`) && i.names.length);
3717
4456
  checks.push(
3718
4457
  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)" }
3719
4458
  );
3720
4459
  }
3721
4460
  const launcher = launcherPath();
3722
- if (fs23.existsSync(launcher)) {
4461
+ if (fs25.existsSync(launcher)) {
3723
4462
  const r = spawnSync7(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
3724
4463
  checks.push(
3725
4464
  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)}` }
@@ -3731,8 +4470,8 @@ function skillChecks() {
3731
4470
  }
3732
4471
  function pluginInstalled() {
3733
4472
  try {
3734
- const f = path19.join(os9.homedir(), ".claude", "plugins", "installed_plugins.json");
3735
- const plugins = JSON.parse(fs23.readFileSync(f, "utf8")).plugins ?? {};
4473
+ const f = path21.join(os9.homedir(), ".claude", "plugins", "installed_plugins.json");
4474
+ const plugins = JSON.parse(fs25.readFileSync(f, "utf8")).plugins ?? {};
3736
4475
  return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
3737
4476
  } catch {
3738
4477
  return false;
@@ -3749,24 +4488,24 @@ function superpowersActive() {
3749
4488
  for (const p of openCodePlugins()) {
3750
4489
  if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
3751
4490
  }
3752
- for (const base2 of [path19.join(os9.homedir(), ".agents", "skills"), path19.join(os9.homedir(), ".claude", "skills")]) {
3753
- const dir = path19.join(base2, "using-superpowers");
3754
- if (fs23.existsSync(path19.join(dir, "SKILL.md"))) found.push(dir);
4491
+ for (const base2 of [path21.join(os9.homedir(), ".agents", "skills"), path21.join(os9.homedir(), ".claude", "skills")]) {
4492
+ const dir = path21.join(base2, "using-superpowers");
4493
+ if (fs25.existsSync(path21.join(dir, "SKILL.md"))) found.push(dir);
3755
4494
  }
3756
4495
  return found;
3757
4496
  }
3758
4497
  function claudePlugins() {
3759
4498
  try {
3760
- const f = path19.join(os9.homedir(), ".claude", "plugins", "installed_plugins.json");
3761
- return Object.keys(JSON.parse(fs23.readFileSync(f, "utf8")).plugins ?? {});
4499
+ const f = path21.join(os9.homedir(), ".claude", "plugins", "installed_plugins.json");
4500
+ return Object.keys(JSON.parse(fs25.readFileSync(f, "utf8")).plugins ?? {});
3762
4501
  } catch {
3763
4502
  return [];
3764
4503
  }
3765
4504
  }
3766
4505
  function claudePluginEnabled(key) {
3767
4506
  try {
3768
- const f = path19.join(os9.homedir(), ".claude", "settings.json");
3769
- return JSON.parse(fs23.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
4507
+ const f = path21.join(os9.homedir(), ".claude", "settings.json");
4508
+ return JSON.parse(fs25.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
3770
4509
  } catch {
3771
4510
  return true;
3772
4511
  }
@@ -3775,7 +4514,7 @@ function codexPlugins() {
3775
4514
  const plugins = /* @__PURE__ */ new Map();
3776
4515
  let text;
3777
4516
  try {
3778
- text = fs23.readFileSync(path19.join(process.env.CODEX_HOME ?? path19.join(os9.homedir(), ".codex"), "config.toml"), "utf8");
4517
+ text = fs25.readFileSync(path21.join(process.env.CODEX_HOME ?? path21.join(os9.homedir(), ".codex"), "config.toml"), "utf8");
3779
4518
  } catch {
3780
4519
  return plugins;
3781
4520
  }
@@ -3793,11 +4532,11 @@ function codexPlugins() {
3793
4532
  return plugins;
3794
4533
  }
3795
4534
  function openCodePlugins() {
3796
- const dir = path19.join(process.env.XDG_CONFIG_HOME ?? path19.join(os9.homedir(), ".config"), "opencode");
4535
+ const dir = path21.join(process.env.XDG_CONFIG_HOME ?? path21.join(os9.homedir(), ".config"), "opencode");
3797
4536
  const plugins = [];
3798
4537
  for (const name of ["opencode.json", "opencode.jsonc"]) {
3799
4538
  try {
3800
- const text = fs23.readFileSync(path19.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
4539
+ const text = fs25.readFileSync(path21.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
3801
4540
  const list2 = JSON.parse(text).plugin;
3802
4541
  if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
3803
4542
  } catch {
@@ -3805,7 +4544,7 @@ function openCodePlugins() {
3805
4544
  }
3806
4545
  for (const folder of ["plugin", "plugins"]) {
3807
4546
  try {
3808
- for (const f of fs23.readdirSync(path19.join(dir, folder))) plugins.push(path19.join(dir, folder, f));
4547
+ for (const f of fs25.readdirSync(path21.join(dir, folder))) plugins.push(path21.join(dir, folder, f));
3809
4548
  } catch {
3810
4549
  }
3811
4550
  }
@@ -3813,6 +4552,7 @@ function openCodePlugins() {
3813
4552
  }
3814
4553
 
3815
4554
  // src/cli.ts
4555
+ useArchive({ onFinished: recordRun, meta: (id) => archivedRun(id)?.meta, id: archivedId });
3816
4556
  var HELP = `pitroom ${VERSION2} \u2014 a free pit crew for your expensive coding agent.
3817
4557
  Delegates bounded tasks to a worker agent CLI (${backendIds().join(", ")}), using the
3818
4558
  worker's own default model, and returns a compact answer, exact changes and a receipt.
@@ -3829,7 +4569,14 @@ Usage
3829
4569
  pitroom status [run | -g NAME] state / live progress (default: latest run)
3830
4570
  pitroom wait [run\u2026 | -g NAME] [--any] [--brief] [--timeout 540]
3831
4571
  block until all (or any) are done, then print reports
3832
- pitroom watch [run\u2026 | -g NAME] [--json] [--interval 2]
4572
+ pitroom watch [run\u2026 | -g NAME] [--json | --brief] [--interval 2]
4573
+ --brief: one card line per start and end (made for Claude Code's Monitor tool)
4574
+ pitroom history [TEXT] [--model M] [--state S] [--since 30d] [--limit N] [--json]
4575
+ every finished run, searchable (SQLite); "history stats" per worker and model,
4576
+ "history import" takes in runs from before the history existed
4577
+ pitroom dash [--detach] [--port N] [--open] [--stop]
4578
+ a live page of the runs on 127.0.0.1 (read-only): open it in any browser
4579
+ or an agent app's browser pane; --detach runs it in the background
3833
4580
  live table (TTY) or one JSON line per change, until done
3834
4581
  pitroom show [run] [--patch|--events|--full|--json]
3835
4582
  pitroom ls [--running] [-g NAME] recent runs
@@ -3892,6 +4639,8 @@ var COMMANDS = {
3892
4639
  status: cmdStatus,
3893
4640
  wait: cmdWait,
3894
4641
  watch: cmdWatch,
4642
+ dash: cmdDash,
4643
+ history: cmdHistory,
3895
4644
  show: cmdShow,
3896
4645
  ls: cmdLs,
3897
4646
  list: cmdLs,