pitroom 0.6.5 → 0.6.6

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,514 @@ 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 stat = (x, value, label) => `<text x="${x}" y="148" class="v">${esc(value)}</text><text x="${x}" y="170" class="l">${esc(label)}</text>`;
1485
+ 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))}">
1486
+ <style>
1487
+ text{font-family:ui-sans-serif,-apple-system,Segoe UI,Helvetica,Arial,sans-serif;fill:#ede9fe}
1488
+ .h{font-size:15px;font-weight:600;fill:#a78bfa;letter-spacing:.08em}
1489
+ .big{font-size:54px;font-weight:800;fill:#fff}
1490
+ .sub{font-size:15px;fill:#c4b5fd}
1491
+ .v{font-size:22px;font-weight:700;fill:#fff}
1492
+ .l{font-size:12px;fill:#a78bfa}
1493
+ .f{font-size:11px;fill:#8b5cf6}
1494
+ </style>
1495
+ <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>
1496
+ <rect width="600" height="220" rx="18" fill="url(#g)"/>
1497
+ <text x="32" y="42" class="h">PITROOM \xB7 ${esc(period.toUpperCase())}</text>
1498
+ <text x="32" y="98" class="big">${esc(usd(t.saved))}</text>
1499
+ <text x="${Math.min(60 + usd(t.saved).length * 30, 330)}" y="98" class="sub">saved on my main coding agent</text>
1500
+ ${stat(32, compact(t.tokens), "tokens offloaded")}
1501
+ ${stat(200, t.ratio ? `${Math.round(t.ratio)}\xD7` : "\u2014", "context compression")}
1502
+ ${stat(380, String(t.runs), "delegated tasks")}
1503
+ <text x="32" y="202" class="f">estimated vs. ${esc(primaryPrice().name)} pricing \xB7 npx pitroom</text>
1504
+ </svg>
1505
+ `;
1506
+ }
1507
+
1508
+ // src/core/ui.ts
1509
+ var WEEK_MS = 7 * 24 * 3600 * 1e3;
1510
+ var RECENT = 40;
1511
+ var RECENT_HOURS = 1;
1512
+ var MAX_CARDS = 5;
1513
+ function activeRuns() {
1514
+ const out = [];
1515
+ for (const id of listRunIds().slice(-RECENT)) {
1516
+ try {
1517
+ const m = readMeta(id);
1518
+ if (isActive(m.state) && (m.state === "queued" || isAlive(m.pid))) out.push(m);
1519
+ } catch {
1520
+ }
1521
+ }
1522
+ return out;
1523
+ }
1524
+ function statusLine() {
1525
+ const running = activeRuns();
1526
+ const saved = totals(readLedger(Date.now() - WEEK_MS)).saved;
1527
+ if (!running.length && !saved) return "";
1528
+ const parts = ["\u{1F3C1} pitroom"];
1529
+ if (running.length) {
1530
+ const who = [...new Set(running.map((m) => (m.ran ?? m.worker).backend))].join(", ");
1531
+ parts.push(`${running.length} running (${who})`);
1532
+ }
1533
+ if (saved) parts.push(`~${usd(saved)} saved this week`);
1534
+ return parts.join(" \xB7 ");
1535
+ }
1536
+ var RUN_ID = /\b\d{8}-\d{6}-[0-9a-f]{4}\b/g;
1537
+ var PITROOM_CALL = /(^|[\s;&|(`])(\S*\/)?pitroom(\.mjs)?(\s|$)/;
1538
+ var ICON = { done: "\u2714", failed: "\u2718", timeout: "\u23F1", stopped: "\u25A0" };
1539
+ function kind(m) {
1540
+ if (m.reviewOf) return m.reviewKind === "range" ? "branch review" : m.reviewKind === "fix" ? "re-review" : "review";
1541
+ return m.mode === "read" ? "research" : m.mode === "isolate" ? "change (isolated copy)" : "change";
1542
+ }
1543
+ var SUBJECT_MAX = 48;
1544
+ function short(text) {
1545
+ const s = text.replace(/`/g, "").replace(/^#+\s*/, "").replace(/\s+/g, " ").trim();
1546
+ if (s.length <= SUBJECT_MAX) return s;
1547
+ const cut = s.slice(0, SUBJECT_MAX);
1548
+ const space = cut.lastIndexOf(" ");
1549
+ return `${(space > SUBJECT_MAX / 2 ? cut.slice(0, space) : cut).replace(/[\s,.;:·-]+$/, "")}\u2026`;
1550
+ }
1551
+ var MODEL_MAX = 24;
1552
+ function workerName(t) {
1553
+ if (!t.model) return t.backend;
1554
+ const model = (t.model.split("/").pop() ?? t.model).replace(/-(contributor-)?free$/, "").replace(/-\d+\.\d+$/, "");
1555
+ return `${t.backend} (${model.length > MODEL_MAX ? `${model.slice(0, MODEL_MAX - 1)}\u2026` : model})`;
1556
+ }
1557
+ function what(m) {
1558
+ if (m.plan) return short(`Task ${m.plan.step}: ${m.plan.title}`);
1559
+ if (m.reviewOf) return `of ${m.reviewOf.replace(/\b([0-9a-f]{9})[0-9a-f]{31}\b/g, "$1")}`;
1560
+ const line = m.task.split("\n").find((l) => l.trim()) ?? "";
1561
+ const brief2 = /^You are implementing Task (\d+)\b.*?\bplan\s+(\S+)/i.exec(line);
1562
+ if (brief2) return short(`Task ${brief2[1]} \xB7 ${brief2[2].split("/").pop().replace(/\.md\W*$/, "")}`);
1563
+ return short(line);
1564
+ }
1565
+ function elapsed(m) {
1566
+ const s = Math.round((Date.parse(m.endedAt ?? "") - Date.parse(m.startedAt)) / 1e3);
1567
+ if (!Number.isFinite(s) || s < 0) return "";
1568
+ return s >= 60 ? `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s` : `${s}s`;
1569
+ }
1570
+ var startedCard = (m) => `\u{1F3C1} Pitroom \u25B6 ${kind(m)} on ${workerName(m.worker)} \xB7 ${what(m)} (${m.id})`;
1571
+ function endedCard(m) {
1572
+ const v = m.verdict;
1573
+ const bits = [
1574
+ `\u{1F3C1} Pitroom ${ICON[m.state] ?? "\u2022"} ${kind(m)} ${m.state} on ${workerName(m.ran ?? m.worker)}`,
1575
+ what(m),
1576
+ elapsed(m),
1577
+ v && `SPEC ${v.spec.toUpperCase()} \xB7 QUALITY ${v.quality.toUpperCase()}`,
1578
+ m.changes?.length ? `${m.changes.length} file${m.changes.length === 1 ? "" : "s"} changed` : "",
1579
+ m.savedUsd ? `~${usd(m.savedUsd)} saved` : ""
1580
+ ];
1581
+ return `${bits.filter(Boolean).join(" \xB7 ")} (${m.id})`;
1582
+ }
1583
+ function cardsFor(m) {
1584
+ const phases = [["started", () => startedCard(m)]];
1585
+ if (!isActive(m.state)) phases.push(["ended", () => endedCard(m)]);
1586
+ if (m.applied) {
1587
+ phases.push(["applied", () => `\u{1F3C1} Pitroom \u2935 applied ${m.changes?.length ?? 0} file(s) from ${m.id} to your tree`]);
1588
+ }
1589
+ const out = [];
1590
+ for (const [phase, text] of phases) {
1591
+ const marker = runFile(m.id, `card-${phase}`);
1592
+ if (fs9.existsSync(marker)) continue;
1593
+ if (!(phase === "started" && !isActive(m.state))) out.push(text());
1594
+ fs9.writeFileSync(marker, "");
1595
+ }
1596
+ return out;
1597
+ }
1598
+ function hookCards(input) {
1599
+ const event = JSON.parse(input);
1600
+ const command = event.tool_input?.command ?? "";
1601
+ if (event.tool_name !== "Bash" || !PITROOM_CALL.test(command)) return "";
1602
+ const response = event.tool_response;
1603
+ const output = typeof response === "string" ? response : [response?.stdout, response?.stderr].filter(Boolean).join("\n");
1604
+ const ids = [...new Set(`${command}
1605
+ ${output}`.match(RUN_ID) ?? [])].filter((id) => fs9.existsSync(`${runsDir()}/${id}`));
1606
+ const since = Date.now() - RECENT_HOURS * 36e5;
1607
+ for (const id of listRunIds().slice(-RECENT)) {
1608
+ if (ids.includes(id) || fs9.existsSync(runFile(id, "card-ended"))) continue;
1609
+ try {
1610
+ const m = readMeta(id);
1611
+ if (!isActive(m.state) && Date.parse(m.endedAt ?? m.startedAt) > since) ids.push(id);
1612
+ } catch {
1613
+ }
1614
+ }
1615
+ const cards = [];
1616
+ for (const id of ids.slice(0, MAX_CARDS)) {
1617
+ try {
1618
+ cards.push(...cardsFor(readMeta(id)));
1619
+ } catch {
1620
+ }
1621
+ }
1622
+ return cards.join("\n");
1623
+ }
1624
+
1625
+ // src/core/history.ts
1626
+ var SCHEMA_VERSION = 1;
1627
+ var PATCH_MAX = 1e6;
1628
+ var ANSWER_MAX = 2e5;
1629
+ var cached2;
1630
+ var historyFile = () => path8.join(home(), "history.db");
1631
+ function openDb() {
1632
+ const file = historyFile();
1633
+ if (cached2?.file === file) return cached2.db;
1634
+ let db;
1635
+ try {
1636
+ const emit = process.emitWarning;
1637
+ process.emitWarning = ((w, ...rest) => /SQLite/i.test(String(w?.message ?? w)) ? void 0 : emit.call(process, w, ...rest));
1638
+ let DatabaseSync;
1639
+ try {
1640
+ ({ DatabaseSync } = createRequire(import.meta.url)("node:sqlite"));
1641
+ } finally {
1642
+ process.emitWarning = emit;
1643
+ }
1644
+ fs10.mkdirSync(path8.dirname(file), { recursive: true });
1645
+ db = new DatabaseSync(file);
1646
+ db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
1647
+ migrate(db);
1648
+ } catch {
1649
+ db = void 0;
1650
+ }
1651
+ cached2 = { file, db };
1652
+ return db;
1653
+ }
1654
+ function migrate(db) {
1655
+ const v = db.prepare("PRAGMA user_version").get()?.user_version ?? 0;
1656
+ if (v >= SCHEMA_VERSION) return;
1657
+ db.exec(`
1658
+ CREATE TABLE IF NOT EXISTS runs (
1659
+ id TEXT PRIMARY KEY, started_at TEXT NOT NULL, ended_at TEXT, state TEXT NOT NULL, mode TEXT, kind TEXT,
1660
+ backend TEXT, model TEXT, task TEXT, grp TEXT, dir TEXT, review_of TEXT, verdict TEXT,
1661
+ seconds INTEGER, steps INTEGER, tool_calls INTEGER, tokens INTEGER, returned_tokens INTEGER,
1662
+ cost REAL, saved REAL, files_changed INTEGER, applied INTEGER, error TEXT,
1663
+ answer TEXT, patch TEXT, meta_json TEXT NOT NULL
1664
+ );
1665
+ CREATE INDEX IF NOT EXISTS runs_started ON runs(started_at);
1666
+ CREATE INDEX IF NOT EXISTS runs_model ON runs(backend, model);
1667
+ CREATE INDEX IF NOT EXISTS runs_grp ON runs(grp);
1668
+ CREATE TABLE IF NOT EXISTS steps (
1669
+ run_id TEXT NOT NULL, n INTEGER NOT NULL, kind TEXT, name TEXT, text TEXT, ok INTEGER, at INTEGER,
1670
+ PRIMARY KEY (run_id, n)
1671
+ ) WITHOUT ROWID;
1672
+ CREATE VIRTUAL TABLE IF NOT EXISTS runs_fts USING fts5(id UNINDEXED, task, answer, steps);
1673
+ PRAGMA user_version = ${SCHEMA_VERSION};
1674
+ `);
1675
+ }
1676
+ function readRunFile(id, name) {
1677
+ const f = runFile(id, name);
1678
+ try {
1679
+ if (fs10.existsSync(f)) return fs10.readFileSync(f, "utf8");
1680
+ if (fs10.existsSync(`${f}.gz`)) return zlib.gunzipSync(fs10.readFileSync(`${f}.gz`)).toString("utf8");
1681
+ } catch {
1682
+ }
1683
+ return void 0;
1684
+ }
1685
+ function gzipFile(f) {
1686
+ if (!fs10.existsSync(f)) return;
1687
+ fs10.writeFileSync(`${f}.gz`, zlib.gzipSync(fs10.readFileSync(f)));
1688
+ fs10.rmSync(f);
1689
+ }
1690
+ function compactRun(meta) {
1691
+ if (!TERMINAL.includes(meta.state)) return;
1692
+ const dir = path8.dirname(runFile(meta.id, "meta.json"));
1693
+ let names;
1694
+ try {
1695
+ names = fs10.readdirSync(dir);
1696
+ } catch {
1697
+ return;
1698
+ }
1699
+ for (const n of names) {
1700
+ if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(path8.join(dir, n));
1701
+ else if (/^stderr(\.attempt-\d+)?\.log$/.test(n)) {
1702
+ if (meta.state === "done" && !/attempt/.test(n)) fs10.rmSync(path8.join(dir, n), { force: true });
1703
+ else gzipFile(path8.join(dir, n));
1704
+ }
1705
+ }
1706
+ }
1707
+ var clip4 = (s, n) => s && s.length > n ? s.slice(0, n) : s;
1708
+ function timelineOf(meta) {
1709
+ const raw = readRunFile(meta.id, "events.jsonl");
1710
+ if (!raw) return [];
1711
+ try {
1712
+ return getBackend((meta.ran ?? meta.worker).backend).parse(raw).timeline ?? [];
1713
+ } catch {
1714
+ return [];
1715
+ }
1716
+ }
1717
+ function recordRun(meta) {
1718
+ if (!TERMINAL.includes(meta.state)) return;
1719
+ const db = openDb();
1720
+ if (!db) return;
1721
+ try {
1722
+ const u = meta.usage;
1723
+ const known = db.prepare("SELECT answer IS NOT NULL AS has FROM runs WHERE id = ?").get(meta.id);
1724
+ const answer = known?.has ? void 0 : clip4(readRunFile(meta.id, "summary.md")?.trim(), ANSWER_MAX);
1725
+ const patch = known?.has ? void 0 : clip4(readRunFile(meta.id, "changes.patch"), PATCH_MAX);
1726
+ const seconds = meta.endedAt ? Math.max(0, Math.round((Date.parse(meta.endedAt) - Date.parse(meta.startedAt)) / 1e3)) : null;
1727
+ const t = meta.ran ?? meta.worker;
1728
+ const fields = {
1729
+ id: meta.id,
1730
+ started_at: meta.startedAt,
1731
+ ended_at: meta.endedAt ?? null,
1732
+ state: meta.state,
1733
+ mode: meta.mode,
1734
+ kind: kind(meta),
1735
+ backend: t.backend,
1736
+ model: meta.resolvedModel ?? t.model ?? null,
1737
+ task: clip4(meta.task, 2e4) ?? "",
1738
+ grp: meta.group ?? null,
1739
+ dir: meta.dir,
1740
+ review_of: meta.reviewOf ?? null,
1741
+ verdict: meta.verdict ? `SPEC ${meta.verdict.spec.toUpperCase()} \xB7 QUALITY ${meta.verdict.quality.toUpperCase()}` : null,
1742
+ seconds,
1743
+ steps: u?.steps ?? null,
1744
+ tool_calls: u?.toolCalls ?? null,
1745
+ tokens: u?.total ?? null,
1746
+ returned_tokens: meta.returnedTokens ?? null,
1747
+ cost: u?.cost ?? null,
1748
+ saved: meta.savedUsd ?? null,
1749
+ files_changed: meta.changes?.length ?? 0,
1750
+ applied: meta.applied ? 1 : 0,
1751
+ error: clip4(meta.error, 2e3) ?? null,
1752
+ meta_json: JSON.stringify(meta)
1753
+ };
1754
+ db.exec("BEGIN IMMEDIATE");
1755
+ try {
1756
+ const cols = Object.keys(fields);
1757
+ db.prepare(`INSERT INTO runs (${cols.join(",")}) VALUES (${cols.map(() => "?").join(",")})
1758
+ ON CONFLICT(id) DO UPDATE SET ${cols.filter((c) => c !== "id").map((c) => `${c}=excluded.${c}`).join(",")}`).run(...Object.values(fields));
1759
+ if (!known?.has) {
1760
+ const steps = timelineOf(meta);
1761
+ db.prepare("UPDATE runs SET answer = ?, patch = ? WHERE id = ?").run(answer ?? "", patch ?? null, meta.id);
1762
+ db.prepare("DELETE FROM steps WHERE run_id = ?").run(meta.id);
1763
+ const ins = db.prepare("INSERT INTO steps (run_id, n, kind, name, text, ok, at) VALUES (?,?,?,?,?,?,?)");
1764
+ 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));
1765
+ db.prepare("DELETE FROM runs_fts WHERE id = ?").run(meta.id);
1766
+ db.prepare("INSERT INTO runs_fts (id, task, answer, steps) VALUES (?,?,?,?)").run(meta.id, fields.task, answer ?? "", steps.map((s) => s.text).join("\n"));
1767
+ }
1768
+ db.exec("COMMIT");
1769
+ } catch (e) {
1770
+ db.exec("ROLLBACK");
1771
+ throw e;
1772
+ }
1773
+ compactRun(meta);
1774
+ } catch {
1775
+ }
1776
+ }
1777
+ function importRuns() {
1778
+ const db = openDb();
1779
+ if (!db) return { imported: 0, known: 0 };
1780
+ const have = new Set(db.prepare("SELECT id FROM runs").all().map((r) => r.id));
1781
+ let imported = 0;
1782
+ for (const id of listRunIds()) {
1783
+ if (have.has(id)) continue;
1784
+ try {
1785
+ const m = readMeta(id);
1786
+ if (!TERMINAL.includes(m.state)) continue;
1787
+ recordRun(m);
1788
+ imported++;
1789
+ } catch {
1790
+ }
1791
+ }
1792
+ return { imported, known: have.size };
1793
+ }
1794
+ function archivedRun(id) {
1795
+ const db = openDb();
1796
+ if (!db) return void 0;
1797
+ try {
1798
+ const r = db.prepare("SELECT meta_json, answer, patch, started_at FROM runs WHERE id = ?").get(id);
1799
+ if (!r) return void 0;
1800
+ const start = Date.parse(r.started_at);
1801
+ const steps = db.prepare("SELECT kind, name, text, ok, at FROM steps WHERE run_id = ? ORDER BY n").all(id).map((s) => ({
1802
+ kind: s.kind,
1803
+ name: s.name ?? void 0,
1804
+ text: s.text,
1805
+ ok: s.ok === null ? void 0 : !!s.ok,
1806
+ at: s.at ?? void 0,
1807
+ t: s.at && Number.isFinite(start) ? Math.max(0, Math.round((s.at - start) / 1e3)) : void 0
1808
+ }));
1809
+ return { meta: JSON.parse(r.meta_json), answer: r.answer ?? "", patch: r.patch ?? void 0, steps };
1810
+ } catch {
1811
+ return void 0;
1812
+ }
1813
+ }
1814
+ function archivedId(ref) {
1815
+ const db = openDb();
1816
+ if (!db || !/^[\w-]{3,}$/.test(ref)) return void 0;
1817
+ try {
1818
+ const rows = db.prepare("SELECT id FROM runs WHERE id = ? OR id LIKE ? OR id LIKE ? ORDER BY id").all(ref, `${ref}%`, `%${ref}`);
1819
+ return rows.length === 1 ? rows[0].id : rows.find((r) => r.id === ref)?.id;
1820
+ } catch {
1821
+ return void 0;
1822
+ }
1823
+ }
1824
+ var ftsQuery = (text) => text.split(/\s+/).filter(Boolean).map((w) => `"${w.replace(/"/g, '""')}"*`).join(" ");
1825
+ function listHistory(q = {}) {
1826
+ const db = openDb();
1827
+ if (!db) return { rows: [], total: 0 };
1828
+ const where = [];
1829
+ const args = [];
1830
+ if (q.text?.trim()) {
1831
+ where.push("r.id IN (SELECT id FROM runs_fts WHERE runs_fts MATCH ?)");
1832
+ args.push(ftsQuery(q.text));
1833
+ }
1834
+ if (q.model) {
1835
+ where.push("r.model = ?");
1836
+ args.push(q.model);
1837
+ }
1838
+ if (q.backend) {
1839
+ where.push("r.backend = ?");
1840
+ args.push(q.backend);
1841
+ }
1842
+ if (q.state) {
1843
+ where.push(q.state === "problem" ? "r.state IN ('failed','timeout','stopped')" : "r.state = ?");
1844
+ if (q.state !== "problem") args.push(q.state);
1845
+ }
1846
+ if (q.group) {
1847
+ where.push("r.grp = ?");
1848
+ args.push(q.group);
1849
+ }
1850
+ if (q.sinceMs) {
1851
+ where.push("r.started_at >= ?");
1852
+ args.push(new Date(q.sinceMs).toISOString());
1853
+ }
1854
+ const base2 = where.length ? `WHERE ${where.join(" AND ")}` : "";
1855
+ try {
1856
+ const total = db.prepare(`SELECT COUNT(*) AS n FROM runs r ${base2}`).get(...args).n;
1857
+ const page = q.beforeId ? `${base2 ? `${base2} AND` : "WHERE"} r.id < ?` : base2;
1858
+ 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));
1859
+ return {
1860
+ total,
1861
+ rows: rows.map((r) => ({
1862
+ id: r.id,
1863
+ startedAt: r.started_at,
1864
+ state: r.state,
1865
+ kind: r.kind,
1866
+ backend: r.backend,
1867
+ model: r.model ?? void 0,
1868
+ 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()) ?? "",
1869
+ group: r.grp ?? void 0,
1870
+ verdict: r.verdict ?? void 0,
1871
+ seconds: r.seconds ?? void 0,
1872
+ steps: r.steps ?? void 0,
1873
+ tokens: r.tokens ?? void 0,
1874
+ saved: r.saved ?? void 0,
1875
+ files: r.files_changed ?? 0,
1876
+ applied: !!r.applied
1877
+ }))
1878
+ };
1879
+ } catch {
1880
+ return { rows: [], total: 0 };
1881
+ }
1882
+ }
1883
+ function historyStats(sinceMs2) {
1884
+ const empty = { totals: { runs: 0, ok: 0, failed: 0, seconds: 0, tokens: 0, saved: 0 }, byWorker: [], byDay: [] };
1885
+ const db = openDb();
1886
+ if (!db) return empty;
1887
+ const since = sinceMs2 ? new Date(sinceMs2).toISOString() : "";
1888
+ try {
1889
+ 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);
1890
+ 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);
1891
+ 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);
1892
+ return {
1893
+ totals: { runs: t.runs, ok: t.ok, failed: t.runs - t.ok, seconds: t.seconds, tokens: t.tokens, saved: t.saved },
1894
+ 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 })),
1895
+ byDay: d.reverse().map((r) => ({ day: r.day, runs: r.runs, ok: r.ok ?? 0, saved: r.saved }))
1896
+ };
1897
+ } catch {
1898
+ return empty;
1899
+ }
1900
+ }
1901
+
1902
+ // src/core/report.ts
1903
+ import fs12 from "node:fs";
1904
+
1905
+ // src/core/plan.ts
1906
+ import fs11 from "node:fs";
1907
+ import path9 from "node:path";
1908
+ var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
1909
+ var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
1910
+ function structure(lines) {
1911
+ const headings = [];
1912
+ const rules = [];
1913
+ const fenced = [];
1914
+ let open;
1915
+ lines.forEach((l, i) => {
1916
+ const f = FENCE.exec(l);
1917
+ if (f) {
1918
+ fenced[i] = true;
1919
+ const mark = f[1];
1920
+ if (!open) open = mark;
1921
+ else if (mark[0] === open[0] && mark.length >= open.length && !f[2].trim()) open = void 0;
1922
+ return;
1923
+ }
1924
+ fenced[i] = !!open;
1925
+ if (open) return;
1926
+ const h = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/.exec(l);
1927
+ if (h) headings.push({ line: i, level: h[1].length, text: h[2] });
1928
+ else if (RULE.test(l)) rules.push(i);
1929
+ });
1930
+ return { headings, rules, fenced };
1931
+ }
1932
+ function parsePlan(text, file = "") {
1933
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
1934
+ const { headings, rules, fenced } = structure(lines);
1935
+ const after = (line) => Math.min(headings.find((h) => h.line > line)?.line ?? lines.length, rules.find((r) => r > line) ?? lines.length);
1936
+ const titleHeading = headings.find((h) => h.level === 1);
1937
+ const start = titleHeading ? titleHeading.line + 1 : 0;
1938
+ const header = lines.slice(start, after(start - 1)).filter((l) => !/^\s*>/.test(l)).join("\n").trim();
1939
+ const gc = headings.find((h) => /^global constraints\b/i.test(h.text));
1940
+ const constraints = gc ? lines.slice(gc.line + 1, after(gc.line)).join("\n").trim() : "";
1941
+ const tasks = [];
1942
+ for (const h of headings) {
1943
+ const m = /^Task\s+(\d+)\b\s*[:.)\-–—]?\s*(.*)$/i.exec(h.text);
1944
+ if (!m) continue;
1945
+ const end = headings.find((o) => o.line > h.line && o.level <= h.level)?.line ?? lines.length;
1946
+ const body = lines.slice(h.line, end).join("\n").replace(/(\n\s*(?:---|\*\*\*|___)\s*)+$/, "").trimEnd();
1947
+ const unfenced = lines.slice(h.line, end).filter((_, j) => !fenced[h.line + j]).join("\n");
1948
+ const tier = /^\s*[-*]?\s*\*\*Worker:\*\*\s*`?([\w-]+)`?/m.exec(unfenced)?.[1]?.toLowerCase();
1949
+ tasks.push({ step: Number(m[1]), title: m[2].trim(), text: body, ...tier ? { tier } : {} });
1950
+ }
1951
+ return { file, title: titleHeading?.text ?? "", header, constraints, tasks };
1952
+ }
1953
+ function loadPlan(file) {
1954
+ const abs = path9.resolve(file);
1955
+ if (!fs11.existsSync(abs) || !fs11.statSync(abs).isFile()) throw new UserError(`plan not found: ${file}`);
1956
+ const plan = parsePlan(fs11.readFileSync(abs, "utf8"), fs11.realpathSync(abs));
1957
+ if (!plan.tasks.length) throw new UserError(`${file} has no "Task N" headings (see pitroom-writing-plans)`);
1958
+ return plan;
1484
1959
  }
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`;
1960
+ function planTask(plan, step) {
1961
+ const t = plan.tasks.find((x) => x.step === step);
1962
+ if (!t) throw new UserError(`no Task ${step} in ${plan.file}; it has ${plan.tasks.map((x) => `Task ${x.step}`).join(", ")}`);
1963
+ return t;
1488
1964
  }
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>
1965
+ function brief(plan, task) {
1966
+ return `${[
1967
+ `# ${plan.title || planName(plan.file)}`,
1968
+ plan.header,
1969
+ "## Global Constraints",
1970
+ plan.constraints || "(none stated in the plan)",
1971
+ task.text
1972
+ ].filter(Boolean).join("\n\n")}
1512
1973
  `;
1513
1974
  }
1975
+ var planName = (file) => path9.basename(file).replace(/\.md$/i, "");
1514
1976
 
1515
1977
  // src/core/report.ts
1516
- var ICON = {
1978
+ var ICON2 = {
1517
1979
  queued: "\u22EF",
1518
1980
  running: "\u2026",
1519
1981
  done: "\u2714",
@@ -1529,11 +1991,11 @@ function duration(meta) {
1529
1991
  }
1530
1992
  function readSummary(meta) {
1531
1993
  const f = runFile(meta.id, "summary.md");
1532
- return fs10.existsSync(f) ? fs10.readFileSync(f, "utf8").trim() : "";
1994
+ return fs12.existsSync(f) ? fs12.readFileSync(f, "utf8").trim() : archivedRun(meta.id)?.answer ?? "";
1533
1995
  }
1534
1996
  function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
1535
1997
  const out = [];
1536
- out.push(`pitroom ${ICON[meta.state]} ${meta.state} \xB7 ${meta.mode} \xB7 ${duration(meta)} \xB7 run ${meta.id}`);
1998
+ out.push(`pitroom ${ICON2[meta.state]} ${meta.state} \xB7 ${meta.mode} \xB7 ${duration(meta)} \xB7 run ${meta.id}`);
1537
1999
  const ran = meta.ran ?? meta.worker;
1538
2000
  const ids = [`worker ${ran.backend}`, meta.resolvedModel && `model ${meta.resolvedModel}`, meta.sessionId && `session ${meta.sessionId}`];
1539
2001
  out.push(ids.filter(Boolean).join(" \xB7 "));
@@ -1604,8 +2066,8 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
1604
2066
  }
1605
2067
  function live(meta) {
1606
2068
  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"));
2069
+ if (!fs12.existsSync(f)) return { steps: 0, toolCalls: 0 };
2070
+ const p = getBackend((meta.ran ?? meta.worker).backend).parse(fs12.readFileSync(f, "utf8"));
1609
2071
  return { steps: p.usage.steps, toolCalls: p.usage.toolCalls, last: p.lastActivity };
1610
2072
  }
1611
2073
  function progress(meta) {
@@ -1676,22 +2138,30 @@ async function waitMany(ids, opts) {
1676
2138
  }
1677
2139
  async function watch(select, opts) {
1678
2140
  const seen = /* @__PURE__ */ new Map();
1679
- const emit = (e) => opts.write(`${JSON.stringify(e)}
2141
+ const line = (s) => opts.write(`${s}
1680
2142
  `);
