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