pitroom 0.16.0 → 0.18.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 +40 -0
- package/README.md +19 -6
- package/dist/lib.mjs +1 -1
- package/dist/pitroom.mjs +824 -511
- package/dist/ui/app.js +30 -30
- package/gemini-extension.json +1 -1
- package/package.json +2 -1
- package/skills/pitroom-driven-development/SKILL.md +1 -1
- package/skills/pitroom-review/SKILL.md +1 -1
package/dist/pitroom.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
4
|
+
import fs36 from "node:fs";
|
|
5
5
|
|
|
6
6
|
// src/core/errors.ts
|
|
7
7
|
var UserError = class extends Error {
|
|
@@ -555,16 +555,33 @@ var codex = {
|
|
|
555
555
|
|
|
556
556
|
// src/backends/gemini/index.ts
|
|
557
557
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
558
|
-
import
|
|
558
|
+
import fs6 from "node:fs";
|
|
559
559
|
import os5 from "node:os";
|
|
560
560
|
import path5 from "node:path";
|
|
561
561
|
import { fileURLToPath } from "node:url";
|
|
562
562
|
|
|
563
563
|
// src/core/store.ts
|
|
564
|
-
import
|
|
564
|
+
import fs5 from "node:fs";
|
|
565
565
|
import os4 from "node:os";
|
|
566
566
|
import path4 from "node:path";
|
|
567
567
|
import crypto from "node:crypto";
|
|
568
|
+
|
|
569
|
+
// src/core/fs-atomic.ts
|
|
570
|
+
import fs4 from "node:fs";
|
|
571
|
+
var BUSY = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
|
|
572
|
+
function renameOver(tmp, file2) {
|
|
573
|
+
for (let attempt3 = 0; ; attempt3++) {
|
|
574
|
+
try {
|
|
575
|
+
fs4.renameSync(tmp, file2);
|
|
576
|
+
return;
|
|
577
|
+
} catch (e) {
|
|
578
|
+
if (process.platform !== "win32" || !BUSY.has(e.code ?? "") || attempt3 >= 40) throw e;
|
|
579
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/core/store.ts
|
|
568
585
|
var TERMINAL = ["done", "failed", "timeout", "stopped"];
|
|
569
586
|
var isActive = (s) => !TERMINAL.includes(s);
|
|
570
587
|
function home() {
|
|
@@ -591,11 +608,11 @@ function useArchive(hooks) {
|
|
|
591
608
|
archive = { meta: hooks.meta, id: hooks.id };
|
|
592
609
|
}
|
|
593
610
|
function writeMeta(meta) {
|
|
594
|
-
|
|
611
|
+
fs5.mkdirSync(runDir(meta.id), { recursive: true });
|
|
595
612
|
const file2 = runFile(meta.id, "meta.json");
|
|
596
613
|
const tmp = `${file2}.${process.pid}.tmp`;
|
|
597
|
-
|
|
598
|
-
|
|
614
|
+
fs5.writeFileSync(tmp, JSON.stringify(meta, null, 2));
|
|
615
|
+
renameOver(tmp, file2);
|
|
599
616
|
if (TERMINAL.includes(meta.state)) {
|
|
600
617
|
try {
|
|
601
618
|
onFinished?.(meta);
|
|
@@ -605,11 +622,11 @@ function writeMeta(meta) {
|
|
|
605
622
|
}
|
|
606
623
|
function readMeta(id) {
|
|
607
624
|
const file2 = runFile(id, "meta.json");
|
|
608
|
-
if (!
|
|
625
|
+
if (!fs5.existsSync(file2)) {
|
|
609
626
|
const kept = archive?.meta(id);
|
|
610
627
|
if (kept) return upgrade(kept);
|
|
611
628
|
}
|
|
612
|
-
return upgrade(JSON.parse(
|
|
629
|
+
return upgrade(JSON.parse(fs5.readFileSync(file2, "utf8")));
|
|
613
630
|
}
|
|
614
631
|
function upgrade(raw) {
|
|
615
632
|
if (!raw.worker) raw.worker = raw.model ? { backend: "opencode", model: raw.model } : { backend: "opencode" };
|
|
@@ -622,8 +639,8 @@ function upgrade(raw) {
|
|
|
622
639
|
return raw;
|
|
623
640
|
}
|
|
624
641
|
function listRunIds() {
|
|
625
|
-
if (!
|
|
626
|
-
return
|
|
642
|
+
if (!fs5.existsSync(runsDir())) return [];
|
|
643
|
+
return fs5.readdirSync(runsDir()).filter((d) => fs5.existsSync(runFile(d, "meta.json"))).sort();
|
|
627
644
|
}
|
|
628
645
|
function resolveRun(ref) {
|
|
629
646
|
const ids = listRunIds();
|
|
@@ -649,13 +666,16 @@ function isAlive(pid) {
|
|
|
649
666
|
return e.code === "EPERM";
|
|
650
667
|
}
|
|
651
668
|
}
|
|
669
|
+
var STOP_FILE = "stop-requested";
|
|
670
|
+
var requestStop = (id) => fs5.writeFileSync(runFile(id, STOP_FILE), "");
|
|
652
671
|
function freshMeta(id) {
|
|
653
672
|
const meta = readMeta(id);
|
|
654
673
|
if (!TERMINAL.includes(meta.state) && meta.pid && !isAlive(meta.pid)) {
|
|
655
674
|
const latest = readMeta(id);
|
|
656
675
|
if (TERMINAL.includes(latest.state)) return latest;
|
|
657
|
-
|
|
658
|
-
|
|
676
|
+
const stopped = latest.stopRequested || fs5.existsSync(runFile(id, STOP_FILE));
|
|
677
|
+
latest.state = stopped ? "stopped" : "failed";
|
|
678
|
+
if (!stopped) latest.error ??= "worker process exited unexpectedly";
|
|
659
679
|
latest.endedAt ??= (/* @__PURE__ */ new Date()).toISOString();
|
|
660
680
|
writeMeta(latest);
|
|
661
681
|
return latest;
|
|
@@ -787,22 +807,22 @@ var policyDir = () => path5.resolve(path5.dirname(fileURLToPath(import.meta.url)
|
|
|
787
807
|
function workerHome() {
|
|
788
808
|
const root = path5.join(home(), "gemini-home");
|
|
789
809
|
const dir = path5.join(root, ".gemini");
|
|
790
|
-
const base2 = JSON.parse(
|
|
810
|
+
const base2 = JSON.parse(fs6.readFileSync(path5.join(policyDir(), "worker-settings.json"), "utf8"));
|
|
791
811
|
const auth = settings().security?.auth;
|
|
792
812
|
const body = `${JSON.stringify(auth ? { ...base2, security: { ...base2.security, auth } } : base2, null, 2)}
|
|
793
813
|
`;
|
|
794
814
|
const file2 = path5.join(dir, "settings.json");
|
|
795
815
|
try {
|
|
796
|
-
if (
|
|
816
|
+
if (fs6.readFileSync(file2, "utf8") === body) return root;
|
|
797
817
|
} catch {
|
|
798
818
|
}
|
|
799
|
-
|
|
800
|
-
|
|
819
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
820
|
+
fs6.writeFileSync(file2, body);
|
|
801
821
|
for (const name of ["oauth_creds.json", "google_accounts.json"]) {
|
|
802
822
|
const from = path5.join(geminiHome(), name);
|
|
803
823
|
const to = path5.join(dir, name);
|
|
804
824
|
try {
|
|
805
|
-
if (
|
|
825
|
+
if (fs6.existsSync(from) && !fs6.existsSync(to)) fs6.symlinkSync(from, to);
|
|
806
826
|
} catch {
|
|
807
827
|
}
|
|
808
828
|
}
|
|
@@ -829,7 +849,7 @@ function invocation3(req) {
|
|
|
829
849
|
const { command, prefix } = resolveCommand(binary3());
|
|
830
850
|
const env = { GEMINI_CLI_HOME: workerHome() };
|
|
831
851
|
const trusted = path5.join(geminiHome(), "trustedFolders.json");
|
|
832
|
-
if (
|
|
852
|
+
if (fs6.existsSync(trusted)) env.GEMINI_CLI_TRUSTED_FOLDERS_PATH = trusted;
|
|
833
853
|
return { command, args: [...prefix, ...args], env };
|
|
834
854
|
}
|
|
835
855
|
function classify3(message) {
|
|
@@ -854,7 +874,7 @@ function geminiHome() {
|
|
|
854
874
|
}
|
|
855
875
|
function settings() {
|
|
856
876
|
try {
|
|
857
|
-
return JSON.parse(
|
|
877
|
+
return JSON.parse(fs6.readFileSync(path5.join(geminiHome(), "settings.json"), "utf8"));
|
|
858
878
|
} catch {
|
|
859
879
|
return {};
|
|
860
880
|
}
|
|
@@ -873,7 +893,7 @@ function doctor3({ models, hasFallback }) {
|
|
|
873
893
|
const checks = [{ level: "ok", message: `Gemini CLI ${version.out.trim()} at ${binary3()}` }];
|
|
874
894
|
const key = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
|
875
895
|
const vertex = process.env.GOOGLE_GENAI_USE_VERTEXAI === "true" || process.env.GOOGLE_GENAI_USE_VERTEXAI === "1";
|
|
876
|
-
const signedIn =
|
|
896
|
+
const signedIn = fs6.existsSync(path5.join(geminiHome(), "oauth_creds.json"));
|
|
877
897
|
const type = settings().security?.auth?.selectedType;
|
|
878
898
|
if (key || vertex) {
|
|
879
899
|
checks.push({ level: "ok", message: `Gemini CLI: ${key ? "an API key is set" : "Vertex AI is set up"}` });
|
|
@@ -1032,7 +1052,7 @@ function describe2(input) {
|
|
|
1032
1052
|
}
|
|
1033
1053
|
|
|
1034
1054
|
// src/backends/opencode/profiles.ts
|
|
1035
|
-
import
|
|
1055
|
+
import fs7 from "node:fs";
|
|
1036
1056
|
import os6 from "node:os";
|
|
1037
1057
|
import path6 from "node:path";
|
|
1038
1058
|
var AGENT = { read: "pitroom-read", write: "pitroom-write" };
|
|
@@ -1143,7 +1163,7 @@ function scratchDirs() {
|
|
|
1143
1163
|
}
|
|
1144
1164
|
function realpath(p) {
|
|
1145
1165
|
try {
|
|
1146
|
-
return
|
|
1166
|
+
return fs7.realpathSync(p);
|
|
1147
1167
|
} catch {
|
|
1148
1168
|
return p;
|
|
1149
1169
|
}
|
|
@@ -1401,10 +1421,10 @@ function getBackend(id) {
|
|
|
1401
1421
|
}
|
|
1402
1422
|
|
|
1403
1423
|
// src/cli/args.ts
|
|
1404
|
-
import
|
|
1424
|
+
import fs9 from "node:fs";
|
|
1405
1425
|
|
|
1406
1426
|
// src/core/config.ts
|
|
1407
|
-
import
|
|
1427
|
+
import fs8 from "node:fs";
|
|
1408
1428
|
import os7 from "node:os";
|
|
1409
1429
|
import path7 from "node:path";
|
|
1410
1430
|
var SCHEMA = {
|
|
@@ -1420,7 +1440,8 @@ var SCHEMA = {
|
|
|
1420
1440
|
tiers: "record",
|
|
1421
1441
|
costs: "numbers",
|
|
1422
1442
|
audit: "number",
|
|
1423
|
-
readIn: "string"
|
|
1443
|
+
readIn: "string",
|
|
1444
|
+
cacheDays: "number"
|
|
1424
1445
|
};
|
|
1425
1446
|
function configPath() {
|
|
1426
1447
|
if (process.env.PITROOM_CONFIG) return path7.resolve(process.env.PITROOM_CONFIG);
|
|
@@ -1433,10 +1454,10 @@ function loadConfig() {
|
|
|
1433
1454
|
const file2 = configPath();
|
|
1434
1455
|
const config = {};
|
|
1435
1456
|
const warnings = [];
|
|
1436
|
-
if (
|
|
1457
|
+
if (fs8.existsSync(file2)) {
|
|
1437
1458
|
let raw;
|
|
1438
1459
|
try {
|
|
1439
|
-
raw = JSON.parse(
|
|
1460
|
+
raw = JSON.parse(fs8.readFileSync(file2, "utf8"));
|
|
1440
1461
|
} catch (e) {
|
|
1441
1462
|
warnings.push(`${file2}: invalid JSON (${e.message}); ignored`);
|
|
1442
1463
|
}
|
|
@@ -1478,6 +1499,10 @@ var rate = (v) => {
|
|
|
1478
1499
|
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
1479
1500
|
return typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.min(n, 1) : void 0;
|
|
1480
1501
|
};
|
|
1502
|
+
var days = (v) => {
|
|
1503
|
+
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
1504
|
+
return typeof n === "number" && Number.isInteger(n) && n >= 0 ? n : void 0;
|
|
1505
|
+
};
|
|
1481
1506
|
var readIn = (v) => v === "auto" || v === "snapshot" || v === "project" ? v : void 0;
|
|
1482
1507
|
var list = (s) => s?.split(",").map((x) => x.trim()).filter(Boolean);
|
|
1483
1508
|
function setting(flag2, env, conf, fallback) {
|
|
@@ -1504,7 +1529,8 @@ function effective(flags = {}) {
|
|
|
1504
1529
|
tiers: setting(void 0, void 0, c.tiers, {}),
|
|
1505
1530
|
costs: setting(void 0, void 0, c.costs, {}),
|
|
1506
1531
|
audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0),
|
|
1507
|
-
readIn: setting(void 0, readIn(e.PITROOM_READ_IN), readIn(c.readIn), "auto")
|
|
1532
|
+
readIn: setting(void 0, readIn(e.PITROOM_READ_IN), readIn(c.readIn), "auto"),
|
|
1533
|
+
cacheDays: setting(void 0, days(e.PITROOM_CACHE_DAYS), days(c.cacheDays), 7)
|
|
1508
1534
|
};
|
|
1509
1535
|
}
|
|
1510
1536
|
|
|
@@ -1581,6 +1607,7 @@ var BOOL_FLAGS = {
|
|
|
1581
1607
|
"--stop": "stop",
|
|
1582
1608
|
"--open": "open",
|
|
1583
1609
|
"--serve": "serve",
|
|
1610
|
+
"--fresh": "fresh",
|
|
1584
1611
|
"--http": "http",
|
|
1585
1612
|
"-h": "help",
|
|
1586
1613
|
"--help": "help",
|
|
@@ -1637,9 +1664,9 @@ function exitCodeFor(meta) {
|
|
|
1637
1664
|
}
|
|
1638
1665
|
function readTask(p) {
|
|
1639
1666
|
const file2 = flag(p, "task-file");
|
|
1640
|
-
if (file2) return
|
|
1667
|
+
if (file2) return fs9.readFileSync(file2, "utf8");
|
|
1641
1668
|
const words = p.positional;
|
|
1642
|
-
if (words.length === 1 && words[0] === "-") return
|
|
1669
|
+
if (words.length === 1 && words[0] === "-") return fs9.readFileSync(0, "utf8");
|
|
1643
1670
|
return words.join(" ");
|
|
1644
1671
|
}
|
|
1645
1672
|
function runOptions(p, task) {
|
|
@@ -1688,13 +1715,13 @@ import readline from "node:readline";
|
|
|
1688
1715
|
|
|
1689
1716
|
// src/core/run.ts
|
|
1690
1717
|
import { spawn as spawn2, spawnSync as spawnSync6 } from "node:child_process";
|
|
1691
|
-
import
|
|
1718
|
+
import fs25 from "node:fs";
|
|
1692
1719
|
import path20 from "node:path";
|
|
1693
1720
|
|
|
1694
1721
|
// src/vcs/git.ts
|
|
1695
1722
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
1696
1723
|
import crypto2 from "node:crypto";
|
|
1697
|
-
import
|
|
1724
|
+
import fs10 from "node:fs";
|
|
1698
1725
|
import os8 from "node:os";
|
|
1699
1726
|
import path8 from "node:path";
|
|
1700
1727
|
var SAFE = [
|
|
@@ -1726,26 +1753,33 @@ function must(cwd, args, env) {
|
|
|
1726
1753
|
function gitAvailable() {
|
|
1727
1754
|
return git(process.cwd(), ["--version"]).code === 0;
|
|
1728
1755
|
}
|
|
1756
|
+
function canonical(dir) {
|
|
1757
|
+
try {
|
|
1758
|
+
return fs10.realpathSync.native(dir);
|
|
1759
|
+
} catch {
|
|
1760
|
+
return dir;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1729
1763
|
function repoRoot(dir) {
|
|
1730
1764
|
const r = git(dir, ["rev-parse", "--show-toplevel"]);
|
|
1731
|
-
return r.code === 0 ? path8.resolve(r.stdout.trim()) : void 0;
|
|
1765
|
+
return r.code === 0 ? canonical(path8.resolve(r.stdout.trim())) : void 0;
|
|
1732
1766
|
}
|
|
1733
|
-
function snapshotTree(root, exclude = [],
|
|
1767
|
+
function snapshotTree(root, exclude = [], drop2 = []) {
|
|
1734
1768
|
const tmp = path8.join(os8.tmpdir(), `pitroom-index-${process.pid}-${crypto2.randomBytes(4).toString("hex")}`);
|
|
1735
1769
|
const real = path8.resolve(root, must(root, ["rev-parse", "--git-path", "index"]).trim());
|
|
1736
1770
|
const env = { ...process.env, GIT_INDEX_FILE: tmp };
|
|
1737
1771
|
try {
|
|
1738
|
-
if (
|
|
1772
|
+
if (fs10.existsSync(real)) fs10.copyFileSync(real, tmp);
|
|
1739
1773
|
else must(root, ["read-tree", "--empty"], env);
|
|
1740
|
-
must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`), ...
|
|
1741
|
-
if (
|
|
1742
|
-
const r = spawnSync5("git", ["update-index", "--force-remove", "-z", "--stdin"], { cwd: root, env, input: `${
|
|
1774
|
+
must(root, ["add", "-A", "--", ":/", ...exclude.map((p) => `:(top,exclude)${p}`), ...drop2.map((p) => `:(top,literal,exclude)${p}`)], env);
|
|
1775
|
+
if (drop2.length) {
|
|
1776
|
+
const r = spawnSync5("git", ["update-index", "--force-remove", "-z", "--stdin"], { cwd: root, env, input: `${drop2.join("\0")}\0`, encoding: "utf8" });
|
|
1743
1777
|
if (r.status !== 0) throw new Error(`git update-index failed: ${r.stderr.trim()}`);
|
|
1744
1778
|
}
|
|
1745
1779
|
return must(root, ["write-tree"], env).trim();
|
|
1746
1780
|
} finally {
|
|
1747
|
-
|
|
1748
|
-
|
|
1781
|
+
fs10.rmSync(tmp, { force: true });
|
|
1782
|
+
fs10.rmSync(`${tmp}.lock`, { force: true });
|
|
1749
1783
|
}
|
|
1750
1784
|
}
|
|
1751
1785
|
var DIFF = ["diff", "--no-ext-diff", "--no-textconv", "--no-renames"];
|
|
@@ -1766,9 +1800,9 @@ function diffTrees(root, a, b) {
|
|
|
1766
1800
|
}
|
|
1767
1801
|
function createIsolatedCopy(root, tree, dest) {
|
|
1768
1802
|
const objects = path8.join(path8.resolve(root, must(root, ["rev-parse", "--git-common-dir"]).trim()), "objects");
|
|
1769
|
-
|
|
1803
|
+
fs10.mkdirSync(path8.dirname(dest), { recursive: true });
|
|
1770
1804
|
must(path8.dirname(dest), ["init", "--quiet", dest]);
|
|
1771
|
-
|
|
1805
|
+
fs10.writeFileSync(path8.join(dest, ".git", "objects", "info", "alternates"), `${objects}
|
|
1772
1806
|
`);
|
|
1773
1807
|
must(dest, ["read-tree", tree]);
|
|
1774
1808
|
must(dest, ["checkout-index", "--all", "--force"]);
|
|
@@ -1776,16 +1810,16 @@ function createIsolatedCopy(root, tree, dest) {
|
|
|
1776
1810
|
function removeIsolatedCopy(dest, ownedBy) {
|
|
1777
1811
|
const rel = path8.relative(ownedBy, dest);
|
|
1778
1812
|
if (!rel || rel.startsWith("..") || path8.isAbsolute(rel)) throw new Error(`refusing to remove ${dest}`);
|
|
1779
|
-
|
|
1813
|
+
fs10.rmSync(dest, { recursive: true, force: true });
|
|
1780
1814
|
}
|
|
1781
1815
|
function linkIntoWorktree(root, worktree, rels) {
|
|
1782
1816
|
const linked = [];
|
|
1783
1817
|
for (const rel of rels) {
|
|
1784
1818
|
const src = path8.join(root, rel);
|
|
1785
1819
|
const dst = path8.join(worktree, rel);
|
|
1786
|
-
if (!
|
|
1787
|
-
|
|
1788
|
-
|
|
1820
|
+
if (!fs10.existsSync(src) || fs10.existsSync(dst)) continue;
|
|
1821
|
+
fs10.mkdirSync(path8.dirname(dst), { recursive: true });
|
|
1822
|
+
fs10.symlinkSync(src, dst, process.platform === "win32" ? "junction" : void 0);
|
|
1789
1823
|
linked.push(rel);
|
|
1790
1824
|
}
|
|
1791
1825
|
return linked;
|
|
@@ -1804,6 +1838,7 @@ ${must(root, [...DIFF, "-U10", a, b])}`;
|
|
|
1804
1838
|
}
|
|
1805
1839
|
function rangeDiff(root, a, b) {
|
|
1806
1840
|
const range = `${a}..${b}`;
|
|
1841
|
+
const since2 = git(root, ["merge-base", a, b]).code === 0 ? `${a}...${b}` : range;
|
|
1807
1842
|
const log2 = must(root, ["log", "--oneline", "--no-decorate", range]).trim();
|
|
1808
1843
|
return [
|
|
1809
1844
|
`## COMMITS
|
|
@@ -1811,10 +1846,10 @@ function rangeDiff(root, a, b) {
|
|
|
1811
1846
|
${log2 || "(none)"}`,
|
|
1812
1847
|
`## FILES CHANGED
|
|
1813
1848
|
|
|
1814
|
-
${must(root, [...DIFF, "--stat",
|
|
1849
|
+
${must(root, [...DIFF, "--stat", since2]).trim() || "(none)"}`,
|
|
1815
1850
|
`## DIFF
|
|
1816
1851
|
|
|
1817
|
-
${must(root, [...DIFF, "-U10",
|
|
1852
|
+
${must(root, [...DIFF, "-U10", since2])}`
|
|
1818
1853
|
].join("\n\n");
|
|
1819
1854
|
}
|
|
1820
1855
|
function commitOf(root, ref) {
|
|
@@ -1890,10 +1925,10 @@ function resolveChain(flags = {}) {
|
|
|
1890
1925
|
|
|
1891
1926
|
// src/core/process.ts
|
|
1892
1927
|
import { spawn } from "node:child_process";
|
|
1893
|
-
import
|
|
1928
|
+
import fs12 from "node:fs";
|
|
1894
1929
|
|
|
1895
1930
|
// src/vcs/guard.ts
|
|
1896
|
-
import
|
|
1931
|
+
import fs11 from "node:fs";
|
|
1897
1932
|
import path9 from "node:path";
|
|
1898
1933
|
var VERSION = 1;
|
|
1899
1934
|
var SHIM = `#!/bin/sh
|
|
@@ -1976,8 +2011,8 @@ function findRealGit(skip) {
|
|
|
1976
2011
|
if (path9.resolve(dir) === skip) continue;
|
|
1977
2012
|
const p = path9.join(dir, "git");
|
|
1978
2013
|
try {
|
|
1979
|
-
|
|
1980
|
-
if (
|
|
2014
|
+
fs11.accessSync(p, fs11.constants.X_OK);
|
|
2015
|
+
if (fs11.statSync(p).isFile()) return p;
|
|
1981
2016
|
} catch {
|
|
1982
2017
|
}
|
|
1983
2018
|
}
|
|
@@ -1992,11 +2027,11 @@ exit 1
|
|
|
1992
2027
|
`;
|
|
1993
2028
|
var hooksDir = () => path9.join(home(), "git-hooks", `v${HOOK_VERSION}`);
|
|
1994
2029
|
function writeIfChanged(file2, content) {
|
|
1995
|
-
if (
|
|
1996
|
-
|
|
2030
|
+
if (fs11.existsSync(file2) && fs11.readFileSync(file2, "utf8") === content) return;
|
|
2031
|
+
fs11.mkdirSync(path9.dirname(file2), { recursive: true });
|
|
1997
2032
|
const tmp = `${file2}.${process.pid}.tmp`;
|
|
1998
|
-
|
|
1999
|
-
|
|
2033
|
+
fs11.writeFileSync(tmp, content, { mode: 493 });
|
|
2034
|
+
fs11.renameSync(tmp, file2);
|
|
2000
2035
|
}
|
|
2001
2036
|
function withGitConfig(env, entries) {
|
|
2002
2037
|
const out = { ...env };
|
|
@@ -2032,8 +2067,8 @@ function guardEnv(env) {
|
|
|
2032
2067
|
|
|
2033
2068
|
// src/core/process.ts
|
|
2034
2069
|
async function spawnWorker(inv, opts) {
|
|
2035
|
-
const out =
|
|
2036
|
-
const err =
|
|
2070
|
+
const out = fs12.openSync(opts.stdoutFile, "w");
|
|
2071
|
+
const err = fs12.openSync(opts.stderrFile, "w");
|
|
2037
2072
|
const res = { code: null, timedOut: false, stopped: false };
|
|
2038
2073
|
const child = spawn(inv.command, inv.args, {
|
|
2039
2074
|
cwd: opts.cwd,
|
|
@@ -2049,24 +2084,24 @@ async function spawnWorker(inv, opts) {
|
|
|
2049
2084
|
process.once("SIGINT", stop);
|
|
2050
2085
|
process.once("SIGTERM", stop);
|
|
2051
2086
|
let killer;
|
|
2052
|
-
const
|
|
2087
|
+
const timer2 = setTimeout(() => {
|
|
2053
2088
|
res.timedOut = true;
|
|
2054
2089
|
child.kill("SIGTERM");
|
|
2055
2090
|
killer = setTimeout(() => child.kill("SIGKILL"), 1e4);
|
|
2056
2091
|
}, opts.timeoutSec * 1e3);
|
|
2057
2092
|
res.code = await new Promise((resolve2) => {
|
|
2058
2093
|
child.on("error", (e) => {
|
|
2059
|
-
res.spawnError = e.code === "ENOENT" ? `${inv.command} not found` : e.message;
|
|
2094
|
+
res.spawnError = e.code === "ENOENT" ? fs12.existsSync(opts.cwd) ? `${inv.command} not found` : `the working directory ${opts.cwd} does not exist` : e.message;
|
|
2060
2095
|
resolve2(127);
|
|
2061
2096
|
});
|
|
2062
2097
|
child.on("close", (c) => resolve2(c));
|
|
2063
2098
|
});
|
|
2064
|
-
clearTimeout(
|
|
2099
|
+
clearTimeout(timer2);
|
|
2065
2100
|
if (killer) clearTimeout(killer);
|
|
2066
2101
|
process.off("SIGINT", stop);
|
|
2067
2102
|
process.off("SIGTERM", stop);
|
|
2068
|
-
|
|
2069
|
-
|
|
2103
|
+
fs12.closeSync(out);
|
|
2104
|
+
fs12.closeSync(err);
|
|
2070
2105
|
return res;
|
|
2071
2106
|
}
|
|
2072
2107
|
|
|
@@ -2098,7 +2133,7 @@ function parseAudit(text) {
|
|
|
2098
2133
|
}
|
|
2099
2134
|
|
|
2100
2135
|
// src/core/cooldown.ts
|
|
2101
|
-
import
|
|
2136
|
+
import fs13 from "node:fs";
|
|
2102
2137
|
import path10 from "node:path";
|
|
2103
2138
|
var MAX_MS = 24 * 36e5;
|
|
2104
2139
|
var OVERLOAD_MS = 20 * 6e4;
|
|
@@ -2126,18 +2161,18 @@ function cooldownMs(message) {
|
|
|
2126
2161
|
}
|
|
2127
2162
|
function read() {
|
|
2128
2163
|
try {
|
|
2129
|
-
const raw = JSON.parse(
|
|
2164
|
+
const raw = JSON.parse(fs13.readFileSync(file(), "utf8"));
|
|
2130
2165
|
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
2131
2166
|
} catch {
|
|
2132
2167
|
return {};
|
|
2133
2168
|
}
|
|
2134
2169
|
}
|
|
2135
2170
|
function write(all) {
|
|
2136
|
-
|
|
2171
|
+
fs13.mkdirSync(path10.dirname(file()), { recursive: true });
|
|
2137
2172
|
const tmp = `${file()}.${process.pid}.tmp`;
|
|
2138
|
-
|
|
2173
|
+
fs13.writeFileSync(tmp, `${JSON.stringify(all, null, 2)}
|
|
2139
2174
|
`);
|
|
2140
|
-
|
|
2175
|
+
renameOver(tmp, file());
|
|
2141
2176
|
}
|
|
2142
2177
|
function activeCooldowns(now = Date.now()) {
|
|
2143
2178
|
return Object.fromEntries(Object.entries(read()).filter(([, c]) => Date.parse(c.until) > now));
|
|
@@ -2165,13 +2200,13 @@ function untilText(c, now = Date.now()) {
|
|
|
2165
2200
|
}
|
|
2166
2201
|
|
|
2167
2202
|
// src/core/snapshot.ts
|
|
2168
|
-
import
|
|
2203
|
+
import fs15 from "node:fs";
|
|
2169
2204
|
import path12 from "node:path";
|
|
2170
2205
|
import crypto3 from "node:crypto";
|
|
2171
2206
|
|
|
2172
2207
|
// src/core/secrets.ts
|
|
2173
2208
|
import { execFileSync } from "node:child_process";
|
|
2174
|
-
import
|
|
2209
|
+
import fs14 from "node:fs";
|
|
2175
2210
|
import path11 from "node:path";
|
|
2176
2211
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
2177
2212
|
var MAX_VISITED = 2e4;
|
|
@@ -2189,7 +2224,7 @@ function findSecretFiles(dir) {
|
|
|
2189
2224
|
if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
|
|
2190
2225
|
let entries;
|
|
2191
2226
|
try {
|
|
2192
|
-
entries =
|
|
2227
|
+
entries = fs14.readdirSync(d, { withFileTypes: true });
|
|
2193
2228
|
} catch {
|
|
2194
2229
|
return;
|
|
2195
2230
|
}
|
|
@@ -2198,7 +2233,7 @@ function findSecretFiles(dir) {
|
|
|
2198
2233
|
if (e.isDirectory()) {
|
|
2199
2234
|
if (!SKIP_DIRS.has(e.name)) walk(path11.join(d, e.name), depth + 1);
|
|
2200
2235
|
} else if (e.isFile() && looksSecret(e.name)) {
|
|
2201
|
-
found.push(path11.relative(dir, path11.join(d, e.name)));
|
|
2236
|
+
found.push(slash(path11.relative(dir, path11.join(d, e.name))));
|
|
2202
2237
|
}
|
|
2203
2238
|
}
|
|
2204
2239
|
};
|
|
@@ -2208,7 +2243,7 @@ function findSecretFiles(dir) {
|
|
|
2208
2243
|
function findSecretFilesInTree(root, dir) {
|
|
2209
2244
|
try {
|
|
2210
2245
|
const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
2211
|
-
return out.split("\0").filter((f) => f && looksSecret(path11.basename(f)) && !path11.relative(dir, path11.join(root, f)).startsWith("..")).map((f) => path11.relative(dir, path11.join(root, f))).sort();
|
|
2246
|
+
return out.split("\0").filter((f) => f && looksSecret(path11.basename(f)) && !path11.relative(dir, path11.join(root, f)).startsWith("..")).map((f) => slash(path11.relative(dir, path11.join(root, f)))).sort();
|
|
2212
2247
|
} catch {
|
|
2213
2248
|
return [];
|
|
2214
2249
|
}
|
|
@@ -2220,6 +2255,7 @@ function secretWarning(files, mode) {
|
|
|
2220
2255
|
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";
|
|
2221
2256
|
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`;
|
|
2222
2257
|
}
|
|
2258
|
+
var slash = (p) => p.split(path11.sep).join("/");
|
|
2223
2259
|
|
|
2224
2260
|
// src/core/snapshot.ts
|
|
2225
2261
|
var READ_IN = ["auto", "snapshot", "project"];
|
|
@@ -2234,10 +2270,10 @@ function wantSnapshot(readIn2, root, dir) {
|
|
|
2234
2270
|
return readIn2 === "snapshot" || secrets.length ? { secrets } : void 0;
|
|
2235
2271
|
}
|
|
2236
2272
|
function readSnapshot(root) {
|
|
2237
|
-
const
|
|
2238
|
-
const tree = snapshotTree(root, [],
|
|
2273
|
+
const drop2 = findSecretFilesInTree(root, root);
|
|
2274
|
+
const tree = snapshotTree(root, [], drop2);
|
|
2239
2275
|
const dest = path12.join(snapshotsDir(), tree);
|
|
2240
|
-
if (
|
|
2276
|
+
if (fs15.existsSync(path12.join(dest, ".git"))) {
|
|
2241
2277
|
touch(dest);
|
|
2242
2278
|
return { dir: dest, tree, created: false };
|
|
2243
2279
|
}
|
|
@@ -2246,9 +2282,9 @@ function readSnapshot(root) {
|
|
|
2246
2282
|
createIsolatedCopy(root, tree, tmp);
|
|
2247
2283
|
readOnly(tmp);
|
|
2248
2284
|
try {
|
|
2249
|
-
|
|
2285
|
+
fs15.renameSync(tmp, dest);
|
|
2250
2286
|
} catch (e) {
|
|
2251
|
-
if (!
|
|
2287
|
+
if (!fs15.existsSync(path12.join(dest, ".git"))) throw e;
|
|
2252
2288
|
removeIsolatedCopy(tmp, snapshotsDir());
|
|
2253
2289
|
}
|
|
2254
2290
|
} catch (e) {
|
|
@@ -2264,17 +2300,17 @@ function readSnapshot(root) {
|
|
|
2264
2300
|
function touch(dir) {
|
|
2265
2301
|
try {
|
|
2266
2302
|
const now = /* @__PURE__ */ new Date();
|
|
2267
|
-
|
|
2303
|
+
fs15.utimesSync(dir, now, now);
|
|
2268
2304
|
} catch {
|
|
2269
2305
|
}
|
|
2270
2306
|
}
|
|
2271
2307
|
function readOnly(dir) {
|
|
2272
2308
|
const walk = (d) => {
|
|
2273
|
-
for (const e of
|
|
2309
|
+
for (const e of fs15.readdirSync(d, { withFileTypes: true })) {
|
|
2274
2310
|
if (e.name === ".git") continue;
|
|
2275
2311
|
const p = path12.join(d, e.name);
|
|
2276
2312
|
if (e.isDirectory()) walk(p);
|
|
2277
|
-
else if (e.isFile())
|
|
2313
|
+
else if (e.isFile()) fs15.chmodSync(p, fs15.statSync(p).mode & ~146);
|
|
2278
2314
|
}
|
|
2279
2315
|
};
|
|
2280
2316
|
try {
|
|
@@ -2285,7 +2321,7 @@ function readOnly(dir) {
|
|
|
2285
2321
|
function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWEST) {
|
|
2286
2322
|
let names;
|
|
2287
2323
|
try {
|
|
2288
|
-
names =
|
|
2324
|
+
names = fs15.readdirSync(snapshotsDir());
|
|
2289
2325
|
} catch {
|
|
2290
2326
|
return 0;
|
|
2291
2327
|
}
|
|
@@ -2300,7 +2336,7 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
|
|
|
2300
2336
|
const entries = names.map((n) => {
|
|
2301
2337
|
const dir = path12.join(snapshotsDir(), n);
|
|
2302
2338
|
try {
|
|
2303
|
-
return { n, dir, age: now -
|
|
2339
|
+
return { n, dir, age: now - fs15.statSync(dir).mtimeMs };
|
|
2304
2340
|
} catch {
|
|
2305
2341
|
return void 0;
|
|
2306
2342
|
}
|
|
@@ -2324,10 +2360,10 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
|
|
|
2324
2360
|
}
|
|
2325
2361
|
function makeWritable(dir) {
|
|
2326
2362
|
const walk = (d) => {
|
|
2327
|
-
for (const e of
|
|
2363
|
+
for (const e of fs15.readdirSync(d, { withFileTypes: true })) {
|
|
2328
2364
|
const p = path12.join(d, e.name);
|
|
2329
2365
|
if (e.isDirectory()) walk(p);
|
|
2330
|
-
else if (e.isFile())
|
|
2366
|
+
else if (e.isFile()) fs15.chmodSync(p, fs15.statSync(p).mode | 128);
|
|
2331
2367
|
}
|
|
2332
2368
|
};
|
|
2333
2369
|
try {
|
|
@@ -2338,7 +2374,7 @@ function makeWritable(dir) {
|
|
|
2338
2374
|
function backToProject(text, snapshotDir, root) {
|
|
2339
2375
|
let real = snapshotDir;
|
|
2340
2376
|
try {
|
|
2341
|
-
real =
|
|
2377
|
+
real = fs15.realpathSync(snapshotDir);
|
|
2342
2378
|
} catch {
|
|
2343
2379
|
}
|
|
2344
2380
|
let out = text;
|
|
@@ -2377,6 +2413,9 @@ function auditTask(meta, answer) {
|
|
|
2377
2413
|
return [
|
|
2378
2414
|
"You are auditing another worker's answer to a read-only question about this project. Do not take the answer on trust:",
|
|
2379
2415
|
"check its key claims yourself against the files here (open the cited files and lines, search for what it says exists or is missing).",
|
|
2416
|
+
"Check the answer against the QUESTION, not only its own claims: every condition the question sets (a directory or scope,",
|
|
2417
|
+
'what to leave out, "all", "only", "exactly", a count) must hold. Valid references do not make an answer right:',
|
|
2418
|
+
"for a list, look for items that are missing and items that do not belong; for a count, count again yourself.",
|
|
2380
2419
|
"Do not edit anything and do not run anything that changes files.",
|
|
2381
2420
|
"",
|
|
2382
2421
|
"QUESTION:",
|
|
@@ -2391,7 +2430,7 @@ function auditTask(meta, answer) {
|
|
|
2391
2430
|
"DISPUTED:",
|
|
2392
2431
|
"- <the claim> \u2014 <what is actually true, with path:line>",
|
|
2393
2432
|
"",
|
|
2394
|
-
"AGREE: every key claim you checked holds. PARTIAL: some are wrong or
|
|
2433
|
+
"AGREE: every key claim you checked holds and the answer meets every condition of the question (no extra or missing items, the right count). PARTIAL: some are wrong, unverified, or the answer goes beyond or falls short of the question's scope. DISAGREE: the main conclusion is wrong.",
|
|
2395
2434
|
'Under DISPUTED list only claims you checked and found wrong or unsupported; write "- (none)" when there are none.'
|
|
2396
2435
|
].join("\n");
|
|
2397
2436
|
}
|
|
@@ -2416,7 +2455,7 @@ function auditBadge(meta) {
|
|
|
2416
2455
|
}
|
|
2417
2456
|
|
|
2418
2457
|
// src/core/plan.ts
|
|
2419
|
-
import
|
|
2458
|
+
import fs16 from "node:fs";
|
|
2420
2459
|
import path13 from "node:path";
|
|
2421
2460
|
var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
2422
2461
|
var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
|
|
@@ -2465,8 +2504,8 @@ function parsePlan(text, file2 = "") {
|
|
|
2465
2504
|
}
|
|
2466
2505
|
function loadPlan(file2) {
|
|
2467
2506
|
const abs = path13.resolve(file2);
|
|
2468
|
-
if (!
|
|
2469
|
-
const plan = parsePlan(
|
|
2507
|
+
if (!fs16.existsSync(abs) || !fs16.statSync(abs).isFile()) throw new UserError(`plan not found: ${file2}`);
|
|
2508
|
+
const plan = parsePlan(fs16.readFileSync(abs, "utf8"), fs16.realpathSync(abs));
|
|
2470
2509
|
if (!plan.tasks.length) throw new UserError(`${file2} has no "Task N" headings (see pitroom-writing-plans)`);
|
|
2471
2510
|
return plan;
|
|
2472
2511
|
}
|
|
@@ -2488,11 +2527,11 @@ function brief(plan, task) {
|
|
|
2488
2527
|
var planName = (file2) => path13.basename(file2).replace(/\.md$/i, "");
|
|
2489
2528
|
|
|
2490
2529
|
// src/core/templates.ts
|
|
2491
|
-
import
|
|
2530
|
+
import fs18 from "node:fs";
|
|
2492
2531
|
import path15 from "node:path";
|
|
2493
2532
|
|
|
2494
2533
|
// src/core/install.ts
|
|
2495
|
-
import
|
|
2534
|
+
import fs17 from "node:fs";
|
|
2496
2535
|
import os9 from "node:os";
|
|
2497
2536
|
import path14 from "node:path";
|
|
2498
2537
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
@@ -2502,18 +2541,30 @@ function packageRoot() {
|
|
|
2502
2541
|
}
|
|
2503
2542
|
function skillNames(root = packageRoot()) {
|
|
2504
2543
|
const dir = path14.join(root, "skills");
|
|
2505
|
-
if (!
|
|
2506
|
-
return
|
|
2544
|
+
if (!fs17.existsSync(dir)) return [];
|
|
2545
|
+
return fs17.readdirSync(dir).filter((d) => fs17.existsSync(path14.join(dir, d, "SKILL.md"))).sort();
|
|
2507
2546
|
}
|
|
2508
2547
|
function skillTargets() {
|
|
2509
2548
|
const home3 = os9.homedir();
|
|
2510
2549
|
const targets = [path14.join(home3, ".agents", "skills")];
|
|
2511
|
-
if (
|
|
2550
|
+
if (fs17.existsSync(path14.join(home3, ".claude"))) targets.push(path14.join(home3, ".claude", "skills"));
|
|
2512
2551
|
return targets;
|
|
2513
2552
|
}
|
|
2514
|
-
var launcherPath = () => path14.join(os9.homedir(), ".local", "bin", "pitroom");
|
|
2553
|
+
var launcherPath = () => path14.join(os9.homedir(), ".local", "bin", process.platform === "win32" ? "pitroom.cmd" : "pitroom");
|
|
2515
2554
|
var LAUNCHER_MARK = "# pitroom launcher";
|
|
2516
2555
|
function launcherScript(bundle) {
|
|
2556
|
+
if (process.platform === "win32") {
|
|
2557
|
+
const q2 = (v) => v.replace(/%/g, "%%");
|
|
2558
|
+
return `@echo off\r
|
|
2559
|
+
rem ${LAUNCHER_MARK} (created by \`pitroom install\`; \`pitroom uninstall\` removes it)\r
|
|
2560
|
+
if defined PITROOM_NODE (\r
|
|
2561
|
+
"%PITROOM_NODE%" "${q2(bundle)}" %*\r
|
|
2562
|
+
) else (\r
|
|
2563
|
+
"${q2(process.execPath)}" "${q2(bundle)}" %*\r
|
|
2564
|
+
)\r
|
|
2565
|
+
exit /b %ERRORLEVEL%\r
|
|
2566
|
+
`;
|
|
2567
|
+
}
|
|
2517
2568
|
const q = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
2518
2569
|
return `#!/bin/sh
|
|
2519
2570
|
${LAUNCHER_MARK} (created by \`pitroom install\`; \`pitroom uninstall\` removes it)
|
|
@@ -2529,7 +2580,7 @@ exit 127
|
|
|
2529
2580
|
function isOurLauncher(file2, root) {
|
|
2530
2581
|
if (linksInto(file2, root)) return true;
|
|
2531
2582
|
try {
|
|
2532
|
-
const s =
|
|
2583
|
+
const s = fs17.readFileSync(file2, "utf8");
|
|
2533
2584
|
return s.includes(LAUNCHER_MARK) && s.includes(root);
|
|
2534
2585
|
} catch {
|
|
2535
2586
|
return false;
|
|
@@ -2537,61 +2588,61 @@ function isOurLauncher(file2, root) {
|
|
|
2537
2588
|
}
|
|
2538
2589
|
function isPitroomLauncher(file2) {
|
|
2539
2590
|
try {
|
|
2540
|
-
return
|
|
2591
|
+
return fs17.readFileSync(file2, "utf8").includes(LAUNCHER_MARK);
|
|
2541
2592
|
} catch {
|
|
2542
2593
|
return false;
|
|
2543
2594
|
}
|
|
2544
2595
|
}
|
|
2545
2596
|
function placeLauncher(bundle, root, force) {
|
|
2546
2597
|
const dest = launcherPath();
|
|
2547
|
-
|
|
2548
|
-
const exists =
|
|
2598
|
+
fs17.mkdirSync(path14.dirname(dest), { recursive: true });
|
|
2599
|
+
const exists = fs17.lstatSync(dest, { throwIfNoEntry: false });
|
|
2549
2600
|
if (exists && !isOurLauncher(dest, root)) {
|
|
2550
2601
|
if (!force) return `! ${dest} exists and is not Pitroom's launcher; kept (use --force to back it up and replace)`;
|
|
2551
|
-
|
|
2602
|
+
fs17.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
2552
2603
|
} else if (exists) {
|
|
2553
|
-
|
|
2604
|
+
fs17.rmSync(dest, { force: true });
|
|
2554
2605
|
}
|
|
2555
|
-
|
|
2606
|
+
fs17.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
|
|
2556
2607
|
return `\u2714 ${dest} \u2192 launcher for ${bundle} (Node 22.13+)`;
|
|
2557
2608
|
}
|
|
2558
2609
|
function linksInto(link, dir) {
|
|
2559
|
-
const st =
|
|
2610
|
+
const st = fs17.lstatSync(link, { throwIfNoEntry: false });
|
|
2560
2611
|
if (!st?.isSymbolicLink()) return false;
|
|
2561
|
-
const target = path14.resolve(path14.dirname(link),
|
|
2612
|
+
const target = path14.resolve(path14.dirname(link), fs17.readlinkSync(link));
|
|
2562
2613
|
const rel = path14.relative(dir, target);
|
|
2563
2614
|
return !rel.startsWith("..") && !path14.isAbsolute(rel);
|
|
2564
2615
|
}
|
|
2565
2616
|
function place(src, dest, opts) {
|
|
2566
|
-
|
|
2567
|
-
const st =
|
|
2617
|
+
fs17.mkdirSync(path14.dirname(dest), { recursive: true });
|
|
2618
|
+
const st = fs17.lstatSync(dest, { throwIfNoEntry: false });
|
|
2568
2619
|
if (st && !st.isSymbolicLink()) {
|
|
2569
2620
|
if (!opts.force) return `! ${dest} exists and is not a link; kept (use --force to back it up and replace)`;
|
|
2570
|
-
|
|
2621
|
+
fs17.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
2571
2622
|
} else if (st) {
|
|
2572
|
-
|
|
2623
|
+
fs17.unlinkSync(dest);
|
|
2573
2624
|
}
|
|
2574
|
-
if (opts.copy)
|
|
2575
|
-
else
|
|
2625
|
+
if (opts.copy) fs17.cpSync(src, dest, { recursive: true });
|
|
2626
|
+
else fs17.symlinkSync(src, dest, process.platform === "win32" ? "junction" : fs17.statSync(src).isDirectory() ? "dir" : "file");
|
|
2576
2627
|
return `\u2714 ${dest} \u2192 ${opts.copy ? "copied" : src}`;
|
|
2577
2628
|
}
|
|
2578
2629
|
function install(opts) {
|
|
2579
2630
|
const root = packageRoot();
|
|
2580
|
-
const
|
|
2581
|
-
if (opts.skills !== false && !
|
|
2631
|
+
const skills2 = opts.skills === false ? [] : skillNames(root);
|
|
2632
|
+
if (opts.skills !== false && !skills2.length) throw new Error(`no skills found under ${path14.join(root, "skills")}`);
|
|
2582
2633
|
const out = [];
|
|
2583
2634
|
for (const base2 of opts.skills === false ? [] : skillTargets()) {
|
|
2584
2635
|
for (const legacy of LEGACY) {
|
|
2585
2636
|
const l = path14.join(base2, legacy);
|
|
2586
|
-
if (!
|
|
2587
|
-
|
|
2637
|
+
if (!skills2.includes(legacy) && linksInto(l, root)) {
|
|
2638
|
+
fs17.unlinkSync(l);
|
|
2588
2639
|
out.push(`\u2714 removed old link ${l}`);
|
|
2589
2640
|
}
|
|
2590
2641
|
}
|
|
2591
|
-
for (const name of
|
|
2642
|
+
for (const name of skills2) out.push(place(path14.join(root, "skills", name), path14.join(base2, name), opts));
|
|
2592
2643
|
}
|
|
2593
2644
|
const bundle = path14.join(root, "dist", "pitroom.mjs");
|
|
2594
|
-
if (
|
|
2645
|
+
if (fs17.existsSync(bundle)) {
|
|
2595
2646
|
out.push(placeLauncher(bundle, root, opts.force));
|
|
2596
2647
|
const onPath = (process.env.PATH ?? "").split(path14.delimiter).some((d) => path14.resolve(d) === path14.dirname(launcherPath()));
|
|
2597
2648
|
if (!onPath) out.push(`! ${path14.dirname(launcherPath())} is not on PATH; add it, or run ${launcherPath()} directly`);
|
|
@@ -2602,24 +2653,24 @@ function uninstall() {
|
|
|
2602
2653
|
const root = packageRoot();
|
|
2603
2654
|
const out = [];
|
|
2604
2655
|
for (const base2 of skillTargets()) {
|
|
2605
|
-
if (!
|
|
2606
|
-
for (const name of
|
|
2656
|
+
if (!fs17.existsSync(base2)) continue;
|
|
2657
|
+
for (const name of fs17.readdirSync(base2)) {
|
|
2607
2658
|
const l = path14.join(base2, name);
|
|
2608
2659
|
if (linksInto(l, root)) {
|
|
2609
|
-
|
|
2660
|
+
fs17.unlinkSync(l);
|
|
2610
2661
|
out.push(`\u2714 removed ${l}`);
|
|
2611
2662
|
}
|
|
2612
2663
|
}
|
|
2613
2664
|
}
|
|
2614
2665
|
if (isOurLauncher(launcherPath(), root)) {
|
|
2615
|
-
|
|
2666
|
+
fs17.rmSync(launcherPath(), { force: true });
|
|
2616
2667
|
out.push(`\u2714 removed ${launcherPath()}`);
|
|
2617
2668
|
}
|
|
2618
2669
|
return out.length ? out : ["nothing to remove"];
|
|
2619
2670
|
}
|
|
2620
2671
|
function installedSkills() {
|
|
2621
2672
|
const names = skillNames();
|
|
2622
|
-
return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) =>
|
|
2673
|
+
return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs17.existsSync(path14.join(base2, n, "SKILL.md"))) }));
|
|
2623
2674
|
}
|
|
2624
2675
|
|
|
2625
2676
|
// src/core/templates.ts
|
|
@@ -2631,8 +2682,8 @@ var FILES = {
|
|
|
2631
2682
|
};
|
|
2632
2683
|
function loadTemplate(name, root = packageRoot()) {
|
|
2633
2684
|
const file2 = path15.join(root, "skills", FILES[name]);
|
|
2634
|
-
if (!
|
|
2635
|
-
return
|
|
2685
|
+
if (!fs18.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
|
|
2686
|
+
return fs18.readFileSync(file2, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
|
|
2636
2687
|
}
|
|
2637
2688
|
function fill(template, values) {
|
|
2638
2689
|
const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
|
|
@@ -2642,7 +2693,7 @@ function fill(template, values) {
|
|
|
2642
2693
|
|
|
2643
2694
|
// src/core/slots.ts
|
|
2644
2695
|
import crypto5 from "node:crypto";
|
|
2645
|
-
import
|
|
2696
|
+
import fs19 from "node:fs";
|
|
2646
2697
|
import path16 from "node:path";
|
|
2647
2698
|
var slotsDir = () => path16.join(home(), "slots");
|
|
2648
2699
|
var locksDir = () => path16.join(home(), "locks");
|
|
@@ -2659,23 +2710,23 @@ function holderActive(runId, self) {
|
|
|
2659
2710
|
}
|
|
2660
2711
|
}
|
|
2661
2712
|
function tryClaim(file2, runId) {
|
|
2662
|
-
|
|
2713
|
+
fs19.mkdirSync(path16.dirname(file2), { recursive: true });
|
|
2663
2714
|
try {
|
|
2664
|
-
|
|
2715
|
+
fs19.writeFileSync(file2, runId, { flag: "wx" });
|
|
2665
2716
|
return true;
|
|
2666
2717
|
} catch (e) {
|
|
2667
2718
|
if (e.code !== "EEXIST") throw e;
|
|
2668
2719
|
}
|
|
2669
2720
|
let owner = "";
|
|
2670
2721
|
try {
|
|
2671
|
-
owner =
|
|
2722
|
+
owner = fs19.readFileSync(file2, "utf8").trim();
|
|
2672
2723
|
} catch {
|
|
2673
2724
|
}
|
|
2674
2725
|
if (owner === runId) return true;
|
|
2675
2726
|
if (holderActive(owner, runId)) return false;
|
|
2676
|
-
|
|
2727
|
+
fs19.rmSync(file2, { force: true });
|
|
2677
2728
|
try {
|
|
2678
|
-
|
|
2729
|
+
fs19.writeFileSync(file2, runId, { flag: "wx" });
|
|
2679
2730
|
return true;
|
|
2680
2731
|
} catch {
|
|
2681
2732
|
return false;
|
|
@@ -2684,7 +2735,7 @@ function tryClaim(file2, runId) {
|
|
|
2684
2735
|
function releaseIfOwner(file2, runId) {
|
|
2685
2736
|
if (!file2) return;
|
|
2686
2737
|
try {
|
|
2687
|
-
if (
|
|
2738
|
+
if (fs19.readFileSync(file2, "utf8").trim() === runId) fs19.rmSync(file2, { force: true });
|
|
2688
2739
|
} catch {
|
|
2689
2740
|
}
|
|
2690
2741
|
}
|
|
@@ -2702,7 +2753,7 @@ function acquireWriteLock(repoRoot2, runId) {
|
|
|
2702
2753
|
if (tryClaim(file2, runId)) return;
|
|
2703
2754
|
let owner = "";
|
|
2704
2755
|
try {
|
|
2705
|
-
owner =
|
|
2756
|
+
owner = fs19.readFileSync(file2, "utf8").trim();
|
|
2706
2757
|
} catch {
|
|
2707
2758
|
}
|
|
2708
2759
|
throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
|
|
@@ -2749,7 +2800,7 @@ ${task}`;
|
|
|
2749
2800
|
}
|
|
2750
2801
|
|
|
2751
2802
|
// src/core/receipt.ts
|
|
2752
|
-
import
|
|
2803
|
+
import fs20 from "node:fs";
|
|
2753
2804
|
import path17 from "node:path";
|
|
2754
2805
|
var PRESETS = {
|
|
2755
2806
|
sonnet: { name: "Claude Sonnet", input: 3, output: 15, cachedInput: 0.3 },
|
|
@@ -2786,13 +2837,13 @@ function record(meta) {
|
|
|
2786
2837
|
saved: meta.savedUsd ?? 0,
|
|
2787
2838
|
price: primaryPrice().name
|
|
2788
2839
|
};
|
|
2789
|
-
|
|
2790
|
-
|
|
2840
|
+
fs20.mkdirSync(path17.dirname(ledgerFile()), { recursive: true });
|
|
2841
|
+
fs20.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
|
|
2791
2842
|
`);
|
|
2792
2843
|
}
|
|
2793
2844
|
function readLedger(sinceMs2) {
|
|
2794
|
-
if (!
|
|
2795
|
-
return
|
|
2845
|
+
if (!fs20.existsSync(ledgerFile())) return [];
|
|
2846
|
+
return fs20.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
|
|
2796
2847
|
try {
|
|
2797
2848
|
return [JSON.parse(l)];
|
|
2798
2849
|
} catch {
|
|
@@ -2867,7 +2918,7 @@ ${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.rou
|
|
|
2867
2918
|
}
|
|
2868
2919
|
|
|
2869
2920
|
// src/core/refs.ts
|
|
2870
|
-
import
|
|
2921
|
+
import fs21 from "node:fs";
|
|
2871
2922
|
import path18 from "node:path";
|
|
2872
2923
|
var FILE_EXTENSIONS = new Set(
|
|
2873
2924
|
"ts tsx mts cts js jsx mjs cjs json jsonc json5 md mdx txt rst py pyi rb go rs java kt kts swift c h cc cpp cxx hpp hh cs fs php lua r jl sh bash zsh fish ps1 bat yml yaml toml ini cfg conf env html htm css scss sass less vue svelte astro sql graphql gql proto lock xml svg csv tsv gradle tf hcl dart ex exs erl hs ml scala clj vim el mk cmake dockerfile gitignore gitattributes editorconfig npmrc nvmrc".split(" ")
|
|
@@ -2924,8 +2975,8 @@ function verifyRefs(refs, dirs) {
|
|
|
2924
2975
|
const lineCache = /* @__PURE__ */ new Map();
|
|
2925
2976
|
const load = (file2) => {
|
|
2926
2977
|
if (!lineCache.has(file2)) {
|
|
2927
|
-
const size =
|
|
2928
|
-
const lines = size > MAX_BYTES ? null :
|
|
2978
|
+
const size = fs21.statSync(file2).size;
|
|
2979
|
+
const lines = size > MAX_BYTES ? null : fs21.readFileSync(file2, "utf8").split("\n");
|
|
2929
2980
|
if (lines && lines[lines.length - 1] === "") lines.pop();
|
|
2930
2981
|
lineCache.set(file2, lines);
|
|
2931
2982
|
}
|
|
@@ -2938,7 +2989,7 @@ function verifyRefs(refs, dirs) {
|
|
|
2938
2989
|
if (direct) return [direct];
|
|
2939
2990
|
if (path18.isAbsolute(ref.file)) return [];
|
|
2940
2991
|
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
2941
|
-
return (byName().get(path18.basename(wanted)) ?? []).filter((f) => f.endsWith(`/${wanted}`) || path18.basename(f) === wanted).slice(0, 20);
|
|
2992
|
+
return (byName().get(path18.basename(wanted)) ?? []).filter((f) => f.split(path18.sep).join("/").endsWith(`/${wanted}`) || path18.basename(f) === wanted).slice(0, 20);
|
|
2942
2993
|
};
|
|
2943
2994
|
const check = (file2, ref) => {
|
|
2944
2995
|
const lines = load(file2);
|
|
@@ -2983,7 +3034,7 @@ function indexFiles(roots) {
|
|
|
2983
3034
|
const walk = (dir) => {
|
|
2984
3035
|
let entries;
|
|
2985
3036
|
try {
|
|
2986
|
-
entries =
|
|
3037
|
+
entries = fs21.readdirSync(dir, { withFileTypes: true });
|
|
2987
3038
|
} catch {
|
|
2988
3039
|
return;
|
|
2989
3040
|
}
|
|
@@ -3006,7 +3057,7 @@ function resolve(ref, roots) {
|
|
|
3006
3057
|
const candidates = path18.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path18.join(r, ref));
|
|
3007
3058
|
return candidates.find((c) => {
|
|
3008
3059
|
try {
|
|
3009
|
-
return
|
|
3060
|
+
return fs21.statSync(c).isFile() && inside(c, roots);
|
|
3010
3061
|
} catch {
|
|
3011
3062
|
return false;
|
|
3012
3063
|
}
|
|
@@ -3021,13 +3072,13 @@ function inside(file2, roots) {
|
|
|
3021
3072
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3022
3073
|
|
|
3023
3074
|
// src/core/history.ts
|
|
3024
|
-
import
|
|
3075
|
+
import fs23 from "node:fs";
|
|
3025
3076
|
import { createRequire } from "node:module";
|
|
3026
3077
|
import path19 from "node:path";
|
|
3027
3078
|
import zlib from "node:zlib";
|
|
3028
3079
|
|
|
3029
3080
|
// src/core/ui.ts
|
|
3030
|
-
import
|
|
3081
|
+
import fs22 from "node:fs";
|
|
3031
3082
|
var WEEK_MS = 7 * 24 * 3600 * 1e3;
|
|
3032
3083
|
var RECENT = 40;
|
|
3033
3084
|
var RECENT_HOURS = 1;
|
|
@@ -3118,9 +3169,9 @@ function cardsFor(m) {
|
|
|
3118
3169
|
const out = [];
|
|
3119
3170
|
for (const [phase, text] of phases) {
|
|
3120
3171
|
const marker = runFile(m.id, `card-${phase}`);
|
|
3121
|
-
if (
|
|
3172
|
+
if (fs22.existsSync(marker)) continue;
|
|
3122
3173
|
if (!(phase === "started" && !isActive(m.state))) out.push(text());
|
|
3123
|
-
|
|
3174
|
+
fs22.writeFileSync(marker, "");
|
|
3124
3175
|
}
|
|
3125
3176
|
return out;
|
|
3126
3177
|
}
|
|
@@ -3131,10 +3182,10 @@ function hookCards(input) {
|
|
|
3131
3182
|
const response = event.tool_response;
|
|
3132
3183
|
const output = typeof response === "string" ? response : [response?.stdout, response?.stderr].filter(Boolean).join("\n");
|
|
3133
3184
|
const ids = [...new Set(`${command}
|
|
3134
|
-
${output}`.match(RUN_ID) ?? [])].filter((id) =>
|
|
3185
|
+
${output}`.match(RUN_ID) ?? [])].filter((id) => fs22.existsSync(`${runsDir()}/${id}`));
|
|
3135
3186
|
const since2 = Date.now() - RECENT_HOURS * 36e5;
|
|
3136
3187
|
for (const id of listRunIds().slice(-RECENT)) {
|
|
3137
|
-
if (ids.includes(id) ||
|
|
3188
|
+
if (ids.includes(id) || fs22.existsSync(runFile(id, "card-ended"))) continue;
|
|
3138
3189
|
try {
|
|
3139
3190
|
const m = readMeta(id);
|
|
3140
3191
|
if (!isActive(m.state) && Date.parse(m.endedAt ?? m.startedAt) > since2) ids.push(id);
|
|
@@ -3170,7 +3221,7 @@ function openDb() {
|
|
|
3170
3221
|
} finally {
|
|
3171
3222
|
process.emitWarning = emit;
|
|
3172
3223
|
}
|
|
3173
|
-
|
|
3224
|
+
fs23.mkdirSync(path19.dirname(file2), { recursive: true });
|
|
3174
3225
|
db = new DatabaseSync(file2);
|
|
3175
3226
|
db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
|
|
3176
3227
|
migrate(db);
|
|
@@ -3205,30 +3256,30 @@ function migrate(db) {
|
|
|
3205
3256
|
function readRunFile(id, name) {
|
|
3206
3257
|
const f = runFile(id, name);
|
|
3207
3258
|
try {
|
|
3208
|
-
if (
|
|
3209
|
-
if (
|
|
3259
|
+
if (fs23.existsSync(f)) return fs23.readFileSync(f, "utf8");
|
|
3260
|
+
if (fs23.existsSync(`${f}.gz`)) return zlib.gunzipSync(fs23.readFileSync(`${f}.gz`)).toString("utf8");
|
|
3210
3261
|
} catch {
|
|
3211
3262
|
}
|
|
3212
3263
|
return void 0;
|
|
3213
3264
|
}
|
|
3214
3265
|
function gzipFile(f) {
|
|
3215
|
-
if (!
|
|
3216
|
-
|
|
3217
|
-
|
|
3266
|
+
if (!fs23.existsSync(f)) return;
|
|
3267
|
+
fs23.writeFileSync(`${f}.gz`, zlib.gzipSync(fs23.readFileSync(f)));
|
|
3268
|
+
fs23.rmSync(f);
|
|
3218
3269
|
}
|
|
3219
3270
|
function compactRun(meta) {
|
|
3220
3271
|
if (!TERMINAL.includes(meta.state)) return;
|
|
3221
3272
|
const dir = path19.dirname(runFile(meta.id, "meta.json"));
|
|
3222
3273
|
let names;
|
|
3223
3274
|
try {
|
|
3224
|
-
names =
|
|
3275
|
+
names = fs23.readdirSync(dir);
|
|
3225
3276
|
} catch {
|
|
3226
3277
|
return;
|
|
3227
3278
|
}
|
|
3228
3279
|
for (const n of names) {
|
|
3229
3280
|
if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(path19.join(dir, n));
|
|
3230
3281
|
else if (/^stderr(\.attempt-\d+)?\.log$/.test(n)) {
|
|
3231
|
-
if (meta.state === "done" && !/attempt/.test(n))
|
|
3282
|
+
if (meta.state === "done" && !/attempt/.test(n)) fs23.rmSync(path19.join(dir, n), { force: true });
|
|
3232
3283
|
else gzipFile(path19.join(dir, n));
|
|
3233
3284
|
}
|
|
3234
3285
|
}
|
|
@@ -3350,6 +3401,7 @@ function archivedId(ref) {
|
|
|
3350
3401
|
return void 0;
|
|
3351
3402
|
}
|
|
3352
3403
|
}
|
|
3404
|
+
var VERIFY_FAILED = "(r.state = 'done' AND json_extract(r.meta_json, '$.verifyResult.ok') = 0)";
|
|
3353
3405
|
var ftsQuery = (text) => text.split(/\s+/).filter(Boolean).map((w) => `"${w.replace(/"/g, '""')}"*`).join(" ");
|
|
3354
3406
|
function listHistory(q = {}) {
|
|
3355
3407
|
const db = openDb();
|
|
@@ -3369,7 +3421,7 @@ function listHistory(q = {}) {
|
|
|
3369
3421
|
args.push(q.backend);
|
|
3370
3422
|
}
|
|
3371
3423
|
if (q.state) {
|
|
3372
|
-
where.push(q.state === "problem" ?
|
|
3424
|
+
where.push(q.state === "problem" ? `(r.state IN ('failed','timeout','stopped') OR ${VERIFY_FAILED})` : "r.state = ?");
|
|
3373
3425
|
if (q.state !== "problem") args.push(q.state);
|
|
3374
3426
|
}
|
|
3375
3427
|
if (q.group) {
|
|
@@ -3383,8 +3435,8 @@ function listHistory(q = {}) {
|
|
|
3383
3435
|
const base2 = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
|
3384
3436
|
try {
|
|
3385
3437
|
const total = db.prepare(`SELECT COUNT(*) AS n FROM runs r ${base2}`).get(...args).n;
|
|
3386
|
-
const
|
|
3387
|
-
const rows = db.prepare(`SELECT r.*, json_extract(r.meta_json, '$.audit.state') AS audit_state, json_extract(r.meta_json, '$.audit.verdict') AS audit_verdict FROM runs r ${
|
|
3438
|
+
const page2 = q.beforeId ? `${base2 ? `${base2} AND` : "WHERE"} r.id < ?` : base2;
|
|
3439
|
+
const rows = db.prepare(`SELECT r.*, json_extract(r.meta_json, '$.audit.state') AS audit_state, json_extract(r.meta_json, '$.audit.verdict') AS audit_verdict, ${VERIFY_FAILED} AS verify_failed FROM runs r ${page2} ORDER BY r.id DESC LIMIT ?`).all(...args, ...q.beforeId ? [q.beforeId] : [], Math.min(Math.max(q.limit ?? 30, 1), 200));
|
|
3388
3440
|
return {
|
|
3389
3441
|
total,
|
|
3390
3442
|
rows: rows.map((r) => ({
|
|
@@ -3398,6 +3450,7 @@ function listHistory(q = {}) {
|
|
|
3398
3450
|
group: r.grp ?? void 0,
|
|
3399
3451
|
verdict: r.verdict ?? void 0,
|
|
3400
3452
|
audit: auditBadge({ audit: r.audit_state ? { id: "", state: r.audit_state, verdict: r.audit_verdict ?? void 0 } : void 0 }),
|
|
3453
|
+
verifyFailed: r.verify_failed ? true : void 0,
|
|
3401
3454
|
seconds: r.seconds ?? void 0,
|
|
3402
3455
|
steps: r.steps ?? void 0,
|
|
3403
3456
|
tokens: r.tokens ?? void 0,
|
|
@@ -3480,7 +3533,7 @@ function historyStats(sinceMs2) {
|
|
|
3480
3533
|
}
|
|
3481
3534
|
|
|
3482
3535
|
// src/core/report.ts
|
|
3483
|
-
import
|
|
3536
|
+
import fs24 from "node:fs";
|
|
3484
3537
|
|
|
3485
3538
|
// src/core/style.ts
|
|
3486
3539
|
var enabled = () => {
|
|
@@ -3540,11 +3593,13 @@ function duration(meta) {
|
|
|
3540
3593
|
}
|
|
3541
3594
|
function readSummary(meta) {
|
|
3542
3595
|
const f = runFile(meta.id, "summary.md");
|
|
3543
|
-
return
|
|
3596
|
+
return fs24.existsSync(f) ? fs24.readFileSync(f, "utf8").trim() : archivedRun(meta.id)?.answer ?? "";
|
|
3544
3597
|
}
|
|
3545
3598
|
function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
3546
3599
|
const out = [];
|
|
3547
|
-
|
|
3600
|
+
const verifyFailed = meta.state === "done" && meta.verifyResult && !meta.verifyResult.ok;
|
|
3601
|
+
const head = verifyFailed ? stateColour("failed", "\u26A0 done \xB7 verify failed") : stateColour(meta.state, `${ICON2[meta.state]} ${meta.state}`);
|
|
3602
|
+
out.push(`${bold("pitroom")} ${head} \xB7 ${meta.mode} \xB7 ${duration(meta)} \xB7 run ${dim(meta.id)}`);
|
|
3548
3603
|
const ran = meta.ran ?? meta.worker;
|
|
3549
3604
|
const ids = [`worker ${ran.backend}`, meta.resolvedModel && `model ${meta.resolvedModel}`, meta.sessionId && `session ${meta.sessionId}`];
|
|
3550
3605
|
out.push(ids.filter(Boolean).join(" \xB7 "));
|
|
@@ -3610,7 +3665,9 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
|
3610
3665
|
}
|
|
3611
3666
|
if (meta.verifyResult) {
|
|
3612
3667
|
const v = meta.verifyResult;
|
|
3613
|
-
|
|
3668
|
+
const notFound = !v.ok && v.code === 127 && /not found|no such file|is not recognized as an internal or external command/i.test(v.tail);
|
|
3669
|
+
out.push(`\u2500\u2500 verify: \`${meta.verify}\` ${v.ok ? "\u2714 passed" : notFound ? "\u2718 could not run (exit 127: command not found)" : `\u2718 failed (exit ${v.code})`}`);
|
|
3670
|
+
if (notFound) out.push(" the command is not on the PATH Pitroom runs with (an app such as an MCP client may start it without your shell's PATH): give its full path, or set PATH in the command");
|
|
3614
3671
|
if (!v.ok && v.tail) out.push(...v.tail.split("\n").map((l) => ` ${l}`));
|
|
3615
3672
|
}
|
|
3616
3673
|
if (isActive(meta.state)) {
|
|
@@ -3622,8 +3679,8 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
|
3622
3679
|
}
|
|
3623
3680
|
function live(meta) {
|
|
3624
3681
|
const f = runFile(meta.id, "events.jsonl");
|
|
3625
|
-
if (!
|
|
3626
|
-
const p = getBackend((meta.ran ?? meta.worker).backend).parse(
|
|
3682
|
+
if (!fs24.existsSync(f)) return { steps: 0, toolCalls: 0 };
|
|
3683
|
+
const p = getBackend((meta.ran ?? meta.worker).backend).parse(fs24.readFileSync(f, "utf8"));
|
|
3627
3684
|
return { steps: p.usage.steps, toolCalls: p.usage.toolCalls, last: p.lastActivity };
|
|
3628
3685
|
}
|
|
3629
3686
|
function progress(meta) {
|
|
@@ -3634,7 +3691,7 @@ function progress(meta) {
|
|
|
3634
3691
|
}
|
|
3635
3692
|
|
|
3636
3693
|
// src/core/run.ts
|
|
3637
|
-
var VERSION2 = true ? "0.
|
|
3694
|
+
var VERSION2 = true ? "0.18.0" : "0.0.0-dev";
|
|
3638
3695
|
function planWork(o) {
|
|
3639
3696
|
if (!o.plan) return { task: o.task.trim(), tier: o.tier };
|
|
3640
3697
|
if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
|
|
@@ -3666,7 +3723,7 @@ function prepareRun(o) {
|
|
|
3666
3723
|
parent = readMeta(o.continueFrom);
|
|
3667
3724
|
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");
|
|
3668
3725
|
if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
|
|
3669
|
-
if (parent.mode === "isolate" && (!parent.worktree || !
|
|
3726
|
+
if (parent.mode === "isolate" && (!parent.worktree || !fs25.existsSync(parent.worktree))) {
|
|
3670
3727
|
throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
|
|
3671
3728
|
}
|
|
3672
3729
|
const ran = parent.ran ?? parent.worker;
|
|
@@ -3685,18 +3742,18 @@ function prepareRun(o) {
|
|
|
3685
3742
|
throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
|
|
3686
3743
|
}
|
|
3687
3744
|
const mode = parent?.mode ?? o.mode;
|
|
3688
|
-
const dir = path20.resolve(parent?.dir ?? o.dir);
|
|
3689
|
-
if (!
|
|
3745
|
+
const dir = canonical(path20.resolve(parent?.dir ?? o.dir));
|
|
3746
|
+
if (!fs25.existsSync(dir) || !fs25.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
|
|
3690
3747
|
const root = repoRoot(dir);
|
|
3691
3748
|
if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
|
|
3692
3749
|
if (mode === "write" && !root && !o.allowNonGit) {
|
|
3693
3750
|
throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
|
|
3694
3751
|
}
|
|
3695
3752
|
const files = o.files.map((f) => path20.resolve(f));
|
|
3696
|
-
for (const f of files) if (!
|
|
3753
|
+
for (const f of files) if (!fs25.existsSync(f)) throw new UserError(`file not found: ${f}`);
|
|
3697
3754
|
const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
|
|
3698
3755
|
const snap = mode === "read" && !o.review && !o.inPlace && !parent ? wantSnapshot(readIn2, root, dir) : void 0;
|
|
3699
|
-
if (parent?.snapshot?.dir && !
|
|
3756
|
+
if (parent?.snapshot?.dir && !fs25.existsSync(parent.snapshot.dir)) {
|
|
3700
3757
|
throw new UserError(`the read snapshot of run ${parent.id} is gone (cleaned up); ask the question again as a new run`, 3);
|
|
3701
3758
|
}
|
|
3702
3759
|
if (!parent && !snap && !process.env.PITROOM_NO_SECRET_WARNING) {
|
|
@@ -3732,6 +3789,7 @@ function prepareRun(o) {
|
|
|
3732
3789
|
auditOf: o.audit?.of,
|
|
3733
3790
|
auditRate: o.auditRate,
|
|
3734
3791
|
inPlace: o.inPlace || void 0,
|
|
3792
|
+
cache: o.cache,
|
|
3735
3793
|
snapshot: parent?.snapshot ?? (snap ? { dir: "", tree: "", left: snap.secrets } : void 0),
|
|
3736
3794
|
plan: work.plan ?? o.review?.plan ?? parent?.plan,
|
|
3737
3795
|
reviewKind: o.review?.kind,
|
|
@@ -3742,13 +3800,13 @@ function prepareRun(o) {
|
|
|
3742
3800
|
try {
|
|
3743
3801
|
acquireWriteLock(root, meta.id);
|
|
3744
3802
|
} catch (e) {
|
|
3745
|
-
|
|
3803
|
+
fs25.rmSync(runDir(meta.id), { recursive: true, force: true });
|
|
3746
3804
|
throw e;
|
|
3747
3805
|
}
|
|
3748
3806
|
}
|
|
3749
|
-
|
|
3807
|
+
fs25.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
|
|
3750
3808
|
`);
|
|
3751
|
-
if (work.brief)
|
|
3809
|
+
if (work.brief) fs25.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
|
|
3752
3810
|
return meta;
|
|
3753
3811
|
}
|
|
3754
3812
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -3828,7 +3886,7 @@ async function execute(meta) {
|
|
|
3828
3886
|
(meta.attempts ??= []).push({ target: describeTarget(target), error: why.message });
|
|
3829
3887
|
for (const f of ["events.jsonl", "stderr.log"]) {
|
|
3830
3888
|
const from = runFile(meta.id, f);
|
|
3831
|
-
if (
|
|
3889
|
+
if (fs25.existsSync(from)) fs25.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
|
|
3832
3890
|
}
|
|
3833
3891
|
writeMeta(meta);
|
|
3834
3892
|
}
|
|
@@ -3893,7 +3951,7 @@ function prepareTree(meta) {
|
|
|
3893
3951
|
meta.snapshot.dir = s.dir;
|
|
3894
3952
|
meta.snapshot.tree = s.tree;
|
|
3895
3953
|
meta.cwd = path20.join(s.dir, path20.relative(root, meta.dir));
|
|
3896
|
-
if (!
|
|
3954
|
+
if (!fs25.existsSync(meta.cwd)) meta.cwd = s.dir;
|
|
3897
3955
|
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`);
|
|
3898
3956
|
} catch (e) {
|
|
3899
3957
|
meta.snapshot = void 0;
|
|
@@ -3928,7 +3986,7 @@ function attempt(meta, backend, target) {
|
|
|
3928
3986
|
timeoutSec: meta.timeoutSec
|
|
3929
3987
|
});
|
|
3930
3988
|
}
|
|
3931
|
-
var read2 = (f) =>
|
|
3989
|
+
var read2 = (f) => fs25.existsSync(f) ? fs25.readFileSync(f, "utf8") : "";
|
|
3932
3990
|
function retryableFailure(meta, backend, res) {
|
|
3933
3991
|
const run2 = backend.parse(read2(runFile(meta.id, "events.jsonl")));
|
|
3934
3992
|
if (run2.usage.steps > 0) return void 0;
|
|
@@ -3948,12 +4006,12 @@ function finalize(meta, res) {
|
|
|
3948
4006
|
if (meta.snapshot?.dir && meta.repoRoot) run2.finalText = backToProject(run2.finalText, meta.snapshot.dir, meta.repoRoot);
|
|
3949
4007
|
meta.sessionId = run2.sessionId ?? meta.sessionId;
|
|
3950
4008
|
meta.usage = run2.usage;
|
|
3951
|
-
|
|
4009
|
+
fs25.writeFileSync(runFile(meta.id, "summary.md"), `${run2.finalText}
|
|
3952
4010
|
`);
|
|
3953
4011
|
if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run2.finalText);
|
|
3954
4012
|
if (meta.reviewOf) {
|
|
3955
4013
|
meta.verdict = parseVerdict(run2.finalText);
|
|
3956
|
-
if (meta.packageFile)
|
|
4014
|
+
if (meta.packageFile) fs25.rmSync(meta.packageFile, { force: true });
|
|
3957
4015
|
}
|
|
3958
4016
|
if (!res.timedOut && !res.stopped && !meta.error) {
|
|
3959
4017
|
const f = backend.failure(run2, read2(runFile(meta.id, "stderr.log")), res.code);
|
|
@@ -4001,7 +4059,13 @@ function captureChanges(meta) {
|
|
|
4001
4059
|
const d = diffTrees(target, meta.baseTree, meta.afterTree);
|
|
4002
4060
|
meta.changes = d.changes;
|
|
4003
4061
|
meta.stats = d.stats;
|
|
4004
|
-
|
|
4062
|
+
fs25.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
|
|
4063
|
+
}
|
|
4064
|
+
function verifyPath() {
|
|
4065
|
+
const extra = [path20.dirname(process.execPath), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
|
|
4066
|
+
const parts = (process.env.PATH ?? "").split(path20.delimiter).filter(Boolean);
|
|
4067
|
+
for (const d of process.platform === "win32" ? [path20.dirname(process.execPath)] : extra) if (!parts.includes(d)) parts.push(d);
|
|
4068
|
+
return parts.join(path20.delimiter);
|
|
4005
4069
|
}
|
|
4006
4070
|
function runVerify(meta) {
|
|
4007
4071
|
const r = spawnSync6(meta.verify, {
|
|
@@ -4011,11 +4075,12 @@ function runVerify(meta) {
|
|
|
4011
4075
|
timeout: 15 * 6e4,
|
|
4012
4076
|
stdio: ["ignore", "pipe", "pipe"],
|
|
4013
4077
|
maxBuffer: 64 * 1024 * 1024,
|
|
4014
|
-
env: { ...process.env, PWD: meta.cwd, PITROOM_ACTIVE: "1" }
|
|
4078
|
+
env: { ...process.env, PATH: verifyPath(), PWD: meta.cwd, PITROOM_ACTIVE: "1" }
|
|
4015
4079
|
});
|
|
4016
4080
|
const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
|
4017
|
-
|
|
4018
|
-
|
|
4081
|
+
fs25.writeFileSync(runFile(meta.id, "verify.log"), output);
|
|
4082
|
+
const notFound = process.platform === "win32" && r.status === 1 && /is not recognized as an internal or external command/.test(output);
|
|
4083
|
+
return { ok: r.status === 0, code: notFound ? 127 : r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
|
|
4019
4084
|
}
|
|
4020
4085
|
function applyRun(meta, allowDelete = false) {
|
|
4021
4086
|
if (meta.mode !== "isolate") throw new UserError(`run ${meta.id} edited your tree directly (${meta.mode}); nothing to apply`);
|
|
@@ -4051,6 +4116,15 @@ ${res.message}`, 1);
|
|
|
4051
4116
|
writeMeta(meta);
|
|
4052
4117
|
return `reverted ${meta.changes.length} file(s) changed by ${meta.id}`;
|
|
4053
4118
|
}
|
|
4119
|
+
function stopRun(id) {
|
|
4120
|
+
const meta = freshMeta(id);
|
|
4121
|
+
if (!isActive(meta.state) || !isAlive(meta.pid)) return void 0;
|
|
4122
|
+
requestStop(meta.id);
|
|
4123
|
+
meta.stopRequested = true;
|
|
4124
|
+
writeMeta(meta);
|
|
4125
|
+
process.kill(meta.pid, "SIGTERM");
|
|
4126
|
+
return meta;
|
|
4127
|
+
}
|
|
4054
4128
|
function discardRun(meta) {
|
|
4055
4129
|
if (meta.mode !== "isolate") throw new UserError("only --isolate runs have an isolated copy to discard");
|
|
4056
4130
|
cleanupWorktree(meta);
|
|
@@ -4059,9 +4133,13 @@ function discardRun(meta) {
|
|
|
4059
4133
|
return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
|
|
4060
4134
|
}
|
|
4061
4135
|
function cleanupWorktree(meta) {
|
|
4062
|
-
if (meta.worktree &&
|
|
4136
|
+
if (meta.worktree && fs25.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
|
|
4063
4137
|
}
|
|
4064
4138
|
|
|
4139
|
+
// src/cli/mcp-extras.ts
|
|
4140
|
+
import fs26 from "node:fs";
|
|
4141
|
+
import path21 from "node:path";
|
|
4142
|
+
|
|
4065
4143
|
// src/cli/mcp-support.ts
|
|
4066
4144
|
import { spawn as spawn3 } from "node:child_process";
|
|
4067
4145
|
|
|
@@ -4248,12 +4326,12 @@ function pit(args, timeoutMs = 60 * 6e4, signal) {
|
|
|
4248
4326
|
child.stdout.on("data", (d) => out += d);
|
|
4249
4327
|
child.stderr.on("data", (d) => err += d);
|
|
4250
4328
|
const stop = () => child.kill("SIGTERM");
|
|
4251
|
-
const
|
|
4329
|
+
const timer2 = setTimeout(stop, timeoutMs);
|
|
4252
4330
|
signal?.addEventListener("abort", stop, { once: true });
|
|
4253
4331
|
if (signal?.aborted) stop();
|
|
4254
4332
|
child.on("error", (e) => resolve2({ code: 1, out, err: err || String(e.message) }));
|
|
4255
4333
|
child.on("close", (code) => {
|
|
4256
|
-
clearTimeout(
|
|
4334
|
+
clearTimeout(timer2);
|
|
4257
4335
|
signal?.removeEventListener("abort", stop);
|
|
4258
4336
|
resolve2({ code, out: strip(out).trim(), err: strip(err).trim() });
|
|
4259
4337
|
});
|
|
@@ -4264,11 +4342,11 @@ function asResult(r, still) {
|
|
|
4264
4342
|
if (r.code === 75 && still) return { text: clip6(`${text}
|
|
4265
4343
|
|
|
4266
4344
|
${still}`) };
|
|
4267
|
-
return { text: clip6(text), isError: r.code === 2 || r.code === 3 || r.code === 1 && !r.out };
|
|
4345
|
+
return { text: clip6(text), isError: r.code === 2 || r.code === 3 || r.code === 5 || r.code === 6 || r.code === 1 && !r.out };
|
|
4268
4346
|
}
|
|
4269
4347
|
var plain = async (args, ctx, timeoutMs = 6e4) => asResult(await pit(args, timeoutMs, ctx.signal));
|
|
4270
4348
|
function trackProgress(ctx, ids) {
|
|
4271
|
-
const
|
|
4349
|
+
const tick2 = () => {
|
|
4272
4350
|
try {
|
|
4273
4351
|
const lines = ids().map((id) => freshMeta(id)).filter((m) => isActive(m.state)).map((m) => progress(m).replace(/^pitroom\s+/, ""));
|
|
4274
4352
|
if (lines.length) ctx.progress(lines.slice(0, 3).join("\n") + (lines.length > 3 ? `
|
|
@@ -4276,11 +4354,11 @@ function trackProgress(ctx, ids) {
|
|
|
4276
4354
|
} catch {
|
|
4277
4355
|
}
|
|
4278
4356
|
};
|
|
4279
|
-
const first = setTimeout(
|
|
4280
|
-
const
|
|
4357
|
+
const first = setTimeout(tick2, 800);
|
|
4358
|
+
const timer2 = setInterval(tick2, PROGRESS_EVERY_MS);
|
|
4281
4359
|
return () => {
|
|
4282
4360
|
clearTimeout(first);
|
|
4283
|
-
clearInterval(
|
|
4361
|
+
clearInterval(timer2);
|
|
4284
4362
|
};
|
|
4285
4363
|
}
|
|
4286
4364
|
async function waitFor(ids, seconds, ctx, group) {
|
|
@@ -4305,13 +4383,17 @@ async function startAndWait(start, seconds, ctx, task) {
|
|
|
4305
4383
|
const started = await pit([...start, "--bg", "--json", ...task === void 0 ? [] : ["--", task]]);
|
|
4306
4384
|
if (started.code !== 0) return asResult(started);
|
|
4307
4385
|
let id;
|
|
4386
|
+
let cached3 = false;
|
|
4308
4387
|
try {
|
|
4309
|
-
id = JSON.parse(started.out)
|
|
4388
|
+
({ id, cached: cached3 = false } = JSON.parse(started.out));
|
|
4310
4389
|
} catch {
|
|
4311
4390
|
return { text: `could not read the run id from: ${started.out.slice(0, 200)}`, isError: true };
|
|
4312
4391
|
}
|
|
4313
4392
|
const result = await waitFor([id], seconds, ctx);
|
|
4314
|
-
if (cancelled(ctx)) await stopQuietly(["stop", id]);
|
|
4393
|
+
if (cancelled(ctx) && !cached3) await stopQuietly(["stop", id]);
|
|
4394
|
+
if (cached3) result.text = `Cached answer: the same question on the same code as run ${id}; no worker ran (fresh: true asks one).
|
|
4395
|
+
|
|
4396
|
+
${result.text}`;
|
|
4315
4397
|
return result;
|
|
4316
4398
|
}
|
|
4317
4399
|
async function startCrew(flags, tasks, seconds, ctx) {
|
|
@@ -4337,6 +4419,7 @@ var RpcError = class extends Error {
|
|
|
4337
4419
|
};
|
|
4338
4420
|
var RUN = /^pitroom:\/\/run\/(\d{8}-\d{6}-[0-9a-f]{4})(\/patch)?$/;
|
|
4339
4421
|
var LISTED = 30;
|
|
4422
|
+
var runOfUri = (uri) => RUN.exec(uri)?.[1];
|
|
4340
4423
|
var oneLine6 = (text, n) => {
|
|
4341
4424
|
const flat = text.replace(/\s+/g, " ").trim();
|
|
4342
4425
|
return flat.length > n ? `${flat.slice(0, n - 1)}\u2026` : flat;
|
|
@@ -4399,16 +4482,50 @@ Call pitroom_run with mode "isolate" (add "verify" with the project's test comma
|
|
|
4399
4482
|
title: "Split work across workers",
|
|
4400
4483
|
description: "Independent tasks run in parallel as one group of workers.",
|
|
4401
4484
|
arguments: [{ name: "tasks", description: "The tasks, one per line.", required: true }],
|
|
4402
|
-
text: (a) => `Run these independent tasks in parallel with
|
|
4485
|
+
text: (a) => `Run these independent tasks in parallel: call pitroom_run with "tasks" (one worker each):
|
|
4403
4486
|
|
|
4404
4487
|
${a.tasks}
|
|
4405
4488
|
|
|
4406
4489
|
Make each task self-contained. Use mode "isolate" if they change files, then read every patch (pitroom_show, patch: true) and apply them with pitroom_apply (group). Only split work that does not depend on each other.`
|
|
4407
4490
|
}
|
|
4408
4491
|
];
|
|
4409
|
-
var
|
|
4492
|
+
var TOOL_MAP = 'Through this MCP server the `pitroom` commands below are tools: `pitroom run` is pitroom_run (`-i` is mode "isolate", `-w` mode "write", `--continue` is continue, `--verify` is verify), `pitroom crew` is pitroom_run with tasks, `pitroom wait` pitroom_wait, `pitroom show`/`status` pitroom_show, `pitroom review` pitroom_review, `pitroom audit` pitroom_audit, `pitroom apply`/`discard`/`revert`/`stop` the tools of those names, `pitroom history`/`ls`/`models`/`doctor` pitroom_info. With a shell the commands work as well.';
|
|
4493
|
+
var skills;
|
|
4494
|
+
function skillPrompts() {
|
|
4495
|
+
if (skills) return skills;
|
|
4496
|
+
const found = [];
|
|
4497
|
+
const root = packageRoot();
|
|
4498
|
+
for (const name of skillNames(root)) {
|
|
4499
|
+
try {
|
|
4500
|
+
const text = fs26.readFileSync(path21.join(root, "skills", name, "SKILL.md"), "utf8");
|
|
4501
|
+
const front = /^---\n([\s\S]*?)\n---\n/.exec(text);
|
|
4502
|
+
const body = (front ? text.slice(front[0].length) : text).trim();
|
|
4503
|
+
const description = /^description:\s*(.+)$/m.exec(front?.[1] ?? "")?.[1]?.trim() ?? `The ${name} skill.`;
|
|
4504
|
+
const title = /^#\s+(.+)$/m.exec(body)?.[1]?.trim() ?? name;
|
|
4505
|
+
found.push({
|
|
4506
|
+
name,
|
|
4507
|
+
title,
|
|
4508
|
+
// the first sentence: prompts/list is read whole by some clients, so it stays short
|
|
4509
|
+
description: `Skill: ${description.split(/(?<=\.)\s/)[0]}`,
|
|
4510
|
+
arguments: [{ name: "task", description: "What you are about to do (optional)." }],
|
|
4511
|
+
text: (a) => `${TOOL_MAP}
|
|
4512
|
+
|
|
4513
|
+
${body}${a.task ? `
|
|
4514
|
+
|
|
4515
|
+
---
|
|
4516
|
+
|
|
4517
|
+
The task: ${a.task}` : ""}`
|
|
4518
|
+
});
|
|
4519
|
+
} catch {
|
|
4520
|
+
}
|
|
4521
|
+
}
|
|
4522
|
+
skills = found.filter((p) => !PROMPTS.some((b) => b.name === p.name));
|
|
4523
|
+
return skills;
|
|
4524
|
+
}
|
|
4525
|
+
var allPrompts = () => [...PROMPTS, ...skillPrompts()];
|
|
4526
|
+
var listPrompts = () => ({ prompts: allPrompts().map(({ name, title, description, arguments: args }) => ({ name, title, description, arguments: args })) });
|
|
4410
4527
|
function getPrompt(name, given) {
|
|
4411
|
-
const p =
|
|
4528
|
+
const p = allPrompts().find((x) => x.name === name);
|
|
4412
4529
|
if (!p) throw new RpcError(-32602, `unknown prompt: ${String(name)}`);
|
|
4413
4530
|
const args = {};
|
|
4414
4531
|
const raw = given && typeof given === "object" && !Array.isArray(given) ? given : {};
|
|
@@ -4422,84 +4539,110 @@ function getPrompt(name, given) {
|
|
|
4422
4539
|
|
|
4423
4540
|
// src/cli/mcp-tools.ts
|
|
4424
4541
|
var WORKER_PROPS = {
|
|
4425
|
-
worker: { type: "string", description: 'Worker
|
|
4426
|
-
model: { type: "string", description: "Model for
|
|
4427
|
-
tier: { type: "string", description: "
|
|
4428
|
-
effort: { type: "string", description: "
|
|
4429
|
-
dir: { type: "string", description: "Project directory (default: the
|
|
4430
|
-
files: { type: "array", items: { type: "string" }, description: "Files to attach
|
|
4431
|
-
verify: { type: "string", description: '
|
|
4432
|
-
link: { type: "array", items: { type: "string" }, description: 'Ignored
|
|
4433
|
-
web: { type: "boolean", description: "
|
|
4434
|
-
group: { type: "string", description: "
|
|
4542
|
+
worker: { type: "string", description: 'Worker "backend[:model]", e.g. "opencode", "claude:haiku". Default: configured.' },
|
|
4543
|
+
model: { type: "string", description: "Model for that worker." },
|
|
4544
|
+
tier: { type: "string", description: "cheap, standard or capable (from the config)." },
|
|
4545
|
+
effort: { type: "string", description: "low, medium, high, xhigh." },
|
|
4546
|
+
dir: { type: "string", description: "Project directory (default: the server's)." },
|
|
4547
|
+
files: { type: "array", items: { type: "string" }, description: "Files to attach." },
|
|
4548
|
+
verify: { type: "string", description: 'Command run afterwards, e.g. "npm test".' },
|
|
4549
|
+
link: { type: "array", items: { type: "string" }, description: 'Ignored dirs to link into an isolated copy, e.g. ["node_modules"].' },
|
|
4550
|
+
web: { type: "boolean", description: "Allow web tools." },
|
|
4551
|
+
group: { type: "string", description: "Group name for related runs." }
|
|
4435
4552
|
};
|
|
4436
|
-
var WAIT_PROP = { waitSeconds: { type: "number", description: `
|
|
4437
|
-
var RUN_ID2 = { type: "string", description: '
|
|
4438
|
-
var SINCE = { type: "string", description: "A window: 7d, 30d, \u2026 or all." };
|
|
4553
|
+
var WAIT_PROP = { waitSeconds: { type: "number", description: `Wait this long before returning "still running" (default ${DEFAULT_WAIT}, max ${MAX_WAIT}).` } };
|
|
4554
|
+
var RUN_ID2 = { type: "string", description: 'Run id, or "last".' };
|
|
4439
4555
|
var READ_ONLY = { readOnlyHint: true, openWorldHint: false };
|
|
4556
|
+
var TOPICS = {
|
|
4557
|
+
runs: ["running", "group"],
|
|
4558
|
+
history: ["text", "state", "model", "since", "limit"],
|
|
4559
|
+
stats: ["since"],
|
|
4560
|
+
savings: ["since", "perModel"],
|
|
4561
|
+
models: ["worker", "all"],
|
|
4562
|
+
cooldown: [],
|
|
4563
|
+
config: [],
|
|
4564
|
+
doctor: []
|
|
4565
|
+
};
|
|
4566
|
+
function infoArgs(topic, a) {
|
|
4567
|
+
const allowed = TOPICS[topic];
|
|
4568
|
+
const stray = Object.keys(a).filter((k) => k !== "topic" && a[k] !== void 0 && a[k] !== null && !allowed.includes(k));
|
|
4569
|
+
if (stray.length) throw new ToolError(`${stray.map((k) => `"${k}"`).join(", ")} ${stray.length > 1 ? "do" : "does"} not go with topic "${topic}" (${allowed.length ? `it takes ${allowed.join(", ")}` : "it takes no options"})`);
|
|
4570
|
+
const group = str(a, "group");
|
|
4571
|
+
const worker = str(a, "worker");
|
|
4572
|
+
switch (topic) {
|
|
4573
|
+
case "runs":
|
|
4574
|
+
return { args: group ? ["status", "-g", group] : ["ls", ...bool(a, "running") ? ["--running"] : []] };
|
|
4575
|
+
case "history": {
|
|
4576
|
+
const limit = a.limit;
|
|
4577
|
+
if (limit !== void 0 && (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1 || limit > 200)) throw new ToolError('"limit" must be a whole number from 1 to 200');
|
|
4578
|
+
const text = str(a, "text");
|
|
4579
|
+
const state = str(a, "state");
|
|
4580
|
+
const model = str(a, "model");
|
|
4581
|
+
return { args: ["history", ...since(a), ...state ? ["--state", state] : [], ...model ? ["--model", model] : [], ...limit ? ["--limit", String(limit)] : [], ...text ? ["--", text] : []] };
|
|
4582
|
+
}
|
|
4583
|
+
case "stats":
|
|
4584
|
+
return { args: ["history", "stats", ...since(a)] };
|
|
4585
|
+
case "savings":
|
|
4586
|
+
return { args: ["savings", ...since(a), ...bool(a, "perModel") ? ["--models"] : []] };
|
|
4587
|
+
case "models":
|
|
4588
|
+
return { args: ["models", ...worker ? [worker] : [], ...bool(a, "all") ? ["--all"] : []] };
|
|
4589
|
+
case "doctor":
|
|
4590
|
+
return { args: ["doctor"], timeoutMs: 12e4 };
|
|
4591
|
+
case "cooldown":
|
|
4592
|
+
case "config":
|
|
4593
|
+
return { args: [topic] };
|
|
4594
|
+
default:
|
|
4595
|
+
throw new Error(`topic "${topic}" has no command`);
|
|
4596
|
+
}
|
|
4597
|
+
}
|
|
4440
4598
|
var TOOLS2 = [
|
|
4441
4599
|
{
|
|
4442
4600
|
name: "pitroom_run",
|
|
4443
4601
|
title: "Run a worker",
|
|
4444
|
-
description:
|
|
4602
|
+
description: 'Hand a bounded task to a cheaper worker agent. mode "read" (default): read-only research, an answer with verified file:line references. "isolate": the worker edits a private copy and you get the exact diff (then pitroom_review, pitroom_apply or pitroom_discard). "write": edits the working tree (undo: pitroom_revert). Give "tasks" instead of "task" to run independent tasks in parallel (read or isolate). Returns the report(s) and a receipt (a read question asked before on the same code comes back cached), or "still running" for pitroom_wait. The using-pitroom skill (or prompt) says when and how to delegate. Cancelling the call stops the run.',
|
|
4445
4603
|
inputSchema: {
|
|
4446
4604
|
type: "object",
|
|
4447
4605
|
properties: {
|
|
4448
|
-
task: { type: "string", description: "What
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4606
|
+
task: { type: "string", description: "What to do, with the context the worker needs." },
|
|
4607
|
+
tasks: { type: "array", items: { type: "string" }, minItems: 1, maxItems: MAX_TASKS, description: "Or several independent tasks, one worker each." },
|
|
4608
|
+
mode: { type: "string", enum: ["read", "isolate", "write"] },
|
|
4609
|
+
continue: { type: "string", description: "A finished run to follow up in the same worker session." },
|
|
4610
|
+
inPlace: { type: "boolean", description: "Read: read the directory itself, not a snapshot without secret-looking files." },
|
|
4611
|
+
audit: { type: "boolean", description: "Read: have another worker re-check the answer (each one, with tasks)." },
|
|
4612
|
+
fresh: { type: "boolean", description: "Read: ask a worker even if this question was answered on the same code." },
|
|
4453
4613
|
...WORKER_PROPS,
|
|
4454
4614
|
...WAIT_PROP
|
|
4455
|
-
}
|
|
4456
|
-
required: ["task"]
|
|
4615
|
+
}
|
|
4457
4616
|
},
|
|
4458
4617
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
4459
4618
|
async call(a, ctx) {
|
|
4460
|
-
const task = str(a, "task"
|
|
4619
|
+
const task = str(a, "task");
|
|
4620
|
+
const tasks = strs(a, "tasks").filter((t) => t.trim());
|
|
4621
|
+
if (!task && !tasks.length) throw new ToolError('give "task" (or "tasks" for parallel work)');
|
|
4622
|
+
if (task && tasks.length) throw new ToolError('give "task" or "tasks", not both');
|
|
4461
4623
|
const mode = oneOf(a, "mode", ["read", "isolate", "write"], "read");
|
|
4462
4624
|
const flags = mode === "isolate" ? ["-i"] : mode === "write" ? ["-w"] : [];
|
|
4463
|
-
const follow = str(a, "continue");
|
|
4464
|
-
if (follow) flags.push("--continue", follow);
|
|
4465
4625
|
if (mode === "read") {
|
|
4466
4626
|
if (bool(a, "inPlace")) flags.push("--in-place");
|
|
4467
4627
|
if (bool(a, "audit")) flags.push("--audit");
|
|
4628
|
+
if (bool(a, "fresh")) flags.push("--fresh");
|
|
4629
|
+
}
|
|
4630
|
+
const follow = str(a, "continue");
|
|
4631
|
+
if (tasks.length) {
|
|
4632
|
+
if (tasks.length > MAX_TASKS) throw new ToolError(`at most ${MAX_TASKS} tasks at once: start the rest when these are done`);
|
|
4633
|
+
if (mode === "write") throw new ToolError('parallel workers never write in place: use mode "isolate" (each gets its own copy)');
|
|
4634
|
+
if (follow) throw new ToolError('"continue" follows up one run: give "task"');
|
|
4635
|
+
return startCrew([...flags, ...workerFlags(a)], tasks, waitSeconds(a), ctx);
|
|
4468
4636
|
}
|
|
4637
|
+
if (follow) flags.push("--continue", follow);
|
|
4469
4638
|
return startAndWait(["run", ...flags, ...workerFlags(a)], waitSeconds(a), ctx, task);
|
|
4470
4639
|
}
|
|
4471
4640
|
},
|
|
4472
|
-
{
|
|
4473
|
-
name: "pitroom_crew",
|
|
4474
|
-
title: "Run workers in parallel",
|
|
4475
|
-
description: 'Start several independent tasks at once as one group of workers (read, or isolate: each edits its own copy) and wait for the group. Returns every report, or "still running" with the group name for pitroom_wait. Only for tasks that do not depend on each other. Cancelling the call stops the group.',
|
|
4476
|
-
inputSchema: {
|
|
4477
|
-
type: "object",
|
|
4478
|
-
properties: {
|
|
4479
|
-
tasks: { type: "array", items: { type: "string" }, minItems: 1, maxItems: MAX_TASKS, description: "One self-contained task per worker." },
|
|
4480
|
-
mode: { type: "string", enum: ["read", "isolate"], description: "read (default) or isolate." },
|
|
4481
|
-
...WORKER_PROPS,
|
|
4482
|
-
...WAIT_PROP
|
|
4483
|
-
},
|
|
4484
|
-
required: ["tasks"]
|
|
4485
|
-
},
|
|
4486
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
4487
|
-
async call(a, ctx) {
|
|
4488
|
-
const tasks = strs(a, "tasks").filter((t) => t.trim());
|
|
4489
|
-
if (!tasks.length) throw new ToolError('"tasks" needs at least one task');
|
|
4490
|
-
if (tasks.length > MAX_TASKS) throw new ToolError(`at most ${MAX_TASKS} tasks at once: start the rest when these are done`);
|
|
4491
|
-
const mode = oneOf(a, "mode", ["read", "isolate"], "read");
|
|
4492
|
-
return startCrew([...mode === "isolate" ? ["-i"] : [], ...workerFlags(a)], tasks, waitSeconds(a), ctx);
|
|
4493
|
-
}
|
|
4494
|
-
},
|
|
4495
4641
|
{
|
|
4496
4642
|
name: "pitroom_wait",
|
|
4497
4643
|
title: "Wait for runs",
|
|
4498
|
-
description: 'Wait for runs
|
|
4499
|
-
inputSchema: {
|
|
4500
|
-
type: "object",
|
|
4501
|
-
properties: { runs: { type: "array", items: { type: "string" }, description: "Run ids." }, group: { type: "string", description: "Or a group name." }, ...WAIT_PROP }
|
|
4502
|
-
},
|
|
4644
|
+
description: 'Wait for runs that came back "still running" and return their reports.',
|
|
4645
|
+
inputSchema: { type: "object", properties: { runs: { type: "array", items: { type: "string" } }, group: { type: "string" }, ...WAIT_PROP } },
|
|
4503
4646
|
annotations: READ_ONLY,
|
|
4504
4647
|
async call(a, ctx) {
|
|
4505
4648
|
const runs = strs(a, "runs");
|
|
@@ -4508,126 +4651,56 @@ var TOOLS2 = [
|
|
|
4508
4651
|
return waitFor(runs, waitSeconds(a), ctx, runs.length ? void 0 : group);
|
|
4509
4652
|
}
|
|
4510
4653
|
},
|
|
4511
|
-
{
|
|
4512
|
-
name: "pitroom_status",
|
|
4513
|
-
title: "Run status",
|
|
4514
|
-
description: "The state and live progress of a run (default: the latest), or of a group.",
|
|
4515
|
-
inputSchema: { type: "object", properties: { run: RUN_ID2, group: { type: "string", description: "A group name." } } },
|
|
4516
|
-
annotations: READ_ONLY,
|
|
4517
|
-
async call(a, ctx) {
|
|
4518
|
-
const group = str(a, "group");
|
|
4519
|
-
const run2 = str(a, "run");
|
|
4520
|
-
if (group && run2) throw new ToolError('give "run" or "group", not both');
|
|
4521
|
-
return plain(["status", ...group ? ["-g", group] : run2 ? [run2] : []], ctx);
|
|
4522
|
-
}
|
|
4523
|
-
},
|
|
4524
|
-
{
|
|
4525
|
-
name: "pitroom_list",
|
|
4526
|
-
title: "List runs",
|
|
4527
|
-
description: "The latest runs with their state, worker and task; with running only the active ones, or one group's.",
|
|
4528
|
-
inputSchema: { type: "object", properties: { running: { type: "boolean", description: "Only runs that are queued or running." }, group: { type: "string", description: "A group name." } } },
|
|
4529
|
-
annotations: READ_ONLY,
|
|
4530
|
-
async call(a, ctx) {
|
|
4531
|
-
const group = str(a, "group");
|
|
4532
|
-
return plain(["ls", ...bool(a, "running") ? ["--running"] : [], ...group ? ["-g", group] : []], ctx);
|
|
4533
|
-
}
|
|
4534
|
-
},
|
|
4535
4654
|
{
|
|
4536
4655
|
name: "pitroom_show",
|
|
4537
4656
|
title: "Show a run",
|
|
4538
|
-
description: "A
|
|
4539
|
-
inputSchema: { type: "object", properties: { run: RUN_ID2, patch: { type: "boolean" }, full: { type: "boolean" } }
|
|
4657
|
+
description: "A run's report (live progress while it runs); patch: the exact diff; full: the untruncated answer.",
|
|
4658
|
+
inputSchema: { type: "object", properties: { run: RUN_ID2, patch: { type: "boolean" }, full: { type: "boolean" } } },
|
|
4540
4659
|
annotations: READ_ONLY,
|
|
4541
4660
|
async call(a, ctx) {
|
|
4542
|
-
const run2 = str(a, "run"
|
|
4661
|
+
const run2 = str(a, "run") ?? "last";
|
|
4543
4662
|
return plain(["show", run2, ...bool(a, "patch") ? ["--patch"] : bool(a, "full") ? ["--full"] : []], ctx);
|
|
4544
4663
|
}
|
|
4545
4664
|
},
|
|
4546
4665
|
{
|
|
4547
|
-
name: "
|
|
4548
|
-
title: "
|
|
4549
|
-
description: "
|
|
4666
|
+
name: "pitroom_info",
|
|
4667
|
+
title: "Pitroom information",
|
|
4668
|
+
description: "Reports, changing nothing. topic: runs (latest runs; running, group), history (search earlier answers before asking again; text, state, model, since, limit), stats (success rate, time, tokens, audits per worker; since), savings (since, perModel), models (a backend's models and costs; worker, all), cooldown (rate-limited models being skipped), config, doctor (setup check).",
|
|
4550
4669
|
inputSchema: {
|
|
4551
4670
|
type: "object",
|
|
4552
4671
|
properties: {
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4672
|
+
topic: { type: "string", enum: Object.keys(TOPICS) },
|
|
4673
|
+
text: { type: "string" },
|
|
4674
|
+
state: { type: "string" },
|
|
4675
|
+
model: { type: "string" },
|
|
4676
|
+
since: { type: "string", description: "7d, 30d, \u2026 or all." },
|
|
4677
|
+
limit: { type: "number" },
|
|
4678
|
+
running: { type: "boolean" },
|
|
4679
|
+
group: { type: "string" },
|
|
4680
|
+
worker: { type: "string" },
|
|
4681
|
+
all: { type: "boolean" },
|
|
4682
|
+
perModel: { type: "boolean" }
|
|
4683
|
+
},
|
|
4684
|
+
required: ["topic"]
|
|
4559
4685
|
},
|
|
4560
4686
|
annotations: READ_ONLY,
|
|
4561
4687
|
async call(a, ctx) {
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
const
|
|
4565
|
-
|
|
4566
|
-
const model = str(a, "model");
|
|
4567
|
-
return plain(["history", ...since(a), ...state ? ["--state", state] : [], ...model ? ["--model", model] : [], ...limit ? ["--limit", String(limit)] : [], ...text ? ["--", text] : []], ctx);
|
|
4568
|
-
}
|
|
4569
|
-
},
|
|
4570
|
-
{
|
|
4571
|
-
name: "pitroom_stats",
|
|
4572
|
-
title: "Worker statistics",
|
|
4573
|
-
description: "Runs, success rate, average time and tokens per worker and model, and how often their answers were confirmed by audits. Use it to pick a worker.",
|
|
4574
|
-
inputSchema: { type: "object", properties: { since: SINCE } },
|
|
4575
|
-
annotations: READ_ONLY,
|
|
4576
|
-
async call(a, ctx) {
|
|
4577
|
-
return plain(["history", "stats", ...since(a)], ctx);
|
|
4578
|
-
}
|
|
4579
|
-
},
|
|
4580
|
-
{
|
|
4581
|
-
name: "pitroom_savings",
|
|
4582
|
-
title: "What the workers saved",
|
|
4583
|
-
description: "The tokens and money workers took over from you, per model, from the receipts of the runs.",
|
|
4584
|
-
inputSchema: { type: "object", properties: { since: SINCE, models: { type: "boolean", description: "Break it down per model." } } },
|
|
4585
|
-
annotations: READ_ONLY,
|
|
4586
|
-
async call(a, ctx) {
|
|
4587
|
-
return plain(["savings", ...since(a), ...bool(a, "models") ? ["--models"] : []], ctx);
|
|
4588
|
-
}
|
|
4589
|
-
},
|
|
4590
|
-
{
|
|
4591
|
-
name: "pitroom_models",
|
|
4592
|
-
title: "Worker models",
|
|
4593
|
-
description: "The models a worker backend offers, with effort levels, the costs set in the config and recent usage. Use it to choose a model for pitroom_run.",
|
|
4594
|
-
inputSchema: { type: "object", properties: { worker: { type: "string", description: "A backend: opencode, codex, claude, gemini. Default: the configured one." }, all: { type: "boolean", description: "Include models that are hidden by default." } } },
|
|
4595
|
-
annotations: READ_ONLY,
|
|
4596
|
-
async call(a, ctx) {
|
|
4597
|
-
const worker = str(a, "worker");
|
|
4598
|
-
return plain(["models", ...worker ? [worker] : [], ...bool(a, "all") ? ["--all"] : []], ctx);
|
|
4599
|
-
}
|
|
4600
|
-
},
|
|
4601
|
-
{
|
|
4602
|
-
name: "pitroom_cooldown",
|
|
4603
|
-
title: "Rate-limited models",
|
|
4604
|
-
description: 'The models that said "rate limited" and are being skipped for now (runs go to the next worker). With clear they are tried again immediately.',
|
|
4605
|
-
inputSchema: { type: "object", properties: { clear: { type: "boolean", description: "Forget the cooldowns." } } },
|
|
4606
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
4607
|
-
async call(a, ctx) {
|
|
4608
|
-
return plain(["cooldown", ...bool(a, "clear") ? ["--clear"] : []], ctx);
|
|
4609
|
-
}
|
|
4610
|
-
},
|
|
4611
|
-
{
|
|
4612
|
-
name: "pitroom_doctor",
|
|
4613
|
-
title: "Check the setup",
|
|
4614
|
-
description: "Checks the setup without spending tokens: the workers found and their logins, the config, the skills, where the MCP server is registered. Use it when a run fails to start.",
|
|
4615
|
-
inputSchema: { type: "object", properties: {} },
|
|
4616
|
-
annotations: READ_ONLY,
|
|
4617
|
-
async call(_a, ctx) {
|
|
4618
|
-
return plain(["doctor"], ctx, 12e4);
|
|
4688
|
+
if (str(a, "topic") === void 0) throw new ToolError(`"topic" is required: one of ${Object.keys(TOPICS).join(", ")}`);
|
|
4689
|
+
const topic = oneOf(a, "topic", Object.keys(TOPICS), "runs");
|
|
4690
|
+
const { args, timeoutMs } = infoArgs(topic, a);
|
|
4691
|
+
return plain(args, ctx, timeoutMs);
|
|
4619
4692
|
}
|
|
4620
4693
|
},
|
|
4621
4694
|
{
|
|
4622
4695
|
name: "pitroom_review",
|
|
4623
4696
|
title: "Review a change",
|
|
4624
|
-
description: `A read-only reviewer (
|
|
4697
|
+
description: `A read-only reviewer (another worker by default) judges a run's change or a commit range ("main..HEAD"): findings with severities and a SPEC / QUALITY verdict.`,
|
|
4625
4698
|
inputSchema: {
|
|
4626
4699
|
type: "object",
|
|
4627
4700
|
properties: {
|
|
4628
|
-
run:
|
|
4701
|
+
run: RUN_ID2,
|
|
4629
4702
|
range: { type: "string", description: 'Or a commit range "A..B".' },
|
|
4630
|
-
plan: { type: "string", description: "With range: the plan file
|
|
4703
|
+
plan: { type: "string", description: "With range: the plan file it implements." },
|
|
4631
4704
|
worker: WORKER_PROPS.worker,
|
|
4632
4705
|
tier: WORKER_PROPS.tier,
|
|
4633
4706
|
dir: WORKER_PROPS.dir,
|
|
@@ -4648,7 +4721,7 @@ var TOOLS2 = [
|
|
|
4648
4721
|
{
|
|
4649
4722
|
name: "pitroom_audit",
|
|
4650
4723
|
title: "Audit an answer",
|
|
4651
|
-
description: "Another worker re-checks a
|
|
4724
|
+
description: "Another worker re-checks a read run's answer: AGREE, PARTIAL or DISAGREE, with the disputed claims.",
|
|
4652
4725
|
inputSchema: { type: "object", properties: { run: RUN_ID2, worker: WORKER_PROPS.worker, ...WAIT_PROP }, required: ["run"] },
|
|
4653
4726
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
4654
4727
|
async call(a, ctx) {
|
|
@@ -4660,7 +4733,7 @@ var TOOLS2 = [
|
|
|
4660
4733
|
{
|
|
4661
4734
|
name: "pitroom_apply",
|
|
4662
4735
|
title: "Apply an isolated change",
|
|
4663
|
-
description: "Land an isolated run's patch
|
|
4736
|
+
description: "Land an isolated run's patch (or a group's, in order) on the working tree; checked first. A patch deleting files needs allowDelete: pass it only after checking the deletions were asked for.",
|
|
4664
4737
|
inputSchema: { type: "object", properties: { run: RUN_ID2, group: { type: "string" }, allowDelete: { type: "boolean" } } },
|
|
4665
4738
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
4666
4739
|
async call(a, ctx) {
|
|
@@ -4668,13 +4741,13 @@ var TOOLS2 = [
|
|
|
4668
4741
|
const run2 = str(a, "run");
|
|
4669
4742
|
if (!run2 && !group) throw new ToolError('give "run" or "group"');
|
|
4670
4743
|
if (run2 && group) throw new ToolError('give "run" or "group", not both');
|
|
4671
|
-
return plain(["apply", ...group
|
|
4744
|
+
return plain(["apply", ...group ? ["-g", group] : [run2], ...bool(a, "allowDelete") ? ["--allow-delete"] : []], ctx, 12e4);
|
|
4672
4745
|
}
|
|
4673
4746
|
},
|
|
4674
4747
|
{
|
|
4675
4748
|
name: "pitroom_discard",
|
|
4676
4749
|
title: "Discard an isolated change",
|
|
4677
|
-
description: "Drop an isolated run's private copy (
|
|
4750
|
+
description: "Drop an isolated run's private copy (the patch is kept).",
|
|
4678
4751
|
inputSchema: { type: "object", properties: { run: RUN_ID2 }, required: ["run"] },
|
|
4679
4752
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
4680
4753
|
async call(a, ctx) {
|
|
@@ -4684,7 +4757,7 @@ var TOOLS2 = [
|
|
|
4684
4757
|
{
|
|
4685
4758
|
name: "pitroom_revert",
|
|
4686
4759
|
title: "Undo an in-place change",
|
|
4687
|
-
description:
|
|
4760
|
+
description: `Undo a mode "write" run's changes (refused if the files changed since).`,
|
|
4688
4761
|
inputSchema: { type: "object", properties: { run: RUN_ID2 }, required: ["run"] },
|
|
4689
4762
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
4690
4763
|
async call(a, ctx) {
|
|
@@ -4694,32 +4767,115 @@ var TOOLS2 = [
|
|
|
4694
4767
|
{
|
|
4695
4768
|
name: "pitroom_stop",
|
|
4696
4769
|
title: "Stop runs",
|
|
4697
|
-
description: "Stop a running or queued run, or
|
|
4698
|
-
inputSchema: { type: "object", properties: { run: RUN_ID2, group: { type: "string" } } },
|
|
4770
|
+
description: "Stop a running or queued run, or a group; cooldowns: try rate-limited models again now.",
|
|
4771
|
+
inputSchema: { type: "object", properties: { run: RUN_ID2, group: { type: "string" }, cooldowns: { type: "boolean" } } },
|
|
4699
4772
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
4700
4773
|
async call(a, ctx) {
|
|
4701
4774
|
const run2 = str(a, "run");
|
|
4702
4775
|
const group = str(a, "group");
|
|
4703
|
-
if (
|
|
4776
|
+
if (bool(a, "cooldowns")) {
|
|
4777
|
+
if (run2 || group) throw new ToolError('"cooldowns" goes alone');
|
|
4778
|
+
return plain(["cooldown", "--clear"], ctx);
|
|
4779
|
+
}
|
|
4780
|
+
if (!run2 && !group) throw new ToolError('give "run" or "group" (or "cooldowns": true)');
|
|
4704
4781
|
if (run2 && group) throw new ToolError('give "run" or "group", not both');
|
|
4705
4782
|
return plain(["stop", ...run2 ? [run2] : ["-g", group]], ctx);
|
|
4706
4783
|
}
|
|
4707
4784
|
}
|
|
4708
4785
|
];
|
|
4709
4786
|
|
|
4787
|
+
// src/cli/mcp-watch.ts
|
|
4788
|
+
var EVERY_MS = 2e3;
|
|
4789
|
+
var MAX_SUBSCRIPTIONS = 100;
|
|
4790
|
+
var watched = /* @__PURE__ */ new Set();
|
|
4791
|
+
var timer;
|
|
4792
|
+
var signature = (uri) => {
|
|
4793
|
+
const id = runOfUri(uri);
|
|
4794
|
+
if (!id) return void 0;
|
|
4795
|
+
try {
|
|
4796
|
+
const m = freshMeta(id);
|
|
4797
|
+
return uri.endsWith("/patch") ? `${m.state}:${m.changes?.length ?? 0}` : m.state;
|
|
4798
|
+
} catch {
|
|
4799
|
+
return void 0;
|
|
4800
|
+
}
|
|
4801
|
+
};
|
|
4802
|
+
var newestRun = () => {
|
|
4803
|
+
try {
|
|
4804
|
+
return listRunIds().at(-1);
|
|
4805
|
+
} catch {
|
|
4806
|
+
return void 0;
|
|
4807
|
+
}
|
|
4808
|
+
};
|
|
4809
|
+
function subscribe(session, uri) {
|
|
4810
|
+
if (typeof uri !== "string") throw new RpcError(-32602, '"uri" is required');
|
|
4811
|
+
if (!runOfUri(uri)) throw new RpcError(-32602, `only runs can be subscribed to (pitroom://run/<id>): ${uri}`);
|
|
4812
|
+
if (!session.subscriptions.has(uri) && session.subscriptions.size >= MAX_SUBSCRIPTIONS) throw new RpcError(-32602, `at most ${MAX_SUBSCRIPTIONS} subscriptions: unsubscribe from finished runs first`);
|
|
4813
|
+
const sig = signature(uri);
|
|
4814
|
+
if (sig === void 0) throw new RpcError(-32002, `unknown resource: ${uri}`);
|
|
4815
|
+
session.subscriptions.set(uri, sig);
|
|
4816
|
+
}
|
|
4817
|
+
function unsubscribe(session, uri) {
|
|
4818
|
+
if (typeof uri !== "string") throw new RpcError(-32602, '"uri" is required');
|
|
4819
|
+
session.subscriptions.delete(uri);
|
|
4820
|
+
}
|
|
4821
|
+
function tick() {
|
|
4822
|
+
const listening = [...watched].filter((s) => s.push);
|
|
4823
|
+
if (!listening.length) return;
|
|
4824
|
+
const newest = newestRun();
|
|
4825
|
+
const seen = /* @__PURE__ */ new Map();
|
|
4826
|
+
for (const s of listening) {
|
|
4827
|
+
const push = s.push;
|
|
4828
|
+
if (newest !== s.newestRun) {
|
|
4829
|
+
s.newestRun = newest;
|
|
4830
|
+
push({ jsonrpc: "2.0", method: "notifications/resources/list_changed" });
|
|
4831
|
+
}
|
|
4832
|
+
for (const [uri, last] of s.subscriptions) {
|
|
4833
|
+
if (!seen.has(uri)) seen.set(uri, signature(uri));
|
|
4834
|
+
const now = seen.get(uri);
|
|
4835
|
+
if (now === last) continue;
|
|
4836
|
+
if (now === void 0) s.subscriptions.delete(uri);
|
|
4837
|
+
else s.subscriptions.set(uri, now);
|
|
4838
|
+
push({ jsonrpc: "2.0", method: "notifications/resources/updated", params: { uri } });
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
}
|
|
4842
|
+
function watch2(session) {
|
|
4843
|
+
session.newestRun = newestRun();
|
|
4844
|
+
watched.add(session);
|
|
4845
|
+
if (!timer) {
|
|
4846
|
+
timer = setInterval(tick, EVERY_MS);
|
|
4847
|
+
timer.unref();
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4850
|
+
function unwatch(session) {
|
|
4851
|
+
watched.delete(session);
|
|
4852
|
+
session.subscriptions.clear();
|
|
4853
|
+
if (!watched.size && timer) {
|
|
4854
|
+
clearInterval(timer);
|
|
4855
|
+
timer = void 0;
|
|
4856
|
+
}
|
|
4857
|
+
}
|
|
4858
|
+
|
|
4710
4859
|
// src/cli/mcp.ts
|
|
4711
4860
|
var PROTOCOLS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
4712
4861
|
var INSTRUCTIONS = [
|
|
4713
4862
|
"Pitroom hands bounded work to cheaper worker agents and returns a verified answer, the exact diff and a cost receipt. You decide, verify and answer.",
|
|
4714
|
-
'Use pitroom_run with mode "read" for research and locating code, mode "isolate" for code changes (the worker edits a copy; check it with pitroom_review, then pitroom_apply or pitroom_discard);
|
|
4863
|
+
'Use pitroom_run with mode "read" for research and locating code, mode "isolate" for code changes (the worker edits a copy; check it with pitroom_review, then pitroom_apply or pitroom_discard); pitroom_run with "tasks" for independent tasks in parallel; pitroom_info (topic history) finds an earlier answer before you ask again.',
|
|
4715
4864
|
'A run can take minutes: pitroom_run waits up to waitSeconds, then returns the run id as "still running"; call pitroom_wait with it.',
|
|
4865
|
+
"Pitroom's skills (using-pitroom, pitroom-research, pitroom-implement, pitroom-review, pitroom-crew, pitroom-debugging, pitroom-tdd and more) say when to delegate and how to check what comes back: if you have them, use the matching one alongside these tools; if not, the prompts of the same names hold them.",
|
|
4716
4866
|
"It is optional: for a small task you can do yourself, skip it."
|
|
4717
4867
|
].join(" ");
|
|
4718
4868
|
var log = (msg) => void process.stderr.write(`pitroom mcp: ${msg}
|
|
4719
4869
|
`);
|
|
4720
|
-
|
|
4870
|
+
function newSession() {
|
|
4871
|
+
const session = { inflight: /* @__PURE__ */ new Map(), subscriptions: /* @__PURE__ */ new Map() };
|
|
4872
|
+
watch2(session);
|
|
4873
|
+
return session;
|
|
4874
|
+
}
|
|
4721
4875
|
function endSession(session) {
|
|
4722
4876
|
for (const { abort } of session.inflight.values()) abort.abort("closed");
|
|
4877
|
+
unwatch(session);
|
|
4878
|
+
session.push = void 0;
|
|
4723
4879
|
}
|
|
4724
4880
|
function progressToken(params) {
|
|
4725
4881
|
const meta = params && typeof params === "object" ? params._meta : void 0;
|
|
@@ -4737,13 +4893,13 @@ function contextFor(params, abort, notify) {
|
|
|
4737
4893
|
}
|
|
4738
4894
|
};
|
|
4739
4895
|
}
|
|
4740
|
-
async function dispatch(method, params, ctx) {
|
|
4896
|
+
async function dispatch(method, params, ctx, session) {
|
|
4741
4897
|
switch (method) {
|
|
4742
4898
|
case "initialize": {
|
|
4743
4899
|
const asked = typeof params.protocolVersion === "string" ? params.protocolVersion : "";
|
|
4744
4900
|
return {
|
|
4745
4901
|
protocolVersion: PROTOCOLS.includes(asked) ? asked : PROTOCOLS[0],
|
|
4746
|
-
capabilities: { tools: { listChanged: false }, resources: { listChanged:
|
|
4902
|
+
capabilities: { tools: { listChanged: false }, resources: { listChanged: true, subscribe: true }, prompts: { listChanged: false } },
|
|
4747
4903
|
serverInfo: { name: "pitroom", title: "Pitroom", version: VERSION2 },
|
|
4748
4904
|
instructions: INSTRUCTIONS
|
|
4749
4905
|
};
|
|
@@ -4772,6 +4928,12 @@ async function dispatch(method, params, ctx) {
|
|
|
4772
4928
|
case "resources/read":
|
|
4773
4929
|
if (typeof params.uri !== "string") throw new RpcError(-32602, '"uri" is required');
|
|
4774
4930
|
return readResource(params.uri, ctx);
|
|
4931
|
+
case "resources/subscribe":
|
|
4932
|
+
subscribe(session, params.uri);
|
|
4933
|
+
return {};
|
|
4934
|
+
case "resources/unsubscribe":
|
|
4935
|
+
unsubscribe(session, params.uri);
|
|
4936
|
+
return {};
|
|
4775
4937
|
case "prompts/list":
|
|
4776
4938
|
return listPrompts();
|
|
4777
4939
|
case "prompts/get":
|
|
@@ -4799,7 +4961,7 @@ async function handle(session, msg, notify) {
|
|
|
4799
4961
|
const entry = { abort, ctx: contextFor(params, abort, notify) };
|
|
4800
4962
|
if (isRequest) session.inflight.set(m.id, entry);
|
|
4801
4963
|
try {
|
|
4802
|
-
const result = await dispatch(m.method, params, entry.ctx);
|
|
4964
|
+
const result = await dispatch(m.method, params, entry.ctx, session);
|
|
4803
4965
|
if (abort.signal.aborted && abort.signal.reason === "cancelled") return void 0;
|
|
4804
4966
|
return isRequest ? { jsonrpc: "2.0", id: m.id, result } : void 0;
|
|
4805
4967
|
} catch (e) {
|
|
@@ -4824,6 +4986,7 @@ async function serveMcp() {
|
|
|
4824
4986
|
const send = (msg) => void process.stdout.write(`${JSON.stringify(msg)}
|
|
4825
4987
|
`);
|
|
4826
4988
|
const session = newSession();
|
|
4989
|
+
session.push = send;
|
|
4827
4990
|
const rl = readline.createInterface({ input: process.stdin });
|
|
4828
4991
|
const pending = /* @__PURE__ */ new Set();
|
|
4829
4992
|
const handleLine = async (line) => {
|
|
@@ -4844,28 +5007,29 @@ async function serveMcp() {
|
|
|
4844
5007
|
void p.finally(() => pending.delete(p));
|
|
4845
5008
|
});
|
|
4846
5009
|
await new Promise((resolve2) => rl.once("close", resolve2));
|
|
5010
|
+
unwatch(session);
|
|
4847
5011
|
await Promise.allSettled([...pending]);
|
|
4848
5012
|
return 0;
|
|
4849
5013
|
}
|
|
4850
5014
|
|
|
4851
5015
|
// src/cli/mcp-http.ts
|
|
4852
5016
|
import crypto7 from "node:crypto";
|
|
4853
|
-
import
|
|
5017
|
+
import fs28 from "node:fs";
|
|
4854
5018
|
import http2 from "node:http";
|
|
4855
|
-
import
|
|
5019
|
+
import path23 from "node:path";
|
|
4856
5020
|
|
|
4857
5021
|
// src/core/dash.ts
|
|
4858
5022
|
import { spawn as spawn4 } from "node:child_process";
|
|
4859
5023
|
import crypto6 from "node:crypto";
|
|
4860
|
-
import
|
|
5024
|
+
import fs27 from "node:fs";
|
|
4861
5025
|
import http from "node:http";
|
|
4862
|
-
import
|
|
5026
|
+
import path22 from "node:path";
|
|
4863
5027
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
4864
5028
|
|
|
4865
5029
|
// src/core/file-diff.ts
|
|
4866
5030
|
var PREVIEW_LINES = 300;
|
|
4867
5031
|
function gitPath(value, prefix = true) {
|
|
4868
|
-
let
|
|
5032
|
+
let path31 = value;
|
|
4869
5033
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
4870
5034
|
const bytes = [];
|
|
4871
5035
|
const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
|
|
@@ -4873,9 +5037,9 @@ function gitPath(value, prefix = true) {
|
|
|
4873
5037
|
if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
|
|
4874
5038
|
else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
|
|
4875
5039
|
}
|
|
4876
|
-
|
|
5040
|
+
path31 = new TextDecoder().decode(new Uint8Array(bytes));
|
|
4877
5041
|
}
|
|
4878
|
-
return prefix ?
|
|
5042
|
+
return prefix ? path31.replace(/^[ab]\//, "") : path31;
|
|
4879
5043
|
}
|
|
4880
5044
|
function headerPath(header) {
|
|
4881
5045
|
const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
|
|
@@ -4977,9 +5141,9 @@ function fileDiffs(patch, changes, sourceTruncated = false) {
|
|
|
4977
5141
|
// src/core/dash-page.ts
|
|
4978
5142
|
var THEME_SCRIPT = "(function(){try{var t=localStorage.getItem('pitroom-theme');var d=t?t==='dark':matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.classList.toggle('dark',d)}catch(e){}})()";
|
|
4979
5143
|
var ICON3 = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%23ff6a2b'/%3E%3Cpath d='M9 8h4v4H9zm8 0h4v4h-4zm-4 4h4v4h-4zm8 0h4v4h-4zM9 16h4v4H9zm8 0h4v4h-4zm-4 4h4v4h-4zm8 0h4v4h-4z' fill='%23fff'/%3E%3C/svg%3E";
|
|
4980
|
-
var
|
|
5144
|
+
var page = (token) => `<!doctype html>
|
|
4981
5145
|
<html lang="en" class="dark"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
4982
|
-
<meta name="color-scheme" content="dark light"><title>Pitroom</title><link rel="icon" href="${ICON3}">
|
|
5146
|
+
<meta name="color-scheme" content="dark light"><meta name="pitroom-token" content="${token}"><title>Pitroom</title><link rel="icon" href="${ICON3}">
|
|
4983
5147
|
<script>${THEME_SCRIPT}</script><link rel="stylesheet" href="/assets/app.css"></head>
|
|
4984
5148
|
<body><div id="root"></div><script type="module" src="/assets/app.js"></script></body></html>`;
|
|
4985
5149
|
var MISSING = `<!doctype html><meta charset="utf-8"><title>Pitroom</title><body style="font:15px system-ui;padding:3rem;max-width:40rem;margin:auto"><h1>Pitroom dash</h1><p>The dashboard files are missing (dist/ui). Run <code>npm run build</code> in the Pitroom repository, or reinstall the package.</p></body>`;
|
|
@@ -5000,11 +5164,13 @@ function findings(m) {
|
|
|
5000
5164
|
const parts = [v.critical && `${v.critical} critical`, v.important && `${v.important} important`, v.minor && `${v.minor} minor`].filter(Boolean);
|
|
5001
5165
|
return parts.length ? parts.join(" \xB7 ") : "no findings";
|
|
5002
5166
|
}
|
|
5167
|
+
var canDiscard = (m) => m.mode === "isolate" && !isActive(m.state) && !m.applied && !m.discarded;
|
|
5003
5168
|
function toRun(m) {
|
|
5004
5169
|
const l = m.state === "running" ? live(m) : void 0;
|
|
5005
5170
|
return {
|
|
5006
5171
|
id: m.id,
|
|
5007
5172
|
state: m.state,
|
|
5173
|
+
verifyFailed: m.verifyResult && !m.verifyResult.ok ? true : void 0,
|
|
5008
5174
|
kind: kind(m),
|
|
5009
5175
|
// the model that really ran, once the CLI has said which; until then just the worker, never a guess
|
|
5010
5176
|
worker: workerName(ranTarget(m)),
|
|
@@ -5019,7 +5185,8 @@ function toRun(m) {
|
|
|
5019
5185
|
audit: auditBadge(m),
|
|
5020
5186
|
changes: m.changes?.length || void 0,
|
|
5021
5187
|
applied: m.applied || void 0,
|
|
5022
|
-
|
|
5188
|
+
discardable: canDiscard(m) || void 0,
|
|
5189
|
+
note: l?.last ? oneLine7(String(l.last), 140) : isActive(m.state) ? "" : m.verifyResult && !m.verifyResult.ok ? `verify failed: ${m.verify ?? ""}` : m.verdict ? findings(m) : m.auditOf && m.state === "done" ? m.auditDisputed?.length ? `${m.auditDisputed.length} disputed` : "nothing disputed" : headline(m, 200) || oneLine7(m.error ?? "", 200)
|
|
5023
5190
|
};
|
|
5024
5191
|
}
|
|
5025
5192
|
function dashState(opts = {}) {
|
|
@@ -5123,29 +5290,55 @@ var assetsDir = () => fileURLToPath3(new URL("./ui/", import.meta.url));
|
|
|
5123
5290
|
var assetCache = /* @__PURE__ */ new Map();
|
|
5124
5291
|
function readAsset(route) {
|
|
5125
5292
|
try {
|
|
5126
|
-
const file2 =
|
|
5127
|
-
const { mtimeMs } =
|
|
5293
|
+
const file2 = path22.join(assetsDir(), path22.basename(route));
|
|
5294
|
+
const { mtimeMs } = fs27.statSync(file2);
|
|
5128
5295
|
const hit = assetCache.get(route);
|
|
5129
5296
|
if (hit && hit.mtimeMs === mtimeMs) return hit.data;
|
|
5130
|
-
const data =
|
|
5297
|
+
const data = fs27.readFileSync(file2);
|
|
5131
5298
|
assetCache.set(route, { mtimeMs, data });
|
|
5132
5299
|
return data;
|
|
5133
5300
|
} catch {
|
|
5134
5301
|
return void 0;
|
|
5135
5302
|
}
|
|
5136
5303
|
}
|
|
5137
|
-
function handler(touch2) {
|
|
5304
|
+
function handler(touch2, token) {
|
|
5138
5305
|
const send = (res, code, type, body) => {
|
|
5139
5306
|
res.writeHead(code, { ...HEADERS, "content-type": type });
|
|
5140
5307
|
res.end(body);
|
|
5141
5308
|
};
|
|
5142
5309
|
const json = (res, code, value) => send(res, code, "application/json; charset=utf-8", JSON.stringify(value));
|
|
5310
|
+
const tokenBuf = Buffer.from(token);
|
|
5311
|
+
const act = (req, res, pathname) => {
|
|
5312
|
+
req.resume();
|
|
5313
|
+
const given = Buffer.from(String(req.headers["x-pitroom-token"] ?? ""));
|
|
5314
|
+
if (given.length !== tokenBuf.length || !crypto6.timingSafeEqual(given, tokenBuf)) return json(res, 403, { error: "forbidden" });
|
|
5315
|
+
if (req.headers.origin !== `http://${req.headers.host}`) return json(res, 403, { error: "forbidden origin" });
|
|
5316
|
+
const m = /^\/api\/run\/([^/]+)\/(stop|discard)$/.exec(pathname);
|
|
5317
|
+
if (!m) return json(res, 404, { error: "not found" });
|
|
5318
|
+
let meta;
|
|
5319
|
+
try {
|
|
5320
|
+
if (!RUN_ID3.test(m[1])) throw new Error("bad id");
|
|
5321
|
+
meta = freshMeta(m[1]);
|
|
5322
|
+
} catch {
|
|
5323
|
+
return json(res, 404, { error: "no such run" });
|
|
5324
|
+
}
|
|
5325
|
+
try {
|
|
5326
|
+
if (m[2] === "stop") {
|
|
5327
|
+
return stopRun(meta.id) ? json(res, 200, { ok: true, message: `stopping ${meta.id}` }) : json(res, 409, { error: "run is not active" });
|
|
5328
|
+
}
|
|
5329
|
+
if (!canDiscard(meta)) return json(res, 409, { error: "only a finished, not yet applied isolate run can be discarded" });
|
|
5330
|
+
return json(res, 200, { ok: true, message: discardRun(meta) });
|
|
5331
|
+
} catch (e) {
|
|
5332
|
+
return json(res, 409, { error: e.message });
|
|
5333
|
+
}
|
|
5334
|
+
};
|
|
5143
5335
|
return (req, res) => {
|
|
5144
5336
|
touch2();
|
|
5145
5337
|
if (!LOCAL_HOST.test(req.headers.host ?? "")) return json(res, 403, { error: "forbidden host" });
|
|
5146
|
-
if (req.method !== "GET" && req.method !== "HEAD") return json(res, 405, { error: "read-only" });
|
|
5147
5338
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
5148
|
-
if (
|
|
5339
|
+
if (req.method === "POST") return act(req, res, url.pathname);
|
|
5340
|
+
if (req.method !== "GET" && req.method !== "HEAD") return json(res, 405, { error: "read-only" });
|
|
5341
|
+
if (url.pathname === "/") return send(res, 200, "text/html; charset=utf-8", readAsset("/assets/app.js") ? page(token) : MISSING);
|
|
5149
5342
|
const type = ASSETS[url.pathname];
|
|
5150
5343
|
if (type) {
|
|
5151
5344
|
const data = readAsset(url.pathname);
|
|
@@ -5160,21 +5353,21 @@ function handler(touch2) {
|
|
|
5160
5353
|
}
|
|
5161
5354
|
if (url.pathname === "/api/history") {
|
|
5162
5355
|
const q = url.searchParams;
|
|
5163
|
-
const
|
|
5356
|
+
const days2 = Number(q.get("days") ?? 0);
|
|
5164
5357
|
return json(res, 200, listHistory({
|
|
5165
5358
|
text: (q.get("q") ?? "").slice(0, 200),
|
|
5166
5359
|
model: q.get("model") || void 0,
|
|
5167
5360
|
backend: q.get("backend") || void 0,
|
|
5168
5361
|
state: q.get("state") || void 0,
|
|
5169
5362
|
group: q.get("group") || void 0,
|
|
5170
|
-
sinceMs:
|
|
5363
|
+
sinceMs: days2 > 0 ? Date.now() - days2 * 864e5 : void 0,
|
|
5171
5364
|
beforeId: RUN_ID3.test(q.get("before") ?? "") ? q.get("before") : void 0,
|
|
5172
5365
|
limit: Number(q.get("limit") ?? 30) || 30
|
|
5173
5366
|
}));
|
|
5174
5367
|
}
|
|
5175
5368
|
if (url.pathname === "/api/stats") {
|
|
5176
|
-
const
|
|
5177
|
-
return json(res, 200, { ...historyStats(
|
|
5369
|
+
const days2 = Number(url.searchParams.get("days") ?? 30);
|
|
5370
|
+
return json(res, 200, { ...historyStats(days2 > 0 ? Date.now() - days2 * 864e5 : void 0), price: primaryPrice().name });
|
|
5178
5371
|
}
|
|
5179
5372
|
const m = /^\/api\/run\/([^/]+)$/.exec(url.pathname);
|
|
5180
5373
|
if (m) {
|
|
@@ -5186,28 +5379,28 @@ function handler(touch2) {
|
|
|
5186
5379
|
}
|
|
5187
5380
|
function startDash(opts) {
|
|
5188
5381
|
let lastRequest = Date.now();
|
|
5189
|
-
const server = http.createServer(handler(() => lastRequest = Date.now()));
|
|
5382
|
+
const server = http.createServer(handler(() => lastRequest = Date.now(), crypto6.randomBytes(24).toString("hex")));
|
|
5190
5383
|
return new Promise((resolve2, reject) => {
|
|
5191
5384
|
server.once("error", reject);
|
|
5192
5385
|
server.listen(opts.port, "127.0.0.1", () => {
|
|
5193
5386
|
const port = server.address().port;
|
|
5194
5387
|
let done;
|
|
5195
5388
|
const closed = new Promise((r) => done = r);
|
|
5196
|
-
let
|
|
5389
|
+
let timer2;
|
|
5197
5390
|
const close = () => {
|
|
5198
|
-
if (
|
|
5391
|
+
if (timer2) clearInterval(timer2);
|
|
5199
5392
|
server.closeAllConnections?.();
|
|
5200
5393
|
return new Promise((r) => server.close(() => (done(), r())));
|
|
5201
5394
|
};
|
|
5202
5395
|
if (opts.idleMs && opts.idleMs > 0) {
|
|
5203
|
-
|
|
5204
|
-
|
|
5396
|
+
timer2 = setInterval(() => Date.now() - lastRequest > opts.idleMs && void close(), Math.min(6e4, opts.idleMs));
|
|
5397
|
+
timer2.unref();
|
|
5205
5398
|
}
|
|
5206
5399
|
resolve2({ port, url: `http://127.0.0.1:${port}/`, closed, close });
|
|
5207
5400
|
});
|
|
5208
5401
|
});
|
|
5209
5402
|
}
|
|
5210
|
-
var registryFile = () =>
|
|
5403
|
+
var registryFile = () => path22.join(home(), "dash.json");
|
|
5211
5404
|
async function ping(port) {
|
|
5212
5405
|
try {
|
|
5213
5406
|
const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
|
|
@@ -5218,11 +5411,11 @@ async function ping(port) {
|
|
|
5218
5411
|
}
|
|
5219
5412
|
async function runningDash() {
|
|
5220
5413
|
try {
|
|
5221
|
-
const r = JSON.parse(
|
|
5414
|
+
const r = JSON.parse(fs27.readFileSync(registryFile(), "utf8"));
|
|
5222
5415
|
if (isAlive(r.pid) && await ping(r.port)) return { ...r, url: `http://127.0.0.1:${r.port}/` };
|
|
5223
5416
|
} catch {
|
|
5224
5417
|
}
|
|
5225
|
-
|
|
5418
|
+
fs27.rmSync(registryFile(), { force: true });
|
|
5226
5419
|
return void 0;
|
|
5227
5420
|
}
|
|
5228
5421
|
function openBrowser(url) {
|
|
@@ -5237,7 +5430,7 @@ async function dashCommand(o) {
|
|
|
5237
5430
|
if (o.stop) {
|
|
5238
5431
|
if (!existing) throw new UserError("pitroom dash is not running");
|
|
5239
5432
|
process.kill(existing.pid, "SIGTERM");
|
|
5240
|
-
|
|
5433
|
+
fs27.rmSync(registryFile(), { force: true });
|
|
5241
5434
|
o.log(`stopped pitroom dash (${existing.url})`);
|
|
5242
5435
|
return 0;
|
|
5243
5436
|
}
|
|
@@ -5254,7 +5447,7 @@ async function dashCommand(o) {
|
|
|
5254
5447
|
});
|
|
5255
5448
|
child.on("error", () => void 0);
|
|
5256
5449
|
child.unref();
|
|
5257
|
-
for (let i = 0; i <
|
|
5450
|
+
for (let i = 0; i < 200; i++) {
|
|
5258
5451
|
await new Promise((r2) => setTimeout(r2, 100));
|
|
5259
5452
|
const r = await runningDash();
|
|
5260
5453
|
if (r && r.pid === child.pid) {
|
|
@@ -5274,8 +5467,8 @@ async function dashCommand(o) {
|
|
|
5274
5467
|
if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
|
|
5275
5468
|
dash = await startDash({ port: 0, idleMs: o.idleMs });
|
|
5276
5469
|
}
|
|
5277
|
-
|
|
5278
|
-
|
|
5470
|
+
fs27.mkdirSync(home(), { recursive: true });
|
|
5471
|
+
fs27.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
|
|
5279
5472
|
const stop = () => void dash.close();
|
|
5280
5473
|
process.once("SIGINT", stop);
|
|
5281
5474
|
process.once("SIGTERM", stop);
|
|
@@ -5284,7 +5477,7 @@ async function dashCommand(o) {
|
|
|
5284
5477
|
if (o.open) openBrowser(dash.url);
|
|
5285
5478
|
await dash.closed;
|
|
5286
5479
|
try {
|
|
5287
|
-
if (JSON.parse(
|
|
5480
|
+
if (JSON.parse(fs27.readFileSync(registryFile(), "utf8")).pid === process.pid) fs27.rmSync(registryFile(), { force: true });
|
|
5288
5481
|
} catch {
|
|
5289
5482
|
}
|
|
5290
5483
|
return 0;
|
|
@@ -5296,10 +5489,10 @@ var MAX_BODY = 4e6;
|
|
|
5296
5489
|
var MAX_SESSIONS = 64;
|
|
5297
5490
|
var IDLE_MS = 60 * 6e4;
|
|
5298
5491
|
var PATH = "/mcp";
|
|
5299
|
-
var tokenFile = () =>
|
|
5492
|
+
var tokenFile = () => path23.join(home(), "mcp-token");
|
|
5300
5493
|
var readToken = (file2) => {
|
|
5301
5494
|
try {
|
|
5302
|
-
const kept =
|
|
5495
|
+
const kept = fs28.readFileSync(file2, "utf8").trim();
|
|
5303
5496
|
return kept.length >= 16 ? kept : void 0;
|
|
5304
5497
|
} catch {
|
|
5305
5498
|
return void 0;
|
|
@@ -5314,10 +5507,10 @@ function mcpToken() {
|
|
|
5314
5507
|
const file2 = tokenFile();
|
|
5315
5508
|
let token = readToken(file2);
|
|
5316
5509
|
if (!token) {
|
|
5317
|
-
|
|
5510
|
+
fs28.mkdirSync(path23.dirname(file2), { recursive: true });
|
|
5318
5511
|
token = crypto7.randomBytes(32).toString("hex");
|
|
5319
5512
|
try {
|
|
5320
|
-
|
|
5513
|
+
fs28.writeFileSync(file2, `${token}
|
|
5321
5514
|
`, { mode: 384, flag: "wx" });
|
|
5322
5515
|
} catch (e) {
|
|
5323
5516
|
if (e.code !== "EEXIST") throw e;
|
|
@@ -5326,7 +5519,7 @@ function mcpToken() {
|
|
|
5326
5519
|
}
|
|
5327
5520
|
}
|
|
5328
5521
|
try {
|
|
5329
|
-
|
|
5522
|
+
fs28.chmodSync(file2, 384);
|
|
5330
5523
|
} catch {
|
|
5331
5524
|
}
|
|
5332
5525
|
return { token, from: file2 };
|
|
@@ -5337,6 +5530,15 @@ var sameSecret = (given, token) => {
|
|
|
5337
5530
|
const b = crypto7.createHash("sha256").update(token).digest();
|
|
5338
5531
|
return crypto7.timingSafeEqual(a, b);
|
|
5339
5532
|
};
|
|
5533
|
+
var idleSince = (k) => Math.max(k.lastSeen, k.closeStream ? k.streamSeen ?? 0 : 0);
|
|
5534
|
+
function drop(sessions, id) {
|
|
5535
|
+
const k = sessions.get(id);
|
|
5536
|
+
if (!k) return;
|
|
5537
|
+
k.closeStream?.();
|
|
5538
|
+
endSession(k.session);
|
|
5539
|
+
sessions.delete(id);
|
|
5540
|
+
}
|
|
5541
|
+
var PING_MS = 25e3;
|
|
5340
5542
|
function reply(res, status, body, headers = {}) {
|
|
5341
5543
|
if (res.headersSent) return void res.end();
|
|
5342
5544
|
const text = body === void 0 ? "" : JSON.stringify(body);
|
|
@@ -5362,9 +5564,9 @@ var wantsStream = (parsed) => {
|
|
|
5362
5564
|
};
|
|
5363
5565
|
function evictIdle(sessions) {
|
|
5364
5566
|
let oldest;
|
|
5365
|
-
for (const entry of sessions) if (!entry[1].session.inflight.size && (!oldest || entry[1]
|
|
5567
|
+
for (const entry of sessions) if (!entry[1].session.inflight.size && (!oldest || idleSince(entry[1]) < idleSince(oldest[1]))) oldest = entry;
|
|
5366
5568
|
if (!oldest) return false;
|
|
5367
|
-
sessions
|
|
5569
|
+
drop(sessions, oldest[0]);
|
|
5368
5570
|
return true;
|
|
5369
5571
|
}
|
|
5370
5572
|
function makeHandler(token, sessions) {
|
|
@@ -5378,13 +5580,44 @@ function makeHandler(token, sessions) {
|
|
|
5378
5580
|
if (!auth || !sameSecret(auth, token)) return reply(res, 401, failure5(-32001, "a bearer token is needed"), { "WWW-Authenticate": "Bearer" });
|
|
5379
5581
|
const sid = typeof req.headers["mcp-session-id"] === "string" ? req.headers["mcp-session-id"] : void 0;
|
|
5380
5582
|
if (req.method === "DELETE") {
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
endSession(kept2.session);
|
|
5384
|
-
sessions.delete(sid);
|
|
5583
|
+
if (!sid || !sessions.has(sid)) return reply(res, 404, failure5(-32600, "no such session"));
|
|
5584
|
+
drop(sessions, sid);
|
|
5385
5585
|
return reply(res, 204);
|
|
5386
5586
|
}
|
|
5387
|
-
if (req.method
|
|
5587
|
+
if (req.method === "GET") {
|
|
5588
|
+
if (!/text\/event-stream|\*\/\*/.test(req.headers.accept ?? "")) return reply(res, 406, failure5(-32600, "a GET opens the event stream: send Accept: text/event-stream"));
|
|
5589
|
+
if (!sid) return reply(res, 400, failure5(-32600, "Mcp-Session-Id is missing: initialize first"));
|
|
5590
|
+
const kept2 = sessions.get(sid);
|
|
5591
|
+
if (!kept2) return reply(res, 404, failure5(-32600, "no such session: initialize again"));
|
|
5592
|
+
kept2.closeStream?.();
|
|
5593
|
+
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" });
|
|
5594
|
+
res.write(": open\n\n");
|
|
5595
|
+
const open = () => !res.writableEnded && !res.destroyed;
|
|
5596
|
+
const push = (msg) => {
|
|
5597
|
+
if (!open()) return;
|
|
5598
|
+
kept2.streamSeen = Date.now();
|
|
5599
|
+
res.write(`event: message
|
|
5600
|
+
data: ${JSON.stringify(msg)}
|
|
5601
|
+
|
|
5602
|
+
`);
|
|
5603
|
+
};
|
|
5604
|
+
const ping2 = setInterval(() => open() && res.write(": ping\n\n"), PING_MS);
|
|
5605
|
+
ping2.unref();
|
|
5606
|
+
res.on("error", () => {
|
|
5607
|
+
});
|
|
5608
|
+
kept2.streamSeen = Date.now();
|
|
5609
|
+
const close = () => {
|
|
5610
|
+
clearInterval(ping2);
|
|
5611
|
+
if (kept2.session.push === push) kept2.session.push = void 0;
|
|
5612
|
+
if (kept2.closeStream === close) kept2.closeStream = void 0;
|
|
5613
|
+
if (!res.writableEnded) res.end();
|
|
5614
|
+
};
|
|
5615
|
+
kept2.session.push = push;
|
|
5616
|
+
kept2.closeStream = close;
|
|
5617
|
+
res.on("close", close);
|
|
5618
|
+
return;
|
|
5619
|
+
}
|
|
5620
|
+
if (req.method !== "POST") return reply(res, 405, failure5(-32600, "GET, POST or DELETE"), { Allow: "GET, POST, DELETE" });
|
|
5388
5621
|
const body = await readBody(req);
|
|
5389
5622
|
if (body === "too-big") return reply(res, 413, failure5(-32600, `a request body is at most ${MAX_BODY} bytes`));
|
|
5390
5623
|
let parsed;
|
|
@@ -5397,12 +5630,8 @@ function makeHandler(token, sessions) {
|
|
|
5397
5630
|
let kept;
|
|
5398
5631
|
const single = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
5399
5632
|
if (single?.method === "initialize") {
|
|
5400
|
-
|
|
5401
|
-
if (
|
|
5402
|
-
endSession(old.session);
|
|
5403
|
-
sessions.delete(sid);
|
|
5404
|
-
}
|
|
5405
|
-
if (sessions.size >= MAX_SESSIONS && !evictIdle(sessions)) return reply(res, 503, failure5(-32e3, "too many sessions with requests in flight"));
|
|
5633
|
+
if (sid) drop(sessions, sid);
|
|
5634
|
+
if (sessions.size >= MAX_SESSIONS && !evictIdle(sessions)) return reply(res, 503, failure5(-32e3, `${MAX_SESSIONS} sessions are busy with requests: try again when one ends`));
|
|
5406
5635
|
const id = crypto7.randomUUID();
|
|
5407
5636
|
kept = { session: newSession(), lastSeen: Date.now() };
|
|
5408
5637
|
sessions.set(id, kept);
|
|
@@ -5444,7 +5673,7 @@ async function serveMcpHttp(opts) {
|
|
|
5444
5673
|
});
|
|
5445
5674
|
});
|
|
5446
5675
|
const sweep = setInterval(() => {
|
|
5447
|
-
for (const [id, k] of sessions) if (Date.now() - k
|
|
5676
|
+
for (const [id, k] of [...sessions]) if (Date.now() - idleSince(k) > IDLE_MS && !k.session.inflight.size) drop(sessions, id);
|
|
5448
5677
|
}, 6e4);
|
|
5449
5678
|
sweep.unref();
|
|
5450
5679
|
const port = await new Promise((resolve2, reject) => {
|
|
@@ -5464,7 +5693,7 @@ async function serveMcpHttp(opts) {
|
|
|
5464
5693
|
process.once("SIGTERM", stop);
|
|
5465
5694
|
});
|
|
5466
5695
|
clearInterval(sweep);
|
|
5467
|
-
for (const
|
|
5696
|
+
for (const id of [...sessions.keys()]) drop(sessions, id);
|
|
5468
5697
|
server.closeAllConnections();
|
|
5469
5698
|
await new Promise((resolve2) => server.close(() => resolve2()));
|
|
5470
5699
|
return 0;
|
|
@@ -5472,16 +5701,16 @@ async function serveMcpHttp(opts) {
|
|
|
5472
5701
|
|
|
5473
5702
|
// src/cli/commands.ts
|
|
5474
5703
|
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
5475
|
-
import
|
|
5476
|
-
import
|
|
5704
|
+
import fs34 from "node:fs";
|
|
5705
|
+
import path29 from "node:path";
|
|
5477
5706
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
5478
5707
|
|
|
5479
5708
|
// src/core/init.ts
|
|
5480
|
-
import
|
|
5481
|
-
import
|
|
5709
|
+
import fs29 from "node:fs";
|
|
5710
|
+
import path24 from "node:path";
|
|
5482
5711
|
var FREE = /-free$/;
|
|
5483
5712
|
var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
|
|
5484
|
-
var isFound = (binary5) =>
|
|
5713
|
+
var isFound = (binary5) => path24.isAbsolute(binary5) && fs29.existsSync(binary5);
|
|
5485
5714
|
function planInit(opts = {}) {
|
|
5486
5715
|
const file2 = configPath();
|
|
5487
5716
|
const workers = backendIds().map((id) => {
|
|
@@ -5527,15 +5756,15 @@ function planInit(opts = {}) {
|
|
|
5527
5756
|
if (Object.keys(tiers).length > 1) config.tiers = tiers;
|
|
5528
5757
|
if (!workers.some((w) => w.found)) blocked = "no worker CLI found: install OpenCode (https://opencode.ai), Codex CLI, Claude Code or Gemini CLI first";
|
|
5529
5758
|
else if (!blocked && !Object.keys(config).length) notes.push("nothing to propose: the default model and the single worker need no config");
|
|
5530
|
-
return { path: file2, exists:
|
|
5759
|
+
return { path: file2, exists: fs29.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
|
|
5531
5760
|
}
|
|
5532
5761
|
function writeInit(plan, force) {
|
|
5533
5762
|
if (plan.blocked) throw new UserError(plan.blocked);
|
|
5534
5763
|
if (!Object.keys(plan.config).length) throw new UserError("nothing to write: the proposal is empty");
|
|
5535
|
-
if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${
|
|
5536
|
-
|
|
5537
|
-
if (plan.exists)
|
|
5538
|
-
|
|
5764
|
+
if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${path24.basename(plan.path)}.bak)`);
|
|
5765
|
+
fs29.mkdirSync(path24.dirname(plan.path), { recursive: true });
|
|
5766
|
+
if (plan.exists) fs29.copyFileSync(plan.path, `${plan.path}.bak`);
|
|
5767
|
+
fs29.writeFileSync(plan.path, `${JSON.stringify(plan.config, null, 2)}
|
|
5539
5768
|
`);
|
|
5540
5769
|
return plan.path;
|
|
5541
5770
|
}
|
|
@@ -5556,17 +5785,17 @@ function formatInit(plan, written) {
|
|
|
5556
5785
|
|
|
5557
5786
|
// src/core/mcp-install.ts
|
|
5558
5787
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
5559
|
-
import
|
|
5788
|
+
import fs30 from "node:fs";
|
|
5560
5789
|
import os10 from "node:os";
|
|
5561
|
-
import
|
|
5790
|
+
import path25 from "node:path";
|
|
5562
5791
|
var CLIENT_IDS = ["claude-code", "codex", "gemini", "cursor", "claude-desktop"];
|
|
5563
5792
|
var NAME = "pitroom";
|
|
5564
5793
|
function mcpCommand() {
|
|
5565
5794
|
const launcher = launcherPath();
|
|
5566
5795
|
if (isPitroomLauncher(launcher)) return { command: launcher, args: ["mcp"] };
|
|
5567
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
5568
|
-
const p =
|
|
5569
|
-
if (
|
|
5796
|
+
for (const dir of (process.env.PATH ?? "").split(path25.delimiter).filter(Boolean)) {
|
|
5797
|
+
const p = path25.join(dir, process.platform === "win32" ? "pitroom.cmd" : "pitroom");
|
|
5798
|
+
if (fs30.existsSync(p)) return { command: path25.resolve(p), args: ["mcp"] };
|
|
5570
5799
|
}
|
|
5571
5800
|
return { command: process.execPath, args: [process.argv[1] ?? "pitroom", "mcp"] };
|
|
5572
5801
|
}
|
|
@@ -5582,7 +5811,7 @@ function run(backendId, args) {
|
|
|
5582
5811
|
var runnable = (backendId) => run(backendId, ["--version"]).ok;
|
|
5583
5812
|
var readJson = (file2) => {
|
|
5584
5813
|
try {
|
|
5585
|
-
return JSON.parse(
|
|
5814
|
+
return JSON.parse(fs30.readFileSync(file2, "utf8"));
|
|
5586
5815
|
} catch {
|
|
5587
5816
|
return void 0;
|
|
5588
5817
|
}
|
|
@@ -5592,7 +5821,7 @@ var jsonState = (file2, c) => {
|
|
|
5592
5821
|
return entry === void 0 ? "absent" : same(c, entry) ? "same" : "different";
|
|
5593
5822
|
};
|
|
5594
5823
|
var home2 = () => os10.homedir();
|
|
5595
|
-
var geminiDir = () =>
|
|
5824
|
+
var geminiDir = () => path25.join(process.env.GEMINI_CLI_HOME ?? home2(), ".gemini");
|
|
5596
5825
|
function cliClient(id, backend, name, where, o) {
|
|
5597
5826
|
return {
|
|
5598
5827
|
id,
|
|
@@ -5615,43 +5844,43 @@ function jsonClient(id, name, file2, dir) {
|
|
|
5615
5844
|
return {
|
|
5616
5845
|
id,
|
|
5617
5846
|
name,
|
|
5618
|
-
found: () =>
|
|
5847
|
+
found: () => fs30.existsSync(dir()),
|
|
5619
5848
|
state: (c) => jsonState(file2(), c),
|
|
5620
5849
|
add: (c) => {
|
|
5621
5850
|
let config = {};
|
|
5622
|
-
if (
|
|
5851
|
+
if (fs30.existsSync(file2())) {
|
|
5623
5852
|
config = readJson(file2());
|
|
5624
5853
|
if (!config || typeof config !== "object" || Array.isArray(config)) return { ok: false, message: `${rel()} is not valid JSON: left alone` };
|
|
5625
5854
|
const backup = `${file2()}.bak-pitroom`;
|
|
5626
|
-
if (!
|
|
5855
|
+
if (!fs30.existsSync(backup)) fs30.copyFileSync(file2(), backup);
|
|
5627
5856
|
}
|
|
5628
5857
|
config.mcpServers = { ...config.mcpServers ?? {}, [NAME]: { command: c.command, args: c.args } };
|
|
5629
|
-
|
|
5858
|
+
fs30.mkdirSync(path25.dirname(file2()), { recursive: true });
|
|
5630
5859
|
const tmp = `${file2()}.${process.pid}.tmp`;
|
|
5631
|
-
|
|
5860
|
+
fs30.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
|
|
5632
5861
|
`);
|
|
5633
|
-
|
|
5862
|
+
fs30.renameSync(tmp, file2());
|
|
5634
5863
|
return { ok: true, message: rel() };
|
|
5635
5864
|
},
|
|
5636
5865
|
remove: () => {
|
|
5637
5866
|
const config = readJson(file2());
|
|
5638
5867
|
if (!config?.mcpServers?.[NAME]) return { ok: true, message: rel() };
|
|
5639
5868
|
delete config.mcpServers[NAME];
|
|
5640
|
-
|
|
5869
|
+
fs30.writeFileSync(file2(), `${JSON.stringify(config, null, 2)}
|
|
5641
5870
|
`);
|
|
5642
5871
|
return { ok: true, message: rel() };
|
|
5643
5872
|
}
|
|
5644
5873
|
};
|
|
5645
5874
|
}
|
|
5646
5875
|
function desktopFile() {
|
|
5647
|
-
if (process.platform === "darwin") return
|
|
5648
|
-
if (process.platform === "win32") return
|
|
5649
|
-
return
|
|
5876
|
+
if (process.platform === "darwin") return path25.join(home2(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
5877
|
+
if (process.platform === "win32") return path25.join(process.env.APPDATA ?? path25.join(home2(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
5878
|
+
return path25.join(process.env.XDG_CONFIG_HOME ?? path25.join(home2(), ".config"), "Claude", "claude_desktop_config.json");
|
|
5650
5879
|
}
|
|
5651
5880
|
function codexState(c) {
|
|
5652
5881
|
let text;
|
|
5653
5882
|
try {
|
|
5654
|
-
text =
|
|
5883
|
+
text = fs30.readFileSync(path25.join(process.env.CODEX_HOME ?? path25.join(home2(), ".codex"), "config.toml"), "utf8");
|
|
5655
5884
|
} catch {
|
|
5656
5885
|
return "absent";
|
|
5657
5886
|
}
|
|
@@ -5669,7 +5898,7 @@ function codexState(c) {
|
|
|
5669
5898
|
function clients() {
|
|
5670
5899
|
return [
|
|
5671
5900
|
cliClient("claude-code", "claude", "Claude Code", "user settings (~/.claude.json)", {
|
|
5672
|
-
state: (c) => jsonState(
|
|
5901
|
+
state: (c) => jsonState(path25.join(home2(), ".claude.json"), c),
|
|
5673
5902
|
add: (c) => ["mcp", "add", "--scope", "user", NAME, "--", c.command, ...c.args],
|
|
5674
5903
|
remove: () => ["mcp", "remove", NAME, "--scope", "user"]
|
|
5675
5904
|
}),
|
|
@@ -5679,12 +5908,12 @@ function clients() {
|
|
|
5679
5908
|
remove: () => ["mcp", "remove", NAME]
|
|
5680
5909
|
}),
|
|
5681
5910
|
cliClient("gemini", "gemini", "Gemini CLI", "user settings (~/.gemini/settings.json)", {
|
|
5682
|
-
state: (c) => jsonState(
|
|
5911
|
+
state: (c) => jsonState(path25.join(geminiDir(), "settings.json"), c),
|
|
5683
5912
|
add: (c) => ["mcp", "add", "--scope", "user", NAME, c.command, ...c.args],
|
|
5684
5913
|
remove: () => ["mcp", "remove", "--scope", "user", NAME]
|
|
5685
5914
|
}),
|
|
5686
|
-
jsonClient("cursor", "Cursor", () =>
|
|
5687
|
-
jsonClient("claude-desktop", "Claude Desktop", desktopFile, () =>
|
|
5915
|
+
jsonClient("cursor", "Cursor", () => path25.join(home2(), ".cursor", "mcp.json"), () => path25.join(home2(), ".cursor")),
|
|
5916
|
+
jsonClient("claude-desktop", "Claude Desktop", desktopFile, () => path25.dirname(desktopFile()))
|
|
5688
5917
|
];
|
|
5689
5918
|
}
|
|
5690
5919
|
function attempt2(f) {
|
|
@@ -5755,16 +5984,16 @@ var parseClients = (value) => {
|
|
|
5755
5984
|
|
|
5756
5985
|
// src/core/plan-status.ts
|
|
5757
5986
|
import crypto8 from "node:crypto";
|
|
5758
|
-
import
|
|
5759
|
-
import
|
|
5987
|
+
import fs31 from "node:fs";
|
|
5988
|
+
import path26 from "node:path";
|
|
5760
5989
|
function notesFile(plan) {
|
|
5761
5990
|
const id = crypto8.createHash("sha1").update(plan.file).digest("hex").slice(0, 12);
|
|
5762
|
-
return
|
|
5991
|
+
return path26.join(home(), "plans", id, "notes.md");
|
|
5763
5992
|
}
|
|
5764
5993
|
function readNotes(plan) {
|
|
5765
5994
|
const f = notesFile(plan);
|
|
5766
|
-
if (!
|
|
5767
|
-
return
|
|
5995
|
+
if (!fs31.existsSync(f)) return [];
|
|
5996
|
+
return fs31.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
|
|
5768
5997
|
const m = /^(\d{4}-\d\d-\d\d \d\d:\d\d) (.*)$/.exec(l);
|
|
5769
5998
|
return m ? { at: m[1], text: m[2] } : { at: "", text: l };
|
|
5770
5999
|
});
|
|
@@ -5772,11 +6001,11 @@ function readNotes(plan) {
|
|
|
5772
6001
|
function addNote(planFile, text) {
|
|
5773
6002
|
const plan = loadPlan(planFile);
|
|
5774
6003
|
const f = notesFile(plan);
|
|
5775
|
-
|
|
5776
|
-
if (!
|
|
6004
|
+
fs31.mkdirSync(path26.dirname(f), { recursive: true });
|
|
6005
|
+
if (!fs31.existsSync(f)) fs31.writeFileSync(f, `# plan: ${plan.file}
|
|
5777
6006
|
`);
|
|
5778
6007
|
const at = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
|
|
5779
|
-
|
|
6008
|
+
fs31.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
|
|
5780
6009
|
`);
|
|
5781
6010
|
return f;
|
|
5782
6011
|
}
|
|
@@ -5842,10 +6071,73 @@ ${rulings}`,
|
|
|
5842
6071
|
].join("\n");
|
|
5843
6072
|
}
|
|
5844
6073
|
|
|
5845
|
-
// src/core/
|
|
6074
|
+
// src/core/cache.ts
|
|
5846
6075
|
import crypto9 from "node:crypto";
|
|
5847
|
-
import
|
|
5848
|
-
import
|
|
6076
|
+
import fs32 from "node:fs";
|
|
6077
|
+
import path27 from "node:path";
|
|
6078
|
+
var SCAN2 = 500;
|
|
6079
|
+
function cacheKey(o) {
|
|
6080
|
+
if (o.verify || o.mode !== "read" || o.continueFrom || o.web || o.plan || o.review || o.audit || o.auditRate === 1) return void 0;
|
|
6081
|
+
if (!(effective().cacheDays.value > 0)) return void 0;
|
|
6082
|
+
const dir = canonical(path27.resolve(o.dir));
|
|
6083
|
+
const root = repoRoot(dir);
|
|
6084
|
+
if (!root) return void 0;
|
|
6085
|
+
try {
|
|
6086
|
+
const tree = snapshotTree(root);
|
|
6087
|
+
const head = commitOf(root, "HEAD") ?? "";
|
|
6088
|
+
const files = o.files.map((f) => {
|
|
6089
|
+
const abs = path27.resolve(f);
|
|
6090
|
+
return [abs, crypto9.createHash("sha256").update(fs32.readFileSync(abs)).digest("hex")];
|
|
6091
|
+
});
|
|
6092
|
+
const question = o.task.replace(/\s+/g, " ").trim();
|
|
6093
|
+
const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
|
|
6094
|
+
const where = !o.inPlace && wantSnapshot(readIn2, root, dir) ? "snapshot" : "project";
|
|
6095
|
+
const key = crypto9.createHash("sha256").update(JSON.stringify([question, path27.relative(root, dir), files, where])).digest("hex");
|
|
6096
|
+
return { key, state: `${head}:${tree}` };
|
|
6097
|
+
} catch {
|
|
6098
|
+
return void 0;
|
|
6099
|
+
}
|
|
6100
|
+
}
|
|
6101
|
+
function heldUp(m) {
|
|
6102
|
+
if (m.state !== "done" || m.mode !== "read" || m.reviewOf || m.auditOf) return false;
|
|
6103
|
+
if (m.refs && m.refs.invalid.length) return false;
|
|
6104
|
+
if (m.warnings.some((w) => w.startsWith("READ-ONLY VIOLATION"))) return false;
|
|
6105
|
+
if (m.audit && (isActive(m.audit.state) || m.audit.verdict === "disagree" || m.audit.verdict === "partial")) return false;
|
|
6106
|
+
try {
|
|
6107
|
+
return readSummary(m).trim() !== "";
|
|
6108
|
+
} catch {
|
|
6109
|
+
return false;
|
|
6110
|
+
}
|
|
6111
|
+
}
|
|
6112
|
+
function findCached(k, now = Date.now()) {
|
|
6113
|
+
const maxAge = effective().cacheDays.value * 864e5;
|
|
6114
|
+
let best;
|
|
6115
|
+
for (const id of listRunIds().slice(-SCAN2)) {
|
|
6116
|
+
let m;
|
|
6117
|
+
try {
|
|
6118
|
+
m = readMeta(id);
|
|
6119
|
+
} catch {
|
|
6120
|
+
continue;
|
|
6121
|
+
}
|
|
6122
|
+
if (m.cache?.key !== k.key || m.cache.state !== k.state) continue;
|
|
6123
|
+
const at = Date.parse(m.endedAt ?? m.startedAt);
|
|
6124
|
+
if (now - at > maxAge || best && at <= best.at || !heldUp(m)) continue;
|
|
6125
|
+
best = { m, at };
|
|
6126
|
+
}
|
|
6127
|
+
return best?.m;
|
|
6128
|
+
}
|
|
6129
|
+
function ago(iso, now = Date.now()) {
|
|
6130
|
+
const s = Math.max(0, (now - Date.parse(iso)) / 1e3);
|
|
6131
|
+
if (s < 3600) return `${Math.max(1, Math.round(s / 60))} min ago`;
|
|
6132
|
+
if (s < 86400) return `${Math.round(s / 3600)} h ago`;
|
|
6133
|
+
return `${Math.round(s / 86400)} d ago`;
|
|
6134
|
+
}
|
|
6135
|
+
var cachedNote = (m) => `pitroom \u27F2 cached answer \xB7 the same question on the same code as run ${m.id} (${ago(m.endedAt ?? m.startedAt)}) \xB7 no worker ran \xB7 --fresh asks one again`;
|
|
6136
|
+
|
|
6137
|
+
// src/core/review.ts
|
|
6138
|
+
import crypto10 from "node:crypto";
|
|
6139
|
+
import fs33 from "node:fs";
|
|
6140
|
+
import path28 from "node:path";
|
|
5849
6141
|
var TEMPLATE2 = { task: "task-reviewer", fix: "re-review", range: "code-reviewer" };
|
|
5850
6142
|
var section = (title, body) => `## ${title}
|
|
5851
6143
|
|
|
@@ -5863,7 +6155,7 @@ function ancestors(m) {
|
|
|
5863
6155
|
function briefOf(m) {
|
|
5864
6156
|
const root = [m, ...ancestors(m)].at(-1);
|
|
5865
6157
|
const f = runFile(root.id, "brief.md");
|
|
5866
|
-
return
|
|
6158
|
+
return fs33.existsSync(f) ? fs33.readFileSync(f, "utf8") : root.task;
|
|
5867
6159
|
}
|
|
5868
6160
|
function latestReview(ids) {
|
|
5869
6161
|
for (const id of listRunIds().reverse()) {
|
|
@@ -5877,7 +6169,7 @@ function latestReview(ids) {
|
|
|
5877
6169
|
}
|
|
5878
6170
|
function diffOf(m, from = m.baseTree) {
|
|
5879
6171
|
const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
|
|
5880
|
-
if (where &&
|
|
6172
|
+
if (where && fs33.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
|
|
5881
6173
|
if (from !== m.baseTree) {
|
|
5882
6174
|
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);
|
|
5883
6175
|
}
|
|
@@ -5891,7 +6183,7 @@ function runReview(id) {
|
|
|
5891
6183
|
if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
|
|
5892
6184
|
const chain = ancestors(m);
|
|
5893
6185
|
const previous = latestReview(chain.map((a) => a.id));
|
|
5894
|
-
const dir = m.mode === "isolate" && m.worktree &&
|
|
6186
|
+
const dir = m.mode === "isolate" && m.worktree && fs33.existsSync(m.worktree) ? m.cwd : m.dir;
|
|
5895
6187
|
const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
|
|
5896
6188
|
const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
|
|
5897
6189
|
if (!previous) {
|
|
@@ -5964,12 +6256,25 @@ function pickReviewer(job) {
|
|
|
5964
6256
|
}
|
|
5965
6257
|
function writePackage(job) {
|
|
5966
6258
|
const g = gitDir(job.dir);
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
6259
|
+
const root = repoRoot(job.dir);
|
|
6260
|
+
if (!g || !root) throw new UserError(`not a git repository: ${job.dir}`);
|
|
6261
|
+
const name = `review-${crypto10.randomBytes(4).toString("hex")}.md`;
|
|
6262
|
+
const inside2 = !path28.relative(fs33.realpathSync(root), fs33.realpathSync(g)).startsWith("..");
|
|
6263
|
+
const file2 = inside2 ? path28.join(g, "pitroom", name) : path28.join(root, ".pitroom", name);
|
|
6264
|
+
if (!inside2) ignoreInGit(root, "/.pitroom/");
|
|
6265
|
+
fs33.mkdirSync(path28.dirname(file2), { recursive: true });
|
|
6266
|
+
fs33.writeFileSync(file2, job.package);
|
|
5971
6267
|
return file2;
|
|
5972
6268
|
}
|
|
6269
|
+
function ignoreInGit(root, pattern) {
|
|
6270
|
+
const exclude = path28.resolve(root, git(root, ["rev-parse", "--git-path", "info/exclude"]).stdout.trim());
|
|
6271
|
+
const text = fs33.existsSync(exclude) ? fs33.readFileSync(exclude, "utf8") : "";
|
|
6272
|
+
if (text.split(/\r?\n/).includes(pattern)) return;
|
|
6273
|
+
fs33.mkdirSync(path28.dirname(exclude), { recursive: true });
|
|
6274
|
+
fs33.appendFileSync(exclude, `${text && !text.endsWith("\n") ? "\n" : ""}# Pitroom review packages in worktrees (removed after each review)
|
|
6275
|
+
${pattern}
|
|
6276
|
+
`);
|
|
6277
|
+
}
|
|
5973
6278
|
|
|
5974
6279
|
// src/core/models.ts
|
|
5975
6280
|
var base = (model) => (model ?? "").split("#")[0];
|
|
@@ -6086,7 +6391,15 @@ warning: ${w}`).join("")
|
|
|
6086
6391
|
async function cmdRun(p) {
|
|
6087
6392
|
const opts = { ...runOptions(p, readTask(p)), plan: planStep(p) };
|
|
6088
6393
|
if (!opts.task.trim() && !opts.plan) throw new UserError('no task given (pitroom "find where X is handled")');
|
|
6089
|
-
|
|
6394
|
+
const cache = cacheKey(opts);
|
|
6395
|
+
const hit = cache && !has(p, "fresh") ? findCached(cache) : void 0;
|
|
6396
|
+
if (hit) {
|
|
6397
|
+
console.log(has(p, "json") ? JSON.stringify({ ...hit, cached: true }, null, 2) : `${cachedNote(hit)}
|
|
6398
|
+
|
|
6399
|
+
${formatReport(hit)}`);
|
|
6400
|
+
return exitCodeFor(hit);
|
|
6401
|
+
}
|
|
6402
|
+
return launch(p, prepareRun({ ...opts, cache }));
|
|
6090
6403
|
}
|
|
6091
6404
|
async function cmdReview(p) {
|
|
6092
6405
|
const range = flag(p, "range");
|
|
@@ -6114,10 +6427,10 @@ async function cmdReview(p) {
|
|
|
6114
6427
|
writeMeta(meta);
|
|
6115
6428
|
}
|
|
6116
6429
|
} catch (e) {
|
|
6117
|
-
|
|
6430
|
+
fs34.rmSync(packageFile, { force: true });
|
|
6118
6431
|
throw e;
|
|
6119
6432
|
}
|
|
6120
|
-
|
|
6433
|
+
fs34.writeFileSync(runFile(meta.id, "package.md"), job.package);
|
|
6121
6434
|
return launch(p, meta);
|
|
6122
6435
|
}
|
|
6123
6436
|
async function cmdAudit(p) {
|
|
@@ -6185,7 +6498,7 @@ function cmdPlan(p) {
|
|
|
6185
6498
|
}
|
|
6186
6499
|
function cmdCrew(p) {
|
|
6187
6500
|
const file2 = flag(p, "task-file");
|
|
6188
|
-
const tasks = (file2 ?
|
|
6501
|
+
const tasks = (file2 ? fs34.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
|
|
6189
6502
|
if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
|
|
6190
6503
|
if (has(p, "write") && tasks.length > 1) {
|
|
6191
6504
|
throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
|
|
@@ -6306,7 +6619,7 @@ ${[fmt2(head2), ...rows2.map(fmt2)].join("\n")}`);
|
|
|
6306
6619
|
const { rows, total } = listHistory({ text: p.positional.join(" "), model: flag(p, "model"), state: flag(p, "state"), group: flag(p, "group"), sinceMs: since2, limit });
|
|
6307
6620
|
if (has(p, "json")) return console.log(JSON.stringify({ total, rows }, null, 2)), 0;
|
|
6308
6621
|
if (!rows.length) return console.log(total ? "nothing on this page" : "no matching runs"), 0;
|
|
6309
|
-
const body = rows.map((r) => [r.id, when(r.startedAt), r.state, `${r.backend}${r.model ? ` (${r.model.split("/").pop()})` : ""}`, secs(r.seconds), r.task.length > 60 ? `${r.task.slice(0, 59)}\u2026` : r.task]);
|
|
6622
|
+
const body = rows.map((r) => [r.id, when(r.startedAt), r.verifyFailed ? "verify failed" : r.state, `${r.backend}${r.model ? ` (${r.model.split("/").pop()})` : ""}`, secs(r.seconds), r.task.length > 60 ? `${r.task.slice(0, 59)}\u2026` : r.task]);
|
|
6310
6623
|
const head = ["RUN", "WHEN", "STATE", "WORKER", "TIME", "TASK"];
|
|
6311
6624
|
const widths = head.map((h, i) => Math.max(h.length, ...body.map((r) => r[i].length)));
|
|
6312
6625
|
const fmt = (r, header = false) => r.map((c, i) => {
|
|
@@ -6371,11 +6684,8 @@ function cmdStop(p) {
|
|
|
6371
6684
|
const ids = selectIds(p, () => [resolveRun(void 0)]);
|
|
6372
6685
|
let stopped = 0;
|
|
6373
6686
|
for (const id of ids) {
|
|
6374
|
-
const meta =
|
|
6375
|
-
if (!
|
|
6376
|
-
meta.stopRequested = true;
|
|
6377
|
-
writeMeta(meta);
|
|
6378
|
-
process.kill(meta.pid, "SIGTERM");
|
|
6687
|
+
const meta = stopRun(id);
|
|
6688
|
+
if (!meta) continue;
|
|
6379
6689
|
console.log(`stopping ${meta.id} (${meta.state})`);
|
|
6380
6690
|
stopped++;
|
|
6381
6691
|
}
|
|
@@ -6388,13 +6698,13 @@ function sinceMs(s) {
|
|
|
6388
6698
|
if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
|
|
6389
6699
|
return Date.now() - Number(m[1]) * 864e5;
|
|
6390
6700
|
}
|
|
6391
|
-
var readStdin = () => process.stdin.isTTY ? "" :
|
|
6701
|
+
var readStdin = () => process.stdin.isTTY ? "" : fs34.readFileSync(0, "utf8");
|
|
6392
6702
|
function cmdStatusline(p) {
|
|
6393
6703
|
const input = readStdin();
|
|
6394
6704
|
const lines = [];
|
|
6395
6705
|
const then = flag(p, "then");
|
|
6396
6706
|
if (then) {
|
|
6397
|
-
const r = spawnSync8("/bin/sh", ["-c", then], { input, encoding: "utf8", timeout: 5e3 });
|
|
6707
|
+
const r = process.platform === "win32" ? spawnSync8(then, { shell: true, input, encoding: "utf8", timeout: 5e3 }) : spawnSync8("/bin/sh", ["-c", then], { input, encoding: "utf8", timeout: 5e3 });
|
|
6398
6708
|
if (r.stdout?.trimEnd()) lines.push(r.stdout.trimEnd());
|
|
6399
6709
|
}
|
|
6400
6710
|
try {
|
|
@@ -6428,8 +6738,8 @@ function cmdModels(p) {
|
|
|
6428
6738
|
return 0;
|
|
6429
6739
|
}
|
|
6430
6740
|
function cmdHookStart() {
|
|
6431
|
-
const script =
|
|
6432
|
-
if (
|
|
6741
|
+
const script = path29.join(path29.dirname(path29.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
|
|
6742
|
+
if (fs34.existsSync(script)) spawnSync8(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
|
|
6433
6743
|
return 0;
|
|
6434
6744
|
}
|
|
6435
6745
|
function cmdInit(p) {
|
|
@@ -6469,8 +6779,8 @@ function cmdSavings(p) {
|
|
|
6469
6779
|
est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
|
|
6470
6780
|
const out = flag(p, "card");
|
|
6471
6781
|
if (out) {
|
|
6472
|
-
|
|
6473
|
-
console.log(`card written to ${
|
|
6782
|
+
fs34.writeFileSync(out, card(t, period));
|
|
6783
|
+
console.log(`card written to ${path29.resolve(out)}`);
|
|
6474
6784
|
}
|
|
6475
6785
|
if (has(p, "badge")) console.log(`})`);
|
|
6476
6786
|
return 0;
|
|
@@ -6512,7 +6822,7 @@ function cmdConfig(p) {
|
|
|
6512
6822
|
console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
|
|
6513
6823
|
return 0;
|
|
6514
6824
|
}
|
|
6515
|
-
console.log(`config file: ${configPath()}${
|
|
6825
|
+
console.log(`config file: ${configPath()}${fs34.existsSync(configPath()) ? "" : " (not present)"}`);
|
|
6516
6826
|
for (const [key, s] of Object.entries(eff)) {
|
|
6517
6827
|
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);
|
|
6518
6828
|
console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
|
|
@@ -6521,22 +6831,22 @@ function cmdConfig(p) {
|
|
|
6521
6831
|
return 0;
|
|
6522
6832
|
}
|
|
6523
6833
|
function cmdClean(p) {
|
|
6524
|
-
const
|
|
6525
|
-
const cutoff = Date.now() -
|
|
6834
|
+
const days2 = Number(flag(p, "days") ?? 14);
|
|
6835
|
+
const cutoff = Date.now() - days2 * 864e5;
|
|
6526
6836
|
const old = listRunIds().filter((id) => {
|
|
6527
6837
|
const m = freshMeta(id);
|
|
6528
6838
|
const pendingPatch = m.mode === "isolate" && !m.applied && !m.discarded && m.changes?.length;
|
|
6529
6839
|
return TERMINAL.includes(m.state) && Date.parse(m.startedAt) < cutoff && !pendingPatch;
|
|
6530
6840
|
});
|
|
6531
6841
|
if (!has(p, "yes")) {
|
|
6532
|
-
console.log(`${old.length} run(s) older than ${
|
|
6842
|
+
console.log(`${old.length} run(s) older than ${days2} days would be removed (unapplied isolate patches are kept). Re-run with --yes.`);
|
|
6533
6843
|
return 0;
|
|
6534
6844
|
}
|
|
6535
6845
|
for (const id of old) {
|
|
6536
6846
|
const m = readMeta(id);
|
|
6537
6847
|
if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
|
|
6538
6848
|
recordRun(m);
|
|
6539
|
-
|
|
6849
|
+
fs34.rmSync(runDir(id), { recursive: true, force: true });
|
|
6540
6850
|
}
|
|
6541
6851
|
const snapshots = prune(Date.now(), 0);
|
|
6542
6852
|
console.log(`removed ${old.length} run(s)${snapshots ? ` and ${snapshots} read snapshot${snapshots === 1 ? "" : "s"}` : ""}; the history and the savings ledger are kept`);
|
|
@@ -6547,9 +6857,9 @@ var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positiona
|
|
|
6547
6857
|
|
|
6548
6858
|
// src/core/doctor.ts
|
|
6549
6859
|
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
6550
|
-
import
|
|
6860
|
+
import fs35 from "node:fs";
|
|
6551
6861
|
import os11 from "node:os";
|
|
6552
|
-
import
|
|
6862
|
+
import path30 from "node:path";
|
|
6553
6863
|
var MARK = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
|
|
6554
6864
|
var NEXT = [
|
|
6555
6865
|
{ when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
|
|
@@ -6559,10 +6869,10 @@ var NEXT = [
|
|
|
6559
6869
|
{ when: /Gemini CLI is not signed in|IneligibleTierError/, command: "export GEMINI_API_KEY=\u2026", why: "a Google AI Studio key for the Gemini worker" }
|
|
6560
6870
|
];
|
|
6561
6871
|
function firstNodeOnPath() {
|
|
6562
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
6563
|
-
const file2 =
|
|
6872
|
+
for (const dir of (process.env.PATH ?? "").split(path30.delimiter).filter(Boolean)) {
|
|
6873
|
+
const file2 = path30.join(dir, process.platform === "win32" ? "node.exe" : "node");
|
|
6564
6874
|
try {
|
|
6565
|
-
if (!
|
|
6875
|
+
if (!fs35.statSync(file2).isFile()) continue;
|
|
6566
6876
|
} catch {
|
|
6567
6877
|
continue;
|
|
6568
6878
|
}
|
|
@@ -6589,14 +6899,16 @@ function doctor5(probe) {
|
|
|
6589
6899
|
const addAll = (list2) => list2.forEach((c) => add(c.level, c.message));
|
|
6590
6900
|
add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
|
|
6591
6901
|
const onPath = firstNodeOnPath();
|
|
6592
|
-
if (onPath && onPath.version && tooOld(onPath.version) &&
|
|
6902
|
+
if (onPath && onPath.version && tooOld(onPath.version) && path30.resolve(onPath.path) !== path30.resolve(process.execPath)) {
|
|
6593
6903
|
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)`);
|
|
6594
6904
|
}
|
|
6595
6905
|
add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
|
|
6596
6906
|
const cfg = loadConfig();
|
|
6597
|
-
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${
|
|
6907
|
+
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs35.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
|
|
6598
6908
|
for (const w of cfg.warnings) add("warn", w);
|
|
6599
|
-
if (process.platform
|
|
6909
|
+
if (process.platform === "win32") {
|
|
6910
|
+
add("warn", "git guard: not available on Windows yet, so Pitroom does not block a worker's git history changes or pushes there (read-only runs are still limited by each worker CLI's own rules, and --isolate keeps edits in a copy); prefer --isolate and check the patch before apply");
|
|
6911
|
+
} else {
|
|
6600
6912
|
const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
|
|
6601
6913
|
add(guarded ? "ok" : "warn", guarded ? "git guard shim ready" : "git guard unavailable (git not on PATH)");
|
|
6602
6914
|
}
|
|
@@ -6747,20 +7059,20 @@ function skillChecks() {
|
|
|
6747
7059
|
else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
|
|
6748
7060
|
}
|
|
6749
7061
|
if (geminiExtensionInstalled()) {
|
|
6750
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
7062
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path30.sep}.agents${path30.sep}`) && i.names.length);
|
|
6751
7063
|
checks.push(
|
|
6752
7064
|
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)" }
|
|
6753
7065
|
);
|
|
6754
7066
|
}
|
|
6755
7067
|
if (viaPlugin) {
|
|
6756
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
7068
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path30.sep}.claude${path30.sep}`) && i.names.length);
|
|
6757
7069
|
checks.push(
|
|
6758
7070
|
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)" }
|
|
6759
7071
|
);
|
|
6760
7072
|
}
|
|
6761
7073
|
const launcher = launcherPath();
|
|
6762
|
-
if (
|
|
6763
|
-
const r = spawnSync9(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
7074
|
+
if (fs35.existsSync(launcher)) {
|
|
7075
|
+
const r = spawnSync9(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
|
|
6764
7076
|
checks.push(
|
|
6765
7077
|
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)}` }
|
|
6766
7078
|
);
|
|
@@ -6771,12 +7083,12 @@ function skillChecks() {
|
|
|
6771
7083
|
}
|
|
6772
7084
|
function geminiExtensionInstalled() {
|
|
6773
7085
|
const home3 = process.env.GEMINI_CLI_HOME ?? os11.homedir();
|
|
6774
|
-
return
|
|
7086
|
+
return fs35.existsSync(path30.join(home3, ".gemini", "extensions", "pitroom"));
|
|
6775
7087
|
}
|
|
6776
7088
|
function pluginInstalled() {
|
|
6777
7089
|
try {
|
|
6778
|
-
const f =
|
|
6779
|
-
const plugins = JSON.parse(
|
|
7090
|
+
const f = path30.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
7091
|
+
const plugins = JSON.parse(fs35.readFileSync(f, "utf8")).plugins ?? {};
|
|
6780
7092
|
return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
|
|
6781
7093
|
} catch {
|
|
6782
7094
|
return false;
|
|
@@ -6793,24 +7105,24 @@ function superpowersActive() {
|
|
|
6793
7105
|
for (const p of openCodePlugins()) {
|
|
6794
7106
|
if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
|
|
6795
7107
|
}
|
|
6796
|
-
for (const base2 of [
|
|
6797
|
-
const dir =
|
|
6798
|
-
if (
|
|
7108
|
+
for (const base2 of [path30.join(os11.homedir(), ".agents", "skills"), path30.join(os11.homedir(), ".claude", "skills")]) {
|
|
7109
|
+
const dir = path30.join(base2, "using-superpowers");
|
|
7110
|
+
if (fs35.existsSync(path30.join(dir, "SKILL.md"))) found.push(dir);
|
|
6799
7111
|
}
|
|
6800
7112
|
return found;
|
|
6801
7113
|
}
|
|
6802
7114
|
function claudePlugins() {
|
|
6803
7115
|
try {
|
|
6804
|
-
const f =
|
|
6805
|
-
return Object.keys(JSON.parse(
|
|
7116
|
+
const f = path30.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
7117
|
+
return Object.keys(JSON.parse(fs35.readFileSync(f, "utf8")).plugins ?? {});
|
|
6806
7118
|
} catch {
|
|
6807
7119
|
return [];
|
|
6808
7120
|
}
|
|
6809
7121
|
}
|
|
6810
7122
|
function claudePluginEnabled(key) {
|
|
6811
7123
|
try {
|
|
6812
|
-
const f =
|
|
6813
|
-
return JSON.parse(
|
|
7124
|
+
const f = path30.join(os11.homedir(), ".claude", "settings.json");
|
|
7125
|
+
return JSON.parse(fs35.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
|
|
6814
7126
|
} catch {
|
|
6815
7127
|
return true;
|
|
6816
7128
|
}
|
|
@@ -6819,7 +7131,7 @@ function codexPlugins() {
|
|
|
6819
7131
|
const plugins = /* @__PURE__ */ new Map();
|
|
6820
7132
|
let text;
|
|
6821
7133
|
try {
|
|
6822
|
-
text =
|
|
7134
|
+
text = fs35.readFileSync(path30.join(process.env.CODEX_HOME ?? path30.join(os11.homedir(), ".codex"), "config.toml"), "utf8");
|
|
6823
7135
|
} catch {
|
|
6824
7136
|
return plugins;
|
|
6825
7137
|
}
|
|
@@ -6837,11 +7149,11 @@ function codexPlugins() {
|
|
|
6837
7149
|
return plugins;
|
|
6838
7150
|
}
|
|
6839
7151
|
function openCodePlugins() {
|
|
6840
|
-
const dir =
|
|
7152
|
+
const dir = path30.join(process.env.XDG_CONFIG_HOME ?? path30.join(os11.homedir(), ".config"), "opencode");
|
|
6841
7153
|
const plugins = [];
|
|
6842
7154
|
for (const name of ["opencode.json", "opencode.jsonc"]) {
|
|
6843
7155
|
try {
|
|
6844
|
-
const text =
|
|
7156
|
+
const text = fs35.readFileSync(path30.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
|
|
6845
7157
|
const list2 = JSON.parse(text).plugin;
|
|
6846
7158
|
if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
|
|
6847
7159
|
} catch {
|
|
@@ -6849,7 +7161,7 @@ function openCodePlugins() {
|
|
|
6849
7161
|
}
|
|
6850
7162
|
for (const folder of ["plugin", "plugins"]) {
|
|
6851
7163
|
try {
|
|
6852
|
-
for (const f of
|
|
7164
|
+
for (const f of fs35.readdirSync(path30.join(dir, folder))) plugins.push(path30.join(dir, folder, f));
|
|
6853
7165
|
} catch {
|
|
6854
7166
|
}
|
|
6855
7167
|
}
|
|
@@ -6929,6 +7241,7 @@ Run options
|
|
|
6929
7241
|
--in-place a read run reads the directory itself, not a clean snapshot (default: a snapshot without secret-looking
|
|
6930
7242
|
files when the directory has any; config "readIn": auto | snapshot | project)
|
|
6931
7243
|
--audit have another worker re-check this read run's answer; --no-audit skips it (default: the config's "audit")
|
|
7244
|
+
--fresh ask a worker even when the same read question was answered on the same code (config "cacheDays", default 7)
|
|
6932
7245
|
--effort LEVEL reasoning effort for the worker: low, medium, high, xhigh, \u2026 (model#level)
|
|
6933
7246
|
--plan PLAN with --step N: implement Task N of a plan (-i or -w); the task text is your notes
|
|
6934
7247
|
--step N the plan task for --plan
|
|
@@ -6950,9 +7263,9 @@ Exit codes: 0 ok \xB7 1 worker failed \xB7 2 usage \xB7 3 refused/setup \xB7 4 t
|
|
|
6950
7263
|
5 read-only violation \xB7 6 verify failed \xB7 75 still running (wait again)
|
|
6951
7264
|
Workers: ${backendIds().join(", ")} (targets: "opencode", "opencode:provider/model", or a bare model)
|
|
6952
7265
|
Env: PITROOM_WORKER, PITROOM_MODEL, PITROOM_FALLBACK="t1,t2", PITROOM_TIMEOUT, PITROOM_MAX_PARALLEL,
|
|
6953
|
-
PITROOM_PRIMARY=sonnet|opus|haiku|gpt-5, PITROOM_PRICE="in,out", PITROOM_HOME, PITROOM_CONFIG,
|
|
7266
|
+
PITROOM_PRIMARY=sonnet|opus|haiku|gpt-5, PITROOM_PRICE="in,out", PITROOM_HOME, PITROOM_CONFIG, PITROOM_CACHE_DAYS,
|
|
6954
7267
|
PITROOM_<WORKER>_BIN
|
|
6955
|
-
Config: ~/.config/pitroom/config.json (worker, fallback, models, tiers, timeout, primary, price, link, web, maxParallel)`;
|
|
7268
|
+
Config: ~/.config/pitroom/config.json (worker, fallback, models, tiers, timeout, primary, price, link, web, maxParallel, audit, readIn, cacheDays)`;
|
|
6956
7269
|
var COMMANDS = {
|
|
6957
7270
|
run: cmdRun,
|
|
6958
7271
|
crew: cmdCrew,
|
|
@@ -6965,7 +7278,7 @@ var COMMANDS = {
|
|
|
6965
7278
|
if (port !== void 0 && !(/^\d+$/.test(port) && Number(port) <= 65535)) throw new UserError("--port takes a number from 0 to 65535 (0 picks a free one)");
|
|
6966
7279
|
const dir = flag(p, "dir");
|
|
6967
7280
|
if (dir !== void 0) {
|
|
6968
|
-
if (!
|
|
7281
|
+
if (!fs36.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) throw new UserError(`-d ${dir}: no such directory`);
|
|
6969
7282
|
process.chdir(dir);
|
|
6970
7283
|
process.env.PWD = process.cwd();
|
|
6971
7284
|
}
|