2143
+ const emit = (e, m) => {
2144
+ if (!opts.brief) return opts.write(`${JSON.stringify(e)}
2145
+ `);
2146
+ if (e.event === "started" && m) line(startedCard(m));
2147
+ else if (e.event === "fallback" && m) line(`\u{1F3C1} Pitroom \u21BB ${m.id} fell back from ${String(e.failed)}: ${String(e.reason)}`);
2148
+ else if (m && TERMINAL.includes(m.state)) line(endedCard(m));
2149
+ 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` : ""})`);
2150
+ };
1681
2151
  const deadline = opts.timeoutMs > 0 ? Date.now() + opts.timeoutMs : Infinity;
1682
2152
  for (; ; ) {
1683
2153
  const metas = select().map((id) => freshMeta(id));
1684
- if (opts.json) {
2154
+ if (opts.json || opts.brief) {
1685
2155
  for (const m of metas) {
1686
2156
  const prev = seen.get(m.id);
1687
2157
  const l = m.state === "running" ? live(m) : { steps: m.usage?.steps ?? 0, toolCalls: m.usage?.toolCalls ?? 0 };
1688
2158
  const attempts = m.attempts?.length ?? 0;
1689
2159
  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 });
2160
+ if (!prev) emit({ event: m.state === "queued" ? "queued" : "started", ...base2 }, m);
2161
+ else if (prev.state === "queued" && m.state === "running") emit({ event: "started", ...base2 }, m);
1692
2162
  if (attempts > (prev?.attempts ?? 0)) {
1693
2163
  const a = m.attempts[attempts - 1];
1694
- emit({ event: "fallback", run: m.id, failed: a.target, reason: oneLine4(a.error, 120) });
2164
+ emit({ event: "fallback", run: m.id, failed: a.target, reason: oneLine4(a.error, 120) }, m);
1695
2165
  }
1696
2166
  if (m.state === "running" && l.steps > (prev?.steps ?? 0)) {
1697
2167
  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 +2176,7 @@ async function watch(select, opts) {
1706
2176
  error: m.error ? oneLine4(m.error, 160) : void 0,
1707
2177
  refs: m.refs ? `${m.refs.valid}/${m.refs.total}` : void 0,
1708
2178
  changes: m.changes?.length || void 0
1709
- });
2179
+ }, m);
1710
2180
  }
