pitroom 0.10.1 → 0.12.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 +3 -2
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +21 -0
- package/README.md +11 -2
- package/dist/lib.mjs +25 -0
- package/dist/pitroom.mjs +379 -252
- package/dist/ui/app.js +1 -1
- package/gemini-context.md +5 -0
- package/gemini-extension.json +6 -0
- package/hooks/claude-hooks.json +16 -0
- package/hooks/hooks.json +1 -13
- package/package.json +3 -1
package/dist/pitroom.mjs
CHANGED
|
@@ -522,9 +522,9 @@ function doctor2({ models, hasFallback }) {
|
|
|
522
522
|
return checks;
|
|
523
523
|
}
|
|
524
524
|
function catalog2() {
|
|
525
|
-
const
|
|
525
|
+
const file2 = path3.join(codexHome(), "models_cache.json");
|
|
526
526
|
try {
|
|
527
|
-
const raw = JSON.parse(fs3.readFileSync(
|
|
527
|
+
const raw = JSON.parse(fs3.readFileSync(file2, "utf8"));
|
|
528
528
|
const models = (raw.models ?? []).filter((m) => m.visibility !== "hide" && typeof m.slug === "string").map((m) => ({
|
|
529
529
|
id: m.slug,
|
|
530
530
|
efforts: (m.supported_reasoning_levels ?? []).map(
|
|
@@ -589,10 +589,10 @@ function useArchive(hooks) {
|
|
|
589
589
|
}
|
|
590
590
|
function writeMeta(meta) {
|
|
591
591
|
fs4.mkdirSync(runDir(meta.id), { recursive: true });
|
|
592
|
-
const
|
|
593
|
-
const tmp = `${
|
|
592
|
+
const file2 = runFile(meta.id, "meta.json");
|
|
593
|
+
const tmp = `${file2}.${process.pid}.tmp`;
|
|
594
594
|
fs4.writeFileSync(tmp, JSON.stringify(meta, null, 2));
|
|
595
|
-
fs4.renameSync(tmp,
|
|
595
|
+
fs4.renameSync(tmp, file2);
|
|
596
596
|
if (TERMINAL.includes(meta.state)) {
|
|
597
597
|
try {
|
|
598
598
|
onFinished?.(meta);
|
|
@@ -601,12 +601,12 @@ function writeMeta(meta) {
|
|
|
601
601
|
}
|
|
602
602
|
}
|
|
603
603
|
function readMeta(id) {
|
|
604
|
-
const
|
|
605
|
-
if (!fs4.existsSync(
|
|
604
|
+
const file2 = runFile(id, "meta.json");
|
|
605
|
+
if (!fs4.existsSync(file2)) {
|
|
606
606
|
const kept = archive?.meta(id);
|
|
607
607
|
if (kept) return upgrade(kept);
|
|
608
608
|
}
|
|
609
|
-
return upgrade(JSON.parse(fs4.readFileSync(
|
|
609
|
+
return upgrade(JSON.parse(fs4.readFileSync(file2, "utf8")));
|
|
610
610
|
}
|
|
611
611
|
function upgrade(raw) {
|
|
612
612
|
if (!raw.worker) raw.worker = raw.model ? { backend: "opencode", model: raw.model } : { backend: "opencode" };
|
|
@@ -727,8 +727,8 @@ function parseEvents3(jsonl) {
|
|
|
727
727
|
tools[name] = (tools[name] ?? 0) + 1;
|
|
728
728
|
const target = p.file_path ?? p.absolute_path ?? p.path ?? p.dir_path ?? p.pattern ?? p.command ?? p.query ?? p.url ?? p.prompt;
|
|
729
729
|
lastActivity = `${name} ${target ? oneLine3(String(target), 60) : ""}`.trim();
|
|
730
|
-
const
|
|
731
|
-
if (EDIT_TOOLS2.has(name) &&
|
|
730
|
+
const file2 = p.file_path ?? p.absolute_path ?? p.path;
|
|
731
|
+
if (EDIT_TOOLS2.has(name) && file2) pendingEdits.set(String(e.tool_id), String(file2));
|
|
732
732
|
if (timeline.length < MAX_STEPS) {
|
|
733
733
|
const step = { kind: EDIT_TOOLS2.has(name) ? "edit" : name === "run_shell_command" ? "shell" : "tool", name, text: clip3(String(target ?? ""), 240) };
|
|
734
734
|
timeline.push(step);
|
|
@@ -788,13 +788,13 @@ function workerHome() {
|
|
|
788
788
|
const auth = settings().security?.auth;
|
|
789
789
|
const body = `${JSON.stringify(auth ? { ...base2, security: { ...base2.security, auth } } : base2, null, 2)}
|
|
790
790
|
`;
|
|
791
|
-
const
|
|
791
|
+
const file2 = path5.join(dir, "settings.json");
|
|
792
792
|
try {
|
|
793
|
-
if (fs5.readFileSync(
|
|
793
|
+
if (fs5.readFileSync(file2, "utf8") === body) return root;
|
|
794
794
|
} catch {
|
|
795
795
|
}
|
|
796
796
|
fs5.mkdirSync(dir, { recursive: true });
|
|
797
|
-
fs5.writeFileSync(
|
|
797
|
+
fs5.writeFileSync(file2, body);
|
|
798
798
|
for (const name of ["oauth_creds.json", "google_accounts.json"]) {
|
|
799
799
|
const from = path5.join(geminiHome(), name);
|
|
800
800
|
const to = path5.join(dir, name);
|
|
@@ -874,6 +874,8 @@ function doctor3({ models, hasFallback }) {
|
|
|
874
874
|
const type = settings().security?.auth?.selectedType;
|
|
875
875
|
if (key || vertex) {
|
|
876
876
|
checks.push({ level: "ok", message: `Gemini CLI: ${key ? "an API key is set" : "Vertex AI is set up"}` });
|
|
877
|
+
} else if (type === "gemini-api-key" || type === "vertex-ai") {
|
|
878
|
+
checks.push({ level: "ok", message: `Gemini CLI: ${type === "vertex-ai" ? "Vertex AI" : "an API key"} is selected (kept by Gemini CLI)` });
|
|
877
879
|
} else if (signedIn && type === "oauth-personal") {
|
|
878
880
|
checks.push({
|
|
879
881
|
level: "warn",
|
|
@@ -1424,25 +1426,25 @@ function configPath() {
|
|
|
1424
1426
|
var cached;
|
|
1425
1427
|
function loadConfig() {
|
|
1426
1428
|
if (cached) return cached;
|
|
1427
|
-
const
|
|
1429
|
+
const file2 = configPath();
|
|
1428
1430
|
const config = {};
|
|
1429
1431
|
const warnings = [];
|
|
1430
|
-
if (fs7.existsSync(
|
|
1432
|
+
if (fs7.existsSync(file2)) {
|
|
1431
1433
|
let raw;
|
|
1432
1434
|
try {
|
|
1433
|
-
raw = JSON.parse(fs7.readFileSync(
|
|
1435
|
+
raw = JSON.parse(fs7.readFileSync(file2, "utf8"));
|
|
1434
1436
|
} catch (e) {
|
|
1435
|
-
warnings.push(`${
|
|
1437
|
+
warnings.push(`${file2}: invalid JSON (${e.message}); ignored`);
|
|
1436
1438
|
}
|
|
1437
1439
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
1438
1440
|
for (const [key, value] of Object.entries(raw)) {
|
|
1439
1441
|
const type = SCHEMA[key];
|
|
1440
1442
|
if (!type) {
|
|
1441
|
-
warnings.push(`${
|
|
1443
|
+
warnings.push(`${file2}: unknown key "${key}" ignored`);
|
|
1442
1444
|
} else if (matches(value, type)) {
|
|
1443
1445
|
config[key] = value;
|
|
1444
1446
|
} else {
|
|
1445
|
-
warnings.push(`${
|
|
1447
|
+
warnings.push(`${file2}: "${key}" must be ${type}; ignored`);
|
|
1446
1448
|
}
|
|
1447
1449
|
}
|
|
1448
1450
|
}
|
|
@@ -1554,6 +1556,7 @@ var BOOL_FLAGS = {
|
|
|
1554
1556
|
"--full": "full",
|
|
1555
1557
|
"--badge": "badge",
|
|
1556
1558
|
"--probe": "probe",
|
|
1559
|
+
"--clear": "clear",
|
|
1557
1560
|
"--copy": "copy",
|
|
1558
1561
|
"--all": "all",
|
|
1559
1562
|
"--models": "models",
|
|
@@ -1621,8 +1624,8 @@ function exitCodeFor(meta) {
|
|
|
1621
1624
|
}
|
|
1622
1625
|
}
|
|
1623
1626
|
function readTask(p) {
|
|
1624
|
-
const
|
|
1625
|
-
if (
|
|
1627
|
+
const file2 = flag(p, "task-file");
|
|
1628
|
+
if (file2) return fs8.readFileSync(file2, "utf8");
|
|
1626
1629
|
const words = p.positional;
|
|
1627
1630
|
if (words.length === 1 && words[0] === "-") return fs8.readFileSync(0, "utf8");
|
|
1628
1631
|
return words.join(" ");
|
|
@@ -1654,12 +1657,12 @@ function runOptions(p, task) {
|
|
|
1654
1657
|
};
|
|
1655
1658
|
}
|
|
1656
1659
|
function planStep(p) {
|
|
1657
|
-
const
|
|
1660
|
+
const file2 = flag(p, "plan");
|
|
1658
1661
|
const step = flag(p, "step");
|
|
1659
|
-
if (!
|
|
1660
|
-
if (!
|
|
1662
|
+
if (!file2 && !step) return void 0;
|
|
1663
|
+
if (!file2 || !step) throw new UserError("--plan and --step go together: pitroom run -i --plan PLAN.md --step N");
|
|
1661
1664
|
if (!/^\d+$/.test(step)) throw new UserError(`--step takes a task number, not "${step}"`);
|
|
1662
|
-
return { file, step: Number(step) };
|
|
1665
|
+
return { file: file2, step: Number(step) };
|
|
1663
1666
|
}
|
|
1664
1667
|
function effortFlag(p) {
|
|
1665
1668
|
const v = flag(p, "effort");
|
|
@@ -1669,8 +1672,8 @@ function effortFlag(p) {
|
|
|
1669
1672
|
|
|
1670
1673
|
// src/cli/commands.ts
|
|
1671
1674
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
1672
|
-
import
|
|
1673
|
-
import
|
|
1675
|
+
import fs28 from "node:fs";
|
|
1676
|
+
import path24 from "node:path";
|
|
1674
1677
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
1675
1678
|
|
|
1676
1679
|
// src/core/init.ts
|
|
@@ -1723,7 +1726,7 @@ var FREE = /-free$/;
|
|
|
1723
1726
|
var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
|
|
1724
1727
|
var isFound = (binary5) => path8.isAbsolute(binary5) && fs9.existsSync(binary5);
|
|
1725
1728
|
function planInit(opts = {}) {
|
|
1726
|
-
const
|
|
1729
|
+
const file2 = configPath();
|
|
1727
1730
|
const workers = backendIds().map((id) => {
|
|
1728
1731
|
const b = getBackend(id);
|
|
1729
1732
|
const binary5 = b.binary();
|
|
@@ -1767,7 +1770,7 @@ function planInit(opts = {}) {
|
|
|
1767
1770
|
if (Object.keys(tiers).length > 1) config.tiers = tiers;
|
|
1768
1771
|
if (!workers.some((w) => w.found)) blocked = "no worker CLI found: install OpenCode (https://opencode.ai), Codex CLI, Claude Code or Gemini CLI first";
|
|
1769
1772
|
else if (!blocked && !Object.keys(config).length) notes.push("nothing to propose: the default model and the single worker need no config");
|
|
1770
|
-
return { path:
|
|
1773
|
+
return { path: file2, exists: fs9.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
|
|
1771
1774
|
}
|
|
1772
1775
|
function writeInit(plan, force) {
|
|
1773
1776
|
if (plan.blocked) throw new UserError(plan.blocked);
|
|
@@ -2137,8 +2140,8 @@ var ANSWER_MAX2 = 2e5;
|
|
|
2137
2140
|
var cached2;
|
|
2138
2141
|
var historyFile = () => path10.join(home(), "history.db");
|
|
2139
2142
|
function openDb() {
|
|
2140
|
-
const
|
|
2141
|
-
if (cached2?.file ===
|
|
2143
|
+
const file2 = historyFile();
|
|
2144
|
+
if (cached2?.file === file2) return cached2.db;
|
|
2142
2145
|
let db;
|
|
2143
2146
|
try {
|
|
2144
2147
|
const emit = process.emitWarning;
|
|
@@ -2149,14 +2152,14 @@ function openDb() {
|
|
|
2149
2152
|
} finally {
|
|
2150
2153
|
process.emitWarning = emit;
|
|
2151
2154
|
}
|
|
2152
|
-
fs12.mkdirSync(path10.dirname(
|
|
2153
|
-
db = new DatabaseSync(
|
|
2155
|
+
fs12.mkdirSync(path10.dirname(file2), { recursive: true });
|
|
2156
|
+
db = new DatabaseSync(file2);
|
|
2154
2157
|
db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
|
|
2155
2158
|
migrate(db);
|
|
2156
2159
|
} catch {
|
|
2157
2160
|
db = void 0;
|
|
2158
2161
|
}
|
|
2159
|
-
cached2 = { file, db };
|
|
2162
|
+
cached2 = { file: file2, db };
|
|
2160
2163
|
return db;
|
|
2161
2164
|
}
|
|
2162
2165
|
function migrate(db) {
|
|
@@ -2488,7 +2491,7 @@ function structure(lines) {
|
|
|
2488
2491
|
});
|
|
2489
2492
|
return { headings, rules, fenced };
|
|
2490
2493
|
}
|
|
2491
|
-
function parsePlan(text,
|
|
2494
|
+
function parsePlan(text, file2 = "") {
|
|
2492
2495
|
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
2493
2496
|
const { headings, rules, fenced } = structure(lines);
|
|
2494
2497
|
const after = (line) => Math.min(headings.find((h) => h.line > line)?.line ?? lines.length, rules.find((r) => r > line) ?? lines.length);
|
|
@@ -2507,13 +2510,13 @@ function parsePlan(text, file = "") {
|
|
|
2507
2510
|
const tier = /^\s*[-*]?\s*\*\*Worker:\*\*\s*`?([\w-]+)`?/m.exec(unfenced)?.[1]?.toLowerCase();
|
|
2508
2511
|
tasks.push({ step: Number(m[1]), title: m[2].trim(), text: body, ...tier ? { tier } : {} });
|
|
2509
2512
|
}
|
|
2510
|
-
return { file, title: titleHeading?.text ?? "", header, constraints, tasks };
|
|
2513
|
+
return { file: file2, title: titleHeading?.text ?? "", header, constraints, tasks };
|
|
2511
2514
|
}
|
|
2512
|
-
function loadPlan(
|
|
2513
|
-
const abs = path11.resolve(
|
|
2514
|
-
if (!fs13.existsSync(abs) || !fs13.statSync(abs).isFile()) throw new UserError(`plan not found: ${
|
|
2515
|
+
function loadPlan(file2) {
|
|
2516
|
+
const abs = path11.resolve(file2);
|
|
2517
|
+
if (!fs13.existsSync(abs) || !fs13.statSync(abs).isFile()) throw new UserError(`plan not found: ${file2}`);
|
|
2515
2518
|
const plan = parsePlan(fs13.readFileSync(abs, "utf8"), fs13.realpathSync(abs));
|
|
2516
|
-
if (!plan.tasks.length) throw new UserError(`${
|
|
2519
|
+
if (!plan.tasks.length) throw new UserError(`${file2} has no "Task N" headings (see pitroom-writing-plans)`);
|
|
2517
2520
|
return plan;
|
|
2518
2521
|
}
|
|
2519
2522
|
function planTask(plan, step) {
|
|
@@ -2531,7 +2534,7 @@ function brief(plan, task) {
|
|
|
2531
2534
|
].filter(Boolean).join("\n\n")}
|
|
2532
2535
|
`;
|
|
2533
2536
|
}
|
|
2534
|
-
var planName = (
|
|
2537
|
+
var planName = (file2) => path11.basename(file2).replace(/\.md$/i, "");
|
|
2535
2538
|
|
|
2536
2539
|
// src/core/report.ts
|
|
2537
2540
|
var ICON2 = {
|
|
@@ -2566,7 +2569,7 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
|
2566
2569
|
const status = meta.taskStatus ? ` \xB7 STATUS ${meta.taskStatus}` : "";
|
|
2567
2570
|
out.push(`plan: ${planName(meta.plan.file)} \xB7 Task ${meta.plan.step}: ${meta.plan.title}${status}`);
|
|
2568
2571
|
}
|
|
2569
|
-
for (const a of meta.attempts ?? []) out.push(`fallback: ${a.target} failed (${a.error.slice(0, 160)})`);
|
|
2572
|
+
for (const a of meta.attempts ?? []) out.push(a.skipped ? `skipped: ${a.target} (${a.error.slice(0, 160)})` : `fallback: ${a.target} failed (${a.error.slice(0, 160)})`);
|
|
2570
2573
|
if (meta.error) out.push(`error: ${meta.error}`);
|
|
2571
2574
|
for (const w of meta.warnings) out.push(`warning: ${w}`);
|
|
2572
2575
|
if (meta.state === "timeout") {
|
|
@@ -2787,10 +2790,10 @@ echo "pitroom: needs Node.js 22.13 or newer (set PITROOM_NODE to its path)" >&2
|
|
|
2787
2790
|
exit 127
|
|
2788
2791
|
`;
|
|
2789
2792
|
}
|
|
2790
|
-
function isOurLauncher(
|
|
2791
|
-
if (linksInto(
|
|
2793
|
+
function isOurLauncher(file2, root) {
|
|
2794
|
+
if (linksInto(file2, root)) return true;
|
|
2792
2795
|
try {
|
|
2793
|
-
const s = fs15.readFileSync(
|
|
2796
|
+
const s = fs15.readFileSync(file2, "utf8");
|
|
2794
2797
|
return s.includes(LAUNCHER_MARK) && s.includes(root);
|
|
2795
2798
|
} catch {
|
|
2796
2799
|
return false;
|
|
@@ -2965,17 +2968,84 @@ ${rulings}`,
|
|
|
2965
2968
|
].join("\n");
|
|
2966
2969
|
}
|
|
2967
2970
|
|
|
2971
|
+
// src/core/cooldown.ts
|
|
2972
|
+
import fs17 from "node:fs";
|
|
2973
|
+
import path14 from "node:path";
|
|
2974
|
+
var MAX_MS = 24 * 36e5;
|
|
2975
|
+
var OVERLOAD_MS = 20 * 6e4;
|
|
2976
|
+
var DAILY_MS = 4 * 36e5;
|
|
2977
|
+
var file = () => path14.join(home(), "cooldowns.json");
|
|
2978
|
+
function cooldownKey(target, backend) {
|
|
2979
|
+
const model = (target.model ?? backend.defaultModel?.() ?? "").split("#")[0];
|
|
2980
|
+
return `${target.backend}:${model}`;
|
|
2981
|
+
}
|
|
2982
|
+
function retryAfterMs(message) {
|
|
2983
|
+
const compact3 = /(?:retry|try again|resets?|available again)[^0-9]{0,24}((?:\d+(?:\.\d+)?\s*[dhms]\s*)+)/i.exec(message)?.[1];
|
|
2984
|
+
const words = /(?:retry|try again|resets?)[^0-9]{0,24}(\d+(?:\.\d+)?)\s*(second|minute|hour)s?/i.exec(message);
|
|
2985
|
+
let ms = 0;
|
|
2986
|
+
if (compact3) {
|
|
2987
|
+
for (const [, n, unit] of compact3.matchAll(/(\d+(?:\.\d+)?)\s*([dhms])/gi)) ms += Number(n) * { d: 864e5, h: 36e5, m: 6e4, s: 1e3 }[unit.toLowerCase()];
|
|
2988
|
+
} else if (words) {
|
|
2989
|
+
ms = Number(words[1]) * { second: 1e3, minute: 6e4, hour: 36e5 }[words[2].toLowerCase()];
|
|
2990
|
+
}
|
|
2991
|
+
return ms > 0 ? ms : void 0;
|
|
2992
|
+
}
|
|
2993
|
+
function cooldownMs(message) {
|
|
2994
|
+
const told = retryAfterMs(message);
|
|
2995
|
+
if (told !== void 0) return Math.min(Math.max(told, 6e4), MAX_MS);
|
|
2996
|
+
return /daily|per.?day|per day|exhausted your/i.test(message) ? DAILY_MS : OVERLOAD_MS;
|
|
2997
|
+
}
|
|
2998
|
+
function read() {
|
|
2999
|
+
try {
|
|
3000
|
+
const raw = JSON.parse(fs17.readFileSync(file(), "utf8"));
|
|
3001
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
3002
|
+
} catch {
|
|
3003
|
+
return {};
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
function write(all) {
|
|
3007
|
+
fs17.mkdirSync(path14.dirname(file()), { recursive: true });
|
|
3008
|
+
const tmp = `${file()}.${process.pid}.tmp`;
|
|
3009
|
+
fs17.writeFileSync(tmp, `${JSON.stringify(all, null, 2)}
|
|
3010
|
+
`);
|
|
3011
|
+
fs17.renameSync(tmp, file());
|
|
3012
|
+
}
|
|
3013
|
+
function activeCooldowns(now = Date.now()) {
|
|
3014
|
+
return Object.fromEntries(Object.entries(read()).filter(([, c]) => Date.parse(c.until) > now));
|
|
3015
|
+
}
|
|
3016
|
+
var activeCooldown = (key, now = Date.now()) => activeCooldowns(now)[key];
|
|
3017
|
+
function recordCooldown(key, targetName, message, now = Date.now()) {
|
|
3018
|
+
try {
|
|
3019
|
+
const all = activeCooldowns(now);
|
|
3020
|
+
all[key] = { until: new Date(now + cooldownMs(message)).toISOString(), reason: message.replace(/\s+/g, " ").trim().slice(0, 160), target: targetName };
|
|
3021
|
+
write(all);
|
|
3022
|
+
} catch {
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
function clearCooldowns(key) {
|
|
3026
|
+
const all = read();
|
|
3027
|
+
const keys = key ? Object.keys(all).filter((k) => k === key) : Object.keys(all);
|
|
3028
|
+
for (const k of keys) delete all[k];
|
|
3029
|
+
if (keys.length) write(all);
|
|
3030
|
+
return keys.length;
|
|
3031
|
+
}
|
|
3032
|
+
function untilText(c, now = Date.now()) {
|
|
3033
|
+
const d = new Date(c.until);
|
|
3034
|
+
const time = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
3035
|
+
return d.toDateString() === new Date(now).toDateString() ? `until ${time}` : `until ${d.toLocaleDateString()} ${time}`;
|
|
3036
|
+
}
|
|
3037
|
+
|
|
2968
3038
|
// src/core/review.ts
|
|
2969
3039
|
import crypto5 from "node:crypto";
|
|
2970
|
-
import
|
|
2971
|
-
import
|
|
3040
|
+
import fs19 from "node:fs";
|
|
3041
|
+
import path16 from "node:path";
|
|
2972
3042
|
|
|
2973
3043
|
// src/vcs/git.ts
|
|
2974
3044
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
2975
3045
|
import crypto4 from "node:crypto";
|
|
2976
|
-
import
|
|
3046
|
+
import fs18 from "node:fs";
|
|
2977
3047
|
import os9 from "node:os";
|
|
2978
|
-
import
|
|
3048
|
+
import path15 from "node:path";
|
|
2979
3049
|
var SAFE = [
|
|
2980
3050
|
"-c",
|
|
2981
3051
|
"color.ui=false",
|
|
@@ -3007,20 +3077,20 @@ function gitAvailable() {
|
|
|
3007
3077
|
}
|
|
3008
3078
|
function repoRoot(dir) {
|
|
3009
3079
|
const r = git(dir, ["rev-parse", "--show-toplevel"]);
|
|
3010
|
-
return r.code === 0 ?
|
|
3080
|
+
return r.code === 0 ? path15.resolve(r.stdout.trim()) : void 0;
|
|
3011
3081
|
}
|
|
3012
3082
|
function snapshotTree(root, exclude = []) {
|
|
3013
|
-
const tmp =
|
|
3014
|
-
const real =
|
|
3083
|
+
const tmp = path15.join(os9.tmpdir(), `pitroom-index-${process.pid}-${crypto4.randomBytes(4).toString("hex")}`);
|
|
3084
|
+
const real = path15.resolve(root, must(root, ["rev-parse", "--git-path", "index"]).trim());
|
|
3015
3085
|
const env = { ...process.env, GIT_INDEX_FILE: tmp };
|
|
3016
3086
|
try {
|
|
3017
|
-
if (
|
|
3087
|
+
if (fs18.existsSync(real)) fs18.copyFileSync(real, tmp);
|
|
3018
3088
|
else must(root, ["read-tree", "--empty"], env);
|
|
3019
3089
|
must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`)], env);
|
|
3020
3090
|
return must(root, ["write-tree"], env).trim();
|
|
3021
3091
|
} finally {
|
|
3022
|
-
|
|
3023
|
-
|
|
3092
|
+
fs18.rmSync(tmp, { force: true });
|
|
3093
|
+
fs18.rmSync(`${tmp}.lock`, { force: true });
|
|
3024
3094
|
}
|
|
3025
3095
|
}
|
|
3026
3096
|
var DIFF = ["diff", "--no-ext-diff", "--no-textconv", "--no-renames"];
|
|
@@ -3040,27 +3110,27 @@ function diffTrees(root, a, b) {
|
|
|
3040
3110
|
return { changes, stats: { files: changes.length, insertions, deletions }, patch };
|
|
3041
3111
|
}
|
|
3042
3112
|
function createIsolatedCopy(root, tree, dest) {
|
|
3043
|
-
const objects =
|
|
3044
|
-
|
|
3045
|
-
must(
|
|
3046
|
-
|
|
3113
|
+
const objects = path15.join(path15.resolve(root, must(root, ["rev-parse", "--git-common-dir"]).trim()), "objects");
|
|
3114
|
+
fs18.mkdirSync(path15.dirname(dest), { recursive: true });
|
|
3115
|
+
must(path15.dirname(dest), ["init", "--quiet", dest]);
|
|
3116
|
+
fs18.writeFileSync(path15.join(dest, ".git", "objects", "info", "alternates"), `${objects}
|
|
3047
3117
|
`);
|
|
3048
3118
|
must(dest, ["read-tree", tree]);
|
|
3049
3119
|
must(dest, ["checkout-index", "--all", "--force"]);
|
|
3050
3120
|
}
|
|
3051
3121
|
function removeIsolatedCopy(dest, ownedBy) {
|
|
3052
|
-
const rel =
|
|
3053
|
-
if (!rel || rel.startsWith("..") ||
|
|
3054
|
-
|
|
3122
|
+
const rel = path15.relative(ownedBy, dest);
|
|
3123
|
+
if (!rel || rel.startsWith("..") || path15.isAbsolute(rel)) throw new Error(`refusing to remove ${dest}`);
|
|
3124
|
+
fs18.rmSync(dest, { recursive: true, force: true });
|
|
3055
3125
|
}
|
|
3056
3126
|
function linkIntoWorktree(root, worktree, rels) {
|
|
3057
3127
|
const linked = [];
|
|
3058
3128
|
for (const rel of rels) {
|
|
3059
|
-
const src =
|
|
3060
|
-
const dst =
|
|
3061
|
-
if (!
|
|
3062
|
-
|
|
3063
|
-
|
|
3129
|
+
const src = path15.join(root, rel);
|
|
3130
|
+
const dst = path15.join(worktree, rel);
|
|
3131
|
+
if (!fs18.existsSync(src) || fs18.existsSync(dst)) continue;
|
|
3132
|
+
fs18.mkdirSync(path15.dirname(dst), { recursive: true });
|
|
3133
|
+
fs18.symlinkSync(src, dst, process.platform === "win32" ? "junction" : void 0);
|
|
3064
3134
|
linked.push(rel);
|
|
3065
3135
|
}
|
|
3066
3136
|
return linked;
|
|
@@ -3119,7 +3189,7 @@ function ancestors(m) {
|
|
|
3119
3189
|
function briefOf(m) {
|
|
3120
3190
|
const root = [m, ...ancestors(m)].at(-1);
|
|
3121
3191
|
const f = runFile(root.id, "brief.md");
|
|
3122
|
-
return
|
|
3192
|
+
return fs19.existsSync(f) ? fs19.readFileSync(f, "utf8") : root.task;
|
|
3123
3193
|
}
|
|
3124
3194
|
function latestReview(ids) {
|
|
3125
3195
|
for (const id of listRunIds().reverse()) {
|
|
@@ -3133,7 +3203,7 @@ function latestReview(ids) {
|
|
|
3133
3203
|
}
|
|
3134
3204
|
function diffOf(m, from = m.baseTree) {
|
|
3135
3205
|
const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
|
|
3136
|
-
if (where &&
|
|
3206
|
+
if (where && fs19.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
|
|
3137
3207
|
if (from !== m.baseTree) {
|
|
3138
3208
|
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);
|
|
3139
3209
|
}
|
|
@@ -3147,7 +3217,7 @@ function runReview(id) {
|
|
|
3147
3217
|
if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
|
|
3148
3218
|
const chain = ancestors(m);
|
|
3149
3219
|
const previous = latestReview(chain.map((a) => a.id));
|
|
3150
|
-
const dir = m.mode === "isolate" && m.worktree &&
|
|
3220
|
+
const dir = m.mode === "isolate" && m.worktree && fs19.existsSync(m.worktree) ? m.cwd : m.dir;
|
|
3151
3221
|
const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
|
|
3152
3222
|
const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
|
|
3153
3223
|
if (!previous) {
|
|
@@ -3221,15 +3291,15 @@ function pickReviewer(job) {
|
|
|
3221
3291
|
function writePackage(job) {
|
|
3222
3292
|
const g = gitDir(job.dir);
|
|
3223
3293
|
if (!g) throw new UserError(`not a git repository: ${job.dir}`);
|
|
3224
|
-
const
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
return
|
|
3294
|
+
const file2 = path16.join(g, "pitroom", `review-${crypto5.randomBytes(4).toString("hex")}.md`);
|
|
3295
|
+
fs19.mkdirSync(path16.dirname(file2), { recursive: true });
|
|
3296
|
+
fs19.writeFileSync(file2, job.package);
|
|
3297
|
+
return file2;
|
|
3228
3298
|
}
|
|
3229
3299
|
|
|
3230
3300
|
// src/core/templates.ts
|
|
3231
|
-
import
|
|
3232
|
-
import
|
|
3301
|
+
import fs20 from "node:fs";
|
|
3302
|
+
import path17 from "node:path";
|
|
3233
3303
|
var FILES = {
|
|
3234
3304
|
implementer: "pitroom-driven-development/implementer-prompt.md",
|
|
3235
3305
|
"task-reviewer": "pitroom-driven-development/task-reviewer-prompt.md",
|
|
@@ -3237,9 +3307,9 @@ var FILES = {
|
|
|
3237
3307
|
"code-reviewer": "pitroom-review/code-reviewer.md"
|
|
3238
3308
|
};
|
|
3239
3309
|
function loadTemplate(name, root = packageRoot()) {
|
|
3240
|
-
const
|
|
3241
|
-
if (!
|
|
3242
|
-
return
|
|
3310
|
+
const file2 = path17.join(root, "skills", FILES[name]);
|
|
3311
|
+
if (!fs20.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
|
|
3312
|
+
return fs20.readFileSync(file2, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
|
|
3243
3313
|
}
|
|
3244
3314
|
function fill(template, values) {
|
|
3245
3315
|
const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
|
|
@@ -3346,15 +3416,15 @@ function formatModels(t) {
|
|
|
3346
3416
|
// src/core/dash.ts
|
|
3347
3417
|
import { spawn } from "node:child_process";
|
|
3348
3418
|
import crypto6 from "node:crypto";
|
|
3349
|
-
import
|
|
3419
|
+
import fs21 from "node:fs";
|
|
3350
3420
|
import http from "node:http";
|
|
3351
|
-
import
|
|
3421
|
+
import path18 from "node:path";
|
|
3352
3422
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
3353
3423
|
|
|
3354
3424
|
// src/core/file-diff.ts
|
|
3355
3425
|
var PREVIEW_LINES = 300;
|
|
3356
3426
|
function gitPath(value, prefix = true) {
|
|
3357
|
-
let
|
|
3427
|
+
let path26 = value;
|
|
3358
3428
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
3359
3429
|
const bytes = [];
|
|
3360
3430
|
const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
|
|
@@ -3362,9 +3432,9 @@ function gitPath(value, prefix = true) {
|
|
|
3362
3432
|
if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
|
|
3363
3433
|
else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
|
|
3364
3434
|
}
|
|
3365
|
-
|
|
3435
|
+
path26 = new TextDecoder().decode(new Uint8Array(bytes));
|
|
3366
3436
|
}
|
|
3367
|
-
return prefix ?
|
|
3437
|
+
return prefix ? path26.replace(/^[ab]\//, "") : path26;
|
|
3368
3438
|
}
|
|
3369
3439
|
function headerPath(header) {
|
|
3370
3440
|
const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
|
|
@@ -3373,7 +3443,7 @@ function headerPath(header) {
|
|
|
3373
3443
|
}
|
|
3374
3444
|
function fileDiffs(patch, changes, sourceTruncated = false) {
|
|
3375
3445
|
const files = [];
|
|
3376
|
-
let
|
|
3446
|
+
let file2;
|
|
3377
3447
|
let oldLine = 0;
|
|
3378
3448
|
let newLine = 0;
|
|
3379
3449
|
let oldLeft = 0;
|
|
@@ -3381,25 +3451,25 @@ function fileDiffs(patch, changes, sourceTruncated = false) {
|
|
|
3381
3451
|
let inHunk = false;
|
|
3382
3452
|
let row = 0;
|
|
3383
3453
|
const finishHunk = () => {
|
|
3384
|
-
if (
|
|
3454
|
+
if (file2 && (oldLeft > 0 || newLeft > 0)) file2.incomplete = true;
|
|
3385
3455
|
oldLeft = newLeft = 0;
|
|
3386
3456
|
inHunk = false;
|
|
3387
3457
|
};
|
|
3388
3458
|
const add = (line) => {
|
|
3389
|
-
if (!
|
|
3390
|
-
if (
|
|
3391
|
-
else
|
|
3459
|
+
if (!file2) return;
|
|
3460
|
+
if (file2.lines.length < PREVIEW_LINES) file2.lines.push({ ...line, id: String(row) });
|
|
3461
|
+
else file2.omittedLines++;
|
|
3392
3462
|
row++;
|
|
3393
3463
|
};
|
|
3394
3464
|
for (const line of patch.split("\n")) {
|
|
3395
3465
|
if (line.startsWith("diff --git ")) {
|
|
3396
3466
|
finishHunk();
|
|
3397
|
-
|
|
3398
|
-
files.push(
|
|
3467
|
+
file2 = { path: headerPath(line), status: "M", lines: [], additions: 0, deletions: 0, omittedLines: 0, binary: false, incomplete: false };
|
|
3468
|
+
files.push(file2);
|
|
3399
3469
|
row = 0;
|
|
3400
3470
|
continue;
|
|
3401
3471
|
}
|
|
3402
|
-
if (!
|
|
3472
|
+
if (!file2) continue;
|
|
3403
3473
|
const hunk = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line);
|
|
3404
3474
|
if (hunk) {
|
|
3405
3475
|
finishHunk();
|
|
@@ -3413,11 +3483,11 @@ function fileDiffs(patch, changes, sourceTruncated = false) {
|
|
|
3413
3483
|
add({ type: "meta", content: line });
|
|
3414
3484
|
} else if (inHunk && (oldLeft > 0 || newLeft > 0)) {
|
|
3415
3485
|
if (line.startsWith("+") && newLeft > 0) {
|
|
3416
|
-
|
|
3486
|
+
file2.additions++;
|
|
3417
3487
|
newLeft--;
|
|
3418
3488
|
add({ type: "added", newLine: newLine++, content: line.slice(1) });
|
|
3419
3489
|
} else if (line.startsWith("-") && oldLeft > 0) {
|
|
3420
|
-
|
|
3490
|
+
file2.deletions++;
|
|
3421
3491
|
oldLeft--;
|
|
3422
3492
|
add({ type: "removed", oldLine: oldLine++, content: line.slice(1) });
|
|
3423
3493
|
} else if (line.startsWith(" ") && oldLeft > 0 && newLeft > 0) {
|
|
@@ -3425,25 +3495,25 @@ function fileDiffs(patch, changes, sourceTruncated = false) {
|
|
|
3425
3495
|
newLeft--;
|
|
3426
3496
|
add({ type: "context", oldLine: oldLine++, newLine: newLine++, content: line.slice(1) });
|
|
3427
3497
|
} else {
|
|
3428
|
-
|
|
3498
|
+
file2.incomplete = true;
|
|
3429
3499
|
}
|
|
3430
3500
|
} else if (!inHunk) {
|
|
3431
|
-
if (line.startsWith("+++ ") && line !== "+++ /dev/null")
|
|
3432
|
-
else if (line.startsWith("--- ") && line !== "--- /dev/null")
|
|
3433
|
-
else if (/^(GIT binary patch|Binary files .* differ)$/.test(line))
|
|
3501
|
+
if (line.startsWith("+++ ") && line !== "+++ /dev/null") file2.path = gitPath(line.slice(4).replace(/\t$/, ""));
|
|
3502
|
+
else if (line.startsWith("--- ") && line !== "--- /dev/null") file2.path = gitPath(line.slice(4).replace(/\t$/, ""));
|
|
3503
|
+
else if (/^(GIT binary patch|Binary files .* differ)$/.test(line)) file2.binary = true;
|
|
3434
3504
|
else if (/^(new file mode|deleted file mode|old mode|new mode|rename from|rename to|similarity index) /.test(line)) {
|
|
3435
|
-
if (line.startsWith("new file mode "))
|
|
3436
|
-
if (line.startsWith("deleted file mode "))
|
|
3505
|
+
if (line.startsWith("new file mode ")) file2.status = "A";
|
|
3506
|
+
if (line.startsWith("deleted file mode ")) file2.status = "D";
|
|
3437
3507
|
if (line.startsWith("rename to ")) {
|
|
3438
|
-
|
|
3439
|
-
|
|
3508
|
+
file2.path = gitPath(line.slice(10), false);
|
|
3509
|
+
file2.status = "R";
|
|
3440
3510
|
}
|
|
3441
3511
|
add({ type: "meta", content: line });
|
|
3442
3512
|
}
|
|
3443
3513
|
}
|
|
3444
3514
|
}
|
|
3445
3515
|
finishHunk();
|
|
3446
|
-
if (sourceTruncated &&
|
|
3516
|
+
if (sourceTruncated && file2) file2.incomplete = true;
|
|
3447
3517
|
for (const f of files) {
|
|
3448
3518
|
if (f.incomplete || f.binary) f.additions = f.deletions = void 0;
|
|
3449
3519
|
}
|
|
@@ -3612,11 +3682,11 @@ var assetsDir = () => fileURLToPath3(new URL("./ui/", import.meta.url));
|
|
|
3612
3682
|
var assetCache = /* @__PURE__ */ new Map();
|
|
3613
3683
|
function readAsset(route) {
|
|
3614
3684
|
try {
|
|
3615
|
-
const
|
|
3616
|
-
const { mtimeMs } =
|
|
3685
|
+
const file2 = path18.join(assetsDir(), path18.basename(route));
|
|
3686
|
+
const { mtimeMs } = fs21.statSync(file2);
|
|
3617
3687
|
const hit = assetCache.get(route);
|
|
3618
3688
|
if (hit && hit.mtimeMs === mtimeMs) return hit.data;
|
|
3619
|
-
const data =
|
|
3689
|
+
const data = fs21.readFileSync(file2);
|
|
3620
3690
|
assetCache.set(route, { mtimeMs, data });
|
|
3621
3691
|
return data;
|
|
3622
3692
|
} catch {
|
|
@@ -3696,7 +3766,7 @@ function startDash(opts) {
|
|
|
3696
3766
|
});
|
|
3697
3767
|
});
|
|
3698
3768
|
}
|
|
3699
|
-
var registryFile = () =>
|
|
3769
|
+
var registryFile = () => path18.join(home(), "dash.json");
|
|
3700
3770
|
async function ping(port) {
|
|
3701
3771
|
try {
|
|
3702
3772
|
const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
|
|
@@ -3707,11 +3777,11 @@ async function ping(port) {
|
|
|
3707
3777
|
}
|
|
3708
3778
|
async function runningDash() {
|
|
3709
3779
|
try {
|
|
3710
|
-
const r = JSON.parse(
|
|
3780
|
+
const r = JSON.parse(fs21.readFileSync(registryFile(), "utf8"));
|
|
3711
3781
|
if (isAlive(r.pid) && await ping(r.port)) return { ...r, url: `http://127.0.0.1:${r.port}/` };
|
|
3712
3782
|
} catch {
|
|
3713
3783
|
}
|
|
3714
|
-
|
|
3784
|
+
fs21.rmSync(registryFile(), { force: true });
|
|
3715
3785
|
return void 0;
|
|
3716
3786
|
}
|
|
3717
3787
|
function openBrowser(url) {
|
|
@@ -3726,7 +3796,7 @@ async function dashCommand(o) {
|
|
|
3726
3796
|
if (o.stop) {
|
|
3727
3797
|
if (!existing) throw new UserError("pitroom dash is not running");
|
|
3728
3798
|
process.kill(existing.pid, "SIGTERM");
|
|
3729
|
-
|
|
3799
|
+
fs21.rmSync(registryFile(), { force: true });
|
|
3730
3800
|
o.log(`stopped pitroom dash (${existing.url})`);
|
|
3731
3801
|
return 0;
|
|
3732
3802
|
}
|
|
@@ -3763,8 +3833,8 @@ async function dashCommand(o) {
|
|
|
3763
3833
|
if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
|
|
3764
3834
|
dash = await startDash({ port: 0, idleMs: o.idleMs });
|
|
3765
3835
|
}
|
|
3766
|
-
|
|
3767
|
-
|
|
3836
|
+
fs21.mkdirSync(home(), { recursive: true });
|
|
3837
|
+
fs21.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
|
|
3768
3838
|
const stop = () => void dash.close();
|
|
3769
3839
|
process.once("SIGINT", stop);
|
|
3770
3840
|
process.once("SIGTERM", stop);
|
|
@@ -3773,7 +3843,7 @@ async function dashCommand(o) {
|
|
|
3773
3843
|
if (o.open) openBrowser(dash.url);
|
|
3774
3844
|
await dash.closed;
|
|
3775
3845
|
try {
|
|
3776
|
-
if (JSON.parse(
|
|
3846
|
+
if (JSON.parse(fs21.readFileSync(registryFile(), "utf8")).pid === process.pid) fs21.rmSync(registryFile(), { force: true });
|
|
3777
3847
|
} catch {
|
|
3778
3848
|
}
|
|
3779
3849
|
return 0;
|
|
@@ -3781,8 +3851,8 @@ async function dashCommand(o) {
|
|
|
3781
3851
|
|
|
3782
3852
|
// src/core/run.ts
|
|
3783
3853
|
import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
|
|
3784
|
-
import
|
|
3785
|
-
import
|
|
3854
|
+
import fs27 from "node:fs";
|
|
3855
|
+
import path23 from "node:path";
|
|
3786
3856
|
|
|
3787
3857
|
// src/core/chain.ts
|
|
3788
3858
|
function resolveChain(flags = {}) {
|
|
@@ -3832,11 +3902,11 @@ function resolveChain(flags = {}) {
|
|
|
3832
3902
|
|
|
3833
3903
|
// src/core/process.ts
|
|
3834
3904
|
import { spawn as spawn2 } from "node:child_process";
|
|
3835
|
-
import
|
|
3905
|
+
import fs23 from "node:fs";
|
|
3836
3906
|
|
|
3837
3907
|
// src/vcs/guard.ts
|
|
3838
|
-
import
|
|
3839
|
-
import
|
|
3908
|
+
import fs22 from "node:fs";
|
|
3909
|
+
import path19 from "node:path";
|
|
3840
3910
|
var VERSION = 1;
|
|
3841
3911
|
var SHIM = `#!/bin/sh
|
|
3842
3912
|
# pitroom git guard v${VERSION}. Blocks git commands that change history, refs, the
|
|
@@ -3911,15 +3981,15 @@ esac
|
|
|
3911
3981
|
exec "$real" "$@"
|
|
3912
3982
|
`;
|
|
3913
3983
|
function shimDir() {
|
|
3914
|
-
return
|
|
3984
|
+
return path19.join(home(), "shim", `v${VERSION}`);
|
|
3915
3985
|
}
|
|
3916
3986
|
function findRealGit(skip) {
|
|
3917
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
3918
|
-
if (
|
|
3919
|
-
const p =
|
|
3987
|
+
for (const dir of (process.env.PATH ?? "").split(path19.delimiter).filter(Boolean)) {
|
|
3988
|
+
if (path19.resolve(dir) === skip) continue;
|
|
3989
|
+
const p = path19.join(dir, "git");
|
|
3920
3990
|
try {
|
|
3921
|
-
|
|
3922
|
-
if (
|
|
3991
|
+
fs22.accessSync(p, fs22.constants.X_OK);
|
|
3992
|
+
if (fs22.statSync(p).isFile()) return p;
|
|
3923
3993
|
} catch {
|
|
3924
3994
|
}
|
|
3925
3995
|
}
|
|
@@ -3932,13 +4002,13 @@ var REF_HOOK = `#!/bin/sh
|
|
|
3932
4002
|
echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
|
|
3933
4003
|
exit 1
|
|
3934
4004
|
`;
|
|
3935
|
-
var hooksDir = () =>
|
|
3936
|
-
function writeIfChanged(
|
|
3937
|
-
if (
|
|
3938
|
-
|
|
3939
|
-
const tmp = `${
|
|
3940
|
-
|
|
3941
|
-
|
|
4005
|
+
var hooksDir = () => path19.join(home(), "git-hooks", `v${HOOK_VERSION}`);
|
|
4006
|
+
function writeIfChanged(file2, content) {
|
|
4007
|
+
if (fs22.existsSync(file2) && fs22.readFileSync(file2, "utf8") === content) return;
|
|
4008
|
+
fs22.mkdirSync(path19.dirname(file2), { recursive: true });
|
|
4009
|
+
const tmp = `${file2}.${process.pid}.tmp`;
|
|
4010
|
+
fs22.writeFileSync(tmp, content, { mode: 493 });
|
|
4011
|
+
fs22.renameSync(tmp, file2);
|
|
3942
4012
|
}
|
|
3943
4013
|
function withGitConfig(env, entries) {
|
|
3944
4014
|
const out = { ...env };
|
|
@@ -3960,7 +4030,7 @@ function guardEnv(env) {
|
|
|
3960
4030
|
PAGER: "cat"
|
|
3961
4031
|
};
|
|
3962
4032
|
if (process.platform === "win32") return { ...env, ...quiet };
|
|
3963
|
-
writeIfChanged(
|
|
4033
|
+
writeIfChanged(path19.join(hooksDir(), "reference-transaction"), REF_HOOK);
|
|
3964
4034
|
env = withGitConfig(env, [
|
|
3965
4035
|
["core.hooksPath", hooksDir()],
|
|
3966
4036
|
["url.pitroom-push-blocked://.pushInsteadOf", ""]
|
|
@@ -3968,14 +4038,14 @@ function guardEnv(env) {
|
|
|
3968
4038
|
const dir = shimDir();
|
|
3969
4039
|
const real = findRealGit(dir);
|
|
3970
4040
|
if (!real) return { ...env, ...quiet };
|
|
3971
|
-
writeIfChanged(
|
|
3972
|
-
return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${
|
|
4041
|
+
writeIfChanged(path19.join(dir, "git"), SHIM);
|
|
4042
|
+
return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${path19.delimiter}${env.PATH ?? ""}` };
|
|
3973
4043
|
}
|
|
3974
4044
|
|
|
3975
4045
|
// src/core/process.ts
|
|
3976
4046
|
async function spawnWorker(inv, opts) {
|
|
3977
|
-
const out =
|
|
3978
|
-
const err =
|
|
4047
|
+
const out = fs23.openSync(opts.stdoutFile, "w");
|
|
4048
|
+
const err = fs23.openSync(opts.stderrFile, "w");
|
|
3979
4049
|
const res = { code: null, timedOut: false, stopped: false };
|
|
3980
4050
|
const child = spawn2(inv.command, inv.args, {
|
|
3981
4051
|
cwd: opts.cwd,
|
|
@@ -4007,8 +4077,8 @@ async function spawnWorker(inv, opts) {
|
|
|
4007
4077
|
if (killer) clearTimeout(killer);
|
|
4008
4078
|
process.off("SIGINT", stop);
|
|
4009
4079
|
process.off("SIGTERM", stop);
|
|
4010
|
-
|
|
4011
|
-
|
|
4080
|
+
fs23.closeSync(out);
|
|
4081
|
+
fs23.closeSync(err);
|
|
4012
4082
|
return res;
|
|
4013
4083
|
}
|
|
4014
4084
|
|
|
@@ -4041,10 +4111,10 @@ function parseAudit(text) {
|
|
|
4041
4111
|
|
|
4042
4112
|
// src/core/slots.ts
|
|
4043
4113
|
import crypto7 from "node:crypto";
|
|
4044
|
-
import
|
|
4045
|
-
import
|
|
4046
|
-
var slotsDir = () =>
|
|
4047
|
-
var locksDir = () =>
|
|
4114
|
+
import fs24 from "node:fs";
|
|
4115
|
+
import path20 from "node:path";
|
|
4116
|
+
var slotsDir = () => path20.join(home(), "slots");
|
|
4117
|
+
var locksDir = () => path20.join(home(), "locks");
|
|
4048
4118
|
var STARTUP_GRACE_MS = 3e4;
|
|
4049
4119
|
function holderActive(runId, self) {
|
|
4050
4120
|
if (!runId || runId === self) return false;
|
|
@@ -4057,51 +4127,51 @@ function holderActive(runId, self) {
|
|
|
4057
4127
|
return false;
|
|
4058
4128
|
}
|
|
4059
4129
|
}
|
|
4060
|
-
function tryClaim(
|
|
4061
|
-
|
|
4130
|
+
function tryClaim(file2, runId) {
|
|
4131
|
+
fs24.mkdirSync(path20.dirname(file2), { recursive: true });
|
|
4062
4132
|
try {
|
|
4063
|
-
|
|
4133
|
+
fs24.writeFileSync(file2, runId, { flag: "wx" });
|
|
4064
4134
|
return true;
|
|
4065
4135
|
} catch (e) {
|
|
4066
4136
|
if (e.code !== "EEXIST") throw e;
|
|
4067
4137
|
}
|
|
4068
4138
|
let owner = "";
|
|
4069
4139
|
try {
|
|
4070
|
-
owner =
|
|
4140
|
+
owner = fs24.readFileSync(file2, "utf8").trim();
|
|
4071
4141
|
} catch {
|
|
4072
4142
|
}
|
|
4073
4143
|
if (owner === runId) return true;
|
|
4074
4144
|
if (holderActive(owner, runId)) return false;
|
|
4075
|
-
|
|
4145
|
+
fs24.rmSync(file2, { force: true });
|
|
4076
4146
|
try {
|
|
4077
|
-
|
|
4147
|
+
fs24.writeFileSync(file2, runId, { flag: "wx" });
|
|
4078
4148
|
return true;
|
|
4079
4149
|
} catch {
|
|
4080
4150
|
return false;
|
|
4081
4151
|
}
|
|
4082
4152
|
}
|
|
4083
|
-
function releaseIfOwner(
|
|
4084
|
-
if (!
|
|
4153
|
+
function releaseIfOwner(file2, runId) {
|
|
4154
|
+
if (!file2) return;
|
|
4085
4155
|
try {
|
|
4086
|
-
if (
|
|
4156
|
+
if (fs24.readFileSync(file2, "utf8").trim() === runId) fs24.rmSync(file2, { force: true });
|
|
4087
4157
|
} catch {
|
|
4088
4158
|
}
|
|
4089
4159
|
}
|
|
4090
4160
|
function tryAcquireSlot(runId, maxParallel) {
|
|
4091
4161
|
for (let n = 0; n < Math.max(1, maxParallel); n++) {
|
|
4092
|
-
const
|
|
4093
|
-
if (tryClaim(
|
|
4162
|
+
const file2 = path20.join(slotsDir(), `slot-${n}`);
|
|
4163
|
+
if (tryClaim(file2, runId)) return file2;
|
|
4094
4164
|
}
|
|
4095
4165
|
return void 0;
|
|
4096
4166
|
}
|
|
4097
|
-
var releaseSlot = (
|
|
4098
|
-
var lockFile = (repoRoot2) =>
|
|
4167
|
+
var releaseSlot = (file2, runId) => releaseIfOwner(file2, runId);
|
|
4168
|
+
var lockFile = (repoRoot2) => path20.join(locksDir(), `write-${crypto7.createHash("sha1").update(path20.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
|
|
4099
4169
|
function acquireWriteLock(repoRoot2, runId) {
|
|
4100
|
-
const
|
|
4101
|
-
if (tryClaim(
|
|
4170
|
+
const file2 = lockFile(repoRoot2);
|
|
4171
|
+
if (tryClaim(file2, runId)) return;
|
|
4102
4172
|
let owner = "";
|
|
4103
4173
|
try {
|
|
4104
|
-
owner =
|
|
4174
|
+
owner = fs24.readFileSync(file2, "utf8").trim();
|
|
4105
4175
|
} catch {
|
|
4106
4176
|
}
|
|
4107
4177
|
throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
|
|
@@ -4148,8 +4218,8 @@ ${task}`;
|
|
|
4148
4218
|
}
|
|
4149
4219
|
|
|
4150
4220
|
// src/core/refs.ts
|
|
4151
|
-
import
|
|
4152
|
-
import
|
|
4221
|
+
import fs25 from "node:fs";
|
|
4222
|
+
import path21 from "node:path";
|
|
4153
4223
|
var FILE_EXTENSIONS = new Set(
|
|
4154
4224
|
"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(" ")
|
|
4155
4225
|
);
|
|
@@ -4200,29 +4270,29 @@ function extractRefs(answer) {
|
|
|
4200
4270
|
return [...seen.values()];
|
|
4201
4271
|
}
|
|
4202
4272
|
function verifyRefs(refs, dirs) {
|
|
4203
|
-
const roots = [...new Set(dirs.filter(Boolean).map((d) =>
|
|
4273
|
+
const roots = [...new Set(dirs.filter(Boolean).map((d) => path21.resolve(d)))];
|
|
4204
4274
|
const invalid = [];
|
|
4205
4275
|
const lineCache = /* @__PURE__ */ new Map();
|
|
4206
|
-
const load = (
|
|
4207
|
-
if (!lineCache.has(
|
|
4208
|
-
const size =
|
|
4209
|
-
const lines = size > MAX_BYTES ? null :
|
|
4276
|
+
const load = (file2) => {
|
|
4277
|
+
if (!lineCache.has(file2)) {
|
|
4278
|
+
const size = fs25.statSync(file2).size;
|
|
4279
|
+
const lines = size > MAX_BYTES ? null : fs25.readFileSync(file2, "utf8").split("\n");
|
|
4210
4280
|
if (lines && lines[lines.length - 1] === "") lines.pop();
|
|
4211
|
-
lineCache.set(
|
|
4281
|
+
lineCache.set(file2, lines);
|
|
4212
4282
|
}
|
|
4213
|
-
return lineCache.get(
|
|
4283
|
+
return lineCache.get(file2);
|
|
4214
4284
|
};
|
|
4215
4285
|
let index;
|
|
4216
4286
|
const byName = () => index ??= indexFiles(roots);
|
|
4217
4287
|
const candidates = (ref) => {
|
|
4218
4288
|
const direct = resolve(ref.file, roots);
|
|
4219
4289
|
if (direct) return [direct];
|
|
4220
|
-
if (
|
|
4290
|
+
if (path21.isAbsolute(ref.file)) return [];
|
|
4221
4291
|
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
4222
|
-
return (byName().get(
|
|
4292
|
+
return (byName().get(path21.basename(wanted)) ?? []).filter((f) => f.endsWith(`/${wanted}`) || path21.basename(f) === wanted).slice(0, 20);
|
|
4223
4293
|
};
|
|
4224
|
-
const check = (
|
|
4225
|
-
const lines = load(
|
|
4294
|
+
const check = (file2, ref) => {
|
|
4295
|
+
const lines = load(file2);
|
|
4226
4296
|
if (!lines) return void 0;
|
|
4227
4297
|
if (ref.end > lines.length) return `file has ${lines.length} lines`;
|
|
4228
4298
|
if (ref.symbol && !mentions(lines, ref) && !enclosedBy(lines, ref)) return `\`${ref.symbol}\` not near line ${ref.start}`;
|
|
@@ -4234,7 +4304,7 @@ function verifyRefs(refs, dirs) {
|
|
|
4234
4304
|
if (!files.length) {
|
|
4235
4305
|
if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
|
|
4236
4306
|
total++;
|
|
4237
|
-
invalid.push({ ref: ref.text, reason:
|
|
4307
|
+
invalid.push({ ref: ref.text, reason: path21.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
|
|
4238
4308
|
continue;
|
|
4239
4309
|
}
|
|
4240
4310
|
total++;
|
|
@@ -4255,7 +4325,7 @@ function enclosedBy(lines, ref) {
|
|
|
4255
4325
|
return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
|
|
4256
4326
|
}
|
|
4257
4327
|
var isFileName = (name) => {
|
|
4258
|
-
const base2 =
|
|
4328
|
+
const base2 = path21.basename(name);
|
|
4259
4329
|
return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
|
|
4260
4330
|
};
|
|
4261
4331
|
function indexFiles(roots) {
|
|
@@ -4264,19 +4334,19 @@ function indexFiles(roots) {
|
|
|
4264
4334
|
const walk = (dir) => {
|
|
4265
4335
|
let entries;
|
|
4266
4336
|
try {
|
|
4267
|
-
entries =
|
|
4337
|
+
entries = fs25.readdirSync(dir, { withFileTypes: true });
|
|
4268
4338
|
} catch {
|
|
4269
4339
|
return;
|
|
4270
4340
|
}
|
|
4271
4341
|
for (const e of entries) {
|
|
4272
4342
|
if (count >= MAX_INDEXED) return;
|
|
4273
4343
|
if (e.isDirectory()) {
|
|
4274
|
-
if (!SKIP_DIRS.has(e.name)) walk(
|
|
4344
|
+
if (!SKIP_DIRS.has(e.name)) walk(path21.join(dir, e.name));
|
|
4275
4345
|
} else if (e.isFile()) {
|
|
4276
4346
|
count++;
|
|
4277
4347
|
const list2 = index.get(e.name);
|
|
4278
|
-
if (list2) list2.push(
|
|
4279
|
-
else index.set(e.name, [
|
|
4348
|
+
if (list2) list2.push(path21.join(dir, e.name));
|
|
4349
|
+
else index.set(e.name, [path21.join(dir, e.name)]);
|
|
4280
4350
|
}
|
|
4281
4351
|
}
|
|
4282
4352
|
};
|
|
@@ -4284,27 +4354,27 @@ function indexFiles(roots) {
|
|
|
4284
4354
|
return index;
|
|
4285
4355
|
}
|
|
4286
4356
|
function resolve(ref, roots) {
|
|
4287
|
-
const candidates =
|
|
4357
|
+
const candidates = path21.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path21.join(r, ref));
|
|
4288
4358
|
return candidates.find((c) => {
|
|
4289
4359
|
try {
|
|
4290
|
-
return
|
|
4360
|
+
return fs25.statSync(c).isFile() && inside(c, roots);
|
|
4291
4361
|
} catch {
|
|
4292
4362
|
return false;
|
|
4293
4363
|
}
|
|
4294
4364
|
});
|
|
4295
4365
|
}
|
|
4296
|
-
function inside(
|
|
4366
|
+
function inside(file2, roots) {
|
|
4297
4367
|
return roots.some((r) => {
|
|
4298
|
-
const rel =
|
|
4299
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
4368
|
+
const rel = path21.relative(r, path21.resolve(file2));
|
|
4369
|
+
return rel !== "" && !rel.startsWith("..") && !path21.isAbsolute(rel);
|
|
4300
4370
|
});
|
|
4301
4371
|
}
|
|
4302
4372
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4303
4373
|
|
|
4304
4374
|
// src/core/secrets.ts
|
|
4305
4375
|
import { execFileSync } from "node:child_process";
|
|
4306
|
-
import
|
|
4307
|
-
import
|
|
4376
|
+
import fs26 from "node:fs";
|
|
4377
|
+
import path22 from "node:path";
|
|
4308
4378
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
4309
4379
|
var MAX_VISITED = 2e4;
|
|
4310
4380
|
var MAX_DEPTH = 4;
|
|
@@ -4320,16 +4390,16 @@ function findSecretFiles(dir) {
|
|
|
4320
4390
|
if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
|
|
4321
4391
|
let entries;
|
|
4322
4392
|
try {
|
|
4323
|
-
entries =
|
|
4393
|
+
entries = fs26.readdirSync(d, { withFileTypes: true });
|
|
4324
4394
|
} catch {
|
|
4325
4395
|
return;
|
|
4326
4396
|
}
|
|
4327
4397
|
for (const e of entries) {
|
|
4328
4398
|
visited++;
|
|
4329
4399
|
if (e.isDirectory()) {
|
|
4330
|
-
if (!SKIP_DIRS2.has(e.name)) walk(
|
|
4400
|
+
if (!SKIP_DIRS2.has(e.name)) walk(path22.join(d, e.name), depth + 1);
|
|
4331
4401
|
} else if (e.isFile() && looksSecret(e.name)) {
|
|
4332
|
-
found.push(
|
|
4402
|
+
found.push(path22.relative(dir, path22.join(d, e.name)));
|
|
4333
4403
|
}
|
|
4334
4404
|
}
|
|
4335
4405
|
};
|
|
@@ -4339,7 +4409,7 @@ function findSecretFiles(dir) {
|
|
|
4339
4409
|
function findSecretFilesInTree(root, dir) {
|
|
4340
4410
|
try {
|
|
4341
4411
|
const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
4342
|
-
return out.split("\0").filter((f) => f && looksSecret(
|
|
4412
|
+
return out.split("\0").filter((f) => f && looksSecret(path22.basename(f)) && !path22.relative(dir, path22.join(root, f)).startsWith("..")).map((f) => path22.relative(dir, path22.join(root, f))).sort();
|
|
4343
4413
|
} catch {
|
|
4344
4414
|
return [];
|
|
4345
4415
|
}
|
|
@@ -4353,7 +4423,7 @@ function secretWarning(files, mode) {
|
|
|
4353
4423
|
}
|
|
4354
4424
|
|
|
4355
4425
|
// src/core/run.ts
|
|
4356
|
-
var VERSION2 = true ? "0.
|
|
4426
|
+
var VERSION2 = true ? "0.12.0" : "0.0.0-dev";
|
|
4357
4427
|
function planWork(o) {
|
|
4358
4428
|
if (!o.plan) return { task: o.task.trim(), tier: o.tier };
|
|
4359
4429
|
if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
|
|
@@ -4385,7 +4455,7 @@ function prepareRun(o) {
|
|
|
4385
4455
|
parent = readMeta(o.continueFrom);
|
|
4386
4456
|
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");
|
|
4387
4457
|
if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
|
|
4388
|
-
if (parent.mode === "isolate" && (!parent.worktree || !
|
|
4458
|
+
if (parent.mode === "isolate" && (!parent.worktree || !fs27.existsSync(parent.worktree))) {
|
|
4389
4459
|
throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
|
|
4390
4460
|
}
|
|
4391
4461
|
const ran = parent.ran ?? parent.worker;
|
|
@@ -4404,15 +4474,15 @@ function prepareRun(o) {
|
|
|
4404
4474
|
throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
|
|
4405
4475
|
}
|
|
4406
4476
|
const mode = parent?.mode ?? o.mode;
|
|
4407
|
-
const dir =
|
|
4408
|
-
if (!
|
|
4477
|
+
const dir = path23.resolve(parent?.dir ?? o.dir);
|
|
4478
|
+
if (!fs27.existsSync(dir) || !fs27.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
|
|
4409
4479
|
const root = repoRoot(dir);
|
|
4410
4480
|
if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
|
|
4411
4481
|
if (mode === "write" && !root && !o.allowNonGit) {
|
|
4412
4482
|
throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
|
|
4413
4483
|
}
|
|
4414
|
-
const files = o.files.map((f) =>
|
|
4415
|
-
for (const f of files) if (!
|
|
4484
|
+
const files = o.files.map((f) => path23.resolve(f));
|
|
4485
|
+
for (const f of files) if (!fs27.existsSync(f)) throw new UserError(`file not found: ${f}`);
|
|
4416
4486
|
if (!parent && !process.env.PITROOM_NO_SECRET_WARNING) {
|
|
4417
4487
|
const secrets = mode === "isolate" && root ? findSecretFilesInTree(root, dir) : findSecretFiles(dir);
|
|
4418
4488
|
const note = secretWarning(secrets, mode);
|
|
@@ -4454,13 +4524,13 @@ function prepareRun(o) {
|
|
|
4454
4524
|
try {
|
|
4455
4525
|
acquireWriteLock(root, meta.id);
|
|
4456
4526
|
} catch (e) {
|
|
4457
|
-
|
|
4527
|
+
fs27.rmSync(runDir(meta.id), { recursive: true, force: true });
|
|
4458
4528
|
throw e;
|
|
4459
4529
|
}
|
|
4460
4530
|
}
|
|
4461
|
-
|
|
4531
|
+
fs27.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
|
|
4462
4532
|
`);
|
|
4463
|
-
if (work.brief)
|
|
4533
|
+
if (work.brief) fs27.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
|
|
4464
4534
|
return meta;
|
|
4465
4535
|
}
|
|
4466
4536
|
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -4517,12 +4587,19 @@ async function execute(meta) {
|
|
|
4517
4587
|
for (let i = 0; i < chain.length; i++) {
|
|
4518
4588
|
const target = chain[i];
|
|
4519
4589
|
const backend = getBackend(target.backend);
|
|
4590
|
+
const cooling = activeCooldown(cooldownKey(target, backend));
|
|
4591
|
+
if (cooling && chain.slice(i + 1).some((t) => !activeCooldown(cooldownKey(t, getBackend(t.backend))))) {
|
|
4592
|
+
(meta.attempts ??= []).push({ target: describeTarget(target), error: `cooling down ${untilText(cooling)}: ${cooling.reason}`, skipped: true });
|
|
4593
|
+
writeMeta(meta);
|
|
4594
|
+
continue;
|
|
4595
|
+
}
|
|
4520
4596
|
meta.ran = target;
|
|
4521
4597
|
writeMeta(meta);
|
|
4522
4598
|
result = await attempt(meta, backend, target);
|
|
4523
4599
|
if (result.timedOut || result.stopped || result.spawnError) break;
|
|
4524
4600
|
const why = retryableFailure(meta, backend, result);
|
|
4525
4601
|
if (!why) break;
|
|
4602
|
+
if (why.kind === "rate-limited") recordCooldown(cooldownKey(target, backend), describeTarget(target), why.message);
|
|
4526
4603
|
if (!target.model && backend.defaultModel) {
|
|
4527
4604
|
const failed = backend.defaultModel();
|
|
4528
4605
|
for (let j = chain.length - 1; j > i; j--) {
|
|
@@ -4533,7 +4610,7 @@ async function execute(meta) {
|
|
|
4533
4610
|
(meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
|
|
4534
4611
|
for (const f of ["events.jsonl", "stderr.log"]) {
|
|
4535
4612
|
const from = runFile(meta.id, f);
|
|
4536
|
-
if (
|
|
4613
|
+
if (fs27.existsSync(from)) fs27.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
|
|
4537
4614
|
}
|
|
4538
4615
|
writeMeta(meta);
|
|
4539
4616
|
}
|
|
@@ -4552,13 +4629,13 @@ async function execute(meta) {
|
|
|
4552
4629
|
}
|
|
4553
4630
|
function autoAudit(meta) {
|
|
4554
4631
|
try {
|
|
4555
|
-
if (!auditable(meta,
|
|
4632
|
+
if (!auditable(meta, read2(runFile(meta.id, "summary.md"))) || !sampled(meta)) return;
|
|
4556
4633
|
startAudit(meta);
|
|
4557
4634
|
} catch {
|
|
4558
4635
|
}
|
|
4559
4636
|
}
|
|
4560
4637
|
function startAudit(meta, worker) {
|
|
4561
|
-
const answer =
|
|
4638
|
+
const answer = read2(runFile(meta.id, "summary.md"));
|
|
4562
4639
|
const auditor = worker ?? pickAuditor(meta);
|
|
4563
4640
|
if (!auditor) return void 0;
|
|
4564
4641
|
const a = prepareRun({
|
|
@@ -4594,9 +4671,9 @@ function prepareTree(meta) {
|
|
|
4594
4671
|
if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
|
|
4595
4672
|
if (root && meta.mode === "isolate" && !meta.worktree) {
|
|
4596
4673
|
meta.baseTree = snapshotTree(root);
|
|
4597
|
-
meta.worktree =
|
|
4674
|
+
meta.worktree = path23.join(worktreesDir(), meta.id);
|
|
4598
4675
|
createIsolatedCopy(root, meta.baseTree, meta.worktree);
|
|
4599
|
-
meta.cwd =
|
|
4676
|
+
meta.cwd = path23.join(meta.worktree, path23.relative(root, meta.dir));
|
|
4600
4677
|
const linked = linkIntoWorktree(root, meta.worktree, meta.link);
|
|
4601
4678
|
if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
|
|
4602
4679
|
}
|
|
@@ -4619,11 +4696,11 @@ function attempt(meta, backend, target) {
|
|
|
4619
4696
|
timeoutSec: meta.timeoutSec
|
|
4620
4697
|
});
|
|
4621
4698
|
}
|
|
4622
|
-
var
|
|
4699
|
+
var read2 = (f) => fs27.existsSync(f) ? fs27.readFileSync(f, "utf8") : "";
|
|
4623
4700
|
function retryableFailure(meta, backend, res) {
|
|
4624
|
-
const run = backend.parse(
|
|
4701
|
+
const run = backend.parse(read2(runFile(meta.id, "events.jsonl")));
|
|
4625
4702
|
if (run.usage.steps > 0) return void 0;
|
|
4626
|
-
const f = backend.failure(run,
|
|
4703
|
+
const f = backend.failure(run, read2(runFile(meta.id, "stderr.log")), res.code);
|
|
4627
4704
|
return f && f.kind !== "other" ? f : void 0;
|
|
4628
4705
|
}
|
|
4629
4706
|
var HINTS = {
|
|
@@ -4635,18 +4712,18 @@ var HINTS = {
|
|
|
4635
4712
|
function finalize(meta, res) {
|
|
4636
4713
|
const ran = meta.ran ?? meta.worker;
|
|
4637
4714
|
const backend = getBackend(ran.backend);
|
|
4638
|
-
const run = backend.parse(
|
|
4715
|
+
const run = backend.parse(read2(runFile(meta.id, "events.jsonl")));
|
|
4639
4716
|
meta.sessionId = run.sessionId ?? meta.sessionId;
|
|
4640
4717
|
meta.usage = run.usage;
|
|
4641
|
-
|
|
4718
|
+
fs27.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
|
|
4642
4719
|
`);
|
|
4643
4720
|
if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run.finalText);
|
|
4644
4721
|
if (meta.reviewOf) {
|
|
4645
4722
|
meta.verdict = parseVerdict(run.finalText);
|
|
4646
|
-
if (meta.packageFile)
|
|
4723
|
+
if (meta.packageFile) fs27.rmSync(meta.packageFile, { force: true });
|
|
4647
4724
|
}
|
|
4648
4725
|
if (!res.timedOut && !res.stopped && !meta.error) {
|
|
4649
|
-
const f = backend.failure(run,
|
|
4726
|
+
const f = backend.failure(run, read2(runFile(meta.id, "stderr.log")), res.code);
|
|
4650
4727
|
if (f) meta.error = HINTS[f.kind] ? `${f.message} (${HINTS[f.kind]})` : f.message;
|
|
4651
4728
|
}
|
|
4652
4729
|
try {
|
|
@@ -4691,7 +4768,7 @@ function captureChanges(meta) {
|
|
|
4691
4768
|
const d = diffTrees(target, meta.baseTree, meta.afterTree);
|
|
4692
4769
|
meta.changes = d.changes;
|
|
4693
4770
|
meta.stats = d.stats;
|
|
4694
|
-
|
|
4771
|
+
fs27.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
|
|
4695
4772
|
}
|
|
4696
4773
|
function runVerify(meta) {
|
|
4697
4774
|
const r = spawnSync6(meta.verify, {
|
|
@@ -4704,7 +4781,7 @@ function runVerify(meta) {
|
|
|
4704
4781
|
env: { ...process.env, PWD: meta.cwd, PITROOM_ACTIVE: "1" }
|
|
4705
4782
|
});
|
|
4706
4783
|
const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
|
4707
|
-
|
|
4784
|
+
fs27.writeFileSync(runFile(meta.id, "verify.log"), output);
|
|
4708
4785
|
return { ok: r.status === 0, code: r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
|
|
4709
4786
|
}
|
|
4710
4787
|
function applyRun(meta, allowDelete = false) {
|
|
@@ -4749,7 +4826,7 @@ function discardRun(meta) {
|
|
|
4749
4826
|
return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
|
|
4750
4827
|
}
|
|
4751
4828
|
function cleanupWorktree(meta) {
|
|
4752
|
-
if (meta.worktree &&
|
|
4829
|
+
if (meta.worktree && fs27.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
|
|
4753
4830
|
}
|
|
4754
4831
|
|
|
4755
4832
|
// src/cli/commands.ts
|
|
@@ -4799,10 +4876,10 @@ async function cmdReview(p) {
|
|
|
4799
4876
|
writeMeta(meta);
|
|
4800
4877
|
}
|
|
4801
4878
|
} catch (e) {
|
|
4802
|
-
|
|
4879
|
+
fs28.rmSync(packageFile, { force: true });
|
|
4803
4880
|
throw e;
|
|
4804
4881
|
}
|
|
4805
|
-
|
|
4882
|
+
fs28.writeFileSync(runFile(meta.id, "package.md"), job.package);
|
|
4806
4883
|
return launch(p, meta);
|
|
4807
4884
|
}
|
|
4808
4885
|
async function cmdAudit(p) {
|
|
@@ -4832,24 +4909,45 @@ async function cmdAudit(p) {
|
|
|
4832
4909
|
writeMeta(m);
|
|
4833
4910
|
return launch(p, a);
|
|
4834
4911
|
}
|
|
4912
|
+
function cmdCooldown(p) {
|
|
4913
|
+
if (has(p, "clear")) {
|
|
4914
|
+
const n = clearCooldowns();
|
|
4915
|
+
console.log(n ? `cleared ${n} cooldown${n === 1 ? "" : "s"}: those models are tried again` : "no cooldowns");
|
|
4916
|
+
return 0;
|
|
4917
|
+
}
|
|
4918
|
+
const all = activeCooldowns();
|
|
4919
|
+
if (has(p, "json")) {
|
|
4920
|
+
console.log(JSON.stringify(all, null, 2));
|
|
4921
|
+
return 0;
|
|
4922
|
+
}
|
|
4923
|
+
const rows = Object.values(all);
|
|
4924
|
+
if (!rows.length) {
|
|
4925
|
+
console.log("no model is cooling down");
|
|
4926
|
+
return 0;
|
|
4927
|
+
}
|
|
4928
|
+
console.log("Models left alone for now (runs go to the next worker; they are tried again after the time shown):");
|
|
4929
|
+
for (const c of rows) console.log(` ${c.target.padEnd(52)} ${untilText(c)} \xB7 ${c.reason.slice(0, 90)}`);
|
|
4930
|
+
console.log("\npitroom cooldown --clear try them again now");
|
|
4931
|
+
return 0;
|
|
4932
|
+
}
|
|
4835
4933
|
function cmdPlan(p) {
|
|
4836
|
-
const [sub,
|
|
4837
|
-
if (sub === "status" &&
|
|
4838
|
-
const s = planStatus(
|
|
4934
|
+
const [sub, file2, ...rest] = p.positional;
|
|
4935
|
+
if (sub === "status" && file2) {
|
|
4936
|
+
const s = planStatus(file2);
|
|
4839
4937
|
console.log(
|
|
4840
4938
|
has(p, "json") ? JSON.stringify({ plan: s.plan.file, title: s.plan.title, tasks: s.tasks, rulings: s.rulings, notesFile: s.notesFile }, null, 2) : formatPlanStatus(s)
|
|
4841
4939
|
);
|
|
4842
4940
|
return 0;
|
|
4843
4941
|
}
|
|
4844
|
-
if (sub === "note" &&
|
|
4845
|
-
console.log(`noted in ${addNote(
|
|
4942
|
+
if (sub === "note" && file2 && rest.length) {
|
|
4943
|
+
console.log(`noted in ${addNote(file2, rest.join(" "))}`);
|
|
4846
4944
|
return 0;
|
|
4847
4945
|
}
|
|
4848
4946
|
throw new UserError('usage: pitroom plan status PLAN.md [--json] | pitroom plan note PLAN.md "Task N: \u2026"');
|
|
4849
4947
|
}
|
|
4850
4948
|
function cmdCrew(p) {
|
|
4851
|
-
const
|
|
4852
|
-
const tasks = (
|
|
4949
|
+
const file2 = flag(p, "task-file");
|
|
4950
|
+
const tasks = (file2 ? fs28.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
|
|
4853
4951
|
if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
|
|
4854
4952
|
if (has(p, "write") && tasks.length > 1) {
|
|
4855
4953
|
throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
|
|
@@ -5050,7 +5148,7 @@ function sinceMs(s) {
|
|
|
5050
5148
|
if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
|
|
5051
5149
|
return Date.now() - Number(m[1]) * 864e5;
|
|
5052
5150
|
}
|
|
5053
|
-
var readStdin = () => process.stdin.isTTY ? "" :
|
|
5151
|
+
var readStdin = () => process.stdin.isTTY ? "" : fs28.readFileSync(0, "utf8");
|
|
5054
5152
|
function cmdStatusline(p) {
|
|
5055
5153
|
const input = readStdin();
|
|
5056
5154
|
const lines = [];
|
|
@@ -5090,8 +5188,8 @@ function cmdModels(p) {
|
|
|
5090
5188
|
return 0;
|
|
5091
5189
|
}
|
|
5092
5190
|
function cmdHookStart() {
|
|
5093
|
-
const script =
|
|
5094
|
-
if (
|
|
5191
|
+
const script = path24.join(path24.dirname(path24.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
|
|
5192
|
+
if (fs28.existsSync(script)) spawnSync7(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
|
|
5095
5193
|
return 0;
|
|
5096
5194
|
}
|
|
5097
5195
|
function cmdInit(p) {
|
|
@@ -5131,8 +5229,8 @@ function cmdSavings(p) {
|
|
|
5131
5229
|
est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
|
|
5132
5230
|
const out = flag(p, "card");
|
|
5133
5231
|
if (out) {
|
|
5134
|
-
|
|
5135
|
-
console.log(`card written to ${
|
|
5232
|
+
fs28.writeFileSync(out, card(t, period));
|
|
5233
|
+
console.log(`card written to ${path24.resolve(out)}`);
|
|
5136
5234
|
}
|
|
5137
5235
|
if (has(p, "badge")) console.log(`})`);
|
|
5138
5236
|
return 0;
|
|
@@ -5152,7 +5250,7 @@ function cmdConfig(p) {
|
|
|
5152
5250
|
console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
|
|
5153
5251
|
return 0;
|
|
5154
5252
|
}
|
|
5155
|
-
console.log(`config file: ${configPath()}${
|
|
5253
|
+
console.log(`config file: ${configPath()}${fs28.existsSync(configPath()) ? "" : " (not present)"}`);
|
|
5156
5254
|
for (const [key, s] of Object.entries(eff)) {
|
|
5157
5255
|
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);
|
|
5158
5256
|
console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
|
|
@@ -5176,7 +5274,7 @@ function cmdClean(p) {
|
|
|
5176
5274
|
const m = readMeta(id);
|
|
5177
5275
|
if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
|
|
5178
5276
|
recordRun(m);
|
|
5179
|
-
|
|
5277
|
+
fs28.rmSync(runDir(id), { recursive: true, force: true });
|
|
5180
5278
|
}
|
|
5181
5279
|
console.log(`removed ${old.length} run(s); the history and the savings ledger are kept`);
|
|
5182
5280
|
return 0;
|
|
@@ -5186,9 +5284,9 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
|
|
|
5186
5284
|
|
|
5187
5285
|
// src/core/doctor.ts
|
|
5188
5286
|
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
5189
|
-
import
|
|
5287
|
+
import fs29 from "node:fs";
|
|
5190
5288
|
import os10 from "node:os";
|
|
5191
|
-
import
|
|
5289
|
+
import path25 from "node:path";
|
|
5192
5290
|
var MARK = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
|
|
5193
5291
|
var NEXT = [
|
|
5194
5292
|
{ when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
|
|
@@ -5198,15 +5296,15 @@ var NEXT = [
|
|
|
5198
5296
|
{ when: /Gemini CLI is not signed in|IneligibleTierError/, command: "export GEMINI_API_KEY=\u2026", why: "a Google AI Studio key for the Gemini worker" }
|
|
5199
5297
|
];
|
|
5200
5298
|
function firstNodeOnPath() {
|
|
5201
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
5202
|
-
const
|
|
5299
|
+
for (const dir of (process.env.PATH ?? "").split(path25.delimiter).filter(Boolean)) {
|
|
5300
|
+
const file2 = path25.join(dir, process.platform === "win32" ? "node.exe" : "node");
|
|
5203
5301
|
try {
|
|
5204
|
-
if (!
|
|
5302
|
+
if (!fs29.statSync(file2).isFile()) continue;
|
|
5205
5303
|
} catch {
|
|
5206
5304
|
continue;
|
|
5207
5305
|
}
|
|
5208
|
-
const r = spawnSync8(
|
|
5209
|
-
return r.status === 0 ? { path:
|
|
5306
|
+
const r = spawnSync8(file2, ["-p", "process.versions.node"], { encoding: "utf8", timeout: 5e3 });
|
|
5307
|
+
return r.status === 0 ? { path: file2, version: r.stdout.trim() } : { path: file2, version: "" };
|
|
5210
5308
|
}
|
|
5211
5309
|
return void 0;
|
|
5212
5310
|
}
|
|
@@ -5228,12 +5326,12 @@ function doctor5(probe) {
|
|
|
5228
5326
|
const addAll = (list2) => list2.forEach((c) => add(c.level, c.message));
|
|
5229
5327
|
add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
|
|
5230
5328
|
const onPath = firstNodeOnPath();
|
|
5231
|
-
if (onPath && onPath.version && tooOld(onPath.version) &&
|
|
5329
|
+
if (onPath && onPath.version && tooOld(onPath.version) && path25.resolve(onPath.path) !== path25.resolve(process.execPath)) {
|
|
5232
5330
|
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)`);
|
|
5233
5331
|
}
|
|
5234
5332
|
add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
|
|
5235
5333
|
const cfg = loadConfig();
|
|
5236
|
-
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${
|
|
5334
|
+
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs29.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
|
|
5237
5335
|
for (const w of cfg.warnings) add("warn", w);
|
|
5238
5336
|
if (process.platform !== "win32") {
|
|
5239
5337
|
const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
|
|
@@ -5275,6 +5373,9 @@ function doctor5(probe) {
|
|
|
5275
5373
|
add("ok", `cost: ${key} = ${mine}${cheaper ? `; you priced ${cheaper[0]} cheaper (${cheaper[1]}): is the dearer one needed?` : ""}`);
|
|
5276
5374
|
}
|
|
5277
5375
|
}
|
|
5376
|
+
for (const c of Object.values(activeCooldowns())) {
|
|
5377
|
+
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`);
|
|
5378
|
+
}
|
|
5278
5379
|
const byBackend = /* @__PURE__ */ new Map();
|
|
5279
5380
|
for (const t of [...chain, ...tierTargets]) byBackend.set(t.backend, [...byBackend.get(t.backend) ?? [], t.model]);
|
|
5280
5381
|
for (const [id, models] of byBackend) {
|
|
@@ -5289,6 +5390,19 @@ function doctor5(probe) {
|
|
|
5289
5390
|
addAll(backend.doctor({ models: [...new Set(models)], hasFallback: chain.length > 1 }));
|
|
5290
5391
|
add("ok", `${backend.name}: read-only runs enforced by ${READ_ONLY_HOW[backend.capabilities.readOnly]}`);
|
|
5291
5392
|
}
|
|
5393
|
+
const unused = allBackends().filter((b) => !byBackend.has(b.id));
|
|
5394
|
+
for (const backend of unused) {
|
|
5395
|
+
let found;
|
|
5396
|
+
try {
|
|
5397
|
+
found = backend.doctor({ models: [], hasFallback: true });
|
|
5398
|
+
} catch {
|
|
5399
|
+
continue;
|
|
5400
|
+
}
|
|
5401
|
+
if (!found.length || found[0].level === "fail") continue;
|
|
5402
|
+
section2(`${backend.name} (installed, not in your config)`);
|
|
5403
|
+
for (const c of found) add("ok", c.message);
|
|
5404
|
+
add("ok", `use it with -W ${backend.id}[:model], or name it in "fallback" or "tiers" in the config`);
|
|
5405
|
+
}
|
|
5292
5406
|
section2("Skills and agents");
|
|
5293
5407
|
addAll(skillChecks());
|
|
5294
5408
|
if (probe && chain[0]) {
|
|
@@ -5364,14 +5478,20 @@ function skillChecks() {
|
|
|
5364
5478
|
else if (names.length) checks.push({ level: "warn", message: `skills in ${base2}: only ${names.join(", ")} of ${all.length}; run \`pitroom install\`` });
|
|
5365
5479
|
else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
|
|
5366
5480
|
}
|
|
5481
|
+
if (geminiExtensionInstalled()) {
|
|
5482
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path25.sep}.agents${path25.sep}`) && i.names.length);
|
|
5483
|
+
checks.push(
|
|
5484
|
+
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)" }
|
|
5485
|
+
);
|
|
5486
|
+
}
|
|
5367
5487
|
if (viaPlugin) {
|
|
5368
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
5488
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path25.sep}.claude${path25.sep}`) && i.names.length);
|
|
5369
5489
|
checks.push(
|
|
5370
5490
|
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)" }
|
|
5371
5491
|
);
|
|
5372
5492
|
}
|
|
5373
5493
|
const launcher = launcherPath();
|
|
5374
|
-
if (
|
|
5494
|
+
if (fs29.existsSync(launcher)) {
|
|
5375
5495
|
const r = spawnSync8(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
5376
5496
|
checks.push(
|
|
5377
5497
|
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)}` }
|
|
@@ -5381,10 +5501,14 @@ function skillChecks() {
|
|
|
5381
5501
|
}
|
|
5382
5502
|
return checks;
|
|
5383
5503
|
}
|
|
5504
|
+
function geminiExtensionInstalled() {
|
|
5505
|
+
const home2 = process.env.GEMINI_CLI_HOME ?? os10.homedir();
|
|
5506
|
+
return fs29.existsSync(path25.join(home2, ".gemini", "extensions", "pitroom"));
|
|
5507
|
+
}
|
|
5384
5508
|
function pluginInstalled() {
|
|
5385
5509
|
try {
|
|
5386
|
-
const f =
|
|
5387
|
-
const plugins = JSON.parse(
|
|
5510
|
+
const f = path25.join(os10.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
5511
|
+
const plugins = JSON.parse(fs29.readFileSync(f, "utf8")).plugins ?? {};
|
|
5388
5512
|
return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
|
|
5389
5513
|
} catch {
|
|
5390
5514
|
return false;
|
|
@@ -5401,24 +5525,24 @@ function superpowersActive() {
|
|
|
5401
5525
|
for (const p of openCodePlugins()) {
|
|
5402
5526
|
if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
|
|
5403
5527
|
}
|
|
5404
|
-
for (const base2 of [
|
|
5405
|
-
const dir =
|
|
5406
|
-
if (
|
|
5528
|
+
for (const base2 of [path25.join(os10.homedir(), ".agents", "skills"), path25.join(os10.homedir(), ".claude", "skills")]) {
|
|
5529
|
+
const dir = path25.join(base2, "using-superpowers");
|
|
5530
|
+
if (fs29.existsSync(path25.join(dir, "SKILL.md"))) found.push(dir);
|
|
5407
5531
|
}
|
|
5408
5532
|
return found;
|
|
5409
5533
|
}
|
|
5410
5534
|
function claudePlugins() {
|
|
5411
5535
|
try {
|
|
5412
|
-
const f =
|
|
5413
|
-
return Object.keys(JSON.parse(
|
|
5536
|
+
const f = path25.join(os10.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
5537
|
+
return Object.keys(JSON.parse(fs29.readFileSync(f, "utf8")).plugins ?? {});
|
|
5414
5538
|
} catch {
|
|
5415
5539
|
return [];
|
|
5416
5540
|
}
|
|
5417
5541
|
}
|
|
5418
5542
|
function claudePluginEnabled(key) {
|
|
5419
5543
|
try {
|
|
5420
|
-
const f =
|
|
5421
|
-
return JSON.parse(
|
|
5544
|
+
const f = path25.join(os10.homedir(), ".claude", "settings.json");
|
|
5545
|
+
return JSON.parse(fs29.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
|
|
5422
5546
|
} catch {
|
|
5423
5547
|
return true;
|
|
5424
5548
|
}
|
|
@@ -5427,7 +5551,7 @@ function codexPlugins() {
|
|
|
5427
5551
|
const plugins = /* @__PURE__ */ new Map();
|
|
5428
5552
|
let text;
|
|
5429
5553
|
try {
|
|
5430
|
-
text =
|
|
5554
|
+
text = fs29.readFileSync(path25.join(process.env.CODEX_HOME ?? path25.join(os10.homedir(), ".codex"), "config.toml"), "utf8");
|
|
5431
5555
|
} catch {
|
|
5432
5556
|
return plugins;
|
|
5433
5557
|
}
|
|
@@ -5445,11 +5569,11 @@ function codexPlugins() {
|
|
|
5445
5569
|
return plugins;
|
|
5446
5570
|
}
|
|
5447
5571
|
function openCodePlugins() {
|
|
5448
|
-
const dir =
|
|
5572
|
+
const dir = path25.join(process.env.XDG_CONFIG_HOME ?? path25.join(os10.homedir(), ".config"), "opencode");
|
|
5449
5573
|
const plugins = [];
|
|
5450
5574
|
for (const name of ["opencode.json", "opencode.jsonc"]) {
|
|
5451
5575
|
try {
|
|
5452
|
-
const text =
|
|
5576
|
+
const text = fs29.readFileSync(path25.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
|
|
5453
5577
|
const list2 = JSON.parse(text).plugin;
|
|
5454
5578
|
if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
|
|
5455
5579
|
} catch {
|
|
@@ -5457,7 +5581,7 @@ function openCodePlugins() {
|
|
|
5457
5581
|
}
|
|
5458
5582
|
for (const folder of ["plugin", "plugins"]) {
|
|
5459
5583
|
try {
|
|
5460
|
-
for (const f of
|
|
5584
|
+
for (const f of fs29.readdirSync(path25.join(dir, folder))) plugins.push(path25.join(dir, folder, f));
|
|
5461
5585
|
} catch {
|
|
5462
5586
|
}
|
|
5463
5587
|
}
|
|
@@ -5507,6 +5631,8 @@ Usage
|
|
|
5507
5631
|
pitroom statusline [--then CMD] status-bar line: running workers, savings this week (after CMD's)
|
|
5508
5632
|
pitroom hook-card PostToolUse hook: a card after each Bash \`pitroom\` command
|
|
5509
5633
|
pitroom hook-start SessionStart hook: introduces Pitroom to the agent (Codex; Claude Code uses its plugin)
|
|
5634
|
+
pitroom cooldown [--clear] models that said "rate limited" and are skipped for a while (a quota used up);
|
|
5635
|
+
--clear tries them again
|
|
5510
5636
|
pitroom doctor [--probe] check workers, models, permissions, skills
|
|
5511
5637
|
pitroom config effective settings, where each comes from, config file path
|
|
5512
5638
|
pitroom init [--model ID] [--fallback A,B] [--yes] [--force]
|
|
@@ -5556,6 +5682,7 @@ var COMMANDS = {
|
|
|
5556
5682
|
crew: cmdCrew,
|
|
5557
5683
|
review: cmdReview,
|
|
5558
5684
|
audit: cmdAudit,
|
|
5685
|
+
cooldown: cmdCooldown,
|
|
5559
5686
|
plan: cmdPlan,
|
|
5560
5687
|
status: cmdStatus,
|
|
5561
5688
|
wait: cmdWait,
|