pitroom 0.19.0 → 0.20.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 +1 -1
- package/CHANGELOG.md +22 -0
- package/README.md +73 -0
- package/dist/lib.mjs +721 -40
- package/dist/pitroom.mjs +1114 -955
- package/dist/ui/app.js +10 -10
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
package/dist/pitroom.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
4
|
+
import fs37 from "node:fs";
|
|
5
5
|
|
|
6
6
|
// src/core/errors.ts
|
|
7
7
|
var UserError = class extends Error {
|
|
@@ -1441,7 +1441,12 @@ var SCHEMA = {
|
|
|
1441
1441
|
costs: "numbers",
|
|
1442
1442
|
audit: "number",
|
|
1443
1443
|
readIn: "string",
|
|
1444
|
-
cacheDays: "number"
|
|
1444
|
+
cacheDays: "number",
|
|
1445
|
+
countRateLimits: "boolean",
|
|
1446
|
+
mcpDash: "boolean",
|
|
1447
|
+
notify: "boolean",
|
|
1448
|
+
notifyCommand: "string",
|
|
1449
|
+
notifyAfter: "number"
|
|
1445
1450
|
};
|
|
1446
1451
|
function configPath() {
|
|
1447
1452
|
if (process.env.PITROOM_CONFIG) return path7.resolve(process.env.PITROOM_CONFIG);
|
|
@@ -1503,6 +1508,10 @@ var days = (v) => {
|
|
|
1503
1508
|
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
1504
1509
|
return typeof n === "number" && Number.isInteger(n) && n >= 0 ? n : void 0;
|
|
1505
1510
|
};
|
|
1511
|
+
var flag01 = (v) => {
|
|
1512
|
+
const t = v?.trim().toLowerCase();
|
|
1513
|
+
return t === void 0 || t === "" ? void 0 : ["1", "true", "on", "yes"].includes(t) ? true : ["0", "false", "off", "no"].includes(t) ? false : void 0;
|
|
1514
|
+
};
|
|
1506
1515
|
var readIn = (v) => v === "auto" || v === "snapshot" || v === "project" ? v : void 0;
|
|
1507
1516
|
var list = (s) => s?.split(",").map((x) => x.trim()).filter(Boolean);
|
|
1508
1517
|
function setting(flag2, env, conf, fallback) {
|
|
@@ -1530,7 +1539,12 @@ function effective(flags = {}) {
|
|
|
1530
1539
|
costs: setting(void 0, void 0, c.costs, {}),
|
|
1531
1540
|
audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0),
|
|
1532
1541
|
readIn: setting(void 0, readIn(e.PITROOM_READ_IN), readIn(c.readIn), "auto"),
|
|
1533
|
-
cacheDays: setting(void 0, days(e.PITROOM_CACHE_DAYS), days(c.cacheDays), 7)
|
|
1542
|
+
cacheDays: setting(void 0, days(e.PITROOM_CACHE_DAYS), days(c.cacheDays), 7),
|
|
1543
|
+
countRateLimits: setting(void 0, void 0, c.countRateLimits, false),
|
|
1544
|
+
mcpDash: setting(void 0, flag01(e.PITROOM_MCP_DASH), c.mcpDash, true),
|
|
1545
|
+
notify: setting(void 0, flag01(e.PITROOM_NOTIFY), c.notify, false),
|
|
1546
|
+
notifyCommand: setting(void 0, e.PITROOM_NOTIFY_COMMAND?.trim() || void 0, c.notifyCommand?.trim() || void 0, void 0),
|
|
1547
|
+
notifyAfter: setting(void 0, days(e.PITROOM_NOTIFY_AFTER), days(c.notifyAfter), 15)
|
|
1534
1548
|
};
|
|
1535
1549
|
}
|
|
1536
1550
|
|
|
@@ -1714,9 +1728,9 @@ function effortFlag(p) {
|
|
|
1714
1728
|
import readline from "node:readline";
|
|
1715
1729
|
|
|
1716
1730
|
// src/core/run.ts
|
|
1717
|
-
import { spawn as
|
|
1718
|
-
import
|
|
1719
|
-
import
|
|
1731
|
+
import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
|
|
1732
|
+
import fs26 from "node:fs";
|
|
1733
|
+
import path22 from "node:path";
|
|
1720
1734
|
|
|
1721
1735
|
// src/vcs/git.ts
|
|
1722
1736
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
@@ -2212,10 +2226,11 @@ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build"
|
|
|
2212
2226
|
var MAX_VISITED = 2e4;
|
|
2213
2227
|
var MAX_DEPTH = 4;
|
|
2214
2228
|
var TEMPLATE = /\.(example|sample|template|dist|defaults?|tpl)$/i;
|
|
2215
|
-
function looksSecret(
|
|
2229
|
+
function looksSecret(file2) {
|
|
2230
|
+
const name = file2.toLowerCase();
|
|
2216
2231
|
if (TEMPLATE.test(name)) return false;
|
|
2217
|
-
if (name === ".env" || name.startsWith(".env.") ||
|
|
2218
|
-
return /\.(pem|p12|pfx|key|jks|keystore)
|
|
2232
|
+
if (name === ".env" || name.startsWith(".env.") || name.endsWith(".env")) return true;
|
|
2233
|
+
return /\.(pem|p12|pfx|key|jks|keystore)$/.test(name) || /^id_(rsa|dsa|ecdsa|ed25519)$/.test(name) || name === ".netrc" || name === "credentials.json" || /^secrets?\.(json|ya?ml|toml)$/.test(name);
|
|
2219
2234
|
}
|
|
2220
2235
|
function findSecretFiles(dir) {
|
|
2221
2236
|
const found = [];
|
|
@@ -2384,6 +2399,7 @@ function backToProject(text, snapshotDir, root) {
|
|
|
2384
2399
|
|
|
2385
2400
|
// src/core/audit.ts
|
|
2386
2401
|
import crypto4 from "node:crypto";
|
|
2402
|
+
import path13 from "node:path";
|
|
2387
2403
|
var ANSWER_MAX = 8e3;
|
|
2388
2404
|
function auditRate(meta) {
|
|
2389
2405
|
return meta.auditRate ?? effective().audit.value;
|
|
@@ -2406,13 +2422,24 @@ function pickAuditor(meta) {
|
|
|
2406
2422
|
const candidates = [tiers.audit, tiers.cheap, ...eff.fallback.value, eff.worker.value].filter((s) => !!s);
|
|
2407
2423
|
return candidates.find((c) => !sameTarget(parseTarget(c, def), ran));
|
|
2408
2424
|
}
|
|
2425
|
+
function relativeToAuditor(answer, meta) {
|
|
2426
|
+
let out = answer;
|
|
2427
|
+
const roots = [...new Set([meta.dir, meta.repoRoot].filter((r) => !!r))].sort((a, b) => b.length - a.length);
|
|
2428
|
+
for (const root of roots) {
|
|
2429
|
+
const rel = path13.relative(meta.dir, root);
|
|
2430
|
+
const prefix = rel ? `${rel.split(path13.sep).join("/")}/` : "";
|
|
2431
|
+
out = out.split(`${root}${path13.sep}`).join(prefix).replace(new RegExp(`${root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w.-])`, "g"), rel || ".");
|
|
2432
|
+
}
|
|
2433
|
+
return out;
|
|
2434
|
+
}
|
|
2409
2435
|
function auditTask(meta, answer) {
|
|
2410
|
-
const text = answer.trim();
|
|
2436
|
+
const text = relativeToAuditor(answer, meta).trim();
|
|
2411
2437
|
const shown = text.length > ANSWER_MAX ? `${text.slice(0, ANSWER_MAX)}
|
|
2412
2438
|
\u2026 (cut: ${text.length - ANSWER_MAX} more characters)` : text;
|
|
2413
2439
|
return [
|
|
2414
2440
|
"You are auditing another worker's answer to a read-only question about this project. Do not take the answer on trust:",
|
|
2415
2441
|
"check its key claims yourself against the files here (open the cited files and lines, search for what it says exists or is missing).",
|
|
2442
|
+
"Paths in the answer are relative to your working directory.",
|
|
2416
2443
|
"Check the answer against the QUESTION, not only its own claims: every condition the question sets (a directory or scope,",
|
|
2417
2444
|
'what to leave out, "all", "only", "exactly", a count) must hold. Valid references do not make an answer right:',
|
|
2418
2445
|
"for a list, look for items that are missing and items that do not belong; for a count, count again yourself.",
|
|
@@ -2454,426 +2481,93 @@ function auditBadge(meta) {
|
|
|
2454
2481
|
return a.state === "running" || a.state === "queued" ? "PENDING" : "FAILED";
|
|
2455
2482
|
}
|
|
2456
2483
|
|
|
2457
|
-
// src/core/
|
|
2458
|
-
import
|
|
2459
|
-
import
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
function structure(lines) {
|
|
2463
|
-
const headings = [];
|
|
2464
|
-
const rules = [];
|
|
2465
|
-
const fenced = [];
|
|
2466
|
-
let open;
|
|
2467
|
-
lines.forEach((l, i) => {
|
|
2468
|
-
const f = FENCE.exec(l);
|
|
2469
|
-
if (f) {
|
|
2470
|
-
fenced[i] = true;
|
|
2471
|
-
const mark = f[1];
|
|
2472
|
-
if (!open) open = mark;
|
|
2473
|
-
else if (mark[0] === open[0] && mark.length >= open.length && !f[2].trim()) open = void 0;
|
|
2474
|
-
return;
|
|
2475
|
-
}
|
|
2476
|
-
fenced[i] = !!open;
|
|
2477
|
-
if (open) return;
|
|
2478
|
-
const h = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/.exec(l);
|
|
2479
|
-
if (h) headings.push({ line: i, level: h[1].length, text: h[2] });
|
|
2480
|
-
else if (RULE.test(l)) rules.push(i);
|
|
2481
|
-
});
|
|
2482
|
-
return { headings, rules, fenced };
|
|
2483
|
-
}
|
|
2484
|
-
function parsePlan(text, file2 = "") {
|
|
2485
|
-
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
2486
|
-
const { headings, rules, fenced } = structure(lines);
|
|
2487
|
-
const after = (line) => Math.min(headings.find((h) => h.line > line)?.line ?? lines.length, rules.find((r) => r > line) ?? lines.length);
|
|
2488
|
-
const titleHeading = headings.find((h) => h.level === 1);
|
|
2489
|
-
const start = titleHeading ? titleHeading.line + 1 : 0;
|
|
2490
|
-
const header = lines.slice(start, after(start - 1)).filter((l) => !/^\s*>/.test(l)).join("\n").trim();
|
|
2491
|
-
const gc = headings.find((h) => /^global constraints\b/i.test(h.text));
|
|
2492
|
-
const constraints = gc ? lines.slice(gc.line + 1, after(gc.line)).join("\n").trim() : "";
|
|
2493
|
-
const tasks = [];
|
|
2494
|
-
for (const h of headings) {
|
|
2495
|
-
const m = /^Task\s+(\d+)\b\s*[:.)\-–—]?\s*(.*)$/i.exec(h.text);
|
|
2496
|
-
if (!m) continue;
|
|
2497
|
-
const end = headings.find((o) => o.line > h.line && o.level <= h.level)?.line ?? lines.length;
|
|
2498
|
-
const body = lines.slice(h.line, end).join("\n").replace(/(\n\s*(?:---|\*\*\*|___)\s*)+$/, "").trimEnd();
|
|
2499
|
-
const unfenced = lines.slice(h.line, end).filter((_, j) => !fenced[h.line + j]).join("\n");
|
|
2500
|
-
const tier = /^\s*[-*]?\s*\*\*Worker:\*\*\s*`?([\w-]+)`?/m.exec(unfenced)?.[1]?.toLowerCase();
|
|
2501
|
-
tasks.push({ step: Number(m[1]), title: m[2].trim(), text: body, ...tier ? { tier } : {} });
|
|
2502
|
-
}
|
|
2503
|
-
return { file: file2, title: titleHeading?.text ?? "", header, constraints, tasks };
|
|
2504
|
-
}
|
|
2505
|
-
function loadPlan(file2) {
|
|
2506
|
-
const abs = path13.resolve(file2);
|
|
2507
|
-
if (!fs16.existsSync(abs) || !fs16.statSync(abs).isFile()) throw new UserError(`plan not found: ${file2}`);
|
|
2508
|
-
const plan = parsePlan(fs16.readFileSync(abs, "utf8"), fs16.realpathSync(abs));
|
|
2509
|
-
if (!plan.tasks.length) throw new UserError(`${file2} has no "Task N" headings (see pitroom-writing-plans)`);
|
|
2510
|
-
return plan;
|
|
2511
|
-
}
|
|
2512
|
-
function planTask(plan, step) {
|
|
2513
|
-
const t = plan.tasks.find((x) => x.step === step);
|
|
2514
|
-
if (!t) throw new UserError(`no Task ${step} in ${plan.file}; it has ${plan.tasks.map((x) => `Task ${x.step}`).join(", ")}`);
|
|
2515
|
-
return t;
|
|
2516
|
-
}
|
|
2517
|
-
function brief(plan, task) {
|
|
2518
|
-
return `${[
|
|
2519
|
-
`# ${plan.title || planName(plan.file)}`,
|
|
2520
|
-
plan.header,
|
|
2521
|
-
"## Global Constraints",
|
|
2522
|
-
plan.constraints || "(none stated in the plan)",
|
|
2523
|
-
task.text
|
|
2524
|
-
].filter(Boolean).join("\n\n")}
|
|
2525
|
-
`;
|
|
2526
|
-
}
|
|
2527
|
-
var planName = (file2) => path13.basename(file2).replace(/\.md$/i, "");
|
|
2484
|
+
// src/core/notify.ts
|
|
2485
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
2486
|
+
import crypto5 from "node:crypto";
|
|
2487
|
+
import fs21 from "node:fs";
|
|
2488
|
+
import path17 from "node:path";
|
|
2528
2489
|
|
|
2529
|
-
// src/core/
|
|
2490
|
+
// src/core/history.ts
|
|
2530
2491
|
import fs18 from "node:fs";
|
|
2492
|
+
import { createRequire } from "node:module";
|
|
2531
2493
|
import path15 from "node:path";
|
|
2494
|
+
import zlib from "node:zlib";
|
|
2532
2495
|
|
|
2533
|
-
// src/core/
|
|
2534
|
-
import
|
|
2535
|
-
import os9 from "node:os";
|
|
2496
|
+
// src/core/receipt.ts
|
|
2497
|
+
import fs16 from "node:fs";
|
|
2536
2498
|
import path14 from "node:path";
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
}
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
const home3 = os9.homedir();
|
|
2549
|
-
const targets = [path14.join(home3, ".agents", "skills")];
|
|
2550
|
-
if (fs17.existsSync(path14.join(home3, ".claude"))) targets.push(path14.join(home3, ".claude", "skills"));
|
|
2551
|
-
return targets;
|
|
2552
|
-
}
|
|
2553
|
-
var launcherPath = () => path14.join(os9.homedir(), ".local", "bin", process.platform === "win32" ? "pitroom.cmd" : "pitroom");
|
|
2554
|
-
var LAUNCHER_MARK = "# pitroom launcher";
|
|
2555
|
-
function launcherScript(bundle) {
|
|
2556
|
-
if (process.platform === "win32") {
|
|
2557
|
-
const q2 = (v) => v.replace(/%/g, "%%");
|
|
2558
|
-
return `@echo off\r
|
|
2559
|
-
rem ${LAUNCHER_MARK} (created by \`pitroom install\`; \`pitroom uninstall\` removes it)\r
|
|
2560
|
-
if defined PITROOM_NODE (\r
|
|
2561
|
-
"%PITROOM_NODE%" "${q2(bundle)}" %*\r
|
|
2562
|
-
) else (\r
|
|
2563
|
-
"${q2(process.execPath)}" "${q2(bundle)}" %*\r
|
|
2564
|
-
)\r
|
|
2565
|
-
exit /b %ERRORLEVEL%\r
|
|
2566
|
-
`;
|
|
2567
|
-
}
|
|
2568
|
-
const q = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
2569
|
-
return `#!/bin/sh
|
|
2570
|
-
${LAUNCHER_MARK} (created by \`pitroom install\`; \`pitroom uninstall\` removes it)
|
|
2571
|
-
cli=${q(bundle)}
|
|
2572
|
-
ok() { [ -n "$1" ] && [ -x "$1" ] && "$1" -e 'const [a,b]=process.versions.node.split(".").map(Number);process.exit(a>22||(a===22&&b>=13)?0:1)' 2>/dev/null; }
|
|
2573
|
-
for n in "\${PITROOM_NODE:-}" ${q(process.execPath)} "$(command -v node 2>/dev/null)" "$HOME"/.nvm/versions/node/*/bin/node /opt/homebrew/bin/node /usr/local/bin/node; do
|
|
2574
|
-
if ok "$n"; then exec "$n" "$cli" "$@"; fi
|
|
2575
|
-
done
|
|
2576
|
-
echo "pitroom: needs Node.js 22.13 or newer (set PITROOM_NODE to its path)" >&2
|
|
2577
|
-
exit 127
|
|
2578
|
-
`;
|
|
2579
|
-
}
|
|
2580
|
-
function isOurLauncher(file2, root) {
|
|
2581
|
-
if (linksInto(file2, root)) return true;
|
|
2582
|
-
try {
|
|
2583
|
-
const s = fs17.readFileSync(file2, "utf8");
|
|
2584
|
-
return s.includes(LAUNCHER_MARK) && s.includes(root);
|
|
2585
|
-
} catch {
|
|
2586
|
-
return false;
|
|
2587
|
-
}
|
|
2588
|
-
}
|
|
2589
|
-
function isPitroomLauncher(file2) {
|
|
2590
|
-
try {
|
|
2591
|
-
return fs17.readFileSync(file2, "utf8").includes(LAUNCHER_MARK);
|
|
2592
|
-
} catch {
|
|
2593
|
-
return false;
|
|
2594
|
-
}
|
|
2595
|
-
}
|
|
2596
|
-
function placeLauncher(bundle, root, force) {
|
|
2597
|
-
const dest = launcherPath();
|
|
2598
|
-
fs17.mkdirSync(path14.dirname(dest), { recursive: true });
|
|
2599
|
-
const exists = fs17.lstatSync(dest, { throwIfNoEntry: false });
|
|
2600
|
-
if (exists && !isOurLauncher(dest, root)) {
|
|
2601
|
-
if (!force) return `! ${dest} exists and is not Pitroom's launcher; kept (use --force to back it up and replace)`;
|
|
2602
|
-
fs17.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
2603
|
-
} else if (exists) {
|
|
2604
|
-
fs17.rmSync(dest, { force: true });
|
|
2499
|
+
var PRESETS = {
|
|
2500
|
+
sonnet: { name: "Claude Sonnet", input: 3, output: 15, cachedInput: 0.3 },
|
|
2501
|
+
opus: { name: "Claude Opus", input: 5, output: 25, cachedInput: 0.5 },
|
|
2502
|
+
haiku: { name: "Claude Haiku", input: 1, output: 5, cachedInput: 0.1 },
|
|
2503
|
+
"gpt-5": { name: "GPT-5", input: 1.25, output: 10, cachedInput: 0.125 }
|
|
2504
|
+
};
|
|
2505
|
+
function primaryPrice() {
|
|
2506
|
+
const eff = effective();
|
|
2507
|
+
const custom = eff.price.value?.split(",").map(Number);
|
|
2508
|
+
if (custom && custom.length >= 2 && custom.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
2509
|
+
return { name: "custom", input: custom[0], output: custom[1], cachedInput: custom[2] ?? custom[0] / 10 };
|
|
2605
2510
|
}
|
|
2606
|
-
|
|
2607
|
-
return `\u2714 ${dest} \u2192 launcher for ${bundle} (Node 22.13+)`;
|
|
2608
|
-
}
|
|
2609
|
-
function linksInto(link, dir) {
|
|
2610
|
-
const st = fs17.lstatSync(link, { throwIfNoEntry: false });
|
|
2611
|
-
if (!st?.isSymbolicLink()) return false;
|
|
2612
|
-
const target = path14.resolve(path14.dirname(link), fs17.readlinkSync(link));
|
|
2613
|
-
const rel = path14.relative(dir, target);
|
|
2614
|
-
return !rel.startsWith("..") && !path14.isAbsolute(rel);
|
|
2511
|
+
return PRESETS[eff.primary.value.toLowerCase()] ?? PRESETS.sonnet;
|
|
2615
2512
|
}
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
const
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
fs17.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
2622
|
-
} else if (st) {
|
|
2623
|
-
fs17.unlinkSync(dest);
|
|
2624
|
-
}
|
|
2625
|
-
if (opts.copy) fs17.cpSync(src, dest, { recursive: true });
|
|
2626
|
-
else fs17.symlinkSync(src, dest, process.platform === "win32" ? "junction" : fs17.statSync(src).isDirectory() ? "dir" : "file");
|
|
2627
|
-
return `\u2714 ${dest} \u2192 ${opts.copy ? "copied" : src}`;
|
|
2513
|
+
var estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
2514
|
+
function savedUsd(usage2, returnedTokens, price = primaryPrice()) {
|
|
2515
|
+
const wouldCost = (usage2.input * price.input + usage2.cacheRead * price.cachedInput + (usage2.output + usage2.reasoning) * price.output) / 1e6;
|
|
2516
|
+
const readingTheReport = returnedTokens * price.input / 1e6;
|
|
2517
|
+
return Math.max(0, wouldCost - (usage2.cost ?? 0) - readingTheReport);
|
|
2628
2518
|
}
|
|
2629
|
-
function
|
|
2630
|
-
|
|
2631
|
-
const
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
}
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
const onPath = (process.env.PATH ?? "").split(path14.delimiter).some((d) => path14.resolve(d) === path14.dirname(launcherPath()));
|
|
2648
|
-
if (!onPath) out.push(`! ${path14.dirname(launcherPath())} is not on PATH; add it, or run ${launcherPath()} directly`);
|
|
2649
|
-
}
|
|
2650
|
-
return out;
|
|
2519
|
+
function record(meta) {
|
|
2520
|
+
if (!meta.usage?.steps || meta.auditOf) return;
|
|
2521
|
+
const entry = {
|
|
2522
|
+
id: meta.id,
|
|
2523
|
+
at: meta.endedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2524
|
+
mode: meta.mode,
|
|
2525
|
+
state: meta.state,
|
|
2526
|
+
backend: (meta.ran ?? meta.worker).backend,
|
|
2527
|
+
model: meta.resolvedModel ?? (meta.ran ?? meta.worker).model,
|
|
2528
|
+
tokens: meta.usage.total,
|
|
2529
|
+
returned: meta.returnedTokens ?? 0,
|
|
2530
|
+
workerCost: meta.usage.cost ?? 0,
|
|
2531
|
+
saved: meta.savedUsd ?? 0,
|
|
2532
|
+
price: primaryPrice().name
|
|
2533
|
+
};
|
|
2534
|
+
fs16.mkdirSync(path14.dirname(ledgerFile()), { recursive: true });
|
|
2535
|
+
fs16.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
|
|
2536
|
+
`);
|
|
2651
2537
|
}
|
|
2652
|
-
function
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
if (linksInto(l, root)) {
|
|
2660
|
-
fs17.unlinkSync(l);
|
|
2661
|
-
out.push(`\u2714 removed ${l}`);
|
|
2662
|
-
}
|
|
2538
|
+
function readLedger(sinceMs2) {
|
|
2539
|
+
if (!fs16.existsSync(ledgerFile())) return [];
|
|
2540
|
+
return fs16.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
|
|
2541
|
+
try {
|
|
2542
|
+
return [JSON.parse(l)];
|
|
2543
|
+
} catch {
|
|
2544
|
+
return [];
|
|
2663
2545
|
}
|
|
2664
|
-
}
|
|
2665
|
-
if (isOurLauncher(launcherPath(), root)) {
|
|
2666
|
-
fs17.rmSync(launcherPath(), { force: true });
|
|
2667
|
-
out.push(`\u2714 removed ${launcherPath()}`);
|
|
2668
|
-
}
|
|
2669
|
-
return out.length ? out : ["nothing to remove"];
|
|
2546
|
+
}).filter((e) => !sinceMs2 || Date.parse(e.at) >= sinceMs2);
|
|
2670
2547
|
}
|
|
2671
|
-
function
|
|
2672
|
-
const
|
|
2673
|
-
|
|
2548
|
+
function totals(entries) {
|
|
2549
|
+
const t = entries.reduce(
|
|
2550
|
+
(a, e) => ({
|
|
2551
|
+
runs: a.runs + 1,
|
|
2552
|
+
tokens: a.tokens + e.tokens,
|
|
2553
|
+
returned: a.returned + e.returned,
|
|
2554
|
+
workerCost: a.workerCost + e.workerCost,
|
|
2555
|
+
saved: a.saved + e.saved
|
|
2556
|
+
}),
|
|
2557
|
+
{ runs: 0, tokens: 0, returned: 0, workerCost: 0, saved: 0 }
|
|
2558
|
+
);
|
|
2559
|
+
return { ...t, ratio: t.returned ? t.tokens / t.returned : 0 };
|
|
2674
2560
|
}
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
"code-reviewer": "pitroom-review/code-reviewer.md"
|
|
2682
|
-
};
|
|
2683
|
-
function loadTemplate(name, root = packageRoot()) {
|
|
2684
|
-
const file2 = path15.join(root, "skills", FILES[name]);
|
|
2685
|
-
if (!fs18.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
|
|
2686
|
-
return fs18.readFileSync(file2, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
|
|
2561
|
+
var usd = (n) => n >= 100 ? `$${n.toFixed(0)}` : n >= 1 ? `$${n.toFixed(2)}` : `$${n.toFixed(3)}`;
|
|
2562
|
+
function compact(n) {
|
|
2563
|
+
const fmt = (v) => v.toFixed(v >= 100 ? 0 : 1).replace(/\.0$/, "");
|
|
2564
|
+
if (n >= 1e6) return `${fmt(n / 1e6)}M`;
|
|
2565
|
+
if (n >= 1e3) return `${fmt(n / 1e3)}k`;
|
|
2566
|
+
return String(Math.round(n));
|
|
2687
2567
|
}
|
|
2688
|
-
function
|
|
2689
|
-
const
|
|
2690
|
-
|
|
2691
|
-
return template.replace(/\{\{([A-Z_]+)\}\}/g, (_, key) => values[key]);
|
|
2692
|
-
}
|
|
2693
|
-
|
|
2694
|
-
// src/core/slots.ts
|
|
2695
|
-
import crypto5 from "node:crypto";
|
|
2696
|
-
import fs19 from "node:fs";
|
|
2697
|
-
import path16 from "node:path";
|
|
2698
|
-
var slotsDir = () => path16.join(home(), "slots");
|
|
2699
|
-
var locksDir = () => path16.join(home(), "locks");
|
|
2700
|
-
var STARTUP_GRACE_MS = 3e4;
|
|
2701
|
-
function holderActive(runId, self) {
|
|
2702
|
-
if (!runId || runId === self) return false;
|
|
2703
|
-
try {
|
|
2704
|
-
const m = freshMeta(runId);
|
|
2705
|
-
if (!isActive(m.state)) return false;
|
|
2706
|
-
if (m.pid) return isAlive(m.pid);
|
|
2707
|
-
return Date.now() - Date.parse(m.startedAt) < STARTUP_GRACE_MS;
|
|
2708
|
-
} catch {
|
|
2709
|
-
return false;
|
|
2710
|
-
}
|
|
2711
|
-
}
|
|
2712
|
-
function tryClaim(file2, runId) {
|
|
2713
|
-
fs19.mkdirSync(path16.dirname(file2), { recursive: true });
|
|
2714
|
-
try {
|
|
2715
|
-
fs19.writeFileSync(file2, runId, { flag: "wx" });
|
|
2716
|
-
return true;
|
|
2717
|
-
} catch (e) {
|
|
2718
|
-
if (e.code !== "EEXIST") throw e;
|
|
2719
|
-
}
|
|
2720
|
-
let owner = "";
|
|
2721
|
-
try {
|
|
2722
|
-
owner = fs19.readFileSync(file2, "utf8").trim();
|
|
2723
|
-
} catch {
|
|
2724
|
-
}
|
|
2725
|
-
if (owner === runId) return true;
|
|
2726
|
-
if (holderActive(owner, runId)) return false;
|
|
2727
|
-
fs19.rmSync(file2, { force: true });
|
|
2728
|
-
try {
|
|
2729
|
-
fs19.writeFileSync(file2, runId, { flag: "wx" });
|
|
2730
|
-
return true;
|
|
2731
|
-
} catch {
|
|
2732
|
-
return false;
|
|
2733
|
-
}
|
|
2734
|
-
}
|
|
2735
|
-
function releaseIfOwner(file2, runId) {
|
|
2736
|
-
if (!file2) return;
|
|
2737
|
-
try {
|
|
2738
|
-
if (fs19.readFileSync(file2, "utf8").trim() === runId) fs19.rmSync(file2, { force: true });
|
|
2739
|
-
} catch {
|
|
2740
|
-
}
|
|
2741
|
-
}
|
|
2742
|
-
function tryAcquireSlot(runId, maxParallel) {
|
|
2743
|
-
for (let n = 0; n < Math.max(1, maxParallel); n++) {
|
|
2744
|
-
const file2 = path16.join(slotsDir(), `slot-${n}`);
|
|
2745
|
-
if (tryClaim(file2, runId)) return file2;
|
|
2746
|
-
}
|
|
2747
|
-
return void 0;
|
|
2748
|
-
}
|
|
2749
|
-
var releaseSlot = (file2, runId) => releaseIfOwner(file2, runId);
|
|
2750
|
-
var lockFile = (repoRoot2) => path16.join(locksDir(), `write-${crypto5.createHash("sha1").update(path16.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
|
|
2751
|
-
function acquireWriteLock(repoRoot2, runId) {
|
|
2752
|
-
const file2 = lockFile(repoRoot2);
|
|
2753
|
-
if (tryClaim(file2, runId)) return;
|
|
2754
|
-
let owner = "";
|
|
2755
|
-
try {
|
|
2756
|
-
owner = fs19.readFileSync(file2, "utf8").trim();
|
|
2757
|
-
} catch {
|
|
2758
|
-
}
|
|
2759
|
-
throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
|
|
2760
|
-
}
|
|
2761
|
-
var releaseWriteLock = (repoRoot2, runId) => releaseIfOwner(lockFile(repoRoot2), runId);
|
|
2762
|
-
|
|
2763
|
-
// src/core/prompt.ts
|
|
2764
|
-
var RULES = {
|
|
2765
|
-
read: ["READ-ONLY task: do not create, modify or delete any file. Read, search and analyse only."],
|
|
2766
|
-
write: [
|
|
2767
|
-
"You may edit files that the task requires. Make the smallest correct change; no drive-by refactors.",
|
|
2768
|
-
"Run the relevant tests, type checks or linters if they are cheap and available, and report the result."
|
|
2769
|
-
],
|
|
2770
|
-
isolate: [
|
|
2771
|
-
"You are in an isolated copy of the project. Edit files as the task requires; the primary agent reviews your diff before anything reaches the real tree.",
|
|
2772
|
-
"Make the smallest correct change; no drive-by refactors. Dependency folders (node_modules, virtualenvs, build output) may be missing unless linked.",
|
|
2773
|
-
"Run the relevant tests, type checks or linters if they are cheap and available, and report the result."
|
|
2774
|
-
]
|
|
2775
|
-
};
|
|
2776
|
-
function buildPrompt(mode, task, followUp) {
|
|
2777
|
-
if (followUp) return `Follow-up from the primary agent. Same rules and answer format as before.
|
|
2778
|
-
|
|
2779
|
-
${task}`;
|
|
2780
|
-
return `You are a worker agent. A primary coding agent delegated this bounded task to you. Your answer is draft work that the primary agent will verify, and it is the only thing the primary sees, so make it self-contained.
|
|
2781
|
-
|
|
2782
|
-
Rules:
|
|
2783
|
-
- ${RULES[mode].join("\n- ")}
|
|
2784
|
-
- Stay inside the task scope and the project directory.
|
|
2785
|
-
- Never commit, push, reset, checkout, restore, stash, clean, rebase or merge, and never discard or overwrite uncommitted work you did not create.
|
|
2786
|
-
- Never read or reveal secrets (.env files, keys, tokens).
|
|
2787
|
-
- Delete a file only when the task explicitly asks for it, and name every file you delete under FILES CHANGED. Whether anything else should be deleted or overwritten is the primary agent's decision: propose it under OPEN ISSUES and leave the file alone.
|
|
2788
|
-
- Do not ask questions. If something is ambiguous, choose the safest reasonable interpretation and state the assumption.
|
|
2789
|
-
- Be economical: open only what you need.
|
|
2790
|
-
|
|
2791
|
-
End with this answer format (plain text, no preamble):
|
|
2792
|
-
SUMMARY: 1-5 lines that directly answer the task.
|
|
2793
|
-
DETAILS: key findings with file:line references; only what the primary agent needs.
|
|
2794
|
-
FILES CHANGED: paths, or "none".
|
|
2795
|
-
VERIFICATION: commands you ran and their results, or "not run".
|
|
2796
|
-
OPEN ISSUES: risks, uncertainties, follow-ups, or "none".
|
|
2797
|
-
|
|
2798
|
-
Task:
|
|
2799
|
-
${task}`;
|
|
2800
|
-
}
|
|
2801
|
-
|
|
2802
|
-
// src/core/receipt.ts
|
|
2803
|
-
import fs20 from "node:fs";
|
|
2804
|
-
import path17 from "node:path";
|
|
2805
|
-
var PRESETS = {
|
|
2806
|
-
sonnet: { name: "Claude Sonnet", input: 3, output: 15, cachedInput: 0.3 },
|
|
2807
|
-
opus: { name: "Claude Opus", input: 5, output: 25, cachedInput: 0.5 },
|
|
2808
|
-
haiku: { name: "Claude Haiku", input: 1, output: 5, cachedInput: 0.1 },
|
|
2809
|
-
"gpt-5": { name: "GPT-5", input: 1.25, output: 10, cachedInput: 0.125 }
|
|
2810
|
-
};
|
|
2811
|
-
function primaryPrice() {
|
|
2812
|
-
const eff = effective();
|
|
2813
|
-
const custom = eff.price.value?.split(",").map(Number);
|
|
2814
|
-
if (custom && custom.length >= 2 && custom.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
2815
|
-
return { name: "custom", input: custom[0], output: custom[1], cachedInput: custom[2] ?? custom[0] / 10 };
|
|
2816
|
-
}
|
|
2817
|
-
return PRESETS[eff.primary.value.toLowerCase()] ?? PRESETS.sonnet;
|
|
2818
|
-
}
|
|
2819
|
-
var estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
2820
|
-
function savedUsd(usage2, returnedTokens, price = primaryPrice()) {
|
|
2821
|
-
const wouldCost = (usage2.input * price.input + usage2.cacheRead * price.cachedInput + (usage2.output + usage2.reasoning) * price.output) / 1e6;
|
|
2822
|
-
const readingTheReport = returnedTokens * price.input / 1e6;
|
|
2823
|
-
return Math.max(0, wouldCost - (usage2.cost ?? 0) - readingTheReport);
|
|
2824
|
-
}
|
|
2825
|
-
function record(meta) {
|
|
2826
|
-
if (!meta.usage?.steps || meta.auditOf) return;
|
|
2827
|
-
const entry = {
|
|
2828
|
-
id: meta.id,
|
|
2829
|
-
at: meta.endedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2830
|
-
mode: meta.mode,
|
|
2831
|
-
state: meta.state,
|
|
2832
|
-
backend: (meta.ran ?? meta.worker).backend,
|
|
2833
|
-
model: meta.resolvedModel ?? (meta.ran ?? meta.worker).model,
|
|
2834
|
-
tokens: meta.usage.total,
|
|
2835
|
-
returned: meta.returnedTokens ?? 0,
|
|
2836
|
-
workerCost: meta.usage.cost ?? 0,
|
|
2837
|
-
saved: meta.savedUsd ?? 0,
|
|
2838
|
-
price: primaryPrice().name
|
|
2839
|
-
};
|
|
2840
|
-
fs20.mkdirSync(path17.dirname(ledgerFile()), { recursive: true });
|
|
2841
|
-
fs20.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
|
|
2842
|
-
`);
|
|
2843
|
-
}
|
|
2844
|
-
function readLedger(sinceMs2) {
|
|
2845
|
-
if (!fs20.existsSync(ledgerFile())) return [];
|
|
2846
|
-
return fs20.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
|
|
2847
|
-
try {
|
|
2848
|
-
return [JSON.parse(l)];
|
|
2849
|
-
} catch {
|
|
2850
|
-
return [];
|
|
2851
|
-
}
|
|
2852
|
-
}).filter((e) => !sinceMs2 || Date.parse(e.at) >= sinceMs2);
|
|
2853
|
-
}
|
|
2854
|
-
function totals(entries) {
|
|
2855
|
-
const t = entries.reduce(
|
|
2856
|
-
(a, e) => ({
|
|
2857
|
-
runs: a.runs + 1,
|
|
2858
|
-
tokens: a.tokens + e.tokens,
|
|
2859
|
-
returned: a.returned + e.returned,
|
|
2860
|
-
workerCost: a.workerCost + e.workerCost,
|
|
2861
|
-
saved: a.saved + e.saved
|
|
2862
|
-
}),
|
|
2863
|
-
{ runs: 0, tokens: 0, returned: 0, workerCost: 0, saved: 0 }
|
|
2864
|
-
);
|
|
2865
|
-
return { ...t, ratio: t.returned ? t.tokens / t.returned : 0 };
|
|
2866
|
-
}
|
|
2867
|
-
var usd = (n) => n >= 100 ? `$${n.toFixed(0)}` : n >= 1 ? `$${n.toFixed(2)}` : `$${n.toFixed(3)}`;
|
|
2868
|
-
function compact(n) {
|
|
2869
|
-
const fmt = (v) => v.toFixed(v >= 100 ? 0 : 1).replace(/\.0$/, "");
|
|
2870
|
-
if (n >= 1e6) return `${fmt(n / 1e6)}M`;
|
|
2871
|
-
if (n >= 1e3) return `${fmt(n / 1e3)}k`;
|
|
2872
|
-
return String(Math.round(n));
|
|
2873
|
-
}
|
|
2874
|
-
function badgeUrl(t) {
|
|
2875
|
-
const msg = encodeURIComponent(`saved ${usd(t.saved)} \xB7 ${compact(t.tokens)} tokens offloaded`).replace(/-/g, "--");
|
|
2876
|
-
return `https://img.shields.io/badge/pitroom-${msg}-7c3aed`;
|
|
2568
|
+
function badgeUrl(t) {
|
|
2569
|
+
const msg = encodeURIComponent(`saved ${usd(t.saved)} \xB7 ${compact(t.tokens)} tokens offloaded`).replace(/-/g, "--");
|
|
2570
|
+
return `https://img.shields.io/badge/pitroom-${msg}-7c3aed`;
|
|
2877
2571
|
}
|
|
2878
2572
|
var esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
2879
2573
|
function card(t, period) {
|
|
@@ -2917,194 +2611,34 @@ ${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.rou
|
|
|
2917
2611
|
`;
|
|
2918
2612
|
}
|
|
2919
2613
|
|
|
2920
|
-
// src/core/
|
|
2921
|
-
import
|
|
2922
|
-
|
|
2923
|
-
var
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
var WINDOW = 5;
|
|
2937
|
-
var DEFINITION_LOOKBACK = 80;
|
|
2938
|
-
function looksLikeSymbol(m) {
|
|
2939
|
-
const name = m[1];
|
|
2940
|
-
return m[0].includes("(") || /[A-Z_$0-9]/.test(name) || name.length >= 5;
|
|
2614
|
+
// src/core/ui.ts
|
|
2615
|
+
import fs17 from "node:fs";
|
|
2616
|
+
var WEEK_MS = 7 * 24 * 3600 * 1e3;
|
|
2617
|
+
var RECENT = 40;
|
|
2618
|
+
var RECENT_HOURS = 1;
|
|
2619
|
+
var MAX_CARDS = 5;
|
|
2620
|
+
function activeRuns() {
|
|
2621
|
+
const out = [];
|
|
2622
|
+
for (const id of listRunIds().slice(-RECENT)) {
|
|
2623
|
+
try {
|
|
2624
|
+
const m = readMeta(id);
|
|
2625
|
+
if (isActive(m.state) && (m.state === "queued" || isAlive(m.pid))) out.push(m);
|
|
2626
|
+
} catch {
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
return out;
|
|
2941
2630
|
}
|
|
2942
|
-
function
|
|
2943
|
-
const
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
const
|
|
2949
|
-
|
|
2950
|
-
const pos = i.index ?? 0;
|
|
2951
|
-
const before = matches2.filter((m) => (m.index ?? 0) + m[0].length <= pos).at(-1);
|
|
2952
|
-
const after = matches2.find((m) => (m.index ?? 0) >= pos + i[0].length);
|
|
2953
|
-
return !(before && after && !sepBetween((before.index ?? 0) + before[0].length, after.index ?? 0));
|
|
2954
|
-
});
|
|
2955
|
-
matches2.forEach((m, k) => {
|
|
2956
|
-
const start = Number(m[2]);
|
|
2957
|
-
const end = m[3] ? Number(m[3]) : start;
|
|
2958
|
-
if (start < 1 || end < start) return;
|
|
2959
|
-
const at = m.index ?? 0;
|
|
2960
|
-
const prev = matches2[k - 1];
|
|
2961
|
-
const next = matches2[k + 1];
|
|
2962
|
-
const from = Math.max(at - 80, prev ? (prev.index ?? 0) + prev[0].length : 0, ...seps.filter((s) => s < at).map((s) => s + 1));
|
|
2963
|
-
const to = Math.min(at + m[0].length + 40, next?.index ?? Infinity, ...seps.filter((s) => s >= at + m[0].length));
|
|
2964
|
-
const near = new Set(idents.filter((i) => (i.index ?? 0) >= from && (i.index ?? 0) < to).map((i) => i[1]));
|
|
2965
|
-
const symbol = near.size === 1 ? [...near][0] : void 0;
|
|
2966
|
-
const key = `${m[1]}:${start}-${end}:${symbol ?? ""}`;
|
|
2967
|
-
if (!seen.has(key)) seen.set(key, { text: m[0], file: m[1], start, end, symbol });
|
|
2968
|
-
});
|
|
2631
|
+
function statusLine() {
|
|
2632
|
+
const running = activeRuns();
|
|
2633
|
+
const saved = totals(readLedger(Date.now() - WEEK_MS)).saved;
|
|
2634
|
+
if (!running.length && !saved) return "";
|
|
2635
|
+
const parts = ["\u{1F3C1} pitroom"];
|
|
2636
|
+
if (running.length) {
|
|
2637
|
+
const who = [...new Set(running.map((m) => (m.ran ?? m.worker).backend))].join(", ");
|
|
2638
|
+
parts.push(`${running.length} running (${who})`);
|
|
2969
2639
|
}
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
function verifyRefs(refs, dirs) {
|
|
2973
|
-
const roots = [...new Set(dirs.filter(Boolean).map((d) => path18.resolve(d)))];
|
|
2974
|
-
const invalid = [];
|
|
2975
|
-
const lineCache = /* @__PURE__ */ new Map();
|
|
2976
|
-
const load = (file2) => {
|
|
2977
|
-
if (!lineCache.has(file2)) {
|
|
2978
|
-
const size = fs21.statSync(file2).size;
|
|
2979
|
-
const lines = size > MAX_BYTES ? null : fs21.readFileSync(file2, "utf8").split("\n");
|
|
2980
|
-
if (lines && lines[lines.length - 1] === "") lines.pop();
|
|
2981
|
-
lineCache.set(file2, lines);
|
|
2982
|
-
}
|
|
2983
|
-
return lineCache.get(file2);
|
|
2984
|
-
};
|
|
2985
|
-
let index;
|
|
2986
|
-
const byName = () => index ??= indexFiles(roots);
|
|
2987
|
-
const candidates = (ref) => {
|
|
2988
|
-
const direct = resolve(ref.file, roots);
|
|
2989
|
-
if (direct) return [direct];
|
|
2990
|
-
if (path18.isAbsolute(ref.file)) return [];
|
|
2991
|
-
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
2992
|
-
return (byName().get(path18.basename(wanted)) ?? []).filter((f) => f.split(path18.sep).join("/").endsWith(`/${wanted}`) || path18.basename(f) === wanted).slice(0, 20);
|
|
2993
|
-
};
|
|
2994
|
-
const check = (file2, ref) => {
|
|
2995
|
-
const lines = load(file2);
|
|
2996
|
-
if (!lines) return void 0;
|
|
2997
|
-
if (ref.end > lines.length) return `file has ${lines.length} lines`;
|
|
2998
|
-
if (ref.symbol && !mentions(lines, ref) && !enclosedBy(lines, ref)) return `\`${ref.symbol}\` not near line ${ref.start}`;
|
|
2999
|
-
return void 0;
|
|
3000
|
-
};
|
|
3001
|
-
let total = 0;
|
|
3002
|
-
for (const ref of refs) {
|
|
3003
|
-
const files = candidates(ref);
|
|
3004
|
-
if (!files.length) {
|
|
3005
|
-
if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
|
|
3006
|
-
total++;
|
|
3007
|
-
invalid.push({ ref: ref.text, reason: path18.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
|
|
3008
|
-
continue;
|
|
3009
|
-
}
|
|
3010
|
-
total++;
|
|
3011
|
-
const reasons = files.map((f) => check(f, ref));
|
|
3012
|
-
if (!reasons.some((r) => r === void 0)) invalid.push({ ref: ref.text, reason: files.length > 1 ? `${reasons[0]} (${files.length} files match)` : reasons[0] });
|
|
3013
|
-
}
|
|
3014
|
-
return { total, valid: total - invalid.length, invalid };
|
|
3015
|
-
}
|
|
3016
|
-
function mentions(lines, ref) {
|
|
3017
|
-
const near = lines.slice(Math.max(0, ref.start - 1 - WINDOW), ref.end + WINDOW).join("\n");
|
|
3018
|
-
return new RegExp(`(^|[^\\w$])${escape(ref.symbol)}([^\\w$]|$)`).test(near);
|
|
3019
|
-
}
|
|
3020
|
-
function enclosedBy(lines, ref) {
|
|
3021
|
-
const name = escape(ref.symbol);
|
|
3022
|
-
const def = new RegExp(
|
|
3023
|
-
`\\b(function|def|fn|func|class|interface|struct|impl|type|const|let|var)\\s+\\*?${name}\\b|^\\s*(export\\s+)?(default\\s+)?(async\\s+)?(static\\s+)?${name}\\s*[(:=]`
|
|
3024
|
-
);
|
|
3025
|
-
return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
|
|
3026
|
-
}
|
|
3027
|
-
var isFileName = (name) => {
|
|
3028
|
-
const base2 = path18.basename(name);
|
|
3029
|
-
return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
|
|
3030
|
-
};
|
|
3031
|
-
function indexFiles(roots) {
|
|
3032
|
-
const index = /* @__PURE__ */ new Map();
|
|
3033
|
-
let count = 0;
|
|
3034
|
-
const walk = (dir) => {
|
|
3035
|
-
let entries;
|
|
3036
|
-
try {
|
|
3037
|
-
entries = fs21.readdirSync(dir, { withFileTypes: true });
|
|
3038
|
-
} catch {
|
|
3039
|
-
return;
|
|
3040
|
-
}
|
|
3041
|
-
for (const e of entries) {
|
|
3042
|
-
if (count >= MAX_INDEXED) return;
|
|
3043
|
-
if (e.isDirectory()) {
|
|
3044
|
-
if (!SKIP_DIRS2.has(e.name)) walk(path18.join(dir, e.name));
|
|
3045
|
-
} else if (e.isFile()) {
|
|
3046
|
-
count++;
|
|
3047
|
-
const list2 = index.get(e.name);
|
|
3048
|
-
if (list2) list2.push(path18.join(dir, e.name));
|
|
3049
|
-
else index.set(e.name, [path18.join(dir, e.name)]);
|
|
3050
|
-
}
|
|
3051
|
-
}
|
|
3052
|
-
};
|
|
3053
|
-
for (const r of roots) walk(r);
|
|
3054
|
-
return index;
|
|
3055
|
-
}
|
|
3056
|
-
function resolve(ref, roots) {
|
|
3057
|
-
const candidates = path18.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path18.join(r, ref));
|
|
3058
|
-
return candidates.find((c) => {
|
|
3059
|
-
try {
|
|
3060
|
-
return fs21.statSync(c).isFile() && inside(c, roots);
|
|
3061
|
-
} catch {
|
|
3062
|
-
return false;
|
|
3063
|
-
}
|
|
3064
|
-
});
|
|
3065
|
-
}
|
|
3066
|
-
function inside(file2, roots) {
|
|
3067
|
-
return roots.some((r) => {
|
|
3068
|
-
const rel = path18.relative(r, path18.resolve(file2));
|
|
3069
|
-
return rel !== "" && !rel.startsWith("..") && !path18.isAbsolute(rel);
|
|
3070
|
-
});
|
|
3071
|
-
}
|
|
3072
|
-
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3073
|
-
|
|
3074
|
-
// src/core/history.ts
|
|
3075
|
-
import fs23 from "node:fs";
|
|
3076
|
-
import { createRequire } from "node:module";
|
|
3077
|
-
import path19 from "node:path";
|
|
3078
|
-
import zlib from "node:zlib";
|
|
3079
|
-
|
|
3080
|
-
// src/core/ui.ts
|
|
3081
|
-
import fs22 from "node:fs";
|
|
3082
|
-
var WEEK_MS = 7 * 24 * 3600 * 1e3;
|
|
3083
|
-
var RECENT = 40;
|
|
3084
|
-
var RECENT_HOURS = 1;
|
|
3085
|
-
var MAX_CARDS = 5;
|
|
3086
|
-
function activeRuns() {
|
|
3087
|
-
const out = [];
|
|
3088
|
-
for (const id of listRunIds().slice(-RECENT)) {
|
|
3089
|
-
try {
|
|
3090
|
-
const m = readMeta(id);
|
|
3091
|
-
if (isActive(m.state) && (m.state === "queued" || isAlive(m.pid))) out.push(m);
|
|
3092
|
-
} catch {
|
|
3093
|
-
}
|
|
3094
|
-
}
|
|
3095
|
-
return out;
|
|
3096
|
-
}
|
|
3097
|
-
function statusLine() {
|
|
3098
|
-
const running = activeRuns();
|
|
3099
|
-
const saved = totals(readLedger(Date.now() - WEEK_MS)).saved;
|
|
3100
|
-
if (!running.length && !saved) return "";
|
|
3101
|
-
const parts = ["\u{1F3C1} pitroom"];
|
|
3102
|
-
if (running.length) {
|
|
3103
|
-
const who = [...new Set(running.map((m) => (m.ran ?? m.worker).backend))].join(", ");
|
|
3104
|
-
parts.push(`${running.length} running (${who})`);
|
|
3105
|
-
}
|
|
3106
|
-
if (saved) parts.push(`~${usd(saved)} saved this week`);
|
|
3107
|
-
return parts.join(" \xB7 ");
|
|
2640
|
+
if (saved) parts.push(`~${usd(saved)} saved this week`);
|
|
2641
|
+
return parts.join(" \xB7 ");
|
|
3108
2642
|
}
|
|
3109
2643
|
var RUN_ID = /\b\d{8}-\d{6}-[0-9a-f]{4}\b/g;
|
|
3110
2644
|
var PITROOM_CALL = /(^|[\s;&|(`])(\S*\/)?pitroom(\.mjs)?(\s|$)/;
|
|
@@ -3169,9 +2703,9 @@ function cardsFor(m) {
|
|
|
3169
2703
|
const out = [];
|
|
3170
2704
|
for (const [phase, text] of phases) {
|
|
3171
2705
|
const marker = runFile(m.id, `card-${phase}`);
|
|
3172
|
-
if (
|
|
2706
|
+
if (fs17.existsSync(marker)) continue;
|
|
3173
2707
|
if (!(phase === "started" && !isActive(m.state))) out.push(text());
|
|
3174
|
-
|
|
2708
|
+
fs17.writeFileSync(marker, "");
|
|
3175
2709
|
}
|
|
3176
2710
|
return out;
|
|
3177
2711
|
}
|
|
@@ -3182,10 +2716,10 @@ function hookCards(input) {
|
|
|
3182
2716
|
const response = event.tool_response;
|
|
3183
2717
|
const output = typeof response === "string" ? response : [response?.stdout, response?.stderr].filter(Boolean).join("\n");
|
|
3184
2718
|
const ids = [...new Set(`${command}
|
|
3185
|
-
${output}`.match(RUN_ID) ?? [])].filter((id) =>
|
|
2719
|
+
${output}`.match(RUN_ID) ?? [])].filter((id) => fs17.existsSync(`${runsDir()}/${id}`));
|
|
3186
2720
|
const since2 = Date.now() - RECENT_HOURS * 36e5;
|
|
3187
2721
|
for (const id of listRunIds().slice(-RECENT)) {
|
|
3188
|
-
if (ids.includes(id) ||
|
|
2722
|
+
if (ids.includes(id) || fs17.existsSync(runFile(id, "card-ended"))) continue;
|
|
3189
2723
|
try {
|
|
3190
2724
|
const m = readMeta(id);
|
|
3191
2725
|
if (!isActive(m.state) && Date.parse(m.endedAt ?? m.startedAt) > since2) ids.push(id);
|
|
@@ -3207,7 +2741,7 @@ var SCHEMA_VERSION = 1;
|
|
|
3207
2741
|
var PATCH_MAX = 1e6;
|
|
3208
2742
|
var ANSWER_MAX2 = 2e5;
|
|
3209
2743
|
var cached2;
|
|
3210
|
-
var historyFile = () =>
|
|
2744
|
+
var historyFile = () => path15.join(home(), "history.db");
|
|
3211
2745
|
function openDb() {
|
|
3212
2746
|
const file2 = historyFile();
|
|
3213
2747
|
if (cached2?.file === file2) return cached2.db;
|
|
@@ -3221,7 +2755,7 @@ function openDb() {
|
|
|
3221
2755
|
} finally {
|
|
3222
2756
|
process.emitWarning = emit;
|
|
3223
2757
|
}
|
|
3224
|
-
|
|
2758
|
+
fs18.mkdirSync(path15.dirname(file2), { recursive: true });
|
|
3225
2759
|
db = new DatabaseSync(file2);
|
|
3226
2760
|
db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
|
|
3227
2761
|
migrate(db);
|
|
@@ -3256,31 +2790,31 @@ function migrate(db) {
|
|
|
3256
2790
|
function readRunFile(id, name) {
|
|
3257
2791
|
const f = runFile(id, name);
|
|
3258
2792
|
try {
|
|
3259
|
-
if (
|
|
3260
|
-
if (
|
|
2793
|
+
if (fs18.existsSync(f)) return fs18.readFileSync(f, "utf8");
|
|
2794
|
+
if (fs18.existsSync(`${f}.gz`)) return zlib.gunzipSync(fs18.readFileSync(`${f}.gz`)).toString("utf8");
|
|
3261
2795
|
} catch {
|
|
3262
2796
|
}
|
|
3263
2797
|
return void 0;
|
|
3264
2798
|
}
|
|
3265
2799
|
function gzipFile(f) {
|
|
3266
|
-
if (!
|
|
3267
|
-
|
|
3268
|
-
|
|
2800
|
+
if (!fs18.existsSync(f)) return;
|
|
2801
|
+
fs18.writeFileSync(`${f}.gz`, zlib.gzipSync(fs18.readFileSync(f)));
|
|
2802
|
+
fs18.rmSync(f);
|
|
3269
2803
|
}
|
|
3270
2804
|
function compactRun(meta) {
|
|
3271
2805
|
if (!TERMINAL.includes(meta.state)) return;
|
|
3272
|
-
const dir =
|
|
2806
|
+
const dir = path15.dirname(runFile(meta.id, "meta.json"));
|
|
3273
2807
|
let names;
|
|
3274
2808
|
try {
|
|
3275
|
-
names =
|
|
2809
|
+
names = fs18.readdirSync(dir);
|
|
3276
2810
|
} catch {
|
|
3277
2811
|
return;
|
|
3278
2812
|
}
|
|
3279
2813
|
for (const n of names) {
|
|
3280
|
-
if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(
|
|
2814
|
+
if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(path15.join(dir, n));
|
|
3281
2815
|
else if (/^stderr(\.attempt-\d+)?\.log$/.test(n)) {
|
|
3282
|
-
if (meta.state === "done" && !/attempt/.test(n))
|
|
3283
|
-
else gzipFile(
|
|
2816
|
+
if (meta.state === "done" && !/attempt/.test(n)) fs18.rmSync(path15.join(dir, n), { force: true });
|
|
2817
|
+
else gzipFile(path15.join(dir, n));
|
|
3284
2818
|
}
|
|
3285
2819
|
}
|
|
3286
2820
|
}
|
|
@@ -3489,14 +3023,14 @@ function auditStats(db, since2) {
|
|
|
3489
3023
|
return { total, byWorker };
|
|
3490
3024
|
}
|
|
3491
3025
|
function workerRows(rows, ledger, audited) {
|
|
3492
|
-
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, audited: 0, agreed: 0 }));
|
|
3026
|
+
const out = rows.map((r) => ({ backend: r.backend, model: r.model ?? void 0, runs: r.runs, ok: r.ok ?? 0, limited: r.limited ?? 0, avgSeconds: r.avg_s, avgTokens: r.avg_t, saved: 0, audited: 0, agreed: 0 }));
|
|
3493
3027
|
const key = (backend, model) => `${backend ?? ""}\0${model ?? ""}`;
|
|
3494
3028
|
const index = new Map(out.map((r) => [key(r.backend, r.model), r]));
|
|
3495
3029
|
for (const e of ledger) {
|
|
3496
3030
|
const backend = e.backend ?? "opencode";
|
|
3497
3031
|
let row = index.get(key(backend, e.model)) ?? (e.model ? out.find((r) => r.backend === backend && r.model?.endsWith(`/${e.model}`)) : void 0);
|
|
3498
3032
|
if (!row) {
|
|
3499
|
-
row = { backend, model: e.model, runs: 0, ok: 0, avgSeconds: null, avgTokens: null, saved: 0, audited: 0, agreed: 0 };
|
|
3033
|
+
row = { backend, model: e.model, runs: 0, ok: 0, limited: 0, avgSeconds: null, avgTokens: null, saved: 0, audited: 0, agreed: 0 };
|
|
3500
3034
|
index.set(key(backend, e.model), row);
|
|
3501
3035
|
out.push(row);
|
|
3502
3036
|
}
|
|
@@ -3508,24 +3042,28 @@ function workerRows(rows, ledger, audited) {
|
|
|
3508
3042
|
}
|
|
3509
3043
|
return out;
|
|
3510
3044
|
}
|
|
3045
|
+
var LIMITED = `(state='failed' AND (json_extract(meta_json,'$.failureKind')='rate-limited' OR error LIKE '%the model is rate-limited or overloaded%'))`;
|
|
3511
3046
|
function historyStats(sinceMs2) {
|
|
3512
|
-
const
|
|
3047
|
+
const counted = effective().countRateLimits.value === true;
|
|
3048
|
+
const K = counted ? "1" : `NOT ${LIMITED}`;
|
|
3049
|
+
const empty = { rateLimits: counted ? "counted" : "excluded", totals: { runs: 0, ok: 0, failed: 0, limited: 0, seconds: 0, tokens: 0, saved: 0 }, audits: { runs: 0, agree: 0, partial: 0, disagree: 0, unclear: 0, tokens: 0 }, byWorker: [], byDay: [] };
|
|
3513
3050
|
const db = openDb();
|
|
3514
3051
|
if (!db) return empty;
|
|
3515
3052
|
const since2 = sinceMs2 ? new Date(sinceMs2).toISOString() : "";
|
|
3516
3053
|
try {
|
|
3517
|
-
const t = db.prepare(`SELECT
|
|
3518
|
-
const w = db.prepare(`SELECT backend, model,
|
|
3519
|
-
const d = db.prepare(`SELECT substr(started_at,1,10) day,
|
|
3054
|
+
const t = db.prepare(`SELECT COALESCE(SUM(${K}),0) runs, COALESCE(SUM(state='done'),0) ok, COALESCE(SUM(${LIMITED}),0) limited, COALESCE(SUM(CASE WHEN ${K} THEN seconds END),0) seconds, COALESCE(SUM(CASE WHEN ${K} THEN tokens END),0) tokens, COALESCE(SUM(saved),0) saved FROM runs WHERE started_at >= ? AND ${NOT_AUDIT}`).get(since2);
|
|
3055
|
+
const w = db.prepare(`SELECT backend, model, SUM(${K}) runs, SUM(state='done') ok, SUM(${LIMITED}) limited, AVG(CASE WHEN ${K} THEN seconds END) avg_s, AVG(CASE WHEN ${K} THEN tokens END) avg_t, COALESCE(SUM(saved),0) saved FROM runs WHERE started_at >= ? AND ${NOT_AUDIT} GROUP BY backend, model ORDER BY runs DESC LIMIT 40`).all(since2);
|
|
3056
|
+
const d = db.prepare(`SELECT substr(started_at,1,10) day, SUM(${K}) runs, SUM(state='done') ok, SUM(${LIMITED}) limited, COALESCE(SUM(saved),0) saved FROM runs WHERE started_at >= ? AND ${NOT_AUDIT} GROUP BY day ORDER BY day DESC LIMIT 60`).all(since2);
|
|
3520
3057
|
const audits = auditStats(db, since2);
|
|
3521
3058
|
const ledger = readLedger(sinceMs2);
|
|
3522
3059
|
const byDay = /* @__PURE__ */ new Map();
|
|
3523
3060
|
for (const e of ledger) byDay.set(e.at.slice(0, 10), (byDay.get(e.at.slice(0, 10)) ?? 0) + e.saved);
|
|
3524
3061
|
return {
|
|
3525
|
-
|
|
3062
|
+
rateLimits: counted ? "counted" : "excluded",
|
|
3063
|
+
totals: { runs: t.runs, ok: t.ok, failed: t.runs - t.ok, limited: t.limited, seconds: t.seconds, tokens: t.tokens, saved: totals(ledger).saved },
|
|
3526
3064
|
audits: audits.total,
|
|
3527
3065
|
byWorker: workerRows(w, ledger, audits.byWorker),
|
|
3528
|
-
byDay: d.reverse().map((r) => ({ day: r.day, runs: r.runs, ok: r.ok ?? 0, saved: byDay.get(r.day) ?? 0 }))
|
|
3066
|
+
byDay: d.reverse().map((r) => ({ day: r.day, runs: r.runs, ok: r.ok ?? 0, limited: r.limited ?? 0, saved: byDay.get(r.day) ?? 0 }))
|
|
3529
3067
|
};
|
|
3530
3068
|
} catch {
|
|
3531
3069
|
return empty;
|
|
@@ -3533,7 +3071,79 @@ function historyStats(sinceMs2) {
|
|
|
3533
3071
|
}
|
|
3534
3072
|
|
|
3535
3073
|
// src/core/report.ts
|
|
3536
|
-
import
|
|
3074
|
+
import fs20 from "node:fs";
|
|
3075
|
+
|
|
3076
|
+
// src/core/plan.ts
|
|
3077
|
+
import fs19 from "node:fs";
|
|
3078
|
+
import path16 from "node:path";
|
|
3079
|
+
var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
3080
|
+
var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
|
|
3081
|
+
function structure(lines) {
|
|
3082
|
+
const headings = [];
|
|
3083
|
+
const rules = [];
|
|
3084
|
+
const fenced = [];
|
|
3085
|
+
let open;
|
|
3086
|
+
lines.forEach((l, i) => {
|
|
3087
|
+
const f = FENCE.exec(l);
|
|
3088
|
+
if (f) {
|
|
3089
|
+
fenced[i] = true;
|
|
3090
|
+
const mark = f[1];
|
|
3091
|
+
if (!open) open = mark;
|
|
3092
|
+
else if (mark[0] === open[0] && mark.length >= open.length && !f[2].trim()) open = void 0;
|
|
3093
|
+
return;
|
|
3094
|
+
}
|
|
3095
|
+
fenced[i] = !!open;
|
|
3096
|
+
if (open) return;
|
|
3097
|
+
const h = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/.exec(l);
|
|
3098
|
+
if (h) headings.push({ line: i, level: h[1].length, text: h[2] });
|
|
3099
|
+
else if (RULE.test(l)) rules.push(i);
|
|
3100
|
+
});
|
|
3101
|
+
return { headings, rules, fenced };
|
|
3102
|
+
}
|
|
3103
|
+
function parsePlan(text, file2 = "") {
|
|
3104
|
+
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
3105
|
+
const { headings, rules, fenced } = structure(lines);
|
|
3106
|
+
const after = (line) => Math.min(headings.find((h) => h.line > line)?.line ?? lines.length, rules.find((r) => r > line) ?? lines.length);
|
|
3107
|
+
const titleHeading = headings.find((h) => h.level === 1);
|
|
3108
|
+
const start = titleHeading ? titleHeading.line + 1 : 0;
|
|
3109
|
+
const header = lines.slice(start, after(start - 1)).filter((l) => !/^\s*>/.test(l)).join("\n").trim();
|
|
3110
|
+
const gc = headings.find((h) => /^global constraints\b/i.test(h.text));
|
|
3111
|
+
const constraints = gc ? lines.slice(gc.line + 1, after(gc.line)).join("\n").trim() : "";
|
|
3112
|
+
const tasks = [];
|
|
3113
|
+
for (const h of headings) {
|
|
3114
|
+
const m = /^Task\s+(\d+)\b\s*[:.)\-–—]?\s*(.*)$/i.exec(h.text);
|
|
3115
|
+
if (!m) continue;
|
|
3116
|
+
const end = headings.find((o) => o.line > h.line && o.level <= h.level)?.line ?? lines.length;
|
|
3117
|
+
const body = lines.slice(h.line, end).join("\n").replace(/(\n\s*(?:---|\*\*\*|___)\s*)+$/, "").trimEnd();
|
|
3118
|
+
const unfenced = lines.slice(h.line, end).filter((_, j) => !fenced[h.line + j]).join("\n");
|
|
3119
|
+
const tier = /^\s*[-*]?\s*\*\*Worker:\*\*\s*`?([\w-]+)`?/m.exec(unfenced)?.[1]?.toLowerCase();
|
|
3120
|
+
tasks.push({ step: Number(m[1]), title: m[2].trim(), text: body, ...tier ? { tier } : {} });
|
|
3121
|
+
}
|
|
3122
|
+
return { file: file2, title: titleHeading?.text ?? "", header, constraints, tasks };
|
|
3123
|
+
}
|
|
3124
|
+
function loadPlan(file2) {
|
|
3125
|
+
const abs = path16.resolve(file2);
|
|
3126
|
+
if (!fs19.existsSync(abs) || !fs19.statSync(abs).isFile()) throw new UserError(`plan not found: ${file2}`);
|
|
3127
|
+
const plan = parsePlan(fs19.readFileSync(abs, "utf8"), fs19.realpathSync(abs));
|
|
3128
|
+
if (!plan.tasks.length) throw new UserError(`${file2} has no "Task N" headings (see pitroom-writing-plans)`);
|
|
3129
|
+
return plan;
|
|
3130
|
+
}
|
|
3131
|
+
function planTask(plan, step) {
|
|
3132
|
+
const t = plan.tasks.find((x) => x.step === step);
|
|
3133
|
+
if (!t) throw new UserError(`no Task ${step} in ${plan.file}; it has ${plan.tasks.map((x) => `Task ${x.step}`).join(", ")}`);
|
|
3134
|
+
return t;
|
|
3135
|
+
}
|
|
3136
|
+
function brief(plan, task) {
|
|
3137
|
+
return `${[
|
|
3138
|
+
`# ${plan.title || planName(plan.file)}`,
|
|
3139
|
+
plan.header,
|
|
3140
|
+
"## Global Constraints",
|
|
3141
|
+
plan.constraints || "(none stated in the plan)",
|
|
3142
|
+
task.text
|
|
3143
|
+
].filter(Boolean).join("\n\n")}
|
|
3144
|
+
`;
|
|
3145
|
+
}
|
|
3146
|
+
var planName = (file2) => path16.basename(file2).replace(/\.md$/i, "");
|
|
3537
3147
|
|
|
3538
3148
|
// src/core/style.ts
|
|
3539
3149
|
var enabled = () => {
|
|
@@ -3593,7 +3203,7 @@ function duration(meta) {
|
|
|
3593
3203
|
}
|
|
3594
3204
|
function readSummary(meta) {
|
|
3595
3205
|
const f = runFile(meta.id, "summary.md");
|
|
3596
|
-
return
|
|
3206
|
+
return fs20.existsSync(f) ? fs20.readFileSync(f, "utf8").trim() : archivedRun(meta.id)?.answer ?? "";
|
|
3597
3207
|
}
|
|
3598
3208
|
function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
3599
3209
|
const out = [];
|
|
@@ -3654,44 +3264,659 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
|
3654
3264
|
meta.mode === "isolate" ? ` \u26A0 deletes ${deleted} file${deleted === 1 ? "" : "s"}: check they are wanted before applying (apply refuses without --allow-delete)` : ` \u26A0 deleted ${deleted} file${deleted === 1 ? "" : "s"} in your tree: check they are wanted (undo: pitroom revert ${meta.id})`
|
|
3655
3265
|
);
|
|
3656
3266
|
}
|
|
3657
|
-
if (meta.changes.length) {
|
|
3658
|
-
out.push(` diff: pitroom show ${meta.id} --patch`);
|
|
3659
|
-
out.push(` review: pitroom review ${meta.id}`);
|
|
3660
|
-
if (meta.mode === "isolate" && !meta.applied && !meta.discarded) {
|
|
3661
|
-
out.push(` apply: pitroom apply ${meta.id}`, ` discard: pitroom discard ${meta.id}`);
|
|
3267
|
+
if (meta.changes.length) {
|
|
3268
|
+
out.push(` diff: pitroom show ${meta.id} --patch`);
|
|
3269
|
+
out.push(` review: pitroom review ${meta.id}`);
|
|
3270
|
+
if (meta.mode === "isolate" && !meta.applied && !meta.discarded) {
|
|
3271
|
+
out.push(` apply: pitroom apply ${meta.id}`, ` discard: pitroom discard ${meta.id}`);
|
|
3272
|
+
}
|
|
3273
|
+
if (meta.mode === "write" && !meta.reverted) out.push(` undo: pitroom revert ${meta.id}`);
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
if (meta.verifyResult) {
|
|
3277
|
+
const v = meta.verifyResult;
|
|
3278
|
+
const notFound = !v.ok && v.code === 127 && /not found|no such file|is not recognized as an internal or external command/i.test(v.tail);
|
|
3279
|
+
out.push(`\u2500\u2500 verify: \`${meta.verify}\` ${v.ok ? "\u2714 passed" : notFound ? "\u2718 could not run (exit 127: command not found)" : `\u2718 failed (exit ${v.code})`}`);
|
|
3280
|
+
if (notFound) out.push(" the command is not on the PATH Pitroom runs with (an app such as an MCP client may start it without your shell's PATH): give its full path, or set PATH in the command");
|
|
3281
|
+
if (!v.ok && v.tail) out.push(...v.tail.split("\n").map((l) => ` ${l}`));
|
|
3282
|
+
}
|
|
3283
|
+
if (isActive(meta.state)) {
|
|
3284
|
+
out.push(`\u2500\u2500 still running: pitroom wait ${meta.id} (stop: pitroom stop ${meta.id})`);
|
|
3285
|
+
} else if (meta.sessionId && !meta.applied && !meta.discarded && !meta.reviewOf) {
|
|
3286
|
+
out.push(`\u2500\u2500 follow up: pitroom run --continue ${meta.id} "\u2026"`);
|
|
3287
|
+
}
|
|
3288
|
+
return out.join("\n");
|
|
3289
|
+
}
|
|
3290
|
+
function live(meta) {
|
|
3291
|
+
const f = runFile(meta.id, "events.jsonl");
|
|
3292
|
+
if (!fs20.existsSync(f)) return { steps: 0, toolCalls: 0 };
|
|
3293
|
+
const p = getBackend((meta.ran ?? meta.worker).backend).parse(fs20.readFileSync(f, "utf8"));
|
|
3294
|
+
return { steps: p.usage.steps, toolCalls: p.usage.toolCalls, last: p.lastActivity };
|
|
3295
|
+
}
|
|
3296
|
+
function progress(meta) {
|
|
3297
|
+
if (meta.state === "queued") return `pitroom \u22EF queued \xB7 ${meta.mode} \xB7 waiting for a free worker slot \xB7 run ${meta.id}`;
|
|
3298
|
+
const l = live(meta);
|
|
3299
|
+
const last = l.last ? ` \xB7 last: ${l.last}` : "";
|
|
3300
|
+
return `pitroom \u2026 running \xB7 ${meta.mode} \xB7 ${duration(meta)} \xB7 ${l.steps} steps, ${l.toolCalls} tool calls${last} \xB7 run ${meta.id}`;
|
|
3301
|
+
}
|
|
3302
|
+
|
|
3303
|
+
// src/core/group.ts
|
|
3304
|
+
function groupIds(group) {
|
|
3305
|
+
return listRunIds().filter((id) => {
|
|
3306
|
+
try {
|
|
3307
|
+
return readMeta(id).group === group;
|
|
3308
|
+
} catch {
|
|
3309
|
+
return false;
|
|
3310
|
+
}
|
|
3311
|
+
});
|
|
3312
|
+
}
|
|
3313
|
+
var oneLine5 = (s, max) => {
|
|
3314
|
+
const t = s.replace(/\s+/g, " ").trim();
|
|
3315
|
+
return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
|
|
3316
|
+
};
|
|
3317
|
+
function headline(meta, max = 160) {
|
|
3318
|
+
const text = readSummary(meta);
|
|
3319
|
+
const line = text.split("\n").find((l) => /^SUMMARY:/i.test(l.trim())) ?? text.split("\n").find((l) => l.trim()) ?? "";
|
|
3320
|
+
return oneLine5(line.replace(/^SUMMARY:\s*/i, ""), max);
|
|
3321
|
+
}
|
|
3322
|
+
function table(metas) {
|
|
3323
|
+
if (!metas.length) return "no runs";
|
|
3324
|
+
const rows = metas.map((m) => {
|
|
3325
|
+
const l = m.state === "running" ? live(m) : void 0;
|
|
3326
|
+
const steps = l ? String(l.steps) : m.usage ? String(m.usage.steps) : "-";
|
|
3327
|
+
const note = l?.last ? oneLine5(l.last, 48) : isActive(m.state) ? oneLine5(m.task, 48) : headline(m, 48) || oneLine5(m.error ?? m.task, 48);
|
|
3328
|
+
return [m.id, m.state, m.mode, duration(m), steps, describeTarget(m.ran ?? m.worker), note];
|
|
3329
|
+
});
|
|
3330
|
+
const head = ["RUN", "STATE", "MODE", "TIME", "STEPS", "WORKER", "NOW / RESULT"];
|
|
3331
|
+
const widths = head.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
3332
|
+
const fmt = (r, header = false) => r.map((c, i) => {
|
|
3333
|
+
const cell2 = i === r.length - 1 ? c : c.padEnd(widths[i]);
|
|
3334
|
+
return header ? dim(cell2) : i === 1 ? stateColour(r[1], cell2) : cell2;
|
|
3335
|
+
}).join(" ");
|
|
3336
|
+
return [fmt(head, true), ...rows.map((r) => fmt(r))].join("\n");
|
|
3337
|
+
}
|
|
3338
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3339
|
+
async function waitMany(ids, opts) {
|
|
3340
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
3341
|
+
for (; ; ) {
|
|
3342
|
+
const metas = ids.map((id) => freshMeta(id));
|
|
3343
|
+
const done = metas.filter((m) => TERMINAL.includes(m.state)).length;
|
|
3344
|
+
if (opts.any ? done > 0 : done === metas.length) return { metas, timedOut: false };
|
|
3345
|
+
if (Date.now() >= deadline) return { metas, timedOut: true };
|
|
3346
|
+
await sleep(1500);
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
async function watch(select, opts) {
|
|
3350
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3351
|
+
const line = (s) => opts.write(`${s}
|
|
3352
|
+
`);
|
|
3353
|
+
const emit = (e, m) => {
|
|
3354
|
+
if (!opts.brief) return opts.write(`${JSON.stringify(e)}
|
|
3355
|
+
`);
|
|
3356
|
+
if (e.event === "started" && m) line(startedCard(m));
|
|
3357
|
+
else if (e.event === "fallback" && m) line(`\u{1F3C1} Pitroom \u21BB ${m.id} fell back from ${String(e.failed)}: ${String(e.reason)}`);
|
|
3358
|
+
else if (m && TERMINAL.includes(m.state)) line(endedCard(m));
|
|
3359
|
+
else if (e.event === "all-done") line(`\u{1F3C1} Pitroom: ${String(e.runs)} run${e.runs === 1 ? "" : "s"} finished (${String(e.ok)} ok${e.failed ? `, ${String(e.failed)} not ok` : ""})`);
|
|
3360
|
+
};
|
|
3361
|
+
const deadline = opts.timeoutMs > 0 ? Date.now() + opts.timeoutMs : Infinity;
|
|
3362
|
+
for (; ; ) {
|
|
3363
|
+
const metas = select().map((id) => freshMeta(id));
|
|
3364
|
+
if (opts.json || opts.brief) {
|
|
3365
|
+
for (const m of metas) {
|
|
3366
|
+
const prev = seen.get(m.id);
|
|
3367
|
+
const l = m.state === "running" ? live(m) : { steps: m.usage?.steps ?? 0, toolCalls: m.usage?.toolCalls ?? 0 };
|
|
3368
|
+
const attempts = m.attempts?.length ?? 0;
|
|
3369
|
+
const base2 = { run: m.id, mode: m.mode, task: oneLine5(m.task, 60) };
|
|
3370
|
+
if (!prev) emit({ event: m.state === "queued" ? "queued" : "started", ...base2 }, m);
|
|
3371
|
+
else if (prev.state === "queued" && m.state === "running") emit({ event: "started", ...base2 }, m);
|
|
3372
|
+
if (attempts > (prev?.attempts ?? 0)) {
|
|
3373
|
+
const a = m.attempts[attempts - 1];
|
|
3374
|
+
emit({ event: "fallback", run: m.id, failed: a.target, reason: oneLine5(a.error, 120) }, m);
|
|
3375
|
+
}
|
|
3376
|
+
if (m.state === "running" && l.steps > (prev?.steps ?? 0)) {
|
|
3377
|
+
emit({ event: "progress", run: m.id, steps: l.steps, tools: l.toolCalls, last: "last" in l && l.last ? oneLine5(String(l.last), 80) : void 0 });
|
|
3378
|
+
}
|
|
3379
|
+
if (TERMINAL.includes(m.state) && prev?.state !== m.state) {
|
|
3380
|
+
emit({
|
|
3381
|
+
event: m.state,
|
|
3382
|
+
run: m.id,
|
|
3383
|
+
time: duration(m),
|
|
3384
|
+
worker: describeTarget(m.ran ?? m.worker),
|
|
3385
|
+
summary: headline(m) || void 0,
|
|
3386
|
+
error: m.error ? oneLine5(m.error, 160) : void 0,
|
|
3387
|
+
refs: m.refs ? `${m.refs.valid}/${m.refs.total}` : void 0,
|
|
3388
|
+
changes: m.changes?.length || void 0
|
|
3389
|
+
}, m);
|
|
3390
|
+
}
|
|
3391
|
+
seen.set(m.id, { state: m.state, steps: l.steps, attempts });
|
|
3392
|
+
}
|
|
3393
|
+
} else {
|
|
3394
|
+
opts.write(`\x1B[H\x1B[2J${table(metas)}
|
|
3395
|
+
|
|
3396
|
+
${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \xB7 Ctrl-C to stop watching (workers keep running)
|
|
3397
|
+
`);
|
|
3398
|
+
}
|
|
3399
|
+
const allDone = metas.length > 0 && metas.every((m) => TERMINAL.includes(m.state));
|
|
3400
|
+
if (allDone || metas.length === 0 && seen.size === 0) {
|
|
3401
|
+
if (opts.json || opts.brief) {
|
|
3402
|
+
const ok = metas.filter((m) => m.state === "done").length;
|
|
3403
|
+
emit({ event: "all-done", runs: metas.length, ok, failed: metas.length - ok });
|
|
3404
|
+
}
|
|
3405
|
+
return { metas, timedOut: false };
|
|
3406
|
+
}
|
|
3407
|
+
if (Date.now() >= deadline) return { metas, timedOut: true };
|
|
3408
|
+
await sleep(opts.intervalMs);
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3412
|
+
// src/core/notify.ts
|
|
3413
|
+
var MARK = { done: "\u2714", failed: "\u2718", timeout: "\u23F1", stopped: "\u25A0" };
|
|
3414
|
+
function noticeFor(meta) {
|
|
3415
|
+
const group = meta.group ? safeGroup(meta.group) : [];
|
|
3416
|
+
if (group.length > 1) {
|
|
3417
|
+
const count = (state) => group.filter((m) => m.state === state).length;
|
|
3418
|
+
const worse = group.length - count("done");
|
|
3419
|
+
const parts = [`${count("done")} done`, ...["failed", "timeout", "stopped"].filter(count).map((s) => `${count(s)} ${s}`)];
|
|
3420
|
+
const changed2 = group.reduce((n, m) => n + (m.changes?.length ?? 0), 0);
|
|
3421
|
+
return {
|
|
3422
|
+
title: `Pitroom ${worse ? "\u26A0" : "\u2714"} group ${meta.group}`,
|
|
3423
|
+
body: `${group.length} runs ended: ${parts.join(", ")}${changed2 ? ` \xB7 ${changed2} file${changed2 === 1 ? "" : "s"} changed` : ""}`
|
|
3424
|
+
};
|
|
3425
|
+
}
|
|
3426
|
+
const changed = meta.changes?.length ? ` \xB7 ${meta.changes.length} file${meta.changes.length === 1 ? "" : "s"} changed` : "";
|
|
3427
|
+
return { title: `Pitroom ${MARK[meta.state] ?? "\u2022"} ${kind(meta)} ${meta.state}`, body: `${what(meta)} \xB7 ${elapsed(meta)}${changed}` };
|
|
3428
|
+
}
|
|
3429
|
+
function safeGroup(name) {
|
|
3430
|
+
try {
|
|
3431
|
+
return groupIds(name).map((id) => readMeta(id));
|
|
3432
|
+
} catch {
|
|
3433
|
+
return [];
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
function shouldNotify(meta) {
|
|
3437
|
+
const eff = effective();
|
|
3438
|
+
if (!eff.notify.value || !meta.background || meta.auditOf || !TERMINAL.includes(meta.state)) return false;
|
|
3439
|
+
const runs = meta.group ? safeGroup(meta.group) : [meta];
|
|
3440
|
+
if (!runs.length || !runs.every((m) => TERMINAL.includes(m.state))) return false;
|
|
3441
|
+
if (runs.every((m) => m.state === "done" || m.state === "stopped")) {
|
|
3442
|
+
const start = Math.min(...runs.map((m) => Date.parse(m.startedAt)));
|
|
3443
|
+
const end = Math.max(...runs.map((m) => Date.parse(m.endedAt ?? "")));
|
|
3444
|
+
if (Number.isFinite(end - start) && (end - start) / 1e3 < eff.notifyAfter.value) return false;
|
|
3445
|
+
if (runs.every((m) => m.state === "stopped")) return false;
|
|
3446
|
+
}
|
|
3447
|
+
return true;
|
|
3448
|
+
}
|
|
3449
|
+
function claimGroup(group) {
|
|
3450
|
+
try {
|
|
3451
|
+
const ids = groupIds(group).sort();
|
|
3452
|
+
if (ids.length < 2) return true;
|
|
3453
|
+
const dir = path17.join(home(), "notified");
|
|
3454
|
+
fs21.mkdirSync(dir, { recursive: true });
|
|
3455
|
+
for (const f of fs21.readdirSync(dir)) {
|
|
3456
|
+
if (Date.now() - fs21.statSync(path17.join(dir, f)).mtimeMs > 30 * 864e5) fs21.rmSync(path17.join(dir, f), { force: true });
|
|
3457
|
+
}
|
|
3458
|
+
fs21.writeFileSync(path17.join(dir, crypto5.createHash("sha1").update(ids.join(",")).digest("hex").slice(0, 16)), "", { flag: "wx" });
|
|
3459
|
+
return true;
|
|
3460
|
+
} catch {
|
|
3461
|
+
return false;
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
function send(notice, meta) {
|
|
3465
|
+
const command = effective().notifyCommand.value;
|
|
3466
|
+
const env = { ...process.env, PITROOM_NOTIFY_TITLE: notice.title, PITROOM_NOTIFY_BODY: notice.body, PITROOM_NOTIFY_RUN: meta.id, PITROOM_NOTIFY_STATE: meta.state };
|
|
3467
|
+
try {
|
|
3468
|
+
let child;
|
|
3469
|
+
if (command) {
|
|
3470
|
+
child = spawn2(command, { shell: true, env, detached: true, stdio: "ignore" });
|
|
3471
|
+
} else if (process.platform === "darwin") {
|
|
3472
|
+
child = spawn2("osascript", ["-e", "on run argv\ndisplay notification (item 2 of argv) with title (item 1 of argv)\nend run", notice.title, notice.body], { env, detached: true, stdio: "ignore" });
|
|
3473
|
+
} else if (process.platform === "linux") {
|
|
3474
|
+
const plain2 = (t) => t.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3475
|
+
child = spawn2("notify-send", ["--", plain2(notice.title), plain2(notice.body)], { env, detached: true, stdio: "ignore" });
|
|
3476
|
+
} else {
|
|
3477
|
+
return;
|
|
3478
|
+
}
|
|
3479
|
+
child.on("error", () => void 0);
|
|
3480
|
+
child.unref();
|
|
3481
|
+
} catch {
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
function notifyEnded(meta) {
|
|
3485
|
+
try {
|
|
3486
|
+
if (shouldNotify(meta) && (!meta.group || claimGroup(meta.group))) send(noticeFor(meta), meta);
|
|
3487
|
+
} catch {
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
// src/core/templates.ts
|
|
3492
|
+
import fs23 from "node:fs";
|
|
3493
|
+
import path19 from "node:path";
|
|
3494
|
+
|
|
3495
|
+
// src/core/install.ts
|
|
3496
|
+
import fs22 from "node:fs";
|
|
3497
|
+
import os9 from "node:os";
|
|
3498
|
+
import path18 from "node:path";
|
|
3499
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3500
|
+
var LEGACY = ["pitroom", "opencode-worker"];
|
|
3501
|
+
function packageRoot() {
|
|
3502
|
+
return path18.resolve(path18.dirname(fileURLToPath2(import.meta.url)), "..");
|
|
3503
|
+
}
|
|
3504
|
+
function skillNames(root = packageRoot()) {
|
|
3505
|
+
const dir = path18.join(root, "skills");
|
|
3506
|
+
if (!fs22.existsSync(dir)) return [];
|
|
3507
|
+
return fs22.readdirSync(dir).filter((d) => fs22.existsSync(path18.join(dir, d, "SKILL.md"))).sort();
|
|
3508
|
+
}
|
|
3509
|
+
function skillTargets() {
|
|
3510
|
+
const home3 = os9.homedir();
|
|
3511
|
+
const targets = [path18.join(home3, ".agents", "skills")];
|
|
3512
|
+
if (fs22.existsSync(path18.join(home3, ".claude"))) targets.push(path18.join(home3, ".claude", "skills"));
|
|
3513
|
+
return targets;
|
|
3514
|
+
}
|
|
3515
|
+
var launcherPath = () => path18.join(os9.homedir(), ".local", "bin", process.platform === "win32" ? "pitroom.cmd" : "pitroom");
|
|
3516
|
+
var LAUNCHER_MARK = "# pitroom launcher";
|
|
3517
|
+
function launcherScript(bundle) {
|
|
3518
|
+
if (process.platform === "win32") {
|
|
3519
|
+
const q2 = (v) => v.replace(/%/g, "%%");
|
|
3520
|
+
return `@echo off\r
|
|
3521
|
+
rem ${LAUNCHER_MARK} (created by \`pitroom install\`; \`pitroom uninstall\` removes it)\r
|
|
3522
|
+
if defined PITROOM_NODE (\r
|
|
3523
|
+
"%PITROOM_NODE%" "${q2(bundle)}" %*\r
|
|
3524
|
+
) else (\r
|
|
3525
|
+
"${q2(process.execPath)}" "${q2(bundle)}" %*\r
|
|
3526
|
+
)\r
|
|
3527
|
+
exit /b %ERRORLEVEL%\r
|
|
3528
|
+
`;
|
|
3529
|
+
}
|
|
3530
|
+
const q = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
3531
|
+
return `#!/bin/sh
|
|
3532
|
+
${LAUNCHER_MARK} (created by \`pitroom install\`; \`pitroom uninstall\` removes it)
|
|
3533
|
+
cli=${q(bundle)}
|
|
3534
|
+
ok() { [ -n "$1" ] && [ -x "$1" ] && "$1" -e 'const [a,b]=process.versions.node.split(".").map(Number);process.exit(a>22||(a===22&&b>=13)?0:1)' 2>/dev/null; }
|
|
3535
|
+
for n in "\${PITROOM_NODE:-}" ${q(process.execPath)} "$(command -v node 2>/dev/null)" "$HOME"/.nvm/versions/node/*/bin/node /opt/homebrew/bin/node /usr/local/bin/node; do
|
|
3536
|
+
if ok "$n"; then exec "$n" "$cli" "$@"; fi
|
|
3537
|
+
done
|
|
3538
|
+
echo "pitroom: needs Node.js 22.13 or newer (set PITROOM_NODE to its path)" >&2
|
|
3539
|
+
exit 127
|
|
3540
|
+
`;
|
|
3541
|
+
}
|
|
3542
|
+
function isOurLauncher(file2, root) {
|
|
3543
|
+
if (linksInto(file2, root)) return true;
|
|
3544
|
+
try {
|
|
3545
|
+
const s = fs22.readFileSync(file2, "utf8");
|
|
3546
|
+
return s.includes(LAUNCHER_MARK) && s.includes(root);
|
|
3547
|
+
} catch {
|
|
3548
|
+
return false;
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
function isPitroomLauncher(file2) {
|
|
3552
|
+
try {
|
|
3553
|
+
return fs22.readFileSync(file2, "utf8").includes(LAUNCHER_MARK);
|
|
3554
|
+
} catch {
|
|
3555
|
+
return false;
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
function placeLauncher(bundle, root, force) {
|
|
3559
|
+
const dest = launcherPath();
|
|
3560
|
+
fs22.mkdirSync(path18.dirname(dest), { recursive: true });
|
|
3561
|
+
const exists = fs22.lstatSync(dest, { throwIfNoEntry: false });
|
|
3562
|
+
if (exists && !isOurLauncher(dest, root)) {
|
|
3563
|
+
if (!force) return `! ${dest} exists and is not Pitroom's launcher; kept (use --force to back it up and replace)`;
|
|
3564
|
+
fs22.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
3565
|
+
} else if (exists) {
|
|
3566
|
+
fs22.rmSync(dest, { force: true });
|
|
3567
|
+
}
|
|
3568
|
+
fs22.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
|
|
3569
|
+
return `\u2714 ${dest} \u2192 launcher for ${bundle} (Node 22.13+)`;
|
|
3570
|
+
}
|
|
3571
|
+
function linksInto(link, dir) {
|
|
3572
|
+
const st = fs22.lstatSync(link, { throwIfNoEntry: false });
|
|
3573
|
+
if (!st?.isSymbolicLink()) return false;
|
|
3574
|
+
const target = path18.resolve(path18.dirname(link), fs22.readlinkSync(link));
|
|
3575
|
+
const rel = path18.relative(dir, target);
|
|
3576
|
+
return !rel.startsWith("..") && !path18.isAbsolute(rel);
|
|
3577
|
+
}
|
|
3578
|
+
function place(src, dest, opts) {
|
|
3579
|
+
fs22.mkdirSync(path18.dirname(dest), { recursive: true });
|
|
3580
|
+
const st = fs22.lstatSync(dest, { throwIfNoEntry: false });
|
|
3581
|
+
if (st && !st.isSymbolicLink()) {
|
|
3582
|
+
if (!opts.force) return `! ${dest} exists and is not a link; kept (use --force to back it up and replace)`;
|
|
3583
|
+
fs22.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
3584
|
+
} else if (st) {
|
|
3585
|
+
fs22.unlinkSync(dest);
|
|
3586
|
+
}
|
|
3587
|
+
if (opts.copy) fs22.cpSync(src, dest, { recursive: true });
|
|
3588
|
+
else fs22.symlinkSync(src, dest, process.platform === "win32" ? "junction" : fs22.statSync(src).isDirectory() ? "dir" : "file");
|
|
3589
|
+
return `\u2714 ${dest} \u2192 ${opts.copy ? "copied" : src}`;
|
|
3590
|
+
}
|
|
3591
|
+
function install(opts) {
|
|
3592
|
+
const root = packageRoot();
|
|
3593
|
+
const skills2 = opts.skills === false ? [] : skillNames(root);
|
|
3594
|
+
if (opts.skills !== false && !skills2.length) throw new Error(`no skills found under ${path18.join(root, "skills")}`);
|
|
3595
|
+
const out = [];
|
|
3596
|
+
for (const base2 of opts.skills === false ? [] : skillTargets()) {
|
|
3597
|
+
for (const legacy of LEGACY) {
|
|
3598
|
+
const l = path18.join(base2, legacy);
|
|
3599
|
+
if (!skills2.includes(legacy) && linksInto(l, root)) {
|
|
3600
|
+
fs22.unlinkSync(l);
|
|
3601
|
+
out.push(`\u2714 removed old link ${l}`);
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
for (const name of skills2) out.push(place(path18.join(root, "skills", name), path18.join(base2, name), opts));
|
|
3605
|
+
}
|
|
3606
|
+
const bundle = path18.join(root, "dist", "pitroom.mjs");
|
|
3607
|
+
if (fs22.existsSync(bundle)) {
|
|
3608
|
+
out.push(placeLauncher(bundle, root, opts.force));
|
|
3609
|
+
const onPath = (process.env.PATH ?? "").split(path18.delimiter).some((d) => path18.resolve(d) === path18.dirname(launcherPath()));
|
|
3610
|
+
if (!onPath) out.push(`! ${path18.dirname(launcherPath())} is not on PATH; add it, or run ${launcherPath()} directly`);
|
|
3611
|
+
}
|
|
3612
|
+
return out;
|
|
3613
|
+
}
|
|
3614
|
+
function uninstall() {
|
|
3615
|
+
const root = packageRoot();
|
|
3616
|
+
const out = [];
|
|
3617
|
+
for (const base2 of skillTargets()) {
|
|
3618
|
+
if (!fs22.existsSync(base2)) continue;
|
|
3619
|
+
for (const name of fs22.readdirSync(base2)) {
|
|
3620
|
+
const l = path18.join(base2, name);
|
|
3621
|
+
if (linksInto(l, root)) {
|
|
3622
|
+
fs22.unlinkSync(l);
|
|
3623
|
+
out.push(`\u2714 removed ${l}`);
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
if (isOurLauncher(launcherPath(), root)) {
|
|
3628
|
+
fs22.rmSync(launcherPath(), { force: true });
|
|
3629
|
+
out.push(`\u2714 removed ${launcherPath()}`);
|
|
3630
|
+
}
|
|
3631
|
+
return out.length ? out : ["nothing to remove"];
|
|
3632
|
+
}
|
|
3633
|
+
function installedSkills() {
|
|
3634
|
+
const names = skillNames();
|
|
3635
|
+
return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs22.existsSync(path18.join(base2, n, "SKILL.md"))) }));
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
// src/core/templates.ts
|
|
3639
|
+
var FILES = {
|
|
3640
|
+
implementer: "pitroom-driven-development/implementer-prompt.md",
|
|
3641
|
+
"task-reviewer": "pitroom-driven-development/task-reviewer-prompt.md",
|
|
3642
|
+
"re-review": "pitroom-driven-development/re-review-prompt.md",
|
|
3643
|
+
"code-reviewer": "pitroom-review/code-reviewer.md"
|
|
3644
|
+
};
|
|
3645
|
+
function loadTemplate(name, root = packageRoot()) {
|
|
3646
|
+
const file2 = path19.join(root, "skills", FILES[name]);
|
|
3647
|
+
if (!fs23.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
|
|
3648
|
+
return fs23.readFileSync(file2, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
|
|
3649
|
+
}
|
|
3650
|
+
function fill(template, values) {
|
|
3651
|
+
const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
|
|
3652
|
+
if (missing.length) throw new UserError(`no value for template placeholder(s): ${[...new Set(missing)].join(", ")}`, 3);
|
|
3653
|
+
return template.replace(/\{\{([A-Z_]+)\}\}/g, (_, key) => values[key]);
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
// src/core/slots.ts
|
|
3657
|
+
import crypto6 from "node:crypto";
|
|
3658
|
+
import fs24 from "node:fs";
|
|
3659
|
+
import path20 from "node:path";
|
|
3660
|
+
var slotsDir = () => path20.join(home(), "slots");
|
|
3661
|
+
var locksDir = () => path20.join(home(), "locks");
|
|
3662
|
+
var STARTUP_GRACE_MS = 3e4;
|
|
3663
|
+
function holderActive(runId, self) {
|
|
3664
|
+
if (!runId || runId === self) return false;
|
|
3665
|
+
try {
|
|
3666
|
+
const m = freshMeta(runId);
|
|
3667
|
+
if (!isActive(m.state)) return false;
|
|
3668
|
+
if (m.pid) return isAlive(m.pid);
|
|
3669
|
+
return Date.now() - Date.parse(m.startedAt) < STARTUP_GRACE_MS;
|
|
3670
|
+
} catch {
|
|
3671
|
+
return false;
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
function tryClaim(file2, runId) {
|
|
3675
|
+
fs24.mkdirSync(path20.dirname(file2), { recursive: true });
|
|
3676
|
+
try {
|
|
3677
|
+
fs24.writeFileSync(file2, runId, { flag: "wx" });
|
|
3678
|
+
return true;
|
|
3679
|
+
} catch (e) {
|
|
3680
|
+
if (e.code !== "EEXIST") throw e;
|
|
3681
|
+
}
|
|
3682
|
+
let owner = "";
|
|
3683
|
+
try {
|
|
3684
|
+
owner = fs24.readFileSync(file2, "utf8").trim();
|
|
3685
|
+
} catch {
|
|
3686
|
+
}
|
|
3687
|
+
if (owner === runId) return true;
|
|
3688
|
+
if (holderActive(owner, runId)) return false;
|
|
3689
|
+
fs24.rmSync(file2, { force: true });
|
|
3690
|
+
try {
|
|
3691
|
+
fs24.writeFileSync(file2, runId, { flag: "wx" });
|
|
3692
|
+
return true;
|
|
3693
|
+
} catch {
|
|
3694
|
+
return false;
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
function releaseIfOwner(file2, runId) {
|
|
3698
|
+
if (!file2) return;
|
|
3699
|
+
try {
|
|
3700
|
+
if (fs24.readFileSync(file2, "utf8").trim() === runId) fs24.rmSync(file2, { force: true });
|
|
3701
|
+
} catch {
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3704
|
+
function tryAcquireSlot(runId, maxParallel) {
|
|
3705
|
+
for (let n = 0; n < Math.max(1, maxParallel); n++) {
|
|
3706
|
+
const file2 = path20.join(slotsDir(), `slot-${n}`);
|
|
3707
|
+
if (tryClaim(file2, runId)) return file2;
|
|
3708
|
+
}
|
|
3709
|
+
return void 0;
|
|
3710
|
+
}
|
|
3711
|
+
var releaseSlot = (file2, runId) => releaseIfOwner(file2, runId);
|
|
3712
|
+
var lockFile = (repoRoot2) => path20.join(locksDir(), `write-${crypto6.createHash("sha1").update(path20.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
|
|
3713
|
+
function acquireWriteLock(repoRoot2, runId) {
|
|
3714
|
+
const file2 = lockFile(repoRoot2);
|
|
3715
|
+
if (tryClaim(file2, runId)) return;
|
|
3716
|
+
let owner = "";
|
|
3717
|
+
try {
|
|
3718
|
+
owner = fs24.readFileSync(file2, "utf8").trim();
|
|
3719
|
+
} catch {
|
|
3720
|
+
}
|
|
3721
|
+
throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
|
|
3722
|
+
}
|
|
3723
|
+
var releaseWriteLock = (repoRoot2, runId) => releaseIfOwner(lockFile(repoRoot2), runId);
|
|
3724
|
+
|
|
3725
|
+
// src/core/prompt.ts
|
|
3726
|
+
var RULES = {
|
|
3727
|
+
read: ["READ-ONLY task: do not create, modify or delete any file. Read, search and analyse only."],
|
|
3728
|
+
write: [
|
|
3729
|
+
"You may edit files that the task requires. Make the smallest correct change; no drive-by refactors.",
|
|
3730
|
+
"Run the relevant tests, type checks or linters if they are cheap and available, and report the result."
|
|
3731
|
+
],
|
|
3732
|
+
isolate: [
|
|
3733
|
+
"You are in an isolated copy of the project. Edit files as the task requires; the primary agent reviews your diff before anything reaches the real tree.",
|
|
3734
|
+
"Make the smallest correct change; no drive-by refactors. Dependency folders (node_modules, virtualenvs, build output) may be missing unless linked.",
|
|
3735
|
+
"Run the relevant tests, type checks or linters if they are cheap and available, and report the result."
|
|
3736
|
+
]
|
|
3737
|
+
};
|
|
3738
|
+
function buildPrompt(mode, task, followUp) {
|
|
3739
|
+
if (followUp) return `Follow-up from the primary agent. Same rules and answer format as before.
|
|
3740
|
+
|
|
3741
|
+
${task}`;
|
|
3742
|
+
return `You are a worker agent. A primary coding agent delegated this bounded task to you. Your answer is draft work that the primary agent will verify, and it is the only thing the primary sees, so make it self-contained.
|
|
3743
|
+
|
|
3744
|
+
Rules:
|
|
3745
|
+
- ${RULES[mode].join("\n- ")}
|
|
3746
|
+
- Stay inside the task scope and the project directory.
|
|
3747
|
+
- Never commit, push, reset, checkout, restore, stash, clean, rebase or merge, and never discard or overwrite uncommitted work you did not create.
|
|
3748
|
+
- Never read or reveal secrets (.env files, keys, tokens).
|
|
3749
|
+
- Delete a file only when the task explicitly asks for it, and name every file you delete under FILES CHANGED. Whether anything else should be deleted or overwritten is the primary agent's decision: propose it under OPEN ISSUES and leave the file alone.
|
|
3750
|
+
- Do not ask questions. If something is ambiguous, choose the safest reasonable interpretation and state the assumption.
|
|
3751
|
+
- Be economical: open only what you need.
|
|
3752
|
+
|
|
3753
|
+
End with this answer format (plain text, no preamble):
|
|
3754
|
+
SUMMARY: 1-5 lines that directly answer the task.
|
|
3755
|
+
DETAILS: key findings with file:line references; only what the primary agent needs.
|
|
3756
|
+
FILES CHANGED: paths, or "none".
|
|
3757
|
+
VERIFICATION: commands you ran and their results, or "not run".
|
|
3758
|
+
OPEN ISSUES: risks, uncertainties, follow-ups, or "none".
|
|
3759
|
+
|
|
3760
|
+
Task:
|
|
3761
|
+
${task}`;
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3764
|
+
// src/core/refs.ts
|
|
3765
|
+
import fs25 from "node:fs";
|
|
3766
|
+
import path21 from "node:path";
|
|
3767
|
+
var FILE_EXTENSIONS = new Set(
|
|
3768
|
+
"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(" ")
|
|
3769
|
+
);
|
|
3770
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
3771
|
+
var MAX_INDEXED = 6e4;
|
|
3772
|
+
var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
|
|
3773
|
+
var REF = new RegExp(
|
|
3774
|
+
String.raw`(?<![\w/:.-])(\.{0,2}/?(?:[\w@.+-]+/)*(?:[\w@+-][\w@.+-]*\.[A-Za-z][A-Za-z0-9]{0,7}|${EXTENSIONLESS})):(\d+)(?:[-–](\d+))?`,
|
|
3775
|
+
"g"
|
|
3776
|
+
);
|
|
3777
|
+
var IDENT = /`([A-Za-z_$][\w$]*)(?:\([^`]*\))?`/g;
|
|
3778
|
+
var CLAUSE = /[;|—]|\.\s|:\s/g;
|
|
3779
|
+
var MAX_BYTES = 5 * 1024 * 1024;
|
|
3780
|
+
var WINDOW = 5;
|
|
3781
|
+
var DEFINITION_LOOKBACK = 80;
|
|
3782
|
+
function looksLikeSymbol(m) {
|
|
3783
|
+
const name = m[1];
|
|
3784
|
+
return m[0].includes("(") || /[A-Z_$0-9]/.test(name) || name.length >= 5;
|
|
3785
|
+
}
|
|
3786
|
+
function extractRefs(answer) {
|
|
3787
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3788
|
+
for (const line of answer.split("\n")) {
|
|
3789
|
+
const matches2 = [...line.matchAll(REF)];
|
|
3790
|
+
const seps = [...line.matchAll(CLAUSE)].map((s) => s.index ?? 0);
|
|
3791
|
+
const sepBetween = (a, b) => seps.some((s) => s >= a && s < b);
|
|
3792
|
+
const idents = [...line.matchAll(IDENT)].filter((i) => {
|
|
3793
|
+
if (!looksLikeSymbol(i)) return false;
|
|
3794
|
+
const pos = i.index ?? 0;
|
|
3795
|
+
const before = matches2.filter((m) => (m.index ?? 0) + m[0].length <= pos).at(-1);
|
|
3796
|
+
const after = matches2.find((m) => (m.index ?? 0) >= pos + i[0].length);
|
|
3797
|
+
return !(before && after && !sepBetween((before.index ?? 0) + before[0].length, after.index ?? 0));
|
|
3798
|
+
});
|
|
3799
|
+
matches2.forEach((m, k) => {
|
|
3800
|
+
const start = Number(m[2]);
|
|
3801
|
+
const end = m[3] ? Number(m[3]) : start;
|
|
3802
|
+
if (start < 1 || end < start) return;
|
|
3803
|
+
const at = m.index ?? 0;
|
|
3804
|
+
const prev = matches2[k - 1];
|
|
3805
|
+
const next = matches2[k + 1];
|
|
3806
|
+
const from = Math.max(at - 80, prev ? (prev.index ?? 0) + prev[0].length : 0, ...seps.filter((s) => s < at).map((s) => s + 1));
|
|
3807
|
+
const to = Math.min(at + m[0].length + 40, next?.index ?? Infinity, ...seps.filter((s) => s >= at + m[0].length));
|
|
3808
|
+
const near = new Set(idents.filter((i) => (i.index ?? 0) >= from && (i.index ?? 0) < to).map((i) => i[1]));
|
|
3809
|
+
const symbol = near.size === 1 ? [...near][0] : void 0;
|
|
3810
|
+
const key = `${m[1]}:${start}-${end}:${symbol ?? ""}`;
|
|
3811
|
+
if (!seen.has(key)) seen.set(key, { text: m[0], file: m[1], start, end, symbol });
|
|
3812
|
+
});
|
|
3813
|
+
}
|
|
3814
|
+
return [...seen.values()];
|
|
3815
|
+
}
|
|
3816
|
+
function verifyRefs(refs, dirs) {
|
|
3817
|
+
const roots = [...new Set(dirs.filter(Boolean).map((d) => path21.resolve(d)))];
|
|
3818
|
+
const invalid = [];
|
|
3819
|
+
const lineCache = /* @__PURE__ */ new Map();
|
|
3820
|
+
const load = (file2) => {
|
|
3821
|
+
if (!lineCache.has(file2)) {
|
|
3822
|
+
const size = fs25.statSync(file2).size;
|
|
3823
|
+
const lines = size > MAX_BYTES ? null : fs25.readFileSync(file2, "utf8").split("\n");
|
|
3824
|
+
if (lines && lines[lines.length - 1] === "") lines.pop();
|
|
3825
|
+
lineCache.set(file2, lines);
|
|
3826
|
+
}
|
|
3827
|
+
return lineCache.get(file2);
|
|
3828
|
+
};
|
|
3829
|
+
let index;
|
|
3830
|
+
const byName = () => index ??= indexFiles(roots);
|
|
3831
|
+
const candidates = (ref) => {
|
|
3832
|
+
const direct = resolve(ref.file, roots);
|
|
3833
|
+
if (direct) return [direct];
|
|
3834
|
+
if (path21.isAbsolute(ref.file)) return [];
|
|
3835
|
+
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
3836
|
+
return (byName().get(path21.basename(wanted)) ?? []).filter((f) => f.split(path21.sep).join("/").endsWith(`/${wanted}`) || path21.basename(f) === wanted).slice(0, 20);
|
|
3837
|
+
};
|
|
3838
|
+
const check = (file2, ref) => {
|
|
3839
|
+
const lines = load(file2);
|
|
3840
|
+
if (!lines) return void 0;
|
|
3841
|
+
if (ref.end > lines.length) return `file has ${lines.length} lines`;
|
|
3842
|
+
if (ref.symbol && !mentions(lines, ref) && !enclosedBy(lines, ref)) return `\`${ref.symbol}\` not near line ${ref.start}`;
|
|
3843
|
+
return void 0;
|
|
3844
|
+
};
|
|
3845
|
+
let total = 0;
|
|
3846
|
+
for (const ref of refs) {
|
|
3847
|
+
const files = candidates(ref);
|
|
3848
|
+
if (!files.length) {
|
|
3849
|
+
if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
|
|
3850
|
+
total++;
|
|
3851
|
+
invalid.push({ ref: ref.text, reason: path21.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
|
|
3852
|
+
continue;
|
|
3853
|
+
}
|
|
3854
|
+
total++;
|
|
3855
|
+
const reasons = files.map((f) => check(f, ref));
|
|
3856
|
+
if (!reasons.some((r) => r === void 0)) invalid.push({ ref: ref.text, reason: files.length > 1 ? `${reasons[0]} (${files.length} files match)` : reasons[0] });
|
|
3857
|
+
}
|
|
3858
|
+
return { total, valid: total - invalid.length, invalid };
|
|
3859
|
+
}
|
|
3860
|
+
function mentions(lines, ref) {
|
|
3861
|
+
const near = lines.slice(Math.max(0, ref.start - 1 - WINDOW), ref.end + WINDOW).join("\n");
|
|
3862
|
+
return new RegExp(`(^|[^\\w$])${escape(ref.symbol)}([^\\w$]|$)`).test(near);
|
|
3863
|
+
}
|
|
3864
|
+
function enclosedBy(lines, ref) {
|
|
3865
|
+
const name = escape(ref.symbol);
|
|
3866
|
+
const def = new RegExp(
|
|
3867
|
+
`\\b(function|def|fn|func|class|interface|struct|impl|type|const|let|var)\\s+\\*?${name}\\b|^\\s*(export\\s+)?(default\\s+)?(async\\s+)?(static\\s+)?${name}\\s*[(:=]`
|
|
3868
|
+
);
|
|
3869
|
+
return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
|
|
3870
|
+
}
|
|
3871
|
+
var isFileName = (name) => {
|
|
3872
|
+
const base2 = path21.basename(name);
|
|
3873
|
+
return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
|
|
3874
|
+
};
|
|
3875
|
+
function indexFiles(roots) {
|
|
3876
|
+
const index = /* @__PURE__ */ new Map();
|
|
3877
|
+
let count = 0;
|
|
3878
|
+
const walk = (dir) => {
|
|
3879
|
+
let entries;
|
|
3880
|
+
try {
|
|
3881
|
+
entries = fs25.readdirSync(dir, { withFileTypes: true });
|
|
3882
|
+
} catch {
|
|
3883
|
+
return;
|
|
3884
|
+
}
|
|
3885
|
+
for (const e of entries) {
|
|
3886
|
+
if (count >= MAX_INDEXED) return;
|
|
3887
|
+
if (e.isDirectory()) {
|
|
3888
|
+
if (!SKIP_DIRS2.has(e.name)) walk(path21.join(dir, e.name));
|
|
3889
|
+
} else if (e.isFile()) {
|
|
3890
|
+
count++;
|
|
3891
|
+
const list2 = index.get(e.name);
|
|
3892
|
+
if (list2) list2.push(path21.join(dir, e.name));
|
|
3893
|
+
else index.set(e.name, [path21.join(dir, e.name)]);
|
|
3662
3894
|
}
|
|
3663
|
-
if (meta.mode === "write" && !meta.reverted) out.push(` undo: pitroom revert ${meta.id}`);
|
|
3664
3895
|
}
|
|
3665
|
-
}
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
const notFound = !v.ok && v.code === 127 && /not found|no such file|is not recognized as an internal or external command/i.test(v.tail);
|
|
3669
|
-
out.push(`\u2500\u2500 verify: \`${meta.verify}\` ${v.ok ? "\u2714 passed" : notFound ? "\u2718 could not run (exit 127: command not found)" : `\u2718 failed (exit ${v.code})`}`);
|
|
3670
|
-
if (notFound) out.push(" the command is not on the PATH Pitroom runs with (an app such as an MCP client may start it without your shell's PATH): give its full path, or set PATH in the command");
|
|
3671
|
-
if (!v.ok && v.tail) out.push(...v.tail.split("\n").map((l) => ` ${l}`));
|
|
3672
|
-
}
|
|
3673
|
-
if (isActive(meta.state)) {
|
|
3674
|
-
out.push(`\u2500\u2500 still running: pitroom wait ${meta.id} (stop: pitroom stop ${meta.id})`);
|
|
3675
|
-
} else if (meta.sessionId && !meta.applied && !meta.discarded && !meta.reviewOf) {
|
|
3676
|
-
out.push(`\u2500\u2500 follow up: pitroom run --continue ${meta.id} "\u2026"`);
|
|
3677
|
-
}
|
|
3678
|
-
return out.join("\n");
|
|
3896
|
+
};
|
|
3897
|
+
for (const r of roots) walk(r);
|
|
3898
|
+
return index;
|
|
3679
3899
|
}
|
|
3680
|
-
function
|
|
3681
|
-
const
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3900
|
+
function resolve(ref, roots) {
|
|
3901
|
+
const candidates = path21.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path21.join(r, ref));
|
|
3902
|
+
return candidates.find((c) => {
|
|
3903
|
+
try {
|
|
3904
|
+
return fs25.statSync(c).isFile() && inside(c, roots);
|
|
3905
|
+
} catch {
|
|
3906
|
+
return false;
|
|
3907
|
+
}
|
|
3908
|
+
});
|
|
3685
3909
|
}
|
|
3686
|
-
function
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3910
|
+
function inside(file2, roots) {
|
|
3911
|
+
return roots.some((r) => {
|
|
3912
|
+
const rel = path21.relative(r, path21.resolve(file2));
|
|
3913
|
+
return rel !== "" && !rel.startsWith("..") && !path21.isAbsolute(rel);
|
|
3914
|
+
});
|
|
3691
3915
|
}
|
|
3916
|
+
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3692
3917
|
|
|
3693
3918
|
// src/core/run.ts
|
|
3694
|
-
var VERSION2 = true ? "0.
|
|
3919
|
+
var VERSION2 = true ? "0.20.0" : "0.0.0-dev";
|
|
3695
3920
|
function planWork(o) {
|
|
3696
3921
|
if (!o.plan) return { task: o.task.trim(), tier: o.tier };
|
|
3697
3922
|
if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
|
|
@@ -3723,7 +3948,7 @@ function prepareRun(o) {
|
|
|
3723
3948
|
parent = readMeta(o.continueFrom);
|
|
3724
3949
|
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");
|
|
3725
3950
|
if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
|
|
3726
|
-
if (parent.mode === "isolate" && (!parent.worktree || !
|
|
3951
|
+
if (parent.mode === "isolate" && (!parent.worktree || !fs26.existsSync(parent.worktree))) {
|
|
3727
3952
|
throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
|
|
3728
3953
|
}
|
|
3729
3954
|
const ran = parent.ran ?? parent.worker;
|
|
@@ -3742,18 +3967,18 @@ function prepareRun(o) {
|
|
|
3742
3967
|
throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
|
|
3743
3968
|
}
|
|
3744
3969
|
const mode = parent?.mode ?? o.mode;
|
|
3745
|
-
const dir = canonical(
|
|
3746
|
-
if (!
|
|
3970
|
+
const dir = canonical(path22.resolve(parent?.dir ?? o.dir));
|
|
3971
|
+
if (!fs26.existsSync(dir) || !fs26.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
|
|
3747
3972
|
const root = repoRoot(dir);
|
|
3748
3973
|
if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
|
|
3749
3974
|
if (mode === "write" && !root && !o.allowNonGit) {
|
|
3750
3975
|
throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
|
|
3751
3976
|
}
|
|
3752
|
-
const files = o.files.map((f) =>
|
|
3753
|
-
for (const f of files) if (!
|
|
3977
|
+
const files = o.files.map((f) => path22.resolve(f));
|
|
3978
|
+
for (const f of files) if (!fs26.existsSync(f)) throw new UserError(`file not found: ${f}`);
|
|
3754
3979
|
const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
|
|
3755
3980
|
const snap = mode === "read" && !o.review && !o.inPlace && !parent ? wantSnapshot(readIn2, root, dir) : void 0;
|
|
3756
|
-
if (parent?.snapshot?.dir && !
|
|
3981
|
+
if (parent?.snapshot?.dir && !fs26.existsSync(parent.snapshot.dir)) {
|
|
3757
3982
|
throw new UserError(`the read snapshot of run ${parent.id} is gone (cleaned up); ask the question again as a new run`, 3);
|
|
3758
3983
|
}
|
|
3759
3984
|
if (!parent && !snap && !process.env.PITROOM_NO_SECRET_WARNING) {
|
|
@@ -3800,16 +4025,16 @@ function prepareRun(o) {
|
|
|
3800
4025
|
try {
|
|
3801
4026
|
acquireWriteLock(root, meta.id);
|
|
3802
4027
|
} catch (e) {
|
|
3803
|
-
|
|
4028
|
+
fs26.rmSync(runDir(meta.id), { recursive: true, force: true });
|
|
3804
4029
|
throw e;
|
|
3805
4030
|
}
|
|
3806
4031
|
}
|
|
3807
|
-
|
|
4032
|
+
fs26.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
|
|
3808
4033
|
`);
|
|
3809
|
-
if (work.brief)
|
|
4034
|
+
if (work.brief) fs26.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
|
|
3810
4035
|
return meta;
|
|
3811
4036
|
}
|
|
3812
|
-
var
|
|
4037
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3813
4038
|
async function waitForSlot(meta) {
|
|
3814
4039
|
let stopped = false;
|
|
3815
4040
|
const stop = () => {
|
|
@@ -3823,7 +4048,7 @@ async function waitForSlot(meta) {
|
|
|
3823
4048
|
const slot = tryAcquireSlot(meta.id, max);
|
|
3824
4049
|
if (slot) return slot;
|
|
3825
4050
|
if (stopped) return void 0;
|
|
3826
|
-
await
|
|
4051
|
+
await sleep2(1e3);
|
|
3827
4052
|
if (stopped) return void 0;
|
|
3828
4053
|
}
|
|
3829
4054
|
} finally {
|
|
@@ -3834,7 +4059,9 @@ async function waitForSlot(meta) {
|
|
|
3834
4059
|
function startInBackground(meta) {
|
|
3835
4060
|
const script = process.argv[1];
|
|
3836
4061
|
if (!script) throw new Error("cannot locate the pitroom executable");
|
|
3837
|
-
|
|
4062
|
+
meta.background = true;
|
|
4063
|
+
writeMeta(meta);
|
|
4064
|
+
const child = spawn3(process.execPath, [script, "__exec", meta.id], {
|
|
3838
4065
|
detached: true,
|
|
3839
4066
|
stdio: "ignore",
|
|
3840
4067
|
cwd: meta.dir,
|
|
@@ -3886,7 +4113,7 @@ async function execute(meta) {
|
|
|
3886
4113
|
(meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
|
|
3887
4114
|
for (const f of ["events.jsonl", "stderr.log"]) {
|
|
3888
4115
|
const from = runFile(meta.id, f);
|
|
3889
|
-
if (
|
|
4116
|
+
if (fs26.existsSync(from)) fs26.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
|
|
3890
4117
|
}
|
|
3891
4118
|
writeMeta(meta);
|
|
3892
4119
|
}
|
|
@@ -3950,8 +4177,8 @@ function prepareTree(meta) {
|
|
|
3950
4177
|
const s = readSnapshot(root);
|
|
3951
4178
|
meta.snapshot.dir = s.dir;
|
|
3952
4179
|
meta.snapshot.tree = s.tree;
|
|
3953
|
-
meta.cwd =
|
|
3954
|
-
if (!
|
|
4180
|
+
meta.cwd = path22.join(s.dir, path22.relative(root, meta.dir));
|
|
4181
|
+
if (!fs26.existsSync(meta.cwd)) meta.cwd = s.dir;
|
|
3955
4182
|
if (shown) meta.warnings.push(`read in a clean snapshot of your project: the secret-looking files (${shown}) and git-ignored files are not in it; --in-place reads the directory itself`);
|
|
3956
4183
|
} catch (e) {
|
|
3957
4184
|
meta.snapshot = void 0;
|
|
@@ -3961,9 +4188,9 @@ function prepareTree(meta) {
|
|
|
3961
4188
|
if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
|
|
3962
4189
|
if (root && meta.mode === "isolate" && !meta.worktree) {
|
|
3963
4190
|
meta.baseTree = snapshotTree(root);
|
|
3964
|
-
meta.worktree =
|
|
4191
|
+
meta.worktree = path22.join(worktreesDir(), meta.id);
|
|
3965
4192
|
createIsolatedCopy(root, meta.baseTree, meta.worktree);
|
|
3966
|
-
meta.cwd =
|
|
4193
|
+
meta.cwd = path22.join(meta.worktree, path22.relative(root, meta.dir));
|
|
3967
4194
|
const linked = linkIntoWorktree(root, meta.worktree, meta.link);
|
|
3968
4195
|
if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
|
|
3969
4196
|
}
|
|
@@ -3986,7 +4213,7 @@ function attempt(meta, backend, target) {
|
|
|
3986
4213
|
timeoutSec: meta.timeoutSec
|
|
3987
4214
|
});
|
|
3988
4215
|
}
|
|
3989
|
-
var read2 = (f) =>
|
|
4216
|
+
var read2 = (f) => fs26.existsSync(f) ? fs26.readFileSync(f, "utf8") : "";
|
|
3990
4217
|
function retryableFailure(meta, backend, res) {
|
|
3991
4218
|
const run2 = backend.parse(read2(runFile(meta.id, "events.jsonl")));
|
|
3992
4219
|
if (run2.usage.steps > 0) return void 0;
|
|
@@ -4006,16 +4233,19 @@ function finalize(meta, res) {
|
|
|
4006
4233
|
if (meta.snapshot?.dir && meta.repoRoot) run2.finalText = backToProject(run2.finalText, meta.snapshot.dir, meta.repoRoot);
|
|
4007
4234
|
meta.sessionId = run2.sessionId ?? meta.sessionId;
|
|
4008
4235
|
meta.usage = run2.usage;
|
|
4009
|
-
|
|
4236
|
+
fs26.writeFileSync(runFile(meta.id, "summary.md"), `${run2.finalText}
|
|
4010
4237
|
`);
|
|
4011
4238
|
if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run2.finalText);
|
|
4012
4239
|
if (meta.reviewOf) {
|
|
4013
4240
|
meta.verdict = parseVerdict(run2.finalText);
|
|
4014
|
-
if (meta.packageFile)
|
|
4241
|
+
if (meta.packageFile) fs26.rmSync(meta.packageFile, { force: true });
|
|
4015
4242
|
}
|
|
4016
4243
|
if (!res.timedOut && !res.stopped && !meta.error) {
|
|
4017
4244
|
const f = backend.failure(run2, read2(runFile(meta.id, "stderr.log")), res.code);
|
|
4018
|
-
if (f)
|
|
4245
|
+
if (f) {
|
|
4246
|
+
meta.error = HINTS[f.kind] ? `${f.message} (${HINTS[f.kind]})` : f.message;
|
|
4247
|
+
meta.failureKind = f.kind;
|
|
4248
|
+
}
|
|
4019
4249
|
}
|
|
4020
4250
|
try {
|
|
4021
4251
|
captureChanges(meta);
|
|
@@ -4049,6 +4279,7 @@ function finalize(meta, res) {
|
|
|
4049
4279
|
if (meta.mode === "write" && meta.repoRoot) releaseWriteLock(meta.repoRoot, meta.id);
|
|
4050
4280
|
record(meta);
|
|
4051
4281
|
if (meta.auditOf) settleAudit(meta);
|
|
4282
|
+
notifyEnded(meta);
|
|
4052
4283
|
return meta;
|
|
4053
4284
|
}
|
|
4054
4285
|
function captureChanges(meta) {
|
|
@@ -4059,13 +4290,13 @@ function captureChanges(meta) {
|
|
|
4059
4290
|
const d = diffTrees(target, meta.baseTree, meta.afterTree);
|
|
4060
4291
|
meta.changes = d.changes;
|
|
4061
4292
|
meta.stats = d.stats;
|
|
4062
|
-
|
|
4293
|
+
fs26.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
|
|
4063
4294
|
}
|
|
4064
4295
|
function verifyPath() {
|
|
4065
|
-
const extra = [
|
|
4066
|
-
const parts = (process.env.PATH ?? "").split(
|
|
4067
|
-
for (const d of process.platform === "win32" ? [
|
|
4068
|
-
return parts.join(
|
|
4296
|
+
const extra = [path22.dirname(process.execPath), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
|
|
4297
|
+
const parts = (process.env.PATH ?? "").split(path22.delimiter).filter(Boolean);
|
|
4298
|
+
for (const d of process.platform === "win32" ? [path22.dirname(process.execPath)] : extra) if (!parts.includes(d)) parts.push(d);
|
|
4299
|
+
return parts.join(path22.delimiter);
|
|
4069
4300
|
}
|
|
4070
4301
|
function runVerify(meta) {
|
|
4071
4302
|
const r = spawnSync6(meta.verify, {
|
|
@@ -4078,7 +4309,7 @@ function runVerify(meta) {
|
|
|
4078
4309
|
env: { ...process.env, PATH: verifyPath(), PWD: meta.cwd, PITROOM_ACTIVE: "1" }
|
|
4079
4310
|
});
|
|
4080
4311
|
const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
|
4081
|
-
|
|
4312
|
+
fs26.writeFileSync(runFile(meta.id, "verify.log"), output);
|
|
4082
4313
|
const notFound = process.platform === "win32" && r.status === 1 && /is not recognized as an internal or external command/.test(output);
|
|
4083
4314
|
return { ok: r.status === 0, code: notFound ? 127 : r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
|
|
4084
4315
|
}
|
|
@@ -4133,126 +4364,15 @@ function discardRun(meta) {
|
|
|
4133
4364
|
return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
|
|
4134
4365
|
}
|
|
4135
4366
|
function cleanupWorktree(meta) {
|
|
4136
|
-
if (meta.worktree &&
|
|
4367
|
+
if (meta.worktree && fs26.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
|
|
4137
4368
|
}
|
|
4138
4369
|
|
|
4139
4370
|
// src/cli/mcp-extras.ts
|
|
4140
|
-
import
|
|
4141
|
-
import
|
|
4142
|
-
|
|
4143
|
-
// src/cli/mcp-support.ts
|
|
4144
|
-
import { spawn as spawn3 } from "node:child_process";
|
|
4145
|
-
|
|
4146
|
-
// src/core/group.ts
|
|
4147
|
-
function groupIds(group) {
|
|
4148
|
-
return listRunIds().filter((id) => {
|
|
4149
|
-
try {
|
|
4150
|
-
return readMeta(id).group === group;
|
|
4151
|
-
} catch {
|
|
4152
|
-
return false;
|
|
4153
|
-
}
|
|
4154
|
-
});
|
|
4155
|
-
}
|
|
4156
|
-
var oneLine5 = (s, max) => {
|
|
4157
|
-
const t = s.replace(/\s+/g, " ").trim();
|
|
4158
|
-
return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
|
|
4159
|
-
};
|
|
4160
|
-
function headline(meta, max = 160) {
|
|
4161
|
-
const text = readSummary(meta);
|
|
4162
|
-
const line = text.split("\n").find((l) => /^SUMMARY:/i.test(l.trim())) ?? text.split("\n").find((l) => l.trim()) ?? "";
|
|
4163
|
-
return oneLine5(line.replace(/^SUMMARY:\s*/i, ""), max);
|
|
4164
|
-
}
|
|
4165
|
-
function table(metas) {
|
|
4166
|
-
if (!metas.length) return "no runs";
|
|
4167
|
-
const rows = metas.map((m) => {
|
|
4168
|
-
const l = m.state === "running" ? live(m) : void 0;
|
|
4169
|
-
const steps = l ? String(l.steps) : m.usage ? String(m.usage.steps) : "-";
|
|
4170
|
-
const note = l?.last ? oneLine5(l.last, 48) : isActive(m.state) ? oneLine5(m.task, 48) : headline(m, 48) || oneLine5(m.error ?? m.task, 48);
|
|
4171
|
-
return [m.id, m.state, m.mode, duration(m), steps, describeTarget(m.ran ?? m.worker), note];
|
|
4172
|
-
});
|
|
4173
|
-
const head = ["RUN", "STATE", "MODE", "TIME", "STEPS", "WORKER", "NOW / RESULT"];
|
|
4174
|
-
const widths = head.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
4175
|
-
const fmt = (r, header = false) => r.map((c, i) => {
|
|
4176
|
-
const cell2 = i === r.length - 1 ? c : c.padEnd(widths[i]);
|
|
4177
|
-
return header ? dim(cell2) : i === 1 ? stateColour(r[1], cell2) : cell2;
|
|
4178
|
-
}).join(" ");
|
|
4179
|
-
return [fmt(head, true), ...rows.map((r) => fmt(r))].join("\n");
|
|
4180
|
-
}
|
|
4181
|
-
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4182
|
-
async function waitMany(ids, opts) {
|
|
4183
|
-
const deadline = Date.now() + opts.timeoutMs;
|
|
4184
|
-
for (; ; ) {
|
|
4185
|
-
const metas = ids.map((id) => freshMeta(id));
|
|
4186
|
-
const done = metas.filter((m) => TERMINAL.includes(m.state)).length;
|
|
4187
|
-
if (opts.any ? done > 0 : done === metas.length) return { metas, timedOut: false };
|
|
4188
|
-
if (Date.now() >= deadline) return { metas, timedOut: true };
|
|
4189
|
-
await sleep2(1500);
|
|
4190
|
-
}
|
|
4191
|
-
}
|
|
4192
|
-
async function watch(select, opts) {
|
|
4193
|
-
const seen = /* @__PURE__ */ new Map();
|
|
4194
|
-
const line = (s) => opts.write(`${s}
|
|
4195
|
-
`);
|
|
4196
|
-
const emit = (e, m) => {
|
|
4197
|
-
if (!opts.brief) return opts.write(`${JSON.stringify(e)}
|
|
4198
|
-
`);
|
|
4199
|
-
if (e.event === "started" && m) line(startedCard(m));
|
|
4200
|
-
else if (e.event === "fallback" && m) line(`\u{1F3C1} Pitroom \u21BB ${m.id} fell back from ${String(e.failed)}: ${String(e.reason)}`);
|
|
4201
|
-
else if (m && TERMINAL.includes(m.state)) line(endedCard(m));
|
|
4202
|
-
else if (e.event === "all-done") line(`\u{1F3C1} Pitroom: ${String(e.runs)} run${e.runs === 1 ? "" : "s"} finished (${String(e.ok)} ok${e.failed ? `, ${String(e.failed)} not ok` : ""})`);
|
|
4203
|
-
};
|
|
4204
|
-
const deadline = opts.timeoutMs > 0 ? Date.now() + opts.timeoutMs : Infinity;
|
|
4205
|
-
for (; ; ) {
|
|
4206
|
-
const metas = select().map((id) => freshMeta(id));
|
|
4207
|
-
if (opts.json || opts.brief) {
|
|
4208
|
-
for (const m of metas) {
|
|
4209
|
-
const prev = seen.get(m.id);
|
|
4210
|
-
const l = m.state === "running" ? live(m) : { steps: m.usage?.steps ?? 0, toolCalls: m.usage?.toolCalls ?? 0 };
|
|
4211
|
-
const attempts = m.attempts?.length ?? 0;
|
|
4212
|
-
const base2 = { run: m.id, mode: m.mode, task: oneLine5(m.task, 60) };
|
|
4213
|
-
if (!prev) emit({ event: m.state === "queued" ? "queued" : "started", ...base2 }, m);
|
|
4214
|
-
else if (prev.state === "queued" && m.state === "running") emit({ event: "started", ...base2 }, m);
|
|
4215
|
-
if (attempts > (prev?.attempts ?? 0)) {
|
|
4216
|
-
const a = m.attempts[attempts - 1];
|
|
4217
|
-
emit({ event: "fallback", run: m.id, failed: a.target, reason: oneLine5(a.error, 120) }, m);
|
|
4218
|
-
}
|
|
4219
|
-
if (m.state === "running" && l.steps > (prev?.steps ?? 0)) {
|
|
4220
|
-
emit({ event: "progress", run: m.id, steps: l.steps, tools: l.toolCalls, last: "last" in l && l.last ? oneLine5(String(l.last), 80) : void 0 });
|
|
4221
|
-
}
|
|
4222
|
-
if (TERMINAL.includes(m.state) && prev?.state !== m.state) {
|
|
4223
|
-
emit({
|
|
4224
|
-
event: m.state,
|
|
4225
|
-
run: m.id,
|
|
4226
|
-
time: duration(m),
|
|
4227
|
-
worker: describeTarget(m.ran ?? m.worker),
|
|
4228
|
-
summary: headline(m) || void 0,
|
|
4229
|
-
error: m.error ? oneLine5(m.error, 160) : void 0,
|
|
4230
|
-
refs: m.refs ? `${m.refs.valid}/${m.refs.total}` : void 0,
|
|
4231
|
-
changes: m.changes?.length || void 0
|
|
4232
|
-
}, m);
|
|
4233
|
-
}
|
|
4234
|
-
seen.set(m.id, { state: m.state, steps: l.steps, attempts });
|
|
4235
|
-
}
|
|
4236
|
-
} else {
|
|
4237
|
-
opts.write(`\x1B[H\x1B[2J${table(metas)}
|
|
4238
|
-
|
|
4239
|
-
${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \xB7 Ctrl-C to stop watching (workers keep running)
|
|
4240
|
-
`);
|
|
4241
|
-
}
|
|
4242
|
-
const allDone = metas.length > 0 && metas.every((m) => TERMINAL.includes(m.state));
|
|
4243
|
-
if (allDone || metas.length === 0 && seen.size === 0) {
|
|
4244
|
-
if (opts.json || opts.brief) {
|
|
4245
|
-
const ok = metas.filter((m) => m.state === "done").length;
|
|
4246
|
-
emit({ event: "all-done", runs: metas.length, ok, failed: metas.length - ok });
|
|
4247
|
-
}
|
|
4248
|
-
return { metas, timedOut: false };
|
|
4249
|
-
}
|
|
4250
|
-
if (Date.now() >= deadline) return { metas, timedOut: true };
|
|
4251
|
-
await sleep2(opts.intervalMs);
|
|
4252
|
-
}
|
|
4253
|
-
}
|
|
4371
|
+
import fs27 from "node:fs";
|
|
4372
|
+
import path23 from "node:path";
|
|
4254
4373
|
|
|
4255
4374
|
// src/cli/mcp-support.ts
|
|
4375
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
4256
4376
|
var MAX_OUTPUT = 12e4;
|
|
4257
4377
|
var DEFAULT_WAIT = 50;
|
|
4258
4378
|
var MAX_WAIT = 540;
|
|
@@ -4320,7 +4440,7 @@ function pit(args, timeoutMs = 60 * 6e4, signal) {
|
|
|
4320
4440
|
return new Promise((resolve2) => {
|
|
4321
4441
|
const script = process.argv[1];
|
|
4322
4442
|
if (!script) return resolve2({ code: 1, out: "", err: "cannot locate the pitroom executable" });
|
|
4323
|
-
const child =
|
|
4443
|
+
const child = spawn4(process.execPath, [script, ...args], { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
4324
4444
|
let out = "";
|
|
4325
4445
|
let err = "";
|
|
4326
4446
|
child.stdout.on("data", (d) => out += d);
|
|
@@ -4379,6 +4499,39 @@ async function stopQuietly(args) {
|
|
|
4379
4499
|
if (r.code !== 0) process.stderr.write(`pitroom mcp: ${args.join(" ")} failed: ${r.err || r.out}
|
|
4380
4500
|
`);
|
|
4381
4501
|
}
|
|
4502
|
+
var dashAsked;
|
|
4503
|
+
function ensureDash() {
|
|
4504
|
+
if (effective().mcpDash.value === false) return Promise.resolve(void 0);
|
|
4505
|
+
if (dashAsked && Date.now() - dashAsked.at < 6e4) return dashAsked.url;
|
|
4506
|
+
const url = pit(["dash", "--detach"], 3e4).then((r) => {
|
|
4507
|
+
const found = /^https?:\/\/127\.0\.0\.1:\d+\/?/m.exec(r.out)?.[0];
|
|
4508
|
+
if (!found) dashAsked = void 0;
|
|
4509
|
+
return found;
|
|
4510
|
+
});
|
|
4511
|
+
dashAsked = { at: Date.now(), url };
|
|
4512
|
+
return url;
|
|
4513
|
+
}
|
|
4514
|
+
function dashIfSlow(ids, delayMs = 3e3) {
|
|
4515
|
+
return new Promise((resolve2) => {
|
|
4516
|
+
const timer2 = setTimeout(() => {
|
|
4517
|
+
try {
|
|
4518
|
+
resolve2(ids().some((id) => isActive(freshMeta(id).state)) ? ensureDash() : void 0);
|
|
4519
|
+
} catch {
|
|
4520
|
+
resolve2(void 0);
|
|
4521
|
+
}
|
|
4522
|
+
}, delayMs);
|
|
4523
|
+
timer2.unref();
|
|
4524
|
+
});
|
|
4525
|
+
}
|
|
4526
|
+
function withLiveView(result, url) {
|
|
4527
|
+
if (!url || !result.text.includes("Not finished yet")) return result;
|
|
4528
|
+
return {
|
|
4529
|
+
...result,
|
|
4530
|
+
text: `${result.text}
|
|
4531
|
+
|
|
4532
|
+
Live view of every run: ${url} (pitroom dash). If your app has a built-in browser pane (the Claude Code and Codex apps do), open it there; otherwise give the user the address.`
|
|
4533
|
+
};
|
|
4534
|
+
}
|
|
4382
4535
|
async function startAndWait(start, seconds, ctx, task) {
|
|
4383
4536
|
const started = await pit([...start, "--bg", "--json", ...task === void 0 ? [] : ["--", task]]);
|
|
4384
4537
|
if (started.code !== 0) return asResult(started);
|
|
@@ -4389,7 +4542,9 @@ async function startAndWait(start, seconds, ctx, task) {
|
|
|
4389
4542
|
} catch {
|
|
4390
4543
|
return { text: `could not read the run id from: ${started.out.slice(0, 200)}`, isError: true };
|
|
4391
4544
|
}
|
|
4392
|
-
const
|
|
4545
|
+
const dash = cached3 ? Promise.resolve(void 0) : dashIfSlow(() => [id]);
|
|
4546
|
+
const waited = await waitFor([id], seconds, ctx);
|
|
4547
|
+
const result = withLiveView(waited, waited.text.includes("Not finished yet") ? await dash : void 0);
|
|
4393
4548
|
if (cancelled(ctx) && !cached3) await stopQuietly(["stop", id]);
|
|
4394
4549
|
if (cached3) result.text = `Cached answer: the same question on the same code as run ${id}; no worker ran (fresh: true asks one).
|
|
4395
4550
|
|
|
@@ -4405,7 +4560,9 @@ async function startCrew(flags, tasks, seconds, ctx) {
|
|
|
4405
4560
|
} catch {
|
|
4406
4561
|
return { text: `could not read the group from: ${started.out.slice(0, 200)}`, isError: true };
|
|
4407
4562
|
}
|
|
4408
|
-
const
|
|
4563
|
+
const dash = dashIfSlow(() => groupIds(group));
|
|
4564
|
+
const waited = await waitFor([], seconds, ctx, group);
|
|
4565
|
+
const result = withLiveView(waited, waited.text.includes("Not finished yet") ? await dash : void 0);
|
|
4409
4566
|
if (cancelled(ctx)) await stopQuietly(["stop", "-g", group]);
|
|
4410
4567
|
return result;
|
|
4411
4568
|
}
|
|
@@ -4497,7 +4654,7 @@ function skillPrompts() {
|
|
|
4497
4654
|
const root = packageRoot();
|
|
4498
4655
|
for (const name of skillNames(root)) {
|
|
4499
4656
|
try {
|
|
4500
|
-
const text =
|
|
4657
|
+
const text = fs27.readFileSync(path23.join(root, "skills", name, "SKILL.md"), "utf8");
|
|
4501
4658
|
const front = /^---\n([\s\S]*?)\n---\n/.exec(text);
|
|
4502
4659
|
const body = (front ? text.slice(front[0].length) : text).trim();
|
|
4503
4660
|
const description = /^description:\s*(.+)$/m.exec(front?.[1] ?? "")?.[1]?.trim() ?? `The ${name} skill.`;
|
|
@@ -4648,7 +4805,8 @@ var TOOLS2 = [
|
|
|
4648
4805
|
const runs = strs(a, "runs");
|
|
4649
4806
|
const group = str(a, "group");
|
|
4650
4807
|
if (!runs.length && !group) throw new ToolError('give "runs" or "group"');
|
|
4651
|
-
|
|
4808
|
+
const result = await waitFor(runs, waitSeconds(a), ctx, runs.length ? void 0 : group);
|
|
4809
|
+
return withLiveView(result, result.text.includes("Not finished yet") ? await ensureDash() : void 0);
|
|
4652
4810
|
}
|
|
4653
4811
|
},
|
|
4654
4812
|
{
|
|
@@ -4861,7 +5019,7 @@ var PROTOCOLS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
|
4861
5019
|
var INSTRUCTIONS = [
|
|
4862
5020
|
"Pitroom hands bounded work to cheaper worker agents and returns a verified answer, the exact diff and a cost receipt. You decide, verify and answer.",
|
|
4863
5021
|
'Use pitroom_run with mode "read" for research and locating code, mode "isolate" for code changes (the worker edits a copy; check it with pitroom_review, then pitroom_apply or pitroom_discard); pitroom_run with "tasks" for independent tasks in parallel; pitroom_info (topic history) finds an earlier answer before you ask again.',
|
|
4864
|
-
|
|
5022
|
+
`A run can take minutes: pitroom_run waits up to waitSeconds, then returns the run id as "still running"; call pitroom_wait with it. A result that is still running also carries a "Live view" address (the dashboard, started for you): open it in your app's built-in browser pane if it has one, else give it to the user.`,
|
|
4865
5023
|
"Pitroom's skills (using-pitroom, pitroom-research, pitroom-implement, pitroom-review, pitroom-crew, pitroom-debugging, pitroom-tdd and more) say when to delegate and how to check what comes back: if you have them, use the matching one alongside these tools; if not, the prompts of the same names hold them.",
|
|
4866
5024
|
"It is optional: for a small task you can do yourself, skip it."
|
|
4867
5025
|
].join(" ");
|
|
@@ -4983,10 +5141,10 @@ var parseError = () => ({ jsonrpc: "2.0", id: null, error: { code: -32700, messa
|
|
|
4983
5141
|
async function serveMcp() {
|
|
4984
5142
|
process.stdout.on("error", () => {
|
|
4985
5143
|
});
|
|
4986
|
-
const
|
|
5144
|
+
const send2 = (msg) => void process.stdout.write(`${JSON.stringify(msg)}
|
|
4987
5145
|
`);
|
|
4988
5146
|
const session = newSession();
|
|
4989
|
-
session.push =
|
|
5147
|
+
session.push = send2;
|
|
4990
5148
|
const rl = readline.createInterface({ input: process.stdin });
|
|
4991
5149
|
const pending = /* @__PURE__ */ new Set();
|
|
4992
5150
|
const handleLine = async (line) => {
|
|
@@ -4994,11 +5152,11 @@ async function serveMcp() {
|
|
|
4994
5152
|
try {
|
|
4995
5153
|
parsed = JSON.parse(line);
|
|
4996
5154
|
} catch {
|
|
4997
|
-
|
|
5155
|
+
send2(parseError());
|
|
4998
5156
|
return;
|
|
4999
5157
|
}
|
|
5000
|
-
const reply2 = await handlePayload(session, parsed,
|
|
5001
|
-
if (reply2)
|
|
5158
|
+
const reply2 = await handlePayload(session, parsed, send2);
|
|
5159
|
+
if (reply2) send2(reply2);
|
|
5002
5160
|
};
|
|
5003
5161
|
rl.on("line", (line) => {
|
|
5004
5162
|
if (!line.trim()) return;
|
|
@@ -5013,23 +5171,23 @@ async function serveMcp() {
|
|
|
5013
5171
|
}
|
|
5014
5172
|
|
|
5015
5173
|
// src/cli/mcp-http.ts
|
|
5016
|
-
import
|
|
5017
|
-
import
|
|
5174
|
+
import crypto8 from "node:crypto";
|
|
5175
|
+
import fs29 from "node:fs";
|
|
5018
5176
|
import http2 from "node:http";
|
|
5019
|
-
import
|
|
5177
|
+
import path25 from "node:path";
|
|
5020
5178
|
|
|
5021
5179
|
// src/core/dash.ts
|
|
5022
|
-
import { spawn as
|
|
5023
|
-
import
|
|
5024
|
-
import
|
|
5180
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
5181
|
+
import crypto7 from "node:crypto";
|
|
5182
|
+
import fs28 from "node:fs";
|
|
5025
5183
|
import http from "node:http";
|
|
5026
|
-
import
|
|
5184
|
+
import path24 from "node:path";
|
|
5027
5185
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
5028
5186
|
|
|
5029
5187
|
// src/core/file-diff.ts
|
|
5030
5188
|
var PREVIEW_LINES = 300;
|
|
5031
5189
|
function gitPath(value, prefix = true) {
|
|
5032
|
-
let
|
|
5190
|
+
let path33 = value;
|
|
5033
5191
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
5034
5192
|
const bytes = [];
|
|
5035
5193
|
const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
|
|
@@ -5037,9 +5195,9 @@ function gitPath(value, prefix = true) {
|
|
|
5037
5195
|
if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
|
|
5038
5196
|
else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
|
|
5039
5197
|
}
|
|
5040
|
-
|
|
5198
|
+
path33 = new TextDecoder().decode(new Uint8Array(bytes));
|
|
5041
5199
|
}
|
|
5042
|
-
return prefix ?
|
|
5200
|
+
return prefix ? path33.replace(/^[ab]\//, "") : path33;
|
|
5043
5201
|
}
|
|
5044
5202
|
function headerPath(header) {
|
|
5045
5203
|
const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
|
|
@@ -5283,18 +5441,18 @@ var HEADERS = {
|
|
|
5283
5441
|
"x-content-type-options": "nosniff",
|
|
5284
5442
|
"referrer-policy": "no-referrer",
|
|
5285
5443
|
// Own files only, plus the hash of the one inline theme script; styles may be inline (the components position popups with them).
|
|
5286
|
-
"content-security-policy": `default-src 'none'; script-src 'self' 'sha256-${
|
|
5444
|
+
"content-security-policy": `default-src 'none'; script-src 'self' 'sha256-${crypto7.createHash("sha256").update(THEME_SCRIPT).digest("base64")}'; style-src 'self' 'unsafe-inline'; img-src data:; connect-src 'self'; base-uri 'none'; form-action 'none'`
|
|
5287
5445
|
};
|
|
5288
5446
|
var ASSETS = { "/assets/app.js": "text/javascript; charset=utf-8", "/assets/app.css": "text/css; charset=utf-8" };
|
|
5289
5447
|
var assetsDir = () => fileURLToPath3(new URL("./ui/", import.meta.url));
|
|
5290
5448
|
var assetCache = /* @__PURE__ */ new Map();
|
|
5291
5449
|
function readAsset(route) {
|
|
5292
5450
|
try {
|
|
5293
|
-
const file2 =
|
|
5294
|
-
const { mtimeMs } =
|
|
5451
|
+
const file2 = path24.join(assetsDir(), path24.basename(route));
|
|
5452
|
+
const { mtimeMs } = fs28.statSync(file2);
|
|
5295
5453
|
const hit = assetCache.get(route);
|
|
5296
5454
|
if (hit && hit.mtimeMs === mtimeMs) return hit.data;
|
|
5297
|
-
const data =
|
|
5455
|
+
const data = fs28.readFileSync(file2);
|
|
5298
5456
|
assetCache.set(route, { mtimeMs, data });
|
|
5299
5457
|
return data;
|
|
5300
5458
|
} catch {
|
|
@@ -5302,16 +5460,16 @@ function readAsset(route) {
|
|
|
5302
5460
|
}
|
|
5303
5461
|
}
|
|
5304
5462
|
function handler(touch2, token) {
|
|
5305
|
-
const
|
|
5463
|
+
const send2 = (res, code, type, body) => {
|
|
5306
5464
|
res.writeHead(code, { ...HEADERS, "content-type": type });
|
|
5307
5465
|
res.end(body);
|
|
5308
5466
|
};
|
|
5309
|
-
const json = (res, code, value) =>
|
|
5467
|
+
const json = (res, code, value) => send2(res, code, "application/json; charset=utf-8", JSON.stringify(value));
|
|
5310
5468
|
const tokenBuf = Buffer.from(token);
|
|
5311
5469
|
const act = (req, res, pathname) => {
|
|
5312
5470
|
req.resume();
|
|
5313
5471
|
const given = Buffer.from(String(req.headers["x-pitroom-token"] ?? ""));
|
|
5314
|
-
if (given.length !== tokenBuf.length || !
|
|
5472
|
+
if (given.length !== tokenBuf.length || !crypto7.timingSafeEqual(given, tokenBuf)) return json(res, 403, { error: "forbidden" });
|
|
5315
5473
|
if (req.headers.origin !== `http://${req.headers.host}`) return json(res, 403, { error: "forbidden origin" });
|
|
5316
5474
|
const m = /^\/api\/run\/([^/]+)\/(stop|discard)$/.exec(pathname);
|
|
5317
5475
|
if (!m) return json(res, 404, { error: "not found" });
|
|
@@ -5338,7 +5496,7 @@ function handler(touch2, token) {
|
|
|
5338
5496
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
5339
5497
|
if (req.method === "POST") return act(req, res, url.pathname);
|
|
5340
5498
|
if (req.method !== "GET" && req.method !== "HEAD") return json(res, 405, { error: "read-only" });
|
|
5341
|
-
if (url.pathname === "/") return
|
|
5499
|
+
if (url.pathname === "/") return send2(res, 200, "text/html; charset=utf-8", readAsset("/assets/app.js") ? page(token) : MISSING);
|
|
5342
5500
|
const type = ASSETS[url.pathname];
|
|
5343
5501
|
if (type) {
|
|
5344
5502
|
const data = readAsset(url.pathname);
|
|
@@ -5379,7 +5537,7 @@ function handler(touch2, token) {
|
|
|
5379
5537
|
}
|
|
5380
5538
|
function startDash(opts) {
|
|
5381
5539
|
let lastRequest = Date.now();
|
|
5382
|
-
const server = http.createServer(handler(() => lastRequest = Date.now(),
|
|
5540
|
+
const server = http.createServer(handler(() => lastRequest = Date.now(), crypto7.randomBytes(24).toString("hex")));
|
|
5383
5541
|
return new Promise((resolve2, reject) => {
|
|
5384
5542
|
server.once("error", reject);
|
|
5385
5543
|
server.listen(opts.port, "127.0.0.1", () => {
|
|
@@ -5400,7 +5558,7 @@ function startDash(opts) {
|
|
|
5400
5558
|
});
|
|
5401
5559
|
});
|
|
5402
5560
|
}
|
|
5403
|
-
var registryFile = () =>
|
|
5561
|
+
var registryFile = () => path24.join(home(), "dash.json");
|
|
5404
5562
|
async function ping(port) {
|
|
5405
5563
|
try {
|
|
5406
5564
|
const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
|
|
@@ -5412,12 +5570,12 @@ async function ping(port) {
|
|
|
5412
5570
|
async function runningDash() {
|
|
5413
5571
|
let r;
|
|
5414
5572
|
try {
|
|
5415
|
-
r = JSON.parse(
|
|
5573
|
+
r = JSON.parse(fs28.readFileSync(registryFile(), "utf8"));
|
|
5416
5574
|
} catch {
|
|
5417
5575
|
return void 0;
|
|
5418
5576
|
}
|
|
5419
5577
|
if (!isAlive(r.pid)) {
|
|
5420
|
-
|
|
5578
|
+
fs28.rmSync(registryFile(), { force: true });
|
|
5421
5579
|
return void 0;
|
|
5422
5580
|
}
|
|
5423
5581
|
return await ping(r.port) ? { ...r, url: `http://127.0.0.1:${r.port}/` } : void 0;
|
|
@@ -5425,7 +5583,7 @@ async function runningDash() {
|
|
|
5425
5583
|
function openBrowser(url) {
|
|
5426
5584
|
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
5427
5585
|
try {
|
|
5428
|
-
|
|
5586
|
+
spawn5(cmd, args, { stdio: "ignore", detached: true }).on("error", () => void 0).unref();
|
|
5429
5587
|
} catch {
|
|
5430
5588
|
}
|
|
5431
5589
|
}
|
|
@@ -5434,7 +5592,7 @@ async function dashCommand(o) {
|
|
|
5434
5592
|
if (o.stop) {
|
|
5435
5593
|
if (!existing) throw new UserError("pitroom dash is not running");
|
|
5436
5594
|
process.kill(existing.pid, "SIGTERM");
|
|
5437
|
-
|
|
5595
|
+
fs28.rmSync(registryFile(), { force: true });
|
|
5438
5596
|
o.log(`stopped pitroom dash (${existing.url})`);
|
|
5439
5597
|
return 0;
|
|
5440
5598
|
}
|
|
@@ -5444,7 +5602,7 @@ async function dashCommand(o) {
|
|
|
5444
5602
|
return 0;
|
|
5445
5603
|
}
|
|
5446
5604
|
if (o.detach) {
|
|
5447
|
-
const child =
|
|
5605
|
+
const child = spawn5(process.execPath, [process.argv[1], "dash", "--serve", ...o.port !== void 0 ? ["--port", String(o.port)] : [], "--idle", "1h"], {
|
|
5448
5606
|
detached: true,
|
|
5449
5607
|
stdio: "ignore",
|
|
5450
5608
|
env: process.env
|
|
@@ -5471,9 +5629,9 @@ async function dashCommand(o) {
|
|
|
5471
5629
|
if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
|
|
5472
5630
|
dash = await startDash({ port: 0, idleMs: o.idleMs });
|
|
5473
5631
|
}
|
|
5474
|
-
|
|
5632
|
+
fs28.mkdirSync(home(), { recursive: true });
|
|
5475
5633
|
const tmp = `${registryFile()}.${process.pid}.tmp`;
|
|
5476
|
-
|
|
5634
|
+
fs28.writeFileSync(tmp, JSON.stringify({ pid: process.pid, port: dash.port }));
|
|
5477
5635
|
renameOver(tmp, registryFile());
|
|
5478
5636
|
const stop = () => void dash.close();
|
|
5479
5637
|
process.once("SIGINT", stop);
|
|
@@ -5483,7 +5641,7 @@ async function dashCommand(o) {
|
|
|
5483
5641
|
if (o.open) openBrowser(dash.url);
|
|
5484
5642
|
await dash.closed;
|
|
5485
5643
|
try {
|
|
5486
|
-
if (JSON.parse(
|
|
5644
|
+
if (JSON.parse(fs28.readFileSync(registryFile(), "utf8")).pid === process.pid) fs28.rmSync(registryFile(), { force: true });
|
|
5487
5645
|
} catch {
|
|
5488
5646
|
}
|
|
5489
5647
|
return 0;
|
|
@@ -5495,10 +5653,10 @@ var MAX_BODY = 4e6;
|
|
|
5495
5653
|
var MAX_SESSIONS = 64;
|
|
5496
5654
|
var IDLE_MS = 60 * 6e4;
|
|
5497
5655
|
var PATH = "/mcp";
|
|
5498
|
-
var tokenFile = () =>
|
|
5656
|
+
var tokenFile = () => path25.join(home(), "mcp-token");
|
|
5499
5657
|
var readToken = (file2) => {
|
|
5500
5658
|
try {
|
|
5501
|
-
const kept =
|
|
5659
|
+
const kept = fs29.readFileSync(file2, "utf8").trim();
|
|
5502
5660
|
return kept.length >= 16 ? kept : void 0;
|
|
5503
5661
|
} catch {
|
|
5504
5662
|
return void 0;
|
|
@@ -5513,10 +5671,10 @@ function mcpToken() {
|
|
|
5513
5671
|
const file2 = tokenFile();
|
|
5514
5672
|
let token = readToken(file2);
|
|
5515
5673
|
if (!token) {
|
|
5516
|
-
|
|
5517
|
-
token =
|
|
5674
|
+
fs29.mkdirSync(path25.dirname(file2), { recursive: true });
|
|
5675
|
+
token = crypto8.randomBytes(32).toString("hex");
|
|
5518
5676
|
try {
|
|
5519
|
-
|
|
5677
|
+
fs29.writeFileSync(file2, `${token}
|
|
5520
5678
|
`, { mode: 384, flag: "wx" });
|
|
5521
5679
|
} catch (e) {
|
|
5522
5680
|
if (e.code !== "EEXIST") throw e;
|
|
@@ -5525,16 +5683,16 @@ function mcpToken() {
|
|
|
5525
5683
|
}
|
|
5526
5684
|
}
|
|
5527
5685
|
try {
|
|
5528
|
-
|
|
5686
|
+
fs29.chmodSync(file2, 384);
|
|
5529
5687
|
} catch {
|
|
5530
5688
|
}
|
|
5531
5689
|
return { token, from: file2 };
|
|
5532
5690
|
}
|
|
5533
5691
|
var LOCAL_ORIGIN = /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/i;
|
|
5534
5692
|
var sameSecret = (given, token) => {
|
|
5535
|
-
const a =
|
|
5536
|
-
const b =
|
|
5537
|
-
return
|
|
5693
|
+
const a = crypto8.createHash("sha256").update(given).digest();
|
|
5694
|
+
const b = crypto8.createHash("sha256").update(token).digest();
|
|
5695
|
+
return crypto8.timingSafeEqual(a, b);
|
|
5538
5696
|
};
|
|
5539
5697
|
var idleSince = (k) => Math.max(k.lastSeen, k.closeStream ? k.streamSeen ?? 0 : 0);
|
|
5540
5698
|
function drop(sessions, id) {
|
|
@@ -5638,7 +5796,7 @@ data: ${JSON.stringify(msg)}
|
|
|
5638
5796
|
if (single?.method === "initialize") {
|
|
5639
5797
|
if (sid) drop(sessions, sid);
|
|
5640
5798
|
if (sessions.size >= MAX_SESSIONS && !evictIdle(sessions)) return reply(res, 503, failure5(-32e3, `${MAX_SESSIONS} sessions are busy with requests: try again when one ends`));
|
|
5641
|
-
const id =
|
|
5799
|
+
const id = crypto8.randomUUID();
|
|
5642
5800
|
kept = { session: newSession(), lastSeen: Date.now() };
|
|
5643
5801
|
sessions.set(id, kept);
|
|
5644
5802
|
headers["Mcp-Session-Id"] = id;
|
|
@@ -5707,16 +5865,16 @@ async function serveMcpHttp(opts) {
|
|
|
5707
5865
|
|
|
5708
5866
|
// src/cli/commands.ts
|
|
5709
5867
|
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
5710
|
-
import
|
|
5711
|
-
import
|
|
5868
|
+
import fs35 from "node:fs";
|
|
5869
|
+
import path31 from "node:path";
|
|
5712
5870
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
5713
5871
|
|
|
5714
5872
|
// src/core/init.ts
|
|
5715
|
-
import
|
|
5716
|
-
import
|
|
5873
|
+
import fs30 from "node:fs";
|
|
5874
|
+
import path26 from "node:path";
|
|
5717
5875
|
var FREE = /-free$/;
|
|
5718
5876
|
var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
|
|
5719
|
-
var isFound = (binary5) =>
|
|
5877
|
+
var isFound = (binary5) => path26.isAbsolute(binary5) && fs30.existsSync(binary5);
|
|
5720
5878
|
function planInit(opts = {}) {
|
|
5721
5879
|
const file2 = configPath();
|
|
5722
5880
|
const workers = backendIds().map((id) => {
|
|
@@ -5762,15 +5920,15 @@ function planInit(opts = {}) {
|
|
|
5762
5920
|
if (Object.keys(tiers).length > 1) config.tiers = tiers;
|
|
5763
5921
|
if (!workers.some((w) => w.found)) blocked = "no worker CLI found: install OpenCode (https://opencode.ai), Codex CLI, Claude Code or Gemini CLI first";
|
|
5764
5922
|
else if (!blocked && !Object.keys(config).length) notes.push("nothing to propose: the default model and the single worker need no config");
|
|
5765
|
-
return { path: file2, exists:
|
|
5923
|
+
return { path: file2, exists: fs30.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
|
|
5766
5924
|
}
|
|
5767
5925
|
function writeInit(plan, force) {
|
|
5768
5926
|
if (plan.blocked) throw new UserError(plan.blocked);
|
|
5769
5927
|
if (!Object.keys(plan.config).length) throw new UserError("nothing to write: the proposal is empty");
|
|
5770
|
-
if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${
|
|
5771
|
-
|
|
5772
|
-
if (plan.exists)
|
|
5773
|
-
|
|
5928
|
+
if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${path26.basename(plan.path)}.bak)`);
|
|
5929
|
+
fs30.mkdirSync(path26.dirname(plan.path), { recursive: true });
|
|
5930
|
+
if (plan.exists) fs30.copyFileSync(plan.path, `${plan.path}.bak`);
|
|
5931
|
+
fs30.writeFileSync(plan.path, `${JSON.stringify(plan.config, null, 2)}
|
|
5774
5932
|
`);
|
|
5775
5933
|
return plan.path;
|
|
5776
5934
|
}
|
|
@@ -5791,17 +5949,17 @@ function formatInit(plan, written) {
|
|
|
5791
5949
|
|
|
5792
5950
|
// src/core/mcp-install.ts
|
|
5793
5951
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
5794
|
-
import
|
|
5952
|
+
import fs31 from "node:fs";
|
|
5795
5953
|
import os10 from "node:os";
|
|
5796
|
-
import
|
|
5954
|
+
import path27 from "node:path";
|
|
5797
5955
|
var CLIENT_IDS = ["claude-code", "codex", "gemini", "cursor", "claude-desktop"];
|
|
5798
5956
|
var NAME = "pitroom";
|
|
5799
5957
|
function mcpCommand() {
|
|
5800
5958
|
const launcher = launcherPath();
|
|
5801
5959
|
if (isPitroomLauncher(launcher)) return { command: launcher, args: ["mcp"] };
|
|
5802
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
5803
|
-
const p =
|
|
5804
|
-
if (
|
|
5960
|
+
for (const dir of (process.env.PATH ?? "").split(path27.delimiter).filter(Boolean)) {
|
|
5961
|
+
const p = path27.join(dir, process.platform === "win32" ? "pitroom.cmd" : "pitroom");
|
|
5962
|
+
if (fs31.existsSync(p)) return { command: path27.resolve(p), args: ["mcp"] };
|
|
5805
5963
|
}
|
|
5806
5964
|
return { command: process.execPath, args: [process.argv[1] ?? "pitroom", "mcp"] };
|
|
5807
5965
|
}
|
|
@@ -5817,7 +5975,7 @@ function run(backendId, args) {
|
|
|
5817
5975
|
var runnable = (backendId) => run(backendId, ["--version"]).ok;
|
|
5818
5976
|
var readJson = (file2) => {
|
|
5819
5977
|
try {
|
|
5820
|
-
return JSON.parse(
|
|
5978
|
+
return JSON.parse(fs31.readFileSync(file2, "utf8"));
|
|
5821
5979
|
} catch {
|
|
5822
5980
|
return void 0;
|
|
5823
5981
|
}
|
|
@@ -5827,7 +5985,7 @@ var jsonState = (file2, c) => {
|
|
|
5827
5985
|
return entry === void 0 ? "absent" : same(c, entry) ? "same" : "different";
|
|
5828
5986
|
};
|
|
5829
5987
|
var home2 = () => os10.homedir();
|
|
5830
|
-
var geminiDir = () =>
|
|
5988
|
+
var geminiDir = () => path27.join(process.env.GEMINI_CLI_HOME ?? home2(), ".gemini");
|
|
5831
5989
|
function cliClient(id, backend, name, where, o) {
|
|
5832
5990
|
return {
|
|
5833
5991
|
id,
|
|
@@ -5850,43 +6008,43 @@ function jsonClient(id, name, file2, dir) {
|
|
|
5850
6008
|
return {
|
|
5851
6009
|
id,
|
|
5852
6010
|
name,
|
|
5853
|
-
found: () =>
|
|
6011
|
+
found: () => fs31.existsSync(dir()),
|
|
5854
6012
|
state: (c) => jsonState(file2(), c),
|
|
5855
6013
|
add: (c) => {
|
|
5856
6014
|
let config = {};
|
|
5857
|
-
if (
|
|
6015
|
+
if (fs31.existsSync(file2())) {
|
|
5858
6016
|
config = readJson(file2());
|
|
5859
6017
|
if (!config || typeof config !== "object" || Array.isArray(config)) return { ok: false, message: `${rel()} is not valid JSON: left alone` };
|
|
5860
6018
|
const backup = `${file2()}.bak-pitroom`;
|
|
5861
|
-
if (!
|
|
6019
|
+
if (!fs31.existsSync(backup)) fs31.copyFileSync(file2(), backup);
|
|
5862
6020
|
}
|
|
5863
6021
|
config.mcpServers = { ...config.mcpServers ?? {}, [NAME]: { command: c.command, args: c.args } };
|
|
5864
|
-
|
|
6022
|
+
fs31.mkdirSync(path27.dirname(file2()), { recursive: true });
|
|
5865
6023
|
const tmp = `${file2()}.${process.pid}.tmp`;
|
|
5866
|
-
|
|
6024
|
+
fs31.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
|
|
5867
6025
|
`);
|
|
5868
|
-
|
|
6026
|
+
fs31.renameSync(tmp, file2());
|
|
5869
6027
|
return { ok: true, message: rel() };
|
|
5870
6028
|
},
|
|
5871
6029
|
remove: () => {
|
|
5872
6030
|
const config = readJson(file2());
|
|
5873
6031
|
if (!config?.mcpServers?.[NAME]) return { ok: true, message: rel() };
|
|
5874
6032
|
delete config.mcpServers[NAME];
|
|
5875
|
-
|
|
6033
|
+
fs31.writeFileSync(file2(), `${JSON.stringify(config, null, 2)}
|
|
5876
6034
|
`);
|
|
5877
6035
|
return { ok: true, message: rel() };
|
|
5878
6036
|
}
|
|
5879
6037
|
};
|
|
5880
6038
|
}
|
|
5881
6039
|
function desktopFile() {
|
|
5882
|
-
if (process.platform === "darwin") return
|
|
5883
|
-
if (process.platform === "win32") return
|
|
5884
|
-
return
|
|
6040
|
+
if (process.platform === "darwin") return path27.join(home2(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
6041
|
+
if (process.platform === "win32") return path27.join(process.env.APPDATA ?? path27.join(home2(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
6042
|
+
return path27.join(process.env.XDG_CONFIG_HOME ?? path27.join(home2(), ".config"), "Claude", "claude_desktop_config.json");
|
|
5885
6043
|
}
|
|
5886
6044
|
function codexState(c) {
|
|
5887
6045
|
let text;
|
|
5888
6046
|
try {
|
|
5889
|
-
text =
|
|
6047
|
+
text = fs31.readFileSync(path27.join(process.env.CODEX_HOME ?? path27.join(home2(), ".codex"), "config.toml"), "utf8");
|
|
5890
6048
|
} catch {
|
|
5891
6049
|
return "absent";
|
|
5892
6050
|
}
|
|
@@ -5904,7 +6062,7 @@ function codexState(c) {
|
|
|
5904
6062
|
function clients() {
|
|
5905
6063
|
return [
|
|
5906
6064
|
cliClient("claude-code", "claude", "Claude Code", "user settings (~/.claude.json)", {
|
|
5907
|
-
state: (c) => jsonState(
|
|
6065
|
+
state: (c) => jsonState(path27.join(home2(), ".claude.json"), c),
|
|
5908
6066
|
add: (c) => ["mcp", "add", "--scope", "user", NAME, "--", c.command, ...c.args],
|
|
5909
6067
|
remove: () => ["mcp", "remove", NAME, "--scope", "user"]
|
|
5910
6068
|
}),
|
|
@@ -5914,12 +6072,12 @@ function clients() {
|
|
|
5914
6072
|
remove: () => ["mcp", "remove", NAME]
|
|
5915
6073
|
}),
|
|
5916
6074
|
cliClient("gemini", "gemini", "Gemini CLI", "user settings (~/.gemini/settings.json)", {
|
|
5917
|
-
state: (c) => jsonState(
|
|
6075
|
+
state: (c) => jsonState(path27.join(geminiDir(), "settings.json"), c),
|
|
5918
6076
|
add: (c) => ["mcp", "add", "--scope", "user", NAME, c.command, ...c.args],
|
|
5919
6077
|
remove: () => ["mcp", "remove", "--scope", "user", NAME]
|
|
5920
6078
|
}),
|
|
5921
|
-
jsonClient("cursor", "Cursor", () =>
|
|
5922
|
-
jsonClient("claude-desktop", "Claude Desktop", desktopFile, () =>
|
|
6079
|
+
jsonClient("cursor", "Cursor", () => path27.join(home2(), ".cursor", "mcp.json"), () => path27.join(home2(), ".cursor")),
|
|
6080
|
+
jsonClient("claude-desktop", "Claude Desktop", desktopFile, () => path27.dirname(desktopFile()))
|
|
5923
6081
|
];
|
|
5924
6082
|
}
|
|
5925
6083
|
function attempt2(f) {
|
|
@@ -5989,17 +6147,17 @@ var parseClients = (value) => {
|
|
|
5989
6147
|
};
|
|
5990
6148
|
|
|
5991
6149
|
// src/core/plan-status.ts
|
|
5992
|
-
import
|
|
5993
|
-
import
|
|
5994
|
-
import
|
|
6150
|
+
import crypto9 from "node:crypto";
|
|
6151
|
+
import fs32 from "node:fs";
|
|
6152
|
+
import path28 from "node:path";
|
|
5995
6153
|
function notesFile(plan) {
|
|
5996
|
-
const id =
|
|
5997
|
-
return
|
|
6154
|
+
const id = crypto9.createHash("sha1").update(plan.file).digest("hex").slice(0, 12);
|
|
6155
|
+
return path28.join(home(), "plans", id, "notes.md");
|
|
5998
6156
|
}
|
|
5999
6157
|
function readNotes(plan) {
|
|
6000
6158
|
const f = notesFile(plan);
|
|
6001
|
-
if (!
|
|
6002
|
-
return
|
|
6159
|
+
if (!fs32.existsSync(f)) return [];
|
|
6160
|
+
return fs32.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
|
|
6003
6161
|
const m = /^(\d{4}-\d\d-\d\d \d\d:\d\d) (.*)$/.exec(l);
|
|
6004
6162
|
return m ? { at: m[1], text: m[2] } : { at: "", text: l };
|
|
6005
6163
|
});
|
|
@@ -6007,11 +6165,11 @@ function readNotes(plan) {
|
|
|
6007
6165
|
function addNote(planFile, text) {
|
|
6008
6166
|
const plan = loadPlan(planFile);
|
|
6009
6167
|
const f = notesFile(plan);
|
|
6010
|
-
|
|
6011
|
-
if (!
|
|
6168
|
+
fs32.mkdirSync(path28.dirname(f), { recursive: true });
|
|
6169
|
+
if (!fs32.existsSync(f)) fs32.writeFileSync(f, `# plan: ${plan.file}
|
|
6012
6170
|
`);
|
|
6013
6171
|
const at = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
|
|
6014
|
-
|
|
6172
|
+
fs32.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
|
|
6015
6173
|
`);
|
|
6016
6174
|
return f;
|
|
6017
6175
|
}
|
|
@@ -6078,27 +6236,27 @@ ${rulings}`,
|
|
|
6078
6236
|
}
|
|
6079
6237
|
|
|
6080
6238
|
// src/core/cache.ts
|
|
6081
|
-
import
|
|
6082
|
-
import
|
|
6083
|
-
import
|
|
6239
|
+
import crypto10 from "node:crypto";
|
|
6240
|
+
import fs33 from "node:fs";
|
|
6241
|
+
import path29 from "node:path";
|
|
6084
6242
|
var SCAN2 = 500;
|
|
6085
6243
|
function cacheKey(o) {
|
|
6086
6244
|
if (o.verify || o.mode !== "read" || o.continueFrom || o.web || o.plan || o.review || o.audit || o.auditRate === 1) return void 0;
|
|
6087
6245
|
if (!(effective().cacheDays.value > 0)) return void 0;
|
|
6088
|
-
const dir = canonical(
|
|
6246
|
+
const dir = canonical(path29.resolve(o.dir));
|
|
6089
6247
|
const root = repoRoot(dir);
|
|
6090
6248
|
if (!root) return void 0;
|
|
6091
6249
|
try {
|
|
6092
6250
|
const tree = snapshotTree(root);
|
|
6093
6251
|
const head = commitOf(root, "HEAD") ?? "";
|
|
6094
6252
|
const files = o.files.map((f) => {
|
|
6095
|
-
const abs =
|
|
6096
|
-
return [abs,
|
|
6253
|
+
const abs = path29.resolve(f);
|
|
6254
|
+
return [abs, crypto10.createHash("sha256").update(fs33.readFileSync(abs)).digest("hex")];
|
|
6097
6255
|
});
|
|
6098
6256
|
const question = o.task.replace(/\s+/g, " ").trim();
|
|
6099
6257
|
const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
|
|
6100
6258
|
const where = !o.inPlace && wantSnapshot(readIn2, root, dir) ? "snapshot" : "project";
|
|
6101
|
-
const key =
|
|
6259
|
+
const key = crypto10.createHash("sha256").update(JSON.stringify([question, path29.relative(root, dir), files, where])).digest("hex");
|
|
6102
6260
|
return { key, state: `${head}:${tree}` };
|
|
6103
6261
|
} catch {
|
|
6104
6262
|
return void 0;
|
|
@@ -6141,9 +6299,9 @@ function ago(iso, now = Date.now()) {
|
|
|
6141
6299
|
var cachedNote = (m) => `pitroom \u27F2 cached answer \xB7 the same question on the same code as run ${m.id} (${ago(m.endedAt ?? m.startedAt)}) \xB7 no worker ran \xB7 --fresh asks one again`;
|
|
6142
6300
|
|
|
6143
6301
|
// src/core/review.ts
|
|
6144
|
-
import
|
|
6145
|
-
import
|
|
6146
|
-
import
|
|
6302
|
+
import crypto11 from "node:crypto";
|
|
6303
|
+
import fs34 from "node:fs";
|
|
6304
|
+
import path30 from "node:path";
|
|
6147
6305
|
var TEMPLATE2 = { task: "task-reviewer", fix: "re-review", range: "code-reviewer" };
|
|
6148
6306
|
var section = (title, body) => `## ${title}
|
|
6149
6307
|
|
|
@@ -6161,7 +6319,7 @@ function ancestors(m) {
|
|
|
6161
6319
|
function briefOf(m) {
|
|
6162
6320
|
const root = [m, ...ancestors(m)].at(-1);
|
|
6163
6321
|
const f = runFile(root.id, "brief.md");
|
|
6164
|
-
return
|
|
6322
|
+
return fs34.existsSync(f) ? fs34.readFileSync(f, "utf8") : root.task;
|
|
6165
6323
|
}
|
|
6166
6324
|
function latestReview(ids) {
|
|
6167
6325
|
for (const id of listRunIds().reverse()) {
|
|
@@ -6175,7 +6333,7 @@ function latestReview(ids) {
|
|
|
6175
6333
|
}
|
|
6176
6334
|
function diffOf(m, from = m.baseTree) {
|
|
6177
6335
|
const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
|
|
6178
|
-
if (where &&
|
|
6336
|
+
if (where && fs34.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
|
|
6179
6337
|
if (from !== m.baseTree) {
|
|
6180
6338
|
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);
|
|
6181
6339
|
}
|
|
@@ -6189,7 +6347,7 @@ function runReview(id) {
|
|
|
6189
6347
|
if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
|
|
6190
6348
|
const chain = ancestors(m);
|
|
6191
6349
|
const previous = latestReview(chain.map((a) => a.id));
|
|
6192
|
-
const dir = m.mode === "isolate" && m.worktree &&
|
|
6350
|
+
const dir = m.mode === "isolate" && m.worktree && fs34.existsSync(m.worktree) ? m.cwd : m.dir;
|
|
6193
6351
|
const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
|
|
6194
6352
|
const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
|
|
6195
6353
|
if (!previous) {
|
|
@@ -6264,20 +6422,20 @@ function writePackage(job) {
|
|
|
6264
6422
|
const g = gitDir(job.dir);
|
|
6265
6423
|
const root = repoRoot(job.dir);
|
|
6266
6424
|
if (!g || !root) throw new UserError(`not a git repository: ${job.dir}`);
|
|
6267
|
-
const name = `review-${
|
|
6268
|
-
const inside2 = !
|
|
6269
|
-
const file2 = inside2 ?
|
|
6425
|
+
const name = `review-${crypto11.randomBytes(4).toString("hex")}.md`;
|
|
6426
|
+
const inside2 = !path30.relative(fs34.realpathSync(root), fs34.realpathSync(g)).startsWith("..");
|
|
6427
|
+
const file2 = inside2 ? path30.join(g, "pitroom", name) : path30.join(root, ".pitroom", name);
|
|
6270
6428
|
if (!inside2) ignoreInGit(root, "/.pitroom/");
|
|
6271
|
-
|
|
6272
|
-
|
|
6429
|
+
fs34.mkdirSync(path30.dirname(file2), { recursive: true });
|
|
6430
|
+
fs34.writeFileSync(file2, job.package);
|
|
6273
6431
|
return file2;
|
|
6274
6432
|
}
|
|
6275
6433
|
function ignoreInGit(root, pattern) {
|
|
6276
|
-
const exclude =
|
|
6277
|
-
const text =
|
|
6434
|
+
const exclude = path30.resolve(root, git(root, ["rev-parse", "--git-path", "info/exclude"]).stdout.trim());
|
|
6435
|
+
const text = fs34.existsSync(exclude) ? fs34.readFileSync(exclude, "utf8") : "";
|
|
6278
6436
|
if (text.split(/\r?\n/).includes(pattern)) return;
|
|
6279
|
-
|
|
6280
|
-
|
|
6437
|
+
fs34.mkdirSync(path30.dirname(exclude), { recursive: true });
|
|
6438
|
+
fs34.appendFileSync(exclude, `${text && !text.endsWith("\n") ? "\n" : ""}# Pitroom review packages in worktrees (removed after each review)
|
|
6281
6439
|
${pattern}
|
|
6282
6440
|
`);
|
|
6283
6441
|
}
|
|
@@ -6433,10 +6591,10 @@ async function cmdReview(p) {
|
|
|
6433
6591
|
writeMeta(meta);
|
|
6434
6592
|
}
|
|
6435
6593
|
} catch (e) {
|
|
6436
|
-
|
|
6594
|
+
fs35.rmSync(packageFile, { force: true });
|
|
6437
6595
|
throw e;
|
|
6438
6596
|
}
|
|
6439
|
-
|
|
6597
|
+
fs35.writeFileSync(runFile(meta.id, "package.md"), job.package);
|
|
6440
6598
|
return launch(p, meta);
|
|
6441
6599
|
}
|
|
6442
6600
|
async function cmdAudit(p) {
|
|
@@ -6504,7 +6662,7 @@ function cmdPlan(p) {
|
|
|
6504
6662
|
}
|
|
6505
6663
|
function cmdCrew(p) {
|
|
6506
6664
|
const file2 = flag(p, "task-file");
|
|
6507
|
-
const tasks = (file2 ?
|
|
6665
|
+
const tasks = (file2 ? fs35.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
|
|
6508
6666
|
if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
|
|
6509
6667
|
if (has(p, "write") && tasks.length > 1) {
|
|
6510
6668
|
throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
|
|
@@ -6612,9 +6770,10 @@ function cmdHistory(p) {
|
|
|
6612
6770
|
const s = historyStats(since2);
|
|
6613
6771
|
if (has(p, "json")) return console.log(JSON.stringify(s, null, 2)), 0;
|
|
6614
6772
|
const t = s.totals;
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
const
|
|
6773
|
+
const limited = t.limited ? ` \xB7 ${t.limited} rate-limited (${s.rateLimits === "counted" ? "counted as not ok" : 'left out; "countRateLimits": true counts them'})` : "";
|
|
6774
|
+
console.log(`${t.runs} runs \xB7 ${t.ok} ok \xB7 ${t.failed} not ok${limited} \xB7 ${secs(t.seconds)} of worker time \xB7 ${(t.tokens / 1e6).toFixed(1)}M tokens \xB7 ~${usd(t.saved)} saved`);
|
|
6775
|
+
const rows2 = s.byWorker.map((w) => [`${w.backend}${w.model ? `:${w.model}` : ""}`, String(w.runs), w.runs ? `${Math.round(100 * w.ok / w.runs)}%` : "-", w.limited ? String(w.limited) : "-", secs(w.avgSeconds), w.avgTokens ? `${Math.round(w.avgTokens / 1e3)}k` : "-", `~${usd(w.saved)}`]);
|
|
6776
|
+
const head2 = ["WORKER", "RUNS", "OK", "LIMITED", "AVG TIME", "AVG TOKENS", "SAVED"];
|
|
6618
6777
|
const widths2 = head2.map((h, i) => Math.max(h.length, ...rows2.map((r) => r[i].length)));
|
|
6619
6778
|
const fmt2 = (r) => r.map((c, i) => i === 0 ? c.padEnd(widths2[i]) : c.padStart(widths2[i])).join(" ");
|
|
6620
6779
|
if (rows2.length) console.log(`
|
|
@@ -6704,7 +6863,7 @@ function sinceMs(s) {
|
|
|
6704
6863
|
if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
|
|
6705
6864
|
return Date.now() - Number(m[1]) * 864e5;
|
|
6706
6865
|
}
|
|
6707
|
-
var readStdin = () => process.stdin.isTTY ? "" :
|
|
6866
|
+
var readStdin = () => process.stdin.isTTY ? "" : fs35.readFileSync(0, "utf8");
|
|
6708
6867
|
function cmdStatusline(p) {
|
|
6709
6868
|
const input = readStdin();
|
|
6710
6869
|
const lines = [];
|
|
@@ -6744,8 +6903,8 @@ function cmdModels(p) {
|
|
|
6744
6903
|
return 0;
|
|
6745
6904
|
}
|
|
6746
6905
|
function cmdHookStart() {
|
|
6747
|
-
const script =
|
|
6748
|
-
if (
|
|
6906
|
+
const script = path31.join(path31.dirname(path31.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
|
|
6907
|
+
if (fs35.existsSync(script)) spawnSync8(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
|
|
6749
6908
|
return 0;
|
|
6750
6909
|
}
|
|
6751
6910
|
function cmdInit(p) {
|
|
@@ -6785,8 +6944,8 @@ function cmdSavings(p) {
|
|
|
6785
6944
|
est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
|
|
6786
6945
|
const out = flag(p, "card");
|
|
6787
6946
|
if (out) {
|
|
6788
|
-
|
|
6789
|
-
console.log(`card written to ${
|
|
6947
|
+
fs35.writeFileSync(out, card(t, period));
|
|
6948
|
+
console.log(`card written to ${path31.resolve(out)}`);
|
|
6790
6949
|
}
|
|
6791
6950
|
if (has(p, "badge")) console.log(`})`);
|
|
6792
6951
|
return 0;
|
|
@@ -6828,7 +6987,7 @@ function cmdConfig(p) {
|
|
|
6828
6987
|
console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
|
|
6829
6988
|
return 0;
|
|
6830
6989
|
}
|
|
6831
|
-
console.log(`config file: ${configPath()}${
|
|
6990
|
+
console.log(`config file: ${configPath()}${fs35.existsSync(configPath()) ? "" : " (not present)"}`);
|
|
6832
6991
|
for (const [key, s] of Object.entries(eff)) {
|
|
6833
6992
|
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);
|
|
6834
6993
|
console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
|
|
@@ -6852,7 +7011,7 @@ function cmdClean(p) {
|
|
|
6852
7011
|
const m = readMeta(id);
|
|
6853
7012
|
if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
|
|
6854
7013
|
recordRun(m);
|
|
6855
|
-
|
|
7014
|
+
fs35.rmSync(runDir(id), { recursive: true, force: true });
|
|
6856
7015
|
}
|
|
6857
7016
|
const snapshots = prune(Date.now(), 0);
|
|
6858
7017
|
console.log(`removed ${old.length} run(s)${snapshots ? ` and ${snapshots} read snapshot${snapshots === 1 ? "" : "s"}` : ""}; the history and the savings ledger are kept`);
|
|
@@ -6863,10 +7022,10 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
|
|
|
6863
7022
|
|
|
6864
7023
|
// src/core/doctor.ts
|
|
6865
7024
|
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
6866
|
-
import
|
|
7025
|
+
import fs36 from "node:fs";
|
|
6867
7026
|
import os11 from "node:os";
|
|
6868
|
-
import
|
|
6869
|
-
var
|
|
7027
|
+
import path32 from "node:path";
|
|
7028
|
+
var MARK2 = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
|
|
6870
7029
|
var NEXT = [
|
|
6871
7030
|
{ when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
|
|
6872
7031
|
{ when: /pitroom install|no Pitroom skills|launcher on PATH/, command: "pitroom install", why: "link the skills and the pitroom command" },
|
|
@@ -6888,10 +7047,10 @@ function usageLine() {
|
|
|
6888
7047
|
return `${ids.length} run${ids.length === 1 ? "" : "s"} so far${when2} (pitroom savings shows what they saved)`;
|
|
6889
7048
|
}
|
|
6890
7049
|
function firstNodeOnPath() {
|
|
6891
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
6892
|
-
const file2 =
|
|
7050
|
+
for (const dir of (process.env.PATH ?? "").split(path32.delimiter).filter(Boolean)) {
|
|
7051
|
+
const file2 = path32.join(dir, process.platform === "win32" ? "node.exe" : "node");
|
|
6893
7052
|
try {
|
|
6894
|
-
if (!
|
|
7053
|
+
if (!fs36.statSync(file2).isFile()) continue;
|
|
6895
7054
|
} catch {
|
|
6896
7055
|
continue;
|
|
6897
7056
|
}
|
|
@@ -6918,12 +7077,12 @@ function doctor5(probe) {
|
|
|
6918
7077
|
const addAll = (list2) => list2.forEach((c) => add(c.level, c.message));
|
|
6919
7078
|
add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
|
|
6920
7079
|
const onPath = firstNodeOnPath();
|
|
6921
|
-
if (onPath && onPath.version && tooOld(onPath.version) &&
|
|
7080
|
+
if (onPath && onPath.version && tooOld(onPath.version) && path32.resolve(onPath.path) !== path32.resolve(process.execPath)) {
|
|
6922
7081
|
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)`);
|
|
6923
7082
|
}
|
|
6924
7083
|
add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
|
|
6925
7084
|
const cfg = loadConfig();
|
|
6926
|
-
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${
|
|
7085
|
+
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs36.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
|
|
6927
7086
|
for (const w of cfg.warnings) add("warn", w);
|
|
6928
7087
|
if (process.platform === "win32") {
|
|
6929
7088
|
add("warn", "git guard: not available on Windows yet, so Pitroom does not block a worker's git history changes or pushes there (read-only runs are still limited by each worker CLI's own rules, and --isolate keeps edits in a copy); prefer --isolate and check the patch before apply");
|
|
@@ -7019,7 +7178,7 @@ function print(checks) {
|
|
|
7019
7178
|
for (const name of [...new Set(checks.map((c) => c.section))]) {
|
|
7020
7179
|
console.log(`
|
|
7021
7180
|
${bold(name)}`);
|
|
7022
|
-
for (const c of checks.filter((x) => x.section === name)) console.log(` ${
|
|
7181
|
+
for (const c of checks.filter((x) => x.section === name)) console.log(` ${MARK2[c.level]()} ${wrapText(c.message, 4)}`);
|
|
7023
7182
|
}
|
|
7024
7183
|
const count = (level) => checks.filter((c) => c.level === level).length;
|
|
7025
7184
|
const [ok, warn, fail] = [count("ok"), count("warn"), count("fail")];
|
|
@@ -7080,19 +7239,19 @@ function skillChecks() {
|
|
|
7080
7239
|
else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
|
|
7081
7240
|
}
|
|
7082
7241
|
if (geminiExtensionInstalled()) {
|
|
7083
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
7242
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path32.sep}.agents${path32.sep}`) && i.names.length);
|
|
7084
7243
|
checks.push(
|
|
7085
7244
|
linked ? { level: "warn", message: "Pitroom is installed as a Gemini CLI extension and linked into ~/.agents/skills: Gemini loads the skills twice; run `pitroom uninstall` or `gemini extensions uninstall pitroom`" } : { level: "ok", message: "Gemini CLI extension installed (skills + context)" }
|
|
7086
7245
|
);
|
|
7087
7246
|
}
|
|
7088
7247
|
if (viaPlugin) {
|
|
7089
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
7248
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path32.sep}.claude${path32.sep}`) && i.names.length);
|
|
7090
7249
|
checks.push(
|
|
7091
7250
|
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)" }
|
|
7092
7251
|
);
|
|
7093
7252
|
}
|
|
7094
7253
|
const launcher = launcherPath();
|
|
7095
|
-
if (
|
|
7254
|
+
if (fs36.existsSync(launcher)) {
|
|
7096
7255
|
const r = spawnSync9(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
|
|
7097
7256
|
checks.push(
|
|
7098
7257
|
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)}` }
|
|
@@ -7104,12 +7263,12 @@ function skillChecks() {
|
|
|
7104
7263
|
}
|
|
7105
7264
|
function geminiExtensionInstalled() {
|
|
7106
7265
|
const home3 = process.env.GEMINI_CLI_HOME ?? os11.homedir();
|
|
7107
|
-
return
|
|
7266
|
+
return fs36.existsSync(path32.join(home3, ".gemini", "extensions", "pitroom"));
|
|
7108
7267
|
}
|
|
7109
7268
|
function pluginInstalled() {
|
|
7110
7269
|
try {
|
|
7111
|
-
const f =
|
|
7112
|
-
const plugins = JSON.parse(
|
|
7270
|
+
const f = path32.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
7271
|
+
const plugins = JSON.parse(fs36.readFileSync(f, "utf8")).plugins ?? {};
|
|
7113
7272
|
return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
|
|
7114
7273
|
} catch {
|
|
7115
7274
|
return false;
|
|
@@ -7126,24 +7285,24 @@ function superpowersActive() {
|
|
|
7126
7285
|
for (const p of openCodePlugins()) {
|
|
7127
7286
|
if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
|
|
7128
7287
|
}
|
|
7129
|
-
for (const base2 of [
|
|
7130
|
-
const dir =
|
|
7131
|
-
if (
|
|
7288
|
+
for (const base2 of [path32.join(os11.homedir(), ".agents", "skills"), path32.join(os11.homedir(), ".claude", "skills")]) {
|
|
7289
|
+
const dir = path32.join(base2, "using-superpowers");
|
|
7290
|
+
if (fs36.existsSync(path32.join(dir, "SKILL.md"))) found.push(dir);
|
|
7132
7291
|
}
|
|
7133
7292
|
return found;
|
|
7134
7293
|
}
|
|
7135
7294
|
function claudePlugins() {
|
|
7136
7295
|
try {
|
|
7137
|
-
const f =
|
|
7138
|
-
return Object.keys(JSON.parse(
|
|
7296
|
+
const f = path32.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
7297
|
+
return Object.keys(JSON.parse(fs36.readFileSync(f, "utf8")).plugins ?? {});
|
|
7139
7298
|
} catch {
|
|
7140
7299
|
return [];
|
|
7141
7300
|
}
|
|
7142
7301
|
}
|
|
7143
7302
|
function claudePluginEnabled(key) {
|
|
7144
7303
|
try {
|
|
7145
|
-
const f =
|
|
7146
|
-
return JSON.parse(
|
|
7304
|
+
const f = path32.join(os11.homedir(), ".claude", "settings.json");
|
|
7305
|
+
return JSON.parse(fs36.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
|
|
7147
7306
|
} catch {
|
|
7148
7307
|
return true;
|
|
7149
7308
|
}
|
|
@@ -7152,7 +7311,7 @@ function codexPlugins() {
|
|
|
7152
7311
|
const plugins = /* @__PURE__ */ new Map();
|
|
7153
7312
|
let text;
|
|
7154
7313
|
try {
|
|
7155
|
-
text =
|
|
7314
|
+
text = fs36.readFileSync(path32.join(process.env.CODEX_HOME ?? path32.join(os11.homedir(), ".codex"), "config.toml"), "utf8");
|
|
7156
7315
|
} catch {
|
|
7157
7316
|
return plugins;
|
|
7158
7317
|
}
|
|
@@ -7170,11 +7329,11 @@ function codexPlugins() {
|
|
|
7170
7329
|
return plugins;
|
|
7171
7330
|
}
|
|
7172
7331
|
function openCodePlugins() {
|
|
7173
|
-
const dir =
|
|
7332
|
+
const dir = path32.join(process.env.XDG_CONFIG_HOME ?? path32.join(os11.homedir(), ".config"), "opencode");
|
|
7174
7333
|
const plugins = [];
|
|
7175
7334
|
for (const name of ["opencode.json", "opencode.jsonc"]) {
|
|
7176
7335
|
try {
|
|
7177
|
-
const text =
|
|
7336
|
+
const text = fs36.readFileSync(path32.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
|
|
7178
7337
|
const list2 = JSON.parse(text).plugin;
|
|
7179
7338
|
if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
|
|
7180
7339
|
} catch {
|
|
@@ -7182,7 +7341,7 @@ function openCodePlugins() {
|
|
|
7182
7341
|
}
|
|
7183
7342
|
for (const folder of ["plugin", "plugins"]) {
|
|
7184
7343
|
try {
|
|
7185
|
-
for (const f of
|
|
7344
|
+
for (const f of fs36.readdirSync(path32.join(dir, folder))) plugins.push(path32.join(dir, folder, f));
|
|
7186
7345
|
} catch {
|
|
7187
7346
|
}
|
|
7188
7347
|
}
|
|
@@ -7299,7 +7458,7 @@ var COMMANDS = {
|
|
|
7299
7458
|
if (port !== void 0 && !(/^\d+$/.test(port) && Number(port) <= 65535)) throw new UserError("--port takes a number from 0 to 65535 (0 picks a free one)");
|
|
7300
7459
|
const dir = flag(p, "dir");
|
|
7301
7460
|
if (dir !== void 0) {
|
|
7302
|
-
if (!
|
|
7461
|
+
if (!fs37.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) throw new UserError(`-d ${dir}: no such directory`);
|
|
7303
7462
|
process.chdir(dir);
|
|
7304
7463
|
process.env.PWD = process.cwd();
|
|
7305
7464
|
}
|