1711
2181
  seen.set(m.id, { state: m.state, steps: l.steps, attempts });
1712
2182
  }
@@ -1718,7 +2188,7 @@ ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \xB7 Ctrl-C to stop watchin
1718
2188
  }
1719
2189
  const allDone = metas.length > 0 && metas.every((m) => TERMINAL.includes(m.state));
1720
2190
  if (allDone || metas.length === 0 && seen.size === 0) {
1721
- if (opts.json) {
2191
+ if (opts.json || opts.brief) {
1722
2192
  const ok = metas.filter((m) => m.state === "done").length;
1723
2193
  emit({ event: "all-done", runs: metas.length, ok, failed: metas.length - ok });
1724
2194
  }
@@ -1730,44 +2200,44 @@ ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \xB7 Ctrl-C to stop watchin
1730
2200
  }
1731
2201
 
1732
2202
  // src/core/install.ts
1733
- import fs11 from "node:fs";
2203
+ import fs13 from "node:fs";
1734
2204
  import os7 from "node:os";
1735
- import path9 from "node:path";
2205
+ import path10 from "node:path";
1736
2206
  import { fileURLToPath } from "node:url";
1737
2207
  var LEGACY = ["pitroom", "opencode-worker"];
1738
2208
  function packageRoot() {
1739
- return path9.resolve(path9.dirname(fileURLToPath(import.meta.url)), "..");
2209
+ return path10.resolve(path10.dirname(fileURLToPath(import.meta.url)), "..");
1740
2210
  }
1741
2211
  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();
2212
+ const dir = path10.join(root, "skills");
2213
+ if (!fs13.existsSync(dir)) return [];
2214
+ return fs13.readdirSync(dir).filter((d) => fs13.existsSync(path10.join(dir, d, "SKILL.md"))).sort();
1745
2215
  }
1746
2216
  function skillTargets() {
1747
2217
  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"));
2218
+ const targets = [path10.join(home2, ".agents", "skills")];
2219
+ if (fs13.existsSync(path10.join(home2, ".claude"))) targets.push(path10.join(home2, ".claude", "skills"));
1750
2220
  return targets;
1751
2221
  }
1752
- var launcherPath = () => path9.join(os7.homedir(), ".local", "bin", "pitroom");
2222
+ var launcherPath = () => path10.join(os7.homedir(), ".local", "bin", "pitroom");
1753
2223
  var LAUNCHER_MARK = "# pitroom launcher";
1754
2224
  function launcherScript(bundle) {
1755
2225
  const q = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
1756
2226
  return `#!/bin/sh
1757
2227
  ${LAUNCHER_MARK} (created by \`pitroom install\`; \`pitroom uninstall\` removes it)
1758
2228
  cli=${q(bundle)}
1759
- ok() { [ -n "$1" ] && [ -x "$1" ] && "$1" -e 'process.exit(+process.versions.node.split(".")[0] >= 18 ? 0 : 1)' 2>/dev/null; }
2229
+ 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
2230
  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
2231
  if ok "$n"; then exec "$n" "$cli" "$@"; fi
1762
2232
  done
1763
- echo "pitroom: needs Node.js 18 or newer (set PITROOM_NODE to its path)" >&2
2233
+ echo "pitroom: needs Node.js 22.13 or newer (set PITROOM_NODE to its path)" >&2
1764
2234
  exit 127
1765
2235
  `;
1766
2236
  }
1767
2237
  function isOurLauncher(file, root) {
1768
2238
  if (linksInto(file, root)) return true;
1769
2239
  try {
1770
- const s = fs11.readFileSync(file, "utf8");
2240
+ const s = fs13.readFileSync(file, "utf8");
1771
2241
  return s.includes(LAUNCHER_MARK) && s.includes(root);
1772
2242
  } catch {
1773
2243
  return false;
@@ -1775,57 +2245,57 @@ function isOurLauncher(file, root) {
1775
2245
  }
1776
2246
  function placeLauncher(bundle, root, force) {
1777
2247
  const dest = launcherPath();
1778
- fs11.mkdirSync(path9.dirname(dest), { recursive: true });
1779
- const exists = fs11.lstatSync(dest, { throwIfNoEntry: false });
2248
+ fs13.mkdirSync(path10.dirname(dest), { recursive: true });
2249
+ const exists = fs13.lstatSync(dest, { throwIfNoEntry: false });
1780
2250
  if (exists && !isOurLauncher(dest, root)) {
1781
2251
  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()}`);
2252
+ fs13.renameSync(dest, `${dest}.bak-${Date.now()}`);
1783
2253
  } else if (exists) {
1784
- fs11.rmSync(dest, { force: true });
2254
+ fs13.rmSync(dest, { force: true });
1785
2255
  }
1786
- fs11.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
1787
- return `\u2714 ${dest} \u2192 launcher for ${bundle} (Node 18+)`;
2256
+ fs13.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
2257
+ return `\u2714 ${dest} \u2192 launcher for ${bundle} (Node 22.13+)`;
1788
2258
  }
1789
2259
  function linksInto(link, dir) {
1790
- const st = fs11.lstatSync(link, { throwIfNoEntry: false });
2260
+ const st = fs13.lstatSync(link, { throwIfNoEntry: false });
1791
2261
  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);
2262
+ const target = path10.resolve(path10.dirname(link), fs13.readlinkSync(link));
2263
+ const rel = path10.relative(dir, target);
2264
+ return !rel.startsWith("..") && !path10.isAbsolute(rel);
1795
2265
  }
1796
2266
  function place(src, dest, opts) {
1797
- fs11.mkdirSync(path9.dirname(dest), { recursive: true });
1798
- const st = fs11.lstatSync(dest, { throwIfNoEntry: false });
2267
+ fs13.mkdirSync(path10.dirname(dest), { recursive: true });
2268
+ const st = fs13.lstatSync(dest, { throwIfNoEntry: false });
1799
2269
  if (st && !st.isSymbolicLink()) {
1800
2270
  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()}`);
2271
+ fs13.renameSync(dest, `${dest}.bak-${Date.now()}`);
1802
2272
  } else if (st) {
1803
- fs11.unlinkSync(dest);
2273
+ fs13.unlinkSync(dest);
1804
2274
  }
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");
2275
+ if (opts.copy) fs13.cpSync(src, dest, { recursive: true });
2276
+ else fs13.symlinkSync(src, dest, process.platform === "win32" ? "junction" : fs13.statSync(src).isDirectory() ? "dir" : "file");
1807
2277
  return `\u2714 ${dest} \u2192 ${opts.copy ? "copied" : src}`;
1808
2278
  }
