pitroom 0.19.0 → 0.21.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 +31 -0
- package/PRIVACY.md +3 -1
- package/README.md +77 -2
- package/dist/lib.mjs +837 -40
- package/dist/pitroom.mjs +1342 -906
- 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 fs38 from "node:fs";
|
|
5
5
|
|
|
6
6
|
// src/core/errors.ts
|
|
7
7
|
var UserError = class extends Error {
|
|
@@ -391,10 +391,10 @@ function parseEvents2(jsonl) {
|
|
|
391
391
|
}
|
|
392
392
|
case "turn.completed": {
|
|
393
393
|
const u = e.usage ?? {};
|
|
394
|
-
const
|
|
394
|
+
const cached4 = num2(u.cached_input_tokens);
|
|
395
395
|
const reasoning = num2(u.reasoning_output_tokens);
|
|
396
|
-
usage2.input += Math.max(0, num2(u.input_tokens) -
|
|
397
|
-
usage2.cacheRead +=
|
|
396
|
+
usage2.input += Math.max(0, num2(u.input_tokens) - cached4);
|
|
397
|
+
usage2.cacheRead += cached4;
|
|
398
398
|
usage2.cacheWrite += num2(u.cache_write_input_tokens);
|
|
399
399
|
usage2.output += Math.max(0, num2(u.output_tokens) - reasoning);
|
|
400
400
|
usage2.reasoning += reasoning;
|
|
@@ -768,9 +768,9 @@ function parseEvents3(jsonl) {
|
|
|
768
768
|
if (e.severity === "error") error ??= String(e.message ?? "Gemini CLI error");
|
|
769
769
|
} else if (e.type === "result") {
|
|
770
770
|
const s = e.stats ?? {};
|
|
771
|
-
const
|
|
772
|
-
usage2.cacheRead +=
|
|
773
|
-
usage2.input += s.input !== void 0 ? num3(s.input) : Math.max(0, num3(s.input_tokens) -
|
|
771
|
+
const cached4 = num3(s.cached);
|
|
772
|
+
usage2.cacheRead += cached4;
|
|
773
|
+
usage2.input += s.input !== void 0 ? num3(s.input) : Math.max(0, num3(s.input_tokens) - cached4);
|
|
774
774
|
usage2.output += num3(s.output_tokens);
|
|
775
775
|
usage2.total += num3(s.total_tokens) || num3(s.input_tokens) + num3(s.output_tokens);
|
|
776
776
|
usage2.toolCalls = Math.max(usage2.toolCalls, num3(s.tool_calls));
|
|
@@ -1441,7 +1441,16 @@ 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",
|
|
1450
|
+
workerPrices: "record",
|
|
1451
|
+
priceFeed: "boolean",
|
|
1452
|
+
priceFeedUrl: "string",
|
|
1453
|
+
priceFeedHours: "number"
|
|
1445
1454
|
};
|
|
1446
1455
|
function configPath() {
|
|
1447
1456
|
if (process.env.PITROOM_CONFIG) return path7.resolve(process.env.PITROOM_CONFIG);
|
|
@@ -1503,6 +1512,10 @@ var days = (v) => {
|
|
|
1503
1512
|
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
1504
1513
|
return typeof n === "number" && Number.isInteger(n) && n >= 0 ? n : void 0;
|
|
1505
1514
|
};
|
|
1515
|
+
var flag01 = (v) => {
|
|
1516
|
+
const t = v?.trim().toLowerCase();
|
|
1517
|
+
return t === void 0 || t === "" ? void 0 : ["1", "true", "on", "yes"].includes(t) ? true : ["0", "false", "off", "no"].includes(t) ? false : void 0;
|
|
1518
|
+
};
|
|
1506
1519
|
var readIn = (v) => v === "auto" || v === "snapshot" || v === "project" ? v : void 0;
|
|
1507
1520
|
var list = (s) => s?.split(",").map((x) => x.trim()).filter(Boolean);
|
|
1508
1521
|
function setting(flag2, env, conf, fallback) {
|
|
@@ -1530,7 +1543,16 @@ function effective(flags = {}) {
|
|
|
1530
1543
|
costs: setting(void 0, void 0, c.costs, {}),
|
|
1531
1544
|
audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0),
|
|
1532
1545
|
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)
|
|
1546
|
+
cacheDays: setting(void 0, days(e.PITROOM_CACHE_DAYS), days(c.cacheDays), 7),
|
|
1547
|
+
countRateLimits: setting(void 0, void 0, c.countRateLimits, false),
|
|
1548
|
+
mcpDash: setting(void 0, flag01(e.PITROOM_MCP_DASH), c.mcpDash, true),
|
|
1549
|
+
notify: setting(void 0, flag01(e.PITROOM_NOTIFY), c.notify, false),
|
|
1550
|
+
notifyCommand: setting(void 0, e.PITROOM_NOTIFY_COMMAND?.trim() || void 0, c.notifyCommand?.trim() || void 0, void 0),
|
|
1551
|
+
notifyAfter: setting(void 0, days(e.PITROOM_NOTIFY_AFTER), days(c.notifyAfter), 15),
|
|
1552
|
+
workerPrices: setting(void 0, void 0, c.workerPrices, {}),
|
|
1553
|
+
priceFeed: setting(void 0, flag01(e.PITROOM_PRICE_FEED), c.priceFeed, false),
|
|
1554
|
+
priceFeedUrl: setting(void 0, e.PITROOM_PRICE_FEED_URL?.trim() || void 0, c.priceFeedUrl?.trim() || void 0, "https://models.dev/api.json"),
|
|
1555
|
+
priceFeedHours: setting(void 0, positiveInt(e.PITROOM_PRICE_FEED_HOURS), positiveInt(c.priceFeedHours), 24)
|
|
1534
1556
|
};
|
|
1535
1557
|
}
|
|
1536
1558
|
|
|
@@ -1594,6 +1616,8 @@ var BOOL_FLAGS = {
|
|
|
1594
1616
|
"--badge": "badge",
|
|
1595
1617
|
"--probe": "probe",
|
|
1596
1618
|
"--clear": "clear",
|
|
1619
|
+
"--refresh": "refresh",
|
|
1620
|
+
"--quiet": "quiet",
|
|
1597
1621
|
"--copy": "copy",
|
|
1598
1622
|
"--all": "all",
|
|
1599
1623
|
"--models": "models",
|
|
@@ -1714,9 +1738,9 @@ function effortFlag(p) {
|
|
|
1714
1738
|
import readline from "node:readline";
|
|
1715
1739
|
|
|
1716
1740
|
// src/core/run.ts
|
|
1717
|
-
import { spawn as
|
|
1718
|
-
import
|
|
1719
|
-
import
|
|
1741
|
+
import { spawn as spawn4, spawnSync as spawnSync6 } from "node:child_process";
|
|
1742
|
+
import fs27 from "node:fs";
|
|
1743
|
+
import path23 from "node:path";
|
|
1720
1744
|
|
|
1721
1745
|
// src/vcs/git.ts
|
|
1722
1746
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
@@ -1930,7 +1954,9 @@ import fs12 from "node:fs";
|
|
|
1930
1954
|
// src/vcs/guard.ts
|
|
1931
1955
|
import fs11 from "node:fs";
|
|
1932
1956
|
import path9 from "node:path";
|
|
1933
|
-
var VERSION =
|
|
1957
|
+
var VERSION = 2;
|
|
1958
|
+
var WINDOWS = process.platform === "win32";
|
|
1959
|
+
var fwd = (p) => WINDOWS ? p.replace(/\\/g, "/") : p;
|
|
1934
1960
|
var SHIM = `#!/bin/sh
|
|
1935
1961
|
# pitroom git guard v${VERSION}. Blocks git commands that change history, refs, the
|
|
1936
1962
|
# index or discard work. The primary agent reviews and commits, not the worker.
|
|
@@ -2007,9 +2033,10 @@ function shimDir() {
|
|
|
2007
2033
|
return path9.join(home(), "shim", `v${VERSION}`);
|
|
2008
2034
|
}
|
|
2009
2035
|
function findRealGit(skip) {
|
|
2036
|
+
const same2 = (a, b) => WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
2010
2037
|
for (const dir of (process.env.PATH ?? "").split(path9.delimiter).filter(Boolean)) {
|
|
2011
|
-
if (path9.resolve(dir)
|
|
2012
|
-
const p = path9.join(dir, "git");
|
|
2038
|
+
if (same2(path9.resolve(dir), skip)) continue;
|
|
2039
|
+
const p = path9.join(dir, WINDOWS ? "git.exe" : "git");
|
|
2013
2040
|
try {
|
|
2014
2041
|
fs11.accessSync(p, fs11.constants.X_OK);
|
|
2015
2042
|
if (fs11.statSync(p).isFile()) return p;
|
|
@@ -2018,6 +2045,24 @@ function findRealGit(skip) {
|
|
|
2018
2045
|
}
|
|
2019
2046
|
return void 0;
|
|
2020
2047
|
}
|
|
2048
|
+
function findGitSh(realGit) {
|
|
2049
|
+
let dir = path9.dirname(realGit);
|
|
2050
|
+
for (let i = 0; i < 4; i++) {
|
|
2051
|
+
for (const rel of ["bin/sh.exe", "usr/bin/sh.exe"]) {
|
|
2052
|
+
const p = path9.join(dir, rel);
|
|
2053
|
+
if (fs11.existsSync(p)) return p;
|
|
2054
|
+
}
|
|
2055
|
+
const up = path9.dirname(dir);
|
|
2056
|
+
if (up === dir) break;
|
|
2057
|
+
dir = up;
|
|
2058
|
+
}
|
|
2059
|
+
return void 0;
|
|
2060
|
+
}
|
|
2061
|
+
var cmdShim = (sh) => `@echo off\r
|
|
2062
|
+
set "MSYS2_ARG_CONV_EXCL=*"\r
|
|
2063
|
+
"${sh}" "%~dp0git" %*\r
|
|
2064
|
+
exit /b %errorlevel%\r
|
|
2065
|
+
`;
|
|
2021
2066
|
var HOOK_VERSION = 1;
|
|
2022
2067
|
var REF_HOOK = `#!/bin/sh
|
|
2023
2068
|
# pitroom git guard (layer 2, v${HOOK_VERSION}): refuse ref updates made by workers.
|
|
@@ -2031,7 +2076,17 @@ function writeIfChanged(file2, content) {
|
|
|
2031
2076
|
fs11.mkdirSync(path9.dirname(file2), { recursive: true });
|
|
2032
2077
|
const tmp = `${file2}.${process.pid}.tmp`;
|
|
2033
2078
|
fs11.writeFileSync(tmp, content, { mode: 493 });
|
|
2034
|
-
|
|
2079
|
+
renameOver(tmp, file2);
|
|
2080
|
+
}
|
|
2081
|
+
function pathKey(env) {
|
|
2082
|
+
const keys = Object.keys(env).filter((k) => k.toLowerCase() === "path");
|
|
2083
|
+
return keys.includes("PATH") ? "PATH" : keys[0] ?? "PATH";
|
|
2084
|
+
}
|
|
2085
|
+
function onePath(env) {
|
|
2086
|
+
const key = pathKey(env);
|
|
2087
|
+
const out = { ...env };
|
|
2088
|
+
for (const k of Object.keys(out)) if (k !== key && k.toLowerCase() === "path") delete out[k];
|
|
2089
|
+
return out;
|
|
2035
2090
|
}
|
|
2036
2091
|
function withGitConfig(env, entries) {
|
|
2037
2092
|
const out = { ...env };
|
|
@@ -2052,17 +2107,27 @@ function guardEnv(env) {
|
|
|
2052
2107
|
GIT_PAGER: "cat",
|
|
2053
2108
|
PAGER: "cat"
|
|
2054
2109
|
};
|
|
2055
|
-
if (process.platform === "win32") return { ...env, ...quiet };
|
|
2056
2110
|
writeIfChanged(path9.join(hooksDir(), "reference-transaction"), REF_HOOK);
|
|
2057
|
-
env = withGitConfig(env, [
|
|
2058
|
-
["core.hooksPath", hooksDir()],
|
|
2111
|
+
env = withGitConfig(onePath(env), [
|
|
2112
|
+
["core.hooksPath", fwd(hooksDir())],
|
|
2059
2113
|
["url.pitroom-push-blocked://.pushInsteadOf", ""]
|
|
2060
2114
|
]);
|
|
2061
2115
|
const dir = shimDir();
|
|
2062
2116
|
const real = findRealGit(dir);
|
|
2063
2117
|
if (!real) return { ...env, ...quiet };
|
|
2118
|
+
let sh;
|
|
2119
|
+
if (WINDOWS) {
|
|
2120
|
+
sh = findGitSh(real);
|
|
2121
|
+
if (!sh) return { ...env, ...quiet };
|
|
2122
|
+
writeIfChanged(path9.join(dir, "git.cmd"), cmdShim(sh));
|
|
2123
|
+
}
|
|
2064
2124
|
writeIfChanged(path9.join(dir, "git"), SHIM);
|
|
2065
|
-
|
|
2125
|
+
const key = pathKey(env);
|
|
2126
|
+
return { ...env, ...quiet, PITROOM_REAL_GIT: fwd(real), [key]: `${dir}${path9.delimiter}${env[key] ?? ""}` };
|
|
2127
|
+
}
|
|
2128
|
+
function shimReady() {
|
|
2129
|
+
const env = guardEnv({ ...process.env });
|
|
2130
|
+
return Boolean(env[pathKey(env)]?.startsWith(shimDir()));
|
|
2066
2131
|
}
|
|
2067
2132
|
|
|
2068
2133
|
// src/core/process.ts
|
|
@@ -2212,10 +2277,11 @@ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build"
|
|
|
2212
2277
|
var MAX_VISITED = 2e4;
|
|
2213
2278
|
var MAX_DEPTH = 4;
|
|
2214
2279
|
var TEMPLATE = /\.(example|sample|template|dist|defaults?|tpl)$/i;
|
|
2215
|
-
function looksSecret(
|
|
2280
|
+
function looksSecret(file2) {
|
|
2281
|
+
const name = file2.toLowerCase();
|
|
2216
2282
|
if (TEMPLATE.test(name)) return false;
|
|
2217
|
-
if (name === ".env" || name.startsWith(".env.") ||
|
|
2218
|
-
return /\.(pem|p12|pfx|key|jks|keystore)
|
|
2283
|
+
if (name === ".env" || name.startsWith(".env.") || name.endsWith(".env")) return true;
|
|
2284
|
+
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
2285
|
}
|
|
2220
2286
|
function findSecretFiles(dir) {
|
|
2221
2287
|
const found = [];
|
|
@@ -2384,6 +2450,7 @@ function backToProject(text, snapshotDir, root) {
|
|
|
2384
2450
|
|
|
2385
2451
|
// src/core/audit.ts
|
|
2386
2452
|
import crypto4 from "node:crypto";
|
|
2453
|
+
import path13 from "node:path";
|
|
2387
2454
|
var ANSWER_MAX = 8e3;
|
|
2388
2455
|
function auditRate(meta) {
|
|
2389
2456
|
return meta.auditRate ?? effective().audit.value;
|
|
@@ -2406,13 +2473,24 @@ function pickAuditor(meta) {
|
|
|
2406
2473
|
const candidates = [tiers.audit, tiers.cheap, ...eff.fallback.value, eff.worker.value].filter((s) => !!s);
|
|
2407
2474
|
return candidates.find((c) => !sameTarget(parseTarget(c, def), ran));
|
|
2408
2475
|
}
|
|
2476
|
+
function relativeToAuditor(answer, meta) {
|
|
2477
|
+
let out = answer;
|
|
2478
|
+
const roots = [...new Set([meta.dir, meta.repoRoot].filter((r) => !!r))].sort((a, b) => b.length - a.length);
|
|
2479
|
+
for (const root of roots) {
|
|
2480
|
+
const rel = path13.relative(meta.dir, root);
|
|
2481
|
+
const prefix = rel ? `${rel.split(path13.sep).join("/")}/` : "";
|
|
2482
|
+
out = out.split(`${root}${path13.sep}`).join(prefix).replace(new RegExp(`${root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w.-])`, "g"), rel || ".");
|
|
2483
|
+
}
|
|
2484
|
+
return out;
|
|
2485
|
+
}
|
|
2409
2486
|
function auditTask(meta, answer) {
|
|
2410
|
-
const text = answer.trim();
|
|
2487
|
+
const text = relativeToAuditor(answer, meta).trim();
|
|
2411
2488
|
const shown = text.length > ANSWER_MAX ? `${text.slice(0, ANSWER_MAX)}
|
|
2412
2489
|
\u2026 (cut: ${text.length - ANSWER_MAX} more characters)` : text;
|
|
2413
2490
|
return [
|
|
2414
2491
|
"You are auditing another worker's answer to a read-only question about this project. Do not take the answer on trust:",
|
|
2415
2492
|
"check its key claims yourself against the files here (open the cited files and lines, search for what it says exists or is missing).",
|
|
2493
|
+
"Paths in the answer are relative to your working directory.",
|
|
2416
2494
|
"Check the answer against the QUESTION, not only its own claims: every condition the question sets (a directory or scope,",
|
|
2417
2495
|
'what to leave out, "all", "only", "exactly", a count) must hold. Valid references do not make an answer right:',
|
|
2418
2496
|
"for a list, look for items that are missing and items that do not belong; for a count, count again yourself.",
|
|
@@ -2454,373 +2532,195 @@ function auditBadge(meta) {
|
|
|
2454
2532
|
return a.state === "running" || a.state === "queued" ? "PENDING" : "FAILED";
|
|
2455
2533
|
}
|
|
2456
2534
|
|
|
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, "");
|
|
2535
|
+
// src/core/notify.ts
|
|
2536
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
2537
|
+
import crypto5 from "node:crypto";
|
|
2538
|
+
import fs22 from "node:fs";
|
|
2539
|
+
import path18 from "node:path";
|
|
2528
2540
|
|
|
2529
|
-
// src/core/
|
|
2530
|
-
import
|
|
2531
|
-
import
|
|
2541
|
+
// src/core/history.ts
|
|
2542
|
+
import fs19 from "node:fs";
|
|
2543
|
+
import { createRequire } from "node:module";
|
|
2544
|
+
import path16 from "node:path";
|
|
2545
|
+
import zlib from "node:zlib";
|
|
2532
2546
|
|
|
2533
|
-
// src/core/
|
|
2547
|
+
// src/core/receipt.ts
|
|
2534
2548
|
import fs17 from "node:fs";
|
|
2535
|
-
import
|
|
2549
|
+
import path15 from "node:path";
|
|
2550
|
+
|
|
2551
|
+
// src/core/prices.ts
|
|
2552
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
2553
|
+
import fs16 from "node:fs";
|
|
2536
2554
|
import path14 from "node:path";
|
|
2537
|
-
|
|
2538
|
-
var
|
|
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;
|
|
2555
|
+
var DEFAULT_URL = "https://models.dev/api.json";
|
|
2556
|
+
var RETRY_AFTER_FAILURE_MS = 60 * 6e4;
|
|
2557
|
+
var MIN_MODELS = 100;
|
|
2558
|
+
function catalogLabel() {
|
|
2559
|
+
const url = effective().priceFeedUrl.value;
|
|
2560
|
+
if (url === DEFAULT_URL) return "models.dev";
|
|
2561
|
+
try {
|
|
2562
|
+
return new URL(url).host;
|
|
2563
|
+
} catch {
|
|
2564
|
+
return url;
|
|
2565
|
+
}
|
|
2552
2566
|
}
|
|
2553
|
-
var
|
|
2554
|
-
var
|
|
2555
|
-
function
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
)
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
exit /b %ERRORLEVEL%\r
|
|
2566
|
-
`;
|
|
2567
|
+
var catalogFile = () => path14.join(home(), "prices.json");
|
|
2568
|
+
var attemptFile = () => path14.join(home(), "prices.attempt");
|
|
2569
|
+
function trim(raw) {
|
|
2570
|
+
const out = {};
|
|
2571
|
+
if (!raw || typeof raw !== "object") return out;
|
|
2572
|
+
for (const [provider, p] of Object.entries(raw)) {
|
|
2573
|
+
for (const [id, m] of Object.entries(p?.models ?? {})) {
|
|
2574
|
+
const c = m?.cost;
|
|
2575
|
+
const ok = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
2576
|
+
if (!c || !ok(c.input) || !ok(c.output)) continue;
|
|
2577
|
+
out[`${provider}/${id}`] = [c.input, c.output, ok(c.cache_read) ? c.cache_read : c.input / 10];
|
|
2578
|
+
}
|
|
2567
2579
|
}
|
|
2568
|
-
|
|
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
|
-
`;
|
|
2580
|
+
return out;
|
|
2579
2581
|
}
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
+
var cached2;
|
|
2583
|
+
function readCatalog() {
|
|
2584
|
+
const file2 = catalogFile();
|
|
2582
2585
|
try {
|
|
2583
|
-
const
|
|
2584
|
-
|
|
2586
|
+
const { mtimeMs } = fs16.statSync(file2);
|
|
2587
|
+
if (cached2?.file === file2 && cached2.mtimeMs === mtimeMs) return cached2.catalog;
|
|
2588
|
+
const parsed = JSON.parse(fs16.readFileSync(file2, "utf8"));
|
|
2589
|
+
const catalog5 = parsed && typeof parsed.models === "object" && parsed.models && Number.isFinite(Date.parse(parsed.fetchedAt)) ? parsed : void 0;
|
|
2590
|
+
cached2 = { file: file2, mtimeMs, catalog: catalog5 };
|
|
2591
|
+
return catalog5;
|
|
2585
2592
|
} catch {
|
|
2586
|
-
return
|
|
2593
|
+
return void 0;
|
|
2587
2594
|
}
|
|
2588
2595
|
}
|
|
2589
|
-
|
|
2596
|
+
var ageMs = (file2) => {
|
|
2590
2597
|
try {
|
|
2591
|
-
return
|
|
2598
|
+
return Date.now() - fs16.statSync(file2).mtimeMs;
|
|
2592
2599
|
} catch {
|
|
2593
|
-
return
|
|
2600
|
+
return Infinity;
|
|
2594
2601
|
}
|
|
2602
|
+
};
|
|
2603
|
+
function refreshDue() {
|
|
2604
|
+
const eff = effective();
|
|
2605
|
+
if (!eff.priceFeed.value) return false;
|
|
2606
|
+
if (ageMs(catalogFile()) < eff.priceFeedHours.value * 36e5) return false;
|
|
2607
|
+
return ageMs(attemptFile()) > RETRY_AFTER_FAILURE_MS;
|
|
2595
2608
|
}
|
|
2596
|
-
function
|
|
2597
|
-
const
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2609
|
+
async function refreshCatalog() {
|
|
2610
|
+
const url = effective().priceFeedUrl.value;
|
|
2611
|
+
try {
|
|
2612
|
+
const r = await fetch(url, { signal: AbortSignal.timeout(3e4), headers: { accept: "application/json" } });
|
|
2613
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
2614
|
+
const models = trim(await r.json());
|
|
2615
|
+
const n = Object.keys(models).length;
|
|
2616
|
+
if (n < MIN_MODELS) throw new Error(`only ${n} priced models in it (a catalog has thousands): not used`);
|
|
2617
|
+
fs16.mkdirSync(home(), { recursive: true });
|
|
2618
|
+
const tmp = `${catalogFile()}.${process.pid}.tmp`;
|
|
2619
|
+
const catalog5 = { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), source: url, models };
|
|
2620
|
+
fs16.writeFileSync(tmp, JSON.stringify(catalog5));
|
|
2621
|
+
renameOver(tmp, catalogFile());
|
|
2622
|
+
fs16.rmSync(attemptFile(), { force: true });
|
|
2623
|
+
return { state: "updated", models: n };
|
|
2624
|
+
} catch (e) {
|
|
2625
|
+
try {
|
|
2626
|
+
fs16.mkdirSync(home(), { recursive: true });
|
|
2627
|
+
fs16.writeFileSync(attemptFile(), `${(/* @__PURE__ */ new Date()).toISOString()} ${e.message}
|
|
2628
|
+
`);
|
|
2629
|
+
} catch {
|
|
2630
|
+
}
|
|
2631
|
+
return { state: "failed", error: e.message };
|
|
2605
2632
|
}
|
|
2606
|
-
fs17.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
|
|
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);
|
|
2615
2633
|
}
|
|
2616
|
-
function
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
if (!
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2634
|
+
function refreshInBackground() {
|
|
2635
|
+
try {
|
|
2636
|
+
if (!refreshDue()) return;
|
|
2637
|
+
const script = process.argv[1];
|
|
2638
|
+
if (!script) return;
|
|
2639
|
+
fs16.mkdirSync(home(), { recursive: true });
|
|
2640
|
+
fs16.writeFileSync(attemptFile(), `${(/* @__PURE__ */ new Date()).toISOString()} refresh started
|
|
2641
|
+
`);
|
|
2642
|
+
const child = spawn2(process.execPath, [script, "prices", "--refresh", "--quiet"], { detached: true, stdio: "ignore", env: process.env });
|
|
2643
|
+
child.on("error", () => void 0);
|
|
2644
|
+
child.unref();
|
|
2645
|
+
} catch {
|
|
2624
2646
|
}
|
|
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}`;
|
|
2628
2647
|
}
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
const
|
|
2632
|
-
if (
|
|
2633
|
-
const
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2648
|
+
var OWN = { codex: ["openai"], claude: ["anthropic"], gemini: ["google"] };
|
|
2649
|
+
function catalogPrice(backend, ...models) {
|
|
2650
|
+
const catalog5 = readCatalog();
|
|
2651
|
+
if (!catalog5) return void 0;
|
|
2652
|
+
for (const raw of models) {
|
|
2653
|
+
const id = raw?.split("#")[0];
|
|
2654
|
+
if (!id) continue;
|
|
2655
|
+
const keys = [id, ...[...OWN[backend] ?? [], "anthropic", "openai", "google"].map((p) => `${p}/${id}`)];
|
|
2656
|
+
const any = keys.find((k) => catalog5.models[k]) ?? Object.keys(catalog5.models).filter((k) => k.endsWith(`/${id}`)).sort()[0];
|
|
2657
|
+
const triple = any ? catalog5.models[any] : void 0;
|
|
2658
|
+
if (any && triple && triple.length === 3 && triple.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
2659
|
+
const [input, output, cachedInput] = triple;
|
|
2660
|
+
return { input, output, cachedInput, key: any };
|
|
2641
2661
|
}
|
|
2642
|
-
for (const name of skills2) out.push(place(path14.join(root, "skills", name), path14.join(base2, name), opts));
|
|
2643
|
-
}
|
|
2644
|
-
const bundle = path14.join(root, "dist", "pitroom.mjs");
|
|
2645
|
-
if (fs17.existsSync(bundle)) {
|
|
2646
|
-
out.push(placeLauncher(bundle, root, opts.force));
|
|
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
2662
|
}
|
|
2650
|
-
return
|
|
2663
|
+
return void 0;
|
|
2651
2664
|
}
|
|
2652
|
-
function
|
|
2653
|
-
const
|
|
2654
|
-
const
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
fs17.unlinkSync(l);
|
|
2661
|
-
out.push(`\u2714 removed ${l}`);
|
|
2662
|
-
}
|
|
2663
|
-
}
|
|
2664
|
-
}
|
|
2665
|
-
if (isOurLauncher(launcherPath(), root)) {
|
|
2666
|
-
fs17.rmSync(launcherPath(), { force: true });
|
|
2667
|
-
out.push(`\u2714 removed ${launcherPath()}`);
|
|
2665
|
+
function catalogStatus() {
|
|
2666
|
+
const eff = effective();
|
|
2667
|
+
const c = readCatalog();
|
|
2668
|
+
let lastFailure;
|
|
2669
|
+
try {
|
|
2670
|
+
const t = fs16.readFileSync(attemptFile(), "utf8").trim();
|
|
2671
|
+
if (t && !t.endsWith("refresh started")) lastFailure = t;
|
|
2672
|
+
} catch {
|
|
2668
2673
|
}
|
|
2669
|
-
return
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
+
return {
|
|
2675
|
+
enabled: eff.priceFeed.value,
|
|
2676
|
+
url: eff.priceFeedUrl.value,
|
|
2677
|
+
hours: eff.priceFeedHours.value,
|
|
2678
|
+
file: catalogFile(),
|
|
2679
|
+
models: c ? Object.keys(c.models).length : void 0,
|
|
2680
|
+
fetchedAt: c?.fetchedAt,
|
|
2681
|
+
ageHours: c ? (Date.now() - Date.parse(c.fetchedAt)) / 36e5 : void 0,
|
|
2682
|
+
lastFailure
|
|
2683
|
+
};
|
|
2674
2684
|
}
|
|
2675
2685
|
|
|
2676
|
-
// src/core/
|
|
2677
|
-
var
|
|
2678
|
-
|
|
2679
|
-
"
|
|
2680
|
-
"
|
|
2681
|
-
"
|
|
2686
|
+
// src/core/receipt.ts
|
|
2687
|
+
var PRESETS = {
|
|
2688
|
+
sonnet: { name: "Claude Sonnet", input: 3, output: 15, cachedInput: 0.3 },
|
|
2689
|
+
opus: { name: "Claude Opus", input: 5, output: 25, cachedInput: 0.5 },
|
|
2690
|
+
haiku: { name: "Claude Haiku", input: 1, output: 5, cachedInput: 0.1 },
|
|
2691
|
+
"gpt-5": { name: "GPT-5", input: 1.25, output: 10, cachedInput: 0.125 }
|
|
2682
2692
|
};
|
|
2683
|
-
function
|
|
2684
|
-
const
|
|
2685
|
-
|
|
2686
|
-
|
|
2693
|
+
function primaryPrice() {
|
|
2694
|
+
const eff = effective();
|
|
2695
|
+
const custom = eff.price.value?.split(",").map(Number);
|
|
2696
|
+
if (custom && custom.length >= 2 && custom.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
2697
|
+
return { name: "custom", input: custom[0], output: custom[1], cachedInput: custom[2] ?? custom[0] / 10 };
|
|
2698
|
+
}
|
|
2699
|
+
const named = eff.primary.value;
|
|
2700
|
+
const preset = PRESETS[named.toLowerCase()];
|
|
2701
|
+
if (preset) return preset;
|
|
2702
|
+
const c = eff.priceFeed.value ? catalogPrice("", named) : void 0;
|
|
2703
|
+
return c ? { name: named, source: "catalog", input: c.input, output: c.output, cachedInput: c.cachedInput } : PRESETS.sonnet;
|
|
2687
2704
|
}
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2705
|
+
var estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
2706
|
+
function parsePriceSpec(spec, name = "worker") {
|
|
2707
|
+
const parts = typeof spec === "string" ? spec.split(",").map((x) => Number(x.trim())) : [];
|
|
2708
|
+
if (parts.length < 2 || parts.length > 3 || !parts.every((n) => Number.isFinite(n) && n >= 0)) return void 0;
|
|
2709
|
+
return { name, source: "config", input: parts[0], output: parts[1], cachedInput: parts[2] ?? parts[0] / 10 };
|
|
2692
2710
|
}
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
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;
|
|
2711
|
+
function workerPrice(backend, ...models) {
|
|
2712
|
+
const prices = effective().workerPrices.value;
|
|
2713
|
+
for (const key of [...models.filter((m) => !!m).map((m) => `${backend}:${m.split("#")[0]}`), backend]) {
|
|
2714
|
+
if (prices[key] !== void 0) return parsePriceSpec(prices[key], key);
|
|
2710
2715
|
}
|
|
2716
|
+
const c = effective().priceFeed.value ? catalogPrice(backend, ...models) : void 0;
|
|
2717
|
+
return c ? { name: c.key, source: "catalog", input: c.input, output: c.output, cachedInput: c.cachedInput } : void 0;
|
|
2711
2718
|
}
|
|
2712
|
-
|
|
2713
|
-
|
|
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);
|
|
2719
|
+
var costAt = (usage2, price) => (usage2.input * price.input + usage2.cacheRead * price.cachedInput + (usage2.output + usage2.reasoning) * price.output) / 1e6;
|
|
2720
|
+
var workerCostOf = (usage2) => usage2?.cost ?? usage2?.costEstimate;
|
|
2820
2721
|
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
2722
|
const readingTheReport = returnedTokens * price.input / 1e6;
|
|
2823
|
-
return Math.max(0,
|
|
2723
|
+
return Math.max(0, costAt(usage2, price) - (workerCostOf(usage2) ?? 0) - readingTheReport);
|
|
2824
2724
|
}
|
|
2825
2725
|
function record(meta) {
|
|
2826
2726
|
if (!meta.usage?.steps || meta.auditOf) return;
|
|
@@ -2833,17 +2733,18 @@ function record(meta) {
|
|
|
2833
2733
|
model: meta.resolvedModel ?? (meta.ran ?? meta.worker).model,
|
|
2834
2734
|
tokens: meta.usage.total,
|
|
2835
2735
|
returned: meta.returnedTokens ?? 0,
|
|
2836
|
-
workerCost: meta.usage
|
|
2736
|
+
workerCost: workerCostOf(meta.usage) ?? 0,
|
|
2737
|
+
...meta.usage.cost === void 0 && meta.usage.costEstimate !== void 0 ? { costEstimated: true, costSource: meta.usage.costSource } : {},
|
|
2837
2738
|
saved: meta.savedUsd ?? 0,
|
|
2838
2739
|
price: primaryPrice().name
|
|
2839
2740
|
};
|
|
2840
|
-
|
|
2841
|
-
|
|
2741
|
+
fs17.mkdirSync(path15.dirname(ledgerFile()), { recursive: true });
|
|
2742
|
+
fs17.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
|
|
2842
2743
|
`);
|
|
2843
2744
|
}
|
|
2844
2745
|
function readLedger(sinceMs2) {
|
|
2845
|
-
if (!
|
|
2846
|
-
return
|
|
2746
|
+
if (!fs17.existsSync(ledgerFile())) return [];
|
|
2747
|
+
return fs17.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
|
|
2847
2748
|
try {
|
|
2848
2749
|
return [JSON.parse(l)];
|
|
2849
2750
|
} catch {
|
|
@@ -2917,168 +2818,8 @@ ${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.rou
|
|
|
2917
2818
|
`;
|
|
2918
2819
|
}
|
|
2919
2820
|
|
|
2920
|
-
// src/core/refs.ts
|
|
2921
|
-
import fs21 from "node:fs";
|
|
2922
|
-
import path18 from "node:path";
|
|
2923
|
-
var FILE_EXTENSIONS = new Set(
|
|
2924
|
-
"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(" ")
|
|
2925
|
-
);
|
|
2926
|
-
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
2927
|
-
var MAX_INDEXED = 6e4;
|
|
2928
|
-
var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
|
|
2929
|
-
var REF = new RegExp(
|
|
2930
|
-
String.raw`(?<![\w/:.-])(\.{0,2}/?(?:[\w@.+-]+/)*(?:[\w@+-][\w@.+-]*\.[A-Za-z][A-Za-z0-9]{0,7}|${EXTENSIONLESS})):(\d+)(?:[-–](\d+))?`,
|
|
2931
|
-
"g"
|
|
2932
|
-
);
|
|
2933
|
-
var IDENT = /`([A-Za-z_$][\w$]*)(?:\([^`]*\))?`/g;
|
|
2934
|
-
var CLAUSE = /[;|—]|\.\s|:\s/g;
|
|
2935
|
-
var MAX_BYTES = 5 * 1024 * 1024;
|
|
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;
|
|
2941
|
-
}
|
|
2942
|
-
function extractRefs(answer) {
|
|
2943
|
-
const seen = /* @__PURE__ */ new Map();
|
|
2944
|
-
for (const line of answer.split("\n")) {
|
|
2945
|
-
const matches2 = [...line.matchAll(REF)];
|
|
2946
|
-
const seps = [...line.matchAll(CLAUSE)].map((s) => s.index ?? 0);
|
|
2947
|
-
const sepBetween = (a, b) => seps.some((s) => s >= a && s < b);
|
|
2948
|
-
const idents = [...line.matchAll(IDENT)].filter((i) => {
|
|
2949
|
-
if (!looksLikeSymbol(i)) return false;
|
|
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
|
-
});
|
|
2969
|
-
}
|
|
2970
|
-
return [...seen.values()];
|
|
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
2821
|
// src/core/ui.ts
|
|
3081
|
-
import
|
|
2822
|
+
import fs18 from "node:fs";
|
|
3082
2823
|
var WEEK_MS = 7 * 24 * 3600 * 1e3;
|
|
3083
2824
|
var RECENT = 40;
|
|
3084
2825
|
var RECENT_HOURS = 1;
|
|
@@ -3169,9 +2910,9 @@ function cardsFor(m) {
|
|
|
3169
2910
|
const out = [];
|
|
3170
2911
|
for (const [phase, text] of phases) {
|
|
3171
2912
|
const marker = runFile(m.id, `card-${phase}`);
|
|
3172
|
-
if (
|
|
2913
|
+
if (fs18.existsSync(marker)) continue;
|
|
3173
2914
|
if (!(phase === "started" && !isActive(m.state))) out.push(text());
|
|
3174
|
-
|
|
2915
|
+
fs18.writeFileSync(marker, "");
|
|
3175
2916
|
}
|
|
3176
2917
|
return out;
|
|
3177
2918
|
}
|
|
@@ -3182,10 +2923,10 @@ function hookCards(input) {
|
|
|
3182
2923
|
const response = event.tool_response;
|
|
3183
2924
|
const output = typeof response === "string" ? response : [response?.stdout, response?.stderr].filter(Boolean).join("\n");
|
|
3184
2925
|
const ids = [...new Set(`${command}
|
|
3185
|
-
${output}`.match(RUN_ID) ?? [])].filter((id) =>
|
|
2926
|
+
${output}`.match(RUN_ID) ?? [])].filter((id) => fs18.existsSync(`${runsDir()}/${id}`));
|
|
3186
2927
|
const since2 = Date.now() - RECENT_HOURS * 36e5;
|
|
3187
2928
|
for (const id of listRunIds().slice(-RECENT)) {
|
|
3188
|
-
if (ids.includes(id) ||
|
|
2929
|
+
if (ids.includes(id) || fs18.existsSync(runFile(id, "card-ended"))) continue;
|
|
3189
2930
|
try {
|
|
3190
2931
|
const m = readMeta(id);
|
|
3191
2932
|
if (!isActive(m.state) && Date.parse(m.endedAt ?? m.startedAt) > since2) ids.push(id);
|
|
@@ -3206,11 +2947,11 @@ ${output}`.match(RUN_ID) ?? [])].filter((id) => fs22.existsSync(`${runsDir()}/${
|
|
|
3206
2947
|
var SCHEMA_VERSION = 1;
|
|
3207
2948
|
var PATCH_MAX = 1e6;
|
|
3208
2949
|
var ANSWER_MAX2 = 2e5;
|
|
3209
|
-
var
|
|
3210
|
-
var historyFile = () =>
|
|
2950
|
+
var cached3;
|
|
2951
|
+
var historyFile = () => path16.join(home(), "history.db");
|
|
3211
2952
|
function openDb() {
|
|
3212
2953
|
const file2 = historyFile();
|
|
3213
|
-
if (
|
|
2954
|
+
if (cached3?.file === file2) return cached3.db;
|
|
3214
2955
|
let db;
|
|
3215
2956
|
try {
|
|
3216
2957
|
const emit = process.emitWarning;
|
|
@@ -3221,14 +2962,14 @@ function openDb() {
|
|
|
3221
2962
|
} finally {
|
|
3222
2963
|
process.emitWarning = emit;
|
|
3223
2964
|
}
|
|
3224
|
-
|
|
2965
|
+
fs19.mkdirSync(path16.dirname(file2), { recursive: true });
|
|
3225
2966
|
db = new DatabaseSync(file2);
|
|
3226
2967
|
db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
|
|
3227
2968
|
migrate(db);
|
|
3228
2969
|
} catch {
|
|
3229
2970
|
db = void 0;
|
|
3230
2971
|
}
|
|
3231
|
-
|
|
2972
|
+
cached3 = { file: file2, db };
|
|
3232
2973
|
return db;
|
|
3233
2974
|
}
|
|
3234
2975
|
function migrate(db) {
|
|
@@ -3256,31 +2997,31 @@ function migrate(db) {
|
|
|
3256
2997
|
function readRunFile(id, name) {
|
|
3257
2998
|
const f = runFile(id, name);
|
|
3258
2999
|
try {
|
|
3259
|
-
if (
|
|
3260
|
-
if (
|
|
3000
|
+
if (fs19.existsSync(f)) return fs19.readFileSync(f, "utf8");
|
|
3001
|
+
if (fs19.existsSync(`${f}.gz`)) return zlib.gunzipSync(fs19.readFileSync(`${f}.gz`)).toString("utf8");
|
|
3261
3002
|
} catch {
|
|
3262
3003
|
}
|
|
3263
3004
|
return void 0;
|
|
3264
3005
|
}
|
|
3265
3006
|
function gzipFile(f) {
|
|
3266
|
-
if (!
|
|
3267
|
-
|
|
3268
|
-
|
|
3007
|
+
if (!fs19.existsSync(f)) return;
|
|
3008
|
+
fs19.writeFileSync(`${f}.gz`, zlib.gzipSync(fs19.readFileSync(f)));
|
|
3009
|
+
fs19.rmSync(f);
|
|
3269
3010
|
}
|
|
3270
3011
|
function compactRun(meta) {
|
|
3271
3012
|
if (!TERMINAL.includes(meta.state)) return;
|
|
3272
|
-
const dir =
|
|
3013
|
+
const dir = path16.dirname(runFile(meta.id, "meta.json"));
|
|
3273
3014
|
let names;
|
|
3274
3015
|
try {
|
|
3275
|
-
names =
|
|
3016
|
+
names = fs19.readdirSync(dir);
|
|
3276
3017
|
} catch {
|
|
3277
3018
|
return;
|
|
3278
3019
|
}
|
|
3279
3020
|
for (const n of names) {
|
|
3280
|
-
if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(
|
|
3021
|
+
if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(path16.join(dir, n));
|
|
3281
3022
|
else if (/^stderr(\.attempt-\d+)?\.log$/.test(n)) {
|
|
3282
|
-
if (meta.state === "done" && !/attempt/.test(n))
|
|
3283
|
-
else gzipFile(
|
|
3023
|
+
if (meta.state === "done" && !/attempt/.test(n)) fs19.rmSync(path16.join(dir, n), { force: true });
|
|
3024
|
+
else gzipFile(path16.join(dir, n));
|
|
3284
3025
|
}
|
|
3285
3026
|
}
|
|
3286
3027
|
}
|
|
@@ -3324,7 +3065,7 @@ function recordRun(meta) {
|
|
|
3324
3065
|
tool_calls: u?.toolCalls ?? null,
|
|
3325
3066
|
tokens: u?.total ?? null,
|
|
3326
3067
|
returned_tokens: meta.returnedTokens ?? null,
|
|
3327
|
-
cost: u?.cost ?? null,
|
|
3068
|
+
cost: u?.cost ?? u?.costEstimate ?? null,
|
|
3328
3069
|
saved: meta.savedUsd ?? null,
|
|
3329
3070
|
files_changed: meta.changes?.length ?? 0,
|
|
3330
3071
|
applied: meta.applied ? 1 : 0,
|
|
@@ -3489,14 +3230,14 @@ function auditStats(db, since2) {
|
|
|
3489
3230
|
return { total, byWorker };
|
|
3490
3231
|
}
|
|
3491
3232
|
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 }));
|
|
3233
|
+
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
3234
|
const key = (backend, model) => `${backend ?? ""}\0${model ?? ""}`;
|
|
3494
3235
|
const index = new Map(out.map((r) => [key(r.backend, r.model), r]));
|
|
3495
3236
|
for (const e of ledger) {
|
|
3496
3237
|
const backend = e.backend ?? "opencode";
|
|
3497
3238
|
let row = index.get(key(backend, e.model)) ?? (e.model ? out.find((r) => r.backend === backend && r.model?.endsWith(`/${e.model}`)) : void 0);
|
|
3498
3239
|
if (!row) {
|
|
3499
|
-
row = { backend, model: e.model, runs: 0, ok: 0, avgSeconds: null, avgTokens: null, saved: 0, audited: 0, agreed: 0 };
|
|
3240
|
+
row = { backend, model: e.model, runs: 0, ok: 0, limited: 0, avgSeconds: null, avgTokens: null, saved: 0, audited: 0, agreed: 0 };
|
|
3500
3241
|
index.set(key(backend, e.model), row);
|
|
3501
3242
|
out.push(row);
|
|
3502
3243
|
}
|
|
@@ -3508,24 +3249,28 @@ function workerRows(rows, ledger, audited) {
|
|
|
3508
3249
|
}
|
|
3509
3250
|
return out;
|
|
3510
3251
|
}
|
|
3252
|
+
var LIMITED = `(state='failed' AND (json_extract(meta_json,'$.failureKind')='rate-limited' OR error LIKE '%the model is rate-limited or overloaded%'))`;
|
|
3511
3253
|
function historyStats(sinceMs2) {
|
|
3512
|
-
const
|
|
3254
|
+
const counted = effective().countRateLimits.value === true;
|
|
3255
|
+
const K = counted ? "1" : `NOT ${LIMITED}`;
|
|
3256
|
+
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
3257
|
const db = openDb();
|
|
3514
3258
|
if (!db) return empty;
|
|
3515
3259
|
const since2 = sinceMs2 ? new Date(sinceMs2).toISOString() : "";
|
|
3516
3260
|
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,
|
|
3261
|
+
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);
|
|
3262
|
+
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);
|
|
3263
|
+
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
3264
|
const audits = auditStats(db, since2);
|
|
3521
3265
|
const ledger = readLedger(sinceMs2);
|
|
3522
3266
|
const byDay = /* @__PURE__ */ new Map();
|
|
3523
3267
|
for (const e of ledger) byDay.set(e.at.slice(0, 10), (byDay.get(e.at.slice(0, 10)) ?? 0) + e.saved);
|
|
3524
3268
|
return {
|
|
3525
|
-
|
|
3269
|
+
rateLimits: counted ? "counted" : "excluded",
|
|
3270
|
+
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
3271
|
audits: audits.total,
|
|
3527
3272
|
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 }))
|
|
3273
|
+
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
3274
|
};
|
|
3530
3275
|
} catch {
|
|
3531
3276
|
return empty;
|
|
@@ -3533,19 +3278,91 @@ function historyStats(sinceMs2) {
|
|
|
3533
3278
|
}
|
|
3534
3279
|
|
|
3535
3280
|
// src/core/report.ts
|
|
3536
|
-
import
|
|
3281
|
+
import fs21 from "node:fs";
|
|
3537
3282
|
|
|
3538
|
-
// src/core/
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3283
|
+
// src/core/plan.ts
|
|
3284
|
+
import fs20 from "node:fs";
|
|
3285
|
+
import path17 from "node:path";
|
|
3286
|
+
var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
3287
|
+
var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
|
|
3288
|
+
function structure(lines) {
|
|
3289
|
+
const headings = [];
|
|
3290
|
+
const rules = [];
|
|
3291
|
+
const fenced = [];
|
|
3292
|
+
let open;
|
|
3293
|
+
lines.forEach((l, i) => {
|
|
3294
|
+
const f = FENCE.exec(l);
|
|
3295
|
+
if (f) {
|
|
3296
|
+
fenced[i] = true;
|
|
3297
|
+
const mark = f[1];
|
|
3298
|
+
if (!open) open = mark;
|
|
3299
|
+
else if (mark[0] === open[0] && mark.length >= open.length && !f[2].trim()) open = void 0;
|
|
3300
|
+
return;
|
|
3301
|
+
}
|
|
3302
|
+
fenced[i] = !!open;
|
|
3303
|
+
if (open) return;
|
|
3304
|
+
const h = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/.exec(l);
|
|
3305
|
+
if (h) headings.push({ line: i, level: h[1].length, text: h[2] });
|
|
3306
|
+
else if (RULE.test(l)) rules.push(i);
|
|
3307
|
+
});
|
|
3308
|
+
return { headings, rules, fenced };
|
|
3309
|
+
}
|
|
3310
|
+
function parsePlan(text, file2 = "") {
|
|
3311
|
+
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
3312
|
+
const { headings, rules, fenced } = structure(lines);
|
|
3313
|
+
const after = (line) => Math.min(headings.find((h) => h.line > line)?.line ?? lines.length, rules.find((r) => r > line) ?? lines.length);
|
|
3314
|
+
const titleHeading = headings.find((h) => h.level === 1);
|
|
3315
|
+
const start = titleHeading ? titleHeading.line + 1 : 0;
|
|
3316
|
+
const header = lines.slice(start, after(start - 1)).filter((l) => !/^\s*>/.test(l)).join("\n").trim();
|
|
3317
|
+
const gc = headings.find((h) => /^global constraints\b/i.test(h.text));
|
|
3318
|
+
const constraints = gc ? lines.slice(gc.line + 1, after(gc.line)).join("\n").trim() : "";
|
|
3319
|
+
const tasks = [];
|
|
3320
|
+
for (const h of headings) {
|
|
3321
|
+
const m = /^Task\s+(\d+)\b\s*[:.)\-–—]?\s*(.*)$/i.exec(h.text);
|
|
3322
|
+
if (!m) continue;
|
|
3323
|
+
const end = headings.find((o) => o.line > h.line && o.level <= h.level)?.line ?? lines.length;
|
|
3324
|
+
const body = lines.slice(h.line, end).join("\n").replace(/(\n\s*(?:---|\*\*\*|___)\s*)+$/, "").trimEnd();
|
|
3325
|
+
const unfenced = lines.slice(h.line, end).filter((_, j) => !fenced[h.line + j]).join("\n");
|
|
3326
|
+
const tier = /^\s*[-*]?\s*\*\*Worker:\*\*\s*`?([\w-]+)`?/m.exec(unfenced)?.[1]?.toLowerCase();
|
|
3327
|
+
tasks.push({ step: Number(m[1]), title: m[2].trim(), text: body, ...tier ? { tier } : {} });
|
|
3328
|
+
}
|
|
3329
|
+
return { file: file2, title: titleHeading?.text ?? "", header, constraints, tasks };
|
|
3330
|
+
}
|
|
3331
|
+
function loadPlan(file2) {
|
|
3332
|
+
const abs = path17.resolve(file2);
|
|
3333
|
+
if (!fs20.existsSync(abs) || !fs20.statSync(abs).isFile()) throw new UserError(`plan not found: ${file2}`);
|
|
3334
|
+
const plan = parsePlan(fs20.readFileSync(abs, "utf8"), fs20.realpathSync(abs));
|
|
3335
|
+
if (!plan.tasks.length) throw new UserError(`${file2} has no "Task N" headings (see pitroom-writing-plans)`);
|
|
3336
|
+
return plan;
|
|
3337
|
+
}
|
|
3338
|
+
function planTask(plan, step) {
|
|
3339
|
+
const t = plan.tasks.find((x) => x.step === step);
|
|
3340
|
+
if (!t) throw new UserError(`no Task ${step} in ${plan.file}; it has ${plan.tasks.map((x) => `Task ${x.step}`).join(", ")}`);
|
|
3341
|
+
return t;
|
|
3342
|
+
}
|
|
3343
|
+
function brief(plan, task) {
|
|
3344
|
+
return `${[
|
|
3345
|
+
`# ${plan.title || planName(plan.file)}`,
|
|
3346
|
+
plan.header,
|
|
3347
|
+
"## Global Constraints",
|
|
3348
|
+
plan.constraints || "(none stated in the plan)",
|
|
3349
|
+
task.text
|
|
3350
|
+
].filter(Boolean).join("\n\n")}
|
|
3351
|
+
`;
|
|
3352
|
+
}
|
|
3353
|
+
var planName = (file2) => path17.basename(file2).replace(/\.md$/i, "");
|
|
3354
|
+
|
|
3355
|
+
// src/core/style.ts
|
|
3356
|
+
var enabled = () => {
|
|
3357
|
+
if (process.env.NO_COLOR) return false;
|
|
3358
|
+
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") return true;
|
|
3359
|
+
return !!process.stdout.isTTY && process.env.TERM !== "dumb";
|
|
3360
|
+
};
|
|
3361
|
+
var wrap = (open, close) => (text) => enabled() ? `\x1B[${open}m${text}\x1B[${close}m` : text;
|
|
3362
|
+
var bold = wrap(1, 22);
|
|
3363
|
+
var dim = wrap(2, 22);
|
|
3364
|
+
var red = wrap(31, 39);
|
|
3365
|
+
var green = wrap(32, 39);
|
|
3549
3366
|
var yellow = wrap(33, 39);
|
|
3550
3367
|
var blue = wrap(34, 39);
|
|
3551
3368
|
var cyan = wrap(36, 39);
|
|
@@ -3593,7 +3410,7 @@ function duration(meta) {
|
|
|
3593
3410
|
}
|
|
3594
3411
|
function readSummary(meta) {
|
|
3595
3412
|
const f = runFile(meta.id, "summary.md");
|
|
3596
|
-
return
|
|
3413
|
+
return fs21.existsSync(f) ? fs21.readFileSync(f, "utf8").trim() : archivedRun(meta.id)?.answer ?? "";
|
|
3597
3414
|
}
|
|
3598
3415
|
function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
3599
3416
|
const out = [];
|
|
@@ -3636,7 +3453,7 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
|
3636
3453
|
if (u && u.steps) {
|
|
3637
3454
|
const ratio = meta.returnedTokens ? `, ${Math.max(1, Math.round(u.total / meta.returnedTokens))}\xD7 compression` : "";
|
|
3638
3455
|
out.push(
|
|
3639
|
-
`\u2500\u2500 receipt: worker processed ${compact(u.total)} tokens in ${plural(u.steps, "step")} (${plural(u.toolCalls, "tool call")}${u.denied ? `, ${u.denied} blocked` : ""}) \xB7 worker cost ${u.cost
|
|
3456
|
+
`\u2500\u2500 receipt: worker processed ${compact(u.total)} tokens in ${plural(u.steps, "step")} (${plural(u.toolCalls, "tool call")}${u.denied ? `, ${u.denied} blocked` : ""}) \xB7 worker cost ${u.cost !== void 0 ? usd(u.cost) : u.costEstimate !== void 0 ? `~${usd(u.costEstimate)} (estimated from ${u.costSource === "catalog" ? `${catalogLabel()} prices` : "your workerPrices"})` : "n/a"} \xB7 returned ~${compact(meta.returnedTokens ?? 0)} tokens${ratio}` + (meta.savedUsd !== void 0 ? ` \xB7 est. saved ${usd(meta.savedUsd)} vs ${primaryPrice().name}` : "")
|
|
3640
3457
|
);
|
|
3641
3458
|
}
|
|
3642
3459
|
if (meta.changes) {
|
|
@@ -3654,44 +3471,659 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
|
3654
3471
|
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
3472
|
);
|
|
3656
3473
|
}
|
|
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}`);
|
|
3474
|
+
if (meta.changes.length) {
|
|
3475
|
+
out.push(` diff: pitroom show ${meta.id} --patch`);
|
|
3476
|
+
out.push(` review: pitroom review ${meta.id}`);
|
|
3477
|
+
if (meta.mode === "isolate" && !meta.applied && !meta.discarded) {
|
|
3478
|
+
out.push(` apply: pitroom apply ${meta.id}`, ` discard: pitroom discard ${meta.id}`);
|
|
3479
|
+
}
|
|
3480
|
+
if (meta.mode === "write" && !meta.reverted) out.push(` undo: pitroom revert ${meta.id}`);
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
if (meta.verifyResult) {
|
|
3484
|
+
const v = meta.verifyResult;
|
|
3485
|
+
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);
|
|
3486
|
+
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})`}`);
|
|
3487
|
+
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");
|
|
3488
|
+
if (!v.ok && v.tail) out.push(...v.tail.split("\n").map((l) => ` ${l}`));
|
|
3489
|
+
}
|
|
3490
|
+
if (isActive(meta.state)) {
|
|
3491
|
+
out.push(`\u2500\u2500 still running: pitroom wait ${meta.id} (stop: pitroom stop ${meta.id})`);
|
|
3492
|
+
} else if (meta.sessionId && !meta.applied && !meta.discarded && !meta.reviewOf) {
|
|
3493
|
+
out.push(`\u2500\u2500 follow up: pitroom run --continue ${meta.id} "\u2026"`);
|
|
3494
|
+
}
|
|
3495
|
+
return out.join("\n");
|
|
3496
|
+
}
|
|
3497
|
+
function live(meta) {
|
|
3498
|
+
const f = runFile(meta.id, "events.jsonl");
|
|
3499
|
+
if (!fs21.existsSync(f)) return { steps: 0, toolCalls: 0 };
|
|
3500
|
+
const p = getBackend((meta.ran ?? meta.worker).backend).parse(fs21.readFileSync(f, "utf8"));
|
|
3501
|
+
return { steps: p.usage.steps, toolCalls: p.usage.toolCalls, last: p.lastActivity };
|
|
3502
|
+
}
|
|
3503
|
+
function progress(meta) {
|
|
3504
|
+
if (meta.state === "queued") return `pitroom \u22EF queued \xB7 ${meta.mode} \xB7 waiting for a free worker slot \xB7 run ${meta.id}`;
|
|
3505
|
+
const l = live(meta);
|
|
3506
|
+
const last = l.last ? ` \xB7 last: ${l.last}` : "";
|
|
3507
|
+
return `pitroom \u2026 running \xB7 ${meta.mode} \xB7 ${duration(meta)} \xB7 ${l.steps} steps, ${l.toolCalls} tool calls${last} \xB7 run ${meta.id}`;
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
// src/core/group.ts
|
|
3511
|
+
function groupIds(group) {
|
|
3512
|
+
return listRunIds().filter((id) => {
|
|
3513
|
+
try {
|
|
3514
|
+
return readMeta(id).group === group;
|
|
3515
|
+
} catch {
|
|
3516
|
+
return false;
|
|
3517
|
+
}
|
|
3518
|
+
});
|
|
3519
|
+
}
|
|
3520
|
+
var oneLine5 = (s, max) => {
|
|
3521
|
+
const t = s.replace(/\s+/g, " ").trim();
|
|
3522
|
+
return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
|
|
3523
|
+
};
|
|
3524
|
+
function headline(meta, max = 160) {
|
|
3525
|
+
const text = readSummary(meta);
|
|
3526
|
+
const line = text.split("\n").find((l) => /^SUMMARY:/i.test(l.trim())) ?? text.split("\n").find((l) => l.trim()) ?? "";
|
|
3527
|
+
return oneLine5(line.replace(/^SUMMARY:\s*/i, ""), max);
|
|
3528
|
+
}
|
|
3529
|
+
function table(metas) {
|
|
3530
|
+
if (!metas.length) return "no runs";
|
|
3531
|
+
const rows = metas.map((m) => {
|
|
3532
|
+
const l = m.state === "running" ? live(m) : void 0;
|
|
3533
|
+
const steps = l ? String(l.steps) : m.usage ? String(m.usage.steps) : "-";
|
|
3534
|
+
const note = l?.last ? oneLine5(l.last, 48) : isActive(m.state) ? oneLine5(m.task, 48) : headline(m, 48) || oneLine5(m.error ?? m.task, 48);
|
|
3535
|
+
return [m.id, m.state, m.mode, duration(m), steps, describeTarget(m.ran ?? m.worker), note];
|
|
3536
|
+
});
|
|
3537
|
+
const head = ["RUN", "STATE", "MODE", "TIME", "STEPS", "WORKER", "NOW / RESULT"];
|
|
3538
|
+
const widths = head.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
3539
|
+
const fmt = (r, header = false) => r.map((c, i) => {
|
|
3540
|
+
const cell2 = i === r.length - 1 ? c : c.padEnd(widths[i]);
|
|
3541
|
+
return header ? dim(cell2) : i === 1 ? stateColour(r[1], cell2) : cell2;
|
|
3542
|
+
}).join(" ");
|
|
3543
|
+
return [fmt(head, true), ...rows.map((r) => fmt(r))].join("\n");
|
|
3544
|
+
}
|
|
3545
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3546
|
+
async function waitMany(ids, opts) {
|
|
3547
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
3548
|
+
for (; ; ) {
|
|
3549
|
+
const metas = ids.map((id) => freshMeta(id));
|
|
3550
|
+
const done = metas.filter((m) => TERMINAL.includes(m.state)).length;
|
|
3551
|
+
if (opts.any ? done > 0 : done === metas.length) return { metas, timedOut: false };
|
|
3552
|
+
if (Date.now() >= deadline) return { metas, timedOut: true };
|
|
3553
|
+
await sleep(1500);
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
async function watch(select, opts) {
|
|
3557
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3558
|
+
const line = (s) => opts.write(`${s}
|
|
3559
|
+
`);
|
|
3560
|
+
const emit = (e, m) => {
|
|
3561
|
+
if (!opts.brief) return opts.write(`${JSON.stringify(e)}
|
|
3562
|
+
`);
|
|
3563
|
+
if (e.event === "started" && m) line(startedCard(m));
|
|
3564
|
+
else if (e.event === "fallback" && m) line(`\u{1F3C1} Pitroom \u21BB ${m.id} fell back from ${String(e.failed)}: ${String(e.reason)}`);
|
|
3565
|
+
else if (m && TERMINAL.includes(m.state)) line(endedCard(m));
|
|
3566
|
+
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` : ""})`);
|
|
3567
|
+
};
|
|
3568
|
+
const deadline = opts.timeoutMs > 0 ? Date.now() + opts.timeoutMs : Infinity;
|
|
3569
|
+
for (; ; ) {
|
|
3570
|
+
const metas = select().map((id) => freshMeta(id));
|
|
3571
|
+
if (opts.json || opts.brief) {
|
|
3572
|
+
for (const m of metas) {
|
|
3573
|
+
const prev = seen.get(m.id);
|
|
3574
|
+
const l = m.state === "running" ? live(m) : { steps: m.usage?.steps ?? 0, toolCalls: m.usage?.toolCalls ?? 0 };
|
|
3575
|
+
const attempts = m.attempts?.length ?? 0;
|
|
3576
|
+
const base2 = { run: m.id, mode: m.mode, task: oneLine5(m.task, 60) };
|
|
3577
|
+
if (!prev) emit({ event: m.state === "queued" ? "queued" : "started", ...base2 }, m);
|
|
3578
|
+
else if (prev.state === "queued" && m.state === "running") emit({ event: "started", ...base2 }, m);
|
|
3579
|
+
if (attempts > (prev?.attempts ?? 0)) {
|
|
3580
|
+
const a = m.attempts[attempts - 1];
|
|
3581
|
+
emit({ event: "fallback", run: m.id, failed: a.target, reason: oneLine5(a.error, 120) }, m);
|
|
3582
|
+
}
|
|
3583
|
+
if (m.state === "running" && l.steps > (prev?.steps ?? 0)) {
|
|
3584
|
+
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 });
|
|
3585
|
+
}
|
|
3586
|
+
if (TERMINAL.includes(m.state) && prev?.state !== m.state) {
|
|
3587
|
+
emit({
|
|
3588
|
+
event: m.state,
|
|
3589
|
+
run: m.id,
|
|
3590
|
+
time: duration(m),
|
|
3591
|
+
worker: describeTarget(m.ran ?? m.worker),
|
|
3592
|
+
summary: headline(m) || void 0,
|
|
3593
|
+
error: m.error ? oneLine5(m.error, 160) : void 0,
|
|
3594
|
+
refs: m.refs ? `${m.refs.valid}/${m.refs.total}` : void 0,
|
|
3595
|
+
changes: m.changes?.length || void 0
|
|
3596
|
+
}, m);
|
|
3597
|
+
}
|
|
3598
|
+
seen.set(m.id, { state: m.state, steps: l.steps, attempts });
|
|
3599
|
+
}
|
|
3600
|
+
} else {
|
|
3601
|
+
opts.write(`\x1B[H\x1B[2J${table(metas)}
|
|
3602
|
+
|
|
3603
|
+
${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \xB7 Ctrl-C to stop watching (workers keep running)
|
|
3604
|
+
`);
|
|
3605
|
+
}
|
|
3606
|
+
const allDone = metas.length > 0 && metas.every((m) => TERMINAL.includes(m.state));
|
|
3607
|
+
if (allDone || metas.length === 0 && seen.size === 0) {
|
|
3608
|
+
if (opts.json || opts.brief) {
|
|
3609
|
+
const ok = metas.filter((m) => m.state === "done").length;
|
|
3610
|
+
emit({ event: "all-done", runs: metas.length, ok, failed: metas.length - ok });
|
|
3611
|
+
}
|
|
3612
|
+
return { metas, timedOut: false };
|
|
3613
|
+
}
|
|
3614
|
+
if (Date.now() >= deadline) return { metas, timedOut: true };
|
|
3615
|
+
await sleep(opts.intervalMs);
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
// src/core/notify.ts
|
|
3620
|
+
var MARK = { done: "\u2714", failed: "\u2718", timeout: "\u23F1", stopped: "\u25A0" };
|
|
3621
|
+
function noticeFor(meta) {
|
|
3622
|
+
const group = meta.group ? safeGroup(meta.group) : [];
|
|
3623
|
+
if (group.length > 1) {
|
|
3624
|
+
const count = (state) => group.filter((m) => m.state === state).length;
|
|
3625
|
+
const worse = group.length - count("done");
|
|
3626
|
+
const parts = [`${count("done")} done`, ...["failed", "timeout", "stopped"].filter(count).map((s) => `${count(s)} ${s}`)];
|
|
3627
|
+
const changed2 = group.reduce((n, m) => n + (m.changes?.length ?? 0), 0);
|
|
3628
|
+
return {
|
|
3629
|
+
title: `Pitroom ${worse ? "\u26A0" : "\u2714"} group ${meta.group}`,
|
|
3630
|
+
body: `${group.length} runs ended: ${parts.join(", ")}${changed2 ? ` \xB7 ${changed2} file${changed2 === 1 ? "" : "s"} changed` : ""}`
|
|
3631
|
+
};
|
|
3632
|
+
}
|
|
3633
|
+
const changed = meta.changes?.length ? ` \xB7 ${meta.changes.length} file${meta.changes.length === 1 ? "" : "s"} changed` : "";
|
|
3634
|
+
return { title: `Pitroom ${MARK[meta.state] ?? "\u2022"} ${kind(meta)} ${meta.state}`, body: `${what(meta)} \xB7 ${elapsed(meta)}${changed}` };
|
|
3635
|
+
}
|
|
3636
|
+
function safeGroup(name) {
|
|
3637
|
+
try {
|
|
3638
|
+
return groupIds(name).map((id) => readMeta(id));
|
|
3639
|
+
} catch {
|
|
3640
|
+
return [];
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
function shouldNotify(meta) {
|
|
3644
|
+
const eff = effective();
|
|
3645
|
+
if (!eff.notify.value || !meta.background || meta.auditOf || !TERMINAL.includes(meta.state)) return false;
|
|
3646
|
+
const runs = meta.group ? safeGroup(meta.group) : [meta];
|
|
3647
|
+
if (!runs.length || !runs.every((m) => TERMINAL.includes(m.state))) return false;
|
|
3648
|
+
if (runs.every((m) => m.state === "done" || m.state === "stopped")) {
|
|
3649
|
+
const start = Math.min(...runs.map((m) => Date.parse(m.startedAt)));
|
|
3650
|
+
const end = Math.max(...runs.map((m) => Date.parse(m.endedAt ?? "")));
|
|
3651
|
+
if (Number.isFinite(end - start) && (end - start) / 1e3 < eff.notifyAfter.value) return false;
|
|
3652
|
+
if (runs.every((m) => m.state === "stopped")) return false;
|
|
3653
|
+
}
|
|
3654
|
+
return true;
|
|
3655
|
+
}
|
|
3656
|
+
function claimGroup(group) {
|
|
3657
|
+
try {
|
|
3658
|
+
const ids = groupIds(group).sort();
|
|
3659
|
+
if (ids.length < 2) return true;
|
|
3660
|
+
const dir = path18.join(home(), "notified");
|
|
3661
|
+
fs22.mkdirSync(dir, { recursive: true });
|
|
3662
|
+
for (const f of fs22.readdirSync(dir)) {
|
|
3663
|
+
if (Date.now() - fs22.statSync(path18.join(dir, f)).mtimeMs > 30 * 864e5) fs22.rmSync(path18.join(dir, f), { force: true });
|
|
3664
|
+
}
|
|
3665
|
+
fs22.writeFileSync(path18.join(dir, crypto5.createHash("sha1").update(ids.join(",")).digest("hex").slice(0, 16)), "", { flag: "wx" });
|
|
3666
|
+
return true;
|
|
3667
|
+
} catch {
|
|
3668
|
+
return false;
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
function send(notice, meta) {
|
|
3672
|
+
const command = effective().notifyCommand.value;
|
|
3673
|
+
const env = { ...process.env, PITROOM_NOTIFY_TITLE: notice.title, PITROOM_NOTIFY_BODY: notice.body, PITROOM_NOTIFY_RUN: meta.id, PITROOM_NOTIFY_STATE: meta.state };
|
|
3674
|
+
try {
|
|
3675
|
+
let child;
|
|
3676
|
+
if (command) {
|
|
3677
|
+
child = spawn3(command, { shell: true, env, detached: true, stdio: "ignore" });
|
|
3678
|
+
} else if (process.platform === "darwin") {
|
|
3679
|
+
child = spawn3("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" });
|
|
3680
|
+
} else if (process.platform === "linux") {
|
|
3681
|
+
const plain2 = (t) => t.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3682
|
+
child = spawn3("notify-send", ["--", plain2(notice.title), plain2(notice.body)], { env, detached: true, stdio: "ignore" });
|
|
3683
|
+
} else {
|
|
3684
|
+
return;
|
|
3685
|
+
}
|
|
3686
|
+
child.on("error", () => void 0);
|
|
3687
|
+
child.unref();
|
|
3688
|
+
} catch {
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
function notifyEnded(meta) {
|
|
3692
|
+
try {
|
|
3693
|
+
if (shouldNotify(meta) && (!meta.group || claimGroup(meta.group))) send(noticeFor(meta), meta);
|
|
3694
|
+
} catch {
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
|
|
3698
|
+
// src/core/templates.ts
|
|
3699
|
+
import fs24 from "node:fs";
|
|
3700
|
+
import path20 from "node:path";
|
|
3701
|
+
|
|
3702
|
+
// src/core/install.ts
|
|
3703
|
+
import fs23 from "node:fs";
|
|
3704
|
+
import os9 from "node:os";
|
|
3705
|
+
import path19 from "node:path";
|
|
3706
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3707
|
+
var LEGACY = ["pitroom", "opencode-worker"];
|
|
3708
|
+
function packageRoot() {
|
|
3709
|
+
return path19.resolve(path19.dirname(fileURLToPath2(import.meta.url)), "..");
|
|
3710
|
+
}
|
|
3711
|
+
function skillNames(root = packageRoot()) {
|
|
3712
|
+
const dir = path19.join(root, "skills");
|
|
3713
|
+
if (!fs23.existsSync(dir)) return [];
|
|
3714
|
+
return fs23.readdirSync(dir).filter((d) => fs23.existsSync(path19.join(dir, d, "SKILL.md"))).sort();
|
|
3715
|
+
}
|
|
3716
|
+
function skillTargets() {
|
|
3717
|
+
const home3 = os9.homedir();
|
|
3718
|
+
const targets = [path19.join(home3, ".agents", "skills")];
|
|
3719
|
+
if (fs23.existsSync(path19.join(home3, ".claude"))) targets.push(path19.join(home3, ".claude", "skills"));
|
|
3720
|
+
return targets;
|
|
3721
|
+
}
|
|
3722
|
+
var launcherPath = () => path19.join(os9.homedir(), ".local", "bin", process.platform === "win32" ? "pitroom.cmd" : "pitroom");
|
|
3723
|
+
var LAUNCHER_MARK = "# pitroom launcher";
|
|
3724
|
+
function launcherScript(bundle) {
|
|
3725
|
+
if (process.platform === "win32") {
|
|
3726
|
+
const q2 = (v) => v.replace(/%/g, "%%");
|
|
3727
|
+
return `@echo off\r
|
|
3728
|
+
rem ${LAUNCHER_MARK} (created by \`pitroom install\`; \`pitroom uninstall\` removes it)\r
|
|
3729
|
+
if defined PITROOM_NODE (\r
|
|
3730
|
+
"%PITROOM_NODE%" "${q2(bundle)}" %*\r
|
|
3731
|
+
) else (\r
|
|
3732
|
+
"${q2(process.execPath)}" "${q2(bundle)}" %*\r
|
|
3733
|
+
)\r
|
|
3734
|
+
exit /b %ERRORLEVEL%\r
|
|
3735
|
+
`;
|
|
3736
|
+
}
|
|
3737
|
+
const q = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
3738
|
+
return `#!/bin/sh
|
|
3739
|
+
${LAUNCHER_MARK} (created by \`pitroom install\`; \`pitroom uninstall\` removes it)
|
|
3740
|
+
cli=${q(bundle)}
|
|
3741
|
+
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; }
|
|
3742
|
+
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
|
|
3743
|
+
if ok "$n"; then exec "$n" "$cli" "$@"; fi
|
|
3744
|
+
done
|
|
3745
|
+
echo "pitroom: needs Node.js 22.13 or newer (set PITROOM_NODE to its path)" >&2
|
|
3746
|
+
exit 127
|
|
3747
|
+
`;
|
|
3748
|
+
}
|
|
3749
|
+
function isOurLauncher(file2, root) {
|
|
3750
|
+
if (linksInto(file2, root)) return true;
|
|
3751
|
+
try {
|
|
3752
|
+
const s = fs23.readFileSync(file2, "utf8");
|
|
3753
|
+
return s.includes(LAUNCHER_MARK) && s.includes(root);
|
|
3754
|
+
} catch {
|
|
3755
|
+
return false;
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
function isPitroomLauncher(file2) {
|
|
3759
|
+
try {
|
|
3760
|
+
return fs23.readFileSync(file2, "utf8").includes(LAUNCHER_MARK);
|
|
3761
|
+
} catch {
|
|
3762
|
+
return false;
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
function placeLauncher(bundle, root, force) {
|
|
3766
|
+
const dest = launcherPath();
|
|
3767
|
+
fs23.mkdirSync(path19.dirname(dest), { recursive: true });
|
|
3768
|
+
const exists = fs23.lstatSync(dest, { throwIfNoEntry: false });
|
|
3769
|
+
if (exists && !isOurLauncher(dest, root)) {
|
|
3770
|
+
if (!force) return `! ${dest} exists and is not Pitroom's launcher; kept (use --force to back it up and replace)`;
|
|
3771
|
+
fs23.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
3772
|
+
} else if (exists) {
|
|
3773
|
+
fs23.rmSync(dest, { force: true });
|
|
3774
|
+
}
|
|
3775
|
+
fs23.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
|
|
3776
|
+
return `\u2714 ${dest} \u2192 launcher for ${bundle} (Node 22.13+)`;
|
|
3777
|
+
}
|
|
3778
|
+
function linksInto(link, dir) {
|
|
3779
|
+
const st = fs23.lstatSync(link, { throwIfNoEntry: false });
|
|
3780
|
+
if (!st?.isSymbolicLink()) return false;
|
|
3781
|
+
const target = path19.resolve(path19.dirname(link), fs23.readlinkSync(link));
|
|
3782
|
+
const rel = path19.relative(dir, target);
|
|
3783
|
+
return !rel.startsWith("..") && !path19.isAbsolute(rel);
|
|
3784
|
+
}
|
|
3785
|
+
function place(src, dest, opts) {
|
|
3786
|
+
fs23.mkdirSync(path19.dirname(dest), { recursive: true });
|
|
3787
|
+
const st = fs23.lstatSync(dest, { throwIfNoEntry: false });
|
|
3788
|
+
if (st && !st.isSymbolicLink()) {
|
|
3789
|
+
if (!opts.force) return `! ${dest} exists and is not a link; kept (use --force to back it up and replace)`;
|
|
3790
|
+
fs23.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
3791
|
+
} else if (st) {
|
|
3792
|
+
fs23.unlinkSync(dest);
|
|
3793
|
+
}
|
|
3794
|
+
if (opts.copy) fs23.cpSync(src, dest, { recursive: true });
|
|
3795
|
+
else fs23.symlinkSync(src, dest, process.platform === "win32" ? "junction" : fs23.statSync(src).isDirectory() ? "dir" : "file");
|
|
3796
|
+
return `\u2714 ${dest} \u2192 ${opts.copy ? "copied" : src}`;
|
|
3797
|
+
}
|
|
3798
|
+
function install(opts) {
|
|
3799
|
+
const root = packageRoot();
|
|
3800
|
+
const skills2 = opts.skills === false ? [] : skillNames(root);
|
|
3801
|
+
if (opts.skills !== false && !skills2.length) throw new Error(`no skills found under ${path19.join(root, "skills")}`);
|
|
3802
|
+
const out = [];
|
|
3803
|
+
for (const base2 of opts.skills === false ? [] : skillTargets()) {
|
|
3804
|
+
for (const legacy of LEGACY) {
|
|
3805
|
+
const l = path19.join(base2, legacy);
|
|
3806
|
+
if (!skills2.includes(legacy) && linksInto(l, root)) {
|
|
3807
|
+
fs23.unlinkSync(l);
|
|
3808
|
+
out.push(`\u2714 removed old link ${l}`);
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
for (const name of skills2) out.push(place(path19.join(root, "skills", name), path19.join(base2, name), opts));
|
|
3812
|
+
}
|
|
3813
|
+
const bundle = path19.join(root, "dist", "pitroom.mjs");
|
|
3814
|
+
if (fs23.existsSync(bundle)) {
|
|
3815
|
+
out.push(placeLauncher(bundle, root, opts.force));
|
|
3816
|
+
const onPath = (process.env.PATH ?? "").split(path19.delimiter).some((d) => path19.resolve(d) === path19.dirname(launcherPath()));
|
|
3817
|
+
if (!onPath) out.push(`! ${path19.dirname(launcherPath())} is not on PATH; add it, or run ${launcherPath()} directly`);
|
|
3818
|
+
}
|
|
3819
|
+
return out;
|
|
3820
|
+
}
|
|
3821
|
+
function uninstall() {
|
|
3822
|
+
const root = packageRoot();
|
|
3823
|
+
const out = [];
|
|
3824
|
+
for (const base2 of skillTargets()) {
|
|
3825
|
+
if (!fs23.existsSync(base2)) continue;
|
|
3826
|
+
for (const name of fs23.readdirSync(base2)) {
|
|
3827
|
+
const l = path19.join(base2, name);
|
|
3828
|
+
if (linksInto(l, root)) {
|
|
3829
|
+
fs23.unlinkSync(l);
|
|
3830
|
+
out.push(`\u2714 removed ${l}`);
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
if (isOurLauncher(launcherPath(), root)) {
|
|
3835
|
+
fs23.rmSync(launcherPath(), { force: true });
|
|
3836
|
+
out.push(`\u2714 removed ${launcherPath()}`);
|
|
3837
|
+
}
|
|
3838
|
+
return out.length ? out : ["nothing to remove"];
|
|
3839
|
+
}
|
|
3840
|
+
function installedSkills() {
|
|
3841
|
+
const names = skillNames();
|
|
3842
|
+
return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs23.existsSync(path19.join(base2, n, "SKILL.md"))) }));
|
|
3843
|
+
}
|
|
3844
|
+
|
|
3845
|
+
// src/core/templates.ts
|
|
3846
|
+
var FILES = {
|
|
3847
|
+
implementer: "pitroom-driven-development/implementer-prompt.md",
|
|
3848
|
+
"task-reviewer": "pitroom-driven-development/task-reviewer-prompt.md",
|
|
3849
|
+
"re-review": "pitroom-driven-development/re-review-prompt.md",
|
|
3850
|
+
"code-reviewer": "pitroom-review/code-reviewer.md"
|
|
3851
|
+
};
|
|
3852
|
+
function loadTemplate(name, root = packageRoot()) {
|
|
3853
|
+
const file2 = path20.join(root, "skills", FILES[name]);
|
|
3854
|
+
if (!fs24.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
|
|
3855
|
+
return fs24.readFileSync(file2, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
|
|
3856
|
+
}
|
|
3857
|
+
function fill(template, values) {
|
|
3858
|
+
const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
|
|
3859
|
+
if (missing.length) throw new UserError(`no value for template placeholder(s): ${[...new Set(missing)].join(", ")}`, 3);
|
|
3860
|
+
return template.replace(/\{\{([A-Z_]+)\}\}/g, (_, key) => values[key]);
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3863
|
+
// src/core/slots.ts
|
|
3864
|
+
import crypto6 from "node:crypto";
|
|
3865
|
+
import fs25 from "node:fs";
|
|
3866
|
+
import path21 from "node:path";
|
|
3867
|
+
var slotsDir = () => path21.join(home(), "slots");
|
|
3868
|
+
var locksDir = () => path21.join(home(), "locks");
|
|
3869
|
+
var STARTUP_GRACE_MS = 3e4;
|
|
3870
|
+
function holderActive(runId, self) {
|
|
3871
|
+
if (!runId || runId === self) return false;
|
|
3872
|
+
try {
|
|
3873
|
+
const m = freshMeta(runId);
|
|
3874
|
+
if (!isActive(m.state)) return false;
|
|
3875
|
+
if (m.pid) return isAlive(m.pid);
|
|
3876
|
+
return Date.now() - Date.parse(m.startedAt) < STARTUP_GRACE_MS;
|
|
3877
|
+
} catch {
|
|
3878
|
+
return false;
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
function tryClaim(file2, runId) {
|
|
3882
|
+
fs25.mkdirSync(path21.dirname(file2), { recursive: true });
|
|
3883
|
+
try {
|
|
3884
|
+
fs25.writeFileSync(file2, runId, { flag: "wx" });
|
|
3885
|
+
return true;
|
|
3886
|
+
} catch (e) {
|
|
3887
|
+
if (e.code !== "EEXIST") throw e;
|
|
3888
|
+
}
|
|
3889
|
+
let owner = "";
|
|
3890
|
+
try {
|
|
3891
|
+
owner = fs25.readFileSync(file2, "utf8").trim();
|
|
3892
|
+
} catch {
|
|
3893
|
+
}
|
|
3894
|
+
if (owner === runId) return true;
|
|
3895
|
+
if (holderActive(owner, runId)) return false;
|
|
3896
|
+
fs25.rmSync(file2, { force: true });
|
|
3897
|
+
try {
|
|
3898
|
+
fs25.writeFileSync(file2, runId, { flag: "wx" });
|
|
3899
|
+
return true;
|
|
3900
|
+
} catch {
|
|
3901
|
+
return false;
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
function releaseIfOwner(file2, runId) {
|
|
3905
|
+
if (!file2) return;
|
|
3906
|
+
try {
|
|
3907
|
+
if (fs25.readFileSync(file2, "utf8").trim() === runId) fs25.rmSync(file2, { force: true });
|
|
3908
|
+
} catch {
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
function tryAcquireSlot(runId, maxParallel) {
|
|
3912
|
+
for (let n = 0; n < Math.max(1, maxParallel); n++) {
|
|
3913
|
+
const file2 = path21.join(slotsDir(), `slot-${n}`);
|
|
3914
|
+
if (tryClaim(file2, runId)) return file2;
|
|
3915
|
+
}
|
|
3916
|
+
return void 0;
|
|
3917
|
+
}
|
|
3918
|
+
var releaseSlot = (file2, runId) => releaseIfOwner(file2, runId);
|
|
3919
|
+
var lockFile = (repoRoot2) => path21.join(locksDir(), `write-${crypto6.createHash("sha1").update(path21.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
|
|
3920
|
+
function acquireWriteLock(repoRoot2, runId) {
|
|
3921
|
+
const file2 = lockFile(repoRoot2);
|
|
3922
|
+
if (tryClaim(file2, runId)) return;
|
|
3923
|
+
let owner = "";
|
|
3924
|
+
try {
|
|
3925
|
+
owner = fs25.readFileSync(file2, "utf8").trim();
|
|
3926
|
+
} catch {
|
|
3927
|
+
}
|
|
3928
|
+
throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
|
|
3929
|
+
}
|
|
3930
|
+
var releaseWriteLock = (repoRoot2, runId) => releaseIfOwner(lockFile(repoRoot2), runId);
|
|
3931
|
+
|
|
3932
|
+
// src/core/prompt.ts
|
|
3933
|
+
var RULES = {
|
|
3934
|
+
read: ["READ-ONLY task: do not create, modify or delete any file. Read, search and analyse only."],
|
|
3935
|
+
write: [
|
|
3936
|
+
"You may edit files that the task requires. Make the smallest correct change; no drive-by refactors.",
|
|
3937
|
+
"Run the relevant tests, type checks or linters if they are cheap and available, and report the result."
|
|
3938
|
+
],
|
|
3939
|
+
isolate: [
|
|
3940
|
+
"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.",
|
|
3941
|
+
"Make the smallest correct change; no drive-by refactors. Dependency folders (node_modules, virtualenvs, build output) may be missing unless linked.",
|
|
3942
|
+
"Run the relevant tests, type checks or linters if they are cheap and available, and report the result."
|
|
3943
|
+
]
|
|
3944
|
+
};
|
|
3945
|
+
function buildPrompt(mode, task, followUp) {
|
|
3946
|
+
if (followUp) return `Follow-up from the primary agent. Same rules and answer format as before.
|
|
3947
|
+
|
|
3948
|
+
${task}`;
|
|
3949
|
+
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.
|
|
3950
|
+
|
|
3951
|
+
Rules:
|
|
3952
|
+
- ${RULES[mode].join("\n- ")}
|
|
3953
|
+
- Stay inside the task scope and the project directory.
|
|
3954
|
+
- Never commit, push, reset, checkout, restore, stash, clean, rebase or merge, and never discard or overwrite uncommitted work you did not create.
|
|
3955
|
+
- Never read or reveal secrets (.env files, keys, tokens).
|
|
3956
|
+
- 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.
|
|
3957
|
+
- Do not ask questions. If something is ambiguous, choose the safest reasonable interpretation and state the assumption.
|
|
3958
|
+
- Be economical: open only what you need.
|
|
3959
|
+
|
|
3960
|
+
End with this answer format (plain text, no preamble):
|
|
3961
|
+
SUMMARY: 1-5 lines that directly answer the task.
|
|
3962
|
+
DETAILS: key findings with file:line references; only what the primary agent needs.
|
|
3963
|
+
FILES CHANGED: paths, or "none".
|
|
3964
|
+
VERIFICATION: commands you ran and their results, or "not run".
|
|
3965
|
+
OPEN ISSUES: risks, uncertainties, follow-ups, or "none".
|
|
3966
|
+
|
|
3967
|
+
Task:
|
|
3968
|
+
${task}`;
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
// src/core/refs.ts
|
|
3972
|
+
import fs26 from "node:fs";
|
|
3973
|
+
import path22 from "node:path";
|
|
3974
|
+
var FILE_EXTENSIONS = new Set(
|
|
3975
|
+
"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(" ")
|
|
3976
|
+
);
|
|
3977
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
3978
|
+
var MAX_INDEXED = 6e4;
|
|
3979
|
+
var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
|
|
3980
|
+
var REF = new RegExp(
|
|
3981
|
+
String.raw`(?<![\w/:.-])(\.{0,2}/?(?:[\w@.+-]+/)*(?:[\w@+-][\w@.+-]*\.[A-Za-z][A-Za-z0-9]{0,7}|${EXTENSIONLESS})):(\d+)(?:[-–](\d+))?`,
|
|
3982
|
+
"g"
|
|
3983
|
+
);
|
|
3984
|
+
var IDENT = /`([A-Za-z_$][\w$]*)(?:\([^`]*\))?`/g;
|
|
3985
|
+
var CLAUSE = /[;|—]|\.\s|:\s/g;
|
|
3986
|
+
var MAX_BYTES = 5 * 1024 * 1024;
|
|
3987
|
+
var WINDOW = 5;
|
|
3988
|
+
var DEFINITION_LOOKBACK = 80;
|
|
3989
|
+
function looksLikeSymbol(m) {
|
|
3990
|
+
const name = m[1];
|
|
3991
|
+
return m[0].includes("(") || /[A-Z_$0-9]/.test(name) || name.length >= 5;
|
|
3992
|
+
}
|
|
3993
|
+
function extractRefs(answer) {
|
|
3994
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3995
|
+
for (const line of answer.split("\n")) {
|
|
3996
|
+
const matches2 = [...line.matchAll(REF)];
|
|
3997
|
+
const seps = [...line.matchAll(CLAUSE)].map((s) => s.index ?? 0);
|
|
3998
|
+
const sepBetween = (a, b) => seps.some((s) => s >= a && s < b);
|
|
3999
|
+
const idents = [...line.matchAll(IDENT)].filter((i) => {
|
|
4000
|
+
if (!looksLikeSymbol(i)) return false;
|
|
4001
|
+
const pos = i.index ?? 0;
|
|
4002
|
+
const before = matches2.filter((m) => (m.index ?? 0) + m[0].length <= pos).at(-1);
|
|
4003
|
+
const after = matches2.find((m) => (m.index ?? 0) >= pos + i[0].length);
|
|
4004
|
+
return !(before && after && !sepBetween((before.index ?? 0) + before[0].length, after.index ?? 0));
|
|
4005
|
+
});
|
|
4006
|
+
matches2.forEach((m, k) => {
|
|
4007
|
+
const start = Number(m[2]);
|
|
4008
|
+
const end = m[3] ? Number(m[3]) : start;
|
|
4009
|
+
if (start < 1 || end < start) return;
|
|
4010
|
+
const at = m.index ?? 0;
|
|
4011
|
+
const prev = matches2[k - 1];
|
|
4012
|
+
const next = matches2[k + 1];
|
|
4013
|
+
const from = Math.max(at - 80, prev ? (prev.index ?? 0) + prev[0].length : 0, ...seps.filter((s) => s < at).map((s) => s + 1));
|
|
4014
|
+
const to = Math.min(at + m[0].length + 40, next?.index ?? Infinity, ...seps.filter((s) => s >= at + m[0].length));
|
|
4015
|
+
const near = new Set(idents.filter((i) => (i.index ?? 0) >= from && (i.index ?? 0) < to).map((i) => i[1]));
|
|
4016
|
+
const symbol = near.size === 1 ? [...near][0] : void 0;
|
|
4017
|
+
const key = `${m[1]}:${start}-${end}:${symbol ?? ""}`;
|
|
4018
|
+
if (!seen.has(key)) seen.set(key, { text: m[0], file: m[1], start, end, symbol });
|
|
4019
|
+
});
|
|
4020
|
+
}
|
|
4021
|
+
return [...seen.values()];
|
|
4022
|
+
}
|
|
4023
|
+
function verifyRefs(refs, dirs) {
|
|
4024
|
+
const roots = [...new Set(dirs.filter(Boolean).map((d) => path22.resolve(d)))];
|
|
4025
|
+
const invalid = [];
|
|
4026
|
+
const lineCache = /* @__PURE__ */ new Map();
|
|
4027
|
+
const load = (file2) => {
|
|
4028
|
+
if (!lineCache.has(file2)) {
|
|
4029
|
+
const size = fs26.statSync(file2).size;
|
|
4030
|
+
const lines = size > MAX_BYTES ? null : fs26.readFileSync(file2, "utf8").split("\n");
|
|
4031
|
+
if (lines && lines[lines.length - 1] === "") lines.pop();
|
|
4032
|
+
lineCache.set(file2, lines);
|
|
4033
|
+
}
|
|
4034
|
+
return lineCache.get(file2);
|
|
4035
|
+
};
|
|
4036
|
+
let index;
|
|
4037
|
+
const byName = () => index ??= indexFiles(roots);
|
|
4038
|
+
const candidates = (ref) => {
|
|
4039
|
+
const direct = resolve(ref.file, roots);
|
|
4040
|
+
if (direct) return [direct];
|
|
4041
|
+
if (path22.isAbsolute(ref.file)) return [];
|
|
4042
|
+
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
4043
|
+
return (byName().get(path22.basename(wanted)) ?? []).filter((f) => f.split(path22.sep).join("/").endsWith(`/${wanted}`) || path22.basename(f) === wanted).slice(0, 20);
|
|
4044
|
+
};
|
|
4045
|
+
const check = (file2, ref) => {
|
|
4046
|
+
const lines = load(file2);
|
|
4047
|
+
if (!lines) return void 0;
|
|
4048
|
+
if (ref.end > lines.length) return `file has ${lines.length} lines`;
|
|
4049
|
+
if (ref.symbol && !mentions(lines, ref) && !enclosedBy(lines, ref)) return `\`${ref.symbol}\` not near line ${ref.start}`;
|
|
4050
|
+
return void 0;
|
|
4051
|
+
};
|
|
4052
|
+
let total = 0;
|
|
4053
|
+
for (const ref of refs) {
|
|
4054
|
+
const files = candidates(ref);
|
|
4055
|
+
if (!files.length) {
|
|
4056
|
+
if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
|
|
4057
|
+
total++;
|
|
4058
|
+
invalid.push({ ref: ref.text, reason: path22.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
|
|
4059
|
+
continue;
|
|
4060
|
+
}
|
|
4061
|
+
total++;
|
|
4062
|
+
const reasons = files.map((f) => check(f, ref));
|
|
4063
|
+
if (!reasons.some((r) => r === void 0)) invalid.push({ ref: ref.text, reason: files.length > 1 ? `${reasons[0]} (${files.length} files match)` : reasons[0] });
|
|
4064
|
+
}
|
|
4065
|
+
return { total, valid: total - invalid.length, invalid };
|
|
4066
|
+
}
|
|
4067
|
+
function mentions(lines, ref) {
|
|
4068
|
+
const near = lines.slice(Math.max(0, ref.start - 1 - WINDOW), ref.end + WINDOW).join("\n");
|
|
4069
|
+
return new RegExp(`(^|[^\\w$])${escape(ref.symbol)}([^\\w$]|$)`).test(near);
|
|
4070
|
+
}
|
|
4071
|
+
function enclosedBy(lines, ref) {
|
|
4072
|
+
const name = escape(ref.symbol);
|
|
4073
|
+
const def = new RegExp(
|
|
4074
|
+
`\\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*[(:=]`
|
|
4075
|
+
);
|
|
4076
|
+
return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
|
|
4077
|
+
}
|
|
4078
|
+
var isFileName = (name) => {
|
|
4079
|
+
const base2 = path22.basename(name);
|
|
4080
|
+
return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
|
|
4081
|
+
};
|
|
4082
|
+
function indexFiles(roots) {
|
|
4083
|
+
const index = /* @__PURE__ */ new Map();
|
|
4084
|
+
let count = 0;
|
|
4085
|
+
const walk = (dir) => {
|
|
4086
|
+
let entries;
|
|
4087
|
+
try {
|
|
4088
|
+
entries = fs26.readdirSync(dir, { withFileTypes: true });
|
|
4089
|
+
} catch {
|
|
4090
|
+
return;
|
|
4091
|
+
}
|
|
4092
|
+
for (const e of entries) {
|
|
4093
|
+
if (count >= MAX_INDEXED) return;
|
|
4094
|
+
if (e.isDirectory()) {
|
|
4095
|
+
if (!SKIP_DIRS2.has(e.name)) walk(path22.join(dir, e.name));
|
|
4096
|
+
} else if (e.isFile()) {
|
|
4097
|
+
count++;
|
|
4098
|
+
const list2 = index.get(e.name);
|
|
4099
|
+
if (list2) list2.push(path22.join(dir, e.name));
|
|
4100
|
+
else index.set(e.name, [path22.join(dir, e.name)]);
|
|
3662
4101
|
}
|
|
3663
|
-
if (meta.mode === "write" && !meta.reverted) out.push(` undo: pitroom revert ${meta.id}`);
|
|
3664
4102
|
}
|
|
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");
|
|
4103
|
+
};
|
|
4104
|
+
for (const r of roots) walk(r);
|
|
4105
|
+
return index;
|
|
3679
4106
|
}
|
|
3680
|
-
function
|
|
3681
|
-
const
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
4107
|
+
function resolve(ref, roots) {
|
|
4108
|
+
const candidates = path22.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path22.join(r, ref));
|
|
4109
|
+
return candidates.find((c) => {
|
|
4110
|
+
try {
|
|
4111
|
+
return fs26.statSync(c).isFile() && inside(c, roots);
|
|
4112
|
+
} catch {
|
|
4113
|
+
return false;
|
|
4114
|
+
}
|
|
4115
|
+
});
|
|
3685
4116
|
}
|
|
3686
|
-
function
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
4117
|
+
function inside(file2, roots) {
|
|
4118
|
+
return roots.some((r) => {
|
|
4119
|
+
const rel = path22.relative(r, path22.resolve(file2));
|
|
4120
|
+
return rel !== "" && !rel.startsWith("..") && !path22.isAbsolute(rel);
|
|
4121
|
+
});
|
|
3691
4122
|
}
|
|
4123
|
+
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3692
4124
|
|
|
3693
4125
|
// src/core/run.ts
|
|
3694
|
-
var VERSION2 = true ? "0.
|
|
4126
|
+
var VERSION2 = true ? "0.21.0" : "0.0.0-dev";
|
|
3695
4127
|
function planWork(o) {
|
|
3696
4128
|
if (!o.plan) return { task: o.task.trim(), tier: o.tier };
|
|
3697
4129
|
if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
|
|
@@ -3723,7 +4155,7 @@ function prepareRun(o) {
|
|
|
3723
4155
|
parent = readMeta(o.continueFrom);
|
|
3724
4156
|
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
4157
|
if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
|
|
3726
|
-
if (parent.mode === "isolate" && (!parent.worktree || !
|
|
4158
|
+
if (parent.mode === "isolate" && (!parent.worktree || !fs27.existsSync(parent.worktree))) {
|
|
3727
4159
|
throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
|
|
3728
4160
|
}
|
|
3729
4161
|
const ran = parent.ran ?? parent.worker;
|
|
@@ -3742,18 +4174,18 @@ function prepareRun(o) {
|
|
|
3742
4174
|
throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
|
|
3743
4175
|
}
|
|
3744
4176
|
const mode = parent?.mode ?? o.mode;
|
|
3745
|
-
const dir = canonical(
|
|
3746
|
-
if (!
|
|
4177
|
+
const dir = canonical(path23.resolve(parent?.dir ?? o.dir));
|
|
4178
|
+
if (!fs27.existsSync(dir) || !fs27.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
|
|
3747
4179
|
const root = repoRoot(dir);
|
|
3748
4180
|
if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
|
|
3749
4181
|
if (mode === "write" && !root && !o.allowNonGit) {
|
|
3750
4182
|
throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
|
|
3751
4183
|
}
|
|
3752
|
-
const files = o.files.map((f) =>
|
|
3753
|
-
for (const f of files) if (!
|
|
4184
|
+
const files = o.files.map((f) => path23.resolve(f));
|
|
4185
|
+
for (const f of files) if (!fs27.existsSync(f)) throw new UserError(`file not found: ${f}`);
|
|
3754
4186
|
const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
|
|
3755
4187
|
const snap = mode === "read" && !o.review && !o.inPlace && !parent ? wantSnapshot(readIn2, root, dir) : void 0;
|
|
3756
|
-
if (parent?.snapshot?.dir && !
|
|
4188
|
+
if (parent?.snapshot?.dir && !fs27.existsSync(parent.snapshot.dir)) {
|
|
3757
4189
|
throw new UserError(`the read snapshot of run ${parent.id} is gone (cleaned up); ask the question again as a new run`, 3);
|
|
3758
4190
|
}
|
|
3759
4191
|
if (!parent && !snap && !process.env.PITROOM_NO_SECRET_WARNING) {
|
|
@@ -3800,16 +4232,16 @@ function prepareRun(o) {
|
|
|
3800
4232
|
try {
|
|
3801
4233
|
acquireWriteLock(root, meta.id);
|
|
3802
4234
|
} catch (e) {
|
|
3803
|
-
|
|
4235
|
+
fs27.rmSync(runDir(meta.id), { recursive: true, force: true });
|
|
3804
4236
|
throw e;
|
|
3805
4237
|
}
|
|
3806
4238
|
}
|
|
3807
|
-
|
|
4239
|
+
fs27.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
|
|
3808
4240
|
`);
|
|
3809
|
-
if (work.brief)
|
|
4241
|
+
if (work.brief) fs27.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
|
|
3810
4242
|
return meta;
|
|
3811
4243
|
}
|
|
3812
|
-
var
|
|
4244
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3813
4245
|
async function waitForSlot(meta) {
|
|
3814
4246
|
let stopped = false;
|
|
3815
4247
|
const stop = () => {
|
|
@@ -3823,7 +4255,7 @@ async function waitForSlot(meta) {
|
|
|
3823
4255
|
const slot = tryAcquireSlot(meta.id, max);
|
|
3824
4256
|
if (slot) return slot;
|
|
3825
4257
|
if (stopped) return void 0;
|
|
3826
|
-
await
|
|
4258
|
+
await sleep2(1e3);
|
|
3827
4259
|
if (stopped) return void 0;
|
|
3828
4260
|
}
|
|
3829
4261
|
} finally {
|
|
@@ -3834,7 +4266,9 @@ async function waitForSlot(meta) {
|
|
|
3834
4266
|
function startInBackground(meta) {
|
|
3835
4267
|
const script = process.argv[1];
|
|
3836
4268
|
if (!script) throw new Error("cannot locate the pitroom executable");
|
|
3837
|
-
|
|
4269
|
+
meta.background = true;
|
|
4270
|
+
writeMeta(meta);
|
|
4271
|
+
const child = spawn4(process.execPath, [script, "__exec", meta.id], {
|
|
3838
4272
|
detached: true,
|
|
3839
4273
|
stdio: "ignore",
|
|
3840
4274
|
cwd: meta.dir,
|
|
@@ -3886,7 +4320,7 @@ async function execute(meta) {
|
|
|
3886
4320
|
(meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
|
|
3887
4321
|
for (const f of ["events.jsonl", "stderr.log"]) {
|
|
3888
4322
|
const from = runFile(meta.id, f);
|
|
3889
|
-
if (
|
|
4323
|
+
if (fs27.existsSync(from)) fs27.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
|
|
3890
4324
|
}
|
|
3891
4325
|
writeMeta(meta);
|
|
3892
4326
|
}
|
|
@@ -3950,8 +4384,8 @@ function prepareTree(meta) {
|
|
|
3950
4384
|
const s = readSnapshot(root);
|
|
3951
4385
|
meta.snapshot.dir = s.dir;
|
|
3952
4386
|
meta.snapshot.tree = s.tree;
|
|
3953
|
-
meta.cwd =
|
|
3954
|
-
if (!
|
|
4387
|
+
meta.cwd = path23.join(s.dir, path23.relative(root, meta.dir));
|
|
4388
|
+
if (!fs27.existsSync(meta.cwd)) meta.cwd = s.dir;
|
|
3955
4389
|
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
4390
|
} catch (e) {
|
|
3957
4391
|
meta.snapshot = void 0;
|
|
@@ -3961,9 +4395,9 @@ function prepareTree(meta) {
|
|
|
3961
4395
|
if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
|
|
3962
4396
|
if (root && meta.mode === "isolate" && !meta.worktree) {
|
|
3963
4397
|
meta.baseTree = snapshotTree(root);
|
|
3964
|
-
meta.worktree =
|
|
4398
|
+
meta.worktree = path23.join(worktreesDir(), meta.id);
|
|
3965
4399
|
createIsolatedCopy(root, meta.baseTree, meta.worktree);
|
|
3966
|
-
meta.cwd =
|
|
4400
|
+
meta.cwd = path23.join(meta.worktree, path23.relative(root, meta.dir));
|
|
3967
4401
|
const linked = linkIntoWorktree(root, meta.worktree, meta.link);
|
|
3968
4402
|
if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
|
|
3969
4403
|
}
|
|
@@ -3986,7 +4420,7 @@ function attempt(meta, backend, target) {
|
|
|
3986
4420
|
timeoutSec: meta.timeoutSec
|
|
3987
4421
|
});
|
|
3988
4422
|
}
|
|
3989
|
-
var read2 = (f) =>
|
|
4423
|
+
var read2 = (f) => fs27.existsSync(f) ? fs27.readFileSync(f, "utf8") : "";
|
|
3990
4424
|
function retryableFailure(meta, backend, res) {
|
|
3991
4425
|
const run2 = backend.parse(read2(runFile(meta.id, "events.jsonl")));
|
|
3992
4426
|
if (run2.usage.steps > 0) return void 0;
|
|
@@ -4006,16 +4440,19 @@ function finalize(meta, res) {
|
|
|
4006
4440
|
if (meta.snapshot?.dir && meta.repoRoot) run2.finalText = backToProject(run2.finalText, meta.snapshot.dir, meta.repoRoot);
|
|
4007
4441
|
meta.sessionId = run2.sessionId ?? meta.sessionId;
|
|
4008
4442
|
meta.usage = run2.usage;
|
|
4009
|
-
|
|
4443
|
+
fs27.writeFileSync(runFile(meta.id, "summary.md"), `${run2.finalText}
|
|
4010
4444
|
`);
|
|
4011
4445
|
if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run2.finalText);
|
|
4012
4446
|
if (meta.reviewOf) {
|
|
4013
4447
|
meta.verdict = parseVerdict(run2.finalText);
|
|
4014
|
-
if (meta.packageFile)
|
|
4448
|
+
if (meta.packageFile) fs27.rmSync(meta.packageFile, { force: true });
|
|
4015
4449
|
}
|
|
4016
4450
|
if (!res.timedOut && !res.stopped && !meta.error) {
|
|
4017
4451
|
const f = backend.failure(run2, read2(runFile(meta.id, "stderr.log")), res.code);
|
|
4018
|
-
if (f)
|
|
4452
|
+
if (f) {
|
|
4453
|
+
meta.error = HINTS[f.kind] ? `${f.message} (${HINTS[f.kind]})` : f.message;
|
|
4454
|
+
meta.failureKind = f.kind;
|
|
4455
|
+
}
|
|
4019
4456
|
}
|
|
4020
4457
|
try {
|
|
4021
4458
|
captureChanges(meta);
|
|
@@ -4043,12 +4480,21 @@ function finalize(meta, res) {
|
|
|
4043
4480
|
if (meta.state === "done" && !run2.finalText) meta.warnings.push("worker finished without a written answer");
|
|
4044
4481
|
if (meta.state === "done" && meta.verify) meta.verifyResult = runVerify(meta);
|
|
4045
4482
|
meta.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4483
|
+
if (meta.usage && meta.usage.cost === void 0) {
|
|
4484
|
+
const price = workerPrice(ran.backend, meta.resolvedModel ?? run2.model, ran.model);
|
|
4485
|
+
if (price) {
|
|
4486
|
+
meta.usage.costEstimate = costAt(meta.usage, price);
|
|
4487
|
+
meta.usage.costSource = price.source ?? "config";
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4046
4490
|
meta.returnedTokens = estimateTokens(formatReport(meta, run2.finalText));
|
|
4047
4491
|
meta.savedUsd = meta.auditOf ? 0 : savedUsd(meta.usage, meta.returnedTokens);
|
|
4048
4492
|
writeMeta(meta);
|
|
4049
4493
|
if (meta.mode === "write" && meta.repoRoot) releaseWriteLock(meta.repoRoot, meta.id);
|
|
4050
4494
|
record(meta);
|
|
4051
4495
|
if (meta.auditOf) settleAudit(meta);
|
|
4496
|
+
notifyEnded(meta);
|
|
4497
|
+
refreshInBackground();
|
|
4052
4498
|
return meta;
|
|
4053
4499
|
}
|
|
4054
4500
|
function captureChanges(meta) {
|
|
@@ -4059,13 +4505,13 @@ function captureChanges(meta) {
|
|
|
4059
4505
|
const d = diffTrees(target, meta.baseTree, meta.afterTree);
|
|
4060
4506
|
meta.changes = d.changes;
|
|
4061
4507
|
meta.stats = d.stats;
|
|
4062
|
-
|
|
4508
|
+
fs27.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
|
|
4063
4509
|
}
|
|
4064
4510
|
function verifyPath() {
|
|
4065
|
-
const extra = [
|
|
4066
|
-
const parts = (process.env.PATH ?? "").split(
|
|
4067
|
-
for (const d of process.platform === "win32" ? [
|
|
4068
|
-
return parts.join(
|
|
4511
|
+
const extra = [path23.dirname(process.execPath), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
|
|
4512
|
+
const parts = (process.env.PATH ?? "").split(path23.delimiter).filter(Boolean);
|
|
4513
|
+
for (const d of process.platform === "win32" ? [path23.dirname(process.execPath)] : extra) if (!parts.includes(d)) parts.push(d);
|
|
4514
|
+
return parts.join(path23.delimiter);
|
|
4069
4515
|
}
|
|
4070
4516
|
function runVerify(meta) {
|
|
4071
4517
|
const r = spawnSync6(meta.verify, {
|
|
@@ -4078,7 +4524,7 @@ function runVerify(meta) {
|
|
|
4078
4524
|
env: { ...process.env, PATH: verifyPath(), PWD: meta.cwd, PITROOM_ACTIVE: "1" }
|
|
4079
4525
|
});
|
|
4080
4526
|
const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
|
4081
|
-
|
|
4527
|
+
fs27.writeFileSync(runFile(meta.id, "verify.log"), output);
|
|
4082
4528
|
const notFound = process.platform === "win32" && r.status === 1 && /is not recognized as an internal or external command/.test(output);
|
|
4083
4529
|
return { ok: r.status === 0, code: notFound ? 127 : r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
|
|
4084
4530
|
}
|
|
@@ -4133,126 +4579,15 @@ function discardRun(meta) {
|
|
|
4133
4579
|
return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
|
|
4134
4580
|
}
|
|
4135
4581
|
function cleanupWorktree(meta) {
|
|
4136
|
-
if (meta.worktree &&
|
|
4582
|
+
if (meta.worktree && fs27.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
|
|
4137
4583
|
}
|
|
4138
4584
|
|
|
4139
4585
|
// 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
|
-
}
|
|
4586
|
+
import fs28 from "node:fs";
|
|
4587
|
+
import path24 from "node:path";
|
|
4254
4588
|
|
|
4255
4589
|
// src/cli/mcp-support.ts
|
|
4590
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
4256
4591
|
var MAX_OUTPUT = 12e4;
|
|
4257
4592
|
var DEFAULT_WAIT = 50;
|
|
4258
4593
|
var MAX_WAIT = 540;
|
|
@@ -4320,7 +4655,7 @@ function pit(args, timeoutMs = 60 * 6e4, signal) {
|
|
|
4320
4655
|
return new Promise((resolve2) => {
|
|
4321
4656
|
const script = process.argv[1];
|
|
4322
4657
|
if (!script) return resolve2({ code: 1, out: "", err: "cannot locate the pitroom executable" });
|
|
4323
|
-
const child =
|
|
4658
|
+
const child = spawn5(process.execPath, [script, ...args], { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
4324
4659
|
let out = "";
|
|
4325
4660
|
let err = "";
|
|
4326
4661
|
child.stdout.on("data", (d) => out += d);
|
|
@@ -4379,19 +4714,54 @@ async function stopQuietly(args) {
|
|
|
4379
4714
|
if (r.code !== 0) process.stderr.write(`pitroom mcp: ${args.join(" ")} failed: ${r.err || r.out}
|
|
4380
4715
|
`);
|
|
4381
4716
|
}
|
|
4717
|
+
var dashAsked;
|
|
4718
|
+
function ensureDash() {
|
|
4719
|
+
if (effective().mcpDash.value === false) return Promise.resolve(void 0);
|
|
4720
|
+
if (dashAsked && Date.now() - dashAsked.at < 6e4) return dashAsked.url;
|
|
4721
|
+
const url = pit(["dash", "--detach"], 3e4).then((r) => {
|
|
4722
|
+
const found = /^https?:\/\/127\.0\.0\.1:\d+\/?/m.exec(r.out)?.[0];
|
|
4723
|
+
if (!found) dashAsked = void 0;
|
|
4724
|
+
return found;
|
|
4725
|
+
});
|
|
4726
|
+
dashAsked = { at: Date.now(), url };
|
|
4727
|
+
return url;
|
|
4728
|
+
}
|
|
4729
|
+
function dashIfSlow(ids, delayMs = 3e3) {
|
|
4730
|
+
return new Promise((resolve2) => {
|
|
4731
|
+
const timer2 = setTimeout(() => {
|
|
4732
|
+
try {
|
|
4733
|
+
resolve2(ids().some((id) => isActive(freshMeta(id).state)) ? ensureDash() : void 0);
|
|
4734
|
+
} catch {
|
|
4735
|
+
resolve2(void 0);
|
|
4736
|
+
}
|
|
4737
|
+
}, delayMs);
|
|
4738
|
+
timer2.unref();
|
|
4739
|
+
});
|
|
4740
|
+
}
|
|
4741
|
+
function withLiveView(result, url) {
|
|
4742
|
+
if (!url || !result.text.includes("Not finished yet")) return result;
|
|
4743
|
+
return {
|
|
4744
|
+
...result,
|
|
4745
|
+
text: `${result.text}
|
|
4746
|
+
|
|
4747
|
+
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.`
|
|
4748
|
+
};
|
|
4749
|
+
}
|
|
4382
4750
|
async function startAndWait(start, seconds, ctx, task) {
|
|
4383
4751
|
const started = await pit([...start, "--bg", "--json", ...task === void 0 ? [] : ["--", task]]);
|
|
4384
4752
|
if (started.code !== 0) return asResult(started);
|
|
4385
4753
|
let id;
|
|
4386
|
-
let
|
|
4754
|
+
let cached4 = false;
|
|
4387
4755
|
try {
|
|
4388
|
-
({ id, cached:
|
|
4756
|
+
({ id, cached: cached4 = false } = JSON.parse(started.out));
|
|
4389
4757
|
} catch {
|
|
4390
4758
|
return { text: `could not read the run id from: ${started.out.slice(0, 200)}`, isError: true };
|
|
4391
4759
|
}
|
|
4392
|
-
const
|
|
4393
|
-
|
|
4394
|
-
|
|
4760
|
+
const dash = cached4 ? Promise.resolve(void 0) : dashIfSlow(() => [id]);
|
|
4761
|
+
const waited = await waitFor([id], seconds, ctx);
|
|
4762
|
+
const result = withLiveView(waited, waited.text.includes("Not finished yet") ? await dash : void 0);
|
|
4763
|
+
if (cancelled(ctx) && !cached4) await stopQuietly(["stop", id]);
|
|
4764
|
+
if (cached4) result.text = `Cached answer: the same question on the same code as run ${id}; no worker ran (fresh: true asks one).
|
|
4395
4765
|
|
|
4396
4766
|
${result.text}`;
|
|
4397
4767
|
return result;
|
|
@@ -4405,7 +4775,9 @@ async function startCrew(flags, tasks, seconds, ctx) {
|
|
|
4405
4775
|
} catch {
|
|
4406
4776
|
return { text: `could not read the group from: ${started.out.slice(0, 200)}`, isError: true };
|
|
4407
4777
|
}
|
|
4408
|
-
const
|
|
4778
|
+
const dash = dashIfSlow(() => groupIds(group));
|
|
4779
|
+
const waited = await waitFor([], seconds, ctx, group);
|
|
4780
|
+
const result = withLiveView(waited, waited.text.includes("Not finished yet") ? await dash : void 0);
|
|
4409
4781
|
if (cancelled(ctx)) await stopQuietly(["stop", "-g", group]);
|
|
4410
4782
|
return result;
|
|
4411
4783
|
}
|
|
@@ -4497,7 +4869,7 @@ function skillPrompts() {
|
|
|
4497
4869
|
const root = packageRoot();
|
|
4498
4870
|
for (const name of skillNames(root)) {
|
|
4499
4871
|
try {
|
|
4500
|
-
const text =
|
|
4872
|
+
const text = fs28.readFileSync(path24.join(root, "skills", name, "SKILL.md"), "utf8");
|
|
4501
4873
|
const front = /^---\n([\s\S]*?)\n---\n/.exec(text);
|
|
4502
4874
|
const body = (front ? text.slice(front[0].length) : text).trim();
|
|
4503
4875
|
const description = /^description:\s*(.+)$/m.exec(front?.[1] ?? "")?.[1]?.trim() ?? `The ${name} skill.`;
|
|
@@ -4648,7 +5020,8 @@ var TOOLS2 = [
|
|
|
4648
5020
|
const runs = strs(a, "runs");
|
|
4649
5021
|
const group = str(a, "group");
|
|
4650
5022
|
if (!runs.length && !group) throw new ToolError('give "runs" or "group"');
|
|
4651
|
-
|
|
5023
|
+
const result = await waitFor(runs, waitSeconds(a), ctx, runs.length ? void 0 : group);
|
|
5024
|
+
return withLiveView(result, result.text.includes("Not finished yet") ? await ensureDash() : void 0);
|
|
4652
5025
|
}
|
|
4653
5026
|
},
|
|
4654
5027
|
{
|
|
@@ -4861,7 +5234,7 @@ var PROTOCOLS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
|
4861
5234
|
var INSTRUCTIONS = [
|
|
4862
5235
|
"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
5236
|
'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
|
-
|
|
5237
|
+
`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
5238
|
"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
5239
|
"It is optional: for a small task you can do yourself, skip it."
|
|
4867
5240
|
].join(" ");
|
|
@@ -4983,10 +5356,10 @@ var parseError = () => ({ jsonrpc: "2.0", id: null, error: { code: -32700, messa
|
|
|
4983
5356
|
async function serveMcp() {
|
|
4984
5357
|
process.stdout.on("error", () => {
|
|
4985
5358
|
});
|
|
4986
|
-
const
|
|
5359
|
+
const send2 = (msg) => void process.stdout.write(`${JSON.stringify(msg)}
|
|
4987
5360
|
`);
|
|
4988
5361
|
const session = newSession();
|
|
4989
|
-
session.push =
|
|
5362
|
+
session.push = send2;
|
|
4990
5363
|
const rl = readline.createInterface({ input: process.stdin });
|
|
4991
5364
|
const pending = /* @__PURE__ */ new Set();
|
|
4992
5365
|
const handleLine = async (line) => {
|
|
@@ -4994,11 +5367,11 @@ async function serveMcp() {
|
|
|
4994
5367
|
try {
|
|
4995
5368
|
parsed = JSON.parse(line);
|
|
4996
5369
|
} catch {
|
|
4997
|
-
|
|
5370
|
+
send2(parseError());
|
|
4998
5371
|
return;
|
|
4999
5372
|
}
|
|
5000
|
-
const reply2 = await handlePayload(session, parsed,
|
|
5001
|
-
if (reply2)
|
|
5373
|
+
const reply2 = await handlePayload(session, parsed, send2);
|
|
5374
|
+
if (reply2) send2(reply2);
|
|
5002
5375
|
};
|
|
5003
5376
|
rl.on("line", (line) => {
|
|
5004
5377
|
if (!line.trim()) return;
|
|
@@ -5013,23 +5386,23 @@ async function serveMcp() {
|
|
|
5013
5386
|
}
|
|
5014
5387
|
|
|
5015
5388
|
// src/cli/mcp-http.ts
|
|
5016
|
-
import
|
|
5017
|
-
import
|
|
5389
|
+
import crypto8 from "node:crypto";
|
|
5390
|
+
import fs30 from "node:fs";
|
|
5018
5391
|
import http2 from "node:http";
|
|
5019
|
-
import
|
|
5392
|
+
import path26 from "node:path";
|
|
5020
5393
|
|
|
5021
5394
|
// src/core/dash.ts
|
|
5022
|
-
import { spawn as
|
|
5023
|
-
import
|
|
5024
|
-
import
|
|
5395
|
+
import { spawn as spawn6 } from "node:child_process";
|
|
5396
|
+
import crypto7 from "node:crypto";
|
|
5397
|
+
import fs29 from "node:fs";
|
|
5025
5398
|
import http from "node:http";
|
|
5026
|
-
import
|
|
5399
|
+
import path25 from "node:path";
|
|
5027
5400
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
5028
5401
|
|
|
5029
5402
|
// src/core/file-diff.ts
|
|
5030
5403
|
var PREVIEW_LINES = 300;
|
|
5031
5404
|
function gitPath(value, prefix = true) {
|
|
5032
|
-
let
|
|
5405
|
+
let path34 = value;
|
|
5033
5406
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
5034
5407
|
const bytes = [];
|
|
5035
5408
|
const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
|
|
@@ -5037,9 +5410,9 @@ function gitPath(value, prefix = true) {
|
|
|
5037
5410
|
if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
|
|
5038
5411
|
else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
|
|
5039
5412
|
}
|
|
5040
|
-
|
|
5413
|
+
path34 = new TextDecoder().decode(new Uint8Array(bytes));
|
|
5041
5414
|
}
|
|
5042
|
-
return prefix ?
|
|
5415
|
+
return prefix ? path34.replace(/^[ab]\//, "") : path34;
|
|
5043
5416
|
}
|
|
5044
5417
|
function headerPath(header) {
|
|
5045
5418
|
const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
|
|
@@ -5264,7 +5637,8 @@ function runDetail(id) {
|
|
|
5264
5637
|
toolCalls: u?.toolCalls,
|
|
5265
5638
|
tokens: u?.total,
|
|
5266
5639
|
returnedTokens: m.returnedTokens,
|
|
5267
|
-
cost: u?.cost,
|
|
5640
|
+
cost: u?.cost ?? u?.costEstimate,
|
|
5641
|
+
costEstimated: u?.cost === void 0 && u?.costEstimate !== void 0 ? 1 : void 0,
|
|
5268
5642
|
saved: m.savedUsd,
|
|
5269
5643
|
group: m.group,
|
|
5270
5644
|
directory: m.dir
|
|
@@ -5283,18 +5657,18 @@ var HEADERS = {
|
|
|
5283
5657
|
"x-content-type-options": "nosniff",
|
|
5284
5658
|
"referrer-policy": "no-referrer",
|
|
5285
5659
|
// 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-${
|
|
5660
|
+
"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
5661
|
};
|
|
5288
5662
|
var ASSETS = { "/assets/app.js": "text/javascript; charset=utf-8", "/assets/app.css": "text/css; charset=utf-8" };
|
|
5289
5663
|
var assetsDir = () => fileURLToPath3(new URL("./ui/", import.meta.url));
|
|
5290
5664
|
var assetCache = /* @__PURE__ */ new Map();
|
|
5291
5665
|
function readAsset(route) {
|
|
5292
5666
|
try {
|
|
5293
|
-
const file2 =
|
|
5294
|
-
const { mtimeMs } =
|
|
5667
|
+
const file2 = path25.join(assetsDir(), path25.basename(route));
|
|
5668
|
+
const { mtimeMs } = fs29.statSync(file2);
|
|
5295
5669
|
const hit = assetCache.get(route);
|
|
5296
5670
|
if (hit && hit.mtimeMs === mtimeMs) return hit.data;
|
|
5297
|
-
const data =
|
|
5671
|
+
const data = fs29.readFileSync(file2);
|
|
5298
5672
|
assetCache.set(route, { mtimeMs, data });
|
|
5299
5673
|
return data;
|
|
5300
5674
|
} catch {
|
|
@@ -5302,16 +5676,16 @@ function readAsset(route) {
|
|
|
5302
5676
|
}
|
|
5303
5677
|
}
|
|
5304
5678
|
function handler(touch2, token) {
|
|
5305
|
-
const
|
|
5679
|
+
const send2 = (res, code, type, body) => {
|
|
5306
5680
|
res.writeHead(code, { ...HEADERS, "content-type": type });
|
|
5307
5681
|
res.end(body);
|
|
5308
5682
|
};
|
|
5309
|
-
const json = (res, code, value) =>
|
|
5683
|
+
const json = (res, code, value) => send2(res, code, "application/json; charset=utf-8", JSON.stringify(value));
|
|
5310
5684
|
const tokenBuf = Buffer.from(token);
|
|
5311
5685
|
const act = (req, res, pathname) => {
|
|
5312
5686
|
req.resume();
|
|
5313
5687
|
const given = Buffer.from(String(req.headers["x-pitroom-token"] ?? ""));
|
|
5314
|
-
if (given.length !== tokenBuf.length || !
|
|
5688
|
+
if (given.length !== tokenBuf.length || !crypto7.timingSafeEqual(given, tokenBuf)) return json(res, 403, { error: "forbidden" });
|
|
5315
5689
|
if (req.headers.origin !== `http://${req.headers.host}`) return json(res, 403, { error: "forbidden origin" });
|
|
5316
5690
|
const m = /^\/api\/run\/([^/]+)\/(stop|discard)$/.exec(pathname);
|
|
5317
5691
|
if (!m) return json(res, 404, { error: "not found" });
|
|
@@ -5338,7 +5712,7 @@ function handler(touch2, token) {
|
|
|
5338
5712
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
5339
5713
|
if (req.method === "POST") return act(req, res, url.pathname);
|
|
5340
5714
|
if (req.method !== "GET" && req.method !== "HEAD") return json(res, 405, { error: "read-only" });
|
|
5341
|
-
if (url.pathname === "/") return
|
|
5715
|
+
if (url.pathname === "/") return send2(res, 200, "text/html; charset=utf-8", readAsset("/assets/app.js") ? page(token) : MISSING);
|
|
5342
5716
|
const type = ASSETS[url.pathname];
|
|
5343
5717
|
if (type) {
|
|
5344
5718
|
const data = readAsset(url.pathname);
|
|
@@ -5379,7 +5753,7 @@ function handler(touch2, token) {
|
|
|
5379
5753
|
}
|
|
5380
5754
|
function startDash(opts) {
|
|
5381
5755
|
let lastRequest = Date.now();
|
|
5382
|
-
const server = http.createServer(handler(() => lastRequest = Date.now(),
|
|
5756
|
+
const server = http.createServer(handler(() => lastRequest = Date.now(), crypto7.randomBytes(24).toString("hex")));
|
|
5383
5757
|
return new Promise((resolve2, reject) => {
|
|
5384
5758
|
server.once("error", reject);
|
|
5385
5759
|
server.listen(opts.port, "127.0.0.1", () => {
|
|
@@ -5400,7 +5774,7 @@ function startDash(opts) {
|
|
|
5400
5774
|
});
|
|
5401
5775
|
});
|
|
5402
5776
|
}
|
|
5403
|
-
var registryFile = () =>
|
|
5777
|
+
var registryFile = () => path25.join(home(), "dash.json");
|
|
5404
5778
|
async function ping(port) {
|
|
5405
5779
|
try {
|
|
5406
5780
|
const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
|
|
@@ -5412,12 +5786,12 @@ async function ping(port) {
|
|
|
5412
5786
|
async function runningDash() {
|
|
5413
5787
|
let r;
|
|
5414
5788
|
try {
|
|
5415
|
-
r = JSON.parse(
|
|
5789
|
+
r = JSON.parse(fs29.readFileSync(registryFile(), "utf8"));
|
|
5416
5790
|
} catch {
|
|
5417
5791
|
return void 0;
|
|
5418
5792
|
}
|
|
5419
5793
|
if (!isAlive(r.pid)) {
|
|
5420
|
-
|
|
5794
|
+
fs29.rmSync(registryFile(), { force: true });
|
|
5421
5795
|
return void 0;
|
|
5422
5796
|
}
|
|
5423
5797
|
return await ping(r.port) ? { ...r, url: `http://127.0.0.1:${r.port}/` } : void 0;
|
|
@@ -5425,7 +5799,7 @@ async function runningDash() {
|
|
|
5425
5799
|
function openBrowser(url) {
|
|
5426
5800
|
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
5427
5801
|
try {
|
|
5428
|
-
|
|
5802
|
+
spawn6(cmd, args, { stdio: "ignore", detached: true }).on("error", () => void 0).unref();
|
|
5429
5803
|
} catch {
|
|
5430
5804
|
}
|
|
5431
5805
|
}
|
|
@@ -5434,7 +5808,7 @@ async function dashCommand(o) {
|
|
|
5434
5808
|
if (o.stop) {
|
|
5435
5809
|
if (!existing) throw new UserError("pitroom dash is not running");
|
|
5436
5810
|
process.kill(existing.pid, "SIGTERM");
|
|
5437
|
-
|
|
5811
|
+
fs29.rmSync(registryFile(), { force: true });
|
|
5438
5812
|
o.log(`stopped pitroom dash (${existing.url})`);
|
|
5439
5813
|
return 0;
|
|
5440
5814
|
}
|
|
@@ -5444,7 +5818,7 @@ async function dashCommand(o) {
|
|
|
5444
5818
|
return 0;
|
|
5445
5819
|
}
|
|
5446
5820
|
if (o.detach) {
|
|
5447
|
-
const child =
|
|
5821
|
+
const child = spawn6(process.execPath, [process.argv[1], "dash", "--serve", ...o.port !== void 0 ? ["--port", String(o.port)] : [], "--idle", "1h"], {
|
|
5448
5822
|
detached: true,
|
|
5449
5823
|
stdio: "ignore",
|
|
5450
5824
|
env: process.env
|
|
@@ -5471,9 +5845,9 @@ async function dashCommand(o) {
|
|
|
5471
5845
|
if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
|
|
5472
5846
|
dash = await startDash({ port: 0, idleMs: o.idleMs });
|
|
5473
5847
|
}
|
|
5474
|
-
|
|
5848
|
+
fs29.mkdirSync(home(), { recursive: true });
|
|
5475
5849
|
const tmp = `${registryFile()}.${process.pid}.tmp`;
|
|
5476
|
-
|
|
5850
|
+
fs29.writeFileSync(tmp, JSON.stringify({ pid: process.pid, port: dash.port }));
|
|
5477
5851
|
renameOver(tmp, registryFile());
|
|
5478
5852
|
const stop = () => void dash.close();
|
|
5479
5853
|
process.once("SIGINT", stop);
|
|
@@ -5483,7 +5857,7 @@ async function dashCommand(o) {
|
|
|
5483
5857
|
if (o.open) openBrowser(dash.url);
|
|
5484
5858
|
await dash.closed;
|
|
5485
5859
|
try {
|
|
5486
|
-
if (JSON.parse(
|
|
5860
|
+
if (JSON.parse(fs29.readFileSync(registryFile(), "utf8")).pid === process.pid) fs29.rmSync(registryFile(), { force: true });
|
|
5487
5861
|
} catch {
|
|
5488
5862
|
}
|
|
5489
5863
|
return 0;
|
|
@@ -5495,10 +5869,10 @@ var MAX_BODY = 4e6;
|
|
|
5495
5869
|
var MAX_SESSIONS = 64;
|
|
5496
5870
|
var IDLE_MS = 60 * 6e4;
|
|
5497
5871
|
var PATH = "/mcp";
|
|
5498
|
-
var tokenFile = () =>
|
|
5872
|
+
var tokenFile = () => path26.join(home(), "mcp-token");
|
|
5499
5873
|
var readToken = (file2) => {
|
|
5500
5874
|
try {
|
|
5501
|
-
const kept =
|
|
5875
|
+
const kept = fs30.readFileSync(file2, "utf8").trim();
|
|
5502
5876
|
return kept.length >= 16 ? kept : void 0;
|
|
5503
5877
|
} catch {
|
|
5504
5878
|
return void 0;
|
|
@@ -5513,10 +5887,10 @@ function mcpToken() {
|
|
|
5513
5887
|
const file2 = tokenFile();
|
|
5514
5888
|
let token = readToken(file2);
|
|
5515
5889
|
if (!token) {
|
|
5516
|
-
|
|
5517
|
-
token =
|
|
5890
|
+
fs30.mkdirSync(path26.dirname(file2), { recursive: true });
|
|
5891
|
+
token = crypto8.randomBytes(32).toString("hex");
|
|
5518
5892
|
try {
|
|
5519
|
-
|
|
5893
|
+
fs30.writeFileSync(file2, `${token}
|
|
5520
5894
|
`, { mode: 384, flag: "wx" });
|
|
5521
5895
|
} catch (e) {
|
|
5522
5896
|
if (e.code !== "EEXIST") throw e;
|
|
@@ -5525,16 +5899,16 @@ function mcpToken() {
|
|
|
5525
5899
|
}
|
|
5526
5900
|
}
|
|
5527
5901
|
try {
|
|
5528
|
-
|
|
5902
|
+
fs30.chmodSync(file2, 384);
|
|
5529
5903
|
} catch {
|
|
5530
5904
|
}
|
|
5531
5905
|
return { token, from: file2 };
|
|
5532
5906
|
}
|
|
5533
5907
|
var LOCAL_ORIGIN = /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/i;
|
|
5534
5908
|
var sameSecret = (given, token) => {
|
|
5535
|
-
const a =
|
|
5536
|
-
const b =
|
|
5537
|
-
return
|
|
5909
|
+
const a = crypto8.createHash("sha256").update(given).digest();
|
|
5910
|
+
const b = crypto8.createHash("sha256").update(token).digest();
|
|
5911
|
+
return crypto8.timingSafeEqual(a, b);
|
|
5538
5912
|
};
|
|
5539
5913
|
var idleSince = (k) => Math.max(k.lastSeen, k.closeStream ? k.streamSeen ?? 0 : 0);
|
|
5540
5914
|
function drop(sessions, id) {
|
|
@@ -5638,7 +6012,7 @@ data: ${JSON.stringify(msg)}
|
|
|
5638
6012
|
if (single?.method === "initialize") {
|
|
5639
6013
|
if (sid) drop(sessions, sid);
|
|
5640
6014
|
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 =
|
|
6015
|
+
const id = crypto8.randomUUID();
|
|
5642
6016
|
kept = { session: newSession(), lastSeen: Date.now() };
|
|
5643
6017
|
sessions.set(id, kept);
|
|
5644
6018
|
headers["Mcp-Session-Id"] = id;
|
|
@@ -5707,16 +6081,16 @@ async function serveMcpHttp(opts) {
|
|
|
5707
6081
|
|
|
5708
6082
|
// src/cli/commands.ts
|
|
5709
6083
|
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
5710
|
-
import
|
|
5711
|
-
import
|
|
6084
|
+
import fs36 from "node:fs";
|
|
6085
|
+
import path32 from "node:path";
|
|
5712
6086
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
5713
6087
|
|
|
5714
6088
|
// src/core/init.ts
|
|
5715
|
-
import
|
|
5716
|
-
import
|
|
6089
|
+
import fs31 from "node:fs";
|
|
6090
|
+
import path27 from "node:path";
|
|
5717
6091
|
var FREE = /-free$/;
|
|
5718
6092
|
var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
|
|
5719
|
-
var isFound = (binary5) =>
|
|
6093
|
+
var isFound = (binary5) => path27.isAbsolute(binary5) && fs31.existsSync(binary5);
|
|
5720
6094
|
function planInit(opts = {}) {
|
|
5721
6095
|
const file2 = configPath();
|
|
5722
6096
|
const workers = backendIds().map((id) => {
|
|
@@ -5762,15 +6136,15 @@ function planInit(opts = {}) {
|
|
|
5762
6136
|
if (Object.keys(tiers).length > 1) config.tiers = tiers;
|
|
5763
6137
|
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
6138
|
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:
|
|
6139
|
+
return { path: file2, exists: fs31.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
|
|
5766
6140
|
}
|
|
5767
6141
|
function writeInit(plan, force) {
|
|
5768
6142
|
if (plan.blocked) throw new UserError(plan.blocked);
|
|
5769
6143
|
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
|
-
|
|
6144
|
+
if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${path27.basename(plan.path)}.bak)`);
|
|
6145
|
+
fs31.mkdirSync(path27.dirname(plan.path), { recursive: true });
|
|
6146
|
+
if (plan.exists) fs31.copyFileSync(plan.path, `${plan.path}.bak`);
|
|
6147
|
+
fs31.writeFileSync(plan.path, `${JSON.stringify(plan.config, null, 2)}
|
|
5774
6148
|
`);
|
|
5775
6149
|
return plan.path;
|
|
5776
6150
|
}
|
|
@@ -5791,17 +6165,17 @@ function formatInit(plan, written) {
|
|
|
5791
6165
|
|
|
5792
6166
|
// src/core/mcp-install.ts
|
|
5793
6167
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
5794
|
-
import
|
|
6168
|
+
import fs32 from "node:fs";
|
|
5795
6169
|
import os10 from "node:os";
|
|
5796
|
-
import
|
|
6170
|
+
import path28 from "node:path";
|
|
5797
6171
|
var CLIENT_IDS = ["claude-code", "codex", "gemini", "cursor", "claude-desktop"];
|
|
5798
6172
|
var NAME = "pitroom";
|
|
5799
6173
|
function mcpCommand() {
|
|
5800
6174
|
const launcher = launcherPath();
|
|
5801
6175
|
if (isPitroomLauncher(launcher)) return { command: launcher, args: ["mcp"] };
|
|
5802
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
5803
|
-
const p =
|
|
5804
|
-
if (
|
|
6176
|
+
for (const dir of (process.env.PATH ?? "").split(path28.delimiter).filter(Boolean)) {
|
|
6177
|
+
const p = path28.join(dir, process.platform === "win32" ? "pitroom.cmd" : "pitroom");
|
|
6178
|
+
if (fs32.existsSync(p)) return { command: path28.resolve(p), args: ["mcp"] };
|
|
5805
6179
|
}
|
|
5806
6180
|
return { command: process.execPath, args: [process.argv[1] ?? "pitroom", "mcp"] };
|
|
5807
6181
|
}
|
|
@@ -5817,7 +6191,7 @@ function run(backendId, args) {
|
|
|
5817
6191
|
var runnable = (backendId) => run(backendId, ["--version"]).ok;
|
|
5818
6192
|
var readJson = (file2) => {
|
|
5819
6193
|
try {
|
|
5820
|
-
return JSON.parse(
|
|
6194
|
+
return JSON.parse(fs32.readFileSync(file2, "utf8"));
|
|
5821
6195
|
} catch {
|
|
5822
6196
|
return void 0;
|
|
5823
6197
|
}
|
|
@@ -5827,7 +6201,7 @@ var jsonState = (file2, c) => {
|
|
|
5827
6201
|
return entry === void 0 ? "absent" : same(c, entry) ? "same" : "different";
|
|
5828
6202
|
};
|
|
5829
6203
|
var home2 = () => os10.homedir();
|
|
5830
|
-
var geminiDir = () =>
|
|
6204
|
+
var geminiDir = () => path28.join(process.env.GEMINI_CLI_HOME ?? home2(), ".gemini");
|
|
5831
6205
|
function cliClient(id, backend, name, where, o) {
|
|
5832
6206
|
return {
|
|
5833
6207
|
id,
|
|
@@ -5850,43 +6224,43 @@ function jsonClient(id, name, file2, dir) {
|
|
|
5850
6224
|
return {
|
|
5851
6225
|
id,
|
|
5852
6226
|
name,
|
|
5853
|
-
found: () =>
|
|
6227
|
+
found: () => fs32.existsSync(dir()),
|
|
5854
6228
|
state: (c) => jsonState(file2(), c),
|
|
5855
6229
|
add: (c) => {
|
|
5856
6230
|
let config = {};
|
|
5857
|
-
if (
|
|
6231
|
+
if (fs32.existsSync(file2())) {
|
|
5858
6232
|
config = readJson(file2());
|
|
5859
6233
|
if (!config || typeof config !== "object" || Array.isArray(config)) return { ok: false, message: `${rel()} is not valid JSON: left alone` };
|
|
5860
6234
|
const backup = `${file2()}.bak-pitroom`;
|
|
5861
|
-
if (!
|
|
6235
|
+
if (!fs32.existsSync(backup)) fs32.copyFileSync(file2(), backup);
|
|
5862
6236
|
}
|
|
5863
6237
|
config.mcpServers = { ...config.mcpServers ?? {}, [NAME]: { command: c.command, args: c.args } };
|
|
5864
|
-
|
|
6238
|
+
fs32.mkdirSync(path28.dirname(file2()), { recursive: true });
|
|
5865
6239
|
const tmp = `${file2()}.${process.pid}.tmp`;
|
|
5866
|
-
|
|
6240
|
+
fs32.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
|
|
5867
6241
|
`);
|
|
5868
|
-
|
|
6242
|
+
fs32.renameSync(tmp, file2());
|
|
5869
6243
|
return { ok: true, message: rel() };
|
|
5870
6244
|
},
|
|
5871
6245
|
remove: () => {
|
|
5872
6246
|
const config = readJson(file2());
|
|
5873
6247
|
if (!config?.mcpServers?.[NAME]) return { ok: true, message: rel() };
|
|
5874
6248
|
delete config.mcpServers[NAME];
|
|
5875
|
-
|
|
6249
|
+
fs32.writeFileSync(file2(), `${JSON.stringify(config, null, 2)}
|
|
5876
6250
|
`);
|
|
5877
6251
|
return { ok: true, message: rel() };
|
|
5878
6252
|
}
|
|
5879
6253
|
};
|
|
5880
6254
|
}
|
|
5881
6255
|
function desktopFile() {
|
|
5882
|
-
if (process.platform === "darwin") return
|
|
5883
|
-
if (process.platform === "win32") return
|
|
5884
|
-
return
|
|
6256
|
+
if (process.platform === "darwin") return path28.join(home2(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
6257
|
+
if (process.platform === "win32") return path28.join(process.env.APPDATA ?? path28.join(home2(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
6258
|
+
return path28.join(process.env.XDG_CONFIG_HOME ?? path28.join(home2(), ".config"), "Claude", "claude_desktop_config.json");
|
|
5885
6259
|
}
|
|
5886
6260
|
function codexState(c) {
|
|
5887
6261
|
let text;
|
|
5888
6262
|
try {
|
|
5889
|
-
text =
|
|
6263
|
+
text = fs32.readFileSync(path28.join(process.env.CODEX_HOME ?? path28.join(home2(), ".codex"), "config.toml"), "utf8");
|
|
5890
6264
|
} catch {
|
|
5891
6265
|
return "absent";
|
|
5892
6266
|
}
|
|
@@ -5904,7 +6278,7 @@ function codexState(c) {
|
|
|
5904
6278
|
function clients() {
|
|
5905
6279
|
return [
|
|
5906
6280
|
cliClient("claude-code", "claude", "Claude Code", "user settings (~/.claude.json)", {
|
|
5907
|
-
state: (c) => jsonState(
|
|
6281
|
+
state: (c) => jsonState(path28.join(home2(), ".claude.json"), c),
|
|
5908
6282
|
add: (c) => ["mcp", "add", "--scope", "user", NAME, "--", c.command, ...c.args],
|
|
5909
6283
|
remove: () => ["mcp", "remove", NAME, "--scope", "user"]
|
|
5910
6284
|
}),
|
|
@@ -5914,12 +6288,12 @@ function clients() {
|
|
|
5914
6288
|
remove: () => ["mcp", "remove", NAME]
|
|
5915
6289
|
}),
|
|
5916
6290
|
cliClient("gemini", "gemini", "Gemini CLI", "user settings (~/.gemini/settings.json)", {
|
|
5917
|
-
state: (c) => jsonState(
|
|
6291
|
+
state: (c) => jsonState(path28.join(geminiDir(), "settings.json"), c),
|
|
5918
6292
|
add: (c) => ["mcp", "add", "--scope", "user", NAME, c.command, ...c.args],
|
|
5919
6293
|
remove: () => ["mcp", "remove", "--scope", "user", NAME]
|
|
5920
6294
|
}),
|
|
5921
|
-
jsonClient("cursor", "Cursor", () =>
|
|
5922
|
-
jsonClient("claude-desktop", "Claude Desktop", desktopFile, () =>
|
|
6295
|
+
jsonClient("cursor", "Cursor", () => path28.join(home2(), ".cursor", "mcp.json"), () => path28.join(home2(), ".cursor")),
|
|
6296
|
+
jsonClient("claude-desktop", "Claude Desktop", desktopFile, () => path28.dirname(desktopFile()))
|
|
5923
6297
|
];
|
|
5924
6298
|
}
|
|
5925
6299
|
function attempt2(f) {
|
|
@@ -5989,17 +6363,17 @@ var parseClients = (value) => {
|
|
|
5989
6363
|
};
|
|
5990
6364
|
|
|
5991
6365
|
// src/core/plan-status.ts
|
|
5992
|
-
import
|
|
5993
|
-
import
|
|
5994
|
-
import
|
|
6366
|
+
import crypto9 from "node:crypto";
|
|
6367
|
+
import fs33 from "node:fs";
|
|
6368
|
+
import path29 from "node:path";
|
|
5995
6369
|
function notesFile(plan) {
|
|
5996
|
-
const id =
|
|
5997
|
-
return
|
|
6370
|
+
const id = crypto9.createHash("sha1").update(plan.file).digest("hex").slice(0, 12);
|
|
6371
|
+
return path29.join(home(), "plans", id, "notes.md");
|
|
5998
6372
|
}
|
|
5999
6373
|
function readNotes(plan) {
|
|
6000
6374
|
const f = notesFile(plan);
|
|
6001
|
-
if (!
|
|
6002
|
-
return
|
|
6375
|
+
if (!fs33.existsSync(f)) return [];
|
|
6376
|
+
return fs33.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
|
|
6003
6377
|
const m = /^(\d{4}-\d\d-\d\d \d\d:\d\d) (.*)$/.exec(l);
|
|
6004
6378
|
return m ? { at: m[1], text: m[2] } : { at: "", text: l };
|
|
6005
6379
|
});
|
|
@@ -6007,11 +6381,11 @@ function readNotes(plan) {
|
|
|
6007
6381
|
function addNote(planFile, text) {
|
|
6008
6382
|
const plan = loadPlan(planFile);
|
|
6009
6383
|
const f = notesFile(plan);
|
|
6010
|
-
|
|
6011
|
-
if (!
|
|
6384
|
+
fs33.mkdirSync(path29.dirname(f), { recursive: true });
|
|
6385
|
+
if (!fs33.existsSync(f)) fs33.writeFileSync(f, `# plan: ${plan.file}
|
|
6012
6386
|
`);
|
|
6013
6387
|
const at = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
|
|
6014
|
-
|
|
6388
|
+
fs33.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
|
|
6015
6389
|
`);
|
|
6016
6390
|
return f;
|
|
6017
6391
|
}
|
|
@@ -6078,27 +6452,27 @@ ${rulings}`,
|
|
|
6078
6452
|
}
|
|
6079
6453
|
|
|
6080
6454
|
// src/core/cache.ts
|
|
6081
|
-
import
|
|
6082
|
-
import
|
|
6083
|
-
import
|
|
6455
|
+
import crypto10 from "node:crypto";
|
|
6456
|
+
import fs34 from "node:fs";
|
|
6457
|
+
import path30 from "node:path";
|
|
6084
6458
|
var SCAN2 = 500;
|
|
6085
6459
|
function cacheKey(o) {
|
|
6086
6460
|
if (o.verify || o.mode !== "read" || o.continueFrom || o.web || o.plan || o.review || o.audit || o.auditRate === 1) return void 0;
|
|
6087
6461
|
if (!(effective().cacheDays.value > 0)) return void 0;
|
|
6088
|
-
const dir = canonical(
|
|
6462
|
+
const dir = canonical(path30.resolve(o.dir));
|
|
6089
6463
|
const root = repoRoot(dir);
|
|
6090
6464
|
if (!root) return void 0;
|
|
6091
6465
|
try {
|
|
6092
6466
|
const tree = snapshotTree(root);
|
|
6093
6467
|
const head = commitOf(root, "HEAD") ?? "";
|
|
6094
6468
|
const files = o.files.map((f) => {
|
|
6095
|
-
const abs =
|
|
6096
|
-
return [abs,
|
|
6469
|
+
const abs = path30.resolve(f);
|
|
6470
|
+
return [abs, crypto10.createHash("sha256").update(fs34.readFileSync(abs)).digest("hex")];
|
|
6097
6471
|
});
|
|
6098
6472
|
const question = o.task.replace(/\s+/g, " ").trim();
|
|
6099
6473
|
const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
|
|
6100
6474
|
const where = !o.inPlace && wantSnapshot(readIn2, root, dir) ? "snapshot" : "project";
|
|
6101
|
-
const key =
|
|
6475
|
+
const key = crypto10.createHash("sha256").update(JSON.stringify([question, path30.relative(root, dir), files, where])).digest("hex");
|
|
6102
6476
|
return { key, state: `${head}:${tree}` };
|
|
6103
6477
|
} catch {
|
|
6104
6478
|
return void 0;
|
|
@@ -6141,9 +6515,9 @@ function ago(iso, now = Date.now()) {
|
|
|
6141
6515
|
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
6516
|
|
|
6143
6517
|
// src/core/review.ts
|
|
6144
|
-
import
|
|
6145
|
-
import
|
|
6146
|
-
import
|
|
6518
|
+
import crypto11 from "node:crypto";
|
|
6519
|
+
import fs35 from "node:fs";
|
|
6520
|
+
import path31 from "node:path";
|
|
6147
6521
|
var TEMPLATE2 = { task: "task-reviewer", fix: "re-review", range: "code-reviewer" };
|
|
6148
6522
|
var section = (title, body) => `## ${title}
|
|
6149
6523
|
|
|
@@ -6161,7 +6535,7 @@ function ancestors(m) {
|
|
|
6161
6535
|
function briefOf(m) {
|
|
6162
6536
|
const root = [m, ...ancestors(m)].at(-1);
|
|
6163
6537
|
const f = runFile(root.id, "brief.md");
|
|
6164
|
-
return
|
|
6538
|
+
return fs35.existsSync(f) ? fs35.readFileSync(f, "utf8") : root.task;
|
|
6165
6539
|
}
|
|
6166
6540
|
function latestReview(ids) {
|
|
6167
6541
|
for (const id of listRunIds().reverse()) {
|
|
@@ -6175,7 +6549,7 @@ function latestReview(ids) {
|
|
|
6175
6549
|
}
|
|
6176
6550
|
function diffOf(m, from = m.baseTree) {
|
|
6177
6551
|
const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
|
|
6178
|
-
if (where &&
|
|
6552
|
+
if (where && fs35.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
|
|
6179
6553
|
if (from !== m.baseTree) {
|
|
6180
6554
|
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
6555
|
}
|
|
@@ -6189,7 +6563,7 @@ function runReview(id) {
|
|
|
6189
6563
|
if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
|
|
6190
6564
|
const chain = ancestors(m);
|
|
6191
6565
|
const previous = latestReview(chain.map((a) => a.id));
|
|
6192
|
-
const dir = m.mode === "isolate" && m.worktree &&
|
|
6566
|
+
const dir = m.mode === "isolate" && m.worktree && fs35.existsSync(m.worktree) ? m.cwd : m.dir;
|
|
6193
6567
|
const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
|
|
6194
6568
|
const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
|
|
6195
6569
|
if (!previous) {
|
|
@@ -6264,20 +6638,20 @@ function writePackage(job) {
|
|
|
6264
6638
|
const g = gitDir(job.dir);
|
|
6265
6639
|
const root = repoRoot(job.dir);
|
|
6266
6640
|
if (!g || !root) throw new UserError(`not a git repository: ${job.dir}`);
|
|
6267
|
-
const name = `review-${
|
|
6268
|
-
const inside2 = !
|
|
6269
|
-
const file2 = inside2 ?
|
|
6641
|
+
const name = `review-${crypto11.randomBytes(4).toString("hex")}.md`;
|
|
6642
|
+
const inside2 = !path31.relative(fs35.realpathSync(root), fs35.realpathSync(g)).startsWith("..");
|
|
6643
|
+
const file2 = inside2 ? path31.join(g, "pitroom", name) : path31.join(root, ".pitroom", name);
|
|
6270
6644
|
if (!inside2) ignoreInGit(root, "/.pitroom/");
|
|
6271
|
-
|
|
6272
|
-
|
|
6645
|
+
fs35.mkdirSync(path31.dirname(file2), { recursive: true });
|
|
6646
|
+
fs35.writeFileSync(file2, job.package);
|
|
6273
6647
|
return file2;
|
|
6274
6648
|
}
|
|
6275
6649
|
function ignoreInGit(root, pattern) {
|
|
6276
|
-
const exclude =
|
|
6277
|
-
const text =
|
|
6650
|
+
const exclude = path31.resolve(root, git(root, ["rev-parse", "--git-path", "info/exclude"]).stdout.trim());
|
|
6651
|
+
const text = fs35.existsSync(exclude) ? fs35.readFileSync(exclude, "utf8") : "";
|
|
6278
6652
|
if (text.split(/\r?\n/).includes(pattern)) return;
|
|
6279
|
-
|
|
6280
|
-
|
|
6653
|
+
fs35.mkdirSync(path31.dirname(exclude), { recursive: true });
|
|
6654
|
+
fs35.appendFileSync(exclude, `${text && !text.endsWith("\n") ? "\n" : ""}# Pitroom review packages in worktrees (removed after each review)
|
|
6281
6655
|
${pattern}
|
|
6282
6656
|
`);
|
|
6283
6657
|
}
|
|
@@ -6433,10 +6807,10 @@ async function cmdReview(p) {
|
|
|
6433
6807
|
writeMeta(meta);
|
|
6434
6808
|
}
|
|
6435
6809
|
} catch (e) {
|
|
6436
|
-
|
|
6810
|
+
fs36.rmSync(packageFile, { force: true });
|
|
6437
6811
|
throw e;
|
|
6438
6812
|
}
|
|
6439
|
-
|
|
6813
|
+
fs36.writeFileSync(runFile(meta.id, "package.md"), job.package);
|
|
6440
6814
|
return launch(p, meta);
|
|
6441
6815
|
}
|
|
6442
6816
|
async function cmdAudit(p) {
|
|
@@ -6466,6 +6840,47 @@ async function cmdAudit(p) {
|
|
|
6466
6840
|
writeMeta(m);
|
|
6467
6841
|
return launch(p, a);
|
|
6468
6842
|
}
|
|
6843
|
+
async function cmdPrices(p) {
|
|
6844
|
+
if (has(p, "refresh")) {
|
|
6845
|
+
const r = await refreshCatalog();
|
|
6846
|
+
if (r.state === "failed") {
|
|
6847
|
+
if (!has(p, "quiet")) console.error(`could not refresh the price catalog: ${r.error}`);
|
|
6848
|
+
return 1;
|
|
6849
|
+
}
|
|
6850
|
+
if (!has(p, "quiet")) console.log(`price catalog updated: ${r.models} priced models`);
|
|
6851
|
+
}
|
|
6852
|
+
const st = catalogStatus();
|
|
6853
|
+
const used = (() => {
|
|
6854
|
+
try {
|
|
6855
|
+
const chain = resolveChain();
|
|
6856
|
+
return [chain.worker, ...chain.fallback].map((t) => {
|
|
6857
|
+
const backend = getBackend(t.backend);
|
|
6858
|
+
const model = (t.model ?? "").split("#")[0];
|
|
6859
|
+
const price = workerPrice(t.backend, model);
|
|
6860
|
+
return {
|
|
6861
|
+
worker: model ? `${t.backend}:${model}` : t.backend,
|
|
6862
|
+
reportsCost: backend.capabilities.reportsCost,
|
|
6863
|
+
price: price ? { name: price.name, source: price.source, input: price.input, output: price.output, cachedInput: price.cachedInput } : void 0
|
|
6864
|
+
};
|
|
6865
|
+
});
|
|
6866
|
+
} catch {
|
|
6867
|
+
return [];
|
|
6868
|
+
}
|
|
6869
|
+
})();
|
|
6870
|
+
if (has(p, "json")) return console.log(JSON.stringify({ ...st, workers: used }, null, 2)), 0;
|
|
6871
|
+
console.log(`price feed: ${st.enabled ? "on" : 'off ("priceFeed": true in the config turns it on: Pitroom then fetches one public file, see PRIVACY.md)'}`);
|
|
6872
|
+
console.log(`source: ${st.url}${st.enabled ? `, refreshed after a run when older than ${st.hours} h` : ""}`);
|
|
6873
|
+
console.log(st.models ? `catalog: ${st.models} priced models, updated ${st.ageHours < 1 ? "within the hour" : `${st.ageHours.toFixed(1)} h ago`}` : "catalog: none yet (pitroom prices --refresh fetches it)");
|
|
6874
|
+
if (st.lastFailure) console.log(`last try: failed: ${st.lastFailure.replace(/^\S+ /, "")}`);
|
|
6875
|
+
if (used.length) {
|
|
6876
|
+
console.log("\nthe workers in use:");
|
|
6877
|
+
for (const u of used) {
|
|
6878
|
+
const what2 = u.reportsCost ? "reports its own cost" : u.price ? `in ${u.price.input} \xB7 out ${u.price.output} \xB7 cached ${u.price.cachedInput} USD/1M (${u.price.source === "catalog" ? `price catalog ${catalogLabel()}` : "your workerPrices"})` : "no price: its runs count as free in the savings";
|
|
6879
|
+
console.log(` ${u.worker.padEnd(36)} ${what2}`);
|
|
6880
|
+
}
|
|
6881
|
+
}
|
|
6882
|
+
return 0;
|
|
6883
|
+
}
|
|
6469
6884
|
function cmdCooldown(p) {
|
|
6470
6885
|
if (has(p, "clear")) {
|
|
6471
6886
|
const n = clearCooldowns();
|
|
@@ -6504,7 +6919,7 @@ function cmdPlan(p) {
|
|
|
6504
6919
|
}
|
|
6505
6920
|
function cmdCrew(p) {
|
|
6506
6921
|
const file2 = flag(p, "task-file");
|
|
6507
|
-
const tasks = (file2 ?
|
|
6922
|
+
const tasks = (file2 ? fs36.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
|
|
6508
6923
|
if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
|
|
6509
6924
|
if (has(p, "write") && tasks.length > 1) {
|
|
6510
6925
|
throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
|
|
@@ -6612,9 +7027,10 @@ function cmdHistory(p) {
|
|
|
6612
7027
|
const s = historyStats(since2);
|
|
6613
7028
|
if (has(p, "json")) return console.log(JSON.stringify(s, null, 2)), 0;
|
|
6614
7029
|
const t = s.totals;
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
const
|
|
7030
|
+
const limited = t.limited ? ` \xB7 ${t.limited} rate-limited (${s.rateLimits === "counted" ? "counted as not ok" : 'left out; "countRateLimits": true counts them'})` : "";
|
|
7031
|
+
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`);
|
|
7032
|
+
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)}`]);
|
|
7033
|
+
const head2 = ["WORKER", "RUNS", "OK", "LIMITED", "AVG TIME", "AVG TOKENS", "SAVED"];
|
|
6618
7034
|
const widths2 = head2.map((h, i) => Math.max(h.length, ...rows2.map((r) => r[i].length)));
|
|
6619
7035
|
const fmt2 = (r) => r.map((c, i) => i === 0 ? c.padEnd(widths2[i]) : c.padStart(widths2[i])).join(" ");
|
|
6620
7036
|
if (rows2.length) console.log(`
|
|
@@ -6704,7 +7120,7 @@ function sinceMs(s) {
|
|
|
6704
7120
|
if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
|
|
6705
7121
|
return Date.now() - Number(m[1]) * 864e5;
|
|
6706
7122
|
}
|
|
6707
|
-
var readStdin = () => process.stdin.isTTY ? "" :
|
|
7123
|
+
var readStdin = () => process.stdin.isTTY ? "" : fs36.readFileSync(0, "utf8");
|
|
6708
7124
|
function cmdStatusline(p) {
|
|
6709
7125
|
const input = readStdin();
|
|
6710
7126
|
const lines = [];
|
|
@@ -6744,8 +7160,8 @@ function cmdModels(p) {
|
|
|
6744
7160
|
return 0;
|
|
6745
7161
|
}
|
|
6746
7162
|
function cmdHookStart() {
|
|
6747
|
-
const script =
|
|
6748
|
-
if (
|
|
7163
|
+
const script = path32.join(path32.dirname(path32.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
|
|
7164
|
+
if (fs36.existsSync(script)) spawnSync8(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
|
|
6749
7165
|
return 0;
|
|
6750
7166
|
}
|
|
6751
7167
|
function cmdInit(p) {
|
|
@@ -6785,8 +7201,8 @@ function cmdSavings(p) {
|
|
|
6785
7201
|
est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
|
|
6786
7202
|
const out = flag(p, "card");
|
|
6787
7203
|
if (out) {
|
|
6788
|
-
|
|
6789
|
-
console.log(`card written to ${
|
|
7204
|
+
fs36.writeFileSync(out, card(t, period));
|
|
7205
|
+
console.log(`card written to ${path32.resolve(out)}`);
|
|
6790
7206
|
}
|
|
6791
7207
|
if (has(p, "badge")) console.log(`})`);
|
|
6792
7208
|
return 0;
|
|
@@ -6828,7 +7244,7 @@ function cmdConfig(p) {
|
|
|
6828
7244
|
console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
|
|
6829
7245
|
return 0;
|
|
6830
7246
|
}
|
|
6831
|
-
console.log(`config file: ${configPath()}${
|
|
7247
|
+
console.log(`config file: ${configPath()}${fs36.existsSync(configPath()) ? "" : " (not present)"}`);
|
|
6832
7248
|
for (const [key, s] of Object.entries(eff)) {
|
|
6833
7249
|
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
7250
|
console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
|
|
@@ -6852,7 +7268,7 @@ function cmdClean(p) {
|
|
|
6852
7268
|
const m = readMeta(id);
|
|
6853
7269
|
if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
|
|
6854
7270
|
recordRun(m);
|
|
6855
|
-
|
|
7271
|
+
fs36.rmSync(runDir(id), { recursive: true, force: true });
|
|
6856
7272
|
}
|
|
6857
7273
|
const snapshots = prune(Date.now(), 0);
|
|
6858
7274
|
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 +7279,10 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
|
|
|
6863
7279
|
|
|
6864
7280
|
// src/core/doctor.ts
|
|
6865
7281
|
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
6866
|
-
import
|
|
7282
|
+
import fs37 from "node:fs";
|
|
6867
7283
|
import os11 from "node:os";
|
|
6868
|
-
import
|
|
6869
|
-
var
|
|
7284
|
+
import path33 from "node:path";
|
|
7285
|
+
var MARK2 = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
|
|
6870
7286
|
var NEXT = [
|
|
6871
7287
|
{ when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
|
|
6872
7288
|
{ when: /pitroom install|no Pitroom skills|launcher on PATH/, command: "pitroom install", why: "link the skills and the pitroom command" },
|
|
@@ -6888,10 +7304,10 @@ function usageLine() {
|
|
|
6888
7304
|
return `${ids.length} run${ids.length === 1 ? "" : "s"} so far${when2} (pitroom savings shows what they saved)`;
|
|
6889
7305
|
}
|
|
6890
7306
|
function firstNodeOnPath() {
|
|
6891
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
6892
|
-
const file2 =
|
|
7307
|
+
for (const dir of (process.env.PATH ?? "").split(path33.delimiter).filter(Boolean)) {
|
|
7308
|
+
const file2 = path33.join(dir, process.platform === "win32" ? "node.exe" : "node");
|
|
6893
7309
|
try {
|
|
6894
|
-
if (!
|
|
7310
|
+
if (!fs37.statSync(file2).isFile()) continue;
|
|
6895
7311
|
} catch {
|
|
6896
7312
|
continue;
|
|
6897
7313
|
}
|
|
@@ -6918,19 +7334,16 @@ function doctor5(probe) {
|
|
|
6918
7334
|
const addAll = (list2) => list2.forEach((c) => add(c.level, c.message));
|
|
6919
7335
|
add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
|
|
6920
7336
|
const onPath = firstNodeOnPath();
|
|
6921
|
-
if (onPath && onPath.version && tooOld(onPath.version) &&
|
|
7337
|
+
if (onPath && onPath.version && tooOld(onPath.version) && path33.resolve(onPath.path) !== path33.resolve(process.execPath)) {
|
|
6922
7338
|
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
7339
|
}
|
|
6924
7340
|
add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
|
|
6925
7341
|
const cfg = loadConfig();
|
|
6926
|
-
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${
|
|
7342
|
+
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs37.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
|
|
6927
7343
|
for (const w of cfg.warnings) add("warn", w);
|
|
6928
|
-
if (
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
|
|
6932
|
-
add(guarded ? "ok" : "warn", guarded ? "git guard shim ready" : "git guard unavailable (git not on PATH)");
|
|
6933
|
-
}
|
|
7344
|
+
if (shimReady()) add("ok", "git guard shim ready");
|
|
7345
|
+
else if (process.platform === "win32") add("warn", "git guard: the ref and push guard is on, but the git shim needs the sh.exe of Git for Windows next to git (not found), so a worker's `git add`, `restore` or `clean` is not blocked; prefer --isolate and check the patch before apply");
|
|
7346
|
+
else add("warn", "git guard unavailable (git not on PATH)");
|
|
6934
7347
|
section2("Worker chain");
|
|
6935
7348
|
const chain = [];
|
|
6936
7349
|
try {
|
|
@@ -6967,6 +7380,25 @@ function doctor5(probe) {
|
|
|
6967
7380
|
add("ok", `cost: ${key} = ${mine}${cheaper ? `; you priced ${cheaper[0]} cheaper (${cheaper[1]}): is the dearer one needed?` : ""}`);
|
|
6968
7381
|
}
|
|
6969
7382
|
}
|
|
7383
|
+
const seenUnpriced = /* @__PURE__ */ new Set();
|
|
7384
|
+
for (const t of [...chain, ...tierTargets]) {
|
|
7385
|
+
let backend;
|
|
7386
|
+
try {
|
|
7387
|
+
backend = getBackend(t.backend);
|
|
7388
|
+
} catch {
|
|
7389
|
+
continue;
|
|
7390
|
+
}
|
|
7391
|
+
const model = (t.model ?? "").split("#")[0];
|
|
7392
|
+
const key = model ? `${t.backend}:${model}` : t.backend;
|
|
7393
|
+
if (backend.capabilities.reportsCost || seenUnpriced.has(key) || workerPrice(t.backend, model)) continue;
|
|
7394
|
+
seenUnpriced.add(key);
|
|
7395
|
+
add("warn", `${key} reports no cost, so its runs count as free in the savings: give its price in the config, "workerPrices": {"${key}": "<in>,<out>[,<cachedIn>]"} (USD per 1M tokens), or turn on "priceFeed": true to use the public price catalog`);
|
|
7396
|
+
}
|
|
7397
|
+
const feed = catalogStatus();
|
|
7398
|
+
if (feed.enabled) {
|
|
7399
|
+
if (feed.models) add(feed.lastFailure ? "warn" : "ok", `price catalog: ${feed.models} priced models from ${feed.url}, updated ${feed.ageHours < 1 ? "within the hour" : `${feed.ageHours.toFixed(1)} h ago`}${feed.lastFailure ? `; the last refresh failed (${feed.lastFailure.replace(/^\S+ /, "")})` : ""}`);
|
|
7400
|
+
else add("warn", `price feed is on but no catalog has been fetched yet${feed.lastFailure ? ` (the last try failed: ${feed.lastFailure.replace(/^\S+ /, "")})` : ""}: it is fetched after a run, or now with \`pitroom prices --refresh\``);
|
|
7401
|
+
}
|
|
6970
7402
|
for (const c of Object.values(activeCooldowns())) {
|
|
6971
7403
|
add("warn", `${c.target} is cooling down ${untilText(c)} (${c.reason.slice(0, 80)}): runs skip it while a fallback is left; \`pitroom cooldown --clear\` tries it again`);
|
|
6972
7404
|
}
|
|
@@ -7019,7 +7451,7 @@ function print(checks) {
|
|
|
7019
7451
|
for (const name of [...new Set(checks.map((c) => c.section))]) {
|
|
7020
7452
|
console.log(`
|
|
7021
7453
|
${bold(name)}`);
|
|
7022
|
-
for (const c of checks.filter((x) => x.section === name)) console.log(` ${
|
|
7454
|
+
for (const c of checks.filter((x) => x.section === name)) console.log(` ${MARK2[c.level]()} ${wrapText(c.message, 4)}`);
|
|
7023
7455
|
}
|
|
7024
7456
|
const count = (level) => checks.filter((c) => c.level === level).length;
|
|
7025
7457
|
const [ok, warn, fail] = [count("ok"), count("warn"), count("fail")];
|
|
@@ -7080,19 +7512,19 @@ function skillChecks() {
|
|
|
7080
7512
|
else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
|
|
7081
7513
|
}
|
|
7082
7514
|
if (geminiExtensionInstalled()) {
|
|
7083
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
7515
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path33.sep}.agents${path33.sep}`) && i.names.length);
|
|
7084
7516
|
checks.push(
|
|
7085
7517
|
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
7518
|
);
|
|
7087
7519
|
}
|
|
7088
7520
|
if (viaPlugin) {
|
|
7089
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
7521
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path33.sep}.claude${path33.sep}`) && i.names.length);
|
|
7090
7522
|
checks.push(
|
|
7091
7523
|
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
7524
|
);
|
|
7093
7525
|
}
|
|
7094
7526
|
const launcher = launcherPath();
|
|
7095
|
-
if (
|
|
7527
|
+
if (fs37.existsSync(launcher)) {
|
|
7096
7528
|
const r = spawnSync9(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
|
|
7097
7529
|
checks.push(
|
|
7098
7530
|
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 +7536,12 @@ function skillChecks() {
|
|
|
7104
7536
|
}
|
|
7105
7537
|
function geminiExtensionInstalled() {
|
|
7106
7538
|
const home3 = process.env.GEMINI_CLI_HOME ?? os11.homedir();
|
|
7107
|
-
return
|
|
7539
|
+
return fs37.existsSync(path33.join(home3, ".gemini", "extensions", "pitroom"));
|
|
7108
7540
|
}
|
|
7109
7541
|
function pluginInstalled() {
|
|
7110
7542
|
try {
|
|
7111
|
-
const f =
|
|
7112
|
-
const plugins = JSON.parse(
|
|
7543
|
+
const f = path33.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
7544
|
+
const plugins = JSON.parse(fs37.readFileSync(f, "utf8")).plugins ?? {};
|
|
7113
7545
|
return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
|
|
7114
7546
|
} catch {
|
|
7115
7547
|
return false;
|
|
@@ -7126,24 +7558,24 @@ function superpowersActive() {
|
|
|
7126
7558
|
for (const p of openCodePlugins()) {
|
|
7127
7559
|
if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
|
|
7128
7560
|
}
|
|
7129
|
-
for (const base2 of [
|
|
7130
|
-
const dir =
|
|
7131
|
-
if (
|
|
7561
|
+
for (const base2 of [path33.join(os11.homedir(), ".agents", "skills"), path33.join(os11.homedir(), ".claude", "skills")]) {
|
|
7562
|
+
const dir = path33.join(base2, "using-superpowers");
|
|
7563
|
+
if (fs37.existsSync(path33.join(dir, "SKILL.md"))) found.push(dir);
|
|
7132
7564
|
}
|
|
7133
7565
|
return found;
|
|
7134
7566
|
}
|
|
7135
7567
|
function claudePlugins() {
|
|
7136
7568
|
try {
|
|
7137
|
-
const f =
|
|
7138
|
-
return Object.keys(JSON.parse(
|
|
7569
|
+
const f = path33.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
7570
|
+
return Object.keys(JSON.parse(fs37.readFileSync(f, "utf8")).plugins ?? {});
|
|
7139
7571
|
} catch {
|
|
7140
7572
|
return [];
|
|
7141
7573
|
}
|
|
7142
7574
|
}
|
|
7143
7575
|
function claudePluginEnabled(key) {
|
|
7144
7576
|
try {
|
|
7145
|
-
const f =
|
|
7146
|
-
return JSON.parse(
|
|
7577
|
+
const f = path33.join(os11.homedir(), ".claude", "settings.json");
|
|
7578
|
+
return JSON.parse(fs37.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
|
|
7147
7579
|
} catch {
|
|
7148
7580
|
return true;
|
|
7149
7581
|
}
|
|
@@ -7152,7 +7584,7 @@ function codexPlugins() {
|
|
|
7152
7584
|
const plugins = /* @__PURE__ */ new Map();
|
|
7153
7585
|
let text;
|
|
7154
7586
|
try {
|
|
7155
|
-
text =
|
|
7587
|
+
text = fs37.readFileSync(path33.join(process.env.CODEX_HOME ?? path33.join(os11.homedir(), ".codex"), "config.toml"), "utf8");
|
|
7156
7588
|
} catch {
|
|
7157
7589
|
return plugins;
|
|
7158
7590
|
}
|
|
@@ -7170,11 +7602,11 @@ function codexPlugins() {
|
|
|
7170
7602
|
return plugins;
|
|
7171
7603
|
}
|
|
7172
7604
|
function openCodePlugins() {
|
|
7173
|
-
const dir =
|
|
7605
|
+
const dir = path33.join(process.env.XDG_CONFIG_HOME ?? path33.join(os11.homedir(), ".config"), "opencode");
|
|
7174
7606
|
const plugins = [];
|
|
7175
7607
|
for (const name of ["opencode.json", "opencode.jsonc"]) {
|
|
7176
7608
|
try {
|
|
7177
|
-
const text =
|
|
7609
|
+
const text = fs37.readFileSync(path33.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
|
|
7178
7610
|
const list2 = JSON.parse(text).plugin;
|
|
7179
7611
|
if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
|
|
7180
7612
|
} catch {
|
|
@@ -7182,7 +7614,7 @@ function openCodePlugins() {
|
|
|
7182
7614
|
}
|
|
7183
7615
|
for (const folder of ["plugin", "plugins"]) {
|
|
7184
7616
|
try {
|
|
7185
|
-
for (const f of
|
|
7617
|
+
for (const f of fs37.readdirSync(path33.join(dir, folder))) plugins.push(path33.join(dir, folder, f));
|
|
7186
7618
|
} catch {
|
|
7187
7619
|
}
|
|
7188
7620
|
}
|
|
@@ -7232,6 +7664,7 @@ Usage
|
|
|
7232
7664
|
pitroom statusline [--then CMD] status-bar line: running workers, savings this week (after CMD's)
|
|
7233
7665
|
pitroom hook-card PostToolUse hook: a card after each Bash \`pitroom\` command
|
|
7234
7666
|
pitroom hook-start SessionStart hook: introduces Pitroom to the agent (Codex; Claude Code uses its plugin)
|
|
7667
|
+
pitroom prices [--refresh] [--json] the price catalog (config "priceFeed") and the price each worker in use gets
|
|
7235
7668
|
pitroom cooldown [--clear] models that said "rate limited" and are skipped for a while (a quota used up);
|
|
7236
7669
|
--clear tries them again
|
|
7237
7670
|
pitroom mcp [-d DIR] [--http [--port N]]
|
|
@@ -7285,21 +7718,24 @@ Exit codes: 0 ok \xB7 1 worker failed \xB7 2 usage \xB7 3 refused/setup \xB7 4 t
|
|
|
7285
7718
|
Workers: ${backendIds().join(", ")} (targets: "opencode", "opencode:provider/model", or a bare model)
|
|
7286
7719
|
Env: PITROOM_WORKER, PITROOM_MODEL, PITROOM_FALLBACK="t1,t2", PITROOM_TIMEOUT, PITROOM_MAX_PARALLEL,
|
|
7287
7720
|
PITROOM_PRIMARY=sonnet|opus|haiku|gpt-5, PITROOM_PRICE="in,out", PITROOM_HOME, PITROOM_CONFIG, PITROOM_CACHE_DAYS,
|
|
7288
|
-
|
|
7289
|
-
|
|
7721
|
+
PITROOM_NOTIFY, PITROOM_NOTIFY_COMMAND, PITROOM_NOTIFY_AFTER, PITROOM_MCP_DASH, PITROOM_PRICE_FEED, PITROOM_PRICE_FEED_URL,
|
|
7722
|
+
PITROOM_PRICE_FEED_HOURS, PITROOM_<WORKER>_BIN
|
|
7723
|
+
Config: ~/.config/pitroom/config.json (worker, fallback, models, tiers, timeout, primary, price, link, web, maxParallel, audit, readIn, cacheDays,
|
|
7724
|
+
countRateLimits, mcpDash, notify, notifyCommand, notifyAfter, workerPrices, priceFeed, priceFeedUrl, priceFeedHours)`;
|
|
7290
7725
|
var COMMANDS = {
|
|
7291
7726
|
run: cmdRun,
|
|
7292
7727
|
crew: cmdCrew,
|
|
7293
7728
|
review: cmdReview,
|
|
7294
7729
|
audit: cmdAudit,
|
|
7295
7730
|
cooldown: cmdCooldown,
|
|
7731
|
+
prices: cmdPrices,
|
|
7296
7732
|
mcp: (p) => {
|
|
7297
7733
|
const port = flag(p, "port");
|
|
7298
7734
|
if (port !== void 0 && !has(p, "http")) throw new UserError("--port goes with --http (on stdio there is no port)");
|
|
7299
7735
|
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
7736
|
const dir = flag(p, "dir");
|
|
7301
7737
|
if (dir !== void 0) {
|
|
7302
|
-
if (!
|
|
7738
|
+
if (!fs38.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) throw new UserError(`-d ${dir}: no such directory`);
|
|
7303
7739
|
process.chdir(dir);
|
|
7304
7740
|
process.env.PWD = process.cwd();
|
|
7305
7741
|
}
|