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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +23 -0
- package/README.md +84 -7
- package/THIRD_PARTY_NOTICES.md +25 -0
- package/dist/lib.mjs +31 -3
- package/dist/pitroom.mjs +1156 -426
- package/dist/ui/app.css +2 -0
- package/dist/ui/app.js +20 -0
- package/docs/backends.md +1 -1
- package/hooks/session-start.mjs +3 -2
- package/package.json +17 -3
- package/skills/pitroom-crew/SKILL.md +2 -1
- package/skills/using-pitroom/SKILL.md +2 -1
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
|
|
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) -
|
|
375
|
-
usage2.cacheRead +=
|
|
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
|
|
1125
|
-
return `${
|
|
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
|
-
|
|
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 (!
|
|
1154
|
-
|
|
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
|
-
|
|
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
|
|
1336
|
-
import
|
|
1394
|
+
import fs24 from "node:fs";
|
|
1395
|
+
import path20 from "node:path";
|
|
1337
1396
|
|
|
1338
|
-
// src/core/
|
|
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/
|
|
1342
|
-
import
|
|
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
|
|
1415
|
-
import
|
|
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
|
-
|
|
1452
|
-
|
|
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 (!
|
|
1457
|
-
return
|
|
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) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[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
|
|
1486
|
-
const
|
|
1487
|
-
|
|
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
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
.
|
|
1496
|
-
.
|
|
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
|
|
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
|
|
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 ${
|
|
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 (!
|
|
1608
|
-
const p = getBackend((meta.ran ?? meta.worker).backend).parse(
|
|
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
|
|
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
|
|
2203
|
+
import fs13 from "node:fs";
|
|
1734
2204
|
import os7 from "node:os";
|
|
1735
|
-
import
|
|
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
|
|
2209
|
+
return path10.resolve(path10.dirname(fileURLToPath(import.meta.url)), "..");
|
|
1740
2210
|
}
|
|
1741
2211
|
function skillNames(root = packageRoot()) {
|
|
1742
|
-
const dir =
|
|
1743
|
-
if (!
|
|
1744
|
-
return
|
|
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 = [
|
|
1749
|
-
if (
|
|
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 = () =>
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1779
|
-
const exists =
|
|
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
|
-
|
|
2252
|
+
fs13.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
1783
2253
|
} else if (exists) {
|
|
1784
|
-
|
|
2254
|
+
fs13.rmSync(dest, { force: true });
|
|
1785
2255
|
}
|
|
1786
|
-
|
|
1787
|
-
return `\u2714 ${dest} \u2192 launcher for ${bundle} (Node
|
|
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 =
|
|
2260
|
+
const st = fs13.lstatSync(link, { throwIfNoEntry: false });
|
|
1791
2261
|
if (!st?.isSymbolicLink()) return false;
|
|
1792
|
-
const target =
|
|
1793
|
-
const rel =
|
|
1794
|
-
return !rel.startsWith("..") && !
|
|
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
|
-
|
|
1798
|
-
const st =
|
|
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
|
-
|
|
2271
|
+
fs13.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
1802
2272
|
} else if (st) {
|
|
1803
|
-
|
|
2273
|
+
fs13.unlinkSync(dest);
|
|
1804
2274
|
}
|
|
1805
|
-
if (opts.copy)
|
|
1806
|
-
else
|
|
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 ${
|
|
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 =
|
|
2286
|
+
const l = path10.join(base2, legacy);
|
|
1817
2287
|
if (!skills.includes(legacy) && linksInto(l, root)) {
|
|
1818
|
-
|
|
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(
|
|
2292
|
+
for (const name of skills) out.push(place(path10.join(root, "skills", name), path10.join(base2, name), opts));
|
|
1823
2293
|
}
|
|
1824
|
-
const bundle =
|
|
1825
|
-
if (
|
|
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(
|
|
1828
|
-
if (!onPath) out.push(`! ${
|
|
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 (!
|
|
1837
|
-
for (const name of
|
|
1838
|
-
const l =
|
|
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
|
-
|
|
2310
|
+
fs13.unlinkSync(l);
|
|
1841
2311
|
out.push(`\u2714 removed ${l}`);
|
|
1842
2312
|
}
|
|
1843
2313
|
}
|
|
1844
2314
|
}
|
|
1845
2315
|
if (isOurLauncher(launcherPath(), root)) {
|
|
1846
|
-
|
|
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) =>
|
|
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
|
|
1859
|
-
import
|
|
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
|
|
2332
|
+
return path11.join(home(), "plans", id, "notes.md");
|
|
1863
2333
|
}
|
|
1864
2334
|
function readNotes(plan) {
|
|
1865
2335
|
const f = notesFile(plan);
|
|
1866
|
-
if (!
|
|
1867
|
-
return
|
|
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
|
-
|
|
1876
|
-
if (!
|
|
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
|
-
|
|
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
|
|
1948
|
-
import
|
|
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
|
|
2423
|
+
import fs15 from "node:fs";
|
|
1954
2424
|
import os8 from "node:os";
|
|
1955
|
-
import
|
|
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 ?
|
|
2457
|
+
return r.code === 0 ? path12.resolve(r.stdout.trim()) : void 0;
|
|
1988
2458
|
}
|
|
1989
2459
|
function snapshotTree(root, exclude = []) {
|
|
1990
|
-
const tmp =
|
|
1991
|
-
const real =
|
|
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 (
|
|
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
|
-
|
|
2000
|
-
|
|
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 =
|
|
2021
|
-
|
|
2022
|
-
must(
|
|
2023
|
-
|
|
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 =
|
|
2030
|
-
if (!rel || rel.startsWith("..") ||
|
|
2031
|
-
|
|
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 =
|
|
2037
|
-
const dst =
|
|
2038
|
-
if (!
|
|
2039
|
-
|
|
2040
|
-
|
|
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
|
|
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 &&
|
|
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
|
|
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 &&
|
|
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 =
|
|
2202
|
-
|
|
2203
|
-
|
|
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
|
|
2209
|
-
import
|
|
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 =
|
|
2218
|
-
if (!
|
|
2219
|
-
return
|
|
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/
|
|
2324
|
-
import
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
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
|
-
|
|
2350
|
-
|
|
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
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
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
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
const
|
|
2365
|
-
return
|
|
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
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
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
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
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
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
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
|
|
2387
|
-
const
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
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
|
-
|
|
2408
|
-
|
|
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
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
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
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
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
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
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
|
-
|
|
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
|
|
2448
|
-
import
|
|
2449
|
-
import
|
|
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
|
|
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
|
|
2505
|
-
import
|
|
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
|
|
3246
|
+
return path16.join(home(), "shim", `v${VERSION}`);
|
|
2581
3247
|
}
|
|
2582
3248
|
function findRealGit(skip) {
|
|
2583
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
2584
|
-
if (
|
|
2585
|
-
const p =
|
|
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
|
-
|
|
2588
|
-
if (
|
|
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 = () =>
|
|
3267
|
+
var hooksDir = () => path16.join(home(), "git-hooks", `v${HOOK_VERSION}`);
|
|
2602
3268
|
function writeIfChanged(file, content) {
|
|
2603
|
-
if (
|
|
2604
|
-
|
|
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
|
-
|
|
2607
|
-
|
|
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(
|
|
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(
|
|
2638
|
-
return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${
|
|
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 =
|
|
2644
|
-
const err =
|
|
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 =
|
|
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
|
-
|
|
2677
|
-
|
|
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
|
|
2703
|
-
import
|
|
2704
|
-
import
|
|
2705
|
-
var slotsDir = () =>
|
|
2706
|
-
var locksDir = () =>
|
|
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
|
-
|
|
3386
|
+
fs21.mkdirSync(path17.dirname(file), { recursive: true });
|
|
2721
3387
|
try {
|
|
2722
|
-
|
|
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 =
|
|
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
|
-
|
|
3400
|
+
fs21.rmSync(file, { force: true });
|
|
2735
3401
|
try {
|
|
2736
|
-
|
|
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 (
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
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
|
|
2811
|
-
import
|
|
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) =>
|
|
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 =
|
|
2863
|
-
const lines = size > MAX_BYTES ? null :
|
|
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:
|
|
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 =
|
|
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
|
|
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 =
|
|
2911
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
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.
|
|
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 || !
|
|
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 =
|
|
2969
|
-
if (!
|
|
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) =>
|
|
2976
|
-
for (const f of files) if (!
|
|
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
|
-
|
|
3677
|
+
fs23.rmSync(runDir(meta.id), { recursive: true, force: true });
|
|
3012
3678
|
throw e;
|
|
3013
3679
|
}
|
|
3014
3680
|
}
|
|
3015
|
-
|
|
3681
|
+
fs23.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
|
|
3016
3682
|
`);
|
|
3017
|
-
if (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 =
|
|
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 (
|
|
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 =
|
|
3776
|
+
meta.worktree = path19.join(worktreesDir(), meta.id);
|
|
3111
3777
|
createIsolatedCopy(root, meta.baseTree, meta.worktree);
|
|
3112
|
-
meta.cwd =
|
|
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) =>
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
|
|
3974
|
+
fs24.rmSync(packageFile, { force: true });
|
|
3309
3975
|
throw e;
|
|
3310
3976
|
}
|
|
3311
|
-
|
|
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 ?
|
|
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
|
|
3412
|
-
process.stdout.write(
|
|
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 ? "" :
|
|
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
|
-
|
|
3546
|
-
console.log(`card written to ${
|
|
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(`})`);
|
|
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()}${
|
|
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
|
-
|
|
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
|
|
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
|
|
4319
|
+
import fs25 from "node:fs";
|
|
3600
4320
|
import os9 from "node:os";
|
|
3601
|
-
import
|
|
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()}${
|
|
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
|
|
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 ${
|
|
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 ${
|
|
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(`${
|
|
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 (
|
|
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 =
|
|
3735
|
-
const plugins = JSON.parse(
|
|
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 [
|
|
3753
|
-
const dir =
|
|
3754
|
-
if (
|
|
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 =
|
|
3761
|
-
return Object.keys(JSON.parse(
|
|
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 =
|
|
3769
|
-
return JSON.parse(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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,
|