1809
2279
  function install(opts) {
1810
2280
  const root = packageRoot();
1811
2281
  const skills = skillNames(root);
1812
- if (!skills.length) throw new Error(`no skills found under ${path9.join(root, "skills")}`);
2282
+ if (!skills.length) throw new Error(`no skills found under ${path10.join(root, "skills")}`);
1813
2283
  const out = [];
1814
2284
  for (const base2 of skillTargets()) {
1815
2285
  for (const legacy of LEGACY) {
1816
- const l = path9.join(base2, legacy);
2286
+ const l = path10.join(base2, legacy);
1817
2287
  if (!skills.includes(legacy) && linksInto(l, root)) {
1818
- fs11.unlinkSync(l);
2288
+ fs13.unlinkSync(l);
1819
2289
  out.push(`\u2714 removed old link ${l}`);
1820
2290
  }
1821
2291
  }
1822
- for (const name of skills) out.push(place(path9.join(root, "skills", name), path9.join(base2, name), opts));
2292
+ for (const name of skills) out.push(place(path10.join(root, "skills", name), path10.join(base2, name), opts));
1823
2293
  }
1824
- const bundle = path9.join(root, "dist", "pitroom.mjs");
1825
- if (fs11.existsSync(bundle)) {
2294
+ const bundle = path10.join(root, "dist", "pitroom.mjs");
2295
+ if (fs13.existsSync(bundle)) {
1826
2296
  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`);
2297
+ const onPath = (process.env.PATH ?? "").split(path10.delimiter).some((d) => path10.resolve(d) === path10.dirname(launcherPath()));
2298
+ if (!onPath) out.push(`! ${path10.dirname(launcherPath())} is not on PATH; add it, or run ${launcherPath()} directly`);
1829
2299
  }
1830
2300
  return out;
1831
2301
  }
@@ -1833,38 +2303,38 @@ function uninstall() {
1833
2303
  const root = packageRoot();
1834
2304
  const out = [];
1835
2305
  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);
2306
+ if (!fs13.existsSync(base2)) continue;
2307
+ for (const name of fs13.readdirSync(base2)) {
2308
+ const l = path10.join(base2, name);
1839
2309
  if (linksInto(l, root)) {
1840
- fs11.unlinkSync(l);
2310
+ fs13.unlinkSync(l);
1841
2311
  out.push(`\u2714 removed ${l}`);
1842
2312
  }
1843
2313
  }
1844
2314
  }
1845
2315
  if (isOurLauncher(launcherPath(), root)) {
1846
- fs11.rmSync(launcherPath(), { force: true });
2316
+ fs13.rmSync(launcherPath(), { force: true });
1847
2317
  out.push(`\u2714 removed ${launcherPath()}`);
1848
2318
  }
1849
2319
  return out.length ? out : ["nothing to remove"];
1850
2320
  }
1851
2321
  function installedSkills() {
1852
2322
  const names = skillNames();
1853
- return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs11.existsSync(path9.join(base2, n, "SKILL.md"))) }));
2323
+ return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs13.existsSync(path10.join(base2, n, "SKILL.md"))) }));
1854
2324
  }
1855
2325
 
1856
2326
  // src/core/plan-status.ts
1857
2327
  import crypto2 from "node:crypto";
1858
- import fs12 from "node:fs";
1859
- import path10 from "node:path";
2328
+ import fs14 from "node:fs";
2329
+ import path11 from "node:path";
1860
2330
  function notesFile(plan) {
1861
2331
  const id = crypto2.createHash("sha1").update(plan.file).digest("hex").slice(0, 12);
1862
- return path10.join(home(), "plans", id, "notes.md");
2332
+ return path11.join(home(), "plans", id, "notes.md");
1863
2333
  }
1864
2334
  function readNotes(plan) {
1865
2335
  const f = notesFile(plan);
1866
- if (!fs12.existsSync(f)) return [];
1867
- return fs12.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
2336
+ if (!fs14.existsSync(f)) return [];
2337
+ return fs14.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
1868
2338
  const m = /^(\d{4}-\d\d-\d\d \d\d:\d\d) (.*)$/.exec(l);
1869
2339
  return m ? { at: m[1], text: m[2] } : { at: "", text: l };
1870
2340
  });
@@ -1872,11 +2342,11 @@ function readNotes(plan) {
1872
2342
  function addNote(planFile, text) {
1873
2343
  const plan = loadPlan(planFile);
1874
2344
  const f = notesFile(plan);
1875
- fs12.mkdirSync(path10.dirname(f), { recursive: true });
1876
- if (!fs12.existsSync(f)) fs12.writeFileSync(f, `# plan: ${plan.file}
2345
+ fs14.mkdirSync(path11.dirname(f), { recursive: true });
2346
+ if (!fs14.existsSync(f)) fs14.writeFileSync(f, `# plan: ${plan.file}
1877
2347
  `);
1878
2348
  const at = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
1879
- fs12.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
2349
+ fs14.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
1880
2350
  `);
1881
2351
  return f;
1882
2352
  }
@@ -1944,15 +2414,15 @@ ${rulings}`,
1944
2414
 
1945
2415
  // src/core/review.ts
1946
2416
  import crypto4 from "node:crypto";
1947
- import fs14 from "node:fs";
1948
- import path12 from "node:path";
2417
+ import fs16 from "node:fs";
2418
+ import path13 from "node:path";
1949
2419
 
1950
2420
  // src/vcs/git.ts
1951
2421
  import { spawnSync as spawnSync4 } from "node:child_process";
1952
2422
  import crypto3 from "node:crypto";
1953
- import fs13 from "node:fs";
2423
+ import fs15 from "node:fs";
1954
2424
  import os8 from "node:os";
1955
- import path11 from "node:path";
2425
+ import path12 from "node:path";
1956
2426
  var SAFE = [
1957
2427
  "-c",
1958
2428
  "color.ui=false",
@@ -1984,20 +2454,20 @@ function gitAvailable() {
1984
2454
  }
1985
2455
  function repoRoot(dir) {
1986
2456
  const r = git(dir, ["rev-parse", "--show-toplevel"]);
1987
- return r.code === 0 ? path11.resolve(r.stdout.trim()) : void 0;
2457
+ return r.code === 0 ? path12.resolve(r.stdout.trim()) : void 0;
1988
2458
  }
1989
2459
  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());
2460
+ const tmp = path12.join(os8.tmpdir(), `pitroom-index-${process.pid}-${crypto3.randomBytes(4).toString("hex")}`);
2461
+ const real = path12.resolve(root, must(root, ["rev-parse", "--git-path", "index"]).trim());
1992
2462
  const env = { ...process.env, GIT_INDEX_FILE: tmp };
1993
2463
  try {
1994
- if (fs13.existsSync(real)) fs13.copyFileSync(real, tmp);
2464
+ if (fs15.existsSync(real)) fs15.copyFileSync(real, tmp);
1995
2465
  else must(root, ["read-tree", "--empty"], env);
1996
2466
  must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`)], env);
1997
2467
  return must(root, ["write-tree"], env).trim();
1998
2468
  } finally {
1999
- fs13.rmSync(tmp, { force: true });
2000
- fs13.rmSync(`${tmp}.lock`, { force: true });
2469
+ fs15.rmSync(tmp, { force: true });
2470
+ fs15.rmSync(`${tmp}.lock`, { force: true });
2001
2471
  }
2002
2472
  }
2003
2473
  var DIFF = ["diff", "--no-ext-diff", "--no-textconv", "--no-renames"];
@@ -2017,27 +2487,27 @@ function diffTrees(root, a, b) {
2017
2487
  return { changes, stats: { files: changes.length, insertions, deletions }, patch };
2018
2488
  }
2019
2489
  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}
2490
+ const objects = path12.join(path12.resolve(root, must(root, ["rev-parse", "--git-common-dir"]).trim()), "objects");
2491
+ fs15.mkdirSync(path12.dirname(dest), { recursive: true });
2492
+ must(path12.dirname(dest), ["init", "--quiet", dest]);
2493
+ fs15.writeFileSync(path12.join(dest, ".git", "objects", "info", "alternates"), `${objects}
2024
2494
  `);
2025
2495
  must(dest, ["read-tree", tree]);
2026
2496
  must(dest, ["checkout-index", "--all", "--force"]);
2027
2497
  }
2028
2498
  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 });
2499
+ const rel = path12.relative(ownedBy, dest);
2500
+ if (!rel || rel.startsWith("..") || path12.isAbsolute(rel)) throw new Error(`refusing to remove ${dest}`);
2501
+ fs15.rmSync(dest, { recursive: true, force: true });
2032
2502
  }
2033
2503
  function linkIntoWorktree(root, worktree, rels) {
2034
2504
  const linked = [];
2035
2505
  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);
2506
+ const src = path12.join(root, rel);
2507
+ const dst = path12.join(worktree, rel);
2508
+ if (!fs15.existsSync(src) || fs15.existsSync(dst)) continue;
2509
+ fs15.mkdirSync(path12.dirname(dst), { recursive: true });
2510
+ fs15.symlinkSync(src, dst, process.platform === "win32" ? "junction" : void 0);
2041
2511
  linked.push(rel);
2042
2512
  }
2043
2513
  return linked;
@@ -2096,7 +2566,7 @@ function ancestors(m) {
2096
2566
  function briefOf(m) {
2097
2567
  const root = [m, ...ancestors(m)].at(-1);
2098
2568
  const f = runFile(root.id, "brief.md");
2099
- return fs14.existsSync(f) ? fs14.readFileSync(f, "utf8") : root.task;
2569
+ return fs16.existsSync(f) ? fs16.readFileSync(f, "utf8") : root.task;
2100
2570
  }
2101
2571
  function latestReview(ids) {
2102
2572
  for (const id of listRunIds().reverse()) {
@@ -2110,11 +2580,11 @@ function latestReview(ids) {
2110
2580
  }
2111
2581
  function diffOf(m, from = m.baseTree) {
2112
2582
  const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
2113
- if (where && fs14.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
2583
+ if (where && fs16.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
2114
2584
  if (from !== m.baseTree) {
2115
2585
  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
2586
  }
2117
- return fs14.readFileSync(runFile(m.id, "changes.patch"), "utf8");
2587
+ return readRunFile(m.id, "changes.patch") ?? archivedRun(m.id)?.patch ?? "";
2118
2588
  }
2119
2589
  function runReview(id) {
2120
2590
  const m = freshMeta(id);
@@ -2124,7 +2594,7 @@ function runReview(id) {
2124
2594
  if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
2125
2595
  const chain = ancestors(m);
2126
2596
  const previous = latestReview(chain.map((a) => a.id));
2127
- const dir = m.mode === "isolate" && m.worktree && fs14.existsSync(m.worktree) ? m.cwd : m.dir;
2597
+ const dir = m.mode === "isolate" && m.worktree && fs16.existsSync(m.worktree) ? m.cwd : m.dir;
2128
2598
  const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
2129
2599
  const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
2130
2600
  if (!previous) {
@@ -2198,15 +2668,15 @@ function pickReviewer(job) {
2198
2668
  function writePackage(job) {
2199
2669
  const g = gitDir(job.dir);
2200
2670
  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);
2671
+ const file = path13.join(g, "pitroom", `review-${crypto4.randomBytes(4).toString("hex")}.md`);
2672
+ fs16.mkdirSync(path13.dirname(file), { recursive: true });
2673
+ fs16.writeFileSync(file, job.package);
2204
2674
  return file;
2205
2675
  }
2206
2676
 
2207
2677
  // src/core/templates.ts
2208
- import fs15 from "node:fs";
2209
- import path13 from "node:path";
2678
+ import fs17 from "node:fs";
2679
+ import path14 from "node:path";
2210
2680
  var FILES = {
2211
2681
  implementer: "pitroom-driven-development/implementer-prompt.md",
2212
2682
  "task-reviewer": "pitroom-driven-development/task-reviewer-prompt.md",
@@ -2214,9 +2684,9 @@ var FILES = {
2214
2684
  "code-reviewer": "pitroom-review/code-reviewer.md"
2215
2685
  };
2216
2686
  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*/, "");
2687
+ const file = path14.join(root, "skills", FILES[name]);
2688
+ if (!fs17.existsSync(file)) throw new UserError(`template missing: ${file} (broken install? run pitroom doctor)`, 3);
2689
+ return fs17.readFileSync(file, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
2220
2690
  }
2221
2691
  function fill(template, values) {
2222
2692
  const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
@@ -2320,133 +2790,329 @@ function formatModels(t) {
2320
2790
  return out.join("\n");
2321
2791
  }
2322
2792
 
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)) {
2793
+ // src/core/dash.ts
2794
+ import { spawn } from "node:child_process";
2795
+ import crypto5 from "node:crypto";
2796
+ import fs18 from "node:fs";
2797
+ import http from "node:http";
2798
+ import path15 from "node:path";
2799
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
2800
+
2801
+ // src/core/dash-page.ts
2802
+ 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){}})()";
2803
+ 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";
2804
+ var PAGE = `<!doctype html>
2805
+ <html lang="en" class="dark"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
2806
+ <meta name="color-scheme" content="dark light"><title>Pitroom</title><link rel="icon" href="${ICON3}">
2807
+ <script>${THEME_SCRIPT}</script><link rel="stylesheet" href="/assets/app.css"></head>
2808
+ <body><div id="root"></div><script type="module" src="/assets/app.js"></script></body></html>`;
2809
+ 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>`;
2810
+
2811
+ // src/core/dash.ts
2812
+ var DEFAULT_PORT = 7878;
2813
+ var WEEK_MS2 = 7 * 24 * 3600 * 1e3;
2814
+ var SCAN = 400;
2815
+ var RUN_ID2 = /^\d{8}-\d{6}-[0-9a-f]{4}$/;
2816
+ var LOCAL_HOST = /^(127\.0\.0\.1|localhost|\[::1\]):\d+$/i;
2817
+ var oneLine5 = (s, max) => {
2818
+ const t = s.replace(/\s+/g, " ").trim();
2819
+ return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
2820
+ };
2821
+ function findings(m) {
2822
+ const v = m.verdict;
2823
+ if (!v) return "";
2824
+ const parts = [v.critical && `${v.critical} critical`, v.important && `${v.important} important`, v.minor && `${v.minor} minor`].filter(Boolean);
2825
+ return parts.length ? parts.join(" \xB7 ") : "no findings";
2826
+ }
2827
+ function toRun(m) {
2828
+ const l = m.state === "running" ? live(m) : void 0;
2829
+ return {
2830
+ id: m.id,
2831
+ state: m.state,
2832
+ kind: kind(m),
2833
+ worker: workerName(m.ran ?? m.worker),
2834
+ task: what(m),
2835
+ group: m.group,
2836
+ startedAt: m.startedAt,
2837
+ time: elapsed(m),
2838
+ steps: l ? l.steps : m.usage?.steps ?? 0,
2839
+ tokens: m.usage?.total,
2840
+ saved: m.savedUsd || void 0,
2841
+ verdict: m.verdict ? `SPEC ${m.verdict.spec.toUpperCase()} \xB7 QUALITY ${m.verdict.quality.toUpperCase()}` : void 0,
2842
+ changes: m.changes?.length || void 0,
2843
+ applied: m.applied || void 0,
2844
+ note: l?.last ? oneLine5(String(l.last), 140) : isActive(m.state) ? "" : m.verdict ? findings(m) : headline(m, 200) || oneLine5(m.error ?? "", 200)
2845
+ };
2846
+ }
2847
+ function dashState(opts = {}) {
2848
+ const limit = Math.min(Math.max(opts.limit ?? 40, 1), 200);
2849
+ const runs = [];
2850
+ const groups = /* @__PURE__ */ new Set();
2851
+ for (const id of listRunIds().slice(-SCAN).reverse()) {
2852
+ let m;
2332
2853
  try {
2333
- const m = readMeta(id);
2334
- if (isActive(m.state) && (m.state === "queued" || isAlive(m.pid))) out.push(m);
2854
+ m = freshMeta(id);
2335
2855
  } catch {
2856
+ continue;
2336
2857
  }
2858
+ if (m.group) groups.add(m.group);
2859
+ if (opts.group && m.group !== opts.group) continue;
2860
+ if (runs.length < limit) runs.push(toRun(m));
2337
2861
  }
2338
- return out;
2862
+ return {
2863
+ running: runs.filter((r) => r.state === "running" || r.state === "queued").length,
2864
+ saved: totals(readLedger(Date.now() - WEEK_MS2)).saved,
2865
+ groups: [...groups].sort(),
2866
+ runs
2867
+ };
2339
2868
  }
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})`);
2869
+ var TASK_MAX = 6e3;
2870
+ var PATCH_LINES = 300;
2871
+ function readFile(id, name) {
2872
+ const f = runFile(id, name);
2873
+ return fs18.existsSync(f) ? fs18.readFileSync(f, "utf8") : "";
2874
+ }
2875
+ function runDetail(id) {
2876
+ if (!RUN_ID2.test(id)) return void 0;
2877
+ let m;
2878
+ try {
2879
+ m = freshMeta(id);
2880
+ } catch {
2881
+ return void 0;
2348
2882
  }
2349
- if (saved) parts.push(`~${usd(saved)} saved this week`);
2350
- return parts.join(" \xB7 ");
2883
+ const start = Date.parse(m.startedAt);
2884
+ const kept = isActive(m.state) ? void 0 : archivedRun(id);
2885
+ let steps = kept?.steps ?? [];
2886
+ if (!steps.length) try {
2887
+ const events = readRunFile(id, "events.jsonl");
2888
+ steps = (events ? getBackend((m.ran ?? m.worker).backend).parse(events).timeline ?? [] : []).map((s) => ({
2889
+ ...s,
2890
+ t: s.at && Number.isFinite(start) ? Math.max(0, Math.round((s.at - start) / 1e3)) : void 0
2891
+ }));
2892
+ } catch {
2893
+ }
2894
+ const rel = (t) => [m.cwd, m.dir].filter(Boolean).reduce((x, base2) => x.split(`${base2}/`).join(""), t);
2895
+ steps = steps.map((x) => ({ ...x, text: rel(x.text) }));
2896
+ const answer = isActive(m.state) ? "" : readSummary(m).replace(/^SUMMARY:\s*SPEC[^\n]*\n+/i, "").replace(/^DETAILS:[ \t]*\n+/i, "").trim();
2897
+ const lastSay = steps.at(-1);
2898
+ if (lastSay?.kind === "say" && answer && (answer.includes(lastSay.text.slice(0, 80)) || lastSay.text.includes(answer.slice(0, 80)))) steps.pop();
2899
+ const patch = kept?.patch ?? readFile(id, "changes.patch");
2900
+ const patchLines = patch.split("\n");
2901
+ const u = m.usage;
2902
+ return {
2903
+ id,
2904
+ state: m.state,
2905
+ 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),
2906
+ steps,
2907
+ answer,
2908
+ changes: m.changes ?? [],
2909
+ patch: patch ? `${patchLines.slice(0, PATCH_LINES).join("\n")}${patchLines.length > PATCH_LINES ? `
2910
+ \u2026 ${patchLines.length - PATCH_LINES} more lines (pitroom show ${id} --patch)` : ""}` : void 0,
2911
+ info: {
2912
+ worker: (m.ran ?? m.worker).backend,
2913
+ // the model has its own line
2914
+ model: m.resolvedModel,
2915
+ mode: m.mode,
2916
+ started: m.startedAt,
2917
+ time: elapsed(m),
2918
+ steps: u?.steps,
2919
+ toolCalls: u?.toolCalls,
2920
+ tokens: u?.total,
2921
+ returnedTokens: m.returnedTokens,
2922
+ cost: u?.cost,
2923
+ saved: m.savedUsd,
2924
+ group: m.group,
2925
+ directory: m.dir
2926
+ },
2927
+ attempts: m.attempts ?? [],
2928
+ warnings: m.warnings ?? [],
2929
+ refs: m.refs ? { valid: m.refs.valid, total: m.refs.total, invalid: m.refs.invalid.map((r) => `${r.ref} (${r.reason})`) } : void 0,
2930
+ verify: m.verifyResult && m.verify ? { command: m.verify, ok: m.verifyResult.ok, tail: m.verifyResult.tail } : void 0,
2931
+ error: m.error,
2932
+ report: isActive(m.state) ? progress(m) : formatReport(m, void 0, 120)
2933
+ };
2351
2934
  }
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";
2935
+ var HEADERS = {
2936
+ "cache-control": "no-store",
2937
+ "x-content-type-options": "nosniff",
2938
+ "referrer-policy": "no-referrer",
2939
+ // Own files only, plus the hash of the one inline theme script; styles may be inline (the components position popups with them).
2940
+ "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'`
2941
+ };
2942
+ var ASSETS = { "/assets/app.js": "text/javascript; charset=utf-8", "/assets/app.css": "text/css; charset=utf-8" };
2943
+ var assetsDir = () => fileURLToPath2(new URL("./ui/", import.meta.url));
2944
+ var assetCache = /* @__PURE__ */ new Map();
2945
+ function readAsset(route) {
2946
+ const hit = assetCache.get(route);
2947
+ if (hit) return hit;
2948
+ try {
2949
+ const data = fs18.readFileSync(path15.join(assetsDir(), path15.basename(route)));
2950
+ assetCache.set(route, data);
2951
+ return data;
2952
+ } catch {
2953
+ return void 0;
2954
+ }
2358
2955
  }
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`;
2956
+ function handler(touch) {
2957
+ const send = (res, code, type, body) => {
2958
+ res.writeHead(code, { ...HEADERS, "content-type": type });
2959
+ res.end(body);
2960
+ };
2961
+ const json = (res, code, value) => send(res, code, "application/json; charset=utf-8", JSON.stringify(value));
2962
+ return (req, res) => {
2963
+ touch();
2964
+ if (!LOCAL_HOST.test(req.headers.host ?? "")) return json(res, 403, { error: "forbidden host" });
2965
+ if (req.method !== "GET" && req.method !== "HEAD") return json(res, 405, { error: "read-only" });
2966
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
2967
+ if (url.pathname === "/") return send(res, 200, "text/html; charset=utf-8", readAsset("/assets/app.js") ? PAGE : MISSING);
2968
+ const type = ASSETS[url.pathname];
2969
+ if (type) {
2970
+ const data = readAsset(url.pathname);
2971
+ if (!data) return json(res, 404, { error: "dashboard files missing" });
2972
+ res.writeHead(200, { ...HEADERS, "content-type": type });
2973
+ return void res.end(req.method === "HEAD" ? void 0 : data);
2974
+ }
2975
+ if (url.pathname === "/api/state") {
2976
+ const group = url.searchParams.get("group") ?? void 0;
2977
+ const limit = Number(url.searchParams.get("limit") ?? 40);
2978
+ return json(res, 200, dashState({ group: group && group.length <= 100 ? group : void 0, limit: Number.isFinite(limit) ? limit : 40 }));
2979
+ }
2980
+ if (url.pathname === "/api/history") {
2981
+ const q = url.searchParams;
2982
+ const days = Number(q.get("days") ?? 0);
2983
+ return json(res, 200, listHistory({
2984
+ text: (q.get("q") ?? "").slice(0, 200),
2985
+ model: q.get("model") || void 0,
2986
+ backend: q.get("backend") || void 0,
2987
+ state: q.get("state") || void 0,
2988
+ group: q.get("group") || void 0,
2989
+ sinceMs: days > 0 ? Date.now() - days * 864e5 : void 0,
2990
+ beforeId: RUN_ID2.test(q.get("before") ?? "") ? q.get("before") : void 0,
2991
+ limit: Number(q.get("limit") ?? 30) || 30
2992
+ }));
2993
+ }
2994
+ if (url.pathname === "/api/stats") {
2995
+ const days = Number(url.searchParams.get("days") ?? 30);
2996
+ return json(res, 200, historyStats(days > 0 ? Date.now() - days * 864e5 : void 0));
2997
+ }
2998
+ const m = /^\/api\/run\/([^/]+)$/.exec(url.pathname);
2999
+ if (m) {
3000
+ const d = runDetail(m[1]);
3001
+ return d ? json(res, 200, d) : json(res, 404, { error: "no such run" });
3002
+ }
3003
+ return json(res, 404, { error: "not found" });
3004
+ };
2366
3005
  }
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})`;
3006
+ function startDash(opts) {
3007
+ let lastRequest = Date.now();
3008
+ const server = http.createServer(handler(() => lastRequest = Date.now()));
3009
+ return new Promise((resolve2, reject) => {
3010
+ server.once("error", reject);
3011
+ server.listen(opts.port, "127.0.0.1", () => {
3012
+ const port = server.address().port;
3013
+ let done;
3014
+ const closed = new Promise((r) => done = r);
3015
+ let timer;
3016
+ const close = () => {
3017
+ if (timer) clearInterval(timer);
3018
+ server.closeAllConnections?.();
3019
+ return new Promise((r) => server.close(() => (done(), r())));
3020
+ };
3021
+ if (opts.idleMs && opts.idleMs > 0) {
3022
+ timer = setInterval(() => Date.now() - lastRequest > opts.idleMs && void close(), Math.min(6e4, opts.idleMs));
3023
+ timer.unref();
3024
+ }
3025
+ resolve2({ port, url: `http://127.0.0.1:${port}/`, closed, close });
3026
+ });
3027
+ });
2372
3028
  }
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);
3029
+ var registryFile = () => path15.join(home(), "dash.json");
3030
+ async function ping(port) {
3031
+ try {
3032
+ const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
3033
+ return r.ok;
3034
+ } catch {
3035
+ return false;
3036
+ }
2380
3037
  }
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`;
3038
+ async function runningDash() {
3039
+ try {
3040
+ const r = JSON.parse(fs18.readFileSync(registryFile(), "utf8"));
3041
+ if (isAlive(r.pid) && await ping(r.port)) return { ...r, url: `http://127.0.0.1:${r.port}/` };
3042
+ } catch {
3043
+ }
3044
+ fs18.rmSync(registryFile(), { force: true });
3045
+ return void 0;
2385
3046
  }
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
- ]);
3047
+ function openBrowser(url) {
3048
+ const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
3049
+ try {
3050
+ spawn(cmd, args, { stdio: "ignore", detached: true }).on("error", () => void 0).unref();
3051
+ } catch {
2406
3052
  }
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`]);
3053
+ }
3054
+ async function dashCommand(o) {
3055
+ const existing = await runningDash();
3056
+ if (o.stop) {
3057
+ if (!existing) throw new UserError("pitroom dash is not running");
3058
+ process.kill(existing.pid, "SIGTERM");
3059
+ fs18.rmSync(registryFile(), { force: true });
3060
+ o.log(`stopped pitroom dash (${existing.url})`);
3061
+ return 0;
2409
3062
  }
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, "");
3063
+ if (existing && !o.serve) {
3064
+ o.log(existing.url);
3065
+ if (o.open) openBrowser(existing.url);
3066
+ return 0;
2416
3067
  }
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 {
3068
+ if (o.detach) {
3069
+ const child = spawn(process.execPath, [process.argv[1], "dash", "--serve", ...o.port !== void 0 ? ["--port", String(o.port)] : [], "--idle", "4h"], {
3070
+ detached: true,
3071
+ stdio: "ignore",
3072
+ env: process.env
3073
+ });
3074
+ child.on("error", () => void 0);
3075
+ child.unref();
3076
+ for (let i = 0; i < 60; i++) {
3077
+ await new Promise((r2) => setTimeout(r2, 100));
3078
+ const r = await runningDash();
3079
+ if (r && r.pid === child.pid) {
3080
+ o.log(r.url);
3081
+ if (o.open) openBrowser(r.url);
3082
+ return 0;
3083
+ }
2434
3084
  }
3085
+ throw new UserError("pitroom dash did not start (is the port taken? try --port 0)");
2435
3086
  }
2436
- const cards = [];
2437
- for (const id of ids.slice(0, MAX_CARDS)) {
2438
- try {
2439
- cards.push(...cardsFor(readMeta(id)));
2440
- } catch {
2441
- }
3087
+ importRuns();
3088
+ const explicit = o.port !== void 0;
3089
+ let dash;
3090
+ try {
3091
+ dash = await startDash({ port: o.port ?? DEFAULT_PORT, idleMs: o.idleMs });
3092
+ } catch (e) {
3093
+ if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
3094
+ dash = await startDash({ port: 0, idleMs: o.idleMs });
2442
3095
  }
2443
- return cards.join("\n");
3096
+ fs18.mkdirSync(home(), { recursive: true });
3097
+ fs18.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
3098
+ const stop = () => void dash.close();
3099
+ process.once("SIGINT", stop);
3100
+ process.once("SIGTERM", stop);
3101
+ if (!o.serve) o.log(`${dash.url}
3102
+ (read-only, this machine only; Ctrl-C stops it)`);
3103
+ if (o.open) openBrowser(dash.url);
3104
+ await dash.closed;
3105
+ try {
3106
+ if (JSON.parse(fs18.readFileSync(registryFile(), "utf8")).pid === process.pid) fs18.rmSync(registryFile(), { force: true });
3107
+ } catch {
3108
+ }
3109
+ return 0;
2444
3110
  }
2445
3111
 
2446
3112
  // 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";
3113
+ import { spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
3114
+ import fs23 from "node:fs";
3115
+ import path19 from "node:path";
2450
3116
 
2451
3117
  // src/core/chain.ts
2452
3118
  function resolveChain(flags = {}) {
@@ -2497,12 +3163,12 @@ function resolveChain(flags = {}) {
2497
3163
  }
2498
3164
 
2499
3165
  // src/core/process.ts
2500
- import { spawn } from "node:child_process";
2501
- import fs18 from "node:fs";
3166
+ import { spawn as spawn2 } from "node:child_process";
3167
+ import fs20 from "node:fs";
2502
3168
 
2503
3169
  // src/vcs/guard.ts
2504
- import fs17 from "node:fs";
2505
- import path14 from "node:path";
3170
+ import fs19 from "node:fs";
3171
+ import path16 from "node:path";
2506
3172
  var VERSION = 1;
2507
3173
  var SHIM = `#!/bin/sh
2508
3174
  # pitroom git guard v${VERSION}. Blocks git commands that change history, refs, the
@@ -2577,15 +3243,15 @@ esac
2577
3243
  exec "$real" "$@"
2578
3244
  `;
2579
3245
  function shimDir() {
2580
- return path14.join(home(), "shim", `v${VERSION}`);
3246
+ return path16.join(home(), "shim", `v${VERSION}`);
2581
3247
  }
2582
3248
  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");
3249
+ for (const dir of (process.env.PATH ?? "").split(path16.delimiter).filter(Boolean)) {
3250
+ if (path16.resolve(dir) === skip) continue;
3251
+ const p = path16.join(dir, "git");
2586
3252
  try {
2587
- fs17.accessSync(p, fs17.constants.X_OK);
2588
- if (fs17.statSync(p).isFile()) return p;
3253
+ fs19.accessSync(p, fs19.constants.X_OK);
3254
+ if (fs19.statSync(p).isFile()) return p;
2589
3255
  } catch {
2590
3256
  }
2591
3257
  }
@@ -2598,13 +3264,13 @@ var REF_HOOK = `#!/bin/sh
2598
3264
  echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
2599
3265
  exit 1
2600
3266
  `;
2601
- var hooksDir = () => path14.join(home(), "git-hooks", `v${HOOK_VERSION}`);
3267
+ var hooksDir = () => path16.join(home(), "git-hooks", `v${HOOK_VERSION}`);
2602
3268
  function writeIfChanged(file, content) {
2603
- if (fs17.existsSync(file) && fs17.readFileSync(file, "utf8") === content) return;
2604
- fs17.mkdirSync(path14.dirname(file), { recursive: true });
3269
+ if (fs19.existsSync(file) && fs19.readFileSync(file, "utf8") === content) return;
3270
+ fs19.mkdirSync(path16.dirname(file), { recursive: true });
2605
3271
  const tmp = `${file}.${process.pid}.tmp`;
2606
- fs17.writeFileSync(tmp, content, { mode: 493 });
2607
- fs17.renameSync(tmp, file);
3272
+ fs19.writeFileSync(tmp, content, { mode: 493 });
3273
+ fs19.renameSync(tmp, file);
2608
3274
  }
2609
3275
  function withGitConfig(env, entries) {
2610
3276
  const out = { ...env };
@@ -2626,7 +3292,7 @@ function guardEnv(env) {
2626
3292
  PAGER: "cat"
2627
3293
  };
2628
3294
  if (process.platform === "win32") return { ...env, ...quiet };
2629
- writeIfChanged(path14.join(hooksDir(), "reference-transaction"), REF_HOOK);
3295
+ writeIfChanged(path16.join(hooksDir(), "reference-transaction"), REF_HOOK);
2630
3296
  env = withGitConfig(env, [
2631
3297
  ["core.hooksPath", hooksDir()],
2632
3298
  ["url.pitroom-push-blocked://.pushInsteadOf", ""]
@@ -2634,16 +3300,16 @@ function guardEnv(env) {
2634
3300
  const dir = shimDir();
2635
3301
  const real = findRealGit(dir);
2636
3302
  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 ?? ""}` };
