pitroom 0.6.10 → 0.8.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 +32 -0
- package/README.md +4 -4
- package/THIRD_PARTY_NOTICES.md +2 -1
- package/bin/pitroom.cjs +68 -0
- package/dist/pitroom.mjs +608 -264
- package/dist/ui/app.css +1 -1
- package/dist/ui/app.js +170 -14
- package/hooks/codex-hooks.json +28 -0
- package/package.json +4 -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.catalog ? b.catalog().models.map((m) => m.id) : 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
|
}
|
|
@@ -1822,7 +1950,7 @@ function archivedRun(id) {
|
|
|
1822
1950
|
at: s.at ?? void 0,
|
|
1823
1951
|
t: s.at && Number.isFinite(start) ? Math.max(0, Math.round((s.at - start) / 1e3)) : void 0
|
|
1824
1952
|
}));
|
|
1825
|
-
return { meta: JSON.parse(r.meta_json), answer: r.answer ?? "", patch: r.patch ?? void 0, steps };
|
|
1953
|
+
return { meta: JSON.parse(r.meta_json), answer: r.answer ?? "", patch: r.patch ?? void 0, patchTruncated: r.patch?.length >= PATCH_MAX, steps };
|
|
1826
1954
|
} catch {
|
|
1827
1955
|
return void 0;
|
|
1828
1956
|
}
|
|
@@ -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,11 +2964,123 @@ 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
|
|
|
2972
|
+
// src/core/file-diff.ts
|
|
2973
|
+
var PREVIEW_LINES = 300;
|
|
2974
|
+
function gitPath(value, prefix = true) {
|
|
2975
|
+
let path24 = value;
|
|
2976
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
2977
|
+
const bytes = [];
|
|
2978
|
+
const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
|
|
2979
|
+
for (const match of value.slice(1, -1).matchAll(/\\([0-7]{1,3}|.)|([^\\]+)/g)) {
|
|
2980
|
+
if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
|
|
2981
|
+
else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
|
|
2982
|
+
}
|
|
2983
|
+
path24 = new TextDecoder().decode(new Uint8Array(bytes));
|
|
2984
|
+
}
|
|
2985
|
+
return prefix ? path24.replace(/^[ab]\//, "") : path24;
|
|
2986
|
+
}
|
|
2987
|
+
function headerPath(header) {
|
|
2988
|
+
const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
|
|
2989
|
+
if (quoted) return gitPath(quoted[2]);
|
|
2990
|
+
return gitPath(header.slice(header.lastIndexOf(" b/") + 1));
|
|
2991
|
+
}
|
|
2992
|
+
function fileDiffs(patch, changes, sourceTruncated = false) {
|
|
2993
|
+
const files = [];
|
|
2994
|
+
let file;
|
|
2995
|
+
let oldLine = 0;
|
|
2996
|
+
let newLine = 0;
|
|
2997
|
+
let oldLeft = 0;
|
|
2998
|
+
let newLeft = 0;
|
|
2999
|
+
let inHunk = false;
|
|
3000
|
+
let row = 0;
|
|
3001
|
+
const finishHunk = () => {
|
|
3002
|
+
if (file && (oldLeft > 0 || newLeft > 0)) file.incomplete = true;
|
|
3003
|
+
oldLeft = newLeft = 0;
|
|
3004
|
+
inHunk = false;
|
|
3005
|
+
};
|
|
3006
|
+
const add = (line) => {
|
|
3007
|
+
if (!file) return;
|
|
3008
|
+
if (file.lines.length < PREVIEW_LINES) file.lines.push({ ...line, id: String(row) });
|
|
3009
|
+
else file.omittedLines++;
|
|
3010
|
+
row++;
|
|
3011
|
+
};
|
|
3012
|
+
for (const line of patch.split("\n")) {
|
|
3013
|
+
if (line.startsWith("diff --git ")) {
|
|
3014
|
+
finishHunk();
|
|
3015
|
+
file = { path: headerPath(line), status: "M", lines: [], additions: 0, deletions: 0, omittedLines: 0, binary: false, incomplete: false };
|
|
3016
|
+
files.push(file);
|
|
3017
|
+
row = 0;
|
|
3018
|
+
continue;
|
|
3019
|
+
}
|
|
3020
|
+
if (!file) continue;
|
|
3021
|
+
const hunk = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line);
|
|
3022
|
+
if (hunk) {
|
|
3023
|
+
finishHunk();
|
|
3024
|
+
oldLine = Number(hunk[1]);
|
|
3025
|
+
newLine = Number(hunk[3]);
|
|
3026
|
+
oldLeft = Number(hunk[2] ?? 1);
|
|
3027
|
+
newLeft = Number(hunk[4] ?? 1);
|
|
3028
|
+
inHunk = true;
|
|
3029
|
+
add({ type: "hunk", content: line });
|
|
3030
|
+
} else if (inHunk && line.startsWith("\")) {
|
|
3031
|
+
add({ type: "meta", content: line });
|
|
3032
|
+
} else if (inHunk && (oldLeft > 0 || newLeft > 0)) {
|
|
3033
|
+
if (line.startsWith("+") && newLeft > 0) {
|
|
3034
|
+
file.additions++;
|
|
3035
|
+
newLeft--;
|
|
3036
|
+
add({ type: "added", newLine: newLine++, content: line.slice(1) });
|
|
3037
|
+
} else if (line.startsWith("-") && oldLeft > 0) {
|
|
3038
|
+
file.deletions++;
|
|
3039
|
+
oldLeft--;
|
|
3040
|
+
add({ type: "removed", oldLine: oldLine++, content: line.slice(1) });
|
|
3041
|
+
} else if (line.startsWith(" ") && oldLeft > 0 && newLeft > 0) {
|
|
3042
|
+
oldLeft--;
|
|
3043
|
+
newLeft--;
|
|
3044
|
+
add({ type: "context", oldLine: oldLine++, newLine: newLine++, content: line.slice(1) });
|
|
3045
|
+
} else {
|
|
3046
|
+
file.incomplete = true;
|
|
3047
|
+
}
|
|
3048
|
+
} else if (!inHunk) {
|
|
3049
|
+
if (line.startsWith("+++ ") && line !== "+++ /dev/null") file.path = gitPath(line.slice(4).replace(/\t$/, ""));
|
|
3050
|
+
else if (line.startsWith("--- ") && line !== "--- /dev/null") file.path = gitPath(line.slice(4).replace(/\t$/, ""));
|
|
3051
|
+
else if (/^(GIT binary patch|Binary files .* differ)$/.test(line)) file.binary = true;
|
|
3052
|
+
else if (/^(new file mode|deleted file mode|old mode|new mode|rename from|rename to|similarity index) /.test(line)) {
|
|
3053
|
+
if (line.startsWith("new file mode ")) file.status = "A";
|
|
3054
|
+
if (line.startsWith("deleted file mode ")) file.status = "D";
|
|
3055
|
+
if (line.startsWith("rename to ")) {
|
|
3056
|
+
file.path = gitPath(line.slice(10), false);
|
|
3057
|
+
file.status = "R";
|
|
3058
|
+
}
|
|
3059
|
+
add({ type: "meta", content: line });
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
finishHunk();
|
|
3064
|
+
if (sourceTruncated && file) file.incomplete = true;
|
|
3065
|
+
for (const f of files) {
|
|
3066
|
+
if (f.incomplete || f.binary) f.additions = f.deletions = void 0;
|
|
3067
|
+
}
|
|
3068
|
+
const byPath = new Map(files.map((f) => [f.path, f]));
|
|
3069
|
+
const result = changes.map((change) => {
|
|
3070
|
+
const diff = byPath.get(change.path);
|
|
3071
|
+
byPath.delete(change.path);
|
|
3072
|
+
return diff ? { ...diff, status: change.status } : {
|
|
3073
|
+
...change,
|
|
3074
|
+
lines: [],
|
|
3075
|
+
omittedLines: 0,
|
|
3076
|
+
binary: false,
|
|
3077
|
+
incomplete: sourceTruncated,
|
|
3078
|
+
unavailable: true
|
|
3079
|
+
};
|
|
3080
|
+
});
|
|
3081
|
+
return [...result, ...byPath.values()];
|
|
3082
|
+
}
|
|
3083
|
+
|
|
2817
3084
|
// src/core/dash-page.ts
|
|
2818
3085
|
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
3086
|
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";
|
|
@@ -2846,7 +3113,8 @@ function toRun(m) {
|
|
|
2846
3113
|
id: m.id,
|
|
2847
3114
|
state: m.state,
|
|
2848
3115
|
kind: kind(m),
|
|
2849
|
-
|
|
3116
|
+
// the model that really ran, once the CLI has said which; until then just the worker, never a guess
|
|
3117
|
+
worker: workerName(ranTarget(m)),
|
|
2850
3118
|
task: what(m),
|
|
2851
3119
|
group: m.group,
|
|
2852
3120
|
startedAt: m.startedAt,
|
|
@@ -2885,10 +3153,6 @@ function dashState(opts = {}) {
|
|
|
2885
3153
|
}
|
|
2886
3154
|
var TASK_MAX = 6e3;
|
|
2887
3155
|
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
3156
|
function runDetail(id) {
|
|
2893
3157
|
if (!RUN_ID2.test(id)) return void 0;
|
|
2894
3158
|
let m;
|
|
@@ -2913,16 +3177,19 @@ function runDetail(id) {
|
|
|
2913
3177
|
const answer = isActive(m.state) ? "" : readSummary(m).replace(/^SUMMARY:\s*SPEC[^\n]*\n+/i, "").replace(/^DETAILS:[ \t]*\n+/i, "").trim();
|
|
2914
3178
|
const lastSay = steps.at(-1);
|
|
2915
3179
|
if (lastSay?.kind === "say" && answer && (answer.includes(lastSay.text.slice(0, 80)) || lastSay.text.includes(answer.slice(0, 80)))) steps.pop();
|
|
2916
|
-
const
|
|
3180
|
+
const fullPatch = readRunFile(id, "changes.patch");
|
|
3181
|
+
const patch = fullPatch ?? kept?.patch ?? "";
|
|
2917
3182
|
const patchLines = patch.split("\n");
|
|
2918
3183
|
const u = m.usage;
|
|
2919
3184
|
return {
|
|
2920
3185
|
id,
|
|
2921
3186
|
state: m.state,
|
|
3187
|
+
card: toRun(m),
|
|
2922
3188
|
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
3189
|
steps,
|
|
2924
3190
|
answer,
|
|
2925
3191
|
changes: m.changes ?? [],
|
|
3192
|
+
fileDiffs: fileDiffs(patch, m.changes ?? [], fullPatch === void 0 && kept?.patchTruncated),
|
|
2926
3193
|
patch: patch ? `${patchLines.slice(0, PATCH_LINES).join("\n")}${patchLines.length > PATCH_LINES ? `
|
|
2927
3194
|
\u2026 ${patchLines.length - PATCH_LINES} more lines (pitroom show ${id} --patch)` : ""}` : void 0,
|
|
2928
3195
|
info: {
|
|
@@ -2961,11 +3228,11 @@ var assetsDir = () => fileURLToPath2(new URL("./ui/", import.meta.url));
|
|
|
2961
3228
|
var assetCache = /* @__PURE__ */ new Map();
|
|
2962
3229
|
function readAsset(route) {
|
|
2963
3230
|
try {
|
|
2964
|
-
const file =
|
|
2965
|
-
const { mtimeMs } =
|
|
3231
|
+
const file = path16.join(assetsDir(), path16.basename(route));
|
|
3232
|
+
const { mtimeMs } = fs19.statSync(file);
|
|
2966
3233
|
const hit = assetCache.get(route);
|
|
2967
3234
|
if (hit && hit.mtimeMs === mtimeMs) return hit.data;
|
|
2968
|
-
const data =
|
|
3235
|
+
const data = fs19.readFileSync(file);
|
|
2969
3236
|
assetCache.set(route, { mtimeMs, data });
|
|
2970
3237
|
return data;
|
|
2971
3238
|
} catch {
|
|
@@ -3045,7 +3312,7 @@ function startDash(opts) {
|
|
|
3045
3312
|
});
|
|
3046
3313
|
});
|
|
3047
3314
|
}
|
|
3048
|
-
var registryFile = () =>
|
|
3315
|
+
var registryFile = () => path16.join(home(), "dash.json");
|
|
3049
3316
|
async function ping(port) {
|
|
3050
3317
|
try {
|
|
3051
3318
|
const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
|
|
@@ -3056,11 +3323,11 @@ async function ping(port) {
|
|
|
3056
3323
|
}
|
|
3057
3324
|
async function runningDash() {
|
|
3058
3325
|
try {
|
|
3059
|
-
const r = JSON.parse(
|
|
3326
|
+
const r = JSON.parse(fs19.readFileSync(registryFile(), "utf8"));
|
|
3060
3327
|
if (isAlive(r.pid) && await ping(r.port)) return { ...r, url: `http://127.0.0.1:${r.port}/` };
|
|
3061
3328
|
} catch {
|
|
3062
3329
|
}
|
|
3063
|
-
|
|
3330
|
+
fs19.rmSync(registryFile(), { force: true });
|
|
3064
3331
|
return void 0;
|
|
3065
3332
|
}
|
|
3066
3333
|
function openBrowser(url) {
|
|
@@ -3075,7 +3342,7 @@ async function dashCommand(o) {
|
|
|
3075
3342
|
if (o.stop) {
|
|
3076
3343
|
if (!existing) throw new UserError("pitroom dash is not running");
|
|
3077
3344
|
process.kill(existing.pid, "SIGTERM");
|
|
3078
|
-
|
|
3345
|
+
fs19.rmSync(registryFile(), { force: true });
|
|
3079
3346
|
o.log(`stopped pitroom dash (${existing.url})`);
|
|
3080
3347
|
return 0;
|
|
3081
3348
|
}
|
|
@@ -3112,8 +3379,8 @@ async function dashCommand(o) {
|
|
|
3112
3379
|
if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
|
|
3113
3380
|
dash = await startDash({ port: 0, idleMs: o.idleMs });
|
|
3114
3381
|
}
|
|
3115
|
-
|
|
3116
|
-
|
|
3382
|
+
fs19.mkdirSync(home(), { recursive: true });
|
|
3383
|
+
fs19.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
|
|
3117
3384
|
const stop = () => void dash.close();
|
|
3118
3385
|
process.once("SIGINT", stop);
|
|
3119
3386
|
process.once("SIGTERM", stop);
|
|
@@ -3122,7 +3389,7 @@ async function dashCommand(o) {
|
|
|
3122
3389
|
if (o.open) openBrowser(dash.url);
|
|
3123
3390
|
await dash.closed;
|
|
3124
3391
|
try {
|
|
3125
|
-
if (JSON.parse(
|
|
3392
|
+
if (JSON.parse(fs19.readFileSync(registryFile(), "utf8")).pid === process.pid) fs19.rmSync(registryFile(), { force: true });
|
|
3126
3393
|
} catch {
|
|
3127
3394
|
}
|
|
3128
3395
|
return 0;
|
|
@@ -3130,8 +3397,8 @@ async function dashCommand(o) {
|
|
|
3130
3397
|
|
|
3131
3398
|
// src/core/run.ts
|
|
3132
3399
|
import { spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
|
|
3133
|
-
import
|
|
3134
|
-
import
|
|
3400
|
+
import fs25 from "node:fs";
|
|
3401
|
+
import path21 from "node:path";
|
|
3135
3402
|
|
|
3136
3403
|
// src/core/chain.ts
|
|
3137
3404
|
function resolveChain(flags = {}) {
|
|
@@ -3181,11 +3448,11 @@ function resolveChain(flags = {}) {
|
|
|
3181
3448
|
|
|
3182
3449
|
// src/core/process.ts
|
|
3183
3450
|
import { spawn as spawn2 } from "node:child_process";
|
|
3184
|
-
import
|
|
3451
|
+
import fs21 from "node:fs";
|
|
3185
3452
|
|
|
3186
3453
|
// src/vcs/guard.ts
|
|
3187
|
-
import
|
|
3188
|
-
import
|
|
3454
|
+
import fs20 from "node:fs";
|
|
3455
|
+
import path17 from "node:path";
|
|
3189
3456
|
var VERSION = 1;
|
|
3190
3457
|
var SHIM = `#!/bin/sh
|
|
3191
3458
|
# pitroom git guard v${VERSION}. Blocks git commands that change history, refs, the
|
|
@@ -3260,15 +3527,15 @@ esac
|
|
|
3260
3527
|
exec "$real" "$@"
|
|
3261
3528
|
`;
|
|
3262
3529
|
function shimDir() {
|
|
3263
|
-
return
|
|
3530
|
+
return path17.join(home(), "shim", `v${VERSION}`);
|
|
3264
3531
|
}
|
|
3265
3532
|
function findRealGit(skip) {
|
|
3266
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
3267
|
-
if (
|
|
3268
|
-
const p =
|
|
3533
|
+
for (const dir of (process.env.PATH ?? "").split(path17.delimiter).filter(Boolean)) {
|
|
3534
|
+
if (path17.resolve(dir) === skip) continue;
|
|
3535
|
+
const p = path17.join(dir, "git");
|
|
3269
3536
|
try {
|
|
3270
|
-
|
|
3271
|
-
if (
|
|
3537
|
+
fs20.accessSync(p, fs20.constants.X_OK);
|
|
3538
|
+
if (fs20.statSync(p).isFile()) return p;
|
|
3272
3539
|
} catch {
|
|
3273
3540
|
}
|
|
3274
3541
|
}
|
|
@@ -3281,13 +3548,13 @@ var REF_HOOK = `#!/bin/sh
|
|
|
3281
3548
|
echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
|
|
3282
3549
|
exit 1
|
|
3283
3550
|
`;
|
|
3284
|
-
var hooksDir = () =>
|
|
3551
|
+
var hooksDir = () => path17.join(home(), "git-hooks", `v${HOOK_VERSION}`);
|
|
3285
3552
|
function writeIfChanged(file, content) {
|
|
3286
|
-
if (
|
|
3287
|
-
|
|
3553
|
+
if (fs20.existsSync(file) && fs20.readFileSync(file, "utf8") === content) return;
|
|
3554
|
+
fs20.mkdirSync(path17.dirname(file), { recursive: true });
|
|
3288
3555
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
3289
|
-
|
|
3290
|
-
|
|
3556
|
+
fs20.writeFileSync(tmp, content, { mode: 493 });
|
|
3557
|
+
fs20.renameSync(tmp, file);
|
|
3291
3558
|
}
|
|
3292
3559
|
function withGitConfig(env, entries) {
|
|
3293
3560
|
const out = { ...env };
|
|
@@ -3309,7 +3576,7 @@ function guardEnv(env) {
|
|
|
3309
3576
|
PAGER: "cat"
|
|
3310
3577
|
};
|
|
3311
3578
|
if (process.platform === "win32") return { ...env, ...quiet };
|
|
3312
|
-
writeIfChanged(
|
|
3579
|
+
writeIfChanged(path17.join(hooksDir(), "reference-transaction"), REF_HOOK);
|
|
3313
3580
|
env = withGitConfig(env, [
|
|
3314
3581
|
["core.hooksPath", hooksDir()],
|
|
3315
3582
|
["url.pitroom-push-blocked://.pushInsteadOf", ""]
|
|
@@ -3317,14 +3584,14 @@ function guardEnv(env) {
|
|
|
3317
3584
|
const dir = shimDir();
|
|
3318
3585
|
const real = findRealGit(dir);
|
|
3319
3586
|
if (!real) return { ...env, ...quiet };
|
|
3320
|
-
writeIfChanged(
|
|
3321
|
-
return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${
|
|
3587
|
+
writeIfChanged(path17.join(dir, "git"), SHIM);
|
|
3588
|
+
return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${path17.delimiter}${env.PATH ?? ""}` };
|
|
3322
3589
|
}
|
|
3323
3590
|
|
|
3324
3591
|
// src/core/process.ts
|
|
3325
3592
|
async function spawnWorker(inv, opts) {
|
|
3326
|
-
const out =
|
|
3327
|
-
const err =
|
|
3593
|
+
const out = fs21.openSync(opts.stdoutFile, "w");
|
|
3594
|
+
const err = fs21.openSync(opts.stderrFile, "w");
|
|
3328
3595
|
const res = { code: null, timedOut: false, stopped: false };
|
|
3329
3596
|
const child = spawn2(inv.command, inv.args, {
|
|
3330
3597
|
cwd: opts.cwd,
|
|
@@ -3356,8 +3623,8 @@ async function spawnWorker(inv, opts) {
|
|
|
3356
3623
|
if (killer) clearTimeout(killer);
|
|
3357
3624
|
process.off("SIGINT", stop);
|
|
3358
3625
|
process.off("SIGTERM", stop);
|
|
3359
|
-
|
|
3360
|
-
|
|
3626
|
+
fs21.closeSync(out);
|
|
3627
|
+
fs21.closeSync(err);
|
|
3361
3628
|
return res;
|
|
3362
3629
|
}
|
|
3363
3630
|
|
|
@@ -3383,10 +3650,10 @@ function parseVerdict(text) {
|
|
|
3383
3650
|
|
|
3384
3651
|
// src/core/slots.ts
|
|
3385
3652
|
import crypto6 from "node:crypto";
|
|
3386
|
-
import
|
|
3387
|
-
import
|
|
3388
|
-
var slotsDir = () =>
|
|
3389
|
-
var locksDir = () =>
|
|
3653
|
+
import fs22 from "node:fs";
|
|
3654
|
+
import path18 from "node:path";
|
|
3655
|
+
var slotsDir = () => path18.join(home(), "slots");
|
|
3656
|
+
var locksDir = () => path18.join(home(), "locks");
|
|
3390
3657
|
var STARTUP_GRACE_MS = 3e4;
|
|
3391
3658
|
function holderActive(runId, self) {
|
|
3392
3659
|
if (!runId || runId === self) return false;
|
|
@@ -3400,23 +3667,23 @@ function holderActive(runId, self) {
|
|
|
3400
3667
|
}
|
|
3401
3668
|
}
|
|
3402
3669
|
function tryClaim(file, runId) {
|
|
3403
|
-
|
|
3670
|
+
fs22.mkdirSync(path18.dirname(file), { recursive: true });
|
|
3404
3671
|
try {
|
|
3405
|
-
|
|
3672
|
+
fs22.writeFileSync(file, runId, { flag: "wx" });
|
|
3406
3673
|
return true;
|
|
3407
3674
|
} catch (e) {
|
|
3408
3675
|
if (e.code !== "EEXIST") throw e;
|
|
3409
3676
|
}
|
|
3410
3677
|
let owner = "";
|
|
3411
3678
|
try {
|
|
3412
|
-
owner =
|
|
3679
|
+
owner = fs22.readFileSync(file, "utf8").trim();
|
|
3413
3680
|
} catch {
|
|
3414
3681
|
}
|
|
3415
3682
|
if (owner === runId) return true;
|
|
3416
3683
|
if (holderActive(owner, runId)) return false;
|
|
3417
|
-
|
|
3684
|
+
fs22.rmSync(file, { force: true });
|
|
3418
3685
|
try {
|
|
3419
|
-
|
|
3686
|
+
fs22.writeFileSync(file, runId, { flag: "wx" });
|
|
3420
3687
|
return true;
|
|
3421
3688
|
} catch {
|
|
3422
3689
|
return false;
|
|
@@ -3425,25 +3692,25 @@ function tryClaim(file, runId) {
|
|
|
3425
3692
|
function releaseIfOwner(file, runId) {
|
|
3426
3693
|
if (!file) return;
|
|
3427
3694
|
try {
|
|
3428
|
-
if (
|
|
3695
|
+
if (fs22.readFileSync(file, "utf8").trim() === runId) fs22.rmSync(file, { force: true });
|
|
3429
3696
|
} catch {
|
|
3430
3697
|
}
|
|
3431
3698
|
}
|
|
3432
3699
|
function tryAcquireSlot(runId, maxParallel) {
|
|
3433
3700
|
for (let n = 0; n < Math.max(1, maxParallel); n++) {
|
|
3434
|
-
const file =
|
|
3701
|
+
const file = path18.join(slotsDir(), `slot-${n}`);
|
|
3435
3702
|
if (tryClaim(file, runId)) return file;
|
|
3436
3703
|
}
|
|
3437
3704
|
return void 0;
|
|
3438
3705
|
}
|
|
3439
3706
|
var releaseSlot = (file, runId) => releaseIfOwner(file, runId);
|
|
3440
|
-
var lockFile = (repoRoot2) =>
|
|
3707
|
+
var lockFile = (repoRoot2) => path18.join(locksDir(), `write-${crypto6.createHash("sha1").update(path18.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
|
|
3441
3708
|
function acquireWriteLock(repoRoot2, runId) {
|
|
3442
3709
|
const file = lockFile(repoRoot2);
|
|
3443
3710
|
if (tryClaim(file, runId)) return;
|
|
3444
3711
|
let owner = "";
|
|
3445
3712
|
try {
|
|
3446
|
-
owner =
|
|
3713
|
+
owner = fs22.readFileSync(file, "utf8").trim();
|
|
3447
3714
|
} catch {
|
|
3448
3715
|
}
|
|
3449
3716
|
throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
|
|
@@ -3490,8 +3757,8 @@ ${task}`;
|
|
|
3490
3757
|
}
|
|
3491
3758
|
|
|
3492
3759
|
// src/core/refs.ts
|
|
3493
|
-
import
|
|
3494
|
-
import
|
|
3760
|
+
import fs23 from "node:fs";
|
|
3761
|
+
import path19 from "node:path";
|
|
3495
3762
|
var FILE_EXTENSIONS = new Set(
|
|
3496
3763
|
"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
3764
|
);
|
|
@@ -3542,13 +3809,13 @@ function extractRefs(answer) {
|
|
|
3542
3809
|
return [...seen.values()];
|
|
3543
3810
|
}
|
|
3544
3811
|
function verifyRefs(refs, dirs) {
|
|
3545
|
-
const roots = [...new Set(dirs.filter(Boolean).map((d) =>
|
|
3812
|
+
const roots = [...new Set(dirs.filter(Boolean).map((d) => path19.resolve(d)))];
|
|
3546
3813
|
const invalid = [];
|
|
3547
3814
|
const lineCache = /* @__PURE__ */ new Map();
|
|
3548
3815
|
const load = (file) => {
|
|
3549
3816
|
if (!lineCache.has(file)) {
|
|
3550
|
-
const size =
|
|
3551
|
-
const lines = size > MAX_BYTES ? null :
|
|
3817
|
+
const size = fs23.statSync(file).size;
|
|
3818
|
+
const lines = size > MAX_BYTES ? null : fs23.readFileSync(file, "utf8").split("\n");
|
|
3552
3819
|
if (lines && lines[lines.length - 1] === "") lines.pop();
|
|
3553
3820
|
lineCache.set(file, lines);
|
|
3554
3821
|
}
|
|
@@ -3559,9 +3826,9 @@ function verifyRefs(refs, dirs) {
|
|
|
3559
3826
|
const candidates = (ref) => {
|
|
3560
3827
|
const direct = resolve(ref.file, roots);
|
|
3561
3828
|
if (direct) return [direct];
|
|
3562
|
-
if (
|
|
3829
|
+
if (path19.isAbsolute(ref.file)) return [];
|
|
3563
3830
|
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
3564
|
-
return (byName().get(
|
|
3831
|
+
return (byName().get(path19.basename(wanted)) ?? []).filter((f) => f.endsWith(`/${wanted}`) || path19.basename(f) === wanted).slice(0, 20);
|
|
3565
3832
|
};
|
|
3566
3833
|
const check = (file, ref) => {
|
|
3567
3834
|
const lines = load(file);
|
|
@@ -3576,7 +3843,7 @@ function verifyRefs(refs, dirs) {
|
|
|
3576
3843
|
if (!files.length) {
|
|
3577
3844
|
if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
|
|
3578
3845
|
total++;
|
|
3579
|
-
invalid.push({ ref: ref.text, reason:
|
|
3846
|
+
invalid.push({ ref: ref.text, reason: path19.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
|
|
3580
3847
|
continue;
|
|
3581
3848
|
}
|
|
3582
3849
|
total++;
|
|
@@ -3597,7 +3864,7 @@ function enclosedBy(lines, ref) {
|
|
|
3597
3864
|
return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
|
|
3598
3865
|
}
|
|
3599
3866
|
var isFileName = (name) => {
|
|
3600
|
-
const base2 =
|
|
3867
|
+
const base2 = path19.basename(name);
|
|
3601
3868
|
return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
|
|
3602
3869
|
};
|
|
3603
3870
|
function indexFiles(roots) {
|
|
@@ -3606,19 +3873,19 @@ function indexFiles(roots) {
|
|
|
3606
3873
|
const walk = (dir) => {
|
|
3607
3874
|
let entries;
|
|
3608
3875
|
try {
|
|
3609
|
-
entries =
|
|
3876
|
+
entries = fs23.readdirSync(dir, { withFileTypes: true });
|
|
3610
3877
|
} catch {
|
|
3611
3878
|
return;
|
|
3612
3879
|
}
|
|
3613
3880
|
for (const e of entries) {
|
|
3614
3881
|
if (count >= MAX_INDEXED) return;
|
|
3615
3882
|
if (e.isDirectory()) {
|
|
3616
|
-
if (!SKIP_DIRS.has(e.name)) walk(
|
|
3883
|
+
if (!SKIP_DIRS.has(e.name)) walk(path19.join(dir, e.name));
|
|
3617
3884
|
} else if (e.isFile()) {
|
|
3618
3885
|
count++;
|
|
3619
3886
|
const list2 = index.get(e.name);
|
|
3620
|
-
if (list2) list2.push(
|
|
3621
|
-
else index.set(e.name, [
|
|
3887
|
+
if (list2) list2.push(path19.join(dir, e.name));
|
|
3888
|
+
else index.set(e.name, [path19.join(dir, e.name)]);
|
|
3622
3889
|
}
|
|
3623
3890
|
}
|
|
3624
3891
|
};
|
|
@@ -3626,10 +3893,10 @@ function indexFiles(roots) {
|
|
|
3626
3893
|
return index;
|
|
3627
3894
|
}
|
|
3628
3895
|
function resolve(ref, roots) {
|
|
3629
|
-
const candidates =
|
|
3896
|
+
const candidates = path19.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path19.join(r, ref));
|
|
3630
3897
|
return candidates.find((c) => {
|
|
3631
3898
|
try {
|
|
3632
|
-
return
|
|
3899
|
+
return fs23.statSync(c).isFile() && inside(c, roots);
|
|
3633
3900
|
} catch {
|
|
3634
3901
|
return false;
|
|
3635
3902
|
}
|
|
@@ -3637,16 +3904,16 @@ function resolve(ref, roots) {
|
|
|
3637
3904
|
}
|
|
3638
3905
|
function inside(file, roots) {
|
|
3639
3906
|
return roots.some((r) => {
|
|
3640
|
-
const rel =
|
|
3641
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
3907
|
+
const rel = path19.relative(r, path19.resolve(file));
|
|
3908
|
+
return rel !== "" && !rel.startsWith("..") && !path19.isAbsolute(rel);
|
|
3642
3909
|
});
|
|
3643
3910
|
}
|
|
3644
3911
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3645
3912
|
|
|
3646
3913
|
// src/core/secrets.ts
|
|
3647
3914
|
import { execFileSync } from "node:child_process";
|
|
3648
|
-
import
|
|
3649
|
-
import
|
|
3915
|
+
import fs24 from "node:fs";
|
|
3916
|
+
import path20 from "node:path";
|
|
3650
3917
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
3651
3918
|
var MAX_VISITED = 2e4;
|
|
3652
3919
|
var MAX_DEPTH = 4;
|
|
@@ -3662,16 +3929,16 @@ function findSecretFiles(dir) {
|
|
|
3662
3929
|
if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
|
|
3663
3930
|
let entries;
|
|
3664
3931
|
try {
|
|
3665
|
-
entries =
|
|
3932
|
+
entries = fs24.readdirSync(d, { withFileTypes: true });
|
|
3666
3933
|
} catch {
|
|
3667
3934
|
return;
|
|
3668
3935
|
}
|
|
3669
3936
|
for (const e of entries) {
|
|
3670
3937
|
visited++;
|
|
3671
3938
|
if (e.isDirectory()) {
|
|
3672
|
-
if (!SKIP_DIRS2.has(e.name)) walk(
|
|
3939
|
+
if (!SKIP_DIRS2.has(e.name)) walk(path20.join(d, e.name), depth + 1);
|
|
3673
3940
|
} else if (e.isFile() && looksSecret(e.name)) {
|
|
3674
|
-
found.push(
|
|
3941
|
+
found.push(path20.relative(dir, path20.join(d, e.name)));
|
|
3675
3942
|
}
|
|
3676
3943
|
}
|
|
3677
3944
|
};
|
|
@@ -3681,7 +3948,7 @@ function findSecretFiles(dir) {
|
|
|
3681
3948
|
function findSecretFilesInTree(root, dir) {
|
|
3682
3949
|
try {
|
|
3683
3950
|
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(
|
|
3951
|
+
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
3952
|
} catch {
|
|
3686
3953
|
return [];
|
|
3687
3954
|
}
|
|
@@ -3695,7 +3962,7 @@ function secretWarning(files, mode) {
|
|
|
3695
3962
|
}
|
|
3696
3963
|
|
|
3697
3964
|
// src/core/run.ts
|
|
3698
|
-
var VERSION2 = true ? "0.
|
|
3965
|
+
var VERSION2 = true ? "0.8.0" : "0.0.0-dev";
|
|
3699
3966
|
function planWork(o) {
|
|
3700
3967
|
if (!o.plan) return { task: o.task.trim(), tier: o.tier };
|
|
3701
3968
|
if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
|
|
@@ -3727,7 +3994,7 @@ function prepareRun(o) {
|
|
|
3727
3994
|
parent = readMeta(o.continueFrom);
|
|
3728
3995
|
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
3996
|
if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
|
|
3730
|
-
if (parent.mode === "isolate" && (!parent.worktree || !
|
|
3997
|
+
if (parent.mode === "isolate" && (!parent.worktree || !fs25.existsSync(parent.worktree))) {
|
|
3731
3998
|
throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
|
|
3732
3999
|
}
|
|
3733
4000
|
const ran = parent.ran ?? parent.worker;
|
|
@@ -3746,15 +4013,15 @@ function prepareRun(o) {
|
|
|
3746
4013
|
throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
|
|
3747
4014
|
}
|
|
3748
4015
|
const mode = parent?.mode ?? o.mode;
|
|
3749
|
-
const dir =
|
|
3750
|
-
if (!
|
|
4016
|
+
const dir = path21.resolve(parent?.dir ?? o.dir);
|
|
4017
|
+
if (!fs25.existsSync(dir) || !fs25.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
|
|
3751
4018
|
const root = repoRoot(dir);
|
|
3752
4019
|
if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
|
|
3753
4020
|
if (mode === "write" && !root && !o.allowNonGit) {
|
|
3754
4021
|
throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
|
|
3755
4022
|
}
|
|
3756
|
-
const files = o.files.map((f) =>
|
|
3757
|
-
for (const f of files) if (!
|
|
4023
|
+
const files = o.files.map((f) => path21.resolve(f));
|
|
4024
|
+
for (const f of files) if (!fs25.existsSync(f)) throw new UserError(`file not found: ${f}`);
|
|
3758
4025
|
if (!parent && !process.env.PITROOM_NO_SECRET_WARNING) {
|
|
3759
4026
|
const secrets = mode === "isolate" && root ? findSecretFilesInTree(root, dir) : findSecretFiles(dir);
|
|
3760
4027
|
const note = secretWarning(secrets, mode);
|
|
@@ -3794,13 +4061,13 @@ function prepareRun(o) {
|
|
|
3794
4061
|
try {
|
|
3795
4062
|
acquireWriteLock(root, meta.id);
|
|
3796
4063
|
} catch (e) {
|
|
3797
|
-
|
|
4064
|
+
fs25.rmSync(runDir(meta.id), { recursive: true, force: true });
|
|
3798
4065
|
throw e;
|
|
3799
4066
|
}
|
|
3800
4067
|
}
|
|
3801
|
-
|
|
4068
|
+
fs25.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
|
|
3802
4069
|
`);
|
|
3803
|
-
if (work.brief)
|
|
4070
|
+
if (work.brief) fs25.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
|
|
3804
4071
|
return meta;
|
|
3805
4072
|
}
|
|
3806
4073
|
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -3873,7 +4140,7 @@ async function execute(meta) {
|
|
|
3873
4140
|
(meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
|
|
3874
4141
|
for (const f of ["events.jsonl", "stderr.log"]) {
|
|
3875
4142
|
const from = runFile(meta.id, f);
|
|
3876
|
-
if (
|
|
4143
|
+
if (fs25.existsSync(from)) fs25.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
|
|
3877
4144
|
}
|
|
3878
4145
|
writeMeta(meta);
|
|
3879
4146
|
}
|
|
@@ -3893,9 +4160,9 @@ function prepareTree(meta) {
|
|
|
3893
4160
|
if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
|
|
3894
4161
|
if (root && meta.mode === "isolate" && !meta.worktree) {
|
|
3895
4162
|
meta.baseTree = snapshotTree(root);
|
|
3896
|
-
meta.worktree =
|
|
4163
|
+
meta.worktree = path21.join(worktreesDir(), meta.id);
|
|
3897
4164
|
createIsolatedCopy(root, meta.baseTree, meta.worktree);
|
|
3898
|
-
meta.cwd =
|
|
4165
|
+
meta.cwd = path21.join(meta.worktree, path21.relative(root, meta.dir));
|
|
3899
4166
|
const linked = linkIntoWorktree(root, meta.worktree, meta.link);
|
|
3900
4167
|
if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
|
|
3901
4168
|
}
|
|
@@ -3918,7 +4185,7 @@ function attempt(meta, backend, target) {
|
|
|
3918
4185
|
timeoutSec: meta.timeoutSec
|
|
3919
4186
|
});
|
|
3920
4187
|
}
|
|
3921
|
-
var read = (f) =>
|
|
4188
|
+
var read = (f) => fs25.existsSync(f) ? fs25.readFileSync(f, "utf8") : "";
|
|
3922
4189
|
function retryableFailure(meta, backend, res) {
|
|
3923
4190
|
const run = backend.parse(read(runFile(meta.id, "events.jsonl")));
|
|
3924
4191
|
if (run.usage.steps > 0) return void 0;
|
|
@@ -3937,12 +4204,12 @@ function finalize(meta, res) {
|
|
|
3937
4204
|
const run = backend.parse(read(runFile(meta.id, "events.jsonl")));
|
|
3938
4205
|
meta.sessionId = run.sessionId ?? meta.sessionId;
|
|
3939
4206
|
meta.usage = run.usage;
|
|
3940
|
-
|
|
4207
|
+
fs25.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
|
|
3941
4208
|
`);
|
|
3942
4209
|
if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run.finalText);
|
|
3943
4210
|
if (meta.reviewOf) {
|
|
3944
4211
|
meta.verdict = parseVerdict(run.finalText);
|
|
3945
|
-
if (meta.packageFile)
|
|
4212
|
+
if (meta.packageFile) fs25.rmSync(meta.packageFile, { force: true });
|
|
3946
4213
|
}
|
|
3947
4214
|
if (!res.timedOut && !res.stopped && !meta.error) {
|
|
3948
4215
|
const f = backend.failure(run, read(runFile(meta.id, "stderr.log")), res.code);
|
|
@@ -3984,7 +4251,7 @@ function captureChanges(meta) {
|
|
|
3984
4251
|
const d = diffTrees(target, meta.baseTree, meta.afterTree);
|
|
3985
4252
|
meta.changes = d.changes;
|
|
3986
4253
|
meta.stats = d.stats;
|
|
3987
|
-
|
|
4254
|
+
fs25.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
|
|
3988
4255
|
}
|
|
3989
4256
|
function runVerify(meta) {
|
|
3990
4257
|
const r = spawnSync5(meta.verify, {
|
|
@@ -3997,7 +4264,7 @@ function runVerify(meta) {
|
|
|
3997
4264
|
env: { ...process.env, PWD: meta.cwd, PITROOM_ACTIVE: "1" }
|
|
3998
4265
|
});
|
|
3999
4266
|
const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
|
4000
|
-
|
|
4267
|
+
fs25.writeFileSync(runFile(meta.id, "verify.log"), output);
|
|
4001
4268
|
return { ok: r.status === 0, code: r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
|
|
4002
4269
|
}
|
|
4003
4270
|
function applyRun(meta, allowDelete = false) {
|
|
@@ -4042,7 +4309,7 @@ function discardRun(meta) {
|
|
|
4042
4309
|
return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
|
|
4043
4310
|
}
|
|
4044
4311
|
function cleanupWorktree(meta) {
|
|
4045
|
-
if (meta.worktree &&
|
|
4312
|
+
if (meta.worktree && fs25.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
|
|
4046
4313
|
}
|
|
4047
4314
|
|
|
4048
4315
|
// src/cli/commands.ts
|
|
@@ -4092,10 +4359,10 @@ async function cmdReview(p) {
|
|
|
4092
4359
|
writeMeta(meta);
|
|
4093
4360
|
}
|
|
4094
4361
|
} catch (e) {
|
|
4095
|
-
|
|
4362
|
+
fs26.rmSync(packageFile, { force: true });
|
|
4096
4363
|
throw e;
|
|
4097
4364
|
}
|
|
4098
|
-
|
|
4365
|
+
fs26.writeFileSync(runFile(meta.id, "package.md"), job.package);
|
|
4099
4366
|
return launch(p, meta);
|
|
4100
4367
|
}
|
|
4101
4368
|
function cmdPlan(p) {
|
|
@@ -4115,7 +4382,7 @@ function cmdPlan(p) {
|
|
|
4115
4382
|
}
|
|
4116
4383
|
function cmdCrew(p) {
|
|
4117
4384
|
const file = flag(p, "task-file");
|
|
4118
|
-
const tasks = (file ?
|
|
4385
|
+
const tasks = (file ? fs26.readFileSync(file, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
|
|
4119
4386
|
if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
|
|
4120
4387
|
if (has(p, "write") && tasks.length > 1) {
|
|
4121
4388
|
throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
|
|
@@ -4224,7 +4491,7 @@ function cmdHistory(p) {
|
|
|
4224
4491
|
if (has(p, "json")) return console.log(JSON.stringify(s, null, 2)), 0;
|
|
4225
4492
|
const t = s.totals;
|
|
4226
4493
|
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)}
|
|
4494
|
+
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
4495
|
const head2 = ["WORKER", "RUNS", "OK", "AVG TIME", "AVG TOKENS", "SAVED"];
|
|
4229
4496
|
const widths2 = head2.map((h, i) => Math.max(h.length, ...rows2.map((r) => r[i].length)));
|
|
4230
4497
|
const fmt2 = (r) => r.map((c, i) => i === 0 ? c.padEnd(widths2[i]) : c.padStart(widths2[i])).join(" ");
|
|
@@ -4239,8 +4506,11 @@ ${[fmt2(head2), ...rows2.map(fmt2)].join("\n")}`);
|
|
|
4239
4506
|
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
4507
|
const head = ["RUN", "WHEN", "STATE", "WORKER", "TIME", "TASK"];
|
|
4241
4508
|
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
|
-
|
|
4509
|
+
const fmt = (r, header = false) => r.map((c, i) => {
|
|
4510
|
+
const cell2 = i === r.length - 1 ? c : c.padEnd(widths[i]);
|
|
4511
|
+
return header ? dim(cell2) : i === 2 ? stateColour(r[2], cell2) : cell2;
|
|
4512
|
+
}).join(" ");
|
|
4513
|
+
console.log([fmt(head, true), ...body.map((r) => fmt(r))].join("\n"));
|
|
4244
4514
|
if (total > rows.length) console.log(`
|
|
4245
4515
|
${rows.length} of ${total} \xB7 --limit N for more \xB7 pitroom show <run> for one`);
|
|
4246
4516
|
return 0;
|
|
@@ -4313,7 +4583,7 @@ function sinceMs(s) {
|
|
|
4313
4583
|
if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
|
|
4314
4584
|
return Date.now() - Number(m[1]) * 864e5;
|
|
4315
4585
|
}
|
|
4316
|
-
var readStdin = () => process.stdin.isTTY ? "" :
|
|
4586
|
+
var readStdin = () => process.stdin.isTTY ? "" : fs26.readFileSync(0, "utf8");
|
|
4317
4587
|
function cmdStatusline(p) {
|
|
4318
4588
|
const input = readStdin();
|
|
4319
4589
|
const lines = [];
|
|
@@ -4352,6 +4622,18 @@ function cmdModels(p) {
|
|
|
4352
4622
|
console.log(has(p, "json") ? JSON.stringify(table2, null, 2) : formatModels(table2));
|
|
4353
4623
|
return 0;
|
|
4354
4624
|
}
|
|
4625
|
+
function cmdHookStart() {
|
|
4626
|
+
const script = path22.join(path22.dirname(path22.dirname(fileURLToPath3(import.meta.url))), "hooks", "session-start.mjs");
|
|
4627
|
+
if (fs26.existsSync(script)) spawnSync6(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
|
|
4628
|
+
return 0;
|
|
4629
|
+
}
|
|
4630
|
+
function cmdInit(p) {
|
|
4631
|
+
const fallback = flag(p, "fallback");
|
|
4632
|
+
const plan = planInit({ model: flag(p, "model"), fallback: fallback === void 0 ? void 0 : fallback.split(",").map((s) => s.trim()).filter(Boolean) });
|
|
4633
|
+
const written = has(p, "yes") ? writeInit(plan, has(p, "force")) : void 0;
|
|
4634
|
+
console.log(has(p, "json") ? JSON.stringify({ ...plan, written }, null, 2) : formatInit(plan, written));
|
|
4635
|
+
return 0;
|
|
4636
|
+
}
|
|
4355
4637
|
function cmdSavings(p) {
|
|
4356
4638
|
const since = flag(p, "since") ?? "all";
|
|
4357
4639
|
const t = totals(readLedger(sinceMs(since)));
|
|
@@ -4382,8 +4664,8 @@ function cmdSavings(p) {
|
|
|
4382
4664
|
est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
|
|
4383
4665
|
const out = flag(p, "card");
|
|
4384
4666
|
if (out) {
|
|
4385
|
-
|
|
4386
|
-
console.log(`card written to ${
|
|
4667
|
+
fs26.writeFileSync(out, card(t, period));
|
|
4668
|
+
console.log(`card written to ${path22.resolve(out)}`);
|
|
4387
4669
|
}
|
|
4388
4670
|
if (has(p, "badge")) console.log(`})`);
|
|
4389
4671
|
return 0;
|
|
@@ -4403,7 +4685,7 @@ function cmdConfig(p) {
|
|
|
4403
4685
|
console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
|
|
4404
4686
|
return 0;
|
|
4405
4687
|
}
|
|
4406
|
-
console.log(`config file: ${configPath()}${
|
|
4688
|
+
console.log(`config file: ${configPath()}${fs26.existsSync(configPath()) ? "" : " (not present)"}`);
|
|
4407
4689
|
for (const [key, s] of Object.entries(eff)) {
|
|
4408
4690
|
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
4691
|
console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
|
|
@@ -4427,7 +4709,7 @@ function cmdClean(p) {
|
|
|
4427
4709
|
const m = readMeta(id);
|
|
4428
4710
|
if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
|
|
4429
4711
|
recordRun(m);
|
|
4430
|
-
|
|
4712
|
+
fs26.rmSync(runDir(id), { recursive: true, force: true });
|
|
4431
4713
|
}
|
|
4432
4714
|
console.log(`removed ${old.length} run(s); the history and the savings ledger are kept`);
|
|
4433
4715
|
return 0;
|
|
@@ -4437,10 +4719,33 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
|
|
|
4437
4719
|
|
|
4438
4720
|
// src/core/doctor.ts
|
|
4439
4721
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
4440
|
-
import
|
|
4722
|
+
import fs27 from "node:fs";
|
|
4441
4723
|
import os9 from "node:os";
|
|
4442
|
-
import
|
|
4443
|
-
var MARK = { ok: "\u2714", warn: "!", fail: "\u2718" };
|
|
4724
|
+
import path23 from "node:path";
|
|
4725
|
+
var MARK = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
|
|
4726
|
+
var NEXT = [
|
|
4727
|
+
{ when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
|
|
4728
|
+
{ when: /pitroom install|no Pitroom skills|launcher on PATH/, command: "pitroom install", why: "link the skills and the pitroom command" },
|
|
4729
|
+
{ when: /first `node` on PATH/, command: "nvm alias default 24", why: "a current Node first in every new shell" },
|
|
4730
|
+
{ when: /not logged in/, command: "claude auth login", why: "sign in the Claude Code worker (Codex: codex login)" }
|
|
4731
|
+
];
|
|
4732
|
+
function firstNodeOnPath() {
|
|
4733
|
+
for (const dir of (process.env.PATH ?? "").split(path23.delimiter).filter(Boolean)) {
|
|
4734
|
+
const file = path23.join(dir, process.platform === "win32" ? "node.exe" : "node");
|
|
4735
|
+
try {
|
|
4736
|
+
if (!fs27.statSync(file).isFile()) continue;
|
|
4737
|
+
} catch {
|
|
4738
|
+
continue;
|
|
4739
|
+
}
|
|
4740
|
+
const r = spawnSync7(file, ["-p", "process.versions.node"], { encoding: "utf8", timeout: 5e3 });
|
|
4741
|
+
return r.status === 0 ? { path: file, version: r.stdout.trim() } : { path: file, version: "" };
|
|
4742
|
+
}
|
|
4743
|
+
return void 0;
|
|
4744
|
+
}
|
|
4745
|
+
var tooOld = (v) => {
|
|
4746
|
+
const [a = 0, b = 0] = v.split(".").map(Number);
|
|
4747
|
+
return a < 22 || a === 22 && b < 13;
|
|
4748
|
+
};
|
|
4444
4749
|
var READ_ONLY_HOW = {
|
|
4445
4750
|
"permission-rules": "per-run permission rules",
|
|
4446
4751
|
"os-sandbox": "an OS sandbox",
|
|
@@ -4449,16 +4754,24 @@ var READ_ONLY_HOW = {
|
|
|
4449
4754
|
};
|
|
4450
4755
|
function doctor4(probe) {
|
|
4451
4756
|
const checks = [];
|
|
4452
|
-
|
|
4757
|
+
let current = "Setup";
|
|
4758
|
+
const section2 = (name) => void (current = name);
|
|
4759
|
+
const add = (level, message) => checks.push({ level, message, section: current });
|
|
4760
|
+
const addAll = (list2) => list2.forEach((c) => add(c.level, c.message));
|
|
4453
4761
|
add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
|
|
4762
|
+
const onPath = firstNodeOnPath();
|
|
4763
|
+
if (onPath && onPath.version && tooOld(onPath.version) && path23.resolve(onPath.path) !== path23.resolve(process.execPath)) {
|
|
4764
|
+
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)`);
|
|
4765
|
+
}
|
|
4454
4766
|
add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
|
|
4455
4767
|
const cfg = loadConfig();
|
|
4456
|
-
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${
|
|
4768
|
+
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs27.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
|
|
4457
4769
|
for (const w of cfg.warnings) add("warn", w);
|
|
4458
4770
|
if (process.platform !== "win32") {
|
|
4459
4771
|
const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
|
|
4460
4772
|
add(guarded ? "ok" : "warn", guarded ? "git guard shim ready" : "git guard unavailable (git not on PATH)");
|
|
4461
4773
|
}
|
|
4774
|
+
section2("Worker chain");
|
|
4462
4775
|
const chain = [];
|
|
4463
4776
|
try {
|
|
4464
4777
|
const resolved = resolveChain();
|
|
@@ -4499,14 +4812,39 @@ function doctor4(probe) {
|
|
|
4499
4812
|
add("fail", e.message);
|
|
4500
4813
|
continue;
|
|
4501
4814
|
}
|
|
4502
|
-
|
|
4815
|
+
section2(backend.name);
|
|
4816
|
+
addAll(backend.doctor({ models: [...new Set(models)], hasFallback: chain.length > 1 }));
|
|
4503
4817
|
add("ok", `${backend.name}: read-only runs enforced by ${READ_ONLY_HOW[backend.capabilities.readOnly]}`);
|
|
4504
4818
|
}
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4819
|
+
section2("Skills and agents");
|
|
4820
|
+
addAll(skillChecks());
|
|
4821
|
+
if (probe && chain[0]) {
|
|
4822
|
+
section2("Live probe");
|
|
4823
|
+
const p = liveProbe(chain[0]);
|
|
4824
|
+
add(p.level, p.message);
|
|
4825
|
+
}
|
|
4826
|
+
print(checks);
|
|
4508
4827
|
return checks.some((c) => c.level === "fail") ? 1 : 0;
|
|
4509
4828
|
}
|
|
4829
|
+
function print(checks) {
|
|
4830
|
+
console.log(`${bold("Pitroom doctor")} ${dim(`v${VERSION2}`)}`);
|
|
4831
|
+
for (const name of [...new Set(checks.map((c) => c.section))]) {
|
|
4832
|
+
console.log(`
|
|
4833
|
+
${bold(name)}`);
|
|
4834
|
+
for (const c of checks.filter((x) => x.section === name)) console.log(` ${MARK[c.level]()} ${wrapText(c.message, 4)}`);
|
|
4835
|
+
}
|
|
4836
|
+
const count = (level) => checks.filter((c) => c.level === level).length;
|
|
4837
|
+
const [ok, warn, fail] = [count("ok"), count("warn"), count("fail")];
|
|
4838
|
+
console.log(`
|
|
4839
|
+
${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")}`);
|
|
4840
|
+
const next = NEXT.filter((n) => checks.some((c) => c.level !== "ok" && n.when.test(c.message)));
|
|
4841
|
+
if (next.length) {
|
|
4842
|
+
console.log(`
|
|
4843
|
+
${bold("Next")}`);
|
|
4844
|
+
const width = Math.max(...next.map((n) => n.command.length));
|
|
4845
|
+
for (const n of next) console.log(` ${cyan(n.command.padEnd(width))} ${dim(n.why)}`);
|
|
4846
|
+
}
|
|
4847
|
+
}
|
|
4510
4848
|
function liveProbe(target) {
|
|
4511
4849
|
let backend;
|
|
4512
4850
|
try {
|
|
@@ -4554,13 +4892,13 @@ function skillChecks() {
|
|
|
4554
4892
|
else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
|
|
4555
4893
|
}
|
|
4556
4894
|
if (viaPlugin) {
|
|
4557
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
4895
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path23.sep}.claude${path23.sep}`) && i.names.length);
|
|
4558
4896
|
checks.push(
|
|
4559
4897
|
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
4898
|
);
|
|
4561
4899
|
}
|
|
4562
4900
|
const launcher = launcherPath();
|
|
4563
|
-
if (
|
|
4901
|
+
if (fs27.existsSync(launcher)) {
|
|
4564
4902
|
const r = spawnSync7(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
4565
4903
|
checks.push(
|
|
4566
4904
|
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 +4910,8 @@ function skillChecks() {
|
|
|
4572
4910
|
}
|
|
4573
4911
|
function pluginInstalled() {
|
|
4574
4912
|
try {
|
|
4575
|
-
const f =
|
|
4576
|
-
const plugins = JSON.parse(
|
|
4913
|
+
const f = path23.join(os9.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
4914
|
+
const plugins = JSON.parse(fs27.readFileSync(f, "utf8")).plugins ?? {};
|
|
4577
4915
|
return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
|
|
4578
4916
|
} catch {
|
|
4579
4917
|
return false;
|
|
@@ -4584,30 +4922,30 @@ function superpowersActive() {
|
|
|
4584
4922
|
for (const key of claudePlugins()) {
|
|
4585
4923
|
if (key.startsWith("superpowers@") && claudePluginEnabled(key)) found.push(`Claude Code plugin ${key}`);
|
|
4586
4924
|
}
|
|
4587
|
-
for (const [key,
|
|
4588
|
-
if (key.startsWith("superpowers@") &&
|
|
4925
|
+
for (const [key, enabled2] of codexPlugins()) {
|
|
4926
|
+
if (key.startsWith("superpowers@") && enabled2) found.push(`Codex plugin ${key}`);
|
|
4589
4927
|
}
|
|
4590
4928
|
for (const p of openCodePlugins()) {
|
|
4591
4929
|
if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
|
|
4592
4930
|
}
|
|
4593
|
-
for (const base2 of [
|
|
4594
|
-
const dir =
|
|
4595
|
-
if (
|
|
4931
|
+
for (const base2 of [path23.join(os9.homedir(), ".agents", "skills"), path23.join(os9.homedir(), ".claude", "skills")]) {
|
|
4932
|
+
const dir = path23.join(base2, "using-superpowers");
|
|
4933
|
+
if (fs27.existsSync(path23.join(dir, "SKILL.md"))) found.push(dir);
|
|
4596
4934
|
}
|
|
4597
4935
|
return found;
|
|
4598
4936
|
}
|
|
4599
4937
|
function claudePlugins() {
|
|
4600
4938
|
try {
|
|
4601
|
-
const f =
|
|
4602
|
-
return Object.keys(JSON.parse(
|
|
4939
|
+
const f = path23.join(os9.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
4940
|
+
return Object.keys(JSON.parse(fs27.readFileSync(f, "utf8")).plugins ?? {});
|
|
4603
4941
|
} catch {
|
|
4604
4942
|
return [];
|
|
4605
4943
|
}
|
|
4606
4944
|
}
|
|
4607
4945
|
function claudePluginEnabled(key) {
|
|
4608
4946
|
try {
|
|
4609
|
-
const f =
|
|
4610
|
-
return JSON.parse(
|
|
4947
|
+
const f = path23.join(os9.homedir(), ".claude", "settings.json");
|
|
4948
|
+
return JSON.parse(fs27.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
|
|
4611
4949
|
} catch {
|
|
4612
4950
|
return true;
|
|
4613
4951
|
}
|
|
@@ -4616,7 +4954,7 @@ function codexPlugins() {
|
|
|
4616
4954
|
const plugins = /* @__PURE__ */ new Map();
|
|
4617
4955
|
let text;
|
|
4618
4956
|
try {
|
|
4619
|
-
text =
|
|
4957
|
+
text = fs27.readFileSync(path23.join(process.env.CODEX_HOME ?? path23.join(os9.homedir(), ".codex"), "config.toml"), "utf8");
|
|
4620
4958
|
} catch {
|
|
4621
4959
|
return plugins;
|
|
4622
4960
|
}
|
|
@@ -4634,11 +4972,11 @@ function codexPlugins() {
|
|
|
4634
4972
|
return plugins;
|
|
4635
4973
|
}
|
|
4636
4974
|
function openCodePlugins() {
|
|
4637
|
-
const dir =
|
|
4975
|
+
const dir = path23.join(process.env.XDG_CONFIG_HOME ?? path23.join(os9.homedir(), ".config"), "opencode");
|
|
4638
4976
|
const plugins = [];
|
|
4639
4977
|
for (const name of ["opencode.json", "opencode.jsonc"]) {
|
|
4640
4978
|
try {
|
|
4641
|
-
const text =
|
|
4979
|
+
const text = fs27.readFileSync(path23.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
|
|
4642
4980
|
const list2 = JSON.parse(text).plugin;
|
|
4643
4981
|
if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
|
|
4644
4982
|
} catch {
|
|
@@ -4646,7 +4984,7 @@ function openCodePlugins() {
|
|
|
4646
4984
|
}
|
|
4647
4985
|
for (const folder of ["plugin", "plugins"]) {
|
|
4648
4986
|
try {
|
|
4649
|
-
for (const f of
|
|
4987
|
+
for (const f of fs27.readdirSync(path23.join(dir, folder))) plugins.push(path23.join(dir, folder, f));
|
|
4650
4988
|
} catch {
|
|
4651
4989
|
}
|
|
4652
4990
|
}
|
|
@@ -4693,8 +5031,12 @@ Usage
|
|
|
4693
5031
|
models each worker offers, with effort levels, your costs and usage
|
|
4694
5032
|
pitroom statusline [--then CMD] status-bar line: running workers, savings this week (after CMD's)
|
|
4695
5033
|
pitroom hook-card PostToolUse hook: a card after each Bash \`pitroom\` command
|
|
5034
|
+
pitroom hook-start SessionStart hook: introduces Pitroom to the agent (Codex; Claude Code uses its plugin)
|
|
4696
5035
|
pitroom doctor [--probe] check workers, models, permissions, skills
|
|
4697
5036
|
pitroom config effective settings, where each comes from, config file path
|
|
5037
|
+
pitroom init [--model ID] [--fallback A,B] [--yes] [--force]
|
|
5038
|
+
propose a starter config from the worker CLIs and models you have;
|
|
5039
|
+
writes it only with --yes (models are suggested, never chosen for you)
|
|
4698
5040
|
pitroom install [--copy] [--force] link the skills into ~/.agents/skills + ~/.claude/skills,
|
|
4699
5041
|
and the CLI into ~/.local/bin
|
|
4700
5042
|
pitroom uninstall remove what install linked
|
|
@@ -4754,8 +5096,10 @@ var COMMANDS = {
|
|
|
4754
5096
|
models: cmdModels,
|
|
4755
5097
|
statusline: cmdStatusline,
|
|
4756
5098
|
"hook-card": cmdHookCard,
|
|
5099
|
+
"hook-start": cmdHookStart,
|
|
4757
5100
|
doctor: (p) => doctor4(has(p, "probe")),
|
|
4758
5101
|
config: cmdConfig,
|
|
5102
|
+
init: cmdInit,
|
|
4759
5103
|
install: cmdInstall,
|
|
4760
5104
|
uninstall: cmdUninstall,
|
|
4761
5105
|
clean: cmdClean,
|