pitroom 0.11.0 → 0.13.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 +18 -0
- package/README.md +6 -3
- package/dist/lib.mjs +23 -0
- package/dist/pitroom.mjs +559 -294
- package/dist/ui/app.js +1 -1
- package/docs/backends.md +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);
|
|
@@ -1416,7 +1416,8 @@ var SCHEMA = {
|
|
|
1416
1416
|
models: "record",
|
|
1417
1417
|
tiers: "record",
|
|
1418
1418
|
costs: "numbers",
|
|
1419
|
-
audit: "number"
|
|
1419
|
+
audit: "number",
|
|
1420
|
+
readIn: "string"
|
|
1420
1421
|
};
|
|
1421
1422
|
function configPath() {
|
|
1422
1423
|
if (process.env.PITROOM_CONFIG) return path7.resolve(process.env.PITROOM_CONFIG);
|
|
@@ -1426,25 +1427,25 @@ function configPath() {
|
|
|
1426
1427
|
var cached;
|
|
1427
1428
|
function loadConfig() {
|
|
1428
1429
|
if (cached) return cached;
|
|
1429
|
-
const
|
|
1430
|
+
const file2 = configPath();
|
|
1430
1431
|
const config = {};
|
|
1431
1432
|
const warnings = [];
|
|
1432
|
-
if (fs7.existsSync(
|
|
1433
|
+
if (fs7.existsSync(file2)) {
|
|
1433
1434
|
let raw;
|
|
1434
1435
|
try {
|
|
1435
|
-
raw = JSON.parse(fs7.readFileSync(
|
|
1436
|
+
raw = JSON.parse(fs7.readFileSync(file2, "utf8"));
|
|
1436
1437
|
} catch (e) {
|
|
1437
|
-
warnings.push(`${
|
|
1438
|
+
warnings.push(`${file2}: invalid JSON (${e.message}); ignored`);
|
|
1438
1439
|
}
|
|
1439
1440
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
1440
1441
|
for (const [key, value] of Object.entries(raw)) {
|
|
1441
1442
|
const type = SCHEMA[key];
|
|
1442
1443
|
if (!type) {
|
|
1443
|
-
warnings.push(`${
|
|
1444
|
+
warnings.push(`${file2}: unknown key "${key}" ignored`);
|
|
1444
1445
|
} else if (matches(value, type)) {
|
|
1445
1446
|
config[key] = value;
|
|
1446
1447
|
} else {
|
|
1447
|
-
warnings.push(`${
|
|
1448
|
+
warnings.push(`${file2}: "${key}" must be ${type}; ignored`);
|
|
1448
1449
|
}
|
|
1449
1450
|
}
|
|
1450
1451
|
}
|
|
@@ -1474,6 +1475,7 @@ var rate = (v) => {
|
|
|
1474
1475
|
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
1475
1476
|
return typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.min(n, 1) : void 0;
|
|
1476
1477
|
};
|
|
1478
|
+
var readIn = (v) => v === "auto" || v === "snapshot" || v === "project" ? v : void 0;
|
|
1477
1479
|
var list = (s) => s?.split(",").map((x) => x.trim()).filter(Boolean);
|
|
1478
1480
|
function setting(flag2, env, conf, fallback) {
|
|
1479
1481
|
if (flag2 !== void 0) return { value: flag2, source: "flag" };
|
|
@@ -1498,7 +1500,8 @@ function effective(flags = {}) {
|
|
|
1498
1500
|
models: setting(void 0, void 0, c.models, {}),
|
|
1499
1501
|
tiers: setting(void 0, void 0, c.tiers, {}),
|
|
1500
1502
|
costs: setting(void 0, void 0, c.costs, {}),
|
|
1501
|
-
audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0)
|
|
1503
|
+
audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0),
|
|
1504
|
+
readIn: setting(void 0, readIn(e.PITROOM_READ_IN), readIn(c.readIn), "auto")
|
|
1502
1505
|
};
|
|
1503
1506
|
}
|
|
1504
1507
|
|
|
@@ -1545,6 +1548,7 @@ var BOOL_FLAGS = {
|
|
|
1545
1548
|
"-i": "isolate",
|
|
1546
1549
|
"--isolate": "isolate",
|
|
1547
1550
|
"--bg": "bg",
|
|
1551
|
+
"--in-place": "in-place",
|
|
1548
1552
|
"--audit": "audit",
|
|
1549
1553
|
"--no-audit": "no-audit",
|
|
1550
1554
|
"--web": "web",
|
|
@@ -1556,6 +1560,7 @@ var BOOL_FLAGS = {
|
|
|
1556
1560
|
"--full": "full",
|
|
1557
1561
|
"--badge": "badge",
|
|
1558
1562
|
"--probe": "probe",
|
|
1563
|
+
"--clear": "clear",
|
|
1559
1564
|
"--copy": "copy",
|
|
1560
1565
|
"--all": "all",
|
|
1561
1566
|
"--models": "models",
|
|
@@ -1623,8 +1628,8 @@ function exitCodeFor(meta) {
|
|
|
1623
1628
|
}
|
|
1624
1629
|
}
|
|
1625
1630
|
function readTask(p) {
|
|
1626
|
-
const
|
|
1627
|
-
if (
|
|
1631
|
+
const file2 = flag(p, "task-file");
|
|
1632
|
+
if (file2) return fs8.readFileSync(file2, "utf8");
|
|
1628
1633
|
const words = p.positional;
|
|
1629
1634
|
if (words.length === 1 && words[0] === "-") return fs8.readFileSync(0, "utf8");
|
|
1630
1635
|
return words.join(" ");
|
|
@@ -1638,6 +1643,7 @@ function runOptions(p, task) {
|
|
|
1638
1643
|
return {
|
|
1639
1644
|
mode: modes[0] ?? "read",
|
|
1640
1645
|
auditRate: has(p, "no-audit") ? 0 : has(p, "audit") ? 1 : void 0,
|
|
1646
|
+
inPlace: has(p, "in-place"),
|
|
1641
1647
|
task,
|
|
1642
1648
|
dir: flag(p, "dir") ?? process.cwd(),
|
|
1643
1649
|
files: p.flags.get("file") ?? [],
|
|
@@ -1656,12 +1662,12 @@ function runOptions(p, task) {
|
|
|
1656
1662
|
};
|
|
1657
1663
|
}
|
|
1658
1664
|
function planStep(p) {
|
|
1659
|
-
const
|
|
1665
|
+
const file2 = flag(p, "plan");
|
|
1660
1666
|
const step = flag(p, "step");
|
|
1661
|
-
if (!
|
|
1662
|
-
if (!
|
|
1667
|
+
if (!file2 && !step) return void 0;
|
|
1668
|
+
if (!file2 || !step) throw new UserError("--plan and --step go together: pitroom run -i --plan PLAN.md --step N");
|
|
1663
1669
|
if (!/^\d+$/.test(step)) throw new UserError(`--step takes a task number, not "${step}"`);
|
|
1664
|
-
return { file, step: Number(step) };
|
|
1670
|
+
return { file: file2, step: Number(step) };
|
|
1665
1671
|
}
|
|
1666
1672
|
function effortFlag(p) {
|
|
1667
1673
|
const v = flag(p, "effort");
|
|
@@ -1671,8 +1677,8 @@ function effortFlag(p) {
|
|
|
1671
1677
|
|
|
1672
1678
|
// src/cli/commands.ts
|
|
1673
1679
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
1674
|
-
import
|
|
1675
|
-
import
|
|
1680
|
+
import fs29 from "node:fs";
|
|
1681
|
+
import path25 from "node:path";
|
|
1676
1682
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
1677
1683
|
|
|
1678
1684
|
// src/core/init.ts
|
|
@@ -1725,7 +1731,7 @@ var FREE = /-free$/;
|
|
|
1725
1731
|
var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
|
|
1726
1732
|
var isFound = (binary5) => path8.isAbsolute(binary5) && fs9.existsSync(binary5);
|
|
1727
1733
|
function planInit(opts = {}) {
|
|
1728
|
-
const
|
|
1734
|
+
const file2 = configPath();
|
|
1729
1735
|
const workers = backendIds().map((id) => {
|
|
1730
1736
|
const b = getBackend(id);
|
|
1731
1737
|
const binary5 = b.binary();
|
|
@@ -1769,7 +1775,7 @@ function planInit(opts = {}) {
|
|
|
1769
1775
|
if (Object.keys(tiers).length > 1) config.tiers = tiers;
|
|
1770
1776
|
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
1777
|
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:
|
|
1778
|
+
return { path: file2, exists: fs9.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
|
|
1773
1779
|
}
|
|
1774
1780
|
function writeInit(plan, force) {
|
|
1775
1781
|
if (plan.blocked) throw new UserError(plan.blocked);
|
|
@@ -2139,8 +2145,8 @@ var ANSWER_MAX2 = 2e5;
|
|
|
2139
2145
|
var cached2;
|
|
2140
2146
|
var historyFile = () => path10.join(home(), "history.db");
|
|
2141
2147
|
function openDb() {
|
|
2142
|
-
const
|
|
2143
|
-
if (cached2?.file ===
|
|
2148
|
+
const file2 = historyFile();
|
|
2149
|
+
if (cached2?.file === file2) return cached2.db;
|
|
2144
2150
|
let db;
|
|
2145
2151
|
try {
|
|
2146
2152
|
const emit = process.emitWarning;
|
|
@@ -2151,14 +2157,14 @@ function openDb() {
|
|
|
2151
2157
|
} finally {
|
|
2152
2158
|
process.emitWarning = emit;
|
|
2153
2159
|
}
|
|
2154
|
-
fs12.mkdirSync(path10.dirname(
|
|
2155
|
-
db = new DatabaseSync(
|
|
2160
|
+
fs12.mkdirSync(path10.dirname(file2), { recursive: true });
|
|
2161
|
+
db = new DatabaseSync(file2);
|
|
2156
2162
|
db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
|
|
2157
2163
|
migrate(db);
|
|
2158
2164
|
} catch {
|
|
2159
2165
|
db = void 0;
|
|
2160
2166
|
}
|
|
2161
|
-
cached2 = { file, db };
|
|
2167
|
+
cached2 = { file: file2, db };
|
|
2162
2168
|
return db;
|
|
2163
2169
|
}
|
|
2164
2170
|
function migrate(db) {
|
|
@@ -2490,7 +2496,7 @@ function structure(lines) {
|
|
|
2490
2496
|
});
|
|
2491
2497
|
return { headings, rules, fenced };
|
|
2492
2498
|
}
|
|
2493
|
-
function parsePlan(text,
|
|
2499
|
+
function parsePlan(text, file2 = "") {
|
|
2494
2500
|
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
2495
2501
|
const { headings, rules, fenced } = structure(lines);
|
|
2496
2502
|
const after = (line) => Math.min(headings.find((h) => h.line > line)?.line ?? lines.length, rules.find((r) => r > line) ?? lines.length);
|
|
@@ -2509,13 +2515,13 @@ function parsePlan(text, file = "") {
|
|
|
2509
2515
|
const tier = /^\s*[-*]?\s*\*\*Worker:\*\*\s*`?([\w-]+)`?/m.exec(unfenced)?.[1]?.toLowerCase();
|
|
2510
2516
|
tasks.push({ step: Number(m[1]), title: m[2].trim(), text: body, ...tier ? { tier } : {} });
|
|
2511
2517
|
}
|
|
2512
|
-
return { file, title: titleHeading?.text ?? "", header, constraints, tasks };
|
|
2518
|
+
return { file: file2, title: titleHeading?.text ?? "", header, constraints, tasks };
|
|
2513
2519
|
}
|
|
2514
|
-
function loadPlan(
|
|
2515
|
-
const abs = path11.resolve(
|
|
2516
|
-
if (!fs13.existsSync(abs) || !fs13.statSync(abs).isFile()) throw new UserError(`plan not found: ${
|
|
2520
|
+
function loadPlan(file2) {
|
|
2521
|
+
const abs = path11.resolve(file2);
|
|
2522
|
+
if (!fs13.existsSync(abs) || !fs13.statSync(abs).isFile()) throw new UserError(`plan not found: ${file2}`);
|
|
2517
2523
|
const plan = parsePlan(fs13.readFileSync(abs, "utf8"), fs13.realpathSync(abs));
|
|
2518
|
-
if (!plan.tasks.length) throw new UserError(`${
|
|
2524
|
+
if (!plan.tasks.length) throw new UserError(`${file2} has no "Task N" headings (see pitroom-writing-plans)`);
|
|
2519
2525
|
return plan;
|
|
2520
2526
|
}
|
|
2521
2527
|
function planTask(plan, step) {
|
|
@@ -2533,7 +2539,7 @@ function brief(plan, task) {
|
|
|
2533
2539
|
].filter(Boolean).join("\n\n")}
|
|
2534
2540
|
`;
|
|
2535
2541
|
}
|
|
2536
|
-
var planName = (
|
|
2542
|
+
var planName = (file2) => path11.basename(file2).replace(/\.md$/i, "");
|
|
2537
2543
|
|
|
2538
2544
|
// src/core/report.ts
|
|
2539
2545
|
var ICON2 = {
|
|
@@ -2568,7 +2574,7 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
|
2568
2574
|
const status = meta.taskStatus ? ` \xB7 STATUS ${meta.taskStatus}` : "";
|
|
2569
2575
|
out.push(`plan: ${planName(meta.plan.file)} \xB7 Task ${meta.plan.step}: ${meta.plan.title}${status}`);
|
|
2570
2576
|
}
|
|
2571
|
-
for (const a of meta.attempts ?? []) out.push(`fallback: ${a.target} failed (${a.error.slice(0, 160)})`);
|
|
2577
|
+
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
2578
|
if (meta.error) out.push(`error: ${meta.error}`);
|
|
2573
2579
|
for (const w of meta.warnings) out.push(`warning: ${w}`);
|
|
2574
2580
|
if (meta.state === "timeout") {
|
|
@@ -2789,10 +2795,10 @@ echo "pitroom: needs Node.js 22.13 or newer (set PITROOM_NODE to its path)" >&2
|
|
|
2789
2795
|
exit 127
|
|
2790
2796
|
`;
|
|
2791
2797
|
}
|
|
2792
|
-
function isOurLauncher(
|
|
2793
|
-
if (linksInto(
|
|
2798
|
+
function isOurLauncher(file2, root) {
|
|
2799
|
+
if (linksInto(file2, root)) return true;
|
|
2794
2800
|
try {
|
|
2795
|
-
const s = fs15.readFileSync(
|
|
2801
|
+
const s = fs15.readFileSync(file2, "utf8");
|
|
2796
2802
|
return s.includes(LAUNCHER_MARK) && s.includes(root);
|
|
2797
2803
|
} catch {
|
|
2798
2804
|
return false;
|
|
@@ -2967,10 +2973,10 @@ ${rulings}`,
|
|
|
2967
2973
|
].join("\n");
|
|
2968
2974
|
}
|
|
2969
2975
|
|
|
2970
|
-
// src/core/
|
|
2976
|
+
// src/core/snapshot.ts
|
|
2977
|
+
import fs19 from "node:fs";
|
|
2978
|
+
import path16 from "node:path";
|
|
2971
2979
|
import crypto5 from "node:crypto";
|
|
2972
|
-
import fs18 from "node:fs";
|
|
2973
|
-
import path15 from "node:path";
|
|
2974
2980
|
|
|
2975
2981
|
// src/vcs/git.ts
|
|
2976
2982
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
@@ -3011,14 +3017,18 @@ function repoRoot(dir) {
|
|
|
3011
3017
|
const r = git(dir, ["rev-parse", "--show-toplevel"]);
|
|
3012
3018
|
return r.code === 0 ? path14.resolve(r.stdout.trim()) : void 0;
|
|
3013
3019
|
}
|
|
3014
|
-
function snapshotTree(root, exclude = []) {
|
|
3020
|
+
function snapshotTree(root, exclude = [], drop = []) {
|
|
3015
3021
|
const tmp = path14.join(os9.tmpdir(), `pitroom-index-${process.pid}-${crypto4.randomBytes(4).toString("hex")}`);
|
|
3016
3022
|
const real = path14.resolve(root, must(root, ["rev-parse", "--git-path", "index"]).trim());
|
|
3017
3023
|
const env = { ...process.env, GIT_INDEX_FILE: tmp };
|
|
3018
3024
|
try {
|
|
3019
3025
|
if (fs17.existsSync(real)) fs17.copyFileSync(real, tmp);
|
|
3020
3026
|
else must(root, ["read-tree", "--empty"], env);
|
|
3021
|
-
must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`)], env);
|
|
3027
|
+
must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`), ...drop.map((p) => `:(top,literal,exclude)${p}`)], env);
|
|
3028
|
+
if (drop.length) {
|
|
3029
|
+
const r = spawnSync5("git", ["update-index", "--force-remove", "-z", "--stdin"], { cwd: root, env, input: `${drop.join("\0")}\0`, encoding: "utf8" });
|
|
3030
|
+
if (r.status !== 0) throw new Error(`git update-index failed: ${r.stderr.trim()}`);
|
|
3031
|
+
}
|
|
3022
3032
|
return must(root, ["write-tree"], env).trim();
|
|
3023
3033
|
} finally {
|
|
3024
3034
|
fs17.rmSync(tmp, { force: true });
|
|
@@ -3103,8 +3113,255 @@ function gitDir(dir) {
|
|
|
3103
3113
|
return r.code === 0 ? r.stdout.trim() : void 0;
|
|
3104
3114
|
}
|
|
3105
3115
|
|
|
3116
|
+
// src/core/secrets.ts
|
|
3117
|
+
import { execFileSync } from "node:child_process";
|
|
3118
|
+
import fs18 from "node:fs";
|
|
3119
|
+
import path15 from "node:path";
|
|
3120
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
3121
|
+
var MAX_VISITED = 2e4;
|
|
3122
|
+
var MAX_DEPTH = 4;
|
|
3123
|
+
var TEMPLATE = /\.(example|sample|template|dist|defaults?|tpl)$/i;
|
|
3124
|
+
function looksSecret(name) {
|
|
3125
|
+
if (TEMPLATE.test(name)) return false;
|
|
3126
|
+
if (name === ".env" || name.startsWith(".env.") || /\.env$/i.test(name)) return true;
|
|
3127
|
+
return /\.(pem|p12|pfx|key|jks|keystore)$/i.test(name) || /^id_(rsa|dsa|ecdsa|ed25519)$/.test(name) || name === ".netrc" || name === "credentials.json" || /^secrets?\.(json|ya?ml|toml)$/i.test(name);
|
|
3128
|
+
}
|
|
3129
|
+
function findSecretFiles(dir) {
|
|
3130
|
+
const found = [];
|
|
3131
|
+
let visited = 0;
|
|
3132
|
+
const walk = (d, depth) => {
|
|
3133
|
+
if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
|
|
3134
|
+
let entries;
|
|
3135
|
+
try {
|
|
3136
|
+
entries = fs18.readdirSync(d, { withFileTypes: true });
|
|
3137
|
+
} catch {
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
for (const e of entries) {
|
|
3141
|
+
visited++;
|
|
3142
|
+
if (e.isDirectory()) {
|
|
3143
|
+
if (!SKIP_DIRS.has(e.name)) walk(path15.join(d, e.name), depth + 1);
|
|
3144
|
+
} else if (e.isFile() && looksSecret(e.name)) {
|
|
3145
|
+
found.push(path15.relative(dir, path15.join(d, e.name)));
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
};
|
|
3149
|
+
walk(dir, 0);
|
|
3150
|
+
return found.sort();
|
|
3151
|
+
}
|
|
3152
|
+
function findSecretFilesInTree(root, dir) {
|
|
3153
|
+
try {
|
|
3154
|
+
const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
3155
|
+
return out.split("\0").filter((f) => f && looksSecret(path15.basename(f)) && !path15.relative(dir, path15.join(root, f)).startsWith("..")).map((f) => path15.relative(dir, path15.join(root, f))).sort();
|
|
3156
|
+
} catch {
|
|
3157
|
+
return [];
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
function secretWarning(files, mode) {
|
|
3161
|
+
if (!files.length) return void 0;
|
|
3162
|
+
const shown = files.slice(0, 3).join(", ") + (files.length > 3 ? `, +${files.length - 3} more` : "");
|
|
3163
|
+
const where = mode === "isolate" ? "would be copied into the isolated copy" : "sit in the directory the worker runs in";
|
|
3164
|
+
const fix = mode === "isolate" ? "add them to .gitignore, or" : "use an isolated copy (-i, git-ignored files are left out) or a clean checkout, or";
|
|
3165
|
+
return `secret-looking files ${where} (${shown}): the worker is told not to read them, but nothing stops it, and its model may be hosted by a third party; ${fix} set PITROOM_NO_SECRET_WARNING=1 to silence this`;
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
// src/core/snapshot.ts
|
|
3169
|
+
var READ_IN = ["auto", "snapshot", "project"];
|
|
3170
|
+
var KEEP_MS = 24 * 36e5;
|
|
3171
|
+
var PRUNE_AGE_MS = 3 * 24 * 36e5;
|
|
3172
|
+
var KEEP_NEWEST = 6;
|
|
3173
|
+
var JUST_USED_MS = 10 * 6e4;
|
|
3174
|
+
var snapshotsDir = () => path16.join(home(), "snapshots");
|
|
3175
|
+
function wantSnapshot(readIn2, root, dir) {
|
|
3176
|
+
if (!root || readIn2 === "project") return void 0;
|
|
3177
|
+
const secrets = findSecretFiles(dir);
|
|
3178
|
+
return readIn2 === "snapshot" || secrets.length ? { secrets } : void 0;
|
|
3179
|
+
}
|
|
3180
|
+
function readSnapshot(root) {
|
|
3181
|
+
const drop = findSecretFilesInTree(root, root);
|
|
3182
|
+
const tree = snapshotTree(root, [], drop);
|
|
3183
|
+
const dest = path16.join(snapshotsDir(), tree);
|
|
3184
|
+
if (fs19.existsSync(path16.join(dest, ".git"))) {
|
|
3185
|
+
touch(dest);
|
|
3186
|
+
return { dir: dest, tree, created: false };
|
|
3187
|
+
}
|
|
3188
|
+
const tmp = path16.join(snapshotsDir(), `.tmp-${process.pid}-${crypto5.randomBytes(3).toString("hex")}`);
|
|
3189
|
+
try {
|
|
3190
|
+
createIsolatedCopy(root, tree, tmp);
|
|
3191
|
+
readOnly(tmp);
|
|
3192
|
+
try {
|
|
3193
|
+
fs19.renameSync(tmp, dest);
|
|
3194
|
+
} catch (e) {
|
|
3195
|
+
if (!fs19.existsSync(path16.join(dest, ".git"))) throw e;
|
|
3196
|
+
removeIsolatedCopy(tmp, snapshotsDir());
|
|
3197
|
+
}
|
|
3198
|
+
} catch (e) {
|
|
3199
|
+
try {
|
|
3200
|
+
removeIsolatedCopy(tmp, snapshotsDir());
|
|
3201
|
+
} catch {
|
|
3202
|
+
}
|
|
3203
|
+
throw e;
|
|
3204
|
+
}
|
|
3205
|
+
prune();
|
|
3206
|
+
return { dir: dest, tree, created: true };
|
|
3207
|
+
}
|
|
3208
|
+
function touch(dir) {
|
|
3209
|
+
try {
|
|
3210
|
+
const now = /* @__PURE__ */ new Date();
|
|
3211
|
+
fs19.utimesSync(dir, now, now);
|
|
3212
|
+
} catch {
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
function readOnly(dir) {
|
|
3216
|
+
const walk = (d) => {
|
|
3217
|
+
for (const e of fs19.readdirSync(d, { withFileTypes: true })) {
|
|
3218
|
+
if (e.name === ".git") continue;
|
|
3219
|
+
const p = path16.join(d, e.name);
|
|
3220
|
+
if (e.isDirectory()) walk(p);
|
|
3221
|
+
else if (e.isFile()) fs19.chmodSync(p, fs19.statSync(p).mode & ~146);
|
|
3222
|
+
}
|
|
3223
|
+
};
|
|
3224
|
+
try {
|
|
3225
|
+
walk(dir);
|
|
3226
|
+
} catch {
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWEST) {
|
|
3230
|
+
let names;
|
|
3231
|
+
try {
|
|
3232
|
+
names = fs19.readdirSync(snapshotsDir());
|
|
3233
|
+
} catch {
|
|
3234
|
+
return 0;
|
|
3235
|
+
}
|
|
3236
|
+
const inUse = /* @__PURE__ */ new Set();
|
|
3237
|
+
for (const id of listRunIds()) {
|
|
3238
|
+
try {
|
|
3239
|
+
const m = readMeta(id);
|
|
3240
|
+
if (m.snapshot?.dir && isActive(m.state)) inUse.add(path16.resolve(m.snapshot.dir));
|
|
3241
|
+
} catch {
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
const entries = names.map((n) => {
|
|
3245
|
+
const dir = path16.join(snapshotsDir(), n);
|
|
3246
|
+
try {
|
|
3247
|
+
return { n, dir, age: now - fs19.statSync(dir).mtimeMs };
|
|
3248
|
+
} catch {
|
|
3249
|
+
return void 0;
|
|
3250
|
+
}
|
|
3251
|
+
}).filter((e) => !!e).sort((x, y) => x.age - y.age);
|
|
3252
|
+
let removed = 0;
|
|
3253
|
+
let rank = 0;
|
|
3254
|
+
for (const e of entries) {
|
|
3255
|
+
if (inUse.has(path16.resolve(e.dir))) continue;
|
|
3256
|
+
const half = e.n.startsWith(".tmp-");
|
|
3257
|
+
const old = e.age >= (half ? KEEP_MS : maxAgeMs);
|
|
3258
|
+
const surplus = !half && ++rank > keepNewest && e.age >= JUST_USED_MS;
|
|
3259
|
+
if (!old && !surplus) continue;
|
|
3260
|
+
try {
|
|
3261
|
+
makeWritable(e.dir);
|
|
3262
|
+
removeIsolatedCopy(e.dir, snapshotsDir());
|
|
3263
|
+
removed++;
|
|
3264
|
+
} catch {
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
return removed;
|
|
3268
|
+
}
|
|
3269
|
+
function makeWritable(dir) {
|
|
3270
|
+
const walk = (d) => {
|
|
3271
|
+
for (const e of fs19.readdirSync(d, { withFileTypes: true })) {
|
|
3272
|
+
const p = path16.join(d, e.name);
|
|
3273
|
+
if (e.isDirectory()) walk(p);
|
|
3274
|
+
else if (e.isFile()) fs19.chmodSync(p, fs19.statSync(p).mode | 128);
|
|
3275
|
+
}
|
|
3276
|
+
};
|
|
3277
|
+
try {
|
|
3278
|
+
walk(dir);
|
|
3279
|
+
} catch {
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
function backToProject(text, snapshotDir, root) {
|
|
3283
|
+
let real = snapshotDir;
|
|
3284
|
+
try {
|
|
3285
|
+
real = fs19.realpathSync(snapshotDir);
|
|
3286
|
+
} catch {
|
|
3287
|
+
}
|
|
3288
|
+
let out = text;
|
|
3289
|
+
for (const dir of /* @__PURE__ */ new Set([real, snapshotDir])) out = out.split(`${dir}${path16.sep}`).join(`${root}${path16.sep}`).split(dir).join(root);
|
|
3290
|
+
return out;
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
// src/core/cooldown.ts
|
|
3294
|
+
import fs20 from "node:fs";
|
|
3295
|
+
import path17 from "node:path";
|
|
3296
|
+
var MAX_MS = 24 * 36e5;
|
|
3297
|
+
var OVERLOAD_MS = 20 * 6e4;
|
|
3298
|
+
var DAILY_MS = 4 * 36e5;
|
|
3299
|
+
var file = () => path17.join(home(), "cooldowns.json");
|
|
3300
|
+
function cooldownKey(target, backend) {
|
|
3301
|
+
const model = (target.model ?? backend.defaultModel?.() ?? "").split("#")[0];
|
|
3302
|
+
return `${target.backend}:${model}`;
|
|
3303
|
+
}
|
|
3304
|
+
function retryAfterMs(message) {
|
|
3305
|
+
const compact3 = /(?:retry|try again|resets?|available again)[^0-9]{0,24}((?:\d+(?:\.\d+)?\s*[dhms]\s*)+)/i.exec(message)?.[1];
|
|
3306
|
+
const words = /(?:retry|try again|resets?)[^0-9]{0,24}(\d+(?:\.\d+)?)\s*(second|minute|hour)s?/i.exec(message);
|
|
3307
|
+
let ms = 0;
|
|
3308
|
+
if (compact3) {
|
|
3309
|
+
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()];
|
|
3310
|
+
} else if (words) {
|
|
3311
|
+
ms = Number(words[1]) * { second: 1e3, minute: 6e4, hour: 36e5 }[words[2].toLowerCase()];
|
|
3312
|
+
}
|
|
3313
|
+
return ms > 0 ? ms : void 0;
|
|
3314
|
+
}
|
|
3315
|
+
function cooldownMs(message) {
|
|
3316
|
+
const told = retryAfterMs(message);
|
|
3317
|
+
if (told !== void 0) return Math.min(Math.max(told, 6e4), MAX_MS);
|
|
3318
|
+
return /daily|per.?day|per day|exhausted your/i.test(message) ? DAILY_MS : OVERLOAD_MS;
|
|
3319
|
+
}
|
|
3320
|
+
function read() {
|
|
3321
|
+
try {
|
|
3322
|
+
const raw = JSON.parse(fs20.readFileSync(file(), "utf8"));
|
|
3323
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
3324
|
+
} catch {
|
|
3325
|
+
return {};
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
function write(all) {
|
|
3329
|
+
fs20.mkdirSync(path17.dirname(file()), { recursive: true });
|
|
3330
|
+
const tmp = `${file()}.${process.pid}.tmp`;
|
|
3331
|
+
fs20.writeFileSync(tmp, `${JSON.stringify(all, null, 2)}
|
|
3332
|
+
`);
|
|
3333
|
+
fs20.renameSync(tmp, file());
|
|
3334
|
+
}
|
|
3335
|
+
function activeCooldowns(now = Date.now()) {
|
|
3336
|
+
return Object.fromEntries(Object.entries(read()).filter(([, c]) => Date.parse(c.until) > now));
|
|
3337
|
+
}
|
|
3338
|
+
var activeCooldown = (key, now = Date.now()) => activeCooldowns(now)[key];
|
|
3339
|
+
function recordCooldown(key, targetName, message, now = Date.now()) {
|
|
3340
|
+
try {
|
|
3341
|
+
const all = activeCooldowns(now);
|
|
3342
|
+
all[key] = { until: new Date(now + cooldownMs(message)).toISOString(), reason: message.replace(/\s+/g, " ").trim().slice(0, 160), target: targetName };
|
|
3343
|
+
write(all);
|
|
3344
|
+
} catch {
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
function clearCooldowns(key) {
|
|
3348
|
+
const all = read();
|
|
3349
|
+
const keys = key ? Object.keys(all).filter((k) => k === key) : Object.keys(all);
|
|
3350
|
+
for (const k of keys) delete all[k];
|
|
3351
|
+
if (keys.length) write(all);
|
|
3352
|
+
return keys.length;
|
|
3353
|
+
}
|
|
3354
|
+
function untilText(c, now = Date.now()) {
|
|
3355
|
+
const d = new Date(c.until);
|
|
3356
|
+
const time = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
3357
|
+
return d.toDateString() === new Date(now).toDateString() ? `until ${time}` : `until ${d.toLocaleDateString()} ${time}`;
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3106
3360
|
// src/core/review.ts
|
|
3107
|
-
|
|
3361
|
+
import crypto6 from "node:crypto";
|
|
3362
|
+
import fs21 from "node:fs";
|
|
3363
|
+
import path18 from "node:path";
|
|
3364
|
+
var TEMPLATE2 = { task: "task-reviewer", fix: "re-review", range: "code-reviewer" };
|
|
3108
3365
|
var section = (title, body) => `## ${title}
|
|
3109
3366
|
|
|
3110
3367
|
${body.trim() || "(empty)"}
|
|
@@ -3121,7 +3378,7 @@ function ancestors(m) {
|
|
|
3121
3378
|
function briefOf(m) {
|
|
3122
3379
|
const root = [m, ...ancestors(m)].at(-1);
|
|
3123
3380
|
const f = runFile(root.id, "brief.md");
|
|
3124
|
-
return
|
|
3381
|
+
return fs21.existsSync(f) ? fs21.readFileSync(f, "utf8") : root.task;
|
|
3125
3382
|
}
|
|
3126
3383
|
function latestReview(ids) {
|
|
3127
3384
|
for (const id of listRunIds().reverse()) {
|
|
@@ -3135,7 +3392,7 @@ function latestReview(ids) {
|
|
|
3135
3392
|
}
|
|
3136
3393
|
function diffOf(m, from = m.baseTree) {
|
|
3137
3394
|
const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
|
|
3138
|
-
if (where &&
|
|
3395
|
+
if (where && fs21.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
|
|
3139
3396
|
if (from !== m.baseTree) {
|
|
3140
3397
|
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
3398
|
}
|
|
@@ -3149,7 +3406,7 @@ function runReview(id) {
|
|
|
3149
3406
|
if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
|
|
3150
3407
|
const chain = ancestors(m);
|
|
3151
3408
|
const previous = latestReview(chain.map((a) => a.id));
|
|
3152
|
-
const dir = m.mode === "isolate" && m.worktree &&
|
|
3409
|
+
const dir = m.mode === "isolate" && m.worktree && fs21.existsSync(m.worktree) ? m.cwd : m.dir;
|
|
3153
3410
|
const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
|
|
3154
3411
|
const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
|
|
3155
3412
|
if (!previous) {
|
|
@@ -3223,15 +3480,15 @@ function pickReviewer(job) {
|
|
|
3223
3480
|
function writePackage(job) {
|
|
3224
3481
|
const g = gitDir(job.dir);
|
|
3225
3482
|
if (!g) throw new UserError(`not a git repository: ${job.dir}`);
|
|
3226
|
-
const
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
return
|
|
3483
|
+
const file2 = path18.join(g, "pitroom", `review-${crypto6.randomBytes(4).toString("hex")}.md`);
|
|
3484
|
+
fs21.mkdirSync(path18.dirname(file2), { recursive: true });
|
|
3485
|
+
fs21.writeFileSync(file2, job.package);
|
|
3486
|
+
return file2;
|
|
3230
3487
|
}
|
|
3231
3488
|
|
|
3232
3489
|
// src/core/templates.ts
|
|
3233
|
-
import
|
|
3234
|
-
import
|
|
3490
|
+
import fs22 from "node:fs";
|
|
3491
|
+
import path19 from "node:path";
|
|
3235
3492
|
var FILES = {
|
|
3236
3493
|
implementer: "pitroom-driven-development/implementer-prompt.md",
|
|
3237
3494
|
"task-reviewer": "pitroom-driven-development/task-reviewer-prompt.md",
|
|
@@ -3239,9 +3496,9 @@ var FILES = {
|
|
|
3239
3496
|
"code-reviewer": "pitroom-review/code-reviewer.md"
|
|
3240
3497
|
};
|
|
3241
3498
|
function loadTemplate(name, root = packageRoot()) {
|
|
3242
|
-
const
|
|
3243
|
-
if (!
|
|
3244
|
-
return
|
|
3499
|
+
const file2 = path19.join(root, "skills", FILES[name]);
|
|
3500
|
+
if (!fs22.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
|
|
3501
|
+
return fs22.readFileSync(file2, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
|
|
3245
3502
|
}
|
|
3246
3503
|
function fill(template, values) {
|
|
3247
3504
|
const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
|
|
@@ -3347,16 +3604,16 @@ function formatModels(t) {
|
|
|
3347
3604
|
|
|
3348
3605
|
// src/core/dash.ts
|
|
3349
3606
|
import { spawn } from "node:child_process";
|
|
3350
|
-
import
|
|
3351
|
-
import
|
|
3607
|
+
import crypto7 from "node:crypto";
|
|
3608
|
+
import fs23 from "node:fs";
|
|
3352
3609
|
import http from "node:http";
|
|
3353
|
-
import
|
|
3610
|
+
import path20 from "node:path";
|
|
3354
3611
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
3355
3612
|
|
|
3356
3613
|
// src/core/file-diff.ts
|
|
3357
3614
|
var PREVIEW_LINES = 300;
|
|
3358
3615
|
function gitPath(value, prefix = true) {
|
|
3359
|
-
let
|
|
3616
|
+
let path27 = value;
|
|
3360
3617
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
3361
3618
|
const bytes = [];
|
|
3362
3619
|
const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
|
|
@@ -3364,9 +3621,9 @@ function gitPath(value, prefix = true) {
|
|
|
3364
3621
|
if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
|
|
3365
3622
|
else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
|
|
3366
3623
|
}
|
|
3367
|
-
|
|
3624
|
+
path27 = new TextDecoder().decode(new Uint8Array(bytes));
|
|
3368
3625
|
}
|
|
3369
|
-
return prefix ?
|
|
3626
|
+
return prefix ? path27.replace(/^[ab]\//, "") : path27;
|
|
3370
3627
|
}
|
|
3371
3628
|
function headerPath(header) {
|
|
3372
3629
|
const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
|
|
@@ -3375,7 +3632,7 @@ function headerPath(header) {
|
|
|
3375
3632
|
}
|
|
3376
3633
|
function fileDiffs(patch, changes, sourceTruncated = false) {
|
|
3377
3634
|
const files = [];
|
|
3378
|
-
let
|
|
3635
|
+
let file2;
|
|
3379
3636
|
let oldLine = 0;
|
|
3380
3637
|
let newLine = 0;
|
|
3381
3638
|
let oldLeft = 0;
|
|
@@ -3383,25 +3640,25 @@ function fileDiffs(patch, changes, sourceTruncated = false) {
|
|
|
3383
3640
|
let inHunk = false;
|
|
3384
3641
|
let row = 0;
|
|
3385
3642
|
const finishHunk = () => {
|
|
3386
|
-
if (
|
|
3643
|
+
if (file2 && (oldLeft > 0 || newLeft > 0)) file2.incomplete = true;
|
|
3387
3644
|
oldLeft = newLeft = 0;
|
|
3388
3645
|
inHunk = false;
|
|
3389
3646
|
};
|
|
3390
3647
|
const add = (line) => {
|
|
3391
|
-
if (!
|
|
3392
|
-
if (
|
|
3393
|
-
else
|
|
3648
|
+
if (!file2) return;
|
|
3649
|
+
if (file2.lines.length < PREVIEW_LINES) file2.lines.push({ ...line, id: String(row) });
|
|
3650
|
+
else file2.omittedLines++;
|
|
3394
3651
|
row++;
|
|
3395
3652
|
};
|
|
3396
3653
|
for (const line of patch.split("\n")) {
|
|
3397
3654
|
if (line.startsWith("diff --git ")) {
|
|
3398
3655
|
finishHunk();
|
|
3399
|
-
|
|
3400
|
-
files.push(
|
|
3656
|
+
file2 = { path: headerPath(line), status: "M", lines: [], additions: 0, deletions: 0, omittedLines: 0, binary: false, incomplete: false };
|
|
3657
|
+
files.push(file2);
|
|
3401
3658
|
row = 0;
|
|
3402
3659
|
continue;
|
|
3403
3660
|
}
|
|
3404
|
-
if (!
|
|
3661
|
+
if (!file2) continue;
|
|
3405
3662
|
const hunk = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line);
|
|
3406
3663
|
if (hunk) {
|
|
3407
3664
|
finishHunk();
|
|
@@ -3415,11 +3672,11 @@ function fileDiffs(patch, changes, sourceTruncated = false) {
|
|
|
3415
3672
|
add({ type: "meta", content: line });
|
|
3416
3673
|
} else if (inHunk && (oldLeft > 0 || newLeft > 0)) {
|
|
3417
3674
|
if (line.startsWith("+") && newLeft > 0) {
|
|
3418
|
-
|
|
3675
|
+
file2.additions++;
|
|
3419
3676
|
newLeft--;
|
|
3420
3677
|
add({ type: "added", newLine: newLine++, content: line.slice(1) });
|
|
3421
3678
|
} else if (line.startsWith("-") && oldLeft > 0) {
|
|
3422
|
-
|
|
3679
|
+
file2.deletions++;
|
|
3423
3680
|
oldLeft--;
|
|
3424
3681
|
add({ type: "removed", oldLine: oldLine++, content: line.slice(1) });
|
|
3425
3682
|
} else if (line.startsWith(" ") && oldLeft > 0 && newLeft > 0) {
|
|
@@ -3427,25 +3684,25 @@ function fileDiffs(patch, changes, sourceTruncated = false) {
|
|
|
3427
3684
|
newLeft--;
|
|
3428
3685
|
add({ type: "context", oldLine: oldLine++, newLine: newLine++, content: line.slice(1) });
|
|
3429
3686
|
} else {
|
|
3430
|
-
|
|
3687
|
+
file2.incomplete = true;
|
|
3431
3688
|
}
|
|
3432
3689
|
} 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))
|
|
3690
|
+
if (line.startsWith("+++ ") && line !== "+++ /dev/null") file2.path = gitPath(line.slice(4).replace(/\t$/, ""));
|
|
3691
|
+
else if (line.startsWith("--- ") && line !== "--- /dev/null") file2.path = gitPath(line.slice(4).replace(/\t$/, ""));
|
|
3692
|
+
else if (/^(GIT binary patch|Binary files .* differ)$/.test(line)) file2.binary = true;
|
|
3436
3693
|
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 "))
|
|
3694
|
+
if (line.startsWith("new file mode ")) file2.status = "A";
|
|
3695
|
+
if (line.startsWith("deleted file mode ")) file2.status = "D";
|
|
3439
3696
|
if (line.startsWith("rename to ")) {
|
|
3440
|
-
|
|
3441
|
-
|
|
3697
|
+
file2.path = gitPath(line.slice(10), false);
|
|
3698
|
+
file2.status = "R";
|
|
3442
3699
|
}
|
|
3443
3700
|
add({ type: "meta", content: line });
|
|
3444
3701
|
}
|
|
3445
3702
|
}
|
|
3446
3703
|
}
|
|
3447
3704
|
finishHunk();
|
|
3448
|
-
if (sourceTruncated &&
|
|
3705
|
+
if (sourceTruncated && file2) file2.incomplete = true;
|
|
3449
3706
|
for (const f of files) {
|
|
3450
3707
|
if (f.incomplete || f.binary) f.additions = f.deletions = void 0;
|
|
3451
3708
|
}
|
|
@@ -3607,32 +3864,32 @@ var HEADERS = {
|
|
|
3607
3864
|
"x-content-type-options": "nosniff",
|
|
3608
3865
|
"referrer-policy": "no-referrer",
|
|
3609
3866
|
// Own files only, plus the hash of the one inline theme script; styles may be inline (the components position popups with them).
|
|
3610
|
-
"content-security-policy": `default-src 'none'; script-src 'self' 'sha256-${
|
|
3867
|
+
"content-security-policy": `default-src 'none'; script-src 'self' 'sha256-${crypto7.createHash("sha256").update(THEME_SCRIPT).digest("base64")}'; style-src 'self' 'unsafe-inline'; img-src data:; connect-src 'self'; base-uri 'none'; form-action 'none'`
|
|
3611
3868
|
};
|
|
3612
3869
|
var ASSETS = { "/assets/app.js": "text/javascript; charset=utf-8", "/assets/app.css": "text/css; charset=utf-8" };
|
|
3613
3870
|
var assetsDir = () => fileURLToPath3(new URL("./ui/", import.meta.url));
|
|
3614
3871
|
var assetCache = /* @__PURE__ */ new Map();
|
|
3615
3872
|
function readAsset(route) {
|
|
3616
3873
|
try {
|
|
3617
|
-
const
|
|
3618
|
-
const { mtimeMs } =
|
|
3874
|
+
const file2 = path20.join(assetsDir(), path20.basename(route));
|
|
3875
|
+
const { mtimeMs } = fs23.statSync(file2);
|
|
3619
3876
|
const hit = assetCache.get(route);
|
|
3620
3877
|
if (hit && hit.mtimeMs === mtimeMs) return hit.data;
|
|
3621
|
-
const data =
|
|
3878
|
+
const data = fs23.readFileSync(file2);
|
|
3622
3879
|
assetCache.set(route, { mtimeMs, data });
|
|
3623
3880
|
return data;
|
|
3624
3881
|
} catch {
|
|
3625
3882
|
return void 0;
|
|
3626
3883
|
}
|
|
3627
3884
|
}
|
|
3628
|
-
function handler(
|
|
3885
|
+
function handler(touch2) {
|
|
3629
3886
|
const send = (res, code, type, body) => {
|
|
3630
3887
|
res.writeHead(code, { ...HEADERS, "content-type": type });
|
|
3631
3888
|
res.end(body);
|
|
3632
3889
|
};
|
|
3633
3890
|
const json = (res, code, value) => send(res, code, "application/json; charset=utf-8", JSON.stringify(value));
|
|
3634
3891
|
return (req, res) => {
|
|
3635
|
-
|
|
3892
|
+
touch2();
|
|
3636
3893
|
if (!LOCAL_HOST.test(req.headers.host ?? "")) return json(res, 403, { error: "forbidden host" });
|
|
3637
3894
|
if (req.method !== "GET" && req.method !== "HEAD") return json(res, 405, { error: "read-only" });
|
|
3638
3895
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -3698,7 +3955,7 @@ function startDash(opts) {
|
|
|
3698
3955
|
});
|
|
3699
3956
|
});
|
|
3700
3957
|
}
|
|
3701
|
-
var registryFile = () =>
|
|
3958
|
+
var registryFile = () => path20.join(home(), "dash.json");
|
|
3702
3959
|
async function ping(port) {
|
|
3703
3960
|
try {
|
|
3704
3961
|
const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
|
|
@@ -3709,11 +3966,11 @@ async function ping(port) {
|
|
|
3709
3966
|
}
|
|
3710
3967
|
async function runningDash() {
|
|
3711
3968
|
try {
|
|
3712
|
-
const r = JSON.parse(
|
|
3969
|
+
const r = JSON.parse(fs23.readFileSync(registryFile(), "utf8"));
|
|
3713
3970
|
if (isAlive(r.pid) && await ping(r.port)) return { ...r, url: `http://127.0.0.1:${r.port}/` };
|
|
3714
3971
|
} catch {
|
|
3715
3972
|
}
|
|
3716
|
-
|
|
3973
|
+
fs23.rmSync(registryFile(), { force: true });
|
|
3717
3974
|
return void 0;
|
|
3718
3975
|
}
|
|
3719
3976
|
function openBrowser(url) {
|
|
@@ -3728,7 +3985,7 @@ async function dashCommand(o) {
|
|
|
3728
3985
|
if (o.stop) {
|
|
3729
3986
|
if (!existing) throw new UserError("pitroom dash is not running");
|
|
3730
3987
|
process.kill(existing.pid, "SIGTERM");
|
|
3731
|
-
|
|
3988
|
+
fs23.rmSync(registryFile(), { force: true });
|
|
3732
3989
|
o.log(`stopped pitroom dash (${existing.url})`);
|
|
3733
3990
|
return 0;
|
|
3734
3991
|
}
|
|
@@ -3765,8 +4022,8 @@ async function dashCommand(o) {
|
|
|
3765
4022
|
if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
|
|
3766
4023
|
dash = await startDash({ port: 0, idleMs: o.idleMs });
|
|
3767
4024
|
}
|
|
3768
|
-
|
|
3769
|
-
|
|
4025
|
+
fs23.mkdirSync(home(), { recursive: true });
|
|
4026
|
+
fs23.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
|
|
3770
4027
|
const stop = () => void dash.close();
|
|
3771
4028
|
process.once("SIGINT", stop);
|
|
3772
4029
|
process.once("SIGTERM", stop);
|
|
@@ -3775,7 +4032,7 @@ async function dashCommand(o) {
|
|
|
3775
4032
|
if (o.open) openBrowser(dash.url);
|
|
3776
4033
|
await dash.closed;
|
|
3777
4034
|
try {
|
|
3778
|
-
if (JSON.parse(
|
|
4035
|
+
if (JSON.parse(fs23.readFileSync(registryFile(), "utf8")).pid === process.pid) fs23.rmSync(registryFile(), { force: true });
|
|
3779
4036
|
} catch {
|
|
3780
4037
|
}
|
|
3781
4038
|
return 0;
|
|
@@ -3783,8 +4040,8 @@ async function dashCommand(o) {
|
|
|
3783
4040
|
|
|
3784
4041
|
// src/core/run.ts
|
|
3785
4042
|
import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
|
|
3786
|
-
import
|
|
3787
|
-
import
|
|
4043
|
+
import fs28 from "node:fs";
|
|
4044
|
+
import path24 from "node:path";
|
|
3788
4045
|
|
|
3789
4046
|
// src/core/chain.ts
|
|
3790
4047
|
function resolveChain(flags = {}) {
|
|
@@ -3834,11 +4091,11 @@ function resolveChain(flags = {}) {
|
|
|
3834
4091
|
|
|
3835
4092
|
// src/core/process.ts
|
|
3836
4093
|
import { spawn as spawn2 } from "node:child_process";
|
|
3837
|
-
import
|
|
4094
|
+
import fs25 from "node:fs";
|
|
3838
4095
|
|
|
3839
4096
|
// src/vcs/guard.ts
|
|
3840
|
-
import
|
|
3841
|
-
import
|
|
4097
|
+
import fs24 from "node:fs";
|
|
4098
|
+
import path21 from "node:path";
|
|
3842
4099
|
var VERSION = 1;
|
|
3843
4100
|
var SHIM = `#!/bin/sh
|
|
3844
4101
|
# pitroom git guard v${VERSION}. Blocks git commands that change history, refs, the
|
|
@@ -3913,15 +4170,15 @@ esac
|
|
|
3913
4170
|
exec "$real" "$@"
|
|
3914
4171
|
`;
|
|
3915
4172
|
function shimDir() {
|
|
3916
|
-
return
|
|
4173
|
+
return path21.join(home(), "shim", `v${VERSION}`);
|
|
3917
4174
|
}
|
|
3918
4175
|
function findRealGit(skip) {
|
|
3919
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
3920
|
-
if (
|
|
3921
|
-
const p =
|
|
4176
|
+
for (const dir of (process.env.PATH ?? "").split(path21.delimiter).filter(Boolean)) {
|
|
4177
|
+
if (path21.resolve(dir) === skip) continue;
|
|
4178
|
+
const p = path21.join(dir, "git");
|
|
3922
4179
|
try {
|
|
3923
|
-
|
|
3924
|
-
if (
|
|
4180
|
+
fs24.accessSync(p, fs24.constants.X_OK);
|
|
4181
|
+
if (fs24.statSync(p).isFile()) return p;
|
|
3925
4182
|
} catch {
|
|
3926
4183
|
}
|
|
3927
4184
|
}
|
|
@@ -3934,13 +4191,13 @@ var REF_HOOK = `#!/bin/sh
|
|
|
3934
4191
|
echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
|
|
3935
4192
|
exit 1
|
|
3936
4193
|
`;
|
|
3937
|
-
var hooksDir = () =>
|
|
3938
|
-
function writeIfChanged(
|
|
3939
|
-
if (
|
|
3940
|
-
|
|
3941
|
-
const tmp = `${
|
|
3942
|
-
|
|
3943
|
-
|
|
4194
|
+
var hooksDir = () => path21.join(home(), "git-hooks", `v${HOOK_VERSION}`);
|
|
4195
|
+
function writeIfChanged(file2, content) {
|
|
4196
|
+
if (fs24.existsSync(file2) && fs24.readFileSync(file2, "utf8") === content) return;
|
|
4197
|
+
fs24.mkdirSync(path21.dirname(file2), { recursive: true });
|
|
4198
|
+
const tmp = `${file2}.${process.pid}.tmp`;
|
|
4199
|
+
fs24.writeFileSync(tmp, content, { mode: 493 });
|
|
4200
|
+
fs24.renameSync(tmp, file2);
|
|
3944
4201
|
}
|
|
3945
4202
|
function withGitConfig(env, entries) {
|
|
3946
4203
|
const out = { ...env };
|
|
@@ -3962,7 +4219,7 @@ function guardEnv(env) {
|
|
|
3962
4219
|
PAGER: "cat"
|
|
3963
4220
|
};
|
|
3964
4221
|
if (process.platform === "win32") return { ...env, ...quiet };
|
|
3965
|
-
writeIfChanged(
|
|
4222
|
+
writeIfChanged(path21.join(hooksDir(), "reference-transaction"), REF_HOOK);
|
|
3966
4223
|
env = withGitConfig(env, [
|
|
3967
4224
|
["core.hooksPath", hooksDir()],
|
|
3968
4225
|
["url.pitroom-push-blocked://.pushInsteadOf", ""]
|
|
@@ -3970,14 +4227,14 @@ function guardEnv(env) {
|
|
|
3970
4227
|
const dir = shimDir();
|
|
3971
4228
|
const real = findRealGit(dir);
|
|
3972
4229
|
if (!real) return { ...env, ...quiet };
|
|
3973
|
-
writeIfChanged(
|
|
3974
|
-
return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${
|
|
4230
|
+
writeIfChanged(path21.join(dir, "git"), SHIM);
|
|
4231
|
+
return { ...env, ...quiet, PITROOM_REAL_GIT: real, PATH: `${dir}${path21.delimiter}${env.PATH ?? ""}` };
|
|
3975
4232
|
}
|
|
3976
4233
|
|
|
3977
4234
|
// src/core/process.ts
|
|
3978
4235
|
async function spawnWorker(inv, opts) {
|
|
3979
|
-
const out =
|
|
3980
|
-
const err =
|
|
4236
|
+
const out = fs25.openSync(opts.stdoutFile, "w");
|
|
4237
|
+
const err = fs25.openSync(opts.stderrFile, "w");
|
|
3981
4238
|
const res = { code: null, timedOut: false, stopped: false };
|
|
3982
4239
|
const child = spawn2(inv.command, inv.args, {
|
|
3983
4240
|
cwd: opts.cwd,
|
|
@@ -4009,8 +4266,8 @@ async function spawnWorker(inv, opts) {
|
|
|
4009
4266
|
if (killer) clearTimeout(killer);
|
|
4010
4267
|
process.off("SIGINT", stop);
|
|
4011
4268
|
process.off("SIGTERM", stop);
|
|
4012
|
-
|
|
4013
|
-
|
|
4269
|
+
fs25.closeSync(out);
|
|
4270
|
+
fs25.closeSync(err);
|
|
4014
4271
|
return res;
|
|
4015
4272
|
}
|
|
4016
4273
|
|
|
@@ -4042,11 +4299,11 @@ function parseAudit(text) {
|
|
|
4042
4299
|
}
|
|
4043
4300
|
|
|
4044
4301
|
// src/core/slots.ts
|
|
4045
|
-
import
|
|
4046
|
-
import
|
|
4047
|
-
import
|
|
4048
|
-
var slotsDir = () =>
|
|
4049
|
-
var locksDir = () =>
|
|
4302
|
+
import crypto8 from "node:crypto";
|
|
4303
|
+
import fs26 from "node:fs";
|
|
4304
|
+
import path22 from "node:path";
|
|
4305
|
+
var slotsDir = () => path22.join(home(), "slots");
|
|
4306
|
+
var locksDir = () => path22.join(home(), "locks");
|
|
4050
4307
|
var STARTUP_GRACE_MS = 3e4;
|
|
4051
4308
|
function holderActive(runId, self) {
|
|
4052
4309
|
if (!runId || runId === self) return false;
|
|
@@ -4059,51 +4316,51 @@ function holderActive(runId, self) {
|
|
|
4059
4316
|
return false;
|
|
4060
4317
|
}
|
|
4061
4318
|
}
|
|
4062
|
-
function tryClaim(
|
|
4063
|
-
|
|
4319
|
+
function tryClaim(file2, runId) {
|
|
4320
|
+
fs26.mkdirSync(path22.dirname(file2), { recursive: true });
|
|
4064
4321
|
try {
|
|
4065
|
-
|
|
4322
|
+
fs26.writeFileSync(file2, runId, { flag: "wx" });
|
|
4066
4323
|
return true;
|
|
4067
4324
|
} catch (e) {
|
|
4068
4325
|
if (e.code !== "EEXIST") throw e;
|
|
4069
4326
|
}
|
|
4070
4327
|
let owner = "";
|
|
4071
4328
|
try {
|
|
4072
|
-
owner =
|
|
4329
|
+
owner = fs26.readFileSync(file2, "utf8").trim();
|
|
4073
4330
|
} catch {
|
|
4074
4331
|
}
|
|
4075
4332
|
if (owner === runId) return true;
|
|
4076
4333
|
if (holderActive(owner, runId)) return false;
|
|
4077
|
-
|
|
4334
|
+
fs26.rmSync(file2, { force: true });
|
|
4078
4335
|
try {
|
|
4079
|
-
|
|
4336
|
+
fs26.writeFileSync(file2, runId, { flag: "wx" });
|
|
4080
4337
|
return true;
|
|
4081
4338
|
} catch {
|
|
4082
4339
|
return false;
|
|
4083
4340
|
}
|
|
4084
4341
|
}
|
|
4085
|
-
function releaseIfOwner(
|
|
4086
|
-
if (!
|
|
4342
|
+
function releaseIfOwner(file2, runId) {
|
|
4343
|
+
if (!file2) return;
|
|
4087
4344
|
try {
|
|
4088
|
-
if (
|
|
4345
|
+
if (fs26.readFileSync(file2, "utf8").trim() === runId) fs26.rmSync(file2, { force: true });
|
|
4089
4346
|
} catch {
|
|
4090
4347
|
}
|
|
4091
4348
|
}
|
|
4092
4349
|
function tryAcquireSlot(runId, maxParallel) {
|
|
4093
4350
|
for (let n = 0; n < Math.max(1, maxParallel); n++) {
|
|
4094
|
-
const
|
|
4095
|
-
if (tryClaim(
|
|
4351
|
+
const file2 = path22.join(slotsDir(), `slot-${n}`);
|
|
4352
|
+
if (tryClaim(file2, runId)) return file2;
|
|
4096
4353
|
}
|
|
4097
4354
|
return void 0;
|
|
4098
4355
|
}
|
|
4099
|
-
var releaseSlot = (
|
|
4100
|
-
var lockFile = (repoRoot2) =>
|
|
4356
|
+
var releaseSlot = (file2, runId) => releaseIfOwner(file2, runId);
|
|
4357
|
+
var lockFile = (repoRoot2) => path22.join(locksDir(), `write-${crypto8.createHash("sha1").update(path22.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
|
|
4101
4358
|
function acquireWriteLock(repoRoot2, runId) {
|
|
4102
|
-
const
|
|
4103
|
-
if (tryClaim(
|
|
4359
|
+
const file2 = lockFile(repoRoot2);
|
|
4360
|
+
if (tryClaim(file2, runId)) return;
|
|
4104
4361
|
let owner = "";
|
|
4105
4362
|
try {
|
|
4106
|
-
owner =
|
|
4363
|
+
owner = fs26.readFileSync(file2, "utf8").trim();
|
|
4107
4364
|
} catch {
|
|
4108
4365
|
}
|
|
4109
4366
|
throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
|
|
@@ -4150,12 +4407,12 @@ ${task}`;
|
|
|
4150
4407
|
}
|
|
4151
4408
|
|
|
4152
4409
|
// src/core/refs.ts
|
|
4153
|
-
import
|
|
4154
|
-
import
|
|
4410
|
+
import fs27 from "node:fs";
|
|
4411
|
+
import path23 from "node:path";
|
|
4155
4412
|
var FILE_EXTENSIONS = new Set(
|
|
4156
4413
|
"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
4414
|
);
|
|
4158
|
-
var
|
|
4415
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
4159
4416
|
var MAX_INDEXED = 6e4;
|
|
4160
4417
|
var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
|
|
4161
4418
|
var REF = new RegExp(
|
|
@@ -4202,29 +4459,29 @@ function extractRefs(answer) {
|
|
|
4202
4459
|
return [...seen.values()];
|
|
4203
4460
|
}
|
|
4204
4461
|
function verifyRefs(refs, dirs) {
|
|
4205
|
-
const roots = [...new Set(dirs.filter(Boolean).map((d) =>
|
|
4462
|
+
const roots = [...new Set(dirs.filter(Boolean).map((d) => path23.resolve(d)))];
|
|
4206
4463
|
const invalid = [];
|
|
4207
4464
|
const lineCache = /* @__PURE__ */ new Map();
|
|
4208
|
-
const load = (
|
|
4209
|
-
if (!lineCache.has(
|
|
4210
|
-
const size =
|
|
4211
|
-
const lines = size > MAX_BYTES ? null :
|
|
4465
|
+
const load = (file2) => {
|
|
4466
|
+
if (!lineCache.has(file2)) {
|
|
4467
|
+
const size = fs27.statSync(file2).size;
|
|
4468
|
+
const lines = size > MAX_BYTES ? null : fs27.readFileSync(file2, "utf8").split("\n");
|
|
4212
4469
|
if (lines && lines[lines.length - 1] === "") lines.pop();
|
|
4213
|
-
lineCache.set(
|
|
4470
|
+
lineCache.set(file2, lines);
|
|
4214
4471
|
}
|
|
4215
|
-
return lineCache.get(
|
|
4472
|
+
return lineCache.get(file2);
|
|
4216
4473
|
};
|
|
4217
4474
|
let index;
|
|
4218
4475
|
const byName = () => index ??= indexFiles(roots);
|
|
4219
4476
|
const candidates = (ref) => {
|
|
4220
4477
|
const direct = resolve(ref.file, roots);
|
|
4221
4478
|
if (direct) return [direct];
|
|
4222
|
-
if (
|
|
4479
|
+
if (path23.isAbsolute(ref.file)) return [];
|
|
4223
4480
|
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
4224
|
-
return (byName().get(
|
|
4481
|
+
return (byName().get(path23.basename(wanted)) ?? []).filter((f) => f.endsWith(`/${wanted}`) || path23.basename(f) === wanted).slice(0, 20);
|
|
4225
4482
|
};
|
|
4226
|
-
const check = (
|
|
4227
|
-
const lines = load(
|
|
4483
|
+
const check = (file2, ref) => {
|
|
4484
|
+
const lines = load(file2);
|
|
4228
4485
|
if (!lines) return void 0;
|
|
4229
4486
|
if (ref.end > lines.length) return `file has ${lines.length} lines`;
|
|
4230
4487
|
if (ref.symbol && !mentions(lines, ref) && !enclosedBy(lines, ref)) return `\`${ref.symbol}\` not near line ${ref.start}`;
|
|
@@ -4236,7 +4493,7 @@ function verifyRefs(refs, dirs) {
|
|
|
4236
4493
|
if (!files.length) {
|
|
4237
4494
|
if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
|
|
4238
4495
|
total++;
|
|
4239
|
-
invalid.push({ ref: ref.text, reason:
|
|
4496
|
+
invalid.push({ ref: ref.text, reason: path23.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
|
|
4240
4497
|
continue;
|
|
4241
4498
|
}
|
|
4242
4499
|
total++;
|
|
@@ -4257,7 +4514,7 @@ function enclosedBy(lines, ref) {
|
|
|
4257
4514
|
return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
|
|
4258
4515
|
}
|
|
4259
4516
|
var isFileName = (name) => {
|
|
4260
|
-
const base2 =
|
|
4517
|
+
const base2 = path23.basename(name);
|
|
4261
4518
|
return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
|
|
4262
4519
|
};
|
|
4263
4520
|
function indexFiles(roots) {
|
|
@@ -4266,19 +4523,19 @@ function indexFiles(roots) {
|
|
|
4266
4523
|
const walk = (dir) => {
|
|
4267
4524
|
let entries;
|
|
4268
4525
|
try {
|
|
4269
|
-
entries =
|
|
4526
|
+
entries = fs27.readdirSync(dir, { withFileTypes: true });
|
|
4270
4527
|
} catch {
|
|
4271
4528
|
return;
|
|
4272
4529
|
}
|
|
4273
4530
|
for (const e of entries) {
|
|
4274
4531
|
if (count >= MAX_INDEXED) return;
|
|
4275
4532
|
if (e.isDirectory()) {
|
|
4276
|
-
if (!
|
|
4533
|
+
if (!SKIP_DIRS2.has(e.name)) walk(path23.join(dir, e.name));
|
|
4277
4534
|
} else if (e.isFile()) {
|
|
4278
4535
|
count++;
|
|
4279
4536
|
const list2 = index.get(e.name);
|
|
4280
|
-
if (list2) list2.push(
|
|
4281
|
-
else index.set(e.name, [
|
|
4537
|
+
if (list2) list2.push(path23.join(dir, e.name));
|
|
4538
|
+
else index.set(e.name, [path23.join(dir, e.name)]);
|
|
4282
4539
|
}
|
|
4283
4540
|
}
|
|
4284
4541
|
};
|
|
@@ -4286,76 +4543,25 @@ function indexFiles(roots) {
|
|
|
4286
4543
|
return index;
|
|
4287
4544
|
}
|
|
4288
4545
|
function resolve(ref, roots) {
|
|
4289
|
-
const candidates =
|
|
4546
|
+
const candidates = path23.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path23.join(r, ref));
|
|
4290
4547
|
return candidates.find((c) => {
|
|
4291
4548
|
try {
|
|
4292
|
-
return
|
|
4549
|
+
return fs27.statSync(c).isFile() && inside(c, roots);
|
|
4293
4550
|
} catch {
|
|
4294
4551
|
return false;
|
|
4295
4552
|
}
|
|
4296
4553
|
});
|
|
4297
4554
|
}
|
|
4298
|
-
function inside(
|
|
4555
|
+
function inside(file2, roots) {
|
|
4299
4556
|
return roots.some((r) => {
|
|
4300
|
-
const rel =
|
|
4301
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
4557
|
+
const rel = path23.relative(r, path23.resolve(file2));
|
|
4558
|
+
return rel !== "" && !rel.startsWith("..") && !path23.isAbsolute(rel);
|
|
4302
4559
|
});
|
|
4303
4560
|
}
|
|
4304
4561
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4305
4562
|
|
|
4306
|
-
// src/core/secrets.ts
|
|
4307
|
-
import { execFileSync } from "node:child_process";
|
|
4308
|
-
import fs25 from "node:fs";
|
|
4309
|
-
import path21 from "node:path";
|
|
4310
|
-
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
4311
|
-
var MAX_VISITED = 2e4;
|
|
4312
|
-
var MAX_DEPTH = 4;
|
|
4313
|
-
var TEMPLATE2 = /\.(example|sample|template|dist|defaults?|tpl)$/i;
|
|
4314
|
-
function looksSecret(name) {
|
|
4315
|
-
if (name === ".env" || name.startsWith(".env.") && !TEMPLATE2.test(name)) return true;
|
|
4316
|
-
return /\.(pem|p12|pfx)$/i.test(name) || /^id_(rsa|dsa|ecdsa|ed25519)$/.test(name) || name === ".netrc" || name === "credentials.json";
|
|
4317
|
-
}
|
|
4318
|
-
function findSecretFiles(dir) {
|
|
4319
|
-
const found = [];
|
|
4320
|
-
let visited = 0;
|
|
4321
|
-
const walk = (d, depth) => {
|
|
4322
|
-
if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
|
|
4323
|
-
let entries;
|
|
4324
|
-
try {
|
|
4325
|
-
entries = fs25.readdirSync(d, { withFileTypes: true });
|
|
4326
|
-
} catch {
|
|
4327
|
-
return;
|
|
4328
|
-
}
|
|
4329
|
-
for (const e of entries) {
|
|
4330
|
-
visited++;
|
|
4331
|
-
if (e.isDirectory()) {
|
|
4332
|
-
if (!SKIP_DIRS2.has(e.name)) walk(path21.join(d, e.name), depth + 1);
|
|
4333
|
-
} else if (e.isFile() && looksSecret(e.name)) {
|
|
4334
|
-
found.push(path21.relative(dir, path21.join(d, e.name)));
|
|
4335
|
-
}
|
|
4336
|
-
}
|
|
4337
|
-
};
|
|
4338
|
-
walk(dir, 0);
|
|
4339
|
-
return found.sort();
|
|
4340
|
-
}
|
|
4341
|
-
function findSecretFilesInTree(root, dir) {
|
|
4342
|
-
try {
|
|
4343
|
-
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(path21.basename(f)) && !path21.relative(dir, path21.join(root, f)).startsWith("..")).map((f) => path21.relative(dir, path21.join(root, f))).sort();
|
|
4345
|
-
} catch {
|
|
4346
|
-
return [];
|
|
4347
|
-
}
|
|
4348
|
-
}
|
|
4349
|
-
function secretWarning(files, mode) {
|
|
4350
|
-
if (!files.length) return void 0;
|
|
4351
|
-
const shown = files.slice(0, 3).join(", ") + (files.length > 3 ? `, +${files.length - 3} more` : "");
|
|
4352
|
-
const where = mode === "isolate" ? "would be copied into the isolated copy" : "sit in the directory the worker runs in";
|
|
4353
|
-
const fix = mode === "isolate" ? "add them to .gitignore, or" : "use an isolated copy (-i, git-ignored files are left out) or a clean checkout, or";
|
|
4354
|
-
return `secret-looking files ${where} (${shown}): the worker is told not to read them, but nothing stops it, and its model may be hosted by a third party; ${fix} set PITROOM_NO_SECRET_WARNING=1 to silence this`;
|
|
4355
|
-
}
|
|
4356
|
-
|
|
4357
4563
|
// src/core/run.ts
|
|
4358
|
-
var VERSION2 = true ? "0.
|
|
4564
|
+
var VERSION2 = true ? "0.13.0" : "0.0.0-dev";
|
|
4359
4565
|
function planWork(o) {
|
|
4360
4566
|
if (!o.plan) return { task: o.task.trim(), tier: o.tier };
|
|
4361
4567
|
if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
|
|
@@ -4387,7 +4593,7 @@ function prepareRun(o) {
|
|
|
4387
4593
|
parent = readMeta(o.continueFrom);
|
|
4388
4594
|
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
4595
|
if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
|
|
4390
|
-
if (parent.mode === "isolate" && (!parent.worktree || !
|
|
4596
|
+
if (parent.mode === "isolate" && (!parent.worktree || !fs28.existsSync(parent.worktree))) {
|
|
4391
4597
|
throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
|
|
4392
4598
|
}
|
|
4393
4599
|
const ran = parent.ran ?? parent.worker;
|
|
@@ -4406,16 +4612,21 @@ function prepareRun(o) {
|
|
|
4406
4612
|
throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
|
|
4407
4613
|
}
|
|
4408
4614
|
const mode = parent?.mode ?? o.mode;
|
|
4409
|
-
const dir =
|
|
4410
|
-
if (!
|
|
4615
|
+
const dir = path24.resolve(parent?.dir ?? o.dir);
|
|
4616
|
+
if (!fs28.existsSync(dir) || !fs28.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
|
|
4411
4617
|
const root = repoRoot(dir);
|
|
4412
4618
|
if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
|
|
4413
4619
|
if (mode === "write" && !root && !o.allowNonGit) {
|
|
4414
4620
|
throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
|
|
4415
4621
|
}
|
|
4416
|
-
const files = o.files.map((f) =>
|
|
4417
|
-
for (const f of files) if (!
|
|
4418
|
-
|
|
4622
|
+
const files = o.files.map((f) => path24.resolve(f));
|
|
4623
|
+
for (const f of files) if (!fs28.existsSync(f)) throw new UserError(`file not found: ${f}`);
|
|
4624
|
+
const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
|
|
4625
|
+
const snap = mode === "read" && !o.review && !o.inPlace && !parent ? wantSnapshot(readIn2, root, dir) : void 0;
|
|
4626
|
+
if (parent?.snapshot?.dir && !fs28.existsSync(parent.snapshot.dir)) {
|
|
4627
|
+
throw new UserError(`the read snapshot of run ${parent.id} is gone (cleaned up); ask the question again as a new run`, 3);
|
|
4628
|
+
}
|
|
4629
|
+
if (!parent && !snap && !process.env.PITROOM_NO_SECRET_WARNING) {
|
|
4419
4630
|
const secrets = mode === "isolate" && root ? findSecretFilesInTree(root, dir) : findSecretFiles(dir);
|
|
4420
4631
|
const note = secretWarning(secrets, mode);
|
|
4421
4632
|
if (note) warnings.push(note);
|
|
@@ -4447,6 +4658,8 @@ function prepareRun(o) {
|
|
|
4447
4658
|
reviewOf: o.review?.of,
|
|
4448
4659
|
auditOf: o.audit?.of,
|
|
4449
4660
|
auditRate: o.auditRate,
|
|
4661
|
+
inPlace: o.inPlace || void 0,
|
|
4662
|
+
snapshot: parent?.snapshot ?? (snap ? { dir: "", tree: "", left: snap.secrets } : void 0),
|
|
4450
4663
|
plan: work.plan ?? o.review?.plan ?? parent?.plan,
|
|
4451
4664
|
reviewKind: o.review?.kind,
|
|
4452
4665
|
packageFile: o.review?.packageFile
|
|
@@ -4456,13 +4669,13 @@ function prepareRun(o) {
|
|
|
4456
4669
|
try {
|
|
4457
4670
|
acquireWriteLock(root, meta.id);
|
|
4458
4671
|
} catch (e) {
|
|
4459
|
-
|
|
4672
|
+
fs28.rmSync(runDir(meta.id), { recursive: true, force: true });
|
|
4460
4673
|
throw e;
|
|
4461
4674
|
}
|
|
4462
4675
|
}
|
|
4463
|
-
|
|
4676
|
+
fs28.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
|
|
4464
4677
|
`);
|
|
4465
|
-
if (work.brief)
|
|
4678
|
+
if (work.brief) fs28.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
|
|
4466
4679
|
return meta;
|
|
4467
4680
|
}
|
|
4468
4681
|
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -4519,12 +4732,19 @@ async function execute(meta) {
|
|
|
4519
4732
|
for (let i = 0; i < chain.length; i++) {
|
|
4520
4733
|
const target = chain[i];
|
|
4521
4734
|
const backend = getBackend(target.backend);
|
|
4735
|
+
const cooling = activeCooldown(cooldownKey(target, backend));
|
|
4736
|
+
if (cooling && chain.slice(i + 1).some((t) => !activeCooldown(cooldownKey(t, getBackend(t.backend))))) {
|
|
4737
|
+
(meta.attempts ??= []).push({ target: describeTarget(target), error: `cooling down ${untilText(cooling)}: ${cooling.reason}`, skipped: true });
|
|
4738
|
+
writeMeta(meta);
|
|
4739
|
+
continue;
|
|
4740
|
+
}
|
|
4522
4741
|
meta.ran = target;
|
|
4523
4742
|
writeMeta(meta);
|
|
4524
4743
|
result = await attempt(meta, backend, target);
|
|
4525
4744
|
if (result.timedOut || result.stopped || result.spawnError) break;
|
|
4526
4745
|
const why = retryableFailure(meta, backend, result);
|
|
4527
4746
|
if (!why) break;
|
|
4747
|
+
if (why.kind === "rate-limited") recordCooldown(cooldownKey(target, backend), describeTarget(target), why.message);
|
|
4528
4748
|
if (!target.model && backend.defaultModel) {
|
|
4529
4749
|
const failed = backend.defaultModel();
|
|
4530
4750
|
for (let j = chain.length - 1; j > i; j--) {
|
|
@@ -4535,7 +4755,7 @@ async function execute(meta) {
|
|
|
4535
4755
|
(meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
|
|
4536
4756
|
for (const f of ["events.jsonl", "stderr.log"]) {
|
|
4537
4757
|
const from = runFile(meta.id, f);
|
|
4538
|
-
if (
|
|
4758
|
+
if (fs28.existsSync(from)) fs28.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
|
|
4539
4759
|
}
|
|
4540
4760
|
writeMeta(meta);
|
|
4541
4761
|
}
|
|
@@ -4554,13 +4774,13 @@ async function execute(meta) {
|
|
|
4554
4774
|
}
|
|
4555
4775
|
function autoAudit(meta) {
|
|
4556
4776
|
try {
|
|
4557
|
-
if (!auditable(meta,
|
|
4777
|
+
if (!auditable(meta, read2(runFile(meta.id, "summary.md"))) || !sampled(meta)) return;
|
|
4558
4778
|
startAudit(meta);
|
|
4559
4779
|
} catch {
|
|
4560
4780
|
}
|
|
4561
4781
|
}
|
|
4562
4782
|
function startAudit(meta, worker) {
|
|
4563
|
-
const answer =
|
|
4783
|
+
const answer = read2(runFile(meta.id, "summary.md"));
|
|
4564
4784
|
const auditor = worker ?? pickAuditor(meta);
|
|
4565
4785
|
if (!auditor) return void 0;
|
|
4566
4786
|
const a = prepareRun({
|
|
@@ -4593,12 +4813,26 @@ function settleAudit(a) {
|
|
|
4593
4813
|
}
|
|
4594
4814
|
function prepareTree(meta) {
|
|
4595
4815
|
const root = meta.repoRoot;
|
|
4816
|
+
if (root && meta.mode === "read" && meta.snapshot && !meta.snapshot.dir) {
|
|
4817
|
+
const shown = meta.snapshot.left.slice(0, 3).join(", ") + (meta.snapshot.left.length > 3 ? `, +${meta.snapshot.left.length - 3} more` : "");
|
|
4818
|
+
try {
|
|
4819
|
+
const s = readSnapshot(root);
|
|
4820
|
+
meta.snapshot.dir = s.dir;
|
|
4821
|
+
meta.snapshot.tree = s.tree;
|
|
4822
|
+
meta.cwd = path24.join(s.dir, path24.relative(root, meta.dir));
|
|
4823
|
+
if (!fs28.existsSync(meta.cwd)) meta.cwd = s.dir;
|
|
4824
|
+
if (shown) meta.warnings.push(`read in a clean snapshot of your project: the secret-looking files (${shown}) and git-ignored files are not in it; --in-place reads the directory itself`);
|
|
4825
|
+
} catch (e) {
|
|
4826
|
+
meta.snapshot = void 0;
|
|
4827
|
+
meta.warnings.push(`could not make a clean snapshot (${e.message.slice(0, 120)}): the worker reads the directory itself${shown ? `, where secret-looking files sit (${shown})` : ""}`);
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4596
4830
|
if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
|
|
4597
4831
|
if (root && meta.mode === "isolate" && !meta.worktree) {
|
|
4598
4832
|
meta.baseTree = snapshotTree(root);
|
|
4599
|
-
meta.worktree =
|
|
4833
|
+
meta.worktree = path24.join(worktreesDir(), meta.id);
|
|
4600
4834
|
createIsolatedCopy(root, meta.baseTree, meta.worktree);
|
|
4601
|
-
meta.cwd =
|
|
4835
|
+
meta.cwd = path24.join(meta.worktree, path24.relative(root, meta.dir));
|
|
4602
4836
|
const linked = linkIntoWorktree(root, meta.worktree, meta.link);
|
|
4603
4837
|
if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
|
|
4604
4838
|
}
|
|
@@ -4621,11 +4855,11 @@ function attempt(meta, backend, target) {
|
|
|
4621
4855
|
timeoutSec: meta.timeoutSec
|
|
4622
4856
|
});
|
|
4623
4857
|
}
|
|
4624
|
-
var
|
|
4858
|
+
var read2 = (f) => fs28.existsSync(f) ? fs28.readFileSync(f, "utf8") : "";
|
|
4625
4859
|
function retryableFailure(meta, backend, res) {
|
|
4626
|
-
const run = backend.parse(
|
|
4860
|
+
const run = backend.parse(read2(runFile(meta.id, "events.jsonl")));
|
|
4627
4861
|
if (run.usage.steps > 0) return void 0;
|
|
4628
|
-
const f = backend.failure(run,
|
|
4862
|
+
const f = backend.failure(run, read2(runFile(meta.id, "stderr.log")), res.code);
|
|
4629
4863
|
return f && f.kind !== "other" ? f : void 0;
|
|
4630
4864
|
}
|
|
4631
4865
|
var HINTS = {
|
|
@@ -4637,18 +4871,19 @@ var HINTS = {
|
|
|
4637
4871
|
function finalize(meta, res) {
|
|
4638
4872
|
const ran = meta.ran ?? meta.worker;
|
|
4639
4873
|
const backend = getBackend(ran.backend);
|
|
4640
|
-
const run = backend.parse(
|
|
4874
|
+
const run = backend.parse(read2(runFile(meta.id, "events.jsonl")));
|
|
4875
|
+
if (meta.snapshot?.dir && meta.repoRoot) run.finalText = backToProject(run.finalText, meta.snapshot.dir, meta.repoRoot);
|
|
4641
4876
|
meta.sessionId = run.sessionId ?? meta.sessionId;
|
|
4642
4877
|
meta.usage = run.usage;
|
|
4643
|
-
|
|
4878
|
+
fs28.writeFileSync(runFile(meta.id, "summary.md"), `${run.finalText}
|
|
4644
4879
|
`);
|
|
4645
4880
|
if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run.finalText);
|
|
4646
4881
|
if (meta.reviewOf) {
|
|
4647
4882
|
meta.verdict = parseVerdict(run.finalText);
|
|
4648
|
-
if (meta.packageFile)
|
|
4883
|
+
if (meta.packageFile) fs28.rmSync(meta.packageFile, { force: true });
|
|
4649
4884
|
}
|
|
4650
4885
|
if (!res.timedOut && !res.stopped && !meta.error) {
|
|
4651
|
-
const f = backend.failure(run,
|
|
4886
|
+
const f = backend.failure(run, read2(runFile(meta.id, "stderr.log")), res.code);
|
|
4652
4887
|
if (f) meta.error = HINTS[f.kind] ? `${f.message} (${HINTS[f.kind]})` : f.message;
|
|
4653
4888
|
}
|
|
4654
4889
|
try {
|
|
@@ -4693,7 +4928,7 @@ function captureChanges(meta) {
|
|
|
4693
4928
|
const d = diffTrees(target, meta.baseTree, meta.afterTree);
|
|
4694
4929
|
meta.changes = d.changes;
|
|
4695
4930
|
meta.stats = d.stats;
|
|
4696
|
-
|
|
4931
|
+
fs28.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
|
|
4697
4932
|
}
|
|
4698
4933
|
function runVerify(meta) {
|
|
4699
4934
|
const r = spawnSync6(meta.verify, {
|
|
@@ -4706,7 +4941,7 @@ function runVerify(meta) {
|
|
|
4706
4941
|
env: { ...process.env, PWD: meta.cwd, PITROOM_ACTIVE: "1" }
|
|
4707
4942
|
});
|
|
4708
4943
|
const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
|
4709
|
-
|
|
4944
|
+
fs28.writeFileSync(runFile(meta.id, "verify.log"), output);
|
|
4710
4945
|
return { ok: r.status === 0, code: r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
|
|
4711
4946
|
}
|
|
4712
4947
|
function applyRun(meta, allowDelete = false) {
|
|
@@ -4751,7 +4986,7 @@ function discardRun(meta) {
|
|
|
4751
4986
|
return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
|
|
4752
4987
|
}
|
|
4753
4988
|
function cleanupWorktree(meta) {
|
|
4754
|
-
if (meta.worktree &&
|
|
4989
|
+
if (meta.worktree && fs28.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
|
|
4755
4990
|
}
|
|
4756
4991
|
|
|
4757
4992
|
// src/cli/commands.ts
|
|
@@ -4786,7 +5021,7 @@ async function cmdReview(p) {
|
|
|
4786
5021
|
let meta;
|
|
4787
5022
|
try {
|
|
4788
5023
|
meta = prepareRun({
|
|
4789
|
-
...runOptions(p, fill(loadTemplate(
|
|
5024
|
+
...runOptions(p, fill(loadTemplate(TEMPLATE2[job.kind]), { PACKAGE_FILE: packageFile })),
|
|
4790
5025
|
mode: "read",
|
|
4791
5026
|
dir: job.dir,
|
|
4792
5027
|
worker: flag(p, "worker") ?? (flag(p, "tier") ? void 0 : pickReviewer(job)),
|
|
@@ -4801,10 +5036,10 @@ async function cmdReview(p) {
|
|
|
4801
5036
|
writeMeta(meta);
|
|
4802
5037
|
}
|
|
4803
5038
|
} catch (e) {
|
|
4804
|
-
|
|
5039
|
+
fs29.rmSync(packageFile, { force: true });
|
|
4805
5040
|
throw e;
|
|
4806
5041
|
}
|
|
4807
|
-
|
|
5042
|
+
fs29.writeFileSync(runFile(meta.id, "package.md"), job.package);
|
|
4808
5043
|
return launch(p, meta);
|
|
4809
5044
|
}
|
|
4810
5045
|
async function cmdAudit(p) {
|
|
@@ -4834,24 +5069,45 @@ async function cmdAudit(p) {
|
|
|
4834
5069
|
writeMeta(m);
|
|
4835
5070
|
return launch(p, a);
|
|
4836
5071
|
}
|
|
5072
|
+
function cmdCooldown(p) {
|
|
5073
|
+
if (has(p, "clear")) {
|
|
5074
|
+
const n = clearCooldowns();
|
|
5075
|
+
console.log(n ? `cleared ${n} cooldown${n === 1 ? "" : "s"}: those models are tried again` : "no cooldowns");
|
|
5076
|
+
return 0;
|
|
5077
|
+
}
|
|
5078
|
+
const all = activeCooldowns();
|
|
5079
|
+
if (has(p, "json")) {
|
|
5080
|
+
console.log(JSON.stringify(all, null, 2));
|
|
5081
|
+
return 0;
|
|
5082
|
+
}
|
|
5083
|
+
const rows = Object.values(all);
|
|
5084
|
+
if (!rows.length) {
|
|
5085
|
+
console.log("no model is cooling down");
|
|
5086
|
+
return 0;
|
|
5087
|
+
}
|
|
5088
|
+
console.log("Models left alone for now (runs go to the next worker; they are tried again after the time shown):");
|
|
5089
|
+
for (const c of rows) console.log(` ${c.target.padEnd(52)} ${untilText(c)} \xB7 ${c.reason.slice(0, 90)}`);
|
|
5090
|
+
console.log("\npitroom cooldown --clear try them again now");
|
|
5091
|
+
return 0;
|
|
5092
|
+
}
|
|
4837
5093
|
function cmdPlan(p) {
|
|
4838
|
-
const [sub,
|
|
4839
|
-
if (sub === "status" &&
|
|
4840
|
-
const s = planStatus(
|
|
5094
|
+
const [sub, file2, ...rest] = p.positional;
|
|
5095
|
+
if (sub === "status" && file2) {
|
|
5096
|
+
const s = planStatus(file2);
|
|
4841
5097
|
console.log(
|
|
4842
5098
|
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
5099
|
);
|
|
4844
5100
|
return 0;
|
|
4845
5101
|
}
|
|
4846
|
-
if (sub === "note" &&
|
|
4847
|
-
console.log(`noted in ${addNote(
|
|
5102
|
+
if (sub === "note" && file2 && rest.length) {
|
|
5103
|
+
console.log(`noted in ${addNote(file2, rest.join(" "))}`);
|
|
4848
5104
|
return 0;
|
|
4849
5105
|
}
|
|
4850
5106
|
throw new UserError('usage: pitroom plan status PLAN.md [--json] | pitroom plan note PLAN.md "Task N: \u2026"');
|
|
4851
5107
|
}
|
|
4852
5108
|
function cmdCrew(p) {
|
|
4853
|
-
const
|
|
4854
|
-
const tasks = (
|
|
5109
|
+
const file2 = flag(p, "task-file");
|
|
5110
|
+
const tasks = (file2 ? fs29.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
|
|
4855
5111
|
if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
|
|
4856
5112
|
if (has(p, "write") && tasks.length > 1) {
|
|
4857
5113
|
throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
|
|
@@ -5052,7 +5308,7 @@ function sinceMs(s) {
|
|
|
5052
5308
|
if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
|
|
5053
5309
|
return Date.now() - Number(m[1]) * 864e5;
|
|
5054
5310
|
}
|
|
5055
|
-
var readStdin = () => process.stdin.isTTY ? "" :
|
|
5311
|
+
var readStdin = () => process.stdin.isTTY ? "" : fs29.readFileSync(0, "utf8");
|
|
5056
5312
|
function cmdStatusline(p) {
|
|
5057
5313
|
const input = readStdin();
|
|
5058
5314
|
const lines = [];
|
|
@@ -5092,8 +5348,8 @@ function cmdModels(p) {
|
|
|
5092
5348
|
return 0;
|
|
5093
5349
|
}
|
|
5094
5350
|
function cmdHookStart() {
|
|
5095
|
-
const script =
|
|
5096
|
-
if (
|
|
5351
|
+
const script = path25.join(path25.dirname(path25.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
|
|
5352
|
+
if (fs29.existsSync(script)) spawnSync7(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
|
|
5097
5353
|
return 0;
|
|
5098
5354
|
}
|
|
5099
5355
|
function cmdInit(p) {
|
|
@@ -5133,8 +5389,8 @@ function cmdSavings(p) {
|
|
|
5133
5389
|
est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
|
|
5134
5390
|
const out = flag(p, "card");
|
|
5135
5391
|
if (out) {
|
|
5136
|
-
|
|
5137
|
-
console.log(`card written to ${
|
|
5392
|
+
fs29.writeFileSync(out, card(t, period));
|
|
5393
|
+
console.log(`card written to ${path25.resolve(out)}`);
|
|
5138
5394
|
}
|
|
5139
5395
|
if (has(p, "badge")) console.log(`})`);
|
|
5140
5396
|
return 0;
|
|
@@ -5154,7 +5410,7 @@ function cmdConfig(p) {
|
|
|
5154
5410
|
console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
|
|
5155
5411
|
return 0;
|
|
5156
5412
|
}
|
|
5157
|
-
console.log(`config file: ${configPath()}${
|
|
5413
|
+
console.log(`config file: ${configPath()}${fs29.existsSync(configPath()) ? "" : " (not present)"}`);
|
|
5158
5414
|
for (const [key, s] of Object.entries(eff)) {
|
|
5159
5415
|
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
5416
|
console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
|
|
@@ -5178,9 +5434,10 @@ function cmdClean(p) {
|
|
|
5178
5434
|
const m = readMeta(id);
|
|
5179
5435
|
if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
|
|
5180
5436
|
recordRun(m);
|
|
5181
|
-
|
|
5437
|
+
fs29.rmSync(runDir(id), { recursive: true, force: true });
|
|
5182
5438
|
}
|
|
5183
|
-
|
|
5439
|
+
const snapshots = prune(Date.now(), 0);
|
|
5440
|
+
console.log(`removed ${old.length} run(s)${snapshots ? ` and ${snapshots} read snapshot${snapshots === 1 ? "" : "s"}` : ""}; the history and the savings ledger are kept`);
|
|
5184
5441
|
return 0;
|
|
5185
5442
|
}
|
|
5186
5443
|
var cmdRevert = (p) => (console.log(revertRun(freshMeta(resolveRun(p.positional[0])))), 0);
|
|
@@ -5188,9 +5445,9 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
|
|
|
5188
5445
|
|
|
5189
5446
|
// src/core/doctor.ts
|
|
5190
5447
|
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
5191
|
-
import
|
|
5448
|
+
import fs30 from "node:fs";
|
|
5192
5449
|
import os10 from "node:os";
|
|
5193
|
-
import
|
|
5450
|
+
import path26 from "node:path";
|
|
5194
5451
|
var MARK = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
|
|
5195
5452
|
var NEXT = [
|
|
5196
5453
|
{ when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
|
|
@@ -5200,15 +5457,15 @@ var NEXT = [
|
|
|
5200
5457
|
{ 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
5458
|
];
|
|
5202
5459
|
function firstNodeOnPath() {
|
|
5203
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
5204
|
-
const
|
|
5460
|
+
for (const dir of (process.env.PATH ?? "").split(path26.delimiter).filter(Boolean)) {
|
|
5461
|
+
const file2 = path26.join(dir, process.platform === "win32" ? "node.exe" : "node");
|
|
5205
5462
|
try {
|
|
5206
|
-
if (!
|
|
5463
|
+
if (!fs30.statSync(file2).isFile()) continue;
|
|
5207
5464
|
} catch {
|
|
5208
5465
|
continue;
|
|
5209
5466
|
}
|
|
5210
|
-
const r = spawnSync8(
|
|
5211
|
-
return r.status === 0 ? { path:
|
|
5467
|
+
const r = spawnSync8(file2, ["-p", "process.versions.node"], { encoding: "utf8", timeout: 5e3 });
|
|
5468
|
+
return r.status === 0 ? { path: file2, version: r.stdout.trim() } : { path: file2, version: "" };
|
|
5212
5469
|
}
|
|
5213
5470
|
return void 0;
|
|
5214
5471
|
}
|
|
@@ -5230,12 +5487,12 @@ function doctor5(probe) {
|
|
|
5230
5487
|
const addAll = (list2) => list2.forEach((c) => add(c.level, c.message));
|
|
5231
5488
|
add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
|
|
5232
5489
|
const onPath = firstNodeOnPath();
|
|
5233
|
-
if (onPath && onPath.version && tooOld(onPath.version) &&
|
|
5490
|
+
if (onPath && onPath.version && tooOld(onPath.version) && path26.resolve(onPath.path) !== path26.resolve(process.execPath)) {
|
|
5234
5491
|
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
5492
|
}
|
|
5236
5493
|
add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
|
|
5237
5494
|
const cfg = loadConfig();
|
|
5238
|
-
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${
|
|
5495
|
+
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs30.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
|
|
5239
5496
|
for (const w of cfg.warnings) add("warn", w);
|
|
5240
5497
|
if (process.platform !== "win32") {
|
|
5241
5498
|
const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
|
|
@@ -5277,6 +5534,9 @@ function doctor5(probe) {
|
|
|
5277
5534
|
add("ok", `cost: ${key} = ${mine}${cheaper ? `; you priced ${cheaper[0]} cheaper (${cheaper[1]}): is the dearer one needed?` : ""}`);
|
|
5278
5535
|
}
|
|
5279
5536
|
}
|
|
5537
|
+
for (const c of Object.values(activeCooldowns())) {
|
|
5538
|
+
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`);
|
|
5539
|
+
}
|
|
5280
5540
|
const byBackend = /* @__PURE__ */ new Map();
|
|
5281
5541
|
for (const t of [...chain, ...tierTargets]) byBackend.set(t.backend, [...byBackend.get(t.backend) ?? [], t.model]);
|
|
5282
5542
|
for (const [id, models] of byBackend) {
|
|
@@ -5380,19 +5640,19 @@ function skillChecks() {
|
|
|
5380
5640
|
else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
|
|
5381
5641
|
}
|
|
5382
5642
|
if (geminiExtensionInstalled()) {
|
|
5383
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
5643
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path26.sep}.agents${path26.sep}`) && i.names.length);
|
|
5384
5644
|
checks.push(
|
|
5385
5645
|
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
5646
|
);
|
|
5387
5647
|
}
|
|
5388
5648
|
if (viaPlugin) {
|
|
5389
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
5649
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path26.sep}.claude${path26.sep}`) && i.names.length);
|
|
5390
5650
|
checks.push(
|
|
5391
5651
|
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
5652
|
);
|
|
5393
5653
|
}
|
|
5394
5654
|
const launcher = launcherPath();
|
|
5395
|
-
if (
|
|
5655
|
+
if (fs30.existsSync(launcher)) {
|
|
5396
5656
|
const r = spawnSync8(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
5397
5657
|
checks.push(
|
|
5398
5658
|
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 +5664,12 @@ function skillChecks() {
|
|
|
5404
5664
|
}
|
|
5405
5665
|
function geminiExtensionInstalled() {
|
|
5406
5666
|
const home2 = process.env.GEMINI_CLI_HOME ?? os10.homedir();
|
|
5407
|
-
return
|
|
5667
|
+
return fs30.existsSync(path26.join(home2, ".gemini", "extensions", "pitroom"));
|
|
5408
5668
|
}
|
|
5409
5669
|
function pluginInstalled() {
|
|
5410
5670
|
try {
|
|
5411
|
-
const f =
|
|
5412
|
-
const plugins = JSON.parse(
|
|
5671
|
+
const f = path26.join(os10.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
5672
|
+
const plugins = JSON.parse(fs30.readFileSync(f, "utf8")).plugins ?? {};
|
|
5413
5673
|
return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
|
|
5414
5674
|
} catch {
|
|
5415
5675
|
return false;
|
|
@@ -5426,24 +5686,24 @@ function superpowersActive() {
|
|
|
5426
5686
|
for (const p of openCodePlugins()) {
|
|
5427
5687
|
if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
|
|
5428
5688
|
}
|
|
5429
|
-
for (const base2 of [
|
|
5430
|
-
const dir =
|
|
5431
|
-
if (
|
|
5689
|
+
for (const base2 of [path26.join(os10.homedir(), ".agents", "skills"), path26.join(os10.homedir(), ".claude", "skills")]) {
|
|
5690
|
+
const dir = path26.join(base2, "using-superpowers");
|
|
5691
|
+
if (fs30.existsSync(path26.join(dir, "SKILL.md"))) found.push(dir);
|
|
5432
5692
|
}
|
|
5433
5693
|
return found;
|
|
5434
5694
|
}
|
|
5435
5695
|
function claudePlugins() {
|
|
5436
5696
|
try {
|
|
5437
|
-
const f =
|
|
5438
|
-
return Object.keys(JSON.parse(
|
|
5697
|
+
const f = path26.join(os10.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
5698
|
+
return Object.keys(JSON.parse(fs30.readFileSync(f, "utf8")).plugins ?? {});
|
|
5439
5699
|
} catch {
|
|
5440
5700
|
return [];
|
|
5441
5701
|
}
|
|
5442
5702
|
}
|
|
5443
5703
|
function claudePluginEnabled(key) {
|
|
5444
5704
|
try {
|
|
5445
|
-
const f =
|
|
5446
|
-
return JSON.parse(
|
|
5705
|
+
const f = path26.join(os10.homedir(), ".claude", "settings.json");
|
|
5706
|
+
return JSON.parse(fs30.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
|
|
5447
5707
|
} catch {
|
|
5448
5708
|
return true;
|
|
5449
5709
|
}
|
|
@@ -5452,7 +5712,7 @@ function codexPlugins() {
|
|
|
5452
5712
|
const plugins = /* @__PURE__ */ new Map();
|
|
5453
5713
|
let text;
|
|
5454
5714
|
try {
|
|
5455
|
-
text =
|
|
5715
|
+
text = fs30.readFileSync(path26.join(process.env.CODEX_HOME ?? path26.join(os10.homedir(), ".codex"), "config.toml"), "utf8");
|
|
5456
5716
|
} catch {
|
|
5457
5717
|
return plugins;
|
|
5458
5718
|
}
|
|
@@ -5470,11 +5730,11 @@ function codexPlugins() {
|
|
|
5470
5730
|
return plugins;
|
|
5471
5731
|
}
|
|
5472
5732
|
function openCodePlugins() {
|
|
5473
|
-
const dir =
|
|
5733
|
+
const dir = path26.join(process.env.XDG_CONFIG_HOME ?? path26.join(os10.homedir(), ".config"), "opencode");
|
|
5474
5734
|
const plugins = [];
|
|
5475
5735
|
for (const name of ["opencode.json", "opencode.jsonc"]) {
|
|
5476
5736
|
try {
|
|
5477
|
-
const text =
|
|
5737
|
+
const text = fs30.readFileSync(path26.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
|
|
5478
5738
|
const list2 = JSON.parse(text).plugin;
|
|
5479
5739
|
if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
|
|
5480
5740
|
} catch {
|
|
@@ -5482,7 +5742,7 @@ function openCodePlugins() {
|
|
|
5482
5742
|
}
|
|
5483
5743
|
for (const folder of ["plugin", "plugins"]) {
|
|
5484
5744
|
try {
|
|
5485
|
-
for (const f of
|
|
5745
|
+
for (const f of fs30.readdirSync(path26.join(dir, folder))) plugins.push(path26.join(dir, folder, f));
|
|
5486
5746
|
} catch {
|
|
5487
5747
|
}
|
|
5488
5748
|
}
|
|
@@ -5532,6 +5792,8 @@ Usage
|
|
|
5532
5792
|
pitroom statusline [--then CMD] status-bar line: running workers, savings this week (after CMD's)
|
|
5533
5793
|
pitroom hook-card PostToolUse hook: a card after each Bash \`pitroom\` command
|
|
5534
5794
|
pitroom hook-start SessionStart hook: introduces Pitroom to the agent (Codex; Claude Code uses its plugin)
|
|
5795
|
+
pitroom cooldown [--clear] models that said "rate limited" and are skipped for a while (a quota used up);
|
|
5796
|
+
--clear tries them again
|
|
5535
5797
|
pitroom doctor [--probe] check workers, models, permissions, skills
|
|
5536
5798
|
pitroom config effective settings, where each comes from, config file path
|
|
5537
5799
|
pitroom init [--model ID] [--fallback A,B] [--yes] [--force]
|
|
@@ -5551,6 +5813,8 @@ Run options
|
|
|
5551
5813
|
-W, --worker T worker target "backend[:model]" (default: config "worker", else opencode)
|
|
5552
5814
|
-m, --model M model for that worker (default: the worker CLI's own default)
|
|
5553
5815
|
--tier NAME a worker from the config's "tiers" (e.g. cheap, standard, capable); -W wins
|
|
5816
|
+
--in-place a read run reads the directory itself, not a clean snapshot (default: a snapshot without secret-looking
|
|
5817
|
+
files when the directory has any; config "readIn": auto | snapshot | project)
|
|
5554
5818
|
--audit have another worker re-check this read run's answer; --no-audit skips it (default: the config's "audit")
|
|
5555
5819
|
--effort LEVEL reasoning effort for the worker: low, medium, high, xhigh, \u2026 (model#level)
|
|
5556
5820
|
--plan PLAN with --step N: implement Task N of a plan (-i or -w); the task text is your notes
|
|
@@ -5581,6 +5845,7 @@ var COMMANDS = {
|
|
|
5581
5845
|
crew: cmdCrew,
|
|
5582
5846
|
review: cmdReview,
|
|
5583
5847
|
audit: cmdAudit,
|
|
5848
|
+
cooldown: cmdCooldown,
|
|
5584
5849
|
plan: cmdPlan,
|
|
5585
5850
|
status: cmdStatus,
|
|
5586
5851
|
wait: cmdWait,
|