3303
+ writeIfChanged(path16.join(dir, "git"), SHIM);
3304
+ return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${path16.delimiter}${env.PATH ?? ""}` };
2639
3305
  }
2640
3306
 
2641
3307
  // src/core/process.ts
2642
3308
  async function spawnWorker(inv, opts) {
2643
- const out = fs18.openSync(opts.stdoutFile, "w");
2644
- const err = fs18.openSync(opts.stderrFile, "w");
3309
+ const out = fs20.openSync(opts.stdoutFile, "w");
3310
+ const err = fs20.openSync(opts.stderrFile, "w");
2645
3311
  const res = { code: null, timedOut: false, stopped: false };
2646
- const child = spawn(inv.command, inv.args, {
3312
+ const child = spawn2(inv.command, inv.args, {
2647
3313
  cwd: opts.cwd,
2648
3314
  stdio: ["ignore", out, err],
2649
3315
  // OpenCode v2 takes its workspace from $PWD, not the process cwd: without this a worker
@@ -2673,8 +3339,8 @@ async function spawnWorker(inv, opts) {
2673
3339
  if (killer) clearTimeout(killer);
2674
3340
  process.off("SIGINT", stop);
2675
3341
  process.off("SIGTERM", stop);
2676
- fs18.closeSync(out);
2677
- fs18.closeSync(err);
3342
+ fs20.closeSync(out);
3343
+ fs20.closeSync(err);
2678
3344
  return res;
2679
3345
  }
2680
3346
 
@@ -2699,11 +3365,11 @@ function parseVerdict(text) {
2699
3365
  }
2700
3366
 
2701
3367
  // 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");
3368
+ import crypto6 from "node:crypto";
3369
+ import fs21 from "node:fs";
3370
+ import path17 from "node:path";
3371
+ var slotsDir = () => path17.join(home(), "slots");
3372
+ var locksDir = () => path17.join(home(), "locks");
2707
3373
  var STARTUP_GRACE_MS = 3e4;
2708
3374
  function holderActive(runId, self) {
2709
3375
  if (!runId || runId === self) return false;
@@ -2717,23 +3383,23 @@ function holderActive(runId, self) {
2717
3383
  }
2718
3384
  }
2719
3385
  function tryClaim(file, runId) {
2720
- fs19.mkdirSync(path15.dirname(file), { recursive: true });
3386
+ fs21.mkdirSync(path17.dirname(file), { recursive: true });
2721
3387
  try {
2722
- fs19.writeFileSync(file, runId, { flag: "wx" });
3388
+ fs21.writeFileSync(file, runId, { flag: "wx" });
2723
3389
  return true;
2724
3390
  } catch (e) {
2725
3391
  if (e.code !== "EEXIST") throw e;
2726
3392
  }
2727
3393
  let owner = "";
2728
3394
  try {
2729
- owner = fs19.readFileSync(file, "utf8").trim();
3395
+ owner = fs21.readFileSync(file, "utf8").trim();
2730
3396
  } catch {
2731
3397
  }
2732
3398
  if (owner === runId) return true;
2733
3399
  if (holderActive(owner, runId)) return false;
2734
- fs19.rmSync(file, { force: true });
3400
+ fs21.rmSync(file, { force: true });
2735
3401
  try {
2736
- fs19.writeFileSync(file, runId, { flag: "wx" });
3402
+ fs21.writeFileSync(file, runId, { flag: "wx" });
2737
3403
  return true;
2738
3404
  } catch {
2739
3405
  return false;
@@ -2742,25 +3408,25 @@ function tryClaim(file, runId) {
2742
3408
  function releaseIfOwner(file, runId) {
2743
3409
  if (!file) return;
2744
3410
  try {
2745
- if (fs19.readFileSync(file, "utf8").trim() === runId) fs19.rmSync(file, { force: true });
3411
+ if (fs21.readFileSync(file, "utf8").trim() === runId) fs21.rmSync(file, { force: true });
2746
3412
  } catch {
2747
3413
  }
2748
3414
  }
2749
3415
  function tryAcquireSlot(runId, maxParallel) {
2750
3416
  for (let n = 0; n < Math.max(1, maxParallel); n++) {
2751
- const file = path15.join(slotsDir(), `slot-${n}`);
3417
+ const file = path17.join(slotsDir(), `slot-${n}`);
2752
3418
  if (tryClaim(file, runId)) return file;
2753
3419
  }
2754
3420
  return void 0;
2755
3421
  }
2756
3422
  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)}`);
