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