pitroom 0.11.0 → 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 +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +7 -0
- package/README.md +2 -1
- package/dist/lib.mjs +23 -0
- package/dist/pitroom.mjs +356 -254
- package/dist/ui/app.js +1 -1
- package/gemini-extension.json +1 -1
- package/package.json +1 -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);
|
|
@@ -1426,25 +1426,25 @@ function configPath() {
|
|
|
1426
1426
|
var cached;
|
|
1427
1427
|
function loadConfig() {
|
|
1428
1428
|
if (cached) return cached;
|
|
1429
|
-
const
|
|
1429
|
+
const file2 = configPath();
|
|
1430
1430
|
const config = {};
|
|
1431
1431
|
const warnings = [];
|
|
1432
|
-
if (fs7.existsSync(
|
|
1432
|
+
if (fs7.existsSync(file2)) {
|
|
1433
1433
|
let raw;
|
|
1434
1434
|
try {
|
|
1435
|
-
raw = JSON.parse(fs7.readFileSync(
|
|
1435
|
+
raw = JSON.parse(fs7.readFileSync(file2, "utf8"));
|
|
1436
1436
|
} catch (e) {
|
|
1437
|
-
warnings.push(`${
|
|
1437
|
+
warnings.push(`${file2}: invalid JSON (${e.message}); ignored`);
|
|
1438
1438
|
}
|
|
1439
1439
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
1440
1440
|
for (const [key, value] of Object.entries(raw)) {
|
|
1441
1441
|
const type = SCHEMA[key];
|
|
1442
1442
|
if (!type) {
|
|
1443
|
-
warnings.push(`${
|
|
1443
|
+
warnings.push(`${file2}: unknown key "${key}" ignored`);
|
|
1444
1444
|
} else if (matches(value, type)) {
|
|
1445
1445
|
config[key] = value;
|
|
1446
1446
|
} else {
|
|
1447
|
-
warnings.push(`${
|
|
1447
|
+
warnings.push(`${file2}: "${key}" must be ${type}; ignored`);
|
|
1448
1448
|
}
|
|
1449
1449
|
}
|
|
1450
1450
|
}
|
|
@@ -1556,6 +1556,7 @@ var BOOL_FLAGS = {
|
|
|
1556
1556
|
"--full": "full",
|
|
1557
1557
|
"--badge": "badge",
|
|
1558
1558
|
"--probe": "probe",
|
|
1559
|
+
"--clear": "clear",
|
|
1559
1560
|
"--copy": "copy",
|
|
1560
1561
|
"--all": "all",
|
|
1561
1562
|
"--models": "models",
|
|
@@ -1623,8 +1624,8 @@ function exitCodeFor(meta) {
|
|
|
1623
1624
|
}
|
|
1624
1625
|
}
|
|
1625
1626
|
function readTask(p) {
|
|
1626
|
-
const
|
|
1627
|
-
if (
|
|
1627
|
+
const file2 = flag(p, "task-file");
|
|
1628
|
+
if (file2) return fs8.readFileSync(file2, "utf8");
|
|
1628
1629
|
const words = p.positional;
|
|
1629
1630
|
if (words.length === 1 && words[0] === "-") return fs8.readFileSync(0, "utf8");
|
|
1630
1631
|
return words.join(" ");
|
|
@@ -1656,12 +1657,12 @@ function runOptions(p, task) {
|
|
|
1656
1657
|
};
|
|
1657
1658
|
}
|
|
1658
1659
|
function planStep(p) {
|
|
1659
|
-
const
|
|
1660
|
+
const file2 = flag(p, "plan");
|
|
1660
1661
|
const step = flag(p, "step");
|
|
1661
|
-
if (!
|
|
1662
|
-
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");
|
|
1663
1664
|
if (!/^\d+$/.test(step)) throw new UserError(`--step takes a task number, not "${step}"`);
|
|
1664
|
-
return { file, step: Number(step) };
|
|
1665
|
+
return { file: file2, step: Number(step) };
|
|
1665
1666
|
}
|
|
1666
1667
|
function effortFlag(p) {
|
|
1667
1668
|
const v = flag(p, "effort");
|
|
@@ -1671,8 +1672,8 @@ function effortFlag(p) {
|
|
|
1671
1672
|
|
|
1672
1673
|
// src/cli/commands.ts
|
|
1673
1674
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
1674
|
-
import
|
|
1675
|
-
import
|
|
1675
|
+
import fs28 from "node:fs";
|
|
1676
|
+
import path24 from "node:path";
|
|
1676
1677
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
1677
1678
|
|
|
1678
1679
|
// src/core/init.ts
|
|
@@ -1725,7 +1726,7 @@ var FREE = /-free$/;
|
|
|
1725
1726
|
var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
|
|
1726
1727
|
var isFound = (binary5) => path8.isAbsolute(binary5) && fs9.existsSync(binary5);
|
|
1727
1728
|
function planInit(opts = {}) {
|
|
1728
|
-
const
|
|
1729
|
+
const file2 = configPath();
|
|
1729
1730
|
const workers = backendIds().map((id) => {
|
|
1730
1731
|
const b = getBackend(id);
|
|
1731
1732
|
const binary5 = b.binary();
|
|
@@ -1769,7 +1770,7 @@ function planInit(opts = {}) {
|
|
|
1769
1770
|
if (Object.keys(tiers).length > 1) config.tiers = tiers;
|
|
1770
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";
|
|
1771
1772
|
else if (!blocked && !Object.keys(config).length) notes.push("nothing to propose: the default model and the single worker need no config");
|
|
1772
|
-
return { path:
|
|
1773
|
+
return { path: file2, exists: fs9.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
|
|
1773
1774
|
}
|
|
1774
1775
|
function writeInit(plan, force) {
|
|
1775
1776
|
if (plan.blocked) throw new UserError(plan.blocked);
|
|
@@ -2139,8 +2140,8 @@ var ANSWER_MAX2 = 2e5;
|
|
|
2139
2140
|
var cached2;
|
|
2140
2141
|
var historyFile = () => path10.join(home(), "history.db");
|
|
2141
2142
|
function openDb() {
|
|
2142
|
-
const
|
|
2143
|
-
if (cached2?.file ===
|
|
2143
|
+
const file2 = historyFile();
|
|
2144
|
+
if (cached2?.file === file2) return cached2.db;
|
|
2144
2145
|
let db;
|
|
2145
2146
|
try {
|
|
2146
2147
|
const emit = process.emitWarning;
|
|
@@ -2151,14 +2152,14 @@ function openDb() {
|
|
|
2151
2152
|
} finally {
|
|
2152
2153
|
process.emitWarning = emit;
|
|
2153
2154
|
}
|
|
2154
|
-
fs12.mkdirSync(path10.dirname(
|
|
2155
|
-
db = new DatabaseSync(
|
|
2155
|
+
fs12.mkdirSync(path10.dirname(file2), { recursive: true });
|
|
2156
|
+
db = new DatabaseSync(file2);
|
|
2156
2157
|
db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
|
|
2157
2158
|
migrate(db);
|
|
2158
2159
|
} catch {
|
|
2159
2160
|
db = void 0;
|
|
2160
2161
|
}
|
|
2161
|
-
cached2 = { file, db };
|
|
2162
|
+
cached2 = { file: file2, db };
|
|
2162
2163
|
return db;
|
|
2163
2164
|
}
|
|
2164
2165
|
function migrate(db) {
|
|
@@ -2490,7 +2491,7 @@ function structure(lines) {
|
|
|
2490
2491
|
});
|
|
2491
2492
|
return { headings, rules, fenced };
|
|
2492
2493
|
}
|
|
2493
|
-
function parsePlan(text,
|
|
2494
|
+
function parsePlan(text, file2 = "") {
|
|
2494
2495
|
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
2495
2496
|
const { headings, rules, fenced } = structure(lines);
|
|
2496
2497
|
const after = (line) => Math.min(headings.find((h) => h.line > line)?.line ?? lines.length, rules.find((r) => r > line) ?? lines.length);
|
|
@@ -2509,13 +2510,13 @@ function parsePlan(text, file = "") {
|
|
|
2509
2510
|
const tier = /^\s*[-*]?\s*\*\*Worker:\*\*\s*`?([\w-]+)`?/m.exec(unfenced)?.[1]?.toLowerCase();
|
|
2510
2511
|
tasks.push({ step: Number(m[1]), title: m[2].trim(), text: body, ...tier ? { tier } : {} });
|
|
2511
2512
|
}
|
|
2512
|
-
return { file, title: titleHeading?.text ?? "", header, constraints, tasks };
|
|
2513
|
+
return { file: file2, title: titleHeading?.text ?? "", header, constraints, tasks };
|
|
2513
2514
|
}
|
|
2514
|
-
function loadPlan(
|
|
2515
|
-
const abs = path11.resolve(
|
|
2516
|
-
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}`);
|
|
2517
2518
|
const plan = parsePlan(fs13.readFileSync(abs, "utf8"), fs13.realpathSync(abs));
|
|
2518
|
-
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)`);
|
|
2519
2520
|
return plan;
|
|
2520
2521
|
}
|
|
2521
2522
|
function planTask(plan, step) {
|
|
@@ -2533,7 +2534,7 @@ function brief(plan, task) {
|
|
|
2533
2534
|
].filter(Boolean).join("\n\n")}
|
|
2534
2535
|
`;
|
|
2535
2536
|
}
|
|
2536
|
-
var planName = (
|
|
2537
|
+
var planName = (file2) => path11.basename(file2).replace(/\.md$/i, "");
|
|
2537
2538
|
|
|
2538
2539
|
// src/core/report.ts
|
|
2539
2540
|
var ICON2 = {
|
|
@@ -2568,7 +2569,7 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
|
2568
2569
|
const status = meta.taskStatus ? ` \xB7 STATUS ${meta.taskStatus}` : "";
|
|
2569
2570
|
out.push(`plan: ${planName(meta.plan.file)} \xB7 Task ${meta.plan.step}: ${meta.plan.title}${status}`);
|
|
2570
2571
|
}
|
|
2571
|
-
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)})`);
|
|
2572
2573
|
if (meta.error) out.push(`error: ${meta.error}`);
|
|
2573
2574
|
for (const w of meta.warnings) out.push(`warning: ${w}`);
|
|
2574
2575
|
if (meta.state === "timeout") {
|
|
@@ -2789,10 +2790,10 @@ echo "pitroom: needs Node.js 22.13 or newer (set PITROOM_NODE to its path)" >&2
|
|
|
2789
2790
|
exit 127
|
|
2790
2791
|
`;
|
|
2791
2792
|
}
|
|
2792
|
-
function isOurLauncher(
|
|
2793
|
-
if (linksInto(
|
|
2793
|
+
function isOurLauncher(file2, root) {
|
|
2794
|
+
if (linksInto(file2, root)) return true;
|
|
2794
2795
|
try {
|
|
2795
|
-
const s = fs15.readFileSync(
|
|
2796
|
+
const s = fs15.readFileSync(file2, "utf8");
|
|
2796
2797
|
return s.includes(LAUNCHER_MARK) && s.includes(root);
|
|
2797
2798
|
} catch {
|
|
2798
2799
|
return false;
|
|
@@ -2967,17 +2968,84 @@ ${rulings}`,
|
|
|
2967
2968
|
].join("\n");
|
|
2968
2969
|
}
|
|
2969
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
|
+
|
|
2970
3038
|
// src/core/review.ts
|
|
2971
3039
|
import crypto5 from "node:crypto";
|
|
2972
|
-
import
|
|
2973
|
-
import
|
|
3040
|
+
import fs19 from "node:fs";
|
|
3041
|
+
import path16 from "node:path";
|
|
2974
3042
|
|
|
2975
3043
|
// src/vcs/git.ts
|
|
2976
3044
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
2977
3045
|
import crypto4 from "node:crypto";
|
|
2978
|
-
import
|
|
3046
|
+
import fs18 from "node:fs";
|
|
2979
3047
|
import os9 from "node:os";
|
|
2980
|
-
import
|
|
3048
|
+
import path15 from "node:path";
|
|
2981
3049
|
var SAFE = [
|
|
2982
3050
|
"-c",
|
|
2983
3051
|
"color.ui=false",
|
|
@@ -3009,20 +3077,20 @@ function gitAvailable() {
|
|
|
3009
3077
|
}
|
|
3010
3078
|
function repoRoot(dir) {
|
|
3011
3079
|
const r = git(dir, ["rev-parse", "--show-toplevel"]);
|
|
3012
|
-
return r.code === 0 ?
|
|
3080
|
+
return r.code === 0 ? path15.resolve(r.stdout.trim()) : void 0;
|
|
3013
3081
|
}
|
|
3014
3082
|
function snapshotTree(root, exclude = []) {
|
|
3015
|
-
const tmp =
|
|
3016
|
-
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());
|
|
3017
3085
|
const env = { ...process.env, GIT_INDEX_FILE: tmp };
|
|
3018
3086
|
try {
|
|
3019
|
-
if (
|
|
3087
|
+
if (fs18.existsSync(real)) fs18.copyFileSync(real, tmp);
|
|
3020
3088
|
else must(root, ["read-tree", "--empty"], env);
|
|
3021
3089
|
must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`)], env);
|
|
3022
3090
|
return must(root, ["write-tree"], env).trim();
|
|
3023
3091
|
} finally {
|
|
3024
|
-
|
|
3025
|
-
|
|
3092
|
+
fs18.rmSync(tmp, { force: true });
|
|
3093
|
+
fs18.rmSync(`${tmp}.lock`, { force: true });
|
|
3026
3094
|
}
|
|
3027
3095
|
}
|
|
3028
3096
|
var DIFF = ["diff", "--no-ext-diff", "--no-textconv", "--no-renames"];
|
|
@@ -3042,27 +3110,27 @@ function diffTrees(root, a, b) {
|
|
|
3042
3110
|
return { changes, stats: { files: changes.length, insertions, deletions }, patch };
|
|
3043
3111
|
}
|
|
3044
3112
|
function createIsolatedCopy(root, tree, dest) {
|
|
3045
|
-
const objects =
|
|
3046
|
-
|
|
3047
|
-
must(
|
|
3048
|
-
|
|
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}
|
|
3049
3117
|
`);
|
|
3050
3118
|
must(dest, ["read-tree", tree]);
|
|
3051
3119
|
must(dest, ["checkout-index", "--all", "--force"]);
|
|
3052
3120
|
}
|
|
3053
3121
|
function removeIsolatedCopy(dest, ownedBy) {
|
|
3054
|
-
const rel =
|
|
3055
|
-
if (!rel || rel.startsWith("..") ||
|
|
3056
|
-
|
|
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 });
|
|
3057
3125
|
}
|
|
3058
3126
|
function linkIntoWorktree(root, worktree, rels) {
|
|
3059
3127
|
const linked = [];
|
|
3060
3128
|
for (const rel of rels) {
|
|
3061
|
-
const src =
|
|
3062
|
-
const dst =
|
|
3063
|
-
if (!
|
|
3064
|
-
|
|
3065
|
-
|
|
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);
|
|
3066
3134
|
linked.push(rel);
|
|
3067
3135
|
}
|
|
3068
3136
|
return linked;
|
|
@@ -3121,7 +3189,7 @@ function ancestors(m) {
|
|
|
3121
3189
|
function briefOf(m) {
|
|
3122
3190
|
const root = [m, ...ancestors(m)].at(-1);
|
|
3123
3191
|
const f = runFile(root.id, "brief.md");
|
|
3124
|
-
return
|
|
3192
|
+
return fs19.existsSync(f) ? fs19.readFileSync(f, "utf8") : root.task;
|
|
3125
3193
|
}
|
|
3126
3194
|
function latestReview(ids) {
|
|
3127
3195
|
for (const id of listRunIds().reverse()) {
|
|
@@ -3135,7 +3203,7 @@ function latestReview(ids) {
|
|
|
3135
3203
|
}
|
|
3136
3204
|
function diffOf(m, from = m.baseTree) {
|
|
3137
3205
|
const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
|
|
3138
|
-
if (where &&
|
|
3206
|
+
if (where && fs19.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
|
|
3139
3207
|
if (from !== m.baseTree) {
|
|
3140
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);
|
|
3141
3209
|
}
|
|
@@ -3149,7 +3217,7 @@ function runReview(id) {
|
|
|
3149
3217
|
if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
|
|
3150
3218
|
const chain = ancestors(m);
|
|
3151
3219
|
const previous = latestReview(chain.map((a) => a.id));
|
|
3152
|
-
const dir = m.mode === "isolate" && m.worktree &&
|
|
3220
|
+
const dir = m.mode === "isolate" && m.worktree && fs19.existsSync(m.worktree) ? m.cwd : m.dir;
|
|
3153
3221
|
const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
|
|
3154
3222
|
const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
|
|
3155
3223
|
if (!previous) {
|
|
@@ -3223,15 +3291,15 @@ function pickReviewer(job) {
|
|
|
3223
3291
|
function writePackage(job) {
|
|
3224
3292
|
const g = gitDir(job.dir);
|
|
3225
3293
|
if (!g) throw new UserError(`not a git repository: ${job.dir}`);
|
|
3226
|
-
const
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
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;
|
|
3230
3298
|
}
|
|
3231
3299
|
|
|
3232
3300
|
// src/core/templates.ts
|
|
3233
|
-
import
|
|
3234
|
-
import
|
|
3301
|
+
import fs20 from "node:fs";
|
|
3302
|
+
import path17 from "node:path";
|
|
3235
3303
|
var FILES = {
|
|
3236
3304
|
implementer: "pitroom-driven-development/implementer-prompt.md",
|
|
3237
3305
|
"task-reviewer": "pitroom-driven-development/task-reviewer-prompt.md",
|
|
@@ -3239,9 +3307,9 @@ var FILES = {
|
|
|
3239
3307
|
"code-reviewer": "pitroom-review/code-reviewer.md"
|
|
3240
3308
|
};
|
|
3241
3309
|
function loadTemplate(name, root = packageRoot()) {
|
|
3242
|
-
const
|
|
3243
|
-
if (!
|
|
3244
|
-
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*/, "");
|
|
3245
3313
|
}
|
|
3246
3314
|
function fill(template, values) {
|
|
3247
3315
|
const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
|
|
@@ -3348,15 +3416,15 @@ function formatModels(t) {
|
|
|
3348
3416
|
// src/core/dash.ts
|
|
3349
3417
|
import { spawn } from "node:child_process";
|
|
3350
3418
|
import crypto6 from "node:crypto";
|
|
3351
|
-
import
|
|
3419
|
+
import fs21 from "node:fs";
|
|
3352
3420
|
import http from "node:http";
|
|
3353
|
-
import
|
|
3421
|
+
import path18 from "node:path";
|
|
3354
3422
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
3355
3423
|
|
|
3356
3424
|
// src/core/file-diff.ts
|
|
3357
3425
|
var PREVIEW_LINES = 300;
|
|
3358
3426
|
function gitPath(value, prefix = true) {
|
|
3359
|
-
let
|
|
3427
|
+
let path26 = value;
|
|
3360
3428
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
3361
3429
|
const bytes = [];
|
|
3362
3430
|
const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
|
|
@@ -3364,9 +3432,9 @@ function gitPath(value, prefix = true) {
|
|
|
3364
3432
|
if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
|
|
3365
3433
|
else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
|
|
3366
3434
|
}
|
|
3367
|
-
|
|
3435
|
+
path26 = new TextDecoder().decode(new Uint8Array(bytes));
|
|
3368
3436
|
}
|
|
3369
|
-
return prefix ?
|
|
3437
|
+
return prefix ? path26.replace(/^[ab]\//, "") : path26;
|
|
3370
3438
|
}
|
|
3371
3439
|
function headerPath(header) {
|
|
3372
3440
|
const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
|
|
@@ -3375,7 +3443,7 @@ function headerPath(header) {
|
|
|
3375
3443
|
}
|
|
3376
3444
|
function fileDiffs(patch, changes, sourceTruncated = false) {
|
|
3377
3445
|
const files = [];
|
|
3378
|
-
let
|
|
3446
|
+
let file2;
|
|
3379
3447
|
let oldLine = 0;
|
|
3380
3448
|
let newLine = 0;
|
|
3381
3449
|
let oldLeft = 0;
|
|
@@ -3383,25 +3451,25 @@ function fileDiffs(patch, changes, sourceTruncated = false) {
|
|
|
3383
3451
|
let inHunk = false;
|
|
3384
3452
|
let row = 0;
|
|
3385
3453
|
const finishHunk = () => {
|
|
3386
|
-
if (
|
|
3454
|
+
if (file2 && (oldLeft > 0 || newLeft > 0)) file2.incomplete = true;
|
|
3387
3455
|
oldLeft = newLeft = 0;
|
|
3388
3456
|
inHunk = false;
|
|
3389
3457
|
};
|
|
3390
3458
|
const add = (line) => {
|
|
3391
|
-
if (!
|
|
3392
|
-
if (
|
|
3393
|
-
else
|
|
3459
|
+
if (!file2) return;
|
|
3460
|
+
if (file2.lines.length < PREVIEW_LINES) file2.lines.push({ ...line, id: String(row) });
|
|
3461
|
+
else file2.omittedLines++;
|
|
3394
3462
|
row++;
|
|
3395
3463
|
};
|
|
3396
3464
|
for (const line of patch.split("\n")) {
|
|
3397
3465
|
if (line.startsWith("diff --git ")) {
|
|
3398
3466
|
finishHunk();
|
|
3399
|
-
|
|
3400
|
-
files.push(
|
|
3467
|
+
file2 = { path: headerPath(line), status: "M", lines: [], additions: 0, deletions: 0, omittedLines: 0, binary: false, incomplete: false };
|
|
3468
|
+
files.push(file2);
|
|
3401
3469
|
row = 0;
|
|
3402
3470
|
continue;
|
|
3403
3471
|
}
|
|
3404
|
-
if (!
|
|
3472
|
+
if (!file2) continue;
|
|
3405
3473
|
const hunk = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line);
|
|
3406
3474
|
if (hunk) {
|
|
3407
3475
|
finishHunk();
|
|
@@ -3415,11 +3483,11 @@ function fileDiffs(patch, changes, sourceTruncated = false) {
|
|
|
3415
3483
|
add({ type: "meta", content: line });
|
|
3416
3484
|
} else if (inHunk && (oldLeft > 0 || newLeft > 0)) {
|
|
3417
3485
|
if (line.startsWith("+") && newLeft > 0) {
|
|
3418
|
-
|
|
3486
|
+
file2.additions++;
|
|
3419
3487
|
newLeft--;
|
|
3420
3488
|
add({ type: "added", newLine: newLine++, content: line.slice(1) });
|
|
3421
3489
|
} else if (line.startsWith("-") && oldLeft > 0) {
|
|
3422
|
-
|
|
3490
|
+
file2.deletions++;
|
|
3423
3491
|
oldLeft--;
|
|
3424
3492
|
add({ type: "removed", oldLine: oldLine++, content: line.slice(1) });
|
|
3425
3493
|
} else if (line.startsWith(" ") && oldLeft > 0 && newLeft > 0) {
|
|
@@ -3427,25 +3495,25 @@ function fileDiffs(patch, changes, sourceTruncated = false) {
|
|
|
3427
3495
|
newLeft--;
|
|
3428
3496
|
add({ type: "context", oldLine: oldLine++, newLine: newLine++, content: line.slice(1) });
|
|
3429
3497
|
} else {
|
|
3430
|
-
|
|
3498
|
+
file2.incomplete = true;
|
|
3431
3499
|
}
|
|
3432
3500
|
} else if (!inHunk) {
|
|
3433
|
-
if (line.startsWith("+++ ") && line !== "+++ /dev/null")
|
|
3434
|
-
else if (line.startsWith("--- ") && line !== "--- /dev/null")
|
|
3435
|
-
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;
|
|
3436
3504
|
else if (/^(new file mode|deleted file mode|old mode|new mode|rename from|rename to|similarity index) /.test(line)) {
|
|
3437
|
-
if (line.startsWith("new file mode "))
|
|
3438
|
-
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";
|
|
3439
3507
|
if (line.startsWith("rename to ")) {
|
|
3440
|
-
|
|
3441
|
-
|
|
3508
|
+
file2.path = gitPath(line.slice(10), false);
|
|
3509
|
+
file2.status = "R";
|
|
3442
3510
|
}
|
|
3443
3511
|
add({ type: "meta", content: line });
|
|
3444
3512
|
}
|
|
3445
3513
|
}
|
|
3446
3514
|
}
|
|
3447
3515
|
finishHunk();
|
|
3448
|
-
if (sourceTruncated &&
|
|
3516
|
+
if (sourceTruncated && file2) file2.incomplete = true;
|
|
3449
3517
|
for (const f of files) {
|
|
3450
3518
|
if (f.incomplete || f.binary) f.additions = f.deletions = void 0;
|
|
3451
3519
|
}
|
|
@@ -3614,11 +3682,11 @@ var assetsDir = () => fileURLToPath3(new URL("./ui/", import.meta.url));
|
|
|
3614
3682
|
var assetCache = /* @__PURE__ */ new Map();
|
|
3615
3683
|
function readAsset(route) {
|
|
3616
3684
|
try {
|
|
3617
|
-
const
|
|
3618
|
-
const { mtimeMs } =
|
|
3685
|
+
const file2 = path18.join(assetsDir(), path18.basename(route));
|
|
3686
|
+
const { mtimeMs } = fs21.statSync(file2);
|
|
3619
3687
|
const hit = assetCache.get(route);
|
|
3620
3688
|
if (hit && hit.mtimeMs === mtimeMs) return hit.data;
|
|
3621
|
-
const data =
|
|
3689
|
+
const data = fs21.readFileSync(file2);
|
|
3622
3690
|
assetCache.set(route, { mtimeMs, data });
|
|
3623
3691
|
return data;
|
|
3624
3692
|
} catch {
|
|
@@ -3698,7 +3766,7 @@ function startDash(opts) {
|
|
|
3698
3766
|
});
|
|
3699
3767
|
});
|
|
3700
3768
|
}
|
|
3701
|
-
var registryFile = () =>
|
|
3769
|
+
var registryFile = () => path18.join(home(), "dash.json");
|
|
3702
3770
|
async function ping(port) {
|
|
3703
3771
|
try {
|
|
3704
3772
|
const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
|
|
@@ -3709,11 +3777,11 @@ async function ping(port) {
|
|
|
3709
3777
|
}
|
|
3710
3778
|
async function runningDash() {
|
|
3711
3779
|
try {
|
|
3712
|
-
const r = JSON.parse(
|
|
3780
|
+
const r = JSON.parse(fs21.readFileSync(registryFile(), "utf8"));
|
|
3713
3781
|
if (isAlive(r.pid) && await ping(r.port)) return { ...r, url: `http://127.0.0.1:${r.port}/` };
|
|
3714
3782
|
} catch {
|
|
3715
3783
|
}
|
|
3716
|
-
|
|
3784
|
+
fs21.rmSync(registryFile(), { force: true });
|
|
3717
3785
|
return void 0;
|
|
3718
3786
|
}
|
|
3719
3787
|
function openBrowser(url) {
|
|
@@ -3728,7 +3796,7 @@ async function dashCommand(o) {
|
|
|
3728
3796
|
if (o.stop) {
|
|
3729
3797
|
if (!existing) throw new UserError("pitroom dash is not running");
|
|
3730
3798
|
process.kill(existing.pid, "SIGTERM");
|
|
3731
|
-
|
|
3799
|
+
fs21.rmSync(registryFile(), { force: true });
|
|
3732
3800
|
o.log(`stopped pitroom dash (${existing.url})`);
|
|
3733
3801
|
return 0;
|
|
3734
3802
|
}
|
|
@@ -3765,8 +3833,8 @@ async function dashCommand(o) {
|
|
|
3765
3833
|
if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
|
|
3766
3834
|
dash = await startDash({ port: 0, idleMs: o.idleMs });
|
|
3767
3835
|
}
|
|
3768
|
-
|
|
3769
|
-
|
|
3836
|
+
fs21.mkdirSync(home(), { recursive: true });
|
|
3837
|
+
fs21.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
|
|
3770
3838
|
const stop = () => void dash.close();
|
|
3771
3839
|
process.once("SIGINT", stop);
|
|
3772
3840
|
process.once("SIGTERM", stop);
|
|
@@ -3775,7 +3843,7 @@ async function dashCommand(o) {
|
|
|
3775
3843
|
if (o.open) openBrowser(dash.url);
|
|
3776
3844
|
await dash.closed;
|
|
3777
3845
|
try {
|
|
3778
|
-
if (JSON.parse(
|
|
3846
|
+
if (JSON.parse(fs21.readFileSync(registryFile(), "utf8")).pid === process.pid) fs21.rmSync(registryFile(), { force: true });
|
|
3779
3847
|
} catch {
|
|
3780
3848
|
}
|
|
3781
3849
|
return 0;
|
|
@@ -3783,8 +3851,8 @@ async function dashCommand(o) {
|
|
|
3783
3851
|
|
|
3784
3852
|
// src/core/run.ts
|
|
3785
3853
|
import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
|
|
3786
|
-
import
|
|
3787
|
-
import
|
|
3854
|
+
import fs27 from "node:fs";
|
|
3855
|
+
import path23 from "node:path";
|
|
3788
3856
|
|
|
3789
3857
|
// src/core/chain.ts
|
|
3790
3858
|
function resolveChain(flags = {}) {
|
|
@@ -3834,11 +3902,11 @@ function resolveChain(flags = {}) {
|
|
|
3834
3902
|
|
|
3835
3903
|
// src/core/process.ts
|
|
3836
3904
|
import { spawn as spawn2 } from "node:child_process";
|
|
3837
|
-
import
|
|
3905
|
+
import fs23 from "node:fs";
|
|
3838
3906
|
|
|
3839
3907
|
// src/vcs/guard.ts
|
|
3840
|
-
import
|
|
3841
|
-
import
|
|
3908
|
+
import fs22 from "node:fs";
|
|
3909
|
+
import path19 from "node:path";
|
|
3842
3910
|
var VERSION = 1;
|
|
3843
3911
|
var SHIM = `#!/bin/sh
|
|
3844
3912
|
# pitroom git guard v${VERSION}. Blocks git commands that change history, refs, the
|
|
@@ -3913,15 +3981,15 @@ esac
|
|
|
3913
3981
|
exec "$real" "$@"
|
|
3914
3982
|
`;
|
|
3915
3983
|
function shimDir() {
|
|
3916
|
-
return
|
|
3984
|
+
return path19.join(home(), "shim", `v${VERSION}`);
|
|
3917
3985
|
}
|
|
3918
3986
|
function findRealGit(skip) {
|
|
3919
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
3920
|
-
if (
|
|
3921
|
-
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");
|
|
3922
3990
|
try {
|
|
3923
|
-
|
|
3924
|
-
if (
|
|
3991
|
+
fs22.accessSync(p, fs22.constants.X_OK);
|
|
3992
|
+
if (fs22.statSync(p).isFile()) return p;
|
|
3925
3993
|
} catch {
|
|
3926
3994
|
}
|
|
3927
3995
|
}
|
|
@@ -3934,13 +4002,13 @@ var REF_HOOK = `#!/bin/sh
|
|
|
3934
4002
|
echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
|
|
3935
4003
|
exit 1
|
|
3936
4004
|
`;
|
|
3937
|
-
var hooksDir = () =>
|
|
3938
|
-
function writeIfChanged(
|
|
3939
|
-
if (
|
|
3940
|
-
|
|
3941
|
-
const tmp = `${
|
|
3942
|
-
|
|
3943
|
-
|
|
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);
|
|
3944
4012
|
}
|
|
3945
4013
|
function withGitConfig(env, entries) {
|
|
3946
4014
|
const out = { ...env };
|
|
@@ -3962,7 +4030,7 @@ function guardEnv(env) {
|
|
|
3962
4030
|
PAGER: "cat"
|
|
3963
4031
|
};
|
|
3964
4032
|
if (process.platform === "win32") return { ...env, ...quiet };
|
|
3965
|
-
writeIfChanged(
|
|
4033
|
+
writeIfChanged(path19.join(hooksDir(), "reference-transaction"), REF_HOOK);
|
|
3966
4034
|
env = withGitConfig(env, [
|
|
3967
4035
|
["core.hooksPath", hooksDir()],
|
|
3968
4036
|
["url.pitroom-push-blocked://.pushInsteadOf", ""]
|
|
@@ -3970,14 +4038,14 @@ function guardEnv(env) {
|
|
|
3970
4038
|
const dir = shimDir();
|
|
3971
4039
|
const real = findRealGit(dir);
|
|
3972
4040
|
if (!real) return { ...env, ...quiet };
|
|
3973
|
-
writeIfChanged(
|
|
3974
|
-
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 ?? ""}` };
|
|
3975
4043
|
}
|
|
3976
4044
|
|
|
3977
4045
|
// src/core/process.ts
|
|
3978
4046
|
async function spawnWorker(inv, opts) {
|
|
3979
|
-
const out =
|
|
3980
|
-
const err =
|
|
4047
|
+
const out = fs23.openSync(opts.stdoutFile, "w");
|
|
4048
|
+
const err = fs23.openSync(opts.stderrFile, "w");
|
|
3981
4049
|
const res = { code: null, timedOut: false, stopped: false };
|
|
3982
4050
|
const child = spawn2(inv.command, inv.args, {
|
|
3983
4051
|
cwd: opts.cwd,
|
|
@@ -4009,8 +4077,8 @@ async function spawnWorker(inv, opts) {
|
|
|
4009
4077
|
if (killer) clearTimeout(killer);
|
|
4010
4078
|
process.off("SIGINT", stop);
|
|
4011
4079
|
process.off("SIGTERM", stop);
|
|
4012
|
-
|
|
4013
|
-
|
|
4080
|
+
fs23.closeSync(out);
|
|
4081
|
+
fs23.closeSync(err);
|
|
4014
4082
|
return res;
|
|
4015
4083
|
}
|
|
4016
4084
|
|
|
@@ -4043,10 +4111,10 @@ function parseAudit(text) {
|
|
|
4043
4111
|
|
|
4044
4112
|
// src/core/slots.ts
|
|
4045
4113
|
import crypto7 from "node:crypto";
|
|
4046
|
-
import
|
|
4047
|
-
import
|
|
4048
|
-
var slotsDir = () =>
|
|
4049
|
-
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");
|
|
4050
4118
|
var STARTUP_GRACE_MS = 3e4;
|
|
4051
4119
|
function holderActive(runId, self) {
|
|
4052
4120
|
if (!runId || runId === self) return false;
|
|
@@ -4059,51 +4127,51 @@ function holderActive(runId, self) {
|
|
|
4059
4127
|
return false;
|
|
4060
4128
|
}
|
|
4061
4129
|
}
|
|
4062
|
-
function tryClaim(
|
|
4063
|
-
|
|
4130
|
+
function tryClaim(file2, runId) {
|
|
4131
|
+
fs24.mkdirSync(path20.dirname(file2), { recursive: true });
|
|
4064
4132
|
try {
|
|
4065
|
-
|
|
4133
|
+
fs24.writeFileSync(file2, runId, { flag: "wx" });
|
|
4066
4134
|
return true;
|
|
4067
4135
|
} catch (e) {
|
|
4068
4136
|
if (e.code !== "EEXIST") throw e;
|
|
4069
4137
|
}
|
|
4070
4138
|
let owner = "";
|
|
4071
4139
|
try {
|
|
4072
|
-
owner =
|
|
4140
|
+
owner = fs24.readFileSync(file2, "utf8").trim();
|
|
4073
4141
|
} catch {
|
|
4074
4142
|
}
|
|
4075
4143
|
if (owner === runId) return true;
|
|
4076
4144
|
if (holderActive(owner, runId)) return false;
|
|
4077
|
-
|
|
4145
|
+
fs24.rmSync(file2, { force: true });
|
|
4078
4146
|
try {
|
|
4079
|
-
|
|
4147
|
+
fs24.writeFileSync(file2, runId, { flag: "wx" });
|
|
4080
4148
|
return true;
|
|
4081
4149
|
} catch {
|
|
4082
4150
|
return false;
|
|
4083
4151
|
}
|
|
4084
4152
|
}
|
|
4085
|
-
function releaseIfOwner(
|
|
4086
|
-
if (!
|
|
4153
|
+
function releaseIfOwner(file2, runId) {
|
|
4154
|
+
if (!file2) return;
|
|
4087
4155
|
try {
|
|
4088
|
-
if (
|
|
4156
|
+
if (fs24.readFileSync(file2, "utf8").trim() === runId) fs24.rmSync(file2, { force: true });
|
|
4089
4157
|
} catch {
|
|
4090
4158
|
}
|
|
4091
4159
|
}
|
|
4092
4160
|
function tryAcquireSlot(runId, maxParallel) {
|
|
4093
4161
|
for (let n = 0; n < Math.max(1, maxParallel); n++) {
|
|
4094
|
-
const
|
|
4095
|
-
if (tryClaim(
|
|
4162
|
+
const file2 = path20.join(slotsDir(), `slot-${n}`);
|
|
4163
|
+
if (tryClaim(file2, runId)) return file2;
|
|
4096
4164
|
}
|
|
4097
4165
|
return void 0;
|
|
4098
4166
|
}
|
|
4099
|
-
var releaseSlot = (
|
|
4100
|
-
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)}`);
|
|
4101
4169
|
function acquireWriteLock(repoRoot2, runId) {
|
|
4102
|
-
const
|
|
4103
|
-
if (tryClaim(
|
|
4170
|
+
const file2 = lockFile(repoRoot2);
|
|
4171
|
+
if (tryClaim(file2, runId)) return;
|
|
4104
4172
|
let owner = "";
|
|
4105
4173
|
try {
|
|
4106
|
-
owner =
|
|
4174
|
+
owner = fs24.readFileSync(file2, "utf8").trim();
|
|
4107
4175
|
} catch {
|
|
4108
4176
|
}
|
|
4109
4177
|
throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
|
|
@@ -4150,8 +4218,8 @@ ${task}`;
|
|
|
4150
4218
|
}
|
|
4151
4219
|
|
|
4152
4220
|
// src/core/refs.ts
|
|
4153
|
-
import
|
|
4154
|
-
import
|
|
4221
|
+
import fs25 from "node:fs";
|
|
4222
|
+
import path21 from "node:path";
|
|
4155
4223
|
var FILE_EXTENSIONS = new Set(
|
|
4156
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(" ")
|
|
4157
4225
|
);
|
|
@@ -4202,29 +4270,29 @@ function extractRefs(answer) {
|
|
|
4202
4270
|
return [...seen.values()];
|
|
4203
4271
|
}
|
|
4204
4272
|
function verifyRefs(refs, dirs) {
|
|
4205
|
-
const roots = [...new Set(dirs.filter(Boolean).map((d) =>
|
|
4273
|
+
const roots = [...new Set(dirs.filter(Boolean).map((d) => path21.resolve(d)))];
|
|
4206
4274
|
const invalid = [];
|
|
4207
4275
|
const lineCache = /* @__PURE__ */ new Map();
|
|
4208
|
-
const load = (
|
|
4209
|
-
if (!lineCache.has(
|
|
4210
|
-
const size =
|
|
4211
|
-
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");
|
|
4212
4280
|
if (lines && lines[lines.length - 1] === "") lines.pop();
|
|
4213
|
-
lineCache.set(
|
|
4281
|
+
lineCache.set(file2, lines);
|
|
4214
4282
|
}
|
|
4215
|
-
return lineCache.get(
|
|
4283
|
+
return lineCache.get(file2);
|
|
4216
4284
|
};
|
|
4217
4285
|
let index;
|
|
4218
4286
|
const byName = () => index ??= indexFiles(roots);
|
|
4219
4287
|
const candidates = (ref) => {
|
|
4220
4288
|
const direct = resolve(ref.file, roots);
|
|
4221
4289
|
if (direct) return [direct];
|
|
4222
|
-
if (
|
|
4290
|
+
if (path21.isAbsolute(ref.file)) return [];
|
|
4223
4291
|
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
4224
|
-
return (byName().get(
|
|
4292
|
+
return (byName().get(path21.basename(wanted)) ?? []).filter((f) => f.endsWith(`/${wanted}`) || path21.basename(f) === wanted).slice(0, 20);
|
|
4225
4293
|
};
|
|
4226
|
-
const check = (
|
|
4227
|
-
const lines = load(
|
|
4294
|
+
const check = (file2, ref) => {
|
|
4295
|
+
const lines = load(file2);
|
|
4228
4296
|
if (!lines) return void 0;
|
|
4229
4297
|
if (ref.end > lines.length) return `file has ${lines.length} lines`;
|
|
4230
4298
|
if (ref.symbol && !mentions(lines, ref) && !enclosedBy(lines, ref)) return `\`${ref.symbol}\` not near line ${ref.start}`;
|
|
@@ -4236,7 +4304,7 @@ function verifyRefs(refs, dirs) {
|
|
|
4236
4304
|
if (!files.length) {
|
|
4237
4305
|
if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
|
|
4238
4306
|
total++;
|
|
4239
|
-
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" });
|
|
4240
4308
|
continue;
|
|
4241
4309
|
}
|
|
4242
4310
|
total++;
|
|
@@ -4257,7 +4325,7 @@ function enclosedBy(lines, ref) {
|
|
|
4257
4325
|
return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
|
|
4258
4326
|
}
|
|
4259
4327
|
var isFileName = (name) => {
|
|
4260
|
-
const base2 =
|
|
4328
|
+
const base2 = path21.basename(name);
|
|
4261
4329
|
return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
|
|
4262
4330
|
};
|
|
4263
4331
|
function indexFiles(roots) {
|
|
@@ -4266,19 +4334,19 @@ function indexFiles(roots) {
|
|
|
4266
4334
|
const walk = (dir) => {
|
|
4267
4335
|
let entries;
|
|
4268
4336
|
try {
|
|
4269
|
-
entries =
|
|
4337
|
+
entries = fs25.readdirSync(dir, { withFileTypes: true });
|
|
4270
4338
|
} catch {
|
|
4271
4339
|
return;
|
|
4272
4340
|
}
|
|
4273
4341
|
for (const e of entries) {
|
|
4274
4342
|
if (count >= MAX_INDEXED) return;
|
|
4275
4343
|
if (e.isDirectory()) {
|
|
4276
|
-
if (!SKIP_DIRS.has(e.name)) walk(
|
|
4344
|
+
if (!SKIP_DIRS.has(e.name)) walk(path21.join(dir, e.name));
|
|
4277
4345
|
} else if (e.isFile()) {
|
|
4278
4346
|
count++;
|
|
4279
4347
|
const list2 = index.get(e.name);
|
|
4280
|
-
if (list2) list2.push(
|
|
4281
|
-
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)]);
|
|
4282
4350
|
}
|
|
4283
4351
|
}
|
|
4284
4352
|
};
|
|
@@ -4286,27 +4354,27 @@ function indexFiles(roots) {
|
|
|
4286
4354
|
return index;
|
|
4287
4355
|
}
|
|
4288
4356
|
function resolve(ref, roots) {
|
|
4289
|
-
const candidates =
|
|
4357
|
+
const candidates = path21.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path21.join(r, ref));
|
|
4290
4358
|
return candidates.find((c) => {
|
|
4291
4359
|
try {
|
|
4292
|
-
return
|
|
4360
|
+
return fs25.statSync(c).isFile() && inside(c, roots);
|
|
4293
4361
|
} catch {
|
|
4294
4362
|
return false;
|
|
4295
4363
|
}
|
|
4296
4364
|
});
|
|
4297
4365
|
}
|
|
4298
|
-
function inside(
|
|
4366
|
+
function inside(file2, roots) {
|
|
4299
4367
|
return roots.some((r) => {
|
|
4300
|
-
const rel =
|
|
4301
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
4368
|
+
const rel = path21.relative(r, path21.resolve(file2));
|
|
4369
|
+
return rel !== "" && !rel.startsWith("..") && !path21.isAbsolute(rel);
|
|
4302
4370
|
});
|
|
4303
4371
|
}
|
|
4304
4372
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4305
4373
|
|
|
4306
4374
|
// src/core/secrets.ts
|
|
4307
4375
|
import { execFileSync } from "node:child_process";
|
|
4308
|
-
import
|
|
4309
|
-
import
|
|
4376
|
+
import fs26 from "node:fs";
|
|
4377
|
+
import path22 from "node:path";
|
|
4310
4378
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
4311
4379
|
var MAX_VISITED = 2e4;
|
|
4312
4380
|
var MAX_DEPTH = 4;
|
|
@@ -4322,16 +4390,16 @@ function findSecretFiles(dir) {
|
|
|
4322
4390
|
if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
|
|
4323
4391
|
let entries;
|
|
4324
4392
|
try {
|
|
4325
|
-
entries =
|
|
4393
|
+
entries = fs26.readdirSync(d, { withFileTypes: true });
|
|
4326
4394
|
} catch {
|
|
4327
4395
|
return;
|
|
4328
4396
|
}
|
|
4329
4397
|
for (const e of entries) {
|
|
4330
4398
|
visited++;
|
|
4331
4399
|
if (e.isDirectory()) {
|
|
4332
|
-
if (!SKIP_DIRS2.has(e.name)) walk(
|
|
4400
|
+
if (!SKIP_DIRS2.has(e.name)) walk(path22.join(d, e.name), depth + 1);
|
|
4333
4401
|
} else if (e.isFile() && looksSecret(e.name)) {
|
|
4334
|
-
found.push(
|
|
4402
|
+
found.push(path22.relative(dir, path22.join(d, e.name)));
|
|
4335
4403
|
}
|
|
4336
4404
|
}
|
|
4337
4405
|
};
|
|
@@ -4341,7 +4409,7 @@ function findSecretFiles(dir) {
|
|
|
4341
4409
|
function findSecretFilesInTree(root, dir) {
|
|
4342
4410
|
try {
|
|
4343
4411
|
const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
4344
|
-
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();
|
|
4345
4413
|
} catch {
|
|
4346
4414
|
return [];
|
|
4347
4415
|
}
|
|
@@ -4355,7 +4423,7 @@ function secretWarning(files, mode) {
|
|
|
4355
4423
|
}
|
|
4356
4424
|
|
|
4357
4425
|
// src/core/run.ts
|
|
4358
|
-
var VERSION2 = true ? "0.
|
|
4426
|
+
var VERSION2 = true ? "0.12.0" : "0.0.0-dev";
|
|
4359
4427
|
function planWork(o) {
|
|
4360
4428
|
if (!o.plan) return { task: o.task.trim(), tier: o.tier };
|
|
4361
4429
|
if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
|
|
@@ -4387,7 +4455,7 @@ function prepareRun(o) {
|
|
|
4387
4455
|
parent = readMeta(o.continueFrom);
|
|
4388
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");
|
|
4389
4457
|
if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
|
|
4390
|
-
if (parent.mode === "isolate" && (!parent.worktree || !
|
|
4458
|
+
if (parent.mode === "isolate" && (!parent.worktree || !fs27.existsSync(parent.worktree))) {
|
|
4391
4459
|
throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
|
|
4392
4460
|
}
|
|
4393
4461
|
const ran = parent.ran ?? parent.worker;
|
|
@@ -4406,15 +4474,15 @@ function prepareRun(o) {
|
|
|
4406
4474
|
throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
|
|
4407
4475
|
}
|
|
4408
4476
|
const mode = parent?.mode ?? o.mode;
|
|
4409
|
-
const dir =
|
|
4410
|
-
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}`);
|
|
4411
4479
|
const root = repoRoot(dir);
|
|
4412
4480
|
if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
|
|
4413
4481
|
if (mode === "write" && !root && !o.allowNonGit) {
|
|
4414
4482
|
throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
|
|
4415
4483
|
}
|
|
4416
|
-
const files = o.files.map((f) =>
|
|
4417
|
-
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}`);
|
|
4418
4486
|
if (!parent && !process.env.PITROOM_NO_SECRET_WARNING) {
|
|
4419
4487
|
const secrets = mode === "isolate" && root ? findSecretFilesInTree(root, dir) : findSecretFiles(dir);
|
|
4420
4488
|
const note = secretWarning(secrets, mode);
|
|
@@ -4456,13 +4524,13 @@ function prepareRun(o) {
|
|
|
4456
4524
|
try {
|
|
4457
4525
|
acquireWriteLock(root, meta.id);
|
|
4458
4526
|
} catch (e) {
|
|
4459
|
-
|
|
4527
|
+
fs27.rmSync(runDir(meta.id), { recursive: true, force: true });
|
|
4460
4528
|
throw e;
|
|
4461
4529
|
}
|
|
4462
4530
|
}
|
|
4463
|
-
|
|
4531
|
+
fs27.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
|
|
4464
4532
|
`);
|
|
4465
|
-
if (work.brief)
|
|
4533
|
+
if (work.brief) fs27.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
|
|
4466
4534
|
return meta;
|
|
4467
4535
|
}
|
|
4468
4536
|
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -4519,12 +4587,19 @@ async function execute(meta) {
|
|
|
4519
4587
|
for (let i = 0; i < chain.length; i++) {
|
|
4520
4588
|
const target = chain[i];
|
|
4521
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
|
+
}
|
|
4522
4596
|
meta.ran = target;
|
|
4523
4597
|
writeMeta(meta);
|
|
4524
4598
|
result = await attempt(meta, backend, target);
|
|
4525
4599
|
if (result.timedOut || result.stopped || result.spawnError) break;
|
|
4526
4600
|
const why = retryableFailure(meta, backend, result);
|
|
4527
4601
|
if (!why) break;
|
|
4602
|
+
if (why.kind === "rate-limited") recordCooldown(cooldownKey(target, backend), describeTarget(target), why.message);
|
|
4528
4603
|
if (!target.model && backend.defaultModel) {
|
|
4529
4604
|
const failed = backend.defaultModel();
|
|
4530
4605
|
for (let j = chain.length - 1; j > i; j--) {
|
|
@@ -4535,7 +4610,7 @@ async function execute(meta) {
|
|
|
4535
4610
|
(meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
|
|
4536
4611
|
for (const f of ["events.jsonl", "stderr.log"]) {
|
|
4537
4612
|
const from = runFile(meta.id, f);
|
|
4538
|
-
if (
|
|
4613
|
+
if (fs27.existsSync(from)) fs27.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
|
|
4539
4614
|
}
|
|
4540
4615
|
writeMeta(meta);
|
|
4541
4616
|
}
|
|
@@ -4554,13 +4629,13 @@ async function execute(meta) {
|
|
|
4554
4629
|
}
|
|
4555
4630
|
function autoAudit(meta) {
|
|
4556
4631
|
try {
|
|
4557
|
-
if (!auditable(meta,
|
|
4632
|
+
if (!auditable(meta, read2(runFile(meta.id, "summary.md"))) || !sampled(meta)) return;
|
|
4558
4633
|
startAudit(meta);
|
|
4559
4634
|
} catch {
|
|
4560
4635
|
}
|
|
4561
4636
|
}
|
|
4562
4637
|
function startAudit(meta, worker) {
|
|
4563
|
-
const answer =
|
|
4638
|
+
const answer = read2(runFile(meta.id, "summary.md"));
|
|
4564
4639
|
const auditor = worker ?? pickAuditor(meta);
|
|
4565
4640
|
if (!auditor) return void 0;
|
|
4566
4641
|
const a = prepareRun({
|
|
@@ -4596,9 +4671,9 @@ function prepareTree(meta) {
|
|
|
4596
4671
|
if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
|
|
4597
4672
|
if (root && meta.mode === "isolate" && !meta.worktree) {
|
|
4598
4673
|
meta.baseTree = snapshotTree(root);
|
|
4599
|
-
meta.worktree =
|
|
4674
|
+
meta.worktree = path23.join(worktreesDir(), meta.id);
|
|
4600
4675
|
createIsolatedCopy(root, meta.baseTree, meta.worktree);
|
|
4601
|
-
meta.cwd =
|
|
4676
|
+
meta.cwd = path23.join(meta.worktree, path23.relative(root, meta.dir));
|
|
4602
4677
|
const linked = linkIntoWorktree(root, meta.worktree, meta.link);
|
|
4603
4678
|
if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
|
|
4604
4679
|
}
|
|
@@ -4621,11 +4696,11 @@ function attempt(meta, backend, target) {
|
|
|
4621
4696
|
timeoutSec: meta.timeoutSec
|
|
4622
4697
|
});
|
|
4623
4698
|
}
|
|
4624
|
-
var
|
|
4699
|
+
var read2 = (f) => fs27.existsSync(f) ? fs27.readFileSync(f, "utf8") : "";
|
|
4625
4700
|
function retryableFailure(meta, backend, res) {
|
|
4626
|
-
const run = backend.parse(
|
|
4701
|
+
const run = backend.parse(read2(runFile(meta.id, "events.jsonl")));
|
|
4627
4702
|
if (run.usage.steps > 0) return void 0;
|
|
4628
|
-
const f = backend.failure(run,
|
|
4703
|
+
const f = backend.failure(run, read2(runFile(meta.id, "stderr.log")), res.code);
|
|
4629
4704
|
return f && f.kind !== "other" ? f : void 0;
|
|
4630
4705
|
}
|
|
4631
4706
|
var HINTS = {
|
|
@@ -4637,18 +4712,18 @@ var HINTS = {
|
|
|
4637
4712
|
function finalize(meta, res) {
|
|
4638
4713
|
const ran = meta.ran ?? meta.worker;
|
|
4639
4714
|
const backend = getBackend(ran.backend);
|
|
4640
|
-
const run = backend.parse(
|
|
4715
|
+
const run = backend.parse(read2(runFile(meta.id, "events.jsonl")));
|
|
4641
4716
|
meta.sessionId = run.sessionId ?? meta.sessionId;
|
|
4642
4717
|
meta.usage = run.usage;
|
|
4643
|
-
|
|
4718
|
+
fs27.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
|
|
4644
4719
|
`);
|
|
4645
4720
|
if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run.finalText);
|
|
4646
4721
|
if (meta.reviewOf) {
|
|
4647
4722
|
meta.verdict = parseVerdict(run.finalText);
|
|
4648
|
-
if (meta.packageFile)
|
|
4723
|
+
if (meta.packageFile) fs27.rmSync(meta.packageFile, { force: true });
|
|
4649
4724
|
}
|
|
4650
4725
|
if (!res.timedOut && !res.stopped && !meta.error) {
|
|
4651
|
-
const f = backend.failure(run,
|
|
4726
|
+
const f = backend.failure(run, read2(runFile(meta.id, "stderr.log")), res.code);
|
|
4652
4727
|
if (f) meta.error = HINTS[f.kind] ? `${f.message} (${HINTS[f.kind]})` : f.message;
|
|
4653
4728
|
}
|
|
4654
4729
|
try {
|
|
@@ -4693,7 +4768,7 @@ function captureChanges(meta) {
|
|
|
4693
4768
|
const d = diffTrees(target, meta.baseTree, meta.afterTree);
|
|
4694
4769
|
meta.changes = d.changes;
|
|
4695
4770
|
meta.stats = d.stats;
|
|
4696
|
-
|
|
4771
|
+
fs27.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
|
|
4697
4772
|
}
|
|
4698
4773
|
function runVerify(meta) {
|
|
4699
4774
|
const r = spawnSync6(meta.verify, {
|
|
@@ -4706,7 +4781,7 @@ function runVerify(meta) {
|
|
|
4706
4781
|
env: { ...process.env, PWD: meta.cwd, PITROOM_ACTIVE: "1" }
|
|
4707
4782
|
});
|
|
4708
4783
|
const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
|
4709
|
-
|
|
4784
|
+
fs27.writeFileSync(runFile(meta.id, "verify.log"), output);
|
|
4710
4785
|
return { ok: r.status === 0, code: r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
|
|
4711
4786
|
}
|
|
4712
4787
|
function applyRun(meta, allowDelete = false) {
|
|
@@ -4751,7 +4826,7 @@ function discardRun(meta) {
|
|
|
4751
4826
|
return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
|
|
4752
4827
|
}
|
|
4753
4828
|
function cleanupWorktree(meta) {
|
|
4754
|
-
if (meta.worktree &&
|
|
4829
|
+
if (meta.worktree && fs27.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
|
|
4755
4830
|
}
|
|
4756
4831
|
|
|
4757
4832
|
// src/cli/commands.ts
|
|
@@ -4801,10 +4876,10 @@ async function cmdReview(p) {
|
|
|
4801
4876
|
writeMeta(meta);
|
|
4802
4877
|
}
|
|
4803
4878
|
} catch (e) {
|
|
4804
|
-
|
|
4879
|
+
fs28.rmSync(packageFile, { force: true });
|
|
4805
4880
|
throw e;
|
|
4806
4881
|
}
|
|
4807
|
-
|
|
4882
|
+
fs28.writeFileSync(runFile(meta.id, "package.md"), job.package);
|
|
4808
4883
|
return launch(p, meta);
|
|
4809
4884
|
}
|
|
4810
4885
|
async function cmdAudit(p) {
|
|
@@ -4834,24 +4909,45 @@ async function cmdAudit(p) {
|
|
|
4834
4909
|
writeMeta(m);
|
|
4835
4910
|
return launch(p, a);
|
|
4836
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
|
+
}
|
|
4837
4933
|
function cmdPlan(p) {
|
|
4838
|
-
const [sub,
|
|
4839
|
-
if (sub === "status" &&
|
|
4840
|
-
const s = planStatus(
|
|
4934
|
+
const [sub, file2, ...rest] = p.positional;
|
|
4935
|
+
if (sub === "status" && file2) {
|
|
4936
|
+
const s = planStatus(file2);
|
|
4841
4937
|
console.log(
|
|
4842
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)
|
|
4843
4939
|
);
|
|
4844
4940
|
return 0;
|
|
4845
4941
|
}
|
|
4846
|
-
if (sub === "note" &&
|
|
4847
|
-
console.log(`noted in ${addNote(
|
|
4942
|
+
if (sub === "note" && file2 && rest.length) {
|
|
4943
|
+
console.log(`noted in ${addNote(file2, rest.join(" "))}`);
|
|
4848
4944
|
return 0;
|
|
4849
4945
|
}
|
|
4850
4946
|
throw new UserError('usage: pitroom plan status PLAN.md [--json] | pitroom plan note PLAN.md "Task N: \u2026"');
|
|
4851
4947
|
}
|
|
4852
4948
|
function cmdCrew(p) {
|
|
4853
|
-
const
|
|
4854
|
-
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);
|
|
4855
4951
|
if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
|
|
4856
4952
|
if (has(p, "write") && tasks.length > 1) {
|
|
4857
4953
|
throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
|
|
@@ -5052,7 +5148,7 @@ function sinceMs(s) {
|
|
|
5052
5148
|
if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
|
|
5053
5149
|
return Date.now() - Number(m[1]) * 864e5;
|
|
5054
5150
|
}
|
|
5055
|
-
var readStdin = () => process.stdin.isTTY ? "" :
|
|
5151
|
+
var readStdin = () => process.stdin.isTTY ? "" : fs28.readFileSync(0, "utf8");
|
|
5056
5152
|
function cmdStatusline(p) {
|
|
5057
5153
|
const input = readStdin();
|
|
5058
5154
|
const lines = [];
|
|
@@ -5092,8 +5188,8 @@ function cmdModels(p) {
|
|
|
5092
5188
|
return 0;
|
|
5093
5189
|
}
|
|
5094
5190
|
function cmdHookStart() {
|
|
5095
|
-
const script =
|
|
5096
|
-
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 });
|
|
5097
5193
|
return 0;
|
|
5098
5194
|
}
|
|
5099
5195
|
function cmdInit(p) {
|
|
@@ -5133,8 +5229,8 @@ function cmdSavings(p) {
|
|
|
5133
5229
|
est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
|
|
5134
5230
|
const out = flag(p, "card");
|
|
5135
5231
|
if (out) {
|
|
5136
|
-
|
|
5137
|
-
console.log(`card written to ${
|
|
5232
|
+
fs28.writeFileSync(out, card(t, period));
|
|
5233
|
+
console.log(`card written to ${path24.resolve(out)}`);
|
|
5138
5234
|
}
|
|
5139
5235
|
if (has(p, "badge")) console.log(`})`);
|
|
5140
5236
|
return 0;
|
|
@@ -5154,7 +5250,7 @@ function cmdConfig(p) {
|
|
|
5154
5250
|
console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
|
|
5155
5251
|
return 0;
|
|
5156
5252
|
}
|
|
5157
|
-
console.log(`config file: ${configPath()}${
|
|
5253
|
+
console.log(`config file: ${configPath()}${fs28.existsSync(configPath()) ? "" : " (not present)"}`);
|
|
5158
5254
|
for (const [key, s] of Object.entries(eff)) {
|
|
5159
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);
|
|
5160
5256
|
console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
|
|
@@ -5178,7 +5274,7 @@ function cmdClean(p) {
|
|
|
5178
5274
|
const m = readMeta(id);
|
|
5179
5275
|
if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
|
|
5180
5276
|
recordRun(m);
|
|
5181
|
-
|
|
5277
|
+
fs28.rmSync(runDir(id), { recursive: true, force: true });
|
|
5182
5278
|
}
|
|
5183
5279
|
console.log(`removed ${old.length} run(s); the history and the savings ledger are kept`);
|
|
5184
5280
|
return 0;
|
|
@@ -5188,9 +5284,9 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
|
|
|
5188
5284
|
|
|
5189
5285
|
// src/core/doctor.ts
|
|
5190
5286
|
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
5191
|
-
import
|
|
5287
|
+
import fs29 from "node:fs";
|
|
5192
5288
|
import os10 from "node:os";
|
|
5193
|
-
import
|
|
5289
|
+
import path25 from "node:path";
|
|
5194
5290
|
var MARK = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
|
|
5195
5291
|
var NEXT = [
|
|
5196
5292
|
{ when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
|
|
@@ -5200,15 +5296,15 @@ var NEXT = [
|
|
|
5200
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" }
|
|
5201
5297
|
];
|
|
5202
5298
|
function firstNodeOnPath() {
|
|
5203
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
5204
|
-
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");
|
|
5205
5301
|
try {
|
|
5206
|
-
if (!
|
|
5302
|
+
if (!fs29.statSync(file2).isFile()) continue;
|
|
5207
5303
|
} catch {
|
|
5208
5304
|
continue;
|
|
5209
5305
|
}
|
|
5210
|
-
const r = spawnSync8(
|
|
5211
|
-
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: "" };
|
|
5212
5308
|
}
|
|
5213
5309
|
return void 0;
|
|
5214
5310
|
}
|
|
@@ -5230,12 +5326,12 @@ function doctor5(probe) {
|
|
|
5230
5326
|
const addAll = (list2) => list2.forEach((c) => add(c.level, c.message));
|
|
5231
5327
|
add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
|
|
5232
5328
|
const onPath = firstNodeOnPath();
|
|
5233
|
-
if (onPath && onPath.version && tooOld(onPath.version) &&
|
|
5329
|
+
if (onPath && onPath.version && tooOld(onPath.version) && path25.resolve(onPath.path) !== path25.resolve(process.execPath)) {
|
|
5234
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)`);
|
|
5235
5331
|
}
|
|
5236
5332
|
add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
|
|
5237
5333
|
const cfg = loadConfig();
|
|
5238
|
-
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)"}`);
|
|
5239
5335
|
for (const w of cfg.warnings) add("warn", w);
|
|
5240
5336
|
if (process.platform !== "win32") {
|
|
5241
5337
|
const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
|
|
@@ -5277,6 +5373,9 @@ function doctor5(probe) {
|
|
|
5277
5373
|
add("ok", `cost: ${key} = ${mine}${cheaper ? `; you priced ${cheaper[0]} cheaper (${cheaper[1]}): is the dearer one needed?` : ""}`);
|
|
5278
5374
|
}
|
|
5279
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
|
+
}
|
|
5280
5379
|
const byBackend = /* @__PURE__ */ new Map();
|
|
5281
5380
|
for (const t of [...chain, ...tierTargets]) byBackend.set(t.backend, [...byBackend.get(t.backend) ?? [], t.model]);
|
|
5282
5381
|
for (const [id, models] of byBackend) {
|
|
@@ -5380,19 +5479,19 @@ function skillChecks() {
|
|
|
5380
5479
|
else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
|
|
5381
5480
|
}
|
|
5382
5481
|
if (geminiExtensionInstalled()) {
|
|
5383
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
5482
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path25.sep}.agents${path25.sep}`) && i.names.length);
|
|
5384
5483
|
checks.push(
|
|
5385
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)" }
|
|
5386
5485
|
);
|
|
5387
5486
|
}
|
|
5388
5487
|
if (viaPlugin) {
|
|
5389
|
-
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);
|
|
5390
5489
|
checks.push(
|
|
5391
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)" }
|
|
5392
5491
|
);
|
|
5393
5492
|
}
|
|
5394
5493
|
const launcher = launcherPath();
|
|
5395
|
-
if (
|
|
5494
|
+
if (fs29.existsSync(launcher)) {
|
|
5396
5495
|
const r = spawnSync8(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
5397
5496
|
checks.push(
|
|
5398
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)}` }
|
|
@@ -5404,12 +5503,12 @@ function skillChecks() {
|
|
|
5404
5503
|
}
|
|
5405
5504
|
function geminiExtensionInstalled() {
|
|
5406
5505
|
const home2 = process.env.GEMINI_CLI_HOME ?? os10.homedir();
|
|
5407
|
-
return
|
|
5506
|
+
return fs29.existsSync(path25.join(home2, ".gemini", "extensions", "pitroom"));
|
|
5408
5507
|
}
|
|
5409
5508
|
function pluginInstalled() {
|
|
5410
5509
|
try {
|
|
5411
|
-
const f =
|
|
5412
|
-
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 ?? {};
|
|
5413
5512
|
return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
|
|
5414
5513
|
} catch {
|
|
5415
5514
|
return false;
|
|
@@ -5426,24 +5525,24 @@ function superpowersActive() {
|
|
|
5426
5525
|
for (const p of openCodePlugins()) {
|
|
5427
5526
|
if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
|
|
5428
5527
|
}
|
|
5429
|
-
for (const base2 of [
|
|
5430
|
-
const dir =
|
|
5431
|
-
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);
|
|
5432
5531
|
}
|
|
5433
5532
|
return found;
|
|
5434
5533
|
}
|
|
5435
5534
|
function claudePlugins() {
|
|
5436
5535
|
try {
|
|
5437
|
-
const f =
|
|
5438
|
-
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 ?? {});
|
|
5439
5538
|
} catch {
|
|
5440
5539
|
return [];
|
|
5441
5540
|
}
|
|
5442
5541
|
}
|
|
5443
5542
|
function claudePluginEnabled(key) {
|
|
5444
5543
|
try {
|
|
5445
|
-
const f =
|
|
5446
|
-
return JSON.parse(
|
|
5544
|
+
const f = path25.join(os10.homedir(), ".claude", "settings.json");
|
|
5545
|
+
return JSON.parse(fs29.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
|
|
5447
5546
|
} catch {
|
|
5448
5547
|
return true;
|
|
5449
5548
|
}
|
|
@@ -5452,7 +5551,7 @@ function codexPlugins() {
|
|
|
5452
5551
|
const plugins = /* @__PURE__ */ new Map();
|
|
5453
5552
|
let text;
|
|
5454
5553
|
try {
|
|
5455
|
-
text =
|
|
5554
|
+
text = fs29.readFileSync(path25.join(process.env.CODEX_HOME ?? path25.join(os10.homedir(), ".codex"), "config.toml"), "utf8");
|
|
5456
5555
|
} catch {
|
|
5457
5556
|
return plugins;
|
|
5458
5557
|
}
|
|
@@ -5470,11 +5569,11 @@ function codexPlugins() {
|
|
|
5470
5569
|
return plugins;
|
|
5471
5570
|
}
|
|
5472
5571
|
function openCodePlugins() {
|
|
5473
|
-
const dir =
|
|
5572
|
+
const dir = path25.join(process.env.XDG_CONFIG_HOME ?? path25.join(os10.homedir(), ".config"), "opencode");
|
|
5474
5573
|
const plugins = [];
|
|
5475
5574
|
for (const name of ["opencode.json", "opencode.jsonc"]) {
|
|
5476
5575
|
try {
|
|
5477
|
-
const text =
|
|
5576
|
+
const text = fs29.readFileSync(path25.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
|
|
5478
5577
|
const list2 = JSON.parse(text).plugin;
|
|
5479
5578
|
if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
|
|
5480
5579
|
} catch {
|
|
@@ -5482,7 +5581,7 @@ function openCodePlugins() {
|
|
|
5482
5581
|
}
|
|
5483
5582
|
for (const folder of ["plugin", "plugins"]) {
|
|
5484
5583
|
try {
|
|
5485
|
-
for (const f of
|
|
5584
|
+
for (const f of fs29.readdirSync(path25.join(dir, folder))) plugins.push(path25.join(dir, folder, f));
|
|
5486
5585
|
} catch {
|
|
5487
5586
|
}
|
|
5488
5587
|
}
|
|
@@ -5532,6 +5631,8 @@ Usage
|
|
|
5532
5631
|
pitroom statusline [--then CMD] status-bar line: running workers, savings this week (after CMD's)
|
|
5533
5632
|
pitroom hook-card PostToolUse hook: a card after each Bash \`pitroom\` command
|
|
5534
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
|
|
5535
5636
|
pitroom doctor [--probe] check workers, models, permissions, skills
|
|
5536
5637
|
pitroom config effective settings, where each comes from, config file path
|
|
5537
5638
|
pitroom init [--model ID] [--fallback A,B] [--yes] [--force]
|
|
@@ -5581,6 +5682,7 @@ var COMMANDS = {
|
|
|
5581
5682
|
crew: cmdCrew,
|
|
5582
5683
|
review: cmdReview,
|
|
5583
5684
|
audit: cmdAudit,
|
|
5685
|
+
cooldown: cmdCooldown,
|
|
5584
5686
|
plan: cmdPlan,
|
|
5585
5687
|
status: cmdStatus,
|
|
5586
5688
|
wait: cmdWait,
|