3423
+ var lockFile = (repoRoot2) => path17.join(locksDir(), `write-${crypto6.createHash("sha1").update(path17.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
2758
3424
  function acquireWriteLock(repoRoot2, runId) {
2759
3425
  const file = lockFile(repoRoot2);
2760
3426
  if (tryClaim(file, runId)) return;
2761
3427
  let owner = "";
2762
3428
  try {
2763
- owner = fs19.readFileSync(file, "utf8").trim();
3429
+ owner = fs21.readFileSync(file, "utf8").trim();
2764
3430
  } catch {
2765
3431
  }
2766
3432
  throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
@@ -2807,8 +3473,8 @@ ${task}`;
2807
3473
  }
2808
3474
 
2809
3475
  // src/core/refs.ts
2810
- import fs20 from "node:fs";
2811
- import path16 from "node:path";
3476
+ import fs22 from "node:fs";
3477
+ import path18 from "node:path";
2812
3478
  var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
2813
3479
  var REF = new RegExp(
2814
3480
  String.raw`(?<![\w/:.-])(\.{0,2}/?(?:[\w@.+-]+/)*(?:[\w@+-][\w@.+-]*\.[A-Za-z][A-Za-z0-9]{0,7}|${EXTENSIONLESS})):(\d+)(?:[-–](\d+))?`,
@@ -2854,13 +3520,13 @@ function extractRefs(answer) {
2854
3520
  return [...seen.values()];
2855
3521
  }
2856
3522
  function verifyRefs(refs, dirs) {
2857
- const roots = [...new Set(dirs.filter(Boolean).map((d) => path16.resolve(d)))];
3523
+ const roots = [...new Set(dirs.filter(Boolean).map((d) => path18.resolve(d)))];
2858
3524
  const invalid = [];
2859
3525
  const lineCache = /* @__PURE__ */ new Map();
2860
3526
  const load = (file) => {
2861
3527
  if (!lineCache.has(file)) {
2862
- const size = fs20.statSync(file).size;
2863
- const lines = size > MAX_BYTES ? null : fs20.readFileSync(file, "utf8").split("\n");
3528
+ const size = fs22.statSync(file).size;
3529
+ const lines = size > MAX_BYTES ? null : fs22.readFileSync(file, "utf8").split("\n");
2864
3530
  if (lines && lines[lines.length - 1] === "") lines.pop();
2865
3531
  lineCache.set(file, lines);
2866
3532
  }
@@ -2869,7 +3535,7 @@ function verifyRefs(refs, dirs) {
2869
3535
  for (const ref of refs) {
2870
3536
  const file = resolve(ref.file, roots);
2871
3537
  if (!file) {
2872
- invalid.push({ ref: ref.text, reason: path16.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
3538
+ invalid.push({ ref: ref.text, reason: path18.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
2873
3539
  continue;
2874
3540
  }
2875
3541
  const lines = load(file);
@@ -2896,10 +3562,10 @@ function enclosedBy(lines, ref) {
2896
3562
  return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
2897
3563
  }
2898
3564
  function resolve(ref, roots) {
2899
- const candidates = path16.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path16.join(r, ref));
3565
+ const candidates = path18.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path18.join(r, ref));
2900
3566
  return candidates.find((c) => {
2901
3567
  try {
2902
- return fs20.statSync(c).isFile() && inside(c, roots);
3568
+ return fs22.statSync(c).isFile() && inside(c, roots);
2903
3569
  } catch {
2904
3570
  return false;
2905
3571
  }
@@ -2907,14 +3573,14 @@ function resolve(ref, roots) {
2907
3573
  }
2908
3574
  function inside(file, roots) {
2909
3575
  return roots.some((r) => {
2910
- const rel = path16.relative(r, path16.resolve(file));
2911
- return rel !== "" && !rel.startsWith("..") && !path16.isAbsolute(rel);
3576
+ const rel = path18.relative(r, path18.resolve(file));
3577
+ return rel !== "" && !rel.startsWith("..") && !path18.isAbsolute(rel);
2912
3578
  });
2913
3579
  }
2914
3580
  var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2915
3581
 
2916
3582
  // src/core/run.ts
2917
- var VERSION2 = true ? "0.6.5" : "0.0.0-dev";
3583
+ var VERSION2 = true ? "0.6.6" : "0.0.0-dev";
2918
3584
  function planWork(o) {
2919
3585
  if (!o.plan) return { task: o.task.trim(), tier: o.tier };
2920
3586
  if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
@@ -2946,7 +3612,7 @@ function prepareRun(o) {
2946
3612
  parent = readMeta(o.continueFrom);
2947
3613
  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
3614
  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))) {
3615
+ if (parent.mode === "isolate" && (!parent.worktree || !fs23.existsSync(parent.worktree))) {
2950
3616
  throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
2951
3617
  }
2952
3618
  const ran = parent.ran ?? parent.worker;
@@ -2965,15 +3631,15 @@ function prepareRun(o) {
2965
3631
  throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
2966
3632
  }
2967
3633
  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}`);
3634
+ const dir = path19.resolve(parent?.dir ?? o.dir);
3635
+ if (!fs23.existsSync(dir) || !fs23.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
2970
3636
  const root = repoRoot(dir);
2971
3637
  if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
2972
3638
  if (mode === "write" && !root && !o.allowNonGit) {
2973
3639
  throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
2974
3640
  }
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}`);
3641
+ const files = o.files.map((f) => path19.resolve(f));
3642
+ for (const f of files) if (!fs23.existsSync(f)) throw new UserError(`file not found: ${f}`);
2977
3643
  const meta = {
2978
3644
  id: newRunId(),
2979
3645
  version: VERSION2,
@@ -3008,13 +3674,13 @@ function prepareRun(o) {
3008
3674
  try {
3009
3675
  acquireWriteLock(root, meta.id);
3010
3676
  } catch (e) {
3011
- fs21.rmSync(runDir(meta.id), { recursive: true, force: true });
3677
+ fs23.rmSync(runDir(meta.id), { recursive: true, force: true });
3012
3678
  throw e;
3013
3679
  }
3014
3680
  }
3015
- fs21.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
3681
+ fs23.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
3016
3682
  `);
3017
- if (work.brief) fs21.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
3683
+ if (work.brief) fs23.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
3018
3684
  return meta;
3019
3685
  }
3020
3686
  var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -3042,7 +3708,7 @@ async function waitForSlot(meta) {
3042
3708
  function startInBackground(meta) {
3043
3709
  const script = process.argv[1];
3044
3710
  if (!script) throw new Error("cannot locate the pitroom executable");
3045
- const child = spawn2(process.execPath, [script, "__exec", meta.id], {
3711
+ const child = spawn3(process.execPath, [script, "__exec", meta.id], {
3046
3712
  detached: true,
3047
3713
  stdio: "ignore",
3048
3714
  cwd: meta.dir,
@@ -3087,7 +3753,7 @@ async function execute(meta) {
3087
3753
  (meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
3088
3754
  for (const f of ["events.jsonl", "stderr.log"]) {
3089
3755
  const from = runFile(meta.id, f);
3090
- if (fs21.existsSync(from)) fs21.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
3756
+ if (fs23.existsSync(from)) fs23.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
3091
3757
  }
3092
3758
  writeMeta(meta);
3093
3759
  }
@@ -3107,9 +3773,9 @@ function prepareTree(meta) {
3107
3773
  if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
3108
3774
  if (root && meta.mode === "isolate" && !meta.worktree) {
3109
3775
  meta.baseTree = snapshotTree(root);
3110
- meta.worktree = path17.join(worktreesDir(), meta.id);
3776
+ meta.worktree = path19.join(worktreesDir(), meta.id);
3111
3777
  createIsolatedCopy(root, meta.baseTree, meta.worktree);
3112
- meta.cwd = path17.join(meta.worktree, path17.relative(root, meta.dir));
3778
+ meta.cwd = path19.join(meta.worktree, path19.relative(root, meta.dir));
3113
3779
  const linked = linkIntoWorktree(root, meta.worktree, meta.link);
3114
3780
  if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
3115
3781
  }
@@ -3132,7 +3798,7 @@ function attempt(meta, backend, target) {
3132
3798
  timeoutSec: meta.timeoutSec
3133
3799
  });
3134
3800
  }
3135
- var read = (f) => fs21.existsSync(f) ? fs21.readFileSync(f, "utf8") : "";
3801
+ var read = (f) => fs23.existsSync(f) ? fs23.readFileSync(f, "utf8") : "";
3136
3802
  function retryableFailure(meta, backend, res) {
3137
3803
  const run = backend.parse(read(runFile(meta.id, "events.jsonl")));
3138
3804
  if (run.usage.steps > 0) return void 0;
@@ -3151,12 +3817,12 @@ function finalize(meta, res) {
3151
3817
  const run = backend.parse(read(runFile(meta.id, "events.jsonl")));
3152
3818
  meta.sessionId = run.sessionId ?? meta.sessionId;
3153
3819
  meta.usage = run.usage;
3154
- fs21.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
3820
+ fs23.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
3155
3821
  `);
3156
3822
  if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run.finalText);
3157
3823
  if (meta.reviewOf) {
3158
3824
  meta.verdict = parseVerdict(run.finalText);
3159
- if (meta.packageFile) fs21.rmSync(meta.packageFile, { force: true });
3825
+ if (meta.packageFile) fs23.rmSync(meta.packageFile, { force: true });
3160
3826
  }
3161
3827
  if (!res.timedOut && !res.stopped && !meta.error) {
3162
3828
  const f = backend.failure(run, read(runFile(meta.id, "stderr.log")), res.code);
@@ -3198,7 +3864,7 @@ function captureChanges(meta) {
3198
3864
  const d = diffTrees(target, meta.baseTree, meta.afterTree);
3199
3865
  meta.changes = d.changes;
3200
3866
  meta.stats = d.stats;
3201
- fs21.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
3867
+ fs23.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
3202
3868
  }
3203
3869
  function runVerify(meta) {
3204
3870
  const r = spawnSync5(meta.verify, {
@@ -3211,7 +3877,7 @@ function runVerify(meta) {
3211
3877
  env: { ...process.env, PWD: meta.cwd, PITROOM_ACTIVE: "1" }
3212
3878
  });
3213
3879
  const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
3214
- fs21.writeFileSync(runFile(meta.id, "verify.log"), output);
3880
+ fs23.writeFileSync(runFile(meta.id, "verify.log"), output);
3215
3881
  return { ok: r.status === 0, code: r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
3216
3882
  }
3217
3883
  function applyRun(meta, allowDelete = false) {
@@ -3256,7 +3922,7 @@ function discardRun(meta) {
3256
3922
  return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
3257
3923
  }
3258
3924
  function cleanupWorktree(meta) {
3259
- if (meta.worktree && fs21.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
3925
+ if (meta.worktree && fs23.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
3260
3926
  }
3261
3927
 
3262
3928
  // src/cli/commands.ts
@@ -3305,10 +3971,10 @@ async function cmdReview(p) {
3305
3971
  writeMeta(meta);
3306
3972
  }
3307
3973
  } catch (e) {
3308
- fs22.rmSync(packageFile, { force: true });
3974
+ fs24.rmSync(packageFile, { force: true });
3309
3975
  throw e;
3310
3976
  }
3311
- fs22.writeFileSync(runFile(meta.id, "package.md"), job.package);
3977
+ fs24.writeFileSync(runFile(meta.id, "package.md"), job.package);
3312
3978
  return launch(p, meta);
3313
3979
  }
3314
3980
  function cmdPlan(p) {
@@ -3328,7 +3994,7 @@ function cmdPlan(p) {
3328
3994
  }
3329
3995
  function cmdCrew(p) {
3330
3996
  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);
3997
+ const tasks = (file ? fs24.readFileSync(file, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
3332
3998
  if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
3333
3999
  if (has(p, "write") && tasks.length > 1) {
3334
4000
  throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
@@ -3396,8 +4062,10 @@ async function cmdWatch(p) {
3396
4062
  console.log(group ? `no runs in group "${group}"` : "nothing is running");
3397
4063
  return 0;
3398
4064
  }
4065
+ const brief2 = has(p, "brief");
3399
4066
  const { metas, timedOut } = await watch(group ? () => groupIds(group) : () => initial, {
3400
- json: has(p, "json") || !process.stdout.isTTY,
4067
+ json: !brief2 && (has(p, "json") || !process.stdout.isTTY),
4068
+ brief: brief2,
3401
4069
  intervalMs: parseDuration(flag(p, "interval") ?? "2") * 1e3,
3402
4070
  timeoutMs: flag(p, "timeout") ? parseDuration(flag(p, "timeout")) * 1e3 : 0,
3403
4071
  write: (s) => process.stdout.write(s)
@@ -3405,11 +4073,62 @@ async function cmdWatch(p) {
3405
4073
  if (timedOut) return 75;
3406
4074
  return metas.some((m) => m.state !== "done") ? 1 : 0;
3407
4075
  }
4076
+ async function cmdDash(p) {
4077
+ const port = flag(p, "port");
4078
+ 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)");
4079
+ return dashCommand({
4080
+ port: port === void 0 ? void 0 : Number(port),
4081
+ idleMs: flag(p, "idle") ? parseDuration(flag(p, "idle")) * 1e3 : void 0,
4082
+ detach: has(p, "detach"),
4083
+ stop: has(p, "stop"),
4084
+ open: has(p, "open"),
4085
+ serve: has(p, "serve"),
4086
+ log: (s) => console.log(s)
4087
+ });
4088
+ }
4089
+ var when = (iso) => new Date(iso).toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
4090
+ 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`;
4091
+ function cmdHistory(p) {
4092
+ if (!openDb()) throw new UserError("the history needs Node.js 22.13+ (node:sqlite)", 3);
4093
+ const sub = p.positional[0];
4094
+ if (sub === "import") {
4095
+ const r = importRuns();
4096
+ console.log(`imported ${r.imported} run(s); ${r.known} were already in the history (${historyFile()})`);
4097
+ return 0;
4098
+ }
4099
+ importRuns();
4100
+ const since = sinceMs(flag(p, "since"));
4101
+ if (sub === "stats") {
4102
+ const s = historyStats(since);
4103
+ if (has(p, "json")) return console.log(JSON.stringify(s, null, 2)), 0;
4104
+ const t = s.totals;
4105
+ 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`);
4106
+ 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)}`]);
4107
+ const head2 = ["WORKER", "RUNS", "OK", "AVG TIME", "AVG TOKENS", "SAVED"];
4108
+ const widths2 = head2.map((h, i) => Math.max(h.length, ...rows2.map((r) => r[i].length)));
4109
+ const fmt2 = (r) => r.map((c, i) => i === 0 ? c.padEnd(widths2[i]) : c.padStart(widths2[i])).join(" ");
4110
+ if (rows2.length) console.log(`
4111
+ ${[fmt2(head2), ...rows2.map(fmt2)].join("\n")}`);
4112
+ return 0;
4113
+ }
4114
+ const limit = flag(p, "limit") ? Number(flag(p, "limit")) : 20;
4115
+ const { rows, total } = listHistory({ text: p.positional.join(" "), model: flag(p, "model"), state: flag(p, "state"), group: flag(p, "group"), sinceMs: since, limit });
4116
+ if (has(p, "json")) return console.log(JSON.stringify({ total, rows }, null, 2)), 0;
4117
+ if (!rows.length) return console.log(total ? "nothing on this page" : "no matching runs"), 0;
4118
+ 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]);
4119
+ const head = ["RUN", "WHEN", "STATE", "WORKER", "TIME", "TASK"];
4120
+ const widths = head.map((h, i) => Math.max(h.length, ...body.map((r) => r[i].length)));
4121
+ const fmt = (r) => r.map((c, i) => i === r.length - 1 ? c : c.padEnd(widths[i])).join(" ");
4122
+ console.log([fmt(head), ...body.map(fmt)].join("\n"));
4123
+ if (total > rows.length) console.log(`
4124
+ ${rows.length} of ${total} \xB7 --limit N for more \xB7 pitroom show <run> for one`);
4125
+ return 0;
4126
+ }
3408
4127
  function cmdShow(p) {
3409
4128
  const meta = freshMeta(resolveRun(p.positional[0]));
3410
4129
  const dump = (name) => {
3411
- const f = runFile(meta.id, name);
3412
- process.stdout.write(fs22.existsSync(f) ? fs22.readFileSync(f, "utf8") : `(no ${name})
4130
+ const text = readRunFile(meta.id, name) ?? (name === "changes.patch" ? archivedRun(meta.id)?.patch : void 0);
4131
+ process.stdout.write(text ?? `(no ${name}${name === "events.jsonl" ? ": the raw stream was removed, the history keeps the steps" : ""})
3413
4132
  `);
3414
4133
  };
3415
4134
  if (has(p, "patch")) dump("changes.patch");
@@ -3473,7 +4192,7 @@ function sinceMs(s) {
3473
4192
  if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
3474
4193
  return Date.now() - Number(m[1]) * 864e5;
3475
4194
  }
3476
- var readStdin = () => process.stdin.isTTY ? "" : fs22.readFileSync(0, "utf8");
4195
+ var readStdin = () => process.stdin.isTTY ? "" : fs24.readFileSync(0, "utf8");
3477
4196
  function cmdStatusline(p) {
3478
4197
  const input = readStdin();
3479
4198
  const lines = [];
@@ -3542,8 +4261,8 @@ function cmdSavings(p) {
3542
4261
  est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
3543
4262
  const out = flag(p, "card");
3544
4263
  if (out) {
3545
- fs22.writeFileSync(out, card(t, period));
3546
- console.log(`card written to ${path18.resolve(out)}`);
4264
+ fs24.writeFileSync(out, card(t, period));
4265
+ console.log(`card written to ${path20.resolve(out)}`);
3547
4266
  }
3548
4267
  if (has(p, "badge")) console.log(`![pitroom](${badgeUrl(t)})`);
3549
4268
  return 0;
@@ -3563,7 +4282,7 @@ function cmdConfig(p) {
3563
4282
  console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
3564
4283
  return 0;
3565
4284
  }
3566
- console.log(`config file: ${configPath()}${fs22.existsSync(configPath()) ? "" : " (not present)"}`);
4285
+ console.log(`config file: ${configPath()}${fs24.existsSync(configPath()) ? "" : " (not present)"}`);
3567
4286
  for (const [key, s] of Object.entries(eff)) {
3568
4287
  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
4288
  console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
@@ -3586,9 +4305,10 @@ function cmdClean(p) {
3586
4305
  for (const id of old) {
3587
4306
  const m = readMeta(id);
3588
4307
  if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
3589
- fs22.rmSync(runDir(id), { recursive: true, force: true });
4308
+ recordRun(m);
4309
+ fs24.rmSync(runDir(id), { recursive: true, force: true });
3590
4310
  }
3591
- console.log(`removed ${old.length} run(s); the savings ledger is kept`);
4311
+ console.log(`removed ${old.length} run(s); the history and the savings ledger are kept`);
3592
4312
  return 0;
3593
4313
  }
3594
4314
  var cmdRevert = (p) => (console.log(revertRun(freshMeta(resolveRun(p.positional[0])))), 0);
@@ -3596,9 +4316,9 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
3596
4316
 
3597
4317
  // src/core/doctor.ts
3598
4318
  import { spawnSync as spawnSync7 } from "node:child_process";
3599
- import fs23 from "node:fs";
4319
+ import fs25 from "node:fs";
3600
4320
  import os9 from "node:os";
3601
- import path19 from "node:path";
4321
+ import path21 from "node:path";
3602
4322
  var MARK = { ok: "\u2714", warn: "!", fail: "\u2718" };
3603
4323
  var READ_ONLY_HOW = {
3604
4324
  "permission-rules": "per-run permission rules",
@@ -3612,7 +4332,7 @@ function doctor4(probe) {
3612
4332
  add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
3613
4333
  add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
3614
4334
  const cfg = loadConfig();
3615
- add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs23.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
4335
+ add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs25.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
3616
4336
  for (const w of cfg.warnings) add("warn", w);
3617
4337
  if (process.platform !== "win32") {
3618
4338
  const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
@@ -3690,11 +4410,11 @@ function liveProbe(target) {
3690
4410
  maxBuffer: 64 * 1024 * 1024,
3691
4411
  env: guardEnv({ ...process.env, ...inv.env, PWD: process.cwd(), PITROOM_ACTIVE: "1" })
3692
4412
  });
3693
- const secs = ((Date.now() - t0) / 1e3).toFixed(1);
4413
+ const secs2 = ((Date.now() - t0) / 1e3).toFixed(1);
3694
4414
  const run = backend.parse(r.stdout ?? "");
3695
- if (run.finalText.includes("PONG")) return { level: "ok", message: `live probe (${describeTarget(target)}) answered in ${secs}s` };
4415
+ if (run.finalText.includes("PONG")) return { level: "ok", message: `live probe (${describeTarget(target)}) answered in ${secs2}s` };
3696
4416
  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}` };
4417
+ return { level: "fail", message: `live probe (${describeTarget(target)}) failed after ${secs2}s: ${why}` };
3698
4418
  }
3699
4419
  function skillChecks() {
3700
4420
  const checks = [];
@@ -3713,13 +4433,13 @@ function skillChecks() {
3713
4433
  else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
3714
4434
  }
3715
4435
  if (viaPlugin) {
3716
- const linked = installedSkills().some((i) => i.base.includes(`${path19.sep}.claude${path19.sep}`) && i.names.length);
4436
+ const linked = installedSkills().some((i) => i.base.includes(`${path21.sep}.claude${path21.sep}`) && i.names.length);
3717
4437
  checks.push(
3718
4438
  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
4439
  );
3720
4440
  }
3721
4441
  const launcher = launcherPath();
3722
- if (fs23.existsSync(launcher)) {
4442
+ if (fs25.existsSync(launcher)) {
3723
4443
  const r = spawnSync7(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
3724
4444
  checks.push(
3725
4445
  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 +4451,8 @@ function skillChecks() {
3731
4451
  }
3732
4452
  function pluginInstalled() {
3733
4453
  try {
3734
- const f = path19.join(os9.homedir(), ".claude", "plugins", "installed_plugins.json");
3735
- const plugins = JSON.parse(fs23.readFileSync(f, "utf8")).plugins ?? {};
4454
+ const f = path21.join(os9.homedir(), ".claude", "plugins", "installed_plugins.json");
4455
+ const plugins = JSON.parse(fs25.readFileSync(f, "utf8")).plugins ?? {};
3736
4456
  return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
3737
4457
  } catch {
3738
4458
  return false;
@@ -3749,24 +4469,24 @@ function superpowersActive() {
3749
4469
  for (const p of openCodePlugins()) {
3750
4470
  if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
3751
4471
  }
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);
4472
+ for (const base2 of [path21.join(os9.homedir(), ".agents", "skills"), path21.join(os9.homedir(), ".claude", "skills")]) {
4473
+ const dir = path21.join(base2, "using-superpowers");
4474
+ if (fs25.existsSync(path21.join(dir, "SKILL.md"))) found.push(dir);
3755
4475
  }
3756
4476
  return found;
3757
4477
  }
3758
4478
  function claudePlugins() {
3759
4479
  try {
3760
- const f = path19.join(os9.homedir(), ".claude", "plugins", "installed_plugins.json");
3761
- return Object.keys(JSON.parse(fs23.readFileSync(f, "utf8")).plugins ?? {});
4480
+ const f = path21.join(os9.homedir(), ".claude", "plugins", "installed_plugins.json");
4481
+ return Object.keys(JSON.parse(fs25.readFileSync(f, "utf8")).plugins ?? {});
3762
4482
  } catch {
3763
4483
  return [];
3764
4484
  }
3765
4485
  }
3766
4486
  function claudePluginEnabled(key) {
3767
4487
  try {
3768
- const f = path19.join(os9.homedir(), ".claude", "settings.json");
3769
- return JSON.parse(fs23.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
4488
+ const f = path21.join(os9.homedir(), ".claude", "settings.json");
4489
+ return JSON.parse(fs25.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
3770
4490
  } catch {
3771
4491
  return true;
3772
4492
  }
@@ -3775,7 +4495,7 @@ function codexPlugins() {
3775
4495
  const plugins = /* @__PURE__ */ new Map();
3776
4496
  let text;
3777
4497
  try {
3778
- text = fs23.readFileSync(path19.join(process.env.CODEX_HOME ?? path19.join(os9.homedir(), ".codex"), "config.toml"), "utf8");
4498
+ text = fs25.readFileSync(path21.join(process.env.CODEX_HOME ?? path21.join(os9.homedir(), ".codex"), "config.toml"), "utf8");
3779
4499
  } catch {
3780
4500
  return plugins;
3781
4501
  }
@@ -3793,11 +4513,11 @@ function codexPlugins() {
3793
4513
  return plugins;
3794
4514
  }
3795
4515
  function openCodePlugins() {
3796
- const dir = path19.join(process.env.XDG_CONFIG_HOME ?? path19.join(os9.homedir(), ".config"), "opencode");
4516
+ const dir = path21.join(process.env.XDG_CONFIG_HOME ?? path21.join(os9.homedir(), ".config"), "opencode");
3797
4517
  const plugins = [];
3798
4518
  for (const name of ["opencode.json", "opencode.jsonc"]) {
3799
4519
  try {
3800
- const text = fs23.readFileSync(path19.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
4520
+ const text = fs25.readFileSync(path21.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
3801
4521
  const list2 = JSON.parse(text).plugin;
3802
4522
  if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
3803
4523
  } catch {
@@ -3805,7 +4525,7 @@ function openCodePlugins() {
3805
4525
  }
3806
4526
  for (const folder of ["plugin", "plugins"]) {
3807
4527
  try {
3808
- for (const f of fs23.readdirSync(path19.join(dir, folder))) plugins.push(path19.join(dir, folder, f));
4528
+ for (const f of fs25.readdirSync(path21.join(dir, folder))) plugins.push(path21.join(dir, folder, f));
3809
4529
  } catch {
3810
4530
  }
3811
4531
  }
@@ -3813,6 +4533,7 @@ function openCodePlugins() {
3813
4533
  }
3814
4534
 
3815
4535
  // src/cli.ts
4536
+ useArchive({ onFinished: recordRun, meta: (id) => archivedRun(id)?.meta, id: archivedId });
3816
4537
  var HELP = `pitroom ${VERSION2} \u2014 a free pit crew for your expensive coding agent.
3817
4538
  Delegates bounded tasks to a worker agent CLI (${backendIds().join(", ")}), using the
3818
4539
  worker's own default model, and returns a compact answer, exact changes and a receipt.
@@ -3829,7 +4550,14 @@ Usage
3829
4550
  pitroom status [run | -g NAME] state / live progress (default: latest run)
3830
4551
  pitroom wait [run\u2026 | -g NAME] [--any] [--brief] [--timeout 540]
3831
4552
  block until all (or any) are done, then print reports
3832
- pitroom watch [run\u2026 | -g NAME] [--json] [--interval 2]
4553
+ pitroom watch [run\u2026 | -g NAME] [--json | --brief] [--interval 2]
4554
+ --brief: one card line per start and end (made for Claude Code's Monitor tool)
4555
+ pitroom history [TEXT] [--model M] [--state S] [--since 30d] [--limit N] [--json]
4556
+ every finished run, searchable (SQLite); "history stats" per worker and model,
4557
+ "history import" takes in runs from before the history existed
4558
+ pitroom dash [--detach] [--port N] [--open] [--stop]
4559
+ a live page of the runs on 127.0.0.1 (read-only): open it in any browser
4560
+ or an agent app's browser pane; --detach runs it in the background
3833
4561
  live table (TTY) or one JSON line per change, until done
3834
4562
  pitroom show [run] [--patch|--events|--full|--json]
3835
4563
  pitroom ls [--running] [-g NAME] recent runs
@@ -3892,6 +4620,8 @@ var COMMANDS = {
3892
4620
  status: cmdStatus,
3893
4621
  wait: cmdWait,
3894
4622
  watch: cmdWatch,
4623
+ dash: cmdDash,
4624
+ history: cmdHistory,
3895
4625
  show: cmdShow,
3896
4626
  ls: cmdLs,
3897
4627
  list: cmdLs,