pitroom 0.6.10 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -1
- package/CHANGELOG.md +22 -0
- package/README.md +3 -3
- package/bin/pitroom.cjs +68 -0
- package/dist/pitroom.mjs +493 -259
- package/dist/ui/app.css +1 -1
- package/dist/ui/app.js +11 -11
- package/hooks/codex-hooks.json +28 -0
- package/package.json +3 -2
package/dist/pitroom.mjs
CHANGED
|
@@ -1220,10 +1220,13 @@ function isAlive(pid) {
|
|
|
1220
1220
|
function freshMeta(id) {
|
|
1221
1221
|
const meta = readMeta(id);
|
|
1222
1222
|
if (!TERMINAL.includes(meta.state) && meta.pid && !isAlive(meta.pid)) {
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1223
|
+
const latest = readMeta(id);
|
|
1224
|
+
if (TERMINAL.includes(latest.state)) return latest;
|
|
1225
|
+
latest.state = "failed";
|
|
1226
|
+
latest.error ??= "worker process exited unexpectedly";
|
|
1227
|
+
latest.endedAt ??= (/* @__PURE__ */ new Date()).toISOString();
|
|
1228
|
+
writeMeta(latest);
|
|
1229
|
+
return latest;
|
|
1227
1230
|
}
|
|
1228
1231
|
return meta;
|
|
1229
1232
|
}
|
|
@@ -1260,7 +1263,8 @@ var VALUE_FLAGS = {
|
|
|
1260
1263
|
"--state": "state",
|
|
1261
1264
|
"--limit": "limit",
|
|
1262
1265
|
"--plan": "plan",
|
|
1263
|
-
"--step": "step"
|
|
1266
|
+
"--step": "step",
|
|
1267
|
+
"--fallback": "fallback"
|
|
1264
1268
|
};
|
|
1265
1269
|
var BOOL_FLAGS = {
|
|
1266
1270
|
"-r": "read",
|
|
@@ -1391,21 +1395,140 @@ function effortFlag(p) {
|
|
|
1391
1395
|
|
|
1392
1396
|
// src/cli/commands.ts
|
|
1393
1397
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
1394
|
-
import
|
|
1395
|
-
import
|
|
1398
|
+
import fs26 from "node:fs";
|
|
1399
|
+
import path22 from "node:path";
|
|
1400
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1401
|
+
|
|
1402
|
+
// src/core/init.ts
|
|
1403
|
+
import fs8 from "node:fs";
|
|
1404
|
+
import path7 from "node:path";
|
|
1405
|
+
|
|
1406
|
+
// src/core/style.ts
|
|
1407
|
+
var enabled = () => {
|
|
1408
|
+
if (process.env.NO_COLOR) return false;
|
|
1409
|
+
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") return true;
|
|
1410
|
+
return !!process.stdout.isTTY && process.env.TERM !== "dumb";
|
|
1411
|
+
};
|
|
1412
|
+
var wrap = (open, close) => (text) => enabled() ? `\x1B[${open}m${text}\x1B[${close}m` : text;
|
|
1413
|
+
var bold = wrap(1, 22);
|
|
1414
|
+
var dim = wrap(2, 22);
|
|
1415
|
+
var red = wrap(31, 39);
|
|
1416
|
+
var green = wrap(32, 39);
|
|
1417
|
+
var yellow = wrap(33, 39);
|
|
1418
|
+
var blue = wrap(34, 39);
|
|
1419
|
+
var cyan = wrap(36, 39);
|
|
1420
|
+
function stateColour(state, text = state) {
|
|
1421
|
+
if (state === "done") return green(text);
|
|
1422
|
+
if (state === "running") return blue(text);
|
|
1423
|
+
if (state === "queued") return yellow(text);
|
|
1424
|
+
if (state === "failed") return red(text);
|
|
1425
|
+
if (state === "timeout") return yellow(text);
|
|
1426
|
+
return dim(text);
|
|
1427
|
+
}
|
|
1428
|
+
function wrapText(text, indent) {
|
|
1429
|
+
const columns = process.stdout.isTTY ? process.stdout.columns : void 0;
|
|
1430
|
+
if (!columns || text.length + indent <= columns) return text;
|
|
1431
|
+
const width = Math.max(40, Math.min(columns, 110) - indent);
|
|
1432
|
+
const lines = [];
|
|
1433
|
+
let line = "";
|
|
1434
|
+
for (const word of text.split(" ")) {
|
|
1435
|
+
if (line && line.length + 1 + word.length > width) {
|
|
1436
|
+
lines.push(line);
|
|
1437
|
+
line = word;
|
|
1438
|
+
} else {
|
|
1439
|
+
line = line ? `${line} ${word}` : word;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
if (line) lines.push(line);
|
|
1443
|
+
return lines.join(`
|
|
1444
|
+
${" ".repeat(indent)}`);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// src/core/init.ts
|
|
1448
|
+
var FREE = /-free$/;
|
|
1449
|
+
var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
|
|
1450
|
+
var isFound = (binary4) => path7.isAbsolute(binary4) && fs8.existsSync(binary4);
|
|
1451
|
+
function planInit(opts = {}) {
|
|
1452
|
+
const file = configPath();
|
|
1453
|
+
const workers = backendIds().map((id) => {
|
|
1454
|
+
const b = getBackend(id);
|
|
1455
|
+
const binary4 = b.binary();
|
|
1456
|
+
return { id, name: b.name, found: isFound(binary4), binary: binary4 };
|
|
1457
|
+
});
|
|
1458
|
+
const notes = [];
|
|
1459
|
+
const config = {};
|
|
1460
|
+
let blocked;
|
|
1461
|
+
let opencode2;
|
|
1462
|
+
if (workers.find((w) => w.id === "opencode")?.found) {
|
|
1463
|
+
const b = getBackend("opencode");
|
|
1464
|
+
let models = [];
|
|
1465
|
+
try {
|
|
1466
|
+
models = b.listModels?.() ?? [];
|
|
1467
|
+
} catch {
|
|
1468
|
+
notes.push("could not list OpenCode models (is it logged in?)");
|
|
1469
|
+
}
|
|
1470
|
+
const defaultModel3 = b.defaultModel?.();
|
|
1471
|
+
const chosen = opts.model ?? defaultModel3;
|
|
1472
|
+
const free = models.filter((m) => FREE.test(m) && m !== chosen);
|
|
1473
|
+
opencode2 = { defaultModel: defaultModel3, models: models.length, free: free.slice(0, 6) };
|
|
1474
|
+
if (opts.model) {
|
|
1475
|
+
if (models.length && !models.includes(opts.model)) throw new UserError(`"${opts.model}" is not in \`opencode models\``);
|
|
1476
|
+
if (!models.length) notes.push(`could not check "${opts.model}": \`opencode models\` listed nothing`);
|
|
1477
|
+
config.models = { opencode: opts.model };
|
|
1478
|
+
} else if (!defaultModel3) {
|
|
1479
|
+
blocked = `OpenCode has no default model: choose one with --model <id>${free.length ? ` (free models you have: ${free.slice(0, 4).join(", ")})` : ""}`;
|
|
1480
|
+
}
|
|
1481
|
+
const fallback = opts.fallback ?? free.slice(0, 2);
|
|
1482
|
+
for (const m of fallback) {
|
|
1483
|
+
if (models.length && !models.includes(m)) throw new UserError(`"${m}" is not in \`opencode models\``);
|
|
1484
|
+
}
|
|
1485
|
+
if (fallback.length) {
|
|
1486
|
+
config.fallback = fallback.map((m) => `opencode:${m}`);
|
|
1487
|
+
if (!opts.fallback) notes.push(`fallback suggested from the free models in your catalogue: ${fallback.join(", ")} (change it with --fallback a,b)`);
|
|
1488
|
+
} else {
|
|
1489
|
+
notes.push("no fallback: no free OpenCode model found to suggest (pass --fallback a,b to name some)");
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
const tiers = Object.fromEntries(TIER_ORDER.filter(([, id]) => workers.find((w) => w.id === id)?.found));
|
|
1493
|
+
if (Object.keys(tiers).length > 1) config.tiers = tiers;
|
|
1494
|
+
if (!workers.some((w) => w.found)) blocked = "no worker CLI found: install OpenCode (https://opencode.ai), Codex CLI or Claude Code first";
|
|
1495
|
+
else if (!blocked && !Object.keys(config).length) notes.push("nothing to propose: the default model and the single worker need no config");
|
|
1496
|
+
return { path: file, exists: fs8.existsSync(file), workers, opencode: opencode2, config, blocked, notes };
|
|
1497
|
+
}
|
|
1498
|
+
function writeInit(plan, force) {
|
|
1499
|
+
if (plan.blocked) throw new UserError(plan.blocked);
|
|
1500
|
+
if (!Object.keys(plan.config).length) throw new UserError("nothing to write: the proposal is empty");
|
|
1501
|
+
if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${path7.basename(plan.path)}.bak)`);
|
|
1502
|
+
fs8.mkdirSync(path7.dirname(plan.path), { recursive: true });
|
|
1503
|
+
if (plan.exists) fs8.copyFileSync(plan.path, `${plan.path}.bak`);
|
|
1504
|
+
fs8.writeFileSync(plan.path, `${JSON.stringify(plan.config, null, 2)}
|
|
1505
|
+
`);
|
|
1506
|
+
return plan.path;
|
|
1507
|
+
}
|
|
1508
|
+
function formatInit(plan, written) {
|
|
1509
|
+
const out = [bold("pitroom init") + dim(": worker CLIs on this machine")];
|
|
1510
|
+
for (const w of plan.workers) out.push(` ${w.found ? green("\u2714") : dim("\xB7")} ${w.name.padEnd(12)} ${w.found ? w.binary : dim("not found")}`);
|
|
1511
|
+
if (plan.opencode) {
|
|
1512
|
+
const o = plan.opencode;
|
|
1513
|
+
out.push(` OpenCode: ${o.models} models, default ${o.defaultModel ?? "none"}${o.free.length ? `, free: ${o.free.slice(0, 3).join(", ")}${o.free.length > 3 ? ", \u2026" : ""}` : ""}`);
|
|
1514
|
+
}
|
|
1515
|
+
out.push("", `config file: ${plan.path}${plan.exists ? " (exists)" : " (not present)"}`, JSON.stringify(plan.config, null, 2));
|
|
1516
|
+
for (const n of plan.notes) out.push(` note: ${n}`);
|
|
1517
|
+
if (written) out.push("", green(`\u2714 written to ${written}`), dim(" next: pitroom doctor, then pitroom install"));
|
|
1518
|
+
else if (plan.blocked) out.push("", yellow(`! ${plan.blocked}`));
|
|
1519
|
+
else out.push("", `nothing written yet: pass --yes to write it${plan.exists ? " (with --force, since the file exists)" : ""}`);
|
|
1520
|
+
return out.join("\n");
|
|
1521
|
+
}
|
|
1396
1522
|
|
|
1397
1523
|
// src/core/history.ts
|
|
1398
|
-
import
|
|
1524
|
+
import fs11 from "node:fs";
|
|
1399
1525
|
import { createRequire } from "node:module";
|
|
1400
|
-
import
|
|
1526
|
+
import path9 from "node:path";
|
|
1401
1527
|
import zlib from "node:zlib";
|
|
1402
1528
|
|
|
1403
|
-
// src/core/ui.ts
|
|
1404
|
-
import fs9 from "node:fs";
|
|
1405
|
-
|
|
1406
1529
|
// src/core/receipt.ts
|
|
1407
|
-
import
|
|
1408
|
-
import
|
|
1530
|
+
import fs9 from "node:fs";
|
|
1531
|
+
import path8 from "node:path";
|
|
1409
1532
|
var PRESETS = {
|
|
1410
1533
|
sonnet: { name: "Claude Sonnet", input: 3, output: 15, cachedInput: 0.3 },
|
|
1411
1534
|
opus: { name: "Claude Opus", input: 5, output: 25, cachedInput: 0.5 },
|
|
@@ -1441,13 +1564,13 @@ function record(meta) {
|
|
|
1441
1564
|
saved: meta.savedUsd ?? 0,
|
|
1442
1565
|
price: primaryPrice().name
|
|
1443
1566
|
};
|
|
1444
|
-
|
|
1445
|
-
|
|
1567
|
+
fs9.mkdirSync(path8.dirname(ledgerFile()), { recursive: true });
|
|
1568
|
+
fs9.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
|
|
1446
1569
|
`);
|
|
1447
1570
|
}
|
|
1448
1571
|
function readLedger(sinceMs2) {
|
|
1449
|
-
if (!
|
|
1450
|
-
return
|
|
1572
|
+
if (!fs9.existsSync(ledgerFile())) return [];
|
|
1573
|
+
return fs9.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
|
|
1451
1574
|
try {
|
|
1452
1575
|
return [JSON.parse(l)];
|
|
1453
1576
|
} catch {
|
|
@@ -1522,6 +1645,7 @@ ${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.rou
|
|
|
1522
1645
|
}
|
|
1523
1646
|
|
|
1524
1647
|
// src/core/ui.ts
|
|
1648
|
+
import fs10 from "node:fs";
|
|
1525
1649
|
var WEEK_MS = 7 * 24 * 3600 * 1e3;
|
|
1526
1650
|
var RECENT = 40;
|
|
1527
1651
|
var RECENT_HOURS = 1;
|
|
@@ -1570,6 +1694,10 @@ function workerName(t) {
|
|
|
1570
1694
|
const model = (t.model.split("/").pop() ?? t.model).replace(/-(contributor-)?free$/, "").replace(/-\d+\.\d+$/, "");
|
|
1571
1695
|
return `${t.backend} (${model.length > MODEL_MAX ? `${model.slice(0, MODEL_MAX - 1)}\u2026` : model})`;
|
|
1572
1696
|
}
|
|
1697
|
+
function ranTarget(m) {
|
|
1698
|
+
const t = m.ran ?? m.worker;
|
|
1699
|
+
return t.model || !m.resolvedModel ? t : { ...t, model: m.resolvedModel };
|
|
1700
|
+
}
|
|
1573
1701
|
function what(m) {
|
|
1574
1702
|
if (m.plan) return short(`Task ${m.plan.step}: ${m.plan.title}`);
|
|
1575
1703
|
if (m.reviewOf) return `of ${m.reviewOf.replace(/\b([0-9a-f]{9})[0-9a-f]{31}\b/g, "$1")}`;
|
|
@@ -1587,7 +1715,7 @@ var startedCard = (m) => `\u{1F3C1} Pitroom \u25B6 ${kind(m)} on ${workerName(m.
|
|
|
1587
1715
|
function endedCard(m) {
|
|
1588
1716
|
const v = m.verdict;
|
|
1589
1717
|
const bits = [
|
|
1590
|
-
`\u{1F3C1} Pitroom ${ICON[m.state] ?? "\u2022"} ${kind(m)} ${m.state} on ${workerName(m
|
|
1718
|
+
`\u{1F3C1} Pitroom ${ICON[m.state] ?? "\u2022"} ${kind(m)} ${m.state} on ${workerName(ranTarget(m))}`,
|
|
1591
1719
|
what(m),
|
|
1592
1720
|
elapsed(m),
|
|
1593
1721
|
v && `SPEC ${v.spec.toUpperCase()} \xB7 QUALITY ${v.quality.toUpperCase()}`,
|
|
@@ -1605,9 +1733,9 @@ function cardsFor(m) {
|
|
|
1605
1733
|
const out = [];
|
|
1606
1734
|
for (const [phase, text] of phases) {
|
|
1607
1735
|
const marker = runFile(m.id, `card-${phase}`);
|
|
1608
|
-
if (
|
|
1736
|
+
if (fs10.existsSync(marker)) continue;
|
|
1609
1737
|
if (!(phase === "started" && !isActive(m.state))) out.push(text());
|
|
1610
|
-
|
|
1738
|
+
fs10.writeFileSync(marker, "");
|
|
1611
1739
|
}
|
|
1612
1740
|
return out;
|
|
1613
1741
|
}
|
|
@@ -1618,10 +1746,10 @@ function hookCards(input) {
|
|
|
1618
1746
|
const response = event.tool_response;
|
|
1619
1747
|
const output = typeof response === "string" ? response : [response?.stdout, response?.stderr].filter(Boolean).join("\n");
|
|
1620
1748
|
const ids = [...new Set(`${command}
|
|
1621
|
-
${output}`.match(RUN_ID) ?? [])].filter((id) =>
|
|
1749
|
+
${output}`.match(RUN_ID) ?? [])].filter((id) => fs10.existsSync(`${runsDir()}/${id}`));
|
|
1622
1750
|
const since = Date.now() - RECENT_HOURS * 36e5;
|
|
1623
1751
|
for (const id of listRunIds().slice(-RECENT)) {
|
|
1624
|
-
if (ids.includes(id) ||
|
|
1752
|
+
if (ids.includes(id) || fs10.existsSync(runFile(id, "card-ended"))) continue;
|
|
1625
1753
|
try {
|
|
1626
1754
|
const m = readMeta(id);
|
|
1627
1755
|
if (!isActive(m.state) && Date.parse(m.endedAt ?? m.startedAt) > since) ids.push(id);
|
|
@@ -1643,7 +1771,7 @@ var SCHEMA_VERSION = 1;
|
|
|
1643
1771
|
var PATCH_MAX = 1e6;
|
|
1644
1772
|
var ANSWER_MAX = 2e5;
|
|
1645
1773
|
var cached2;
|
|
1646
|
-
var historyFile = () =>
|
|
1774
|
+
var historyFile = () => path9.join(home(), "history.db");
|
|
1647
1775
|
function openDb() {
|
|
1648
1776
|
const file = historyFile();
|
|
1649
1777
|
if (cached2?.file === file) return cached2.db;
|
|
@@ -1657,7 +1785,7 @@ function openDb() {
|
|
|
1657
1785
|
} finally {
|
|
1658
1786
|
process.emitWarning = emit;
|
|
1659
1787
|
}
|
|
1660
|
-
|
|
1788
|
+
fs11.mkdirSync(path9.dirname(file), { recursive: true });
|
|
1661
1789
|
db = new DatabaseSync(file);
|
|
1662
1790
|
db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
|
|
1663
1791
|
migrate(db);
|
|
@@ -1692,31 +1820,31 @@ function migrate(db) {
|
|
|
1692
1820
|
function readRunFile(id, name) {
|
|
1693
1821
|
const f = runFile(id, name);
|
|
1694
1822
|
try {
|
|
1695
|
-
if (
|
|
1696
|
-
if (
|
|
1823
|
+
if (fs11.existsSync(f)) return fs11.readFileSync(f, "utf8");
|
|
1824
|
+
if (fs11.existsSync(`${f}.gz`)) return zlib.gunzipSync(fs11.readFileSync(`${f}.gz`)).toString("utf8");
|
|
1697
1825
|
} catch {
|
|
1698
1826
|
}
|
|
1699
1827
|
return void 0;
|
|
1700
1828
|
}
|
|
1701
1829
|
function gzipFile(f) {
|
|
1702
|
-
if (!
|
|
1703
|
-
|
|
1704
|
-
|
|
1830
|
+
if (!fs11.existsSync(f)) return;
|
|
1831
|
+
fs11.writeFileSync(`${f}.gz`, zlib.gzipSync(fs11.readFileSync(f)));
|
|
1832
|
+
fs11.rmSync(f);
|
|
1705
1833
|
}
|
|
1706
1834
|
function compactRun(meta) {
|
|
1707
1835
|
if (!TERMINAL.includes(meta.state)) return;
|
|
1708
|
-
const dir =
|
|
1836
|
+
const dir = path9.dirname(runFile(meta.id, "meta.json"));
|
|
1709
1837
|
let names;
|
|
1710
1838
|
try {
|
|
1711
|
-
names =
|
|
1839
|
+
names = fs11.readdirSync(dir);
|
|
1712
1840
|
} catch {
|
|
1713
1841
|
return;
|
|
1714
1842
|
}
|
|
1715
1843
|
for (const n of names) {
|
|
1716
|
-
if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(
|
|
1844
|
+
if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(path9.join(dir, n));
|
|
1717
1845
|
else if (/^stderr(\.attempt-\d+)?\.log$/.test(n)) {
|
|
1718
|
-
if (meta.state === "done" && !/attempt/.test(n))
|
|
1719
|
-
else gzipFile(
|
|
1846
|
+
if (meta.state === "done" && !/attempt/.test(n)) fs11.rmSync(path9.join(dir, n), { force: true });
|
|
1847
|
+
else gzipFile(path9.join(dir, n));
|
|
1720
1848
|
}
|
|
1721
1849
|
}
|
|
1722
1850
|
}
|
|
@@ -1896,6 +2024,21 @@ function listHistory(q = {}) {
|
|
|
1896
2024
|
return { rows: [], total: 0 };
|
|
1897
2025
|
}
|
|
1898
2026
|
}
|
|
2027
|
+
function workerRows(rows, ledger) {
|
|
2028
|
+
const out = rows.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: 0 }));
|
|
2029
|
+
const key = (backend, model) => `${backend ?? ""}\0${model ?? ""}`;
|
|
2030
|
+
const index = new Map(out.map((r) => [key(r.backend, r.model), r]));
|
|
2031
|
+
for (const e of ledger) {
|
|
2032
|
+
let row = index.get(key(e.backend, e.model));
|
|
2033
|
+
if (!row) {
|
|
2034
|
+
row = { backend: e.backend ?? "unknown", model: e.model, runs: 0, ok: 0, avgSeconds: null, avgTokens: null, saved: 0 };
|
|
2035
|
+
index.set(key(e.backend, e.model), row);
|
|
2036
|
+
out.push(row);
|
|
2037
|
+
}
|
|
2038
|
+
row.saved += e.saved;
|
|
2039
|
+
}
|
|
2040
|
+
return out;
|
|
2041
|
+
}
|
|
1899
2042
|
function historyStats(sinceMs2) {
|
|
1900
2043
|
const empty = { totals: { runs: 0, ok: 0, failed: 0, seconds: 0, tokens: 0, saved: 0 }, byWorker: [], byDay: [] };
|
|
1901
2044
|
const db = openDb();
|
|
@@ -1905,10 +2048,13 @@ function historyStats(sinceMs2) {
|
|
|
1905
2048
|
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
2049
|
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
2050
|
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);
|
|
2051
|
+
const ledger = readLedger(sinceMs2);
|
|
2052
|
+
const byDay = /* @__PURE__ */ new Map();
|
|
2053
|
+
for (const e of ledger) byDay.set(e.at.slice(0, 10), (byDay.get(e.at.slice(0, 10)) ?? 0) + e.saved);
|
|
1908
2054
|
return {
|
|
1909
|
-
totals: { runs: t.runs, ok: t.ok, failed: t.runs - t.ok, seconds: t.seconds, tokens: t.tokens, saved:
|
|
1910
|
-
byWorker: w
|
|
1911
|
-
byDay: d.reverse().map((r) => ({ day: r.day, runs: r.runs, ok: r.ok ?? 0, saved: r.
|
|
2055
|
+
totals: { runs: t.runs, ok: t.ok, failed: t.runs - t.ok, seconds: t.seconds, tokens: t.tokens, saved: totals(ledger).saved },
|
|
2056
|
+
byWorker: workerRows(w, ledger),
|
|
2057
|
+
byDay: d.reverse().map((r) => ({ day: r.day, runs: r.runs, ok: r.ok ?? 0, saved: byDay.get(r.day) ?? 0 }))
|
|
1912
2058
|
};
|
|
1913
2059
|
} catch {
|
|
1914
2060
|
return empty;
|
|
@@ -1916,11 +2062,11 @@ function historyStats(sinceMs2) {
|
|
|
1916
2062
|
}
|
|
1917
2063
|
|
|
1918
2064
|
// src/core/report.ts
|
|
1919
|
-
import
|
|
2065
|
+
import fs13 from "node:fs";
|
|
1920
2066
|
|
|
1921
2067
|
// src/core/plan.ts
|
|
1922
|
-
import
|
|
1923
|
-
import
|
|
2068
|
+
import fs12 from "node:fs";
|
|
2069
|
+
import path10 from "node:path";
|
|
1924
2070
|
var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
1925
2071
|
var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
|
|
1926
2072
|
function structure(lines) {
|
|
@@ -1967,9 +2113,9 @@ function parsePlan(text, file = "") {
|
|
|
1967
2113
|
return { file, title: titleHeading?.text ?? "", header, constraints, tasks };
|
|
1968
2114
|
}
|
|
1969
2115
|
function loadPlan(file) {
|
|
1970
|
-
const abs =
|
|
1971
|
-
if (!
|
|
1972
|
-
const plan = parsePlan(
|
|
2116
|
+
const abs = path10.resolve(file);
|
|
2117
|
+
if (!fs12.existsSync(abs) || !fs12.statSync(abs).isFile()) throw new UserError(`plan not found: ${file}`);
|
|
2118
|
+
const plan = parsePlan(fs12.readFileSync(abs, "utf8"), fs12.realpathSync(abs));
|
|
1973
2119
|
if (!plan.tasks.length) throw new UserError(`${file} has no "Task N" headings (see pitroom-writing-plans)`);
|
|
1974
2120
|
return plan;
|
|
1975
2121
|
}
|
|
@@ -1988,7 +2134,7 @@ function brief(plan, task) {
|
|
|
1988
2134
|
].filter(Boolean).join("\n\n")}
|
|
1989
2135
|
`;
|
|
1990
2136
|
}
|
|
1991
|
-
var planName = (file) =>
|
|
2137
|
+
var planName = (file) => path10.basename(file).replace(/\.md$/i, "");
|
|
1992
2138
|
|
|
1993
2139
|
// src/core/report.ts
|
|
1994
2140
|
var ICON2 = {
|
|
@@ -2007,11 +2153,11 @@ function duration(meta) {
|
|
|
2007
2153
|
}
|
|
2008
2154
|
function readSummary(meta) {
|
|
2009
2155
|
const f = runFile(meta.id, "summary.md");
|
|
2010
|
-
return
|
|
2156
|
+
return fs13.existsSync(f) ? fs13.readFileSync(f, "utf8").trim() : archivedRun(meta.id)?.answer ?? "";
|
|
2011
2157
|
}
|
|
2012
2158
|
function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
2013
2159
|
const out = [];
|
|
2014
|
-
out.push(
|
|
2160
|
+
out.push(`${bold("pitroom")} ${stateColour(meta.state, `${ICON2[meta.state]} ${meta.state}`)} \xB7 ${meta.mode} \xB7 ${duration(meta)} \xB7 run ${dim(meta.id)}`);
|
|
2015
2161
|
const ran = meta.ran ?? meta.worker;
|
|
2016
2162
|
const ids = [`worker ${ran.backend}`, meta.resolvedModel && `model ${meta.resolvedModel}`, meta.sessionId && `session ${meta.sessionId}`];
|
|
2017
2163
|
out.push(ids.filter(Boolean).join(" \xB7 "));
|
|
@@ -2026,6 +2172,12 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
|
2026
2172
|
for (const a of meta.attempts ?? []) out.push(`fallback: ${a.target} failed (${a.error.slice(0, 160)})`);
|
|
2027
2173
|
if (meta.error) out.push(`error: ${meta.error}`);
|
|
2028
2174
|
for (const w of meta.warnings) out.push(`warning: ${w}`);
|
|
2175
|
+
if (meta.state === "timeout") {
|
|
2176
|
+
const limit = meta.timeoutSec >= 60 ? `${Math.round(meta.timeoutSec / 60)}m` : `${meta.timeoutSec}s`;
|
|
2177
|
+
out.push(
|
|
2178
|
+
`hint: the worker hit its time limit (${limit}). What it did is kept. Most runs finish in minutes, so first check it was not going in circles; for a genuinely long job raise the limit with -t 1h (or PITROOM_TIMEOUT, or "timeout" in the config)${meta.sessionId ? `, or carry on: pitroom run --continue ${meta.id} "\u2026"` : ""}`
|
|
2179
|
+
);
|
|
2180
|
+
}
|
|
2029
2181
|
if (finalText) {
|
|
2030
2182
|
const lines = finalText.split("\n");
|
|
2031
2183
|
out.push("", ...lines.slice(0, maxLines));
|
|
@@ -2082,8 +2234,8 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
|
2082
2234
|
}
|
|
2083
2235
|
function live(meta) {
|
|
2084
2236
|
const f = runFile(meta.id, "events.jsonl");
|
|
2085
|
-
if (!
|
|
2086
|
-
const p = getBackend((meta.ran ?? meta.worker).backend).parse(
|
|
2237
|
+
if (!fs13.existsSync(f)) return { steps: 0, toolCalls: 0 };
|
|
2238
|
+
const p = getBackend((meta.ran ?? meta.worker).backend).parse(fs13.readFileSync(f, "utf8"));
|
|
2087
2239
|
return { steps: p.usage.steps, toolCalls: p.usage.toolCalls, last: p.lastActivity };
|
|
2088
2240
|
}
|
|
2089
2241
|
function progress(meta) {
|
|
@@ -2138,8 +2290,11 @@ function table(metas) {
|
|
|
2138
2290
|
});
|
|
2139
2291
|
const head = ["RUN", "STATE", "MODE", "TIME", "STEPS", "WORKER", "NOW / RESULT"];
|
|
2140
2292
|
const widths = head.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2141
|
-
const fmt = (r) => r.map((c, i) =>
|
|
2142
|
-
|
|
2293
|
+
const fmt = (r, header = false) => r.map((c, i) => {
|
|
2294
|
+
const cell2 = i === r.length - 1 ? c : c.padEnd(widths[i]);
|
|
2295
|
+
return header ? dim(cell2) : i === 1 ? stateColour(r[1], cell2) : cell2;
|
|
2296
|
+
}).join(" ");
|
|
2297
|
+
return [fmt(head, true), ...rows.map((r) => fmt(r))].join("\n");
|
|
2143
2298
|
}
|
|
2144
2299
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2145
2300
|
async function waitMany(ids, opts) {
|
|
@@ -2216,26 +2371,26 @@ ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \xB7 Ctrl-C to stop watchin
|
|
|
2216
2371
|
}
|
|
2217
2372
|
|
|
2218
2373
|
// src/core/install.ts
|
|
2219
|
-
import
|
|
2374
|
+
import fs14 from "node:fs";
|
|
2220
2375
|
import os7 from "node:os";
|
|
2221
|
-
import
|
|
2376
|
+
import path11 from "node:path";
|
|
2222
2377
|
import { fileURLToPath } from "node:url";
|
|
2223
2378
|
var LEGACY = ["pitroom", "opencode-worker"];
|
|
2224
2379
|
function packageRoot() {
|
|
2225
|
-
return
|
|
2380
|
+
return path11.resolve(path11.dirname(fileURLToPath(import.meta.url)), "..");
|
|
2226
2381
|
}
|
|
2227
2382
|
function skillNames(root = packageRoot()) {
|
|
2228
|
-
const dir =
|
|
2229
|
-
if (!
|
|
2230
|
-
return
|
|
2383
|
+
const dir = path11.join(root, "skills");
|
|
2384
|
+
if (!fs14.existsSync(dir)) return [];
|
|
2385
|
+
return fs14.readdirSync(dir).filter((d) => fs14.existsSync(path11.join(dir, d, "SKILL.md"))).sort();
|
|
2231
2386
|
}
|
|
2232
2387
|
function skillTargets() {
|
|
2233
2388
|
const home2 = os7.homedir();
|
|
2234
|
-
const targets = [
|
|
2235
|
-
if (
|
|
2389
|
+
const targets = [path11.join(home2, ".agents", "skills")];
|
|
2390
|
+
if (fs14.existsSync(path11.join(home2, ".claude"))) targets.push(path11.join(home2, ".claude", "skills"));
|
|
2236
2391
|
return targets;
|
|
2237
2392
|
}
|
|
2238
|
-
var launcherPath = () =>
|
|
2393
|
+
var launcherPath = () => path11.join(os7.homedir(), ".local", "bin", "pitroom");
|
|
2239
2394
|
var LAUNCHER_MARK = "# pitroom launcher";
|
|
2240
2395
|
function launcherScript(bundle) {
|
|
2241
2396
|
const q = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
@@ -2253,7 +2408,7 @@ exit 127
|
|
|
2253
2408
|
function isOurLauncher(file, root) {
|
|
2254
2409
|
if (linksInto(file, root)) return true;
|
|
2255
2410
|
try {
|
|
2256
|
-
const s =
|
|
2411
|
+
const s = fs14.readFileSync(file, "utf8");
|
|
2257
2412
|
return s.includes(LAUNCHER_MARK) && s.includes(root);
|
|
2258
2413
|
} catch {
|
|
2259
2414
|
return false;
|
|
@@ -2261,57 +2416,57 @@ function isOurLauncher(file, root) {
|
|
|
2261
2416
|
}
|
|
2262
2417
|
function placeLauncher(bundle, root, force) {
|
|
2263
2418
|
const dest = launcherPath();
|
|
2264
|
-
|
|
2265
|
-
const exists =
|
|
2419
|
+
fs14.mkdirSync(path11.dirname(dest), { recursive: true });
|
|
2420
|
+
const exists = fs14.lstatSync(dest, { throwIfNoEntry: false });
|
|
2266
2421
|
if (exists && !isOurLauncher(dest, root)) {
|
|
2267
2422
|
if (!force) return `! ${dest} exists and is not Pitroom's launcher; kept (use --force to back it up and replace)`;
|
|
2268
|
-
|
|
2423
|
+
fs14.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
2269
2424
|
} else if (exists) {
|
|
2270
|
-
|
|
2425
|
+
fs14.rmSync(dest, { force: true });
|
|
2271
2426
|
}
|
|
2272
|
-
|
|
2427
|
+
fs14.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
|
|
2273
2428
|
return `\u2714 ${dest} \u2192 launcher for ${bundle} (Node 22.13+)`;
|
|
2274
2429
|
}
|
|
2275
2430
|
function linksInto(link, dir) {
|
|
2276
|
-
const st =
|
|
2431
|
+
const st = fs14.lstatSync(link, { throwIfNoEntry: false });
|
|
2277
2432
|
if (!st?.isSymbolicLink()) return false;
|
|
2278
|
-
const target =
|
|
2279
|
-
const rel =
|
|
2280
|
-
return !rel.startsWith("..") && !
|
|
2433
|
+
const target = path11.resolve(path11.dirname(link), fs14.readlinkSync(link));
|
|
2434
|
+
const rel = path11.relative(dir, target);
|
|
2435
|
+
return !rel.startsWith("..") && !path11.isAbsolute(rel);
|
|
2281
2436
|
}
|
|
2282
2437
|
function place(src, dest, opts) {
|
|
2283
|
-
|
|
2284
|
-
const st =
|
|
2438
|
+
fs14.mkdirSync(path11.dirname(dest), { recursive: true });
|
|
2439
|
+
const st = fs14.lstatSync(dest, { throwIfNoEntry: false });
|
|
2285
2440
|
if (st && !st.isSymbolicLink()) {
|
|
2286
2441
|
if (!opts.force) return `! ${dest} exists and is not a link; kept (use --force to back it up and replace)`;
|
|
2287
|
-
|
|
2442
|
+
fs14.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
2288
2443
|
} else if (st) {
|
|
2289
|
-
|
|
2444
|
+
fs14.unlinkSync(dest);
|
|
2290
2445
|
}
|
|
2291
|
-
if (opts.copy)
|
|
2292
|
-
else
|
|
2446
|
+
if (opts.copy) fs14.cpSync(src, dest, { recursive: true });
|
|
2447
|
+
else fs14.symlinkSync(src, dest, process.platform === "win32" ? "junction" : fs14.statSync(src).isDirectory() ? "dir" : "file");
|
|
2293
2448
|
return `\u2714 ${dest} \u2192 ${opts.copy ? "copied" : src}`;
|
|
2294
2449
|
}
|
|
2295
2450
|
function install(opts) {
|
|
2296
2451
|
const root = packageRoot();
|
|
2297
2452
|
const skills = skillNames(root);
|
|
2298
|
-
if (!skills.length) throw new Error(`no skills found under ${
|
|
2453
|
+
if (!skills.length) throw new Error(`no skills found under ${path11.join(root, "skills")}`);
|
|
2299
2454
|
const out = [];
|
|
2300
2455
|
for (const base2 of skillTargets()) {
|
|
2301
2456
|
for (const legacy of LEGACY) {
|
|
2302
|
-
const l =
|
|
2457
|
+
const l = path11.join(base2, legacy);
|
|
2303
2458
|
if (!skills.includes(legacy) && linksInto(l, root)) {
|
|
2304
|
-
|
|
2459
|
+
fs14.unlinkSync(l);
|
|
2305
2460
|
out.push(`\u2714 removed old link ${l}`);
|
|
2306
2461
|
}
|
|
2307
2462
|
}
|
|
2308
|
-
for (const name of skills) out.push(place(
|
|
2463
|
+
for (const name of skills) out.push(place(path11.join(root, "skills", name), path11.join(base2, name), opts));
|
|
2309
2464
|
}
|
|
2310
|
-
const bundle =
|
|
2311
|
-
if (
|
|
2465
|
+
const bundle = path11.join(root, "dist", "pitroom.mjs");
|
|
2466
|
+
if (fs14.existsSync(bundle)) {
|
|
2312
2467
|
out.push(placeLauncher(bundle, root, opts.force));
|
|
2313
|
-
const onPath = (process.env.PATH ?? "").split(
|
|
2314
|
-
if (!onPath) out.push(`! ${
|
|
2468
|
+
const onPath = (process.env.PATH ?? "").split(path11.delimiter).some((d) => path11.resolve(d) === path11.dirname(launcherPath()));
|
|
2469
|
+
if (!onPath) out.push(`! ${path11.dirname(launcherPath())} is not on PATH; add it, or run ${launcherPath()} directly`);
|
|
2315
2470
|
}
|
|
2316
2471
|
return out;
|
|
2317
2472
|
}
|
|
@@ -2319,38 +2474,38 @@ function uninstall() {
|
|
|
2319
2474
|
const root = packageRoot();
|
|
2320
2475
|
const out = [];
|
|
2321
2476
|
for (const base2 of skillTargets()) {
|
|
2322
|
-
if (!
|
|
2323
|
-
for (const name of
|
|
2324
|
-
const l =
|
|
2477
|
+
if (!fs14.existsSync(base2)) continue;
|
|
2478
|
+
for (const name of fs14.readdirSync(base2)) {
|
|
2479
|
+
const l = path11.join(base2, name);
|
|
2325
2480
|
if (linksInto(l, root)) {
|
|
2326
|
-
|
|
2481
|
+
fs14.unlinkSync(l);
|
|
2327
2482
|
out.push(`\u2714 removed ${l}`);
|
|
2328
2483
|
}
|
|
2329
2484
|
}
|
|
2330
2485
|
}
|
|
2331
2486
|
if (isOurLauncher(launcherPath(), root)) {
|
|
2332
|
-
|
|
2487
|
+
fs14.rmSync(launcherPath(), { force: true });
|
|
2333
2488
|
out.push(`\u2714 removed ${launcherPath()}`);
|
|
2334
2489
|
}
|
|
2335
2490
|
return out.length ? out : ["nothing to remove"];
|
|
2336
2491
|
}
|
|
2337
2492
|
function installedSkills() {
|
|
2338
2493
|
const names = skillNames();
|
|
2339
|
-
return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) =>
|
|
2494
|
+
return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs14.existsSync(path11.join(base2, n, "SKILL.md"))) }));
|
|
2340
2495
|
}
|
|
2341
2496
|
|
|
2342
2497
|
// src/core/plan-status.ts
|
|
2343
2498
|
import crypto2 from "node:crypto";
|
|
2344
|
-
import
|
|
2345
|
-
import
|
|
2499
|
+
import fs15 from "node:fs";
|
|
2500
|
+
import path12 from "node:path";
|
|
2346
2501
|
function notesFile(plan) {
|
|
2347
2502
|
const id = crypto2.createHash("sha1").update(plan.file).digest("hex").slice(0, 12);
|
|
2348
|
-
return
|
|
2503
|
+
return path12.join(home(), "plans", id, "notes.md");
|
|
2349
2504
|
}
|
|
2350
2505
|
function readNotes(plan) {
|
|
2351
2506
|
const f = notesFile(plan);
|
|
2352
|
-
if (!
|
|
2353
|
-
return
|
|
2507
|
+
if (!fs15.existsSync(f)) return [];
|
|
2508
|
+
return fs15.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
|
|
2354
2509
|
const m = /^(\d{4}-\d\d-\d\d \d\d:\d\d) (.*)$/.exec(l);
|
|
2355
2510
|
return m ? { at: m[1], text: m[2] } : { at: "", text: l };
|
|
2356
2511
|
});
|
|
@@ -2358,11 +2513,11 @@ function readNotes(plan) {
|
|
|
2358
2513
|
function addNote(planFile, text) {
|
|
2359
2514
|
const plan = loadPlan(planFile);
|
|
2360
2515
|
const f = notesFile(plan);
|
|
2361
|
-
|
|
2362
|
-
if (!
|
|
2516
|
+
fs15.mkdirSync(path12.dirname(f), { recursive: true });
|
|
2517
|
+
if (!fs15.existsSync(f)) fs15.writeFileSync(f, `# plan: ${plan.file}
|
|
2363
2518
|
`);
|
|
2364
2519
|
const at = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
|
|
2365
|
-
|
|
2520
|
+
fs15.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
|
|
2366
2521
|
`);
|
|
2367
2522
|
return f;
|
|
2368
2523
|
}
|
|
@@ -2430,15 +2585,15 @@ ${rulings}`,
|
|
|
2430
2585
|
|
|
2431
2586
|
// src/core/review.ts
|
|
2432
2587
|
import crypto4 from "node:crypto";
|
|
2433
|
-
import
|
|
2434
|
-
import
|
|
2588
|
+
import fs17 from "node:fs";
|
|
2589
|
+
import path14 from "node:path";
|
|
2435
2590
|
|
|
2436
2591
|
// src/vcs/git.ts
|
|
2437
2592
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
2438
2593
|
import crypto3 from "node:crypto";
|
|
2439
|
-
import
|
|
2594
|
+
import fs16 from "node:fs";
|
|
2440
2595
|
import os8 from "node:os";
|
|
2441
|
-
import
|
|
2596
|
+
import path13 from "node:path";
|
|
2442
2597
|
var SAFE = [
|
|
2443
2598
|
"-c",
|
|
2444
2599
|
"color.ui=false",
|
|
@@ -2470,20 +2625,20 @@ function gitAvailable() {
|
|
|
2470
2625
|
}
|
|
2471
2626
|
function repoRoot(dir) {
|
|
2472
2627
|
const r = git(dir, ["rev-parse", "--show-toplevel"]);
|
|
2473
|
-
return r.code === 0 ?
|
|
2628
|
+
return r.code === 0 ? path13.resolve(r.stdout.trim()) : void 0;
|
|
2474
2629
|
}
|
|
2475
2630
|
function snapshotTree(root, exclude = []) {
|
|
2476
|
-
const tmp =
|
|
2477
|
-
const real =
|
|
2631
|
+
const tmp = path13.join(os8.tmpdir(), `pitroom-index-${process.pid}-${crypto3.randomBytes(4).toString("hex")}`);
|
|
2632
|
+
const real = path13.resolve(root, must(root, ["rev-parse", "--git-path", "index"]).trim());
|
|
2478
2633
|
const env = { ...process.env, GIT_INDEX_FILE: tmp };
|
|
2479
2634
|
try {
|
|
2480
|
-
if (
|
|
2635
|
+
if (fs16.existsSync(real)) fs16.copyFileSync(real, tmp);
|
|
2481
2636
|
else must(root, ["read-tree", "--empty"], env);
|
|
2482
2637
|
must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`)], env);
|
|
2483
2638
|
return must(root, ["write-tree"], env).trim();
|
|
2484
2639
|
} finally {
|
|
2485
|
-
|
|
2486
|
-
|
|
2640
|
+
fs16.rmSync(tmp, { force: true });
|
|
2641
|
+
fs16.rmSync(`${tmp}.lock`, { force: true });
|
|
2487
2642
|
}
|
|
2488
2643
|
}
|
|
2489
2644
|
var DIFF = ["diff", "--no-ext-diff", "--no-textconv", "--no-renames"];
|
|
@@ -2503,27 +2658,27 @@ function diffTrees(root, a, b) {
|
|
|
2503
2658
|
return { changes, stats: { files: changes.length, insertions, deletions }, patch };
|
|
2504
2659
|
}
|
|
2505
2660
|
function createIsolatedCopy(root, tree, dest) {
|
|
2506
|
-
const objects =
|
|
2507
|
-
|
|
2508
|
-
must(
|
|
2509
|
-
|
|
2661
|
+
const objects = path13.join(path13.resolve(root, must(root, ["rev-parse", "--git-common-dir"]).trim()), "objects");
|
|
2662
|
+
fs16.mkdirSync(path13.dirname(dest), { recursive: true });
|
|
2663
|
+
must(path13.dirname(dest), ["init", "--quiet", dest]);
|
|
2664
|
+
fs16.writeFileSync(path13.join(dest, ".git", "objects", "info", "alternates"), `${objects}
|
|
2510
2665
|
`);
|
|
2511
2666
|
must(dest, ["read-tree", tree]);
|
|
2512
2667
|
must(dest, ["checkout-index", "--all", "--force"]);
|
|
2513
2668
|
}
|
|
2514
2669
|
function removeIsolatedCopy(dest, ownedBy) {
|
|
2515
|
-
const rel =
|
|
2516
|
-
if (!rel || rel.startsWith("..") ||
|
|
2517
|
-
|
|
2670
|
+
const rel = path13.relative(ownedBy, dest);
|
|
2671
|
+
if (!rel || rel.startsWith("..") || path13.isAbsolute(rel)) throw new Error(`refusing to remove ${dest}`);
|
|
2672
|
+
fs16.rmSync(dest, { recursive: true, force: true });
|
|
2518
2673
|
}
|
|
2519
2674
|
function linkIntoWorktree(root, worktree, rels) {
|
|
2520
2675
|
const linked = [];
|
|
2521
2676
|
for (const rel of rels) {
|
|
2522
|
-
const src =
|
|
2523
|
-
const dst =
|
|
2524
|
-
if (!
|
|
2525
|
-
|
|
2526
|
-
|
|
2677
|
+
const src = path13.join(root, rel);
|
|
2678
|
+
const dst = path13.join(worktree, rel);
|
|
2679
|
+
if (!fs16.existsSync(src) || fs16.existsSync(dst)) continue;
|
|
2680
|
+
fs16.mkdirSync(path13.dirname(dst), { recursive: true });
|
|
2681
|
+
fs16.symlinkSync(src, dst, process.platform === "win32" ? "junction" : void 0);
|
|
2527
2682
|
linked.push(rel);
|
|
2528
2683
|
}
|
|
2529
2684
|
return linked;
|
|
@@ -2582,7 +2737,7 @@ function ancestors(m) {
|
|
|
2582
2737
|
function briefOf(m) {
|
|
2583
2738
|
const root = [m, ...ancestors(m)].at(-1);
|
|
2584
2739
|
const f = runFile(root.id, "brief.md");
|
|
2585
|
-
return
|
|
2740
|
+
return fs17.existsSync(f) ? fs17.readFileSync(f, "utf8") : root.task;
|
|
2586
2741
|
}
|
|
2587
2742
|
function latestReview(ids) {
|
|
2588
2743
|
for (const id of listRunIds().reverse()) {
|
|
@@ -2596,7 +2751,7 @@ function latestReview(ids) {
|
|
|
2596
2751
|
}
|
|
2597
2752
|
function diffOf(m, from = m.baseTree) {
|
|
2598
2753
|
const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
|
|
2599
|
-
if (where &&
|
|
2754
|
+
if (where && fs17.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
|
|
2600
2755
|
if (from !== m.baseTree) {
|
|
2601
2756
|
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);
|
|
2602
2757
|
}
|
|
@@ -2610,7 +2765,7 @@ function runReview(id) {
|
|
|
2610
2765
|
if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
|
|
2611
2766
|
const chain = ancestors(m);
|
|
2612
2767
|
const previous = latestReview(chain.map((a) => a.id));
|
|
2613
|
-
const dir = m.mode === "isolate" && m.worktree &&
|
|
2768
|
+
const dir = m.mode === "isolate" && m.worktree && fs17.existsSync(m.worktree) ? m.cwd : m.dir;
|
|
2614
2769
|
const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
|
|
2615
2770
|
const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
|
|
2616
2771
|
if (!previous) {
|
|
@@ -2684,15 +2839,15 @@ function pickReviewer(job) {
|
|
|
2684
2839
|
function writePackage(job) {
|
|
2685
2840
|
const g = gitDir(job.dir);
|
|
2686
2841
|
if (!g) throw new UserError(`not a git repository: ${job.dir}`);
|
|
2687
|
-
const file =
|
|
2688
|
-
|
|
2689
|
-
|
|
2842
|
+
const file = path14.join(g, "pitroom", `review-${crypto4.randomBytes(4).toString("hex")}.md`);
|
|
2843
|
+
fs17.mkdirSync(path14.dirname(file), { recursive: true });
|
|
2844
|
+
fs17.writeFileSync(file, job.package);
|
|
2690
2845
|
return file;
|
|
2691
2846
|
}
|
|
2692
2847
|
|
|
2693
2848
|
// src/core/templates.ts
|
|
2694
|
-
import
|
|
2695
|
-
import
|
|
2849
|
+
import fs18 from "node:fs";
|
|
2850
|
+
import path15 from "node:path";
|
|
2696
2851
|
var FILES = {
|
|
2697
2852
|
implementer: "pitroom-driven-development/implementer-prompt.md",
|
|
2698
2853
|
"task-reviewer": "pitroom-driven-development/task-reviewer-prompt.md",
|
|
@@ -2700,9 +2855,9 @@ var FILES = {
|
|
|
2700
2855
|
"code-reviewer": "pitroom-review/code-reviewer.md"
|
|
2701
2856
|
};
|
|
2702
2857
|
function loadTemplate(name, root = packageRoot()) {
|
|
2703
|
-
const file =
|
|
2704
|
-
if (!
|
|
2705
|
-
return
|
|
2858
|
+
const file = path15.join(root, "skills", FILES[name]);
|
|
2859
|
+
if (!fs18.existsSync(file)) throw new UserError(`template missing: ${file} (broken install? run pitroom doctor)`, 3);
|
|
2860
|
+
return fs18.readFileSync(file, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
|
|
2706
2861
|
}
|
|
2707
2862
|
function fill(template, values) {
|
|
2708
2863
|
const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
|
|
@@ -2809,9 +2964,9 @@ function formatModels(t) {
|
|
|
2809
2964
|
// src/core/dash.ts
|
|
2810
2965
|
import { spawn } from "node:child_process";
|
|
2811
2966
|
import crypto5 from "node:crypto";
|
|
2812
|
-
import
|
|
2967
|
+
import fs19 from "node:fs";
|
|
2813
2968
|
import http from "node:http";
|
|
2814
|
-
import
|
|
2969
|
+
import path16 from "node:path";
|
|
2815
2970
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2816
2971
|
|
|
2817
2972
|
// src/core/dash-page.ts
|
|
@@ -2846,7 +3001,8 @@ function toRun(m) {
|
|
|
2846
3001
|
id: m.id,
|
|
2847
3002
|
state: m.state,
|
|
2848
3003
|
kind: kind(m),
|
|
2849
|
-
|
|
3004
|
+
// the model that really ran, once the CLI has said which; until then just the worker, never a guess
|
|
3005
|
+
worker: workerName(ranTarget(m)),
|
|
2850
3006
|
task: what(m),
|
|
2851
3007
|
group: m.group,
|
|
2852
3008
|
startedAt: m.startedAt,
|
|
@@ -2887,7 +3043,7 @@ var TASK_MAX = 6e3;
|
|
|
2887
3043
|
var PATCH_LINES = 300;
|
|
2888
3044
|
function readFile(id, name) {
|
|
2889
3045
|
const f = runFile(id, name);
|
|
2890
|
-
return
|
|
3046
|
+
return fs19.existsSync(f) ? fs19.readFileSync(f, "utf8") : "";
|
|
2891
3047
|
}
|
|
2892
3048
|
function runDetail(id) {
|
|
2893
3049
|
if (!RUN_ID2.test(id)) return void 0;
|
|
@@ -2919,6 +3075,7 @@ function runDetail(id) {
|
|
|
2919
3075
|
return {
|
|
2920
3076
|
id,
|
|
2921
3077
|
state: m.state,
|
|
3078
|
+
card: toRun(m),
|
|
2922
3079
|
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
3080
|
steps,
|
|
2924
3081
|
answer,
|
|
@@ -2961,11 +3118,11 @@ var assetsDir = () => fileURLToPath2(new URL("./ui/", import.meta.url));
|
|
|
2961
3118
|
var assetCache = /* @__PURE__ */ new Map();
|
|
2962
3119
|
function readAsset(route) {
|
|
2963
3120
|
try {
|
|
2964
|
-
const file =
|
|
2965
|
-
const { mtimeMs } =
|
|
3121
|
+
const file = path16.join(assetsDir(), path16.basename(route));
|
|
3122
|
+
const { mtimeMs } = fs19.statSync(file);
|
|
2966
3123
|
const hit = assetCache.get(route);
|
|
2967
3124
|
if (hit && hit.mtimeMs === mtimeMs) return hit.data;
|
|
2968
|
-
const data =
|
|
3125
|
+
const data = fs19.readFileSync(file);
|
|
2969
3126
|
assetCache.set(route, { mtimeMs, data });
|
|
2970
3127
|
return data;
|
|
2971
3128
|
} catch {
|
|
@@ -3045,7 +3202,7 @@ function startDash(opts) {
|
|
|
3045
3202
|
});
|
|
3046
3203
|
});
|
|
3047
3204
|
}
|
|
3048
|
-
var registryFile = () =>
|
|
3205
|
+
var registryFile = () => path16.join(home(), "dash.json");
|
|
3049
3206
|
async function ping(port) {
|
|
3050
3207
|
try {
|
|
3051
3208
|
const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
|
|
@@ -3056,11 +3213,11 @@ async function ping(port) {
|
|
|
3056
3213
|
}
|
|
3057
3214
|
async function runningDash() {
|
|
3058
3215
|
try {
|
|
3059
|
-
const r = JSON.parse(
|
|
3216
|
+
const r = JSON.parse(fs19.readFileSync(registryFile(), "utf8"));
|
|
3060
3217
|
if (isAlive(r.pid) && await ping(r.port)) return { ...r, url: `http://127.0.0.1:${r.port}/` };
|
|
3061
3218
|
} catch {
|
|
3062
3219
|
}
|
|
3063
|
-
|
|
3220
|
+
fs19.rmSync(registryFile(), { force: true });
|
|
3064
3221
|
return void 0;
|
|
3065
3222
|
}
|
|
3066
3223
|
function openBrowser(url) {
|
|
@@ -3075,7 +3232,7 @@ async function dashCommand(o) {
|
|
|
3075
3232
|
if (o.stop) {
|
|
3076
3233
|
if (!existing) throw new UserError("pitroom dash is not running");
|
|
3077
3234
|
process.kill(existing.pid, "SIGTERM");
|
|
3078
|
-
|
|
3235
|
+
fs19.rmSync(registryFile(), { force: true });
|
|
3079
3236
|
o.log(`stopped pitroom dash (${existing.url})`);
|
|
3080
3237
|
return 0;
|
|
3081
3238
|
}
|
|
@@ -3112,8 +3269,8 @@ async function dashCommand(o) {
|
|
|
3112
3269
|
if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
|
|
3113
3270
|
dash = await startDash({ port: 0, idleMs: o.idleMs });
|
|
3114
3271
|
}
|
|
3115
|
-
|
|
3116
|
-
|
|
3272
|
+
fs19.mkdirSync(home(), { recursive: true });
|
|
3273
|
+
fs19.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
|
|
3117
3274
|
const stop = () => void dash.close();
|
|
3118
3275
|
process.once("SIGINT", stop);
|
|
3119
3276
|
process.once("SIGTERM", stop);
|
|
@@ -3122,7 +3279,7 @@ async function dashCommand(o) {
|
|
|
3122
3279
|
if (o.open) openBrowser(dash.url);
|
|
3123
3280
|
await dash.closed;
|
|
3124
3281
|
try {
|
|
3125
|
-
if (JSON.parse(
|
|
3282
|
+
if (JSON.parse(fs19.readFileSync(registryFile(), "utf8")).pid === process.pid) fs19.rmSync(registryFile(), { force: true });
|
|
3126
3283
|
} catch {
|
|
3127
3284
|
}
|
|
3128
3285
|
return 0;
|
|
@@ -3130,8 +3287,8 @@ async function dashCommand(o) {
|
|
|
3130
3287
|
|
|
3131
3288
|
// src/core/run.ts
|
|
3132
3289
|
import { spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
|
|
3133
|
-
import
|
|
3134
|
-
import
|
|
3290
|
+
import fs25 from "node:fs";
|
|
3291
|
+
import path21 from "node:path";
|
|
3135
3292
|
|
|
3136
3293
|
// src/core/chain.ts
|
|
3137
3294
|
function resolveChain(flags = {}) {
|
|
@@ -3181,11 +3338,11 @@ function resolveChain(flags = {}) {
|
|
|
3181
3338
|
|
|
3182
3339
|
// src/core/process.ts
|
|
3183
3340
|
import { spawn as spawn2 } from "node:child_process";
|
|
3184
|
-
import
|
|
3341
|
+
import fs21 from "node:fs";
|
|
3185
3342
|
|
|
3186
3343
|
// src/vcs/guard.ts
|
|
3187
|
-
import
|
|
3188
|
-
import
|
|
3344
|
+
import fs20 from "node:fs";
|
|
3345
|
+
import path17 from "node:path";
|
|
3189
3346
|
var VERSION = 1;
|
|
3190
3347
|
var SHIM = `#!/bin/sh
|
|
3191
3348
|
# pitroom git guard v${VERSION}. Blocks git commands that change history, refs, the
|
|
@@ -3260,15 +3417,15 @@ esac
|
|
|
3260
3417
|
exec "$real" "$@"
|
|
3261
3418
|
`;
|
|
3262
3419
|
function shimDir() {
|
|
3263
|
-
return
|
|
3420
|
+
return path17.join(home(), "shim", `v${VERSION}`);
|
|
3264
3421
|
}
|
|
3265
3422
|
function findRealGit(skip) {
|
|
3266
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
3267
|
-
if (
|
|
3268
|
-
const p =
|
|
3423
|
+
for (const dir of (process.env.PATH ?? "").split(path17.delimiter).filter(Boolean)) {
|
|
3424
|
+
if (path17.resolve(dir) === skip) continue;
|
|
3425
|
+
const p = path17.join(dir, "git");
|
|
3269
3426
|
try {
|
|
3270
|
-
|
|
3271
|
-
if (
|
|
3427
|
+
fs20.accessSync(p, fs20.constants.X_OK);
|
|
3428
|
+
if (fs20.statSync(p).isFile()) return p;
|
|
3272
3429
|
} catch {
|
|
3273
3430
|
}
|
|
3274
3431
|
}
|
|
@@ -3281,13 +3438,13 @@ var REF_HOOK = `#!/bin/sh
|
|
|
3281
3438
|
echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
|
|
3282
3439
|
exit 1
|
|
3283
3440
|
`;
|
|
3284
|
-
var hooksDir = () =>
|
|
3441
|
+
var hooksDir = () => path17.join(home(), "git-hooks", `v${HOOK_VERSION}`);
|
|
3285
3442
|
function writeIfChanged(file, content) {
|
|
3286
|
-
if (
|
|
3287
|
-
|
|
3443
|
+
if (fs20.existsSync(file) && fs20.readFileSync(file, "utf8") === content) return;
|
|
3444
|
+
fs20.mkdirSync(path17.dirname(file), { recursive: true });
|
|
3288
3445
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
3289
|
-
|
|
3290
|
-
|
|
3446
|
+
fs20.writeFileSync(tmp, content, { mode: 493 });
|
|
3447
|
+
fs20.renameSync(tmp, file);
|
|
3291
3448
|
}
|
|
3292
3449
|
function withGitConfig(env, entries) {
|
|
3293
3450
|
const out = { ...env };
|
|
@@ -3309,7 +3466,7 @@ function guardEnv(env) {
|
|
|
3309
3466
|
PAGER: "cat"
|
|
3310
3467
|
};
|
|
3311
3468
|
if (process.platform === "win32") return { ...env, ...quiet };
|
|
3312
|
-
writeIfChanged(
|
|
3469
|
+
writeIfChanged(path17.join(hooksDir(), "reference-transaction"), REF_HOOK);
|
|
3313
3470
|
env = withGitConfig(env, [
|
|
3314
3471
|
["core.hooksPath", hooksDir()],
|
|
3315
3472
|
["url.pitroom-push-blocked://.pushInsteadOf", ""]
|
|
@@ -3317,14 +3474,14 @@ function guardEnv(env) {
|
|
|
3317
3474
|
const dir = shimDir();
|
|
3318
3475
|
const real = findRealGit(dir);
|
|
3319
3476
|
if (!real) return { ...env, ...quiet };
|
|
3320
|
-
writeIfChanged(
|
|
3321
|
-
return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${
|
|
3477
|
+
writeIfChanged(path17.join(dir, "git"), SHIM);
|
|
3478
|
+
return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${path17.delimiter}${env.PATH ?? ""}` };
|
|
3322
3479
|
}
|
|
3323
3480
|
|
|
3324
3481
|
// src/core/process.ts
|
|
3325
3482
|
async function spawnWorker(inv, opts) {
|
|
3326
|
-
const out =
|
|
3327
|
-
const err =
|
|
3483
|
+
const out = fs21.openSync(opts.stdoutFile, "w");
|
|
3484
|
+
const err = fs21.openSync(opts.stderrFile, "w");
|
|
3328
3485
|
const res = { code: null, timedOut: false, stopped: false };
|
|
3329
3486
|
const child = spawn2(inv.command, inv.args, {
|
|
3330
3487
|
cwd: opts.cwd,
|
|
@@ -3356,8 +3513,8 @@ async function spawnWorker(inv, opts) {
|
|
|
3356
3513
|
if (killer) clearTimeout(killer);
|
|
3357
3514
|
process.off("SIGINT", stop);
|
|
3358
3515
|
process.off("SIGTERM", stop);
|
|
3359
|
-
|
|
3360
|
-
|
|
3516
|
+
fs21.closeSync(out);
|
|
3517
|
+
fs21.closeSync(err);
|
|
3361
3518
|
return res;
|
|
3362
3519
|
}
|
|
3363
3520
|
|
|
@@ -3383,10 +3540,10 @@ function parseVerdict(text) {
|
|
|
3383
3540
|
|
|
3384
3541
|
// src/core/slots.ts
|
|
3385
3542
|
import crypto6 from "node:crypto";
|
|
3386
|
-
import
|
|
3387
|
-
import
|
|
3388
|
-
var slotsDir = () =>
|
|
3389
|
-
var locksDir = () =>
|
|
3543
|
+
import fs22 from "node:fs";
|
|
3544
|
+
import path18 from "node:path";
|
|
3545
|
+
var slotsDir = () => path18.join(home(), "slots");
|
|
3546
|
+
var locksDir = () => path18.join(home(), "locks");
|
|
3390
3547
|
var STARTUP_GRACE_MS = 3e4;
|
|
3391
3548
|
function holderActive(runId, self) {
|
|
3392
3549
|
if (!runId || runId === self) return false;
|
|
@@ -3400,23 +3557,23 @@ function holderActive(runId, self) {
|
|
|
3400
3557
|
}
|
|
3401
3558
|
}
|
|
3402
3559
|
function tryClaim(file, runId) {
|
|
3403
|
-
|
|
3560
|
+
fs22.mkdirSync(path18.dirname(file), { recursive: true });
|
|
3404
3561
|
try {
|
|
3405
|
-
|
|
3562
|
+
fs22.writeFileSync(file, runId, { flag: "wx" });
|
|
3406
3563
|
return true;
|
|
3407
3564
|
} catch (e) {
|
|
3408
3565
|
if (e.code !== "EEXIST") throw e;
|
|
3409
3566
|
}
|
|
3410
3567
|
let owner = "";
|
|
3411
3568
|
try {
|
|
3412
|
-
owner =
|
|
3569
|
+
owner = fs22.readFileSync(file, "utf8").trim();
|
|
3413
3570
|
} catch {
|
|
3414
3571
|
}
|
|
3415
3572
|
if (owner === runId) return true;
|
|
3416
3573
|
if (holderActive(owner, runId)) return false;
|
|
3417
|
-
|
|
3574
|
+
fs22.rmSync(file, { force: true });
|
|
3418
3575
|
try {
|
|
3419
|
-
|
|
3576
|
+
fs22.writeFileSync(file, runId, { flag: "wx" });
|
|
3420
3577
|
return true;
|
|
3421
3578
|
} catch {
|
|
3422
3579
|
return false;
|
|
@@ -3425,25 +3582,25 @@ function tryClaim(file, runId) {
|
|
|
3425
3582
|
function releaseIfOwner(file, runId) {
|
|
3426
3583
|
if (!file) return;
|
|
3427
3584
|
try {
|
|
3428
|
-
if (
|
|
3585
|
+
if (fs22.readFileSync(file, "utf8").trim() === runId) fs22.rmSync(file, { force: true });
|
|
3429
3586
|
} catch {
|
|
3430
3587
|
}
|
|
3431
3588
|
}
|
|
3432
3589
|
function tryAcquireSlot(runId, maxParallel) {
|
|
3433
3590
|
for (let n = 0; n < Math.max(1, maxParallel); n++) {
|
|
3434
|
-
const file =
|
|
3591
|
+
const file = path18.join(slotsDir(), `slot-${n}`);
|
|
3435
3592
|
if (tryClaim(file, runId)) return file;
|
|
3436
3593
|
}
|
|
3437
3594
|
return void 0;
|
|
3438
3595
|
}
|
|
3439
3596
|
var releaseSlot = (file, runId) => releaseIfOwner(file, runId);
|
|
3440
|
-
var lockFile = (repoRoot2) =>
|
|
3597
|
+
var lockFile = (repoRoot2) => path18.join(locksDir(), `write-${crypto6.createHash("sha1").update(path18.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
|
|
3441
3598
|
function acquireWriteLock(repoRoot2, runId) {
|
|
3442
3599
|
const file = lockFile(repoRoot2);
|
|
3443
3600
|
if (tryClaim(file, runId)) return;
|
|
3444
3601
|
let owner = "";
|
|
3445
3602
|
try {
|
|
3446
|
-
owner =
|
|
3603
|
+
owner = fs22.readFileSync(file, "utf8").trim();
|
|
3447
3604
|
} catch {
|
|
3448
3605
|
}
|
|
3449
3606
|
throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
|
|
@@ -3490,8 +3647,8 @@ ${task}`;
|
|
|
3490
3647
|
}
|
|
3491
3648
|
|
|
3492
3649
|
// src/core/refs.ts
|
|
3493
|
-
import
|
|
3494
|
-
import
|
|
3650
|
+
import fs23 from "node:fs";
|
|
3651
|
+
import path19 from "node:path";
|
|
3495
3652
|
var FILE_EXTENSIONS = new Set(
|
|
3496
3653
|
"ts tsx mts cts js jsx mjs cjs json jsonc json5 md mdx txt rst py pyi rb go rs java kt kts swift c h cc cpp cxx hpp hh cs fs php lua r jl sh bash zsh fish ps1 bat yml yaml toml ini cfg conf env html htm css scss sass less vue svelte astro sql graphql gql proto lock xml svg csv tsv gradle tf hcl dart ex exs erl hs ml scala clj vim el mk cmake dockerfile gitignore gitattributes editorconfig npmrc nvmrc".split(" ")
|
|
3497
3654
|
);
|
|
@@ -3542,13 +3699,13 @@ function extractRefs(answer) {
|
|
|
3542
3699
|
return [...seen.values()];
|
|
3543
3700
|
}
|
|
3544
3701
|
function verifyRefs(refs, dirs) {
|
|
3545
|
-
const roots = [...new Set(dirs.filter(Boolean).map((d) =>
|
|
3702
|
+
const roots = [...new Set(dirs.filter(Boolean).map((d) => path19.resolve(d)))];
|
|
3546
3703
|
const invalid = [];
|
|
3547
3704
|
const lineCache = /* @__PURE__ */ new Map();
|
|
3548
3705
|
const load = (file) => {
|
|
3549
3706
|
if (!lineCache.has(file)) {
|
|
3550
|
-
const size =
|
|
3551
|
-
const lines = size > MAX_BYTES ? null :
|
|
3707
|
+
const size = fs23.statSync(file).size;
|
|
3708
|
+
const lines = size > MAX_BYTES ? null : fs23.readFileSync(file, "utf8").split("\n");
|
|
3552
3709
|
if (lines && lines[lines.length - 1] === "") lines.pop();
|
|
3553
3710
|
lineCache.set(file, lines);
|
|
3554
3711
|
}
|
|
@@ -3559,9 +3716,9 @@ function verifyRefs(refs, dirs) {
|
|
|
3559
3716
|
const candidates = (ref) => {
|
|
3560
3717
|
const direct = resolve(ref.file, roots);
|
|
3561
3718
|
if (direct) return [direct];
|
|
3562
|
-
if (
|
|
3719
|
+
if (path19.isAbsolute(ref.file)) return [];
|
|
3563
3720
|
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
3564
|
-
return (byName().get(
|
|
3721
|
+
return (byName().get(path19.basename(wanted)) ?? []).filter((f) => f.endsWith(`/${wanted}`) || path19.basename(f) === wanted).slice(0, 20);
|
|
3565
3722
|
};
|
|
3566
3723
|
const check = (file, ref) => {
|
|
3567
3724
|
const lines = load(file);
|
|
@@ -3576,7 +3733,7 @@ function verifyRefs(refs, dirs) {
|
|
|
3576
3733
|
if (!files.length) {
|
|
3577
3734
|
if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
|
|
3578
3735
|
total++;
|
|
3579
|
-
invalid.push({ ref: ref.text, reason:
|
|
3736
|
+
invalid.push({ ref: ref.text, reason: path19.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
|
|
3580
3737
|
continue;
|
|
3581
3738
|
}
|
|
3582
3739
|
total++;
|
|
@@ -3597,7 +3754,7 @@ function enclosedBy(lines, ref) {
|
|
|
3597
3754
|
return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
|
|
3598
3755
|
}
|
|
3599
3756
|
var isFileName = (name) => {
|
|
3600
|
-
const base2 =
|
|
3757
|
+
const base2 = path19.basename(name);
|
|
3601
3758
|
return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
|
|
3602
3759
|
};
|
|
3603
3760
|
function indexFiles(roots) {
|
|
@@ -3606,19 +3763,19 @@ function indexFiles(roots) {
|
|
|
3606
3763
|
const walk = (dir) => {
|
|
3607
3764
|
let entries;
|
|
3608
3765
|
try {
|
|
3609
|
-
entries =
|
|
3766
|
+
entries = fs23.readdirSync(dir, { withFileTypes: true });
|
|
3610
3767
|
} catch {
|
|
3611
3768
|
return;
|
|
3612
3769
|
}
|
|
3613
3770
|
for (const e of entries) {
|
|
3614
3771
|
if (count >= MAX_INDEXED) return;
|
|
3615
3772
|
if (e.isDirectory()) {
|
|
3616
|
-
if (!SKIP_DIRS.has(e.name)) walk(
|
|
3773
|
+
if (!SKIP_DIRS.has(e.name)) walk(path19.join(dir, e.name));
|
|
3617
3774
|
} else if (e.isFile()) {
|
|
3618
3775
|
count++;
|
|
3619
3776
|
const list2 = index.get(e.name);
|
|
3620
|
-
if (list2) list2.push(
|
|
3621
|
-
else index.set(e.name, [
|
|
3777
|
+
if (list2) list2.push(path19.join(dir, e.name));
|
|
3778
|
+
else index.set(e.name, [path19.join(dir, e.name)]);
|
|
3622
3779
|
}
|
|
3623
3780
|
}
|
|
3624
3781
|
};
|
|
@@ -3626,10 +3783,10 @@ function indexFiles(roots) {
|
|
|
3626
3783
|
return index;
|
|
3627
3784
|
}
|
|
3628
3785
|
function resolve(ref, roots) {
|
|
3629
|
-
const candidates =
|
|
3786
|
+
const candidates = path19.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path19.join(r, ref));
|
|
3630
3787
|
return candidates.find((c) => {
|
|
3631
3788
|
try {
|
|
3632
|
-
return
|
|
3789
|
+
return fs23.statSync(c).isFile() && inside(c, roots);
|
|
3633
3790
|
} catch {
|
|
3634
3791
|
return false;
|
|
3635
3792
|
}
|
|
@@ -3637,16 +3794,16 @@ function resolve(ref, roots) {
|
|
|
3637
3794
|
}
|
|
3638
3795
|
function inside(file, roots) {
|
|
3639
3796
|
return roots.some((r) => {
|
|
3640
|
-
const rel =
|
|
3641
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
3797
|
+
const rel = path19.relative(r, path19.resolve(file));
|
|
3798
|
+
return rel !== "" && !rel.startsWith("..") && !path19.isAbsolute(rel);
|
|
3642
3799
|
});
|
|
3643
3800
|
}
|
|
3644
3801
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3645
3802
|
|
|
3646
3803
|
// src/core/secrets.ts
|
|
3647
3804
|
import { execFileSync } from "node:child_process";
|
|
3648
|
-
import
|
|
3649
|
-
import
|
|
3805
|
+
import fs24 from "node:fs";
|
|
3806
|
+
import path20 from "node:path";
|
|
3650
3807
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
3651
3808
|
var MAX_VISITED = 2e4;
|
|
3652
3809
|
var MAX_DEPTH = 4;
|
|
@@ -3662,16 +3819,16 @@ function findSecretFiles(dir) {
|
|
|
3662
3819
|
if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
|
|
3663
3820
|
let entries;
|
|
3664
3821
|
try {
|
|
3665
|
-
entries =
|
|
3822
|
+
entries = fs24.readdirSync(d, { withFileTypes: true });
|
|
3666
3823
|
} catch {
|
|
3667
3824
|
return;
|
|
3668
3825
|
}
|
|
3669
3826
|
for (const e of entries) {
|
|
3670
3827
|
visited++;
|
|
3671
3828
|
if (e.isDirectory()) {
|
|
3672
|
-
if (!SKIP_DIRS2.has(e.name)) walk(
|
|
3829
|
+
if (!SKIP_DIRS2.has(e.name)) walk(path20.join(d, e.name), depth + 1);
|
|
3673
3830
|
} else if (e.isFile() && looksSecret(e.name)) {
|
|
3674
|
-
found.push(
|
|
3831
|
+
found.push(path20.relative(dir, path20.join(d, e.name)));
|
|
3675
3832
|
}
|
|
3676
3833
|
}
|
|
3677
3834
|
};
|
|
@@ -3681,7 +3838,7 @@ function findSecretFiles(dir) {
|
|
|
3681
3838
|
function findSecretFilesInTree(root, dir) {
|
|
3682
3839
|
try {
|
|
3683
3840
|
const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
3684
|
-
return out.split("\0").filter((f) => f && looksSecret(
|
|
3841
|
+
return out.split("\0").filter((f) => f && looksSecret(path20.basename(f)) && !path20.relative(dir, path20.join(root, f)).startsWith("..")).map((f) => path20.relative(dir, path20.join(root, f))).sort();
|
|
3685
3842
|
} catch {
|
|
3686
3843
|
return [];
|
|
3687
3844
|
}
|
|
@@ -3695,7 +3852,7 @@ function secretWarning(files, mode) {
|
|
|
3695
3852
|
}
|
|
3696
3853
|
|
|
3697
3854
|
// src/core/run.ts
|
|
3698
|
-
var VERSION2 = true ? "0.
|
|
3855
|
+
var VERSION2 = true ? "0.7.0" : "0.0.0-dev";
|
|
3699
3856
|
function planWork(o) {
|
|
3700
3857
|
if (!o.plan) return { task: o.task.trim(), tier: o.tier };
|
|
3701
3858
|
if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
|
|
@@ -3727,7 +3884,7 @@ function prepareRun(o) {
|
|
|
3727
3884
|
parent = readMeta(o.continueFrom);
|
|
3728
3885
|
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");
|
|
3729
3886
|
if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
|
|
3730
|
-
if (parent.mode === "isolate" && (!parent.worktree || !
|
|
3887
|
+
if (parent.mode === "isolate" && (!parent.worktree || !fs25.existsSync(parent.worktree))) {
|
|
3731
3888
|
throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
|
|
3732
3889
|
}
|
|
3733
3890
|
const ran = parent.ran ?? parent.worker;
|
|
@@ -3746,15 +3903,15 @@ function prepareRun(o) {
|
|
|
3746
3903
|
throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
|
|
3747
3904
|
}
|
|
3748
3905
|
const mode = parent?.mode ?? o.mode;
|
|
3749
|
-
const dir =
|
|
3750
|
-
if (!
|
|
3906
|
+
const dir = path21.resolve(parent?.dir ?? o.dir);
|
|
3907
|
+
if (!fs25.existsSync(dir) || !fs25.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
|
|
3751
3908
|
const root = repoRoot(dir);
|
|
3752
3909
|
if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
|
|
3753
3910
|
if (mode === "write" && !root && !o.allowNonGit) {
|
|
3754
3911
|
throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
|
|
3755
3912
|
}
|
|
3756
|
-
const files = o.files.map((f) =>
|
|
3757
|
-
for (const f of files) if (!
|
|
3913
|
+
const files = o.files.map((f) => path21.resolve(f));
|
|
3914
|
+
for (const f of files) if (!fs25.existsSync(f)) throw new UserError(`file not found: ${f}`);
|
|
3758
3915
|
if (!parent && !process.env.PITROOM_NO_SECRET_WARNING) {
|
|
3759
3916
|
const secrets = mode === "isolate" && root ? findSecretFilesInTree(root, dir) : findSecretFiles(dir);
|
|
3760
3917
|
const note = secretWarning(secrets, mode);
|
|
@@ -3794,13 +3951,13 @@ function prepareRun(o) {
|
|
|
3794
3951
|
try {
|
|
3795
3952
|
acquireWriteLock(root, meta.id);
|
|
3796
3953
|
} catch (e) {
|
|
3797
|
-
|
|
3954
|
+
fs25.rmSync(runDir(meta.id), { recursive: true, force: true });
|
|
3798
3955
|
throw e;
|
|
3799
3956
|
}
|
|
3800
3957
|
}
|
|
3801
|
-
|
|
3958
|
+
fs25.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
|
|
3802
3959
|
`);
|
|
3803
|
-
if (work.brief)
|
|
3960
|
+
if (work.brief) fs25.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
|
|
3804
3961
|
return meta;
|
|
3805
3962
|
}
|
|
3806
3963
|
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -3873,7 +4030,7 @@ async function execute(meta) {
|
|
|
3873
4030
|
(meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
|
|
3874
4031
|
for (const f of ["events.jsonl", "stderr.log"]) {
|
|
3875
4032
|
const from = runFile(meta.id, f);
|
|
3876
|
-
if (
|
|
4033
|
+
if (fs25.existsSync(from)) fs25.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
|
|
3877
4034
|
}
|
|
3878
4035
|
writeMeta(meta);
|
|
3879
4036
|
}
|
|
@@ -3893,9 +4050,9 @@ function prepareTree(meta) {
|
|
|
3893
4050
|
if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
|
|
3894
4051
|
if (root && meta.mode === "isolate" && !meta.worktree) {
|
|
3895
4052
|
meta.baseTree = snapshotTree(root);
|
|
3896
|
-
meta.worktree =
|
|
4053
|
+
meta.worktree = path21.join(worktreesDir(), meta.id);
|
|
3897
4054
|
createIsolatedCopy(root, meta.baseTree, meta.worktree);
|
|
3898
|
-
meta.cwd =
|
|
4055
|
+
meta.cwd = path21.join(meta.worktree, path21.relative(root, meta.dir));
|
|
3899
4056
|
const linked = linkIntoWorktree(root, meta.worktree, meta.link);
|
|
3900
4057
|
if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
|
|
3901
4058
|
}
|
|
@@ -3918,7 +4075,7 @@ function attempt(meta, backend, target) {
|
|
|
3918
4075
|
timeoutSec: meta.timeoutSec
|
|
3919
4076
|
});
|
|
3920
4077
|
}
|
|
3921
|
-
var read = (f) =>
|
|
4078
|
+
var read = (f) => fs25.existsSync(f) ? fs25.readFileSync(f, "utf8") : "";
|
|
3922
4079
|
function retryableFailure(meta, backend, res) {
|
|
3923
4080
|
const run = backend.parse(read(runFile(meta.id, "events.jsonl")));
|
|
3924
4081
|
if (run.usage.steps > 0) return void 0;
|
|
@@ -3937,12 +4094,12 @@ function finalize(meta, res) {
|
|
|
3937
4094
|
const run = backend.parse(read(runFile(meta.id, "events.jsonl")));
|
|
3938
4095
|
meta.sessionId = run.sessionId ?? meta.sessionId;
|
|
3939
4096
|
meta.usage = run.usage;
|
|
3940
|
-
|
|
4097
|
+
fs25.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
|
|
3941
4098
|
`);
|
|
3942
4099
|
if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run.finalText);
|
|
3943
4100
|
if (meta.reviewOf) {
|
|
3944
4101
|
meta.verdict = parseVerdict(run.finalText);
|
|
3945
|
-
if (meta.packageFile)
|
|
4102
|
+
if (meta.packageFile) fs25.rmSync(meta.packageFile, { force: true });
|
|
3946
4103
|
}
|
|
3947
4104
|
if (!res.timedOut && !res.stopped && !meta.error) {
|
|
3948
4105
|
const f = backend.failure(run, read(runFile(meta.id, "stderr.log")), res.code);
|
|
@@ -3984,7 +4141,7 @@ function captureChanges(meta) {
|
|
|
3984
4141
|
const d = diffTrees(target, meta.baseTree, meta.afterTree);
|
|
3985
4142
|
meta.changes = d.changes;
|
|
3986
4143
|
meta.stats = d.stats;
|
|
3987
|
-
|
|
4144
|
+
fs25.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
|
|
3988
4145
|
}
|
|
3989
4146
|
function runVerify(meta) {
|
|
3990
4147
|
const r = spawnSync5(meta.verify, {
|
|
@@ -3997,7 +4154,7 @@ function runVerify(meta) {
|
|
|
3997
4154
|
env: { ...process.env, PWD: meta.cwd, PITROOM_ACTIVE: "1" }
|
|
3998
4155
|
});
|
|
3999
4156
|
const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
|
4000
|
-
|
|
4157
|
+
fs25.writeFileSync(runFile(meta.id, "verify.log"), output);
|
|
4001
4158
|
return { ok: r.status === 0, code: r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
|
|
4002
4159
|
}
|
|
4003
4160
|
function applyRun(meta, allowDelete = false) {
|
|
@@ -4042,7 +4199,7 @@ function discardRun(meta) {
|
|
|
4042
4199
|
return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
|
|
4043
4200
|
}
|
|
4044
4201
|
function cleanupWorktree(meta) {
|
|
4045
|
-
if (meta.worktree &&
|
|
4202
|
+
if (meta.worktree && fs25.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
|
|
4046
4203
|
}
|
|
4047
4204
|
|
|
4048
4205
|
// src/cli/commands.ts
|
|
@@ -4092,10 +4249,10 @@ async function cmdReview(p) {
|
|
|
4092
4249
|
writeMeta(meta);
|
|
4093
4250
|
}
|
|
4094
4251
|
} catch (e) {
|
|
4095
|
-
|
|
4252
|
+
fs26.rmSync(packageFile, { force: true });
|
|
4096
4253
|
throw e;
|
|
4097
4254
|
}
|
|
4098
|
-
|
|
4255
|
+
fs26.writeFileSync(runFile(meta.id, "package.md"), job.package);
|
|
4099
4256
|
return launch(p, meta);
|
|
4100
4257
|
}
|
|
4101
4258
|
function cmdPlan(p) {
|
|
@@ -4115,7 +4272,7 @@ function cmdPlan(p) {
|
|
|
4115
4272
|
}
|
|
4116
4273
|
function cmdCrew(p) {
|
|
4117
4274
|
const file = flag(p, "task-file");
|
|
4118
|
-
const tasks = (file ?
|
|
4275
|
+
const tasks = (file ? fs26.readFileSync(file, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
|
|
4119
4276
|
if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
|
|
4120
4277
|
if (has(p, "write") && tasks.length > 1) {
|
|
4121
4278
|
throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
|
|
@@ -4224,7 +4381,7 @@ function cmdHistory(p) {
|
|
|
4224
4381
|
if (has(p, "json")) return console.log(JSON.stringify(s, null, 2)), 0;
|
|
4225
4382
|
const t = s.totals;
|
|
4226
4383
|
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`);
|
|
4227
|
-
const rows2 = s.byWorker.map((w) => [`${w.backend}${w.model ? `:${w.model}` : ""}`, String(w.runs), `${Math.round(100 * w.ok / w.runs)}
|
|
4384
|
+
const rows2 = s.byWorker.map((w) => [`${w.backend}${w.model ? `:${w.model}` : ""}`, String(w.runs), w.runs ? `${Math.round(100 * w.ok / w.runs)}%` : "-", secs(w.avgSeconds), w.avgTokens ? `${Math.round(w.avgTokens / 1e3)}k` : "-", `~${usd(w.saved)}`]);
|
|
4228
4385
|
const head2 = ["WORKER", "RUNS", "OK", "AVG TIME", "AVG TOKENS", "SAVED"];
|
|
4229
4386
|
const widths2 = head2.map((h, i) => Math.max(h.length, ...rows2.map((r) => r[i].length)));
|
|
4230
4387
|
const fmt2 = (r) => r.map((c, i) => i === 0 ? c.padEnd(widths2[i]) : c.padStart(widths2[i])).join(" ");
|
|
@@ -4239,8 +4396,11 @@ ${[fmt2(head2), ...rows2.map(fmt2)].join("\n")}`);
|
|
|
4239
4396
|
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]);
|
|
4240
4397
|
const head = ["RUN", "WHEN", "STATE", "WORKER", "TIME", "TASK"];
|
|
4241
4398
|
const widths = head.map((h, i) => Math.max(h.length, ...body.map((r) => r[i].length)));
|
|
4242
|
-
const fmt = (r) => r.map((c, i) =>
|
|
4243
|
-
|
|
4399
|
+
const fmt = (r, header = false) => r.map((c, i) => {
|
|
4400
|
+
const cell2 = i === r.length - 1 ? c : c.padEnd(widths[i]);
|
|
4401
|
+
return header ? dim(cell2) : i === 2 ? stateColour(r[2], cell2) : cell2;
|
|
4402
|
+
}).join(" ");
|
|
4403
|
+
console.log([fmt(head, true), ...body.map((r) => fmt(r))].join("\n"));
|
|
4244
4404
|
if (total > rows.length) console.log(`
|
|
4245
4405
|
${rows.length} of ${total} \xB7 --limit N for more \xB7 pitroom show <run> for one`);
|
|
4246
4406
|
return 0;
|
|
@@ -4313,7 +4473,7 @@ function sinceMs(s) {
|
|
|
4313
4473
|
if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
|
|
4314
4474
|
return Date.now() - Number(m[1]) * 864e5;
|
|
4315
4475
|
}
|
|
4316
|
-
var readStdin = () => process.stdin.isTTY ? "" :
|
|
4476
|
+
var readStdin = () => process.stdin.isTTY ? "" : fs26.readFileSync(0, "utf8");
|
|
4317
4477
|
function cmdStatusline(p) {
|
|
4318
4478
|
const input = readStdin();
|
|
4319
4479
|
const lines = [];
|
|
@@ -4352,6 +4512,18 @@ function cmdModels(p) {
|
|
|
4352
4512
|
console.log(has(p, "json") ? JSON.stringify(table2, null, 2) : formatModels(table2));
|
|
4353
4513
|
return 0;
|
|
4354
4514
|
}
|
|
4515
|
+
function cmdHookStart() {
|
|
4516
|
+
const script = path22.join(path22.dirname(path22.dirname(fileURLToPath3(import.meta.url))), "hooks", "session-start.mjs");
|
|
4517
|
+
if (fs26.existsSync(script)) spawnSync6(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
|
|
4518
|
+
return 0;
|
|
4519
|
+
}
|
|
4520
|
+
function cmdInit(p) {
|
|
4521
|
+
const fallback = flag(p, "fallback");
|
|
4522
|
+
const plan = planInit({ model: flag(p, "model"), fallback: fallback === void 0 ? void 0 : fallback.split(",").map((s) => s.trim()).filter(Boolean) });
|
|
4523
|
+
const written = has(p, "yes") ? writeInit(plan, has(p, "force")) : void 0;
|
|
4524
|
+
console.log(has(p, "json") ? JSON.stringify({ ...plan, written }, null, 2) : formatInit(plan, written));
|
|
4525
|
+
return 0;
|
|
4526
|
+
}
|
|
4355
4527
|
function cmdSavings(p) {
|
|
4356
4528
|
const since = flag(p, "since") ?? "all";
|
|
4357
4529
|
const t = totals(readLedger(sinceMs(since)));
|
|
@@ -4382,8 +4554,8 @@ function cmdSavings(p) {
|
|
|
4382
4554
|
est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
|
|
4383
4555
|
const out = flag(p, "card");
|
|
4384
4556
|
if (out) {
|
|
4385
|
-
|
|
4386
|
-
console.log(`card written to ${
|
|
4557
|
+
fs26.writeFileSync(out, card(t, period));
|
|
4558
|
+
console.log(`card written to ${path22.resolve(out)}`);
|
|
4387
4559
|
}
|
|
4388
4560
|
if (has(p, "badge")) console.log(`})`);
|
|
4389
4561
|
return 0;
|
|
@@ -4403,7 +4575,7 @@ function cmdConfig(p) {
|
|
|
4403
4575
|
console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
|
|
4404
4576
|
return 0;
|
|
4405
4577
|
}
|
|
4406
|
-
console.log(`config file: ${configPath()}${
|
|
4578
|
+
console.log(`config file: ${configPath()}${fs26.existsSync(configPath()) ? "" : " (not present)"}`);
|
|
4407
4579
|
for (const [key, s] of Object.entries(eff)) {
|
|
4408
4580
|
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);
|
|
4409
4581
|
console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
|
|
@@ -4427,7 +4599,7 @@ function cmdClean(p) {
|
|
|
4427
4599
|
const m = readMeta(id);
|
|
4428
4600
|
if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
|
|
4429
4601
|
recordRun(m);
|
|
4430
|
-
|
|
4602
|
+
fs26.rmSync(runDir(id), { recursive: true, force: true });
|
|
4431
4603
|
}
|
|
4432
4604
|
console.log(`removed ${old.length} run(s); the history and the savings ledger are kept`);
|
|
4433
4605
|
return 0;
|
|
@@ -4437,10 +4609,33 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
|
|
|
4437
4609
|
|
|
4438
4610
|
// src/core/doctor.ts
|
|
4439
4611
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
4440
|
-
import
|
|
4612
|
+
import fs27 from "node:fs";
|
|
4441
4613
|
import os9 from "node:os";
|
|
4442
|
-
import
|
|
4443
|
-
var MARK = { ok: "\u2714", warn: "!", fail: "\u2718" };
|
|
4614
|
+
import path23 from "node:path";
|
|
4615
|
+
var MARK = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
|
|
4616
|
+
var NEXT = [
|
|
4617
|
+
{ when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
|
|
4618
|
+
{ when: /pitroom install|no Pitroom skills|launcher on PATH/, command: "pitroom install", why: "link the skills and the pitroom command" },
|
|
4619
|
+
{ when: /first `node` on PATH/, command: "nvm alias default 24", why: "a current Node first in every new shell" },
|
|
4620
|
+
{ when: /not logged in/, command: "claude auth login", why: "sign in the Claude Code worker (Codex: codex login)" }
|
|
4621
|
+
];
|
|
4622
|
+
function firstNodeOnPath() {
|
|
4623
|
+
for (const dir of (process.env.PATH ?? "").split(path23.delimiter).filter(Boolean)) {
|
|
4624
|
+
const file = path23.join(dir, process.platform === "win32" ? "node.exe" : "node");
|
|
4625
|
+
try {
|
|
4626
|
+
if (!fs27.statSync(file).isFile()) continue;
|
|
4627
|
+
} catch {
|
|
4628
|
+
continue;
|
|
4629
|
+
}
|
|
4630
|
+
const r = spawnSync7(file, ["-p", "process.versions.node"], { encoding: "utf8", timeout: 5e3 });
|
|
4631
|
+
return r.status === 0 ? { path: file, version: r.stdout.trim() } : { path: file, version: "" };
|
|
4632
|
+
}
|
|
4633
|
+
return void 0;
|
|
4634
|
+
}
|
|
4635
|
+
var tooOld = (v) => {
|
|
4636
|
+
const [a = 0, b = 0] = v.split(".").map(Number);
|
|
4637
|
+
return a < 22 || a === 22 && b < 13;
|
|
4638
|
+
};
|
|
4444
4639
|
var READ_ONLY_HOW = {
|
|
4445
4640
|
"permission-rules": "per-run permission rules",
|
|
4446
4641
|
"os-sandbox": "an OS sandbox",
|
|
@@ -4449,16 +4644,24 @@ var READ_ONLY_HOW = {
|
|
|
4449
4644
|
};
|
|
4450
4645
|
function doctor4(probe) {
|
|
4451
4646
|
const checks = [];
|
|
4452
|
-
|
|
4647
|
+
let current = "Setup";
|
|
4648
|
+
const section2 = (name) => void (current = name);
|
|
4649
|
+
const add = (level, message) => checks.push({ level, message, section: current });
|
|
4650
|
+
const addAll = (list2) => list2.forEach((c) => add(c.level, c.message));
|
|
4453
4651
|
add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
|
|
4652
|
+
const onPath = firstNodeOnPath();
|
|
4653
|
+
if (onPath && onPath.version && tooOld(onPath.version) && path23.resolve(onPath.path) !== path23.resolve(process.execPath)) {
|
|
4654
|
+
add("warn", `the first \`node\` on PATH is v${onPath.version} (${onPath.path}), older than the 22.13 Pitroom needs: shells that agent apps start may use it (the pitroom command finds a newer Node itself; other tools may not)`);
|
|
4655
|
+
}
|
|
4454
4656
|
add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
|
|
4455
4657
|
const cfg = loadConfig();
|
|
4456
|
-
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${
|
|
4658
|
+
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs27.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
|
|
4457
4659
|
for (const w of cfg.warnings) add("warn", w);
|
|
4458
4660
|
if (process.platform !== "win32") {
|
|
4459
4661
|
const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
|
|
4460
4662
|
add(guarded ? "ok" : "warn", guarded ? "git guard shim ready" : "git guard unavailable (git not on PATH)");
|
|
4461
4663
|
}
|
|
4664
|
+
section2("Worker chain");
|
|
4462
4665
|
const chain = [];
|
|
4463
4666
|
try {
|
|
4464
4667
|
const resolved = resolveChain();
|
|
@@ -4499,14 +4702,39 @@ function doctor4(probe) {
|
|
|
4499
4702
|
add("fail", e.message);
|
|
4500
4703
|
continue;
|
|
4501
4704
|
}
|
|
4502
|
-
|
|
4705
|
+
section2(backend.name);
|
|
4706
|
+
addAll(backend.doctor({ models: [...new Set(models)], hasFallback: chain.length > 1 }));
|
|
4503
4707
|
add("ok", `${backend.name}: read-only runs enforced by ${READ_ONLY_HOW[backend.capabilities.readOnly]}`);
|
|
4504
4708
|
}
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4709
|
+
section2("Skills and agents");
|
|
4710
|
+
addAll(skillChecks());
|
|
4711
|
+
if (probe && chain[0]) {
|
|
4712
|
+
section2("Live probe");
|
|
4713
|
+
const p = liveProbe(chain[0]);
|
|
4714
|
+
add(p.level, p.message);
|
|
4715
|
+
}
|
|
4716
|
+
print(checks);
|
|
4508
4717
|
return checks.some((c) => c.level === "fail") ? 1 : 0;
|
|
4509
4718
|
}
|
|
4719
|
+
function print(checks) {
|
|
4720
|
+
console.log(`${bold("Pitroom doctor")} ${dim(`v${VERSION2}`)}`);
|
|
4721
|
+
for (const name of [...new Set(checks.map((c) => c.section))]) {
|
|
4722
|
+
console.log(`
|
|
4723
|
+
${bold(name)}`);
|
|
4724
|
+
for (const c of checks.filter((x) => x.section === name)) console.log(` ${MARK[c.level]()} ${wrapText(c.message, 4)}`);
|
|
4725
|
+
}
|
|
4726
|
+
const count = (level) => checks.filter((c) => c.level === level).length;
|
|
4727
|
+
const [ok, warn, fail] = [count("ok"), count("warn"), count("fail")];
|
|
4728
|
+
console.log(`
|
|
4729
|
+
${green(`\u2714 ${ok} ok`)} ${warn ? yellow(`! ${warn} warning${warn === 1 ? "" : "s"}`) : dim("! 0 warnings")} ${fail ? red(`\u2718 ${fail} problem${fail === 1 ? "" : "s"}`) : dim("\u2718 0 problems")}`);
|
|
4730
|
+
const next = NEXT.filter((n) => checks.some((c) => c.level !== "ok" && n.when.test(c.message)));
|
|
4731
|
+
if (next.length) {
|
|
4732
|
+
console.log(`
|
|
4733
|
+
${bold("Next")}`);
|
|
4734
|
+
const width = Math.max(...next.map((n) => n.command.length));
|
|
4735
|
+
for (const n of next) console.log(` ${cyan(n.command.padEnd(width))} ${dim(n.why)}`);
|
|
4736
|
+
}
|
|
4737
|
+
}
|
|
4510
4738
|
function liveProbe(target) {
|
|
4511
4739
|
let backend;
|
|
4512
4740
|
try {
|
|
@@ -4554,13 +4782,13 @@ function skillChecks() {
|
|
|
4554
4782
|
else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
|
|
4555
4783
|
}
|
|
4556
4784
|
if (viaPlugin) {
|
|
4557
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
4785
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path23.sep}.claude${path23.sep}`) && i.names.length);
|
|
4558
4786
|
checks.push(
|
|
4559
4787
|
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)" }
|
|
4560
4788
|
);
|
|
4561
4789
|
}
|
|
4562
4790
|
const launcher = launcherPath();
|
|
4563
|
-
if (
|
|
4791
|
+
if (fs27.existsSync(launcher)) {
|
|
4564
4792
|
const r = spawnSync7(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
4565
4793
|
checks.push(
|
|
4566
4794
|
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)}` }
|
|
@@ -4572,8 +4800,8 @@ function skillChecks() {
|
|
|
4572
4800
|
}
|
|
4573
4801
|
function pluginInstalled() {
|
|
4574
4802
|
try {
|
|
4575
|
-
const f =
|
|
4576
|
-
const plugins = JSON.parse(
|
|
4803
|
+
const f = path23.join(os9.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
4804
|
+
const plugins = JSON.parse(fs27.readFileSync(f, "utf8")).plugins ?? {};
|
|
4577
4805
|
return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
|
|
4578
4806
|
} catch {
|
|
4579
4807
|
return false;
|
|
@@ -4584,30 +4812,30 @@ function superpowersActive() {
|
|
|
4584
4812
|
for (const key of claudePlugins()) {
|
|
4585
4813
|
if (key.startsWith("superpowers@") && claudePluginEnabled(key)) found.push(`Claude Code plugin ${key}`);
|
|
4586
4814
|
}
|
|
4587
|
-
for (const [key,
|
|
4588
|
-
if (key.startsWith("superpowers@") &&
|
|
4815
|
+
for (const [key, enabled2] of codexPlugins()) {
|
|
4816
|
+
if (key.startsWith("superpowers@") && enabled2) found.push(`Codex plugin ${key}`);
|
|
4589
4817
|
}
|
|
4590
4818
|
for (const p of openCodePlugins()) {
|
|
4591
4819
|
if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
|
|
4592
4820
|
}
|
|
4593
|
-
for (const base2 of [
|
|
4594
|
-
const dir =
|
|
4595
|
-
if (
|
|
4821
|
+
for (const base2 of [path23.join(os9.homedir(), ".agents", "skills"), path23.join(os9.homedir(), ".claude", "skills")]) {
|
|
4822
|
+
const dir = path23.join(base2, "using-superpowers");
|
|
4823
|
+
if (fs27.existsSync(path23.join(dir, "SKILL.md"))) found.push(dir);
|
|
4596
4824
|
}
|
|
4597
4825
|
return found;
|
|
4598
4826
|
}
|
|
4599
4827
|
function claudePlugins() {
|
|
4600
4828
|
try {
|
|
4601
|
-
const f =
|
|
4602
|
-
return Object.keys(JSON.parse(
|
|
4829
|
+
const f = path23.join(os9.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
4830
|
+
return Object.keys(JSON.parse(fs27.readFileSync(f, "utf8")).plugins ?? {});
|
|
4603
4831
|
} catch {
|
|
4604
4832
|
return [];
|
|
4605
4833
|
}
|
|
4606
4834
|
}
|
|
4607
4835
|
function claudePluginEnabled(key) {
|
|
4608
4836
|
try {
|
|
4609
|
-
const f =
|
|
4610
|
-
return JSON.parse(
|
|
4837
|
+
const f = path23.join(os9.homedir(), ".claude", "settings.json");
|
|
4838
|
+
return JSON.parse(fs27.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
|
|
4611
4839
|
} catch {
|
|
4612
4840
|
return true;
|
|
4613
4841
|
}
|
|
@@ -4616,7 +4844,7 @@ function codexPlugins() {
|
|
|
4616
4844
|
const plugins = /* @__PURE__ */ new Map();
|
|
4617
4845
|
let text;
|
|
4618
4846
|
try {
|
|
4619
|
-
text =
|
|
4847
|
+
text = fs27.readFileSync(path23.join(process.env.CODEX_HOME ?? path23.join(os9.homedir(), ".codex"), "config.toml"), "utf8");
|
|
4620
4848
|
} catch {
|
|
4621
4849
|
return plugins;
|
|
4622
4850
|
}
|
|
@@ -4634,11 +4862,11 @@ function codexPlugins() {
|
|
|
4634
4862
|
return plugins;
|
|
4635
4863
|
}
|
|
4636
4864
|
function openCodePlugins() {
|
|
4637
|
-
const dir =
|
|
4865
|
+
const dir = path23.join(process.env.XDG_CONFIG_HOME ?? path23.join(os9.homedir(), ".config"), "opencode");
|
|
4638
4866
|
const plugins = [];
|
|
4639
4867
|
for (const name of ["opencode.json", "opencode.jsonc"]) {
|
|
4640
4868
|
try {
|
|
4641
|
-
const text =
|
|
4869
|
+
const text = fs27.readFileSync(path23.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
|
|
4642
4870
|
const list2 = JSON.parse(text).plugin;
|
|
4643
4871
|
if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
|
|
4644
4872
|
} catch {
|
|
@@ -4646,7 +4874,7 @@ function openCodePlugins() {
|
|
|
4646
4874
|
}
|
|
4647
4875
|
for (const folder of ["plugin", "plugins"]) {
|
|
4648
4876
|
try {
|
|
4649
|
-
for (const f of
|
|
4877
|
+
for (const f of fs27.readdirSync(path23.join(dir, folder))) plugins.push(path23.join(dir, folder, f));
|
|
4650
4878
|
} catch {
|
|
4651
4879
|
}
|
|
4652
4880
|
}
|
|
@@ -4693,8 +4921,12 @@ Usage
|
|
|
4693
4921
|
models each worker offers, with effort levels, your costs and usage
|
|
4694
4922
|
pitroom statusline [--then CMD] status-bar line: running workers, savings this week (after CMD's)
|
|
4695
4923
|
pitroom hook-card PostToolUse hook: a card after each Bash \`pitroom\` command
|
|
4924
|
+
pitroom hook-start SessionStart hook: introduces Pitroom to the agent (Codex; Claude Code uses its plugin)
|
|
4696
4925
|
pitroom doctor [--probe] check workers, models, permissions, skills
|
|
4697
4926
|
pitroom config effective settings, where each comes from, config file path
|
|
4927
|
+
pitroom init [--model ID] [--fallback A,B] [--yes] [--force]
|
|
4928
|
+
propose a starter config from the worker CLIs and models you have;
|
|
4929
|
+
writes it only with --yes (models are suggested, never chosen for you)
|
|
4698
4930
|
pitroom install [--copy] [--force] link the skills into ~/.agents/skills + ~/.claude/skills,
|
|
4699
4931
|
and the CLI into ~/.local/bin
|
|
4700
4932
|
pitroom uninstall remove what install linked
|
|
@@ -4754,8 +4986,10 @@ var COMMANDS = {
|
|
|
4754
4986
|
models: cmdModels,
|
|
4755
4987
|
statusline: cmdStatusline,
|
|
4756
4988
|
"hook-card": cmdHookCard,
|
|
4989
|
+
"hook-start": cmdHookStart,
|
|
4757
4990
|
doctor: (p) => doctor4(has(p, "probe")),
|
|
4758
4991
|
config: cmdConfig,
|
|
4992
|
+
init: cmdInit,
|
|
4759
4993
|
install: cmdInstall,
|
|
4760
4994
|
uninstall: cmdUninstall,
|
|
4761
4995
|
clean: cmdClean,
|