pitroom 0.25.0 → 0.27.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 +42 -0
- package/README.md +24 -21
- package/dist/lib.mjs +205 -31
- package/dist/pitroom.mjs +755 -438
- package/docs/backends.md +9 -2
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/skills/pitroom-driven-development/re-review-prompt.md +2 -2
- package/skills/pitroom-driven-development/task-reviewer-prompt.md +2 -2
- package/skills/pitroom-review/code-reviewer.md +2 -2
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 fs44 from "node:fs";
|
|
5
5
|
|
|
6
6
|
// src/core/errors.ts
|
|
7
7
|
var UserError = class extends Error {
|
|
@@ -558,7 +558,7 @@ function denyCommandRules() {
|
|
|
558
558
|
"command(eval)"
|
|
559
559
|
];
|
|
560
560
|
}
|
|
561
|
-
function workerSettings(workspace, workerHome3, reads = []) {
|
|
561
|
+
function workerSettings(workspace, workerHome3, reads = [], writes = false) {
|
|
562
562
|
const real = (p) => {
|
|
563
563
|
try {
|
|
564
564
|
return fs6.realpathSync.native(p);
|
|
@@ -582,6 +582,8 @@ function workerSettings(workspace, workerHome3, reads = []) {
|
|
|
582
582
|
allow: [
|
|
583
583
|
"command(*)",
|
|
584
584
|
...both(path5.join(dataDir(workerHome3), "brain")).map((p) => `write_file(${rulePath(p)})`),
|
|
585
|
+
// agy 1.4 asks for edits in the workspace too, --mode accept-edits or not, and a headless run cannot ask
|
|
586
|
+
...writes ? both(workspace).map((p) => `write_file(${rulePath(p)})`) : [],
|
|
585
587
|
...toolchains().flatMap(both).map((p) => `read_file(${rulePath(p)})`),
|
|
586
588
|
// what the task names outside the workspace: a headless agy cannot ask, and would end the turn unanswered
|
|
587
589
|
...reads.map((r) => r.path).flatMap(both).map((p) => `read_file(${rulePath(p)})`)
|
|
@@ -707,12 +709,12 @@ function baseSettings() {
|
|
|
707
709
|
}
|
|
708
710
|
var rulePath = (p) => p.replace(/^[A-Za-z]:(?=[\\/])/, "").replace(/\\/g, "/");
|
|
709
711
|
var HOMES_KEPT_DAYS = 14;
|
|
710
|
-
function workerHome(workspace, reads = []) {
|
|
712
|
+
function workerHome(workspace, reads = [], writes = false) {
|
|
711
713
|
const root = path5.join(home(), "antigravity-home");
|
|
712
714
|
const id2 = crypto3.createHash("sha256").update(path5.resolve(workspace)).digest("hex").slice(0, 16);
|
|
713
715
|
const dir = path5.join(root, id2);
|
|
714
716
|
fs6.mkdirSync(dataDir(dir), { recursive: true });
|
|
715
|
-
writeIfChanged(path5.join(dataDir(dir), "settings.json"), `${JSON.stringify(workerSettings(workspace, dir, reads), null, 2)}
|
|
717
|
+
writeIfChanged(path5.join(dataDir(dir), "settings.json"), `${JSON.stringify(workerSettings(workspace, dir, reads, writes), null, 2)}
|
|
716
718
|
`);
|
|
717
719
|
if (process.platform === "darwin") {
|
|
718
720
|
fs6.mkdirSync(path5.join(dir, "Library"), { recursive: true });
|
|
@@ -769,13 +771,13 @@ function invocation(req) {
|
|
|
769
771
|
if (model) args.push("--model", model);
|
|
770
772
|
if (effort) args.push("--effort", effort);
|
|
771
773
|
const { command, prefix } = resolveCommand(binary());
|
|
772
|
-
const workerDir = workerHome(req.cwd, outsideReads(prompt, req.cwd, req.project?.root));
|
|
774
|
+
const workerDir = workerHome(req.cwd, outsideReads(prompt, req.cwd, req.project?.root), req.mode !== "read");
|
|
773
775
|
const env = { HOME: workerDir };
|
|
774
776
|
if (process.platform === "win32") env.USERPROFILE = workerDir;
|
|
775
777
|
return { command, args: [...prefix, ...args], env };
|
|
776
778
|
}
|
|
777
779
|
function classify(message) {
|
|
778
|
-
if (/RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|capacity/i.test(message)) return "rate-limited";
|
|
780
|
+
if (/usage limit|session limit|RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|capacity/i.test(message)) return "rate-limited";
|
|
779
781
|
if (/not (signed|logged) in|\bsign.?in\b|\blog ?in\b|authenticat|PERMISSION_DENIED|oauth|credential|keyring|keychain|\b401\b|unauthori[sz]ed/i.test(message)) {
|
|
780
782
|
return "auth";
|
|
781
783
|
}
|
|
@@ -826,9 +828,10 @@ function doctor({ models, hasFallback }) {
|
|
|
826
828
|
}
|
|
827
829
|
const checks = [
|
|
828
830
|
{ level: "ok", message: `Antigravity CLI ${version.out.trim().split("\n")[0]} at ${binary()}` },
|
|
829
|
-
|
|
831
|
+
// checked on macOS (agy 1.4.7, scripts/check-worker.mjs); Linux and Windows not run yet
|
|
832
|
+
process.platform === "darwin" ? { level: "ok", message: "agy has no read-only mode for headless runs, so its read runs always read a snapshot of the project; prefer -i for changes" } : {
|
|
830
833
|
level: "warn",
|
|
831
|
-
message:
|
|
834
|
+
message: `the Antigravity worker is in beta on ${process.platform === "win32" ? "Windows" : "Linux"} (checked on macOS only; node scripts/check-worker.mjs antigravity tries it here). agy has no read-only mode for headless runs, so its read runs always read a snapshot of the project; prefer -i for changes`
|
|
832
835
|
}
|
|
833
836
|
];
|
|
834
837
|
if (process.platform === "win32") {
|
|
@@ -1013,7 +1016,7 @@ function classify2(message) {
|
|
|
1013
1016
|
if (/authentication_failed|failed to authenticate|oauth|not logged in|\/login|invalid api key|\b40[13]\b|unauthori[sz]ed/i.test(message)) {
|
|
1014
1017
|
return "auth";
|
|
1015
1018
|
}
|
|
1016
|
-
if (/rate.?limit|too many requests|\b429\b|\b529\b|overloaded|
|
|
1019
|
+
if (/usage limit|session limit|limit reached|rate.?limit|too many requests|\b429\b|\b529\b|overloaded|quota|capacity/i.test(message)) return "rate-limited";
|
|
1017
1020
|
if (/model[^.]*(not found|does not exist|invalid|not available)|invalid model|not_found_error/i.test(message)) return "model-unavailable";
|
|
1018
1021
|
return "other";
|
|
1019
1022
|
}
|
|
@@ -1214,7 +1217,7 @@ function classify3(message) {
|
|
|
1214
1217
|
if (/not supported|model[^.]*(not found|does not exist|unknown|unavailable)|unsupported model|no such model/i.test(message)) {
|
|
1215
1218
|
return "model-unavailable";
|
|
1216
1219
|
}
|
|
1217
|
-
if (/rate.?limit|too many requests|\b429\b|
|
|
1220
|
+
if (/usage limit|session limit|rate.?limit|too many requests|\b429\b|quota|overloaded|\b50[23]\b|capacity/i.test(message)) return "rate-limited";
|
|
1218
1221
|
if (/unauthori[sz]ed|\b40[13]\b|not logged in|log ?in|api.?key|credits?\b|billing|subscription/i.test(message)) return "auth";
|
|
1219
1222
|
return "other";
|
|
1220
1223
|
}
|
|
@@ -1452,7 +1455,7 @@ function classify4(message) {
|
|
|
1452
1455
|
if (/IneligibleTier|no longer supported for Gemini|error authenticating|not logged in|please (log ?in|sign in)|credentials|api key|UNAUTHENTICATED|PERMISSION_DENIED|\b40[13]\b|unauthori[sz]ed/i.test(message)) {
|
|
1453
1456
|
return "auth";
|
|
1454
1457
|
}
|
|
1455
|
-
if (/RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|exhausted your capacity|capacity/i.test(message)) return "rate-limited";
|
|
1458
|
+
if (/usage limit|session limit|RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|exhausted your capacity|capacity/i.test(message)) return "rate-limited";
|
|
1456
1459
|
if (/model.{0,80}(not found|not available|no longer available|does not exist|unsupported|invalid)|\b404\b|NOT_FOUND|invalid model|is not supported/i.test(message)) return "model-unavailable";
|
|
1457
1460
|
return "other";
|
|
1458
1461
|
}
|
|
@@ -1896,7 +1899,7 @@ function majorVersion(output) {
|
|
|
1896
1899
|
}
|
|
1897
1900
|
function classify5(message) {
|
|
1898
1901
|
if (/model not found|ModelNotFound|unknown model|no such model|deprecated/i.test(message)) return "model-unavailable";
|
|
1899
|
-
if (/rate.?limit|too many requests|\b429\b|quota|overloaded|\b50[23]\b|unavailable|capacity/i.test(message)) return "rate-limited";
|
|
1902
|
+
if (/usage limit|session limit|rate.?limit|too many requests|\b429\b|quota|overloaded|\b50[23]\b|unavailable|capacity/i.test(message)) return "rate-limited";
|
|
1900
1903
|
if (/provider\.auth|FreeTierError|free tier|unauthori[sz]ed|\b40[13]\b|api.?key|insufficient|credits?\b|billing|payment/i.test(message)) {
|
|
1901
1904
|
return "auth";
|
|
1902
1905
|
}
|
|
@@ -2241,7 +2244,7 @@ function invocation7(req) {
|
|
|
2241
2244
|
}
|
|
2242
2245
|
function classify6(message) {
|
|
2243
2246
|
if (/\b40[13]\b|incorrect api key|invalid api key|unauthori[sz]ed|authenticat|not logged in|no auth/i.test(message)) return "auth";
|
|
2244
|
-
if (
|
|
2247
|
+
if (/usage limit|session limit|\b429\b|rate.?limit|too many requests|quota|capacity|overloaded|\b529\b/i.test(message)) return "rate-limited";
|
|
2245
2248
|
if (/model[^.]*(not found|does not exist|not available|not supported)|invalid model|model_not_found|\b404\b/i.test(message)) return "model-unavailable";
|
|
2246
2249
|
return "other";
|
|
2247
2250
|
}
|
|
@@ -2343,7 +2346,8 @@ var SCHEMA = {
|
|
|
2343
2346
|
priceFeedUrl: "string",
|
|
2344
2347
|
priceFeedHours: "number",
|
|
2345
2348
|
budgetDaily: "number",
|
|
2346
|
-
rankWorkers: "boolean"
|
|
2349
|
+
rankWorkers: "boolean",
|
|
2350
|
+
autoFallback: "boolean"
|
|
2347
2351
|
};
|
|
2348
2352
|
function configPath() {
|
|
2349
2353
|
if (process.env.PITROOM_CONFIG) return path12.resolve(process.env.PITROOM_CONFIG);
|
|
@@ -2453,6 +2457,7 @@ function effective(flags = {}) {
|
|
|
2453
2457
|
priceFeedUrl: setting(void 0, e.PITROOM_PRICE_FEED_URL?.trim() || void 0, c.priceFeedUrl?.trim() || void 0, "https://models.dev/api.json"),
|
|
2454
2458
|
priceFeedHours: setting(void 0, positiveInt(e.PITROOM_PRICE_FEED_HOURS), positiveInt(c.priceFeedHours), 24),
|
|
2455
2459
|
rankWorkers: setting(void 0, flag01(e.PITROOM_RANK_WORKERS), c.rankWorkers, false),
|
|
2460
|
+
autoFallback: setting(void 0, flag01(e.PITROOM_AUTO_FALLBACK), c.autoFallback, false),
|
|
2456
2461
|
budgetDaily: setting(void 0, amount(e.PITROOM_BUDGET_DAILY), amount(c.budgetDaily), void 0)
|
|
2457
2462
|
};
|
|
2458
2463
|
}
|
|
@@ -2639,9 +2644,9 @@ function effortFlag(p) {
|
|
|
2639
2644
|
import readline from "node:readline";
|
|
2640
2645
|
|
|
2641
2646
|
// src/core/run.ts
|
|
2642
|
-
import { spawn as spawn4, spawnSync as
|
|
2643
|
-
import
|
|
2644
|
-
import
|
|
2647
|
+
import { spawn as spawn4, spawnSync as spawnSync10 } from "node:child_process";
|
|
2648
|
+
import fs32 from "node:fs";
|
|
2649
|
+
import path28 from "node:path";
|
|
2645
2650
|
|
|
2646
2651
|
// src/vcs/git.ts
|
|
2647
2652
|
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
@@ -2772,7 +2777,29 @@ function applyPatch(root, patchFile, reverse) {
|
|
|
2772
2777
|
function reviewDiff(root, a, b) {
|
|
2773
2778
|
return `${must(root, [...DIFF, "--stat", a, b]).trim()}
|
|
2774
2779
|
|
|
2775
|
-
${must(root, [...DIFF, "-U10", a, b])}`;
|
|
2780
|
+
${numberDiff(must(root, [...DIFF, "-U10", a, b]))}`;
|
|
2781
|
+
}
|
|
2782
|
+
var NUMBERED = "Each line of a hunk starts with its line number in the file after the change (a removed line has none): cite those.";
|
|
2783
|
+
function numberDiff(diff) {
|
|
2784
|
+
let next = 0;
|
|
2785
|
+
let width = 0;
|
|
2786
|
+
let inHunk = false;
|
|
2787
|
+
return diff.split("\n").map((l) => {
|
|
2788
|
+
if (l.startsWith("diff --git")) {
|
|
2789
|
+
inHunk = false;
|
|
2790
|
+
return l;
|
|
2791
|
+
}
|
|
2792
|
+
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(l);
|
|
2793
|
+
if (hunk) {
|
|
2794
|
+
inHunk = true;
|
|
2795
|
+
next = Number(hunk[1]);
|
|
2796
|
+
width = String(next + Number(hunk[2] ?? 1)).length;
|
|
2797
|
+
return l;
|
|
2798
|
+
}
|
|
2799
|
+
if (!inHunk || !l || !"+- ".includes(l[0])) return l;
|
|
2800
|
+
if (l[0] === "-") return `-${" ".repeat(width)}| ${l.slice(1)}`;
|
|
2801
|
+
return `${l[0]}${String(next++).padStart(width)}| ${l.slice(1)}`;
|
|
2802
|
+
}).join("\n");
|
|
2776
2803
|
}
|
|
2777
2804
|
function rangeDiff(root, a, b) {
|
|
2778
2805
|
const range = `${a}..${b}`;
|
|
@@ -2787,7 +2814,9 @@ ${log2 || "(none)"}`,
|
|
|
2787
2814
|
${must(root, [...DIFF, "--stat", since2]).trim() || "(none)"}`,
|
|
2788
2815
|
`## DIFF
|
|
2789
2816
|
|
|
2790
|
-
${
|
|
2817
|
+
${NUMBERED}
|
|
2818
|
+
|
|
2819
|
+
${numberDiff(must(root, [...DIFF, "-U10", since2]))}`
|
|
2791
2820
|
].join("\n\n");
|
|
2792
2821
|
}
|
|
2793
2822
|
function commitOf(root, ref) {
|
|
@@ -2799,6 +2828,10 @@ function gitDir(dir) {
|
|
|
2799
2828
|
return r.code === 0 ? r.stdout.trim() : void 0;
|
|
2800
2829
|
}
|
|
2801
2830
|
|
|
2831
|
+
// src/core/chain.ts
|
|
2832
|
+
import fs15 from "node:fs";
|
|
2833
|
+
import path14 from "node:path";
|
|
2834
|
+
|
|
2802
2835
|
// src/core/target.ts
|
|
2803
2836
|
function parseTarget(spec, defaultBackend) {
|
|
2804
2837
|
const s = spec.trim();
|
|
@@ -2857,17 +2890,52 @@ function resolveChain(flags = {}) {
|
|
|
2857
2890
|
warnings2.push(`fallback ${s} skipped: ${e.message}`);
|
|
2858
2891
|
}
|
|
2859
2892
|
}
|
|
2893
|
+
if (!fallback.length && eff.autoFallback.value) fallback.push(...autoFallback(worker, withDefault));
|
|
2860
2894
|
}
|
|
2861
2895
|
return { worker, fallback, warnings: warnings2 };
|
|
2862
2896
|
}
|
|
2897
|
+
var AUTO_MAX = 4;
|
|
2898
|
+
var bare = (t) => `${t.backend}:${(t.model ?? "").split("#")[0]}`;
|
|
2899
|
+
function installed(backend) {
|
|
2900
|
+
try {
|
|
2901
|
+
const bin = getBackend(backend).binary();
|
|
2902
|
+
return path14.isAbsolute(bin) && fs15.statSync(bin).isFile();
|
|
2903
|
+
} catch {
|
|
2904
|
+
return false;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
function autoFallback(worker, withDefault = (t) => t) {
|
|
2908
|
+
const eff = effective();
|
|
2909
|
+
const named = [
|
|
2910
|
+
...Object.values(eff.tiers.value),
|
|
2911
|
+
...Object.entries(eff.models.value).map(([backend, model]) => `${backend}:${model}`),
|
|
2912
|
+
eff.worker.value
|
|
2913
|
+
];
|
|
2914
|
+
const costs = eff.costs.value;
|
|
2915
|
+
const out = [];
|
|
2916
|
+
for (const spec of named) {
|
|
2917
|
+
let t;
|
|
2918
|
+
try {
|
|
2919
|
+
t = withDefault(parseTarget(spec, worker.backend));
|
|
2920
|
+
getBackend(t.backend);
|
|
2921
|
+
} catch {
|
|
2922
|
+
continue;
|
|
2923
|
+
}
|
|
2924
|
+
if (bare(t) === bare(worker) || out.some((o) => bare(o) === bare(t)) || !installed(t.backend)) continue;
|
|
2925
|
+
out.push(t);
|
|
2926
|
+
}
|
|
2927
|
+
const cost = (t) => costs[bare(t)] ?? Infinity;
|
|
2928
|
+
return out.map((t, i) => ({ t, i })).sort((a, b) => cost(a.t) - cost(b.t) || a.i - b.i).slice(0, AUTO_MAX).map((x) => x.t);
|
|
2929
|
+
}
|
|
2863
2930
|
|
|
2864
2931
|
// src/core/process.ts
|
|
2865
2932
|
import { spawn } from "node:child_process";
|
|
2866
|
-
import
|
|
2933
|
+
import fs17 from "node:fs";
|
|
2867
2934
|
|
|
2868
2935
|
// src/vcs/guard.ts
|
|
2869
|
-
import
|
|
2870
|
-
import
|
|
2936
|
+
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
2937
|
+
import fs16 from "node:fs";
|
|
2938
|
+
import path15 from "node:path";
|
|
2871
2939
|
var VERSION = 2;
|
|
2872
2940
|
var WINDOWS = process.platform === "win32";
|
|
2873
2941
|
var fwd = (p) => WINDOWS ? p.replace(/\\/g, "/") : p;
|
|
@@ -2944,39 +3012,122 @@ esac
|
|
|
2944
3012
|
exec "$real" "$@"
|
|
2945
3013
|
`;
|
|
2946
3014
|
function shimDir() {
|
|
2947
|
-
return
|
|
3015
|
+
return path15.join(home(), "shim", `v${VERSION}`);
|
|
3016
|
+
}
|
|
3017
|
+
function scoopTarget(exe) {
|
|
3018
|
+
try {
|
|
3019
|
+
const text = fs16.readFileSync(exe.replace(/\.exe$/i, ".shim"), "utf8");
|
|
3020
|
+
const target = /^\s*path\s*=\s*"?([^"\r\n]+?)"?\s*$/m.exec(text)?.[1];
|
|
3021
|
+
return target && fs16.statSync(target).isFile() ? target : void 0;
|
|
3022
|
+
} catch {
|
|
3023
|
+
return void 0;
|
|
3024
|
+
}
|
|
2948
3025
|
}
|
|
2949
3026
|
function findRealGit(skip) {
|
|
2950
3027
|
const same2 = (a, b) => WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
2951
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
2952
|
-
if (same2(
|
|
2953
|
-
const p =
|
|
3028
|
+
for (const dir of (process.env.PATH ?? "").split(path15.delimiter).filter(Boolean)) {
|
|
3029
|
+
if (same2(path15.resolve(dir), skip)) continue;
|
|
3030
|
+
const p = path15.join(dir, WINDOWS ? "git.exe" : "git");
|
|
2954
3031
|
try {
|
|
2955
|
-
|
|
2956
|
-
if (
|
|
3032
|
+
fs16.accessSync(p, fs16.constants.X_OK);
|
|
3033
|
+
if (fs16.statSync(p).isFile()) return WINDOWS && scoopTarget(p) || p;
|
|
2957
3034
|
} catch {
|
|
2958
3035
|
}
|
|
2959
3036
|
}
|
|
2960
3037
|
return void 0;
|
|
2961
3038
|
}
|
|
2962
3039
|
function findGitSh(realGit) {
|
|
2963
|
-
let dir =
|
|
3040
|
+
let dir = path15.dirname(realGit);
|
|
2964
3041
|
for (let i = 0; i < 4; i++) {
|
|
2965
3042
|
for (const rel of ["bin/sh.exe", "usr/bin/sh.exe"]) {
|
|
2966
|
-
const p =
|
|
2967
|
-
if (
|
|
3043
|
+
const p = path15.join(dir, rel);
|
|
3044
|
+
if (fs16.existsSync(p)) return p;
|
|
2968
3045
|
}
|
|
2969
|
-
const up =
|
|
3046
|
+
const up = path15.dirname(dir);
|
|
2970
3047
|
if (up === dir) break;
|
|
2971
3048
|
dir = up;
|
|
2972
3049
|
}
|
|
2973
3050
|
return void 0;
|
|
2974
3051
|
}
|
|
2975
|
-
var cmdShim = (sh) =>
|
|
2976
|
-
|
|
2977
|
-
"
|
|
2978
|
-
|
|
3052
|
+
var cmdShim = (sh) => [
|
|
3053
|
+
"@echo off",
|
|
3054
|
+
"setlocal EnableExtensions DisableDelayedExpansion",
|
|
3055
|
+
'set "MSYS2_ARG_CONV_EXCL=*"',
|
|
3056
|
+
'set "PITROOM_SELF=%~f0"',
|
|
3057
|
+
'set q=^"',
|
|
3058
|
+
"setlocal EnableDelayedExpansion",
|
|
3059
|
+
'set "line=!cmdcmdline!"',
|
|
3060
|
+
'if not "!line: /d =!"=="!line!" goto plain',
|
|
3061
|
+
'if not "!line: /s =!"=="!line!" goto plain',
|
|
3062
|
+
'set "tail=!line:*/c ""%PITROOM_SELF%"=!"',
|
|
3063
|
+
'if "!tail!"=="!line!" set "tail=!line:*/c "%PITROOM_SELF%=!"',
|
|
3064
|
+
'if "!tail!"=="!line!" goto plain',
|
|
3065
|
+
'if "!tail:~-1!"=="!q!" set "tail=!tail:~0,-1!"',
|
|
3066
|
+
`"${sh}" "%~dp0git" !tail!`,
|
|
3067
|
+
"exit /b !errorlevel!",
|
|
3068
|
+
":plain",
|
|
3069
|
+
"endlocal",
|
|
3070
|
+
`"${sh}" "%~dp0git" %*`,
|
|
3071
|
+
"exit /b %errorlevel%",
|
|
3072
|
+
""
|
|
3073
|
+
].join("\r\n");
|
|
3074
|
+
var launcherSource = (sh) => `// pitroom git guard: git.exe launcher v${VERSION} (generated; edit src/vcs/guard.ts)
|
|
3075
|
+
using System;
|
|
3076
|
+
using System.Diagnostics;
|
|
3077
|
+
using System.IO;
|
|
3078
|
+
|
|
3079
|
+
static class PitroomGit {
|
|
3080
|
+
const string Sh = @"${sh.replace(/"/g, '""')}";
|
|
3081
|
+
|
|
3082
|
+
static int Main() {
|
|
3083
|
+
string line = Environment.CommandLine;
|
|
3084
|
+
// this program's own name, as Windows reads it: up to the closing quote when it starts with one, else to a space
|
|
3085
|
+
int i = 0;
|
|
3086
|
+
if (line.Length > 0 && line[0] == '"') { int q = line.IndexOf('"', 1); i = q < 0 ? line.Length : q + 1; }
|
|
3087
|
+
else { while (i < line.Length && line[i] != ' ' && line[i] != '\\t') i++; }
|
|
3088
|
+
string script = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "git");
|
|
3089
|
+
ProcessStartInfo start = new ProcessStartInfo(Sh, "\\"" + script + "\\"" + line.Substring(i));
|
|
3090
|
+
start.UseShellExecute = false;
|
|
3091
|
+
start.EnvironmentVariables["MSYS2_ARG_CONV_EXCL"] = "*";
|
|
3092
|
+
// Ctrl+C reaches git too: wait for it to end and pass its exit code on
|
|
3093
|
+
Console.CancelKeyPress += delegate (object o, ConsoleCancelEventArgs e) { e.Cancel = true; };
|
|
3094
|
+
using (Process git = Process.Start(start)) {
|
|
3095
|
+
git.WaitForExit();
|
|
3096
|
+
return git.ExitCode;
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
2979
3100
|
`;
|
|
3101
|
+
function findCsc() {
|
|
3102
|
+
const windir = process.env.WINDIR ?? process.env.SystemRoot ?? "C:\\Windows";
|
|
3103
|
+
for (const fw of ["Framework64", "Framework"]) {
|
|
3104
|
+
const p = path15.join(windir, "Microsoft.NET", fw, "v4.0.30319", "csc.exe");
|
|
3105
|
+
if (fs16.existsSync(p)) return p;
|
|
3106
|
+
}
|
|
3107
|
+
return void 0;
|
|
3108
|
+
}
|
|
3109
|
+
function buildLauncher(dir, sh) {
|
|
3110
|
+
const source = launcherSource(sh);
|
|
3111
|
+
const src = path15.join(dir, "git-launcher.cs");
|
|
3112
|
+
const exe = path15.join(dir, "git.exe");
|
|
3113
|
+
if (fs16.existsSync(exe) && fs16.existsSync(src) && fs16.readFileSync(src, "utf8") === source) return;
|
|
3114
|
+
const csc = findCsc();
|
|
3115
|
+
if (!csc) return;
|
|
3116
|
+
const tmp = path15.join(dir, `git.${process.pid}.tmp.exe`);
|
|
3117
|
+
const tmpSrc = `${src}.${process.pid}.tmp`;
|
|
3118
|
+
try {
|
|
3119
|
+
fs16.mkdirSync(dir, { recursive: true });
|
|
3120
|
+
fs16.writeFileSync(tmpSrc, source);
|
|
3121
|
+
const r = spawnSync9(csc, ["/nologo", "/optimize+", "/target:exe", `/out:${tmp}`, tmpSrc], { encoding: "utf8", timeout: 6e4, windowsHide: true });
|
|
3122
|
+
if (r.status !== 0 || !fs16.existsSync(tmp)) return;
|
|
3123
|
+
renameOver(tmp, exe);
|
|
3124
|
+
renameOver(tmpSrc, src);
|
|
3125
|
+
} catch {
|
|
3126
|
+
} finally {
|
|
3127
|
+
fs16.rmSync(tmp, { force: true });
|
|
3128
|
+
fs16.rmSync(tmpSrc, { force: true });
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
2980
3131
|
var HOOK_VERSION = 1;
|
|
2981
3132
|
var REF_HOOK = `#!/bin/sh
|
|
2982
3133
|
# pitroom git guard (layer 2, v${HOOK_VERSION}): refuse ref updates made by workers.
|
|
@@ -2984,12 +3135,12 @@ var REF_HOOK = `#!/bin/sh
|
|
|
2984
3135
|
echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
|
|
2985
3136
|
exit 1
|
|
2986
3137
|
`;
|
|
2987
|
-
var hooksDir = () =>
|
|
3138
|
+
var hooksDir = () => path15.join(home(), "git-hooks", `v${HOOK_VERSION}`);
|
|
2988
3139
|
function writeIfChanged2(file2, content) {
|
|
2989
|
-
if (
|
|
2990
|
-
|
|
3140
|
+
if (fs16.existsSync(file2) && fs16.readFileSync(file2, "utf8") === content) return;
|
|
3141
|
+
fs16.mkdirSync(path15.dirname(file2), { recursive: true });
|
|
2991
3142
|
const tmp = `${file2}.${process.pid}.tmp`;
|
|
2992
|
-
|
|
3143
|
+
fs16.writeFileSync(tmp, content, { mode: 493 });
|
|
2993
3144
|
renameOver(tmp, file2);
|
|
2994
3145
|
}
|
|
2995
3146
|
function pathKey(env) {
|
|
@@ -3021,7 +3172,7 @@ function guardEnv(env) {
|
|
|
3021
3172
|
GIT_PAGER: "cat",
|
|
3022
3173
|
PAGER: "cat"
|
|
3023
3174
|
};
|
|
3024
|
-
writeIfChanged2(
|
|
3175
|
+
writeIfChanged2(path15.join(hooksDir(), "reference-transaction"), REF_HOOK);
|
|
3025
3176
|
env = withGitConfig(onePath(env), [
|
|
3026
3177
|
["core.hooksPath", fwd(hooksDir())],
|
|
3027
3178
|
["url.pitroom-push-blocked://.pushInsteadOf", ""]
|
|
@@ -3033,11 +3184,12 @@ function guardEnv(env) {
|
|
|
3033
3184
|
if (WINDOWS) {
|
|
3034
3185
|
sh = findGitSh(real);
|
|
3035
3186
|
if (!sh) return { ...env, ...quiet };
|
|
3036
|
-
writeIfChanged2(
|
|
3187
|
+
writeIfChanged2(path15.join(dir, "git.cmd"), cmdShim(sh));
|
|
3188
|
+
buildLauncher(dir, sh);
|
|
3037
3189
|
}
|
|
3038
|
-
writeIfChanged2(
|
|
3190
|
+
writeIfChanged2(path15.join(dir, "git"), SHIM);
|
|
3039
3191
|
const key = pathKey(env);
|
|
3040
|
-
return { ...env, ...quiet, PITROOM_REAL_GIT: fwd(real), [key]: `${dir}${
|
|
3192
|
+
return { ...env, ...quiet, PITROOM_REAL_GIT: fwd(real), [key]: `${dir}${path15.delimiter}${env[key] ?? ""}` };
|
|
3041
3193
|
}
|
|
3042
3194
|
function shimReady() {
|
|
3043
3195
|
const env = guardEnv({ ...process.env });
|
|
@@ -3046,8 +3198,8 @@ function shimReady() {
|
|
|
3046
3198
|
|
|
3047
3199
|
// src/core/process.ts
|
|
3048
3200
|
async function spawnWorker(inv, opts) {
|
|
3049
|
-
const out =
|
|
3050
|
-
const err =
|
|
3201
|
+
const out = fs17.openSync(opts.stdoutFile, "w");
|
|
3202
|
+
const err = fs17.openSync(opts.stderrFile, "w");
|
|
3051
3203
|
const res = { code: null, timedOut: false, stopped: false };
|
|
3052
3204
|
const child = spawn(inv.command, inv.args, {
|
|
3053
3205
|
cwd: opts.cwd,
|
|
@@ -3070,7 +3222,7 @@ async function spawnWorker(inv, opts) {
|
|
|
3070
3222
|
}, opts.timeoutSec * 1e3);
|
|
3071
3223
|
res.code = await new Promise((resolve2) => {
|
|
3072
3224
|
child.on("error", (e) => {
|
|
3073
|
-
res.spawnError = e.code === "ENOENT" ?
|
|
3225
|
+
res.spawnError = e.code === "ENOENT" ? fs17.existsSync(opts.cwd) ? `${inv.command} not found` : `the working directory ${opts.cwd} does not exist` : e.message;
|
|
3074
3226
|
resolve2(127);
|
|
3075
3227
|
});
|
|
3076
3228
|
child.on("close", (c) => resolve2(c));
|
|
@@ -3079,8 +3231,8 @@ async function spawnWorker(inv, opts) {
|
|
|
3079
3231
|
if (killer) clearTimeout(killer);
|
|
3080
3232
|
process.off("SIGINT", stop);
|
|
3081
3233
|
process.off("SIGTERM", stop);
|
|
3082
|
-
|
|
3083
|
-
|
|
3234
|
+
fs17.closeSync(out);
|
|
3235
|
+
fs17.closeSync(err);
|
|
3084
3236
|
return res;
|
|
3085
3237
|
}
|
|
3086
3238
|
|
|
@@ -3112,12 +3264,13 @@ function parseAudit(text) {
|
|
|
3112
3264
|
}
|
|
3113
3265
|
|
|
3114
3266
|
// src/core/cooldown.ts
|
|
3115
|
-
import
|
|
3116
|
-
import
|
|
3267
|
+
import fs18 from "node:fs";
|
|
3268
|
+
import path16 from "node:path";
|
|
3117
3269
|
var MAX_MS = 24 * 36e5;
|
|
3118
3270
|
var OVERLOAD_MS = 20 * 6e4;
|
|
3271
|
+
var USAGE_MS = 36e5;
|
|
3119
3272
|
var DAILY_MS = 4 * 36e5;
|
|
3120
|
-
var file = () =>
|
|
3273
|
+
var file = () => path16.join(home(), "cooldowns.json");
|
|
3121
3274
|
function cooldownKey(target, backend) {
|
|
3122
3275
|
const model = (target.model ?? backend.defaultModel?.() ?? "").split("#")[0];
|
|
3123
3276
|
return `${target.backend}:${model}`;
|
|
@@ -3133,23 +3286,54 @@ function retryAfterMs(message) {
|
|
|
3133
3286
|
}
|
|
3134
3287
|
return ms > 0 ? ms : void 0;
|
|
3135
3288
|
}
|
|
3136
|
-
function
|
|
3137
|
-
const
|
|
3289
|
+
function wallClock(at, timeZone) {
|
|
3290
|
+
const p = Object.fromEntries(
|
|
3291
|
+
new Intl.DateTimeFormat("en-US", { timeZone, hourCycle: "h23", year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", second: "numeric" }).formatToParts(at).map((x) => [x.type, Number(x.value)])
|
|
3292
|
+
);
|
|
3293
|
+
return Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);
|
|
3294
|
+
}
|
|
3295
|
+
function retryAtMs(message, now = Date.now()) {
|
|
3296
|
+
const m = /(?:try again at|resets?(?: at)?)\s+(\d{1,2})(?::(\d{2}))?\s*([ap])\.?m\b\.?(?:\s*\(([A-Za-z_]+(?:\/[A-Za-z0-9_+-]+)*)\))?/i.exec(message) ?? /(?:try again at|resets?(?: at)?)\s+(\d{1,2}):(\d{2})()(?!\d)(?:\s*\(([A-Za-z_]+(?:\/[A-Za-z0-9_+-]+)*)\))?/i.exec(message);
|
|
3297
|
+
if (!m) return void 0;
|
|
3298
|
+
let hour = Number(m[1]);
|
|
3299
|
+
const minute = Number(m[2] ?? 0);
|
|
3300
|
+
const half = m[3]?.toLowerCase();
|
|
3301
|
+
if (minute > 59 || (half ? hour < 1 || hour > 12 : hour > 23)) return void 0;
|
|
3302
|
+
if (half === "p" && hour < 12) hour += 12;
|
|
3303
|
+
if (half === "a" && hour === 12) hour = 0;
|
|
3304
|
+
let zone = m[4];
|
|
3305
|
+
try {
|
|
3306
|
+
if (zone) new Intl.DateTimeFormat("en-US", { timeZone: zone });
|
|
3307
|
+
} catch {
|
|
3308
|
+
zone = void 0;
|
|
3309
|
+
}
|
|
3310
|
+
const wall = wallClock(now, zone);
|
|
3311
|
+
for (const day of [0, 1]) {
|
|
3312
|
+
const d = new Date(wall);
|
|
3313
|
+
const target = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + day, hour, minute);
|
|
3314
|
+
const at = target - (wallClock(target, zone) - target);
|
|
3315
|
+
if (at > now) return at - now;
|
|
3316
|
+
}
|
|
3317
|
+
return void 0;
|
|
3318
|
+
}
|
|
3319
|
+
function cooldownMs(message, now = Date.now()) {
|
|
3320
|
+
const told = retryAfterMs(message) ?? retryAtMs(message, now);
|
|
3138
3321
|
if (told !== void 0) return Math.min(Math.max(told, 6e4), MAX_MS);
|
|
3139
|
-
|
|
3322
|
+
if (/daily|per.?day|per day|exhausted your/i.test(message)) return DAILY_MS;
|
|
3323
|
+
return /usage limit|session limit|quota|FreeUsageLimit/i.test(message) ? USAGE_MS : OVERLOAD_MS;
|
|
3140
3324
|
}
|
|
3141
3325
|
function read() {
|
|
3142
3326
|
try {
|
|
3143
|
-
const raw = JSON.parse(
|
|
3327
|
+
const raw = JSON.parse(fs18.readFileSync(file(), "utf8"));
|
|
3144
3328
|
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
3145
3329
|
} catch {
|
|
3146
3330
|
return {};
|
|
3147
3331
|
}
|
|
3148
3332
|
}
|
|
3149
3333
|
function write(all) {
|
|
3150
|
-
|
|
3334
|
+
fs18.mkdirSync(path16.dirname(file()), { recursive: true });
|
|
3151
3335
|
const tmp = `${file()}.${process.pid}.tmp`;
|
|
3152
|
-
|
|
3336
|
+
fs18.writeFileSync(tmp, `${JSON.stringify(all, null, 2)}
|
|
3153
3337
|
`);
|
|
3154
3338
|
renameOver(tmp, file());
|
|
3155
3339
|
}
|
|
@@ -3160,7 +3344,7 @@ var activeCooldown = (key, now = Date.now()) => activeCooldowns(now)[key];
|
|
|
3160
3344
|
function recordCooldown(key, targetName, message, now = Date.now()) {
|
|
3161
3345
|
try {
|
|
3162
3346
|
const all = activeCooldowns(now);
|
|
3163
|
-
all[key] = { until: new Date(now + cooldownMs(message)).toISOString(), reason: message.replace(/\s+/g, " ").trim().slice(0, 160), target: targetName };
|
|
3347
|
+
all[key] = { until: new Date(now + cooldownMs(message, now)).toISOString(), reason: message.replace(/\s+/g, " ").trim().slice(0, 160), target: targetName };
|
|
3164
3348
|
write(all);
|
|
3165
3349
|
} catch {
|
|
3166
3350
|
}
|
|
@@ -3179,15 +3363,15 @@ function untilText(c, now = Date.now()) {
|
|
|
3179
3363
|
}
|
|
3180
3364
|
|
|
3181
3365
|
// src/core/snapshot.ts
|
|
3182
|
-
import
|
|
3183
|
-
import
|
|
3366
|
+
import fs19 from "node:fs";
|
|
3367
|
+
import path17 from "node:path";
|
|
3184
3368
|
import crypto5 from "node:crypto";
|
|
3185
3369
|
var READ_IN = ["auto", "snapshot", "project"];
|
|
3186
3370
|
var KEEP_MS = 24 * 36e5;
|
|
3187
3371
|
var PRUNE_AGE_MS = 3 * 24 * 36e5;
|
|
3188
3372
|
var KEEP_NEWEST = 6;
|
|
3189
3373
|
var JUST_USED_MS = 10 * 6e4;
|
|
3190
|
-
var snapshotsDir = () =>
|
|
3374
|
+
var snapshotsDir = () => path17.join(home(), "snapshots");
|
|
3191
3375
|
function wantSnapshot(readIn2, root, dir) {
|
|
3192
3376
|
if (!root || readIn2 === "project") return void 0;
|
|
3193
3377
|
const secrets = findSecretFiles(dir);
|
|
@@ -3196,8 +3380,8 @@ function wantSnapshot(readIn2, root, dir) {
|
|
|
3196
3380
|
function readSnapshot(root, owner) {
|
|
3197
3381
|
const drop2 = findSecretFilesInTree(root, root);
|
|
3198
3382
|
const tree = snapshotTree(root, [], drop2);
|
|
3199
|
-
const dest =
|
|
3200
|
-
if (
|
|
3383
|
+
const dest = path17.join(snapshotsDir(), owner ? `${tree}-${owner}` : tree);
|
|
3384
|
+
if (fs19.existsSync(path17.join(dest, ".git"))) {
|
|
3201
3385
|
touch(dest);
|
|
3202
3386
|
return { dir: dest, tree, created: false };
|
|
3203
3387
|
}
|
|
@@ -3206,19 +3390,19 @@ function readSnapshot(root, owner) {
|
|
|
3206
3390
|
return { dir: dest, tree, created: true };
|
|
3207
3391
|
}
|
|
3208
3392
|
function restoreSnapshot(root, dir, tree) {
|
|
3209
|
-
const rel =
|
|
3210
|
-
if (!rel || rel.startsWith("..") ||
|
|
3211
|
-
if (!
|
|
3393
|
+
const rel = path17.relative(snapshotsDir(), dir);
|
|
3394
|
+
if (!rel || rel.startsWith("..") || path17.isAbsolute(rel)) throw new Error(`not a snapshot: ${dir}`);
|
|
3395
|
+
if (!fs19.existsSync(path17.join(dir, ".git"))) materialize(root, tree, dir);
|
|
3212
3396
|
}
|
|
3213
3397
|
function materialize(root, tree, dest) {
|
|
3214
|
-
const tmp =
|
|
3398
|
+
const tmp = path17.join(snapshotsDir(), `.tmp-${process.pid}-${crypto5.randomBytes(3).toString("hex")}`);
|
|
3215
3399
|
try {
|
|
3216
3400
|
createIsolatedCopy(root, tree, tmp);
|
|
3217
3401
|
readOnly(tmp);
|
|
3218
3402
|
try {
|
|
3219
|
-
|
|
3403
|
+
fs19.renameSync(tmp, dest);
|
|
3220
3404
|
} catch (e) {
|
|
3221
|
-
if (!
|
|
3405
|
+
if (!fs19.existsSync(path17.join(dest, ".git"))) throw e;
|
|
3222
3406
|
removeIsolatedCopy(tmp, snapshotsDir());
|
|
3223
3407
|
}
|
|
3224
3408
|
} catch (e) {
|
|
@@ -3232,17 +3416,17 @@ function materialize(root, tree, dest) {
|
|
|
3232
3416
|
function touch(dir) {
|
|
3233
3417
|
try {
|
|
3234
3418
|
const now = /* @__PURE__ */ new Date();
|
|
3235
|
-
|
|
3419
|
+
fs19.utimesSync(dir, now, now);
|
|
3236
3420
|
} catch {
|
|
3237
3421
|
}
|
|
3238
3422
|
}
|
|
3239
3423
|
function readOnly(dir) {
|
|
3240
3424
|
const walk = (d) => {
|
|
3241
|
-
for (const e of
|
|
3425
|
+
for (const e of fs19.readdirSync(d, { withFileTypes: true })) {
|
|
3242
3426
|
if (e.name === ".git") continue;
|
|
3243
|
-
const p =
|
|
3427
|
+
const p = path17.join(d, e.name);
|
|
3244
3428
|
if (e.isDirectory()) walk(p);
|
|
3245
|
-
else if (e.isFile())
|
|
3429
|
+
else if (e.isFile()) fs19.chmodSync(p, fs19.statSync(p).mode & ~146);
|
|
3246
3430
|
}
|
|
3247
3431
|
};
|
|
3248
3432
|
try {
|
|
@@ -3253,7 +3437,7 @@ function readOnly(dir) {
|
|
|
3253
3437
|
function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWEST) {
|
|
3254
3438
|
let names;
|
|
3255
3439
|
try {
|
|
3256
|
-
names =
|
|
3440
|
+
names = fs19.readdirSync(snapshotsDir());
|
|
3257
3441
|
} catch {
|
|
3258
3442
|
return 0;
|
|
3259
3443
|
}
|
|
@@ -3261,14 +3445,14 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
|
|
|
3261
3445
|
for (const id2 of listRunIds()) {
|
|
3262
3446
|
try {
|
|
3263
3447
|
const m = readMeta(id2);
|
|
3264
|
-
if (m.snapshot?.dir && isActive(m.state)) inUse.add(
|
|
3448
|
+
if (m.snapshot?.dir && isActive(m.state)) inUse.add(path17.resolve(m.snapshot.dir));
|
|
3265
3449
|
} catch {
|
|
3266
3450
|
}
|
|
3267
3451
|
}
|
|
3268
3452
|
const entries = names.map((n) => {
|
|
3269
|
-
const dir =
|
|
3453
|
+
const dir = path17.join(snapshotsDir(), n);
|
|
3270
3454
|
try {
|
|
3271
|
-
return { n, dir, age: now -
|
|
3455
|
+
return { n, dir, age: now - fs19.statSync(dir).mtimeMs };
|
|
3272
3456
|
} catch {
|
|
3273
3457
|
return void 0;
|
|
3274
3458
|
}
|
|
@@ -3276,7 +3460,7 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
|
|
|
3276
3460
|
let removed = 0;
|
|
3277
3461
|
const rank = { shared: 0, own: 0 };
|
|
3278
3462
|
for (const e of entries) {
|
|
3279
|
-
if (inUse.has(
|
|
3463
|
+
if (inUse.has(path17.resolve(e.dir))) continue;
|
|
3280
3464
|
const half = e.n.startsWith(".tmp-");
|
|
3281
3465
|
const old = e.age >= (half ? KEEP_MS : maxAgeMs);
|
|
3282
3466
|
const surplus = !half && ++rank[e.n.includes("-") ? "own" : "shared"] > keepNewest && e.age >= JUST_USED_MS;
|
|
@@ -3292,10 +3476,10 @@ function prune(now = Date.now(), maxAgeMs = PRUNE_AGE_MS, keepNewest = KEEP_NEWE
|
|
|
3292
3476
|
}
|
|
3293
3477
|
function makeWritable(dir) {
|
|
3294
3478
|
const walk = (d) => {
|
|
3295
|
-
for (const e of
|
|
3296
|
-
const p =
|
|
3479
|
+
for (const e of fs19.readdirSync(d, { withFileTypes: true })) {
|
|
3480
|
+
const p = path17.join(d, e.name);
|
|
3297
3481
|
if (e.isDirectory()) walk(p);
|
|
3298
|
-
else if (e.isFile())
|
|
3482
|
+
else if (e.isFile()) fs19.chmodSync(p, fs19.statSync(p).mode | 128);
|
|
3299
3483
|
}
|
|
3300
3484
|
};
|
|
3301
3485
|
try {
|
|
@@ -3306,17 +3490,17 @@ function makeWritable(dir) {
|
|
|
3306
3490
|
function backToProject(text, snapshotDir, root) {
|
|
3307
3491
|
let real = snapshotDir;
|
|
3308
3492
|
try {
|
|
3309
|
-
real =
|
|
3493
|
+
real = fs19.realpathSync(snapshotDir);
|
|
3310
3494
|
} catch {
|
|
3311
3495
|
}
|
|
3312
3496
|
let out = text;
|
|
3313
|
-
for (const dir of /* @__PURE__ */ new Set([real, snapshotDir])) out = out.split(`${dir}${
|
|
3497
|
+
for (const dir of /* @__PURE__ */ new Set([real, snapshotDir])) out = out.split(`${dir}${path17.sep}`).join(`${root}${path17.sep}`).split(dir).join(root);
|
|
3314
3498
|
return out;
|
|
3315
3499
|
}
|
|
3316
3500
|
|
|
3317
3501
|
// src/core/audit.ts
|
|
3318
3502
|
import crypto6 from "node:crypto";
|
|
3319
|
-
import
|
|
3503
|
+
import path18 from "node:path";
|
|
3320
3504
|
var ANSWER_MAX = 8e3;
|
|
3321
3505
|
function auditRate(meta) {
|
|
3322
3506
|
return meta.auditRate ?? effective().audit.value;
|
|
@@ -3349,20 +3533,30 @@ function focusedRate(base2, task, record2) {
|
|
|
3349
3533
|
return { rate: Math.min(rate2, 1), why };
|
|
3350
3534
|
}
|
|
3351
3535
|
function pickAuditor(meta) {
|
|
3536
|
+
return auditors(meta)[0];
|
|
3537
|
+
}
|
|
3538
|
+
function auditors(meta) {
|
|
3352
3539
|
const eff = effective();
|
|
3353
3540
|
const tiers = eff.tiers.value;
|
|
3354
3541
|
const def = parseTarget(eff.worker.value, DEFAULT_BACKEND).backend;
|
|
3355
3542
|
const ran = meta.ran ?? meta.worker;
|
|
3543
|
+
const bare2 = (t) => ({ backend: t.backend, model: t.model?.split("#")[0] || void 0 });
|
|
3356
3544
|
const candidates = [tiers.audit, tiers.cheap, ...eff.fallback.value, eff.worker.value].filter((s) => !!s);
|
|
3357
|
-
|
|
3545
|
+
const out = [];
|
|
3546
|
+
for (const c of candidates) {
|
|
3547
|
+
const t = bare2(parseTarget(c, def));
|
|
3548
|
+
if (sameTarget(t, bare2(ran)) || out.some((o) => sameTarget(bare2(parseTarget(o, def)), t))) continue;
|
|
3549
|
+
out.push(c);
|
|
3550
|
+
}
|
|
3551
|
+
return out;
|
|
3358
3552
|
}
|
|
3359
3553
|
function relativeToAuditor(answer, meta) {
|
|
3360
3554
|
let out = answer;
|
|
3361
3555
|
const roots = [...new Set([meta.dir, meta.repoRoot].filter((r) => !!r))].sort((a, b) => b.length - a.length);
|
|
3362
3556
|
for (const root of roots) {
|
|
3363
|
-
const rel =
|
|
3364
|
-
const prefix = rel ? `${rel.split(
|
|
3365
|
-
out = out.split(`${root}${
|
|
3557
|
+
const rel = path18.relative(meta.dir, root);
|
|
3558
|
+
const prefix = rel ? `${rel.split(path18.sep).join("/")}/` : "";
|
|
3559
|
+
out = out.split(`${root}${path18.sep}`).join(prefix).replace(new RegExp(`${root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w.-])`, "g"), rel || ".");
|
|
3366
3560
|
}
|
|
3367
3561
|
return out;
|
|
3368
3562
|
}
|
|
@@ -3432,19 +3626,19 @@ function auditBadge(meta) {
|
|
|
3432
3626
|
}
|
|
3433
3627
|
|
|
3434
3628
|
// src/core/history.ts
|
|
3435
|
-
import
|
|
3629
|
+
import fs23 from "node:fs";
|
|
3436
3630
|
import { createRequire } from "node:module";
|
|
3437
|
-
import
|
|
3631
|
+
import path21 from "node:path";
|
|
3438
3632
|
import zlib from "node:zlib";
|
|
3439
3633
|
|
|
3440
3634
|
// src/core/receipt.ts
|
|
3441
|
-
import
|
|
3442
|
-
import
|
|
3635
|
+
import fs21 from "node:fs";
|
|
3636
|
+
import path20 from "node:path";
|
|
3443
3637
|
|
|
3444
3638
|
// src/core/prices.ts
|
|
3445
3639
|
import { spawn as spawn2 } from "node:child_process";
|
|
3446
|
-
import
|
|
3447
|
-
import
|
|
3640
|
+
import fs20 from "node:fs";
|
|
3641
|
+
import path19 from "node:path";
|
|
3448
3642
|
var DEFAULT_URL = "https://models.dev/api.json";
|
|
3449
3643
|
var RETRY_AFTER_FAILURE_MS = 60 * 6e4;
|
|
3450
3644
|
var MIN_MODELS = 100;
|
|
@@ -3457,8 +3651,8 @@ function catalogLabel() {
|
|
|
3457
3651
|
return url;
|
|
3458
3652
|
}
|
|
3459
3653
|
}
|
|
3460
|
-
var catalogFile = () =>
|
|
3461
|
-
var attemptFile = () =>
|
|
3654
|
+
var catalogFile = () => path19.join(home(), "prices.json");
|
|
3655
|
+
var attemptFile = () => path19.join(home(), "prices.attempt");
|
|
3462
3656
|
function trim(raw) {
|
|
3463
3657
|
const out = {};
|
|
3464
3658
|
if (!raw || typeof raw !== "object") return out;
|
|
@@ -3476,9 +3670,9 @@ var cached2;
|
|
|
3476
3670
|
function readCatalog() {
|
|
3477
3671
|
const file2 = catalogFile();
|
|
3478
3672
|
try {
|
|
3479
|
-
const { mtimeMs } =
|
|
3673
|
+
const { mtimeMs } = fs20.statSync(file2);
|
|
3480
3674
|
if (cached2?.file === file2 && cached2.mtimeMs === mtimeMs) return cached2.catalog;
|
|
3481
|
-
const parsed = JSON.parse(
|
|
3675
|
+
const parsed = JSON.parse(fs20.readFileSync(file2, "utf8"));
|
|
3482
3676
|
const catalog6 = parsed && typeof parsed.models === "object" && parsed.models && Number.isFinite(Date.parse(parsed.fetchedAt)) ? parsed : void 0;
|
|
3483
3677
|
cached2 = { file: file2, mtimeMs, catalog: catalog6 };
|
|
3484
3678
|
return catalog6;
|
|
@@ -3488,7 +3682,7 @@ function readCatalog() {
|
|
|
3488
3682
|
}
|
|
3489
3683
|
var ageMs = (file2) => {
|
|
3490
3684
|
try {
|
|
3491
|
-
return Date.now() -
|
|
3685
|
+
return Date.now() - fs20.statSync(file2).mtimeMs;
|
|
3492
3686
|
} catch {
|
|
3493
3687
|
return Infinity;
|
|
3494
3688
|
}
|
|
@@ -3507,17 +3701,17 @@ async function refreshCatalog() {
|
|
|
3507
3701
|
const models = trim(await r.json());
|
|
3508
3702
|
const n = Object.keys(models).length;
|
|
3509
3703
|
if (n < MIN_MODELS) throw new Error(`only ${n} priced models in it (a catalog has thousands): not used`);
|
|
3510
|
-
|
|
3704
|
+
fs20.mkdirSync(home(), { recursive: true });
|
|
3511
3705
|
const tmp = `${catalogFile()}.${process.pid}.tmp`;
|
|
3512
3706
|
const catalog6 = { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), source: url, models };
|
|
3513
|
-
|
|
3707
|
+
fs20.writeFileSync(tmp, JSON.stringify(catalog6));
|
|
3514
3708
|
renameOver(tmp, catalogFile());
|
|
3515
|
-
|
|
3709
|
+
fs20.rmSync(attemptFile(), { force: true });
|
|
3516
3710
|
return { state: "updated", models: n };
|
|
3517
3711
|
} catch (e) {
|
|
3518
3712
|
try {
|
|
3519
|
-
|
|
3520
|
-
|
|
3713
|
+
fs20.mkdirSync(home(), { recursive: true });
|
|
3714
|
+
fs20.writeFileSync(attemptFile(), `${(/* @__PURE__ */ new Date()).toISOString()} ${e.message}
|
|
3521
3715
|
`);
|
|
3522
3716
|
} catch {
|
|
3523
3717
|
}
|
|
@@ -3529,8 +3723,8 @@ function refreshInBackground() {
|
|
|
3529
3723
|
if (!refreshDue()) return;
|
|
3530
3724
|
const script = process.argv[1];
|
|
3531
3725
|
if (!script) return;
|
|
3532
|
-
|
|
3533
|
-
|
|
3726
|
+
fs20.mkdirSync(home(), { recursive: true });
|
|
3727
|
+
fs20.writeFileSync(attemptFile(), `${(/* @__PURE__ */ new Date()).toISOString()} refresh started
|
|
3534
3728
|
`);
|
|
3535
3729
|
const child = spawn2(process.execPath, [script, "prices", "--refresh", "--quiet"], { detached: true, stdio: "ignore", env: process.env });
|
|
3536
3730
|
child.on("error", () => void 0);
|
|
@@ -3560,7 +3754,7 @@ function catalogStatus() {
|
|
|
3560
3754
|
const c = readCatalog();
|
|
3561
3755
|
let lastFailure;
|
|
3562
3756
|
try {
|
|
3563
|
-
const t =
|
|
3757
|
+
const t = fs20.readFileSync(attemptFile(), "utf8").trim();
|
|
3564
3758
|
if (t && !t.endsWith("refresh started")) lastFailure = t;
|
|
3565
3759
|
} catch {
|
|
3566
3760
|
}
|
|
@@ -3611,12 +3805,21 @@ function workerPrice(backend, ...models) {
|
|
|
3611
3805
|
}
|
|
3612
3806
|
var costAt = (usage2, price) => (usage2.input * price.input + usage2.cacheRead * price.cachedInput + (usage2.output + usage2.reasoning) * price.output) / 1e6;
|
|
3613
3807
|
var workerCostOf = (usage2) => usage2?.cost ?? usage2?.costEstimate;
|
|
3808
|
+
function addUsage(a, b) {
|
|
3809
|
+
const sum = (k) => (a[k] ?? 0) + (b[k] ?? 0);
|
|
3810
|
+
const out = { input: sum("input"), output: sum("output"), reasoning: sum("reasoning"), cacheRead: sum("cacheRead"), cacheWrite: sum("cacheWrite"), total: sum("total"), steps: sum("steps"), toolCalls: sum("toolCalls"), denied: sum("denied") };
|
|
3811
|
+
const ca = workerCostOf(a);
|
|
3812
|
+
const cb = workerCostOf(b);
|
|
3813
|
+
if (ca === void 0 && cb === void 0) return out;
|
|
3814
|
+
if (a.cost !== void 0 && b.cost !== void 0) return { ...out, cost: a.cost + b.cost };
|
|
3815
|
+
return { ...out, costEstimate: (ca ?? 0) + (cb ?? 0), costSource: a.costSource ?? b.costSource };
|
|
3816
|
+
}
|
|
3614
3817
|
function savedUsd(usage2, returnedTokens, price = primaryPrice()) {
|
|
3615
3818
|
const readingTheReport = returnedTokens * price.input / 1e6;
|
|
3616
3819
|
return Math.max(0, costAt(usage2, price) - (workerCostOf(usage2) ?? 0) - readingTheReport);
|
|
3617
3820
|
}
|
|
3618
3821
|
function record(meta) {
|
|
3619
|
-
if (!meta.usage?.steps
|
|
3822
|
+
if (meta.auditOf || !meta.usage?.steps && meta.state !== "done") return false;
|
|
3620
3823
|
const entry = {
|
|
3621
3824
|
id: meta.id,
|
|
3622
3825
|
at: meta.endedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -3624,22 +3827,49 @@ function record(meta) {
|
|
|
3624
3827
|
state: meta.state,
|
|
3625
3828
|
backend: (meta.ran ?? meta.worker).backend,
|
|
3626
3829
|
model: meta.resolvedModel ?? (meta.ran ?? meta.worker).model,
|
|
3627
|
-
tokens: meta.usage
|
|
3830
|
+
tokens: meta.usage?.total ?? 0,
|
|
3628
3831
|
returned: meta.returnedTokens ?? 0,
|
|
3629
3832
|
workerCost: workerCostOf(meta.usage) ?? 0,
|
|
3630
|
-
...meta.usage
|
|
3833
|
+
...meta.usage?.cost === void 0 && meta.usage?.costEstimate !== void 0 ? { costEstimated: true, costSource: meta.usage.costSource } : {},
|
|
3631
3834
|
saved: meta.savedUsd ?? 0,
|
|
3632
3835
|
price: primaryPrice().name
|
|
3633
3836
|
};
|
|
3634
|
-
|
|
3635
|
-
|
|
3837
|
+
fs21.mkdirSync(path20.dirname(ledgerFile()), { recursive: true });
|
|
3838
|
+
fs21.appendFileSync(ledgerFile(), `${JSON.stringify(entry)}
|
|
3636
3839
|
`);
|
|
3840
|
+
return true;
|
|
3841
|
+
}
|
|
3842
|
+
var SETTLING_MS = 10 * 6e4;
|
|
3843
|
+
function settleLedger(metas) {
|
|
3844
|
+
const known = new Set(readLedger().map((e) => e.id));
|
|
3845
|
+
let n = 0;
|
|
3846
|
+
for (const m of metas ?? finishedRuns()) {
|
|
3847
|
+
if (known.has(m.id) || !TERMINAL.includes(m.state)) continue;
|
|
3848
|
+
if (m.pid !== process.pid && isAlive(m.pid) && (!m.endedAt || Date.now() - Date.parse(m.endedAt) < SETTLING_MS)) continue;
|
|
3849
|
+
if (record(m)) {
|
|
3850
|
+
known.add(m.id);
|
|
3851
|
+
n++;
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
return n;
|
|
3855
|
+
}
|
|
3856
|
+
function* finishedRuns() {
|
|
3857
|
+
for (const id2 of listRunIds()) {
|
|
3858
|
+
try {
|
|
3859
|
+
yield readMeta(id2);
|
|
3860
|
+
} catch {
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3637
3863
|
}
|
|
3638
3864
|
function readLedger(sinceMs2) {
|
|
3639
|
-
if (!
|
|
3640
|
-
|
|
3865
|
+
if (!fs21.existsSync(ledgerFile())) return [];
|
|
3866
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3867
|
+
return fs21.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
|
|
3641
3868
|
try {
|
|
3642
|
-
|
|
3869
|
+
const e = JSON.parse(l);
|
|
3870
|
+
if (seen.has(e.id)) return [];
|
|
3871
|
+
seen.add(e.id);
|
|
3872
|
+
return [e];
|
|
3643
3873
|
} catch {
|
|
3644
3874
|
return [];
|
|
3645
3875
|
}
|
|
@@ -3712,7 +3942,7 @@ ${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.rou
|
|
|
3712
3942
|
}
|
|
3713
3943
|
|
|
3714
3944
|
// src/core/ui.ts
|
|
3715
|
-
import
|
|
3945
|
+
import fs22 from "node:fs";
|
|
3716
3946
|
var WEEK_MS = 7 * 24 * 3600 * 1e3;
|
|
3717
3947
|
var RECENT = 40;
|
|
3718
3948
|
var RECENT_HOURS = 1;
|
|
@@ -3803,9 +4033,9 @@ function cardsFor(m) {
|
|
|
3803
4033
|
const out = [];
|
|
3804
4034
|
for (const [phase, text] of phases) {
|
|
3805
4035
|
const marker = runFile(m.id, `card-${phase}`);
|
|
3806
|
-
if (
|
|
4036
|
+
if (fs22.existsSync(marker)) continue;
|
|
3807
4037
|
if (!(phase === "started" && !isActive(m.state))) out.push(text());
|
|
3808
|
-
|
|
4038
|
+
fs22.writeFileSync(marker, "");
|
|
3809
4039
|
}
|
|
3810
4040
|
return out;
|
|
3811
4041
|
}
|
|
@@ -3816,10 +4046,10 @@ function hookCards(input) {
|
|
|
3816
4046
|
const response = event.tool_response;
|
|
3817
4047
|
const output = typeof response === "string" ? response : [response?.stdout, response?.stderr].filter(Boolean).join("\n");
|
|
3818
4048
|
const ids = [...new Set(`${command}
|
|
3819
|
-
${output}`.match(RUN_ID) ?? [])].filter((id2) =>
|
|
4049
|
+
${output}`.match(RUN_ID) ?? [])].filter((id2) => fs22.existsSync(`${runsDir()}/${id2}`));
|
|
3820
4050
|
const since2 = Date.now() - RECENT_HOURS * 36e5;
|
|
3821
4051
|
for (const id2 of listRunIds().slice(-RECENT)) {
|
|
3822
|
-
if (ids.includes(id2) ||
|
|
4052
|
+
if (ids.includes(id2) || fs22.existsSync(runFile(id2, "card-ended"))) continue;
|
|
3823
4053
|
try {
|
|
3824
4054
|
const m = readMeta(id2);
|
|
3825
4055
|
if (!isActive(m.state) && Date.parse(m.endedAt ?? m.startedAt) > since2) ids.push(id2);
|
|
@@ -3841,7 +4071,7 @@ var SCHEMA_VERSION = 1;
|
|
|
3841
4071
|
var PATCH_MAX = 1e6;
|
|
3842
4072
|
var ANSWER_MAX2 = 2e5;
|
|
3843
4073
|
var cached3;
|
|
3844
|
-
var historyFile = () =>
|
|
4074
|
+
var historyFile = () => path21.join(home(), "history.db");
|
|
3845
4075
|
function openDb() {
|
|
3846
4076
|
const file2 = historyFile();
|
|
3847
4077
|
if (cached3?.file === file2) return cached3.db;
|
|
@@ -3855,7 +4085,7 @@ function openDb() {
|
|
|
3855
4085
|
} finally {
|
|
3856
4086
|
process.emitWarning = emit;
|
|
3857
4087
|
}
|
|
3858
|
-
|
|
4088
|
+
fs23.mkdirSync(path21.dirname(file2), { recursive: true });
|
|
3859
4089
|
db = new DatabaseSync(file2);
|
|
3860
4090
|
db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
|
|
3861
4091
|
migrate(db);
|
|
@@ -3890,31 +4120,31 @@ function migrate(db) {
|
|
|
3890
4120
|
function readRunFile(id2, name) {
|
|
3891
4121
|
const f = runFile(id2, name);
|
|
3892
4122
|
try {
|
|
3893
|
-
if (
|
|
3894
|
-
if (
|
|
4123
|
+
if (fs23.existsSync(f)) return fs23.readFileSync(f, "utf8");
|
|
4124
|
+
if (fs23.existsSync(`${f}.gz`)) return zlib.gunzipSync(fs23.readFileSync(`${f}.gz`)).toString("utf8");
|
|
3895
4125
|
} catch {
|
|
3896
4126
|
}
|
|
3897
4127
|
return void 0;
|
|
3898
4128
|
}
|
|
3899
4129
|
function gzipFile(f) {
|
|
3900
|
-
if (!
|
|
3901
|
-
|
|
3902
|
-
|
|
4130
|
+
if (!fs23.existsSync(f)) return;
|
|
4131
|
+
fs23.writeFileSync(`${f}.gz`, zlib.gzipSync(fs23.readFileSync(f)));
|
|
4132
|
+
fs23.rmSync(f);
|
|
3903
4133
|
}
|
|
3904
4134
|
function compactRun(meta) {
|
|
3905
4135
|
if (!TERMINAL.includes(meta.state)) return;
|
|
3906
|
-
const dir =
|
|
4136
|
+
const dir = path21.dirname(runFile(meta.id, "meta.json"));
|
|
3907
4137
|
let names;
|
|
3908
4138
|
try {
|
|
3909
|
-
names =
|
|
4139
|
+
names = fs23.readdirSync(dir);
|
|
3910
4140
|
} catch {
|
|
3911
4141
|
return;
|
|
3912
4142
|
}
|
|
3913
4143
|
for (const n of names) {
|
|
3914
|
-
if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(
|
|
4144
|
+
if (/^events(\.attempt-\d+)?\.jsonl$/.test(n)) gzipFile(path21.join(dir, n));
|
|
3915
4145
|
else if (/^stderr(\.attempt-\d+)?\.log$/.test(n)) {
|
|
3916
|
-
if (meta.state === "done" && !/attempt/.test(n))
|
|
3917
|
-
else gzipFile(
|
|
4146
|
+
if (meta.state === "done" && !/attempt/.test(n)) fs23.rmSync(path21.join(dir, n), { force: true });
|
|
4147
|
+
else gzipFile(path21.join(dir, n));
|
|
3918
4148
|
}
|
|
3919
4149
|
}
|
|
3920
4150
|
}
|
|
@@ -4036,6 +4266,7 @@ function archivedId(ref) {
|
|
|
4036
4266
|
}
|
|
4037
4267
|
}
|
|
4038
4268
|
var VERIFY_FAILED = "(r.state = 'done' AND json_extract(r.meta_json, '$.verifyResult.ok') = 0)";
|
|
4269
|
+
var COST_ESTIMATED = "(json_extract(r.meta_json, '$.usage.cost') IS NULL AND json_extract(r.meta_json, '$.usage.costEstimate') IS NOT NULL)";
|
|
4039
4270
|
var ftsQuery = (text) => text.split(/\s+/).filter(Boolean).map((w) => `"${w.replace(/"/g, '""')}"*`).join(" ");
|
|
4040
4271
|
function listHistory(q = {}) {
|
|
4041
4272
|
const db = openDb();
|
|
@@ -4070,7 +4301,7 @@ function listHistory(q = {}) {
|
|
|
4070
4301
|
try {
|
|
4071
4302
|
const total = db.prepare(`SELECT COUNT(*) AS n FROM runs r ${base2}`).get(...args).n;
|
|
4072
4303
|
const page2 = q.beforeId ? `${base2 ? `${base2} AND` : "WHERE"} r.id < ?` : base2;
|
|
4073
|
-
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));
|
|
4304
|
+
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, ${COST_ESTIMATED} AS cost_estimated 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));
|
|
4074
4305
|
return {
|
|
4075
4306
|
total,
|
|
4076
4307
|
rows: rows.map((r) => ({
|
|
@@ -4088,6 +4319,8 @@ function listHistory(q = {}) {
|
|
|
4088
4319
|
seconds: r.seconds ?? void 0,
|
|
4089
4320
|
steps: r.steps ?? void 0,
|
|
4090
4321
|
tokens: r.tokens ?? void 0,
|
|
4322
|
+
cost: r.cost ?? void 0,
|
|
4323
|
+
...r.cost_estimated ? { costEstimated: true } : {},
|
|
4091
4324
|
saved: r.saved ?? void 0,
|
|
4092
4325
|
files: r.files_changed ?? 0,
|
|
4093
4326
|
applied: !!r.applied
|
|
@@ -4122,18 +4355,32 @@ function auditStats(db, since2) {
|
|
|
4122
4355
|
}
|
|
4123
4356
|
return { total, byWorker };
|
|
4124
4357
|
}
|
|
4125
|
-
|
|
4358
|
+
var WORK_SQL = { research: "kind = 'research'", change: "kind LIKE 'change%'", review: "kind LIKE '%review'" };
|
|
4359
|
+
function workerRecord(backend, model, sinceMs2, work) {
|
|
4126
4360
|
const db = openDb();
|
|
4127
4361
|
if (!db) return void 0;
|
|
4128
4362
|
const since2 = new Date(sinceMs2).toISOString();
|
|
4129
4363
|
const fits = (m) => !model || m === model || !!m?.endsWith(`/${model}`) || model.endsWith(`/${m}`);
|
|
4130
4364
|
const rec = { runs: 0, ok: 0, audited: 0, agreed: 0 };
|
|
4131
4365
|
try {
|
|
4132
|
-
for (const r of db.prepare(`SELECT model, SUM(NOT ${LIMITED}) runs, SUM(state='done') ok FROM runs WHERE backend = ? AND started_at >= ? AND ${NOT_AUDIT} AND ${ATTEMPTED} GROUP BY model`).all(backend, since2)) {
|
|
4366
|
+
for (const r of db.prepare(`SELECT model, SUM(NOT ${LIMITED}) runs, SUM(state='done') ok FROM runs WHERE backend = ? AND started_at >= ? AND ${NOT_AUDIT} AND ${ATTEMPTED} AND ${work ? WORK_SQL[work] : "1"} GROUP BY model`).all(backend, since2)) {
|
|
4133
4367
|
if (!fits(r.model)) continue;
|
|
4134
4368
|
rec.runs += r.runs ?? 0;
|
|
4135
4369
|
rec.ok += r.ok ?? 0;
|
|
4136
4370
|
}
|
|
4371
|
+
if (work === "review") return rec;
|
|
4372
|
+
if (work === "change") {
|
|
4373
|
+
const reviews = db.prepare(`SELECT r.model, json_extract(v.meta_json, '$.verdict.spec') spec, json_extract(v.meta_json, '$.verdict.quality') quality, COUNT(*) n
|
|
4374
|
+
FROM runs v JOIN runs r ON r.id = v.review_of
|
|
4375
|
+
WHERE r.backend = ? AND v.started_at >= ? AND v.state = 'done' AND r.kind LIKE 'change%' AND json_extract(v.meta_json, '$.reviewOf') IS NOT NULL
|
|
4376
|
+
GROUP BY r.model, spec, quality`).all(backend, since2);
|
|
4377
|
+
for (const r of reviews) {
|
|
4378
|
+
if (!fits(r.model) || !["approved", "needs-fixes"].includes(r.quality)) continue;
|
|
4379
|
+
rec.audited += r.n;
|
|
4380
|
+
if (r.spec === "pass" && r.quality === "approved") rec.agreed += r.n;
|
|
4381
|
+
}
|
|
4382
|
+
return rec;
|
|
4383
|
+
}
|
|
4137
4384
|
const audits = db.prepare(`SELECT r.model, json_extract(a.meta_json, '$.auditVerdict') v, COUNT(*) n
|
|
4138
4385
|
FROM runs a JOIN runs r ON r.id = json_extract(a.meta_json, '$.auditOf')
|
|
4139
4386
|
WHERE r.backend = ? AND a.started_at >= ? AND a.state = 'done' AND json_extract(a.meta_json, '$.auditOf') IS NOT NULL
|
|
@@ -4217,7 +4464,7 @@ function historyStats(sinceMs2) {
|
|
|
4217
4464
|
}
|
|
4218
4465
|
|
|
4219
4466
|
// src/core/budget.ts
|
|
4220
|
-
import
|
|
4467
|
+
import fs24 from "node:fs";
|
|
4221
4468
|
var startOfDay = (now = /* @__PURE__ */ new Date()) => new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
4222
4469
|
function activeSpend(sinceMs2) {
|
|
4223
4470
|
let total = 0;
|
|
@@ -4227,8 +4474,8 @@ function activeSpend(sinceMs2) {
|
|
|
4227
4474
|
if (!isActive(m.state) || Date.parse(m.startedAt) < sinceMs2) continue;
|
|
4228
4475
|
const target = m.ran ?? m.worker;
|
|
4229
4476
|
const events = runFile(id2, "events.jsonl");
|
|
4230
|
-
if (!
|
|
4231
|
-
const run2 = getBackend(target.backend).parse(
|
|
4477
|
+
if (!fs24.existsSync(events)) continue;
|
|
4478
|
+
const run2 = getBackend(target.backend).parse(fs24.readFileSync(events, "utf8"));
|
|
4232
4479
|
if (run2.usage.cost !== void 0) total += run2.usage.cost;
|
|
4233
4480
|
else {
|
|
4234
4481
|
const price = workerPrice(target.backend, run2.model, target.model);
|
|
@@ -4279,7 +4526,7 @@ function scoreOf(r) {
|
|
|
4279
4526
|
return r.audited >= MIN_AUDITS ? ok * (r.agreed / r.audited) : ok;
|
|
4280
4527
|
}
|
|
4281
4528
|
var pct = (x) => `${Math.round(x * 100)}%`;
|
|
4282
|
-
function rankChain(chain, recordOf) {
|
|
4529
|
+
function rankChain(chain, recordOf, work) {
|
|
4283
4530
|
const scored = chain.map((t, i) => ({ t, i, score: scoreOf(recordOf(t)) }));
|
|
4284
4531
|
const slots = scored.filter((s) => s.score !== void 0);
|
|
4285
4532
|
if (slots.length < 2) return { chain };
|
|
@@ -4290,22 +4537,22 @@ function rankChain(chain, recordOf) {
|
|
|
4290
4537
|
const by = (t) => scored.find((s) => s.t === t).score;
|
|
4291
4538
|
return {
|
|
4292
4539
|
chain: out,
|
|
4293
|
-
note: `ranked by record ("rankWorkers"): ${describeTarget(out[0])} (${pct(by(out[0]))}) goes before ${describeTarget(chain[0])} (${pct(by(chain[0]))})`
|
|
4540
|
+
note: `ranked by record${work ? ` on ${work}` : ""} ("rankWorkers"): ${describeTarget(out[0])} (${pct(by(out[0]))}) goes before ${describeTarget(chain[0])} (${pct(by(chain[0]))})`
|
|
4294
4541
|
};
|
|
4295
4542
|
}
|
|
4296
4543
|
|
|
4297
4544
|
// src/core/notify.ts
|
|
4298
4545
|
import { spawn as spawn3 } from "node:child_process";
|
|
4299
4546
|
import crypto7 from "node:crypto";
|
|
4300
|
-
import
|
|
4301
|
-
import
|
|
4547
|
+
import fs27 from "node:fs";
|
|
4548
|
+
import path23 from "node:path";
|
|
4302
4549
|
|
|
4303
4550
|
// src/core/report.ts
|
|
4304
|
-
import
|
|
4551
|
+
import fs26 from "node:fs";
|
|
4305
4552
|
|
|
4306
4553
|
// src/core/plan.ts
|
|
4307
|
-
import
|
|
4308
|
-
import
|
|
4554
|
+
import fs25 from "node:fs";
|
|
4555
|
+
import path22 from "node:path";
|
|
4309
4556
|
var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
4310
4557
|
var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
|
|
4311
4558
|
function structure(lines) {
|
|
@@ -4352,9 +4599,9 @@ function parsePlan(text, file2 = "") {
|
|
|
4352
4599
|
return { file: file2, title: titleHeading?.text ?? "", header, constraints, tasks };
|
|
4353
4600
|
}
|
|
4354
4601
|
function loadPlan(file2) {
|
|
4355
|
-
const abs =
|
|
4356
|
-
if (!
|
|
4357
|
-
const plan = parsePlan(
|
|
4602
|
+
const abs = path22.resolve(file2);
|
|
4603
|
+
if (!fs25.existsSync(abs) || !fs25.statSync(abs).isFile()) throw new UserError(`plan not found: ${file2}`);
|
|
4604
|
+
const plan = parsePlan(fs25.readFileSync(abs, "utf8"), fs25.realpathSync(abs));
|
|
4358
4605
|
if (!plan.tasks.length) throw new UserError(`${file2} has no "Task N" headings (see pitroom-writing-plans)`);
|
|
4359
4606
|
return plan;
|
|
4360
4607
|
}
|
|
@@ -4373,7 +4620,7 @@ function brief(plan, task) {
|
|
|
4373
4620
|
].filter(Boolean).join("\n\n")}
|
|
4374
4621
|
`;
|
|
4375
4622
|
}
|
|
4376
|
-
var planName = (file2) =>
|
|
4623
|
+
var planName = (file2) => path22.basename(file2).replace(/\.md$/i, "");
|
|
4377
4624
|
|
|
4378
4625
|
// src/core/style.ts
|
|
4379
4626
|
var enabled = () => {
|
|
@@ -4433,7 +4680,7 @@ function duration(meta) {
|
|
|
4433
4680
|
}
|
|
4434
4681
|
function readSummary(meta) {
|
|
4435
4682
|
const f = runFile(meta.id, "summary.md");
|
|
4436
|
-
return
|
|
4683
|
+
return fs26.existsSync(f) ? fs26.readFileSync(f, "utf8").trim() : archivedRun(meta.id)?.answer ?? "";
|
|
4437
4684
|
}
|
|
4438
4685
|
function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
4439
4686
|
const out = [];
|
|
@@ -4451,7 +4698,7 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
|
4451
4698
|
const status = meta.taskStatus ? ` \xB7 STATUS ${meta.taskStatus}` : "";
|
|
4452
4699
|
out.push(`plan: ${planName(meta.plan.file)} \xB7 Task ${meta.plan.step}: ${meta.plan.title}${status}`);
|
|
4453
4700
|
}
|
|
4454
|
-
for (const a of meta.attempts ?? []) out.push(a.skipped ? `skipped: ${a.target} (${a.error.slice(0, 160)})` : `fallback: ${a.target} failed (${a.error.slice(0, 160)})`);
|
|
4701
|
+
for (const a of meta.attempts ?? []) out.push(a.skipped ? `skipped: ${a.target} (${a.error.slice(0, 160)})` : a.partial ? `fallback: ${a.target} hit a limit part way and handed the task over (${a.error.slice(0, 160)})` : `fallback: ${a.target} failed (${a.error.slice(0, 160)})`);
|
|
4455
4702
|
if (meta.error) out.push(`error: ${meta.error}`);
|
|
4456
4703
|
for (const w of meta.warnings) out.push(`warning: ${w}`);
|
|
4457
4704
|
if (meta.state === "timeout") {
|
|
@@ -4519,8 +4766,8 @@ function formatReport(meta, finalText = readSummary(meta), maxLines = 400) {
|
|
|
4519
4766
|
}
|
|
4520
4767
|
function live(meta) {
|
|
4521
4768
|
const f = runFile(meta.id, "events.jsonl");
|
|
4522
|
-
if (!
|
|
4523
|
-
const p = getBackend((meta.ran ?? meta.worker).backend).parse(
|
|
4769
|
+
if (!fs26.existsSync(f)) return { steps: 0, toolCalls: 0 };
|
|
4770
|
+
const p = getBackend((meta.ran ?? meta.worker).backend).parse(fs26.readFileSync(f, "utf8"));
|
|
4524
4771
|
return { steps: p.usage.steps, toolCalls: p.usage.toolCalls, last: p.lastActivity };
|
|
4525
4772
|
}
|
|
4526
4773
|
function progress(meta) {
|
|
@@ -4692,12 +4939,12 @@ function claimGroup(group) {
|
|
|
4692
4939
|
try {
|
|
4693
4940
|
const ids = groupIds(group).sort();
|
|
4694
4941
|
if (ids.length < 2) return true;
|
|
4695
|
-
const dir =
|
|
4696
|
-
|
|
4697
|
-
for (const f of
|
|
4698
|
-
if (Date.now() -
|
|
4942
|
+
const dir = path23.join(home(), "notified");
|
|
4943
|
+
fs27.mkdirSync(dir, { recursive: true });
|
|
4944
|
+
for (const f of fs27.readdirSync(dir)) {
|
|
4945
|
+
if (Date.now() - fs27.statSync(path23.join(dir, f)).mtimeMs > 30 * 864e5) fs27.rmSync(path23.join(dir, f), { force: true });
|
|
4699
4946
|
}
|
|
4700
|
-
|
|
4947
|
+
fs27.writeFileSync(path23.join(dir, crypto7.createHash("sha1").update(ids.join(",")).digest("hex").slice(0, 16)), "", { flag: "wx" });
|
|
4701
4948
|
return true;
|
|
4702
4949
|
} catch {
|
|
4703
4950
|
return false;
|
|
@@ -4731,30 +4978,30 @@ function notifyEnded(meta) {
|
|
|
4731
4978
|
}
|
|
4732
4979
|
|
|
4733
4980
|
// src/core/templates.ts
|
|
4734
|
-
import
|
|
4735
|
-
import
|
|
4981
|
+
import fs29 from "node:fs";
|
|
4982
|
+
import path25 from "node:path";
|
|
4736
4983
|
|
|
4737
4984
|
// src/core/install.ts
|
|
4738
|
-
import
|
|
4985
|
+
import fs28 from "node:fs";
|
|
4739
4986
|
import os11 from "node:os";
|
|
4740
|
-
import
|
|
4987
|
+
import path24 from "node:path";
|
|
4741
4988
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
4742
4989
|
var LEGACY = ["pitroom", "opencode-worker"];
|
|
4743
4990
|
function packageRoot() {
|
|
4744
|
-
return
|
|
4991
|
+
return path24.resolve(path24.dirname(fileURLToPath3(import.meta.url)), "..");
|
|
4745
4992
|
}
|
|
4746
4993
|
function skillNames(root = packageRoot()) {
|
|
4747
|
-
const dir =
|
|
4748
|
-
if (!
|
|
4749
|
-
return
|
|
4994
|
+
const dir = path24.join(root, "skills");
|
|
4995
|
+
if (!fs28.existsSync(dir)) return [];
|
|
4996
|
+
return fs28.readdirSync(dir).filter((d) => fs28.existsSync(path24.join(dir, d, "SKILL.md"))).sort();
|
|
4750
4997
|
}
|
|
4751
4998
|
function skillTargets() {
|
|
4752
4999
|
const home3 = os11.homedir();
|
|
4753
|
-
const targets = [
|
|
4754
|
-
if (
|
|
5000
|
+
const targets = [path24.join(home3, ".agents", "skills")];
|
|
5001
|
+
if (fs28.existsSync(path24.join(home3, ".claude"))) targets.push(path24.join(home3, ".claude", "skills"));
|
|
4755
5002
|
return targets;
|
|
4756
5003
|
}
|
|
4757
|
-
var launcherPath = () =>
|
|
5004
|
+
var launcherPath = () => path24.join(os11.homedir(), ".local", "bin", process.platform === "win32" ? "pitroom.cmd" : "pitroom");
|
|
4758
5005
|
var LAUNCHER_MARK = "# pitroom launcher";
|
|
4759
5006
|
function launcherScript(bundle) {
|
|
4760
5007
|
if (process.platform === "win32") {
|
|
@@ -4784,7 +5031,7 @@ exit 127
|
|
|
4784
5031
|
function isOurLauncher(file2, root) {
|
|
4785
5032
|
if (linksInto(file2, root)) return true;
|
|
4786
5033
|
try {
|
|
4787
|
-
const s =
|
|
5034
|
+
const s = fs28.readFileSync(file2, "utf8");
|
|
4788
5035
|
return s.includes(LAUNCHER_MARK) && s.includes(root);
|
|
4789
5036
|
} catch {
|
|
4790
5037
|
return false;
|
|
@@ -4792,64 +5039,64 @@ function isOurLauncher(file2, root) {
|
|
|
4792
5039
|
}
|
|
4793
5040
|
function isPitroomLauncher(file2) {
|
|
4794
5041
|
try {
|
|
4795
|
-
return
|
|
5042
|
+
return fs28.readFileSync(file2, "utf8").includes(LAUNCHER_MARK);
|
|
4796
5043
|
} catch {
|
|
4797
5044
|
return false;
|
|
4798
5045
|
}
|
|
4799
5046
|
}
|
|
4800
5047
|
function placeLauncher(bundle, root, force) {
|
|
4801
5048
|
const dest = launcherPath();
|
|
4802
|
-
|
|
4803
|
-
const exists =
|
|
5049
|
+
fs28.mkdirSync(path24.dirname(dest), { recursive: true });
|
|
5050
|
+
const exists = fs28.lstatSync(dest, { throwIfNoEntry: false });
|
|
4804
5051
|
if (exists && !isOurLauncher(dest, root)) {
|
|
4805
5052
|
if (!force) return `! ${dest} exists and is not Pitroom's launcher; kept (use --force to back it up and replace)`;
|
|
4806
|
-
|
|
5053
|
+
fs28.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
4807
5054
|
} else if (exists) {
|
|
4808
|
-
|
|
5055
|
+
fs28.rmSync(dest, { force: true });
|
|
4809
5056
|
}
|
|
4810
|
-
|
|
5057
|
+
fs28.writeFileSync(dest, launcherScript(bundle), { mode: 493 });
|
|
4811
5058
|
return `\u2714 ${dest} \u2192 launcher for ${bundle} (Node 22.13+)`;
|
|
4812
5059
|
}
|
|
4813
5060
|
function linksInto(link, dir) {
|
|
4814
|
-
const st =
|
|
5061
|
+
const st = fs28.lstatSync(link, { throwIfNoEntry: false });
|
|
4815
5062
|
if (!st?.isSymbolicLink()) return false;
|
|
4816
|
-
const target =
|
|
4817
|
-
const rel =
|
|
4818
|
-
return !rel.startsWith("..") && !
|
|
5063
|
+
const target = path24.resolve(path24.dirname(link), fs28.readlinkSync(link));
|
|
5064
|
+
const rel = path24.relative(dir, target);
|
|
5065
|
+
return !rel.startsWith("..") && !path24.isAbsolute(rel);
|
|
4819
5066
|
}
|
|
4820
5067
|
function place(src, dest, opts) {
|
|
4821
|
-
|
|
4822
|
-
const st =
|
|
5068
|
+
fs28.mkdirSync(path24.dirname(dest), { recursive: true });
|
|
5069
|
+
const st = fs28.lstatSync(dest, { throwIfNoEntry: false });
|
|
4823
5070
|
if (st && !st.isSymbolicLink()) {
|
|
4824
5071
|
if (!opts.force) return `! ${dest} exists and is not a link; kept (use --force to back it up and replace)`;
|
|
4825
|
-
|
|
5072
|
+
fs28.renameSync(dest, `${dest}.bak-${Date.now()}`);
|
|
4826
5073
|
} else if (st) {
|
|
4827
|
-
|
|
5074
|
+
fs28.unlinkSync(dest);
|
|
4828
5075
|
}
|
|
4829
|
-
if (opts.copy)
|
|
4830
|
-
else
|
|
5076
|
+
if (opts.copy) fs28.cpSync(src, dest, { recursive: true });
|
|
5077
|
+
else fs28.symlinkSync(src, dest, process.platform === "win32" ? "junction" : fs28.statSync(src).isDirectory() ? "dir" : "file");
|
|
4831
5078
|
return `\u2714 ${dest} \u2192 ${opts.copy ? "copied" : src}`;
|
|
4832
5079
|
}
|
|
4833
5080
|
function install(opts) {
|
|
4834
5081
|
const root = packageRoot();
|
|
4835
5082
|
const skills2 = opts.skills === false ? [] : skillNames(root);
|
|
4836
|
-
if (opts.skills !== false && !skills2.length) throw new Error(`no skills found under ${
|
|
5083
|
+
if (opts.skills !== false && !skills2.length) throw new Error(`no skills found under ${path24.join(root, "skills")}`);
|
|
4837
5084
|
const out = [];
|
|
4838
5085
|
for (const base2 of opts.skills === false ? [] : skillTargets()) {
|
|
4839
5086
|
for (const legacy of LEGACY) {
|
|
4840
|
-
const l =
|
|
5087
|
+
const l = path24.join(base2, legacy);
|
|
4841
5088
|
if (!skills2.includes(legacy) && linksInto(l, root)) {
|
|
4842
|
-
|
|
5089
|
+
fs28.unlinkSync(l);
|
|
4843
5090
|
out.push(`\u2714 removed old link ${l}`);
|
|
4844
5091
|
}
|
|
4845
5092
|
}
|
|
4846
|
-
for (const name of skills2) out.push(place(
|
|
5093
|
+
for (const name of skills2) out.push(place(path24.join(root, "skills", name), path24.join(base2, name), opts));
|
|
4847
5094
|
}
|
|
4848
|
-
const bundle =
|
|
4849
|
-
if (
|
|
5095
|
+
const bundle = path24.join(root, "dist", "pitroom.mjs");
|
|
5096
|
+
if (fs28.existsSync(bundle)) {
|
|
4850
5097
|
out.push(placeLauncher(bundle, root, opts.force));
|
|
4851
|
-
const onPath = (process.env.PATH ?? "").split(
|
|
4852
|
-
if (!onPath) out.push(`! ${
|
|
5098
|
+
const onPath = (process.env.PATH ?? "").split(path24.delimiter).some((d) => path24.resolve(d) === path24.dirname(launcherPath()));
|
|
5099
|
+
if (!onPath) out.push(`! ${path24.dirname(launcherPath())} is not on PATH; add it, or run ${launcherPath()} directly`);
|
|
4853
5100
|
}
|
|
4854
5101
|
return out;
|
|
4855
5102
|
}
|
|
@@ -4857,24 +5104,24 @@ function uninstall() {
|
|
|
4857
5104
|
const root = packageRoot();
|
|
4858
5105
|
const out = [];
|
|
4859
5106
|
for (const base2 of skillTargets()) {
|
|
4860
|
-
if (!
|
|
4861
|
-
for (const name of
|
|
4862
|
-
const l =
|
|
5107
|
+
if (!fs28.existsSync(base2)) continue;
|
|
5108
|
+
for (const name of fs28.readdirSync(base2)) {
|
|
5109
|
+
const l = path24.join(base2, name);
|
|
4863
5110
|
if (linksInto(l, root)) {
|
|
4864
|
-
|
|
5111
|
+
fs28.unlinkSync(l);
|
|
4865
5112
|
out.push(`\u2714 removed ${l}`);
|
|
4866
5113
|
}
|
|
4867
5114
|
}
|
|
4868
5115
|
}
|
|
4869
5116
|
if (isOurLauncher(launcherPath(), root)) {
|
|
4870
|
-
|
|
5117
|
+
fs28.rmSync(launcherPath(), { force: true });
|
|
4871
5118
|
out.push(`\u2714 removed ${launcherPath()}`);
|
|
4872
5119
|
}
|
|
4873
5120
|
return out.length ? out : ["nothing to remove"];
|
|
4874
5121
|
}
|
|
4875
5122
|
function installedSkills() {
|
|
4876
5123
|
const names = skillNames();
|
|
4877
|
-
return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) =>
|
|
5124
|
+
return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs28.existsSync(path24.join(base2, n, "SKILL.md"))) }));
|
|
4878
5125
|
}
|
|
4879
5126
|
|
|
4880
5127
|
// src/core/templates.ts
|
|
@@ -4885,9 +5132,9 @@ var FILES = {
|
|
|
4885
5132
|
"code-reviewer": "pitroom-review/code-reviewer.md"
|
|
4886
5133
|
};
|
|
4887
5134
|
function loadTemplate(name, root = packageRoot()) {
|
|
4888
|
-
const file2 =
|
|
4889
|
-
if (!
|
|
4890
|
-
return
|
|
5135
|
+
const file2 = path25.join(root, "skills", FILES[name]);
|
|
5136
|
+
if (!fs29.existsSync(file2)) throw new UserError(`template missing: ${file2} (broken install? run pitroom doctor)`, 3);
|
|
5137
|
+
return fs29.readFileSync(file2, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
|
|
4891
5138
|
}
|
|
4892
5139
|
function fill(template, values) {
|
|
4893
5140
|
const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
|
|
@@ -4897,10 +5144,10 @@ function fill(template, values) {
|
|
|
4897
5144
|
|
|
4898
5145
|
// src/core/slots.ts
|
|
4899
5146
|
import crypto8 from "node:crypto";
|
|
4900
|
-
import
|
|
4901
|
-
import
|
|
4902
|
-
var slotsDir = () =>
|
|
4903
|
-
var locksDir = () =>
|
|
5147
|
+
import fs30 from "node:fs";
|
|
5148
|
+
import path26 from "node:path";
|
|
5149
|
+
var slotsDir = () => path26.join(home(), "slots");
|
|
5150
|
+
var locksDir = () => path26.join(home(), "locks");
|
|
4904
5151
|
var STARTUP_GRACE_MS = 3e4;
|
|
4905
5152
|
function holderActive(runId, self) {
|
|
4906
5153
|
if (!runId || runId === self) return false;
|
|
@@ -4914,23 +5161,23 @@ function holderActive(runId, self) {
|
|
|
4914
5161
|
}
|
|
4915
5162
|
}
|
|
4916
5163
|
function tryClaim(file2, runId) {
|
|
4917
|
-
|
|
5164
|
+
fs30.mkdirSync(path26.dirname(file2), { recursive: true });
|
|
4918
5165
|
try {
|
|
4919
|
-
|
|
5166
|
+
fs30.writeFileSync(file2, runId, { flag: "wx" });
|
|
4920
5167
|
return true;
|
|
4921
5168
|
} catch (e) {
|
|
4922
5169
|
if (e.code !== "EEXIST") throw e;
|
|
4923
5170
|
}
|
|
4924
5171
|
let owner = "";
|
|
4925
5172
|
try {
|
|
4926
|
-
owner =
|
|
5173
|
+
owner = fs30.readFileSync(file2, "utf8").trim();
|
|
4927
5174
|
} catch {
|
|
4928
5175
|
}
|
|
4929
5176
|
if (owner === runId) return true;
|
|
4930
5177
|
if (holderActive(owner, runId)) return false;
|
|
4931
|
-
|
|
5178
|
+
fs30.rmSync(file2, { force: true });
|
|
4932
5179
|
try {
|
|
4933
|
-
|
|
5180
|
+
fs30.writeFileSync(file2, runId, { flag: "wx" });
|
|
4934
5181
|
return true;
|
|
4935
5182
|
} catch {
|
|
4936
5183
|
return false;
|
|
@@ -4939,25 +5186,25 @@ function tryClaim(file2, runId) {
|
|
|
4939
5186
|
function releaseIfOwner(file2, runId) {
|
|
4940
5187
|
if (!file2) return;
|
|
4941
5188
|
try {
|
|
4942
|
-
if (
|
|
5189
|
+
if (fs30.readFileSync(file2, "utf8").trim() === runId) fs30.rmSync(file2, { force: true });
|
|
4943
5190
|
} catch {
|
|
4944
5191
|
}
|
|
4945
5192
|
}
|
|
4946
5193
|
function tryAcquireSlot(runId, maxParallel) {
|
|
4947
5194
|
for (let n = 0; n < Math.max(1, maxParallel); n++) {
|
|
4948
|
-
const file2 =
|
|
5195
|
+
const file2 = path26.join(slotsDir(), `slot-${n}`);
|
|
4949
5196
|
if (tryClaim(file2, runId)) return file2;
|
|
4950
5197
|
}
|
|
4951
5198
|
return void 0;
|
|
4952
5199
|
}
|
|
4953
5200
|
var releaseSlot = (file2, runId) => releaseIfOwner(file2, runId);
|
|
4954
|
-
var lockFile = (repoRoot2) =>
|
|
5201
|
+
var lockFile = (repoRoot2) => path26.join(locksDir(), `write-${crypto8.createHash("sha1").update(path26.resolve(repoRoot2)).digest("hex").slice(0, 16)}`);
|
|
4955
5202
|
function acquireWriteLock(repoRoot2, runId) {
|
|
4956
5203
|
const file2 = lockFile(repoRoot2);
|
|
4957
5204
|
if (tryClaim(file2, runId)) return;
|
|
4958
5205
|
let owner = "";
|
|
4959
5206
|
try {
|
|
4960
|
-
owner =
|
|
5207
|
+
owner = fs30.readFileSync(file2, "utf8").trim();
|
|
4961
5208
|
} catch {
|
|
4962
5209
|
}
|
|
4963
5210
|
throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
|
|
@@ -5007,13 +5254,14 @@ ${task}`;
|
|
|
5007
5254
|
}
|
|
5008
5255
|
|
|
5009
5256
|
// src/core/refs.ts
|
|
5010
|
-
import
|
|
5011
|
-
import
|
|
5257
|
+
import fs31 from "node:fs";
|
|
5258
|
+
import path27 from "node:path";
|
|
5012
5259
|
var FILE_EXTENSIONS = new Set(
|
|
5013
5260
|
"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(" ")
|
|
5014
5261
|
);
|
|
5015
5262
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
5016
5263
|
var MAX_INDEXED = 6e4;
|
|
5264
|
+
var MAX_SKIPPED = 3e5;
|
|
5017
5265
|
var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
|
|
5018
5266
|
var REF = new RegExp(
|
|
5019
5267
|
String.raw`(?<![\w/:.-])(\.{0,2}/?(?:[\w@.+-]+/)*(?:[\w@+-][\w@.+-]*\.[A-Za-z][A-Za-z0-9]{0,7}|${EXTENSIONLESS})):(\d+)(?:[-–](\d+))?`,
|
|
@@ -5059,26 +5307,30 @@ function extractRefs(answer) {
|
|
|
5059
5307
|
return [...seen.values()];
|
|
5060
5308
|
}
|
|
5061
5309
|
function verifyRefs(refs, dirs) {
|
|
5062
|
-
const roots = [...new Set(dirs.filter(Boolean).map((d) =>
|
|
5310
|
+
const roots = [...new Set(dirs.filter(Boolean).map((d) => path27.resolve(d)))];
|
|
5063
5311
|
const invalid = [];
|
|
5064
5312
|
const lineCache = /* @__PURE__ */ new Map();
|
|
5065
5313
|
const load = (file2) => {
|
|
5066
5314
|
if (!lineCache.has(file2)) {
|
|
5067
|
-
const size =
|
|
5068
|
-
const lines = size > MAX_BYTES ? null :
|
|
5315
|
+
const size = fs31.statSync(file2).size;
|
|
5316
|
+
const lines = size > MAX_BYTES ? null : fs31.readFileSync(file2, "utf8").split("\n");
|
|
5069
5317
|
if (lines && lines[lines.length - 1] === "") lines.pop();
|
|
5070
5318
|
lineCache.set(file2, lines);
|
|
5071
5319
|
}
|
|
5072
5320
|
return lineCache.get(file2);
|
|
5073
5321
|
};
|
|
5074
5322
|
let index;
|
|
5323
|
+
let skipped;
|
|
5075
5324
|
const byName = () => index ??= indexFiles(roots);
|
|
5325
|
+
const inSkipped = () => skipped ??= indexFiles(roots, true);
|
|
5076
5326
|
const candidates = (ref) => {
|
|
5077
5327
|
const direct = resolve(ref.file, roots);
|
|
5078
5328
|
if (direct) return [direct];
|
|
5079
|
-
if (
|
|
5329
|
+
if (path27.isAbsolute(ref.file)) return [];
|
|
5080
5330
|
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
5081
|
-
|
|
5331
|
+
const fits = (f) => path27.basename(f) === wanted || foldersInOrder(f.split(path27.sep).join("/"), wanted);
|
|
5332
|
+
const own = (byName().get(path27.basename(wanted)) ?? []).filter(fits);
|
|
5333
|
+
return (own.length || !isFileName(wanted) ? own : (inSkipped().get(path27.basename(wanted)) ?? []).filter(fits)).slice(0, 20);
|
|
5082
5334
|
};
|
|
5083
5335
|
const check = (file2, ref) => {
|
|
5084
5336
|
const lines = load(file2);
|
|
@@ -5093,7 +5345,7 @@ function verifyRefs(refs, dirs) {
|
|
|
5093
5345
|
if (!files.length) {
|
|
5094
5346
|
if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
|
|
5095
5347
|
total++;
|
|
5096
|
-
invalid.push({ ref: ref.text, reason:
|
|
5348
|
+
invalid.push({ ref: ref.text, reason: path27.isAbsolute(ref.file) && !inside2(ref.file, roots) ? "outside the project" : "file not found" });
|
|
5097
5349
|
continue;
|
|
5098
5350
|
}
|
|
5099
5351
|
total++;
|
|
@@ -5114,39 +5366,54 @@ function enclosedBy(lines, ref) {
|
|
|
5114
5366
|
return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
|
|
5115
5367
|
}
|
|
5116
5368
|
var isFileName = (name) => {
|
|
5117
|
-
const base2 =
|
|
5369
|
+
const base2 = path27.basename(name);
|
|
5118
5370
|
return !base2.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base2) || FILE_EXTENSIONS.has(base2.split(".").pop().toLowerCase());
|
|
5119
5371
|
};
|
|
5120
|
-
function
|
|
5372
|
+
function foldersInOrder(file2, wanted) {
|
|
5373
|
+
const want = wanted.split("/");
|
|
5374
|
+
const have = file2.split("/");
|
|
5375
|
+
if (have.at(-1) !== want.at(-1)) return false;
|
|
5376
|
+
let i = have.length - 2;
|
|
5377
|
+
for (let j = want.length - 2; j >= 0; j--) {
|
|
5378
|
+
while (i >= 0 && have[i] !== want[j]) i--;
|
|
5379
|
+
if (i < 0) return false;
|
|
5380
|
+
i--;
|
|
5381
|
+
}
|
|
5382
|
+
return true;
|
|
5383
|
+
}
|
|
5384
|
+
function indexFiles(roots, skipped = false) {
|
|
5121
5385
|
const index = /* @__PURE__ */ new Map();
|
|
5386
|
+
const cap = skipped ? MAX_SKIPPED : MAX_INDEXED;
|
|
5122
5387
|
let count = 0;
|
|
5123
|
-
const walk = (dir) => {
|
|
5388
|
+
const walk = (dir, within) => {
|
|
5124
5389
|
let entries;
|
|
5125
5390
|
try {
|
|
5126
|
-
entries =
|
|
5391
|
+
entries = fs31.readdirSync(dir, { withFileTypes: true });
|
|
5127
5392
|
} catch {
|
|
5128
5393
|
return;
|
|
5129
5394
|
}
|
|
5130
5395
|
for (const e of entries) {
|
|
5131
|
-
if (count >=
|
|
5396
|
+
if (count >= cap) return;
|
|
5132
5397
|
if (e.isDirectory()) {
|
|
5133
|
-
if (
|
|
5134
|
-
|
|
5398
|
+
if (e.name === ".git") continue;
|
|
5399
|
+
const skip = SKIP_DIRS2.has(e.name);
|
|
5400
|
+
if (skipped || !skip) walk(path27.join(dir, e.name), within || skip);
|
|
5401
|
+
} else if (e.isFile() && within === skipped) {
|
|
5135
5402
|
count++;
|
|
5136
5403
|
const list2 = index.get(e.name);
|
|
5137
|
-
if (list2) list2.push(
|
|
5138
|
-
else index.set(e.name, [
|
|
5404
|
+
if (list2) list2.push(path27.join(dir, e.name));
|
|
5405
|
+
else index.set(e.name, [path27.join(dir, e.name)]);
|
|
5139
5406
|
}
|
|
5140
5407
|
}
|
|
5141
5408
|
};
|
|
5142
|
-
for (const r of roots) walk(r);
|
|
5409
|
+
for (const r of roots) walk(r, false);
|
|
5143
5410
|
return index;
|
|
5144
5411
|
}
|
|
5145
5412
|
function resolve(ref, roots) {
|
|
5146
|
-
const candidates =
|
|
5413
|
+
const candidates = path27.isAbsolute(ref) ? inside2(ref, roots) ? [ref] : [] : roots.map((r) => path27.join(r, ref));
|
|
5147
5414
|
return candidates.find((c) => {
|
|
5148
5415
|
try {
|
|
5149
|
-
return
|
|
5416
|
+
return fs31.statSync(c).isFile() && inside2(c, roots);
|
|
5150
5417
|
} catch {
|
|
5151
5418
|
return false;
|
|
5152
5419
|
}
|
|
@@ -5154,14 +5421,14 @@ function resolve(ref, roots) {
|
|
|
5154
5421
|
}
|
|
5155
5422
|
function inside2(file2, roots) {
|
|
5156
5423
|
return roots.some((r) => {
|
|
5157
|
-
const rel =
|
|
5158
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
5424
|
+
const rel = path27.relative(r, path27.resolve(file2));
|
|
5425
|
+
return rel !== "" && !rel.startsWith("..") && !path27.isAbsolute(rel);
|
|
5159
5426
|
});
|
|
5160
5427
|
}
|
|
5161
5428
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5162
5429
|
|
|
5163
5430
|
// src/core/run.ts
|
|
5164
|
-
var VERSION2 = true ? "0.
|
|
5431
|
+
var VERSION2 = true ? "0.27.0" : "0.0.0-dev";
|
|
5165
5432
|
function planWork(o) {
|
|
5166
5433
|
if (!o.plan) return { task: o.task.trim(), tier: o.tier };
|
|
5167
5434
|
if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
|
|
@@ -5193,19 +5460,21 @@ function prepareRun(o) {
|
|
|
5193
5460
|
parent = readMeta(o.continueFrom);
|
|
5194
5461
|
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");
|
|
5195
5462
|
if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
|
|
5196
|
-
if (parent.mode === "isolate" && (!parent.worktree || !
|
|
5463
|
+
if (parent.mode === "isolate" && (!parent.worktree || !fs32.existsSync(parent.worktree))) {
|
|
5197
5464
|
throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
|
|
5198
5465
|
}
|
|
5199
5466
|
const ran = parent.ran ?? parent.worker;
|
|
5200
5467
|
worker = o.model ? { ...ran, model: o.model } : ran;
|
|
5201
5468
|
fallback = o.noFallback ? [] : parent.fallback.filter((t) => t.backend === ran.backend);
|
|
5202
5469
|
} else {
|
|
5203
|
-
const chain = resolveChain({ worker: o.worker, model: o.model, tier: work.tier, effort: o.effort, noFallback: o.noFallback });
|
|
5470
|
+
const chain = resolveChain({ worker: o.worker, model: o.model, tier: work.tier, effort: o.effort, noFallback: o.noFallback || !!o.fallbackTo });
|
|
5204
5471
|
({ worker, fallback } = chain);
|
|
5472
|
+
if (o.fallbackTo && !o.noFallback) fallback = o.fallbackTo.map((w) => resolveChain({ worker: w, noFallback: true }).worker);
|
|
5205
5473
|
warnings2.push(...chain.warnings);
|
|
5206
5474
|
if (effective().rankWorkers.value && !o.worker && !o.model && !work.tier && fallback.length) {
|
|
5207
5475
|
const since2 = Date.now() - 30 * 864e5;
|
|
5208
|
-
const
|
|
5476
|
+
const work2 = o.review ? "review" : o.mode === "read" ? "research" : "change";
|
|
5477
|
+
const ranked = rankChain([worker, ...fallback], (t) => workerRecord(t.backend, t.model, since2, work2), work2 === "change" ? "changes" : work2);
|
|
5209
5478
|
[worker, ...fallback] = ranked.chain;
|
|
5210
5479
|
if (ranked.note) warnings2.push(ranked.note);
|
|
5211
5480
|
}
|
|
@@ -5221,22 +5490,22 @@ function prepareRun(o) {
|
|
|
5221
5490
|
throw new UserError(`the ${backend.name} worker cannot attach files; put the content in the task`);
|
|
5222
5491
|
}
|
|
5223
5492
|
const mode = parent?.mode ?? o.mode;
|
|
5224
|
-
const dir = canonical(
|
|
5225
|
-
if (!
|
|
5493
|
+
const dir = canonical(path28.resolve(parent?.dir ?? o.dir));
|
|
5494
|
+
if (!fs32.existsSync(dir) || !fs32.statSync(dir).isDirectory()) throw new UserError(`not a directory: ${dir}`);
|
|
5226
5495
|
const root = repoRoot(dir);
|
|
5227
5496
|
if (mode === "isolate" && !root) throw new UserError("--isolate needs a git repository", 3);
|
|
5228
5497
|
if (mode === "write" && !root && !o.allowNonGit) {
|
|
5229
5498
|
throw new UserError("--write outside a git repo cannot be tracked or reverted; pass --allow-non-git to accept that", 3);
|
|
5230
5499
|
}
|
|
5231
|
-
const files = o.files.map((f) =>
|
|
5232
|
-
for (const f of files) if (!
|
|
5500
|
+
const files = o.files.map((f) => path28.resolve(f));
|
|
5501
|
+
for (const f of files) if (!fs32.existsSync(f)) throw new UserError(`file not found: ${f}`);
|
|
5233
5502
|
const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
|
|
5234
5503
|
const untrusted = [worker, ...fallback].map((t) => getBackend(t.backend)).find(readsOwnSnapshot);
|
|
5235
5504
|
const snap = mode === "read" && !o.review && !o.inPlace && !parent ? wantSnapshot(readIn2, root, dir) : void 0;
|
|
5236
5505
|
if (untrusted && mode === "read" && !parent && (o.review || o.inPlace || !root)) {
|
|
5237
5506
|
warnings2.push(`this read run is not in a snapshot${root ? "" : " (not a git repository)"}, and ${untrusted.name} does not reliably stay read-only: it could change files here`);
|
|
5238
5507
|
}
|
|
5239
|
-
if (parent?.snapshot?.dir && !
|
|
5508
|
+
if (parent?.snapshot?.dir && !fs32.existsSync(parent.snapshot.dir)) {
|
|
5240
5509
|
let restored = false;
|
|
5241
5510
|
if (parent.snapshot.private && root && parent.snapshot.tree) {
|
|
5242
5511
|
try {
|
|
@@ -5293,13 +5562,13 @@ function prepareRun(o) {
|
|
|
5293
5562
|
try {
|
|
5294
5563
|
acquireWriteLock(root, meta.id);
|
|
5295
5564
|
} catch (e) {
|
|
5296
|
-
|
|
5565
|
+
fs32.rmSync(runDir(meta.id), { recursive: true, force: true });
|
|
5297
5566
|
throw e;
|
|
5298
5567
|
}
|
|
5299
5568
|
}
|
|
5300
|
-
|
|
5569
|
+
fs32.writeFileSync(runFile(meta.id, "task.md"), `${meta.task}
|
|
5301
5570
|
`);
|
|
5302
|
-
if (work.brief)
|
|
5571
|
+
if (work.brief) fs32.writeFileSync(runFile(meta.id, "brief.md"), work.brief);
|
|
5303
5572
|
return meta;
|
|
5304
5573
|
}
|
|
5305
5574
|
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -5396,10 +5665,11 @@ async function execute(meta) {
|
|
|
5396
5665
|
}
|
|
5397
5666
|
}
|
|
5398
5667
|
if (i === chain.length - 1) break;
|
|
5399
|
-
(
|
|
5668
|
+
if (why.partial) handOver(meta, backend, target);
|
|
5669
|
+
(meta.attempts ??= []).push({ target: describeTarget(target), error: why.message, ...why.partial ? { partial: true } : {} });
|
|
5400
5670
|
for (const f of ["events.jsonl", "stderr.log"]) {
|
|
5401
5671
|
const from = runFile(meta.id, f);
|
|
5402
|
-
if (
|
|
5672
|
+
if (fs32.existsSync(from)) fs32.renameSync(from, runFile(meta.id, f.replace(".", `.attempt-${i + 1}.`)));
|
|
5403
5673
|
}
|
|
5404
5674
|
writeMeta(meta);
|
|
5405
5675
|
}
|
|
@@ -5422,7 +5692,7 @@ function autoAudit(meta) {
|
|
|
5422
5692
|
let rate2 = auditRate(meta);
|
|
5423
5693
|
if (meta.auditRate === void 0 && effective().auditFocus.value) {
|
|
5424
5694
|
const ran = meta.ran ?? meta.worker;
|
|
5425
|
-
const focus = focusedRate(rate2, meta.task, workerRecord(ran.backend, meta.resolvedModel ?? ran.model, Date.now() - 30 * 864e5));
|
|
5695
|
+
const focus = focusedRate(rate2, meta.task, workerRecord(ran.backend, meta.resolvedModel ?? ran.model, Date.now() - 30 * 864e5, "research"));
|
|
5426
5696
|
rate2 = focus.rate;
|
|
5427
5697
|
if (focus.why.length) {
|
|
5428
5698
|
meta.auditFocus = { rate: rate2, why: focus.why };
|
|
@@ -5436,7 +5706,7 @@ function autoAudit(meta) {
|
|
|
5436
5706
|
}
|
|
5437
5707
|
function startAudit(meta, worker) {
|
|
5438
5708
|
const answer = read2(runFile(meta.id, "summary.md"));
|
|
5439
|
-
const auditor = worker
|
|
5709
|
+
const [auditor, ...others] = worker ? [worker] : auditors(meta);
|
|
5440
5710
|
if (!auditor) return void 0;
|
|
5441
5711
|
const a = prepareRun({
|
|
5442
5712
|
mode: "read",
|
|
@@ -5448,8 +5718,9 @@ function startAudit(meta, worker) {
|
|
|
5448
5718
|
timeoutSec: Math.min(meta.timeoutSec, 15 * 60),
|
|
5449
5719
|
allowNonGit: true,
|
|
5450
5720
|
web: false,
|
|
5451
|
-
//
|
|
5452
|
-
noFallback:
|
|
5721
|
+
// the other auditors, never the worker being audited; one you named has none
|
|
5722
|
+
noFallback: !!worker,
|
|
5723
|
+
fallbackTo: others,
|
|
5453
5724
|
group: meta.group,
|
|
5454
5725
|
audit: { of: meta.id }
|
|
5455
5726
|
});
|
|
@@ -5495,9 +5766,9 @@ function prepareTree(meta) {
|
|
|
5495
5766
|
if (root && meta.mode === "write") meta.baseTree = snapshotTree(root);
|
|
5496
5767
|
if (root && meta.mode === "isolate" && !meta.worktree) {
|
|
5497
5768
|
meta.baseTree = snapshotTree(root);
|
|
5498
|
-
meta.worktree =
|
|
5769
|
+
meta.worktree = path28.join(worktreesDir(), meta.id);
|
|
5499
5770
|
createIsolatedCopy(root, meta.baseTree, meta.worktree);
|
|
5500
|
-
meta.cwd =
|
|
5771
|
+
meta.cwd = path28.join(meta.worktree, path28.relative(root, meta.dir));
|
|
5501
5772
|
const linked = linkIntoWorktree(root, meta.worktree, meta.link);
|
|
5502
5773
|
if (linked.length) meta.warnings.push(`linked into the isolated copy (shared with your tree): ${linked.join(", ")}`);
|
|
5503
5774
|
}
|
|
@@ -5528,8 +5799,8 @@ function ownSnapshot(meta, root, backend, planned, attemptNo) {
|
|
|
5528
5799
|
try {
|
|
5529
5800
|
const s = readSnapshot(root, planned.own ? `${meta.id}-${attemptNo}` : meta.id);
|
|
5530
5801
|
meta.snapshot = { dir: s.dir, tree: s.tree, left, private: true };
|
|
5531
|
-
meta.cwd =
|
|
5532
|
-
if (!
|
|
5802
|
+
meta.cwd = path28.join(s.dir, path28.relative(root, meta.dir));
|
|
5803
|
+
if (!fs32.existsSync(meta.cwd)) meta.cwd = s.dir;
|
|
5533
5804
|
planned.own = { snapshot: { ...meta.snapshot }, cwd: meta.cwd };
|
|
5534
5805
|
const note = `${backend.name} reads a snapshot of your project of its own (it does not reliably stay read-only)`;
|
|
5535
5806
|
if (!meta.warnings.some((w) => w.startsWith(note))) {
|
|
@@ -5561,8 +5832,8 @@ function sharedSnapshot(meta, root) {
|
|
|
5561
5832
|
const s = readSnapshot(root);
|
|
5562
5833
|
snapshot.dir = s.dir;
|
|
5563
5834
|
snapshot.tree = s.tree;
|
|
5564
|
-
meta.cwd =
|
|
5565
|
-
if (!
|
|
5835
|
+
meta.cwd = path28.join(s.dir, path28.relative(root, meta.dir));
|
|
5836
|
+
if (!fs32.existsSync(meta.cwd)) meta.cwd = s.dir;
|
|
5566
5837
|
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`);
|
|
5567
5838
|
} catch (e) {
|
|
5568
5839
|
meta.snapshot = void 0;
|
|
@@ -5572,7 +5843,9 @@ function sharedSnapshot(meta, root) {
|
|
|
5572
5843
|
function attempt(meta, backend, target) {
|
|
5573
5844
|
const inv = backend.invocation({
|
|
5574
5845
|
mode: meta.mode,
|
|
5575
|
-
prompt: buildPrompt(meta.mode, meta.
|
|
5846
|
+
prompt: buildPrompt(meta.mode, meta.handoff ? `${meta.task}
|
|
5847
|
+
|
|
5848
|
+
${meta.handoff}` : meta.task, !!meta.parent, !!meta.auditOf),
|
|
5576
5849
|
cwd: meta.cwd,
|
|
5577
5850
|
model: target.model,
|
|
5578
5851
|
sessionId: meta.sessionId,
|
|
@@ -5588,12 +5861,31 @@ function attempt(meta, backend, target) {
|
|
|
5588
5861
|
timeoutSec: meta.timeoutSec
|
|
5589
5862
|
});
|
|
5590
5863
|
}
|
|
5591
|
-
var read2 = (f) =>
|
|
5864
|
+
var read2 = (f) => fs32.existsSync(f) ? fs32.readFileSync(f, "utf8") : "";
|
|
5592
5865
|
function retryableFailure(meta, backend, res) {
|
|
5593
5866
|
const run2 = backend.parse(read2(runFile(meta.id, "events.jsonl")));
|
|
5594
|
-
if (run2.usage.steps > 0) return void 0;
|
|
5595
5867
|
const f = backend.failure(run2, read2(runFile(meta.id, "stderr.log")), res.code);
|
|
5596
|
-
|
|
5868
|
+
if (!f || f.kind === "other") return void 0;
|
|
5869
|
+
if (run2.usage.steps === 0) return f;
|
|
5870
|
+
return f.kind === "rate-limited" ? { ...f, partial: true } : void 0;
|
|
5871
|
+
}
|
|
5872
|
+
function handOver(meta, backend, target) {
|
|
5873
|
+
const run2 = backend.parse(read2(runFile(meta.id, "events.jsonl")));
|
|
5874
|
+
const used = { ...run2.usage };
|
|
5875
|
+
if (used.cost === void 0) {
|
|
5876
|
+
const price = workerPrice(target.backend, run2.model, target.model);
|
|
5877
|
+
if (price) Object.assign(used, { costEstimate: costAt(used, price), costSource: price.source ?? "config" });
|
|
5878
|
+
}
|
|
5879
|
+
meta.priorUsage = meta.priorUsage ? addUsage(meta.priorUsage, used) : used;
|
|
5880
|
+
if (meta.mode === "read") return;
|
|
5881
|
+
let changed = [];
|
|
5882
|
+
try {
|
|
5883
|
+
const root = meta.mode === "isolate" ? meta.worktree : meta.repoRoot;
|
|
5884
|
+
if (meta.baseTree) changed = diffTrees(root, meta.baseTree, snapshotTree(root, meta.mode === "isolate" ? meta.link : [])).changes.map((c) => c.path);
|
|
5885
|
+
} catch {
|
|
5886
|
+
}
|
|
5887
|
+
const list2 = changed.length ? `: ${changed.slice(0, 30).join(", ")}${changed.length > 30 ? `, and ${changed.length - 30} more` : ""}` : "";
|
|
5888
|
+
meta.handoff = `Another worker started this task and stopped part way, at its usage limit. Its edits are already in the files${list2}. Do not start over: check what it changed against the task, keep what is right, fix what is not, and finish the task.`;
|
|
5597
5889
|
}
|
|
5598
5890
|
var HINTS = {
|
|
5599
5891
|
"model-unavailable": "the model is not available; fix the worker config, pass --worker/--model, or configure fallback workers (see `pitroom doctor`)",
|
|
@@ -5608,12 +5900,12 @@ function finalize(meta, res) {
|
|
|
5608
5900
|
if (meta.snapshot?.dir && meta.repoRoot) run2.finalText = backToProject(run2.finalText, meta.snapshot.dir, meta.repoRoot);
|
|
5609
5901
|
meta.sessionId = run2.sessionId ?? meta.sessionId;
|
|
5610
5902
|
meta.usage = run2.usage;
|
|
5611
|
-
|
|
5903
|
+
fs32.writeFileSync(runFile(meta.id, "summary.md"), `${run2.finalText}
|
|
5612
5904
|
`);
|
|
5613
5905
|
if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run2.finalText);
|
|
5614
5906
|
if (meta.reviewOf) {
|
|
5615
5907
|
meta.verdict = parseVerdict(run2.finalText);
|
|
5616
|
-
if (meta.packageFile)
|
|
5908
|
+
if (meta.packageFile) fs32.rmSync(meta.packageFile, { force: true });
|
|
5617
5909
|
}
|
|
5618
5910
|
for (const w of backend.warnings?.(run2, read2(runFile(meta.id, "stderr.log"))) ?? []) {
|
|
5619
5911
|
if (!meta.warnings.includes(w)) meta.warnings.push(w);
|
|
@@ -5659,6 +5951,7 @@ function finalize(meta, res) {
|
|
|
5659
5951
|
meta.usage.costSource = price.source ?? "config";
|
|
5660
5952
|
}
|
|
5661
5953
|
}
|
|
5954
|
+
if (meta.priorUsage && meta.usage) meta.usage = addUsage(meta.priorUsage, meta.usage);
|
|
5662
5955
|
meta.returnedTokens = estimateTokens(formatReport(meta, run2.finalText));
|
|
5663
5956
|
meta.savedUsd = meta.auditOf ? 0 : savedUsd(meta.usage, meta.returnedTokens);
|
|
5664
5957
|
writeMeta(meta);
|
|
@@ -5677,16 +5970,16 @@ function captureChanges(meta) {
|
|
|
5677
5970
|
const d = diffTrees(target, meta.baseTree, meta.afterTree);
|
|
5678
5971
|
meta.changes = d.changes;
|
|
5679
5972
|
meta.stats = d.stats;
|
|
5680
|
-
|
|
5973
|
+
fs32.writeFileSync(runFile(meta.id, "changes.patch"), d.patch);
|
|
5681
5974
|
}
|
|
5682
5975
|
function verifyPath() {
|
|
5683
|
-
const extra = [
|
|
5684
|
-
const parts = (process.env.PATH ?? "").split(
|
|
5685
|
-
for (const d of process.platform === "win32" ? [
|
|
5686
|
-
return parts.join(
|
|
5976
|
+
const extra = [path28.dirname(process.execPath), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
|
|
5977
|
+
const parts = (process.env.PATH ?? "").split(path28.delimiter).filter(Boolean);
|
|
5978
|
+
for (const d of process.platform === "win32" ? [path28.dirname(process.execPath)] : extra) if (!parts.includes(d)) parts.push(d);
|
|
5979
|
+
return parts.join(path28.delimiter);
|
|
5687
5980
|
}
|
|
5688
5981
|
function runVerify(meta) {
|
|
5689
|
-
const r =
|
|
5982
|
+
const r = spawnSync10(meta.verify, {
|
|
5690
5983
|
cwd: meta.cwd,
|
|
5691
5984
|
shell: true,
|
|
5692
5985
|
encoding: "utf8",
|
|
@@ -5696,7 +5989,7 @@ function runVerify(meta) {
|
|
|
5696
5989
|
env: { ...process.env, PATH: verifyPath(), PWD: meta.cwd, PITROOM_ACTIVE: "1" }
|
|
5697
5990
|
});
|
|
5698
5991
|
const output = `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
|
5699
|
-
|
|
5992
|
+
fs32.writeFileSync(runFile(meta.id, "verify.log"), output);
|
|
5700
5993
|
const notFound = process.platform === "win32" && r.status === 1 && /is not recognized as an internal or external command/.test(output);
|
|
5701
5994
|
return { ok: r.status === 0, code: notFound ? 127 : r.status, tail: output.trimEnd().split("\n").slice(-25).join("\n") };
|
|
5702
5995
|
}
|
|
@@ -5751,12 +6044,12 @@ function discardRun(meta) {
|
|
|
5751
6044
|
return `discarded the isolated copy of ${meta.id}; the patch stays in ${runFile(meta.id, "changes.patch")}`;
|
|
5752
6045
|
}
|
|
5753
6046
|
function cleanupWorktree(meta) {
|
|
5754
|
-
if (meta.worktree &&
|
|
6047
|
+
if (meta.worktree && fs32.existsSync(meta.worktree)) removeIsolatedCopy(meta.worktree, worktreesDir());
|
|
5755
6048
|
}
|
|
5756
6049
|
|
|
5757
6050
|
// src/cli/mcp-extras.ts
|
|
5758
|
-
import
|
|
5759
|
-
import
|
|
6051
|
+
import fs33 from "node:fs";
|
|
6052
|
+
import path29 from "node:path";
|
|
5760
6053
|
|
|
5761
6054
|
// src/cli/mcp-support.ts
|
|
5762
6055
|
import { spawn as spawn5 } from "node:child_process";
|
|
@@ -6041,7 +6334,7 @@ function skillPrompts() {
|
|
|
6041
6334
|
const root = packageRoot();
|
|
6042
6335
|
for (const name of skillNames(root)) {
|
|
6043
6336
|
try {
|
|
6044
|
-
const text =
|
|
6337
|
+
const text = fs33.readFileSync(path29.join(root, "skills", name, "SKILL.md"), "utf8");
|
|
6045
6338
|
const front = /^---\n([\s\S]*?)\n---\n/.exec(text);
|
|
6046
6339
|
const body = (front ? text.slice(front[0].length) : text).trim();
|
|
6047
6340
|
const description = /^description:\s*(.+)$/m.exec(front?.[1] ?? "")?.[1]?.trim() ?? `The ${name} skill.`;
|
|
@@ -6103,6 +6396,7 @@ var TOPICS = {
|
|
|
6103
6396
|
stats: ["since"],
|
|
6104
6397
|
savings: ["since", "perModel"],
|
|
6105
6398
|
models: ["worker", "all"],
|
|
6399
|
+
prices: ["worker"],
|
|
6106
6400
|
cooldown: [],
|
|
6107
6401
|
config: [],
|
|
6108
6402
|
doctor: []
|
|
@@ -6128,6 +6422,8 @@ function infoArgs(topic, a) {
|
|
|
6128
6422
|
return { args: ["history", "stats", ...since(a)] };
|
|
6129
6423
|
case "savings":
|
|
6130
6424
|
return { args: ["savings", ...since(a), ...bool(a, "perModel") ? ["--models"] : []] };
|
|
6425
|
+
case "prices":
|
|
6426
|
+
return { args: ["prices", ...worker ? ["--", worker] : []] };
|
|
6131
6427
|
case "models":
|
|
6132
6428
|
return { args: ["models", ...worker ? [worker] : [], ...bool(a, "all") ? ["--all"] : []] };
|
|
6133
6429
|
case "doctor":
|
|
@@ -6210,7 +6506,7 @@ var TOOLS2 = [
|
|
|
6210
6506
|
{
|
|
6211
6507
|
name: "pitroom_info",
|
|
6212
6508
|
title: "Pitroom information",
|
|
6213
|
-
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).",
|
|
6509
|
+
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), prices (USD per 1M tokens: the primary the savings are measured against, and each worker in use or the one named; worker), cooldown (rate-limited models being skipped), config, doctor (setup check).",
|
|
6214
6510
|
inputSchema: {
|
|
6215
6511
|
type: "object",
|
|
6216
6512
|
properties: {
|
|
@@ -6559,22 +6855,22 @@ async function serveMcp() {
|
|
|
6559
6855
|
|
|
6560
6856
|
// src/cli/mcp-http.ts
|
|
6561
6857
|
import crypto10 from "node:crypto";
|
|
6562
|
-
import
|
|
6858
|
+
import fs35 from "node:fs";
|
|
6563
6859
|
import http2 from "node:http";
|
|
6564
|
-
import
|
|
6860
|
+
import path31 from "node:path";
|
|
6565
6861
|
|
|
6566
6862
|
// src/core/dash.ts
|
|
6567
6863
|
import { spawn as spawn6 } from "node:child_process";
|
|
6568
6864
|
import crypto9 from "node:crypto";
|
|
6569
|
-
import
|
|
6865
|
+
import fs34 from "node:fs";
|
|
6570
6866
|
import http from "node:http";
|
|
6571
|
-
import
|
|
6867
|
+
import path30 from "node:path";
|
|
6572
6868
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
6573
6869
|
|
|
6574
6870
|
// src/core/file-diff.ts
|
|
6575
6871
|
var PREVIEW_LINES = 300;
|
|
6576
6872
|
function gitPath(value, prefix = true) {
|
|
6577
|
-
let
|
|
6873
|
+
let path39 = value;
|
|
6578
6874
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
6579
6875
|
const bytes = [];
|
|
6580
6876
|
const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
|
|
@@ -6582,9 +6878,9 @@ function gitPath(value, prefix = true) {
|
|
|
6582
6878
|
if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
|
|
6583
6879
|
else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
|
|
6584
6880
|
}
|
|
6585
|
-
|
|
6881
|
+
path39 = new TextDecoder().decode(new Uint8Array(bytes));
|
|
6586
6882
|
}
|
|
6587
|
-
return prefix ?
|
|
6883
|
+
return prefix ? path39.replace(/^[ab]\//, "") : path39;
|
|
6588
6884
|
}
|
|
6589
6885
|
function headerPath(header) {
|
|
6590
6886
|
const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
|
|
@@ -6753,6 +7049,7 @@ function toRun(m) {
|
|
|
6753
7049
|
}
|
|
6754
7050
|
function dashState(opts = {}) {
|
|
6755
7051
|
const limit = Math.min(Math.max(opts.limit ?? 40, 1), 200);
|
|
7052
|
+
const scanned = [];
|
|
6756
7053
|
const metas = [];
|
|
6757
7054
|
const groups = /* @__PURE__ */ new Set();
|
|
6758
7055
|
for (const id2 of listRunIds().slice(-SCAN).reverse()) {
|
|
@@ -6762,6 +7059,7 @@ function dashState(opts = {}) {
|
|
|
6762
7059
|
} catch {
|
|
6763
7060
|
continue;
|
|
6764
7061
|
}
|
|
7062
|
+
scanned.push(m);
|
|
6765
7063
|
if (m.group) groups.add(m.group);
|
|
6766
7064
|
if (opts.group && m.group !== opts.group) continue;
|
|
6767
7065
|
metas.push(m);
|
|
@@ -6769,6 +7067,7 @@ function dashState(opts = {}) {
|
|
|
6769
7067
|
const whole = new Set(metas.slice(0, limit).flatMap((m) => m.group ? [m.group] : []));
|
|
6770
7068
|
const runs = metas.filter((m, i) => i < limit || m.group && whole.has(m.group)).map(toRun);
|
|
6771
7069
|
const now = Date.now();
|
|
7070
|
+
settleLedger(scanned);
|
|
6772
7071
|
const ledger = readLedger();
|
|
6773
7072
|
const within = (at, days2) => !!at && Date.parse(at) >= now - days2 * DAY_MS;
|
|
6774
7073
|
const per = (f) => Object.fromEntries(Object.entries(PERIODS).map(([p, days2]) => [p, f(days2)]));
|
|
@@ -6862,11 +7161,11 @@ var assetsDir = () => fileURLToPath4(new URL("./ui/", import.meta.url));
|
|
|
6862
7161
|
var assetCache = /* @__PURE__ */ new Map();
|
|
6863
7162
|
function readAsset(route) {
|
|
6864
7163
|
try {
|
|
6865
|
-
const file2 =
|
|
6866
|
-
const { mtimeMs } =
|
|
7164
|
+
const file2 = path30.join(assetsDir(), path30.basename(route));
|
|
7165
|
+
const { mtimeMs } = fs34.statSync(file2);
|
|
6867
7166
|
const hit = assetCache.get(route);
|
|
6868
7167
|
if (hit && hit.mtimeMs === mtimeMs) return hit.data;
|
|
6869
|
-
const data =
|
|
7168
|
+
const data = fs34.readFileSync(file2);
|
|
6870
7169
|
assetCache.set(route, { mtimeMs, data });
|
|
6871
7170
|
return data;
|
|
6872
7171
|
} catch {
|
|
@@ -6995,7 +7294,7 @@ function startDash(opts) {
|
|
|
6995
7294
|
});
|
|
6996
7295
|
});
|
|
6997
7296
|
}
|
|
6998
|
-
var registryFile = () =>
|
|
7297
|
+
var registryFile = () => path30.join(home(), "dash.json");
|
|
6999
7298
|
async function ping(port) {
|
|
7000
7299
|
try {
|
|
7001
7300
|
const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
|
|
@@ -7007,12 +7306,12 @@ async function ping(port) {
|
|
|
7007
7306
|
async function runningDash() {
|
|
7008
7307
|
let r;
|
|
7009
7308
|
try {
|
|
7010
|
-
r = JSON.parse(
|
|
7309
|
+
r = JSON.parse(fs34.readFileSync(registryFile(), "utf8"));
|
|
7011
7310
|
} catch {
|
|
7012
7311
|
return void 0;
|
|
7013
7312
|
}
|
|
7014
7313
|
if (!isAlive(r.pid)) {
|
|
7015
|
-
|
|
7314
|
+
fs34.rmSync(registryFile(), { force: true });
|
|
7016
7315
|
return void 0;
|
|
7017
7316
|
}
|
|
7018
7317
|
return await ping(r.port) ? { ...r, url: `http://127.0.0.1:${r.port}/` } : void 0;
|
|
@@ -7029,7 +7328,7 @@ async function dashCommand(o) {
|
|
|
7029
7328
|
if (o.stop) {
|
|
7030
7329
|
if (!existing) throw new UserError("pitroom dash is not running");
|
|
7031
7330
|
process.kill(existing.pid, "SIGTERM");
|
|
7032
|
-
|
|
7331
|
+
fs34.rmSync(registryFile(), { force: true });
|
|
7033
7332
|
o.log(`stopped pitroom dash (${existing.url})`);
|
|
7034
7333
|
return 0;
|
|
7035
7334
|
}
|
|
@@ -7058,6 +7357,7 @@ async function dashCommand(o) {
|
|
|
7058
7357
|
throw new UserError("pitroom dash did not start (is the port taken? try --port 0)");
|
|
7059
7358
|
}
|
|
7060
7359
|
importRuns();
|
|
7360
|
+
settleLedger();
|
|
7061
7361
|
const explicit = o.port !== void 0;
|
|
7062
7362
|
let dash;
|
|
7063
7363
|
try {
|
|
@@ -7066,9 +7366,9 @@ async function dashCommand(o) {
|
|
|
7066
7366
|
if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
|
|
7067
7367
|
dash = await startDash({ port: 0, idleMs: o.idleMs });
|
|
7068
7368
|
}
|
|
7069
|
-
|
|
7369
|
+
fs34.mkdirSync(home(), { recursive: true });
|
|
7070
7370
|
const tmp = `${registryFile()}.${process.pid}.tmp`;
|
|
7071
|
-
|
|
7371
|
+
fs34.writeFileSync(tmp, JSON.stringify({ pid: process.pid, port: dash.port }));
|
|
7072
7372
|
renameOver(tmp, registryFile());
|
|
7073
7373
|
const stop = () => void dash.close();
|
|
7074
7374
|
process.once("SIGINT", stop);
|
|
@@ -7078,7 +7378,7 @@ async function dashCommand(o) {
|
|
|
7078
7378
|
if (o.open) openBrowser(dash.url);
|
|
7079
7379
|
await dash.closed;
|
|
7080
7380
|
try {
|
|
7081
|
-
if (JSON.parse(
|
|
7381
|
+
if (JSON.parse(fs34.readFileSync(registryFile(), "utf8")).pid === process.pid) fs34.rmSync(registryFile(), { force: true });
|
|
7082
7382
|
} catch {
|
|
7083
7383
|
}
|
|
7084
7384
|
return 0;
|
|
@@ -7090,10 +7390,10 @@ var MAX_BODY = 4e6;
|
|
|
7090
7390
|
var MAX_SESSIONS = 64;
|
|
7091
7391
|
var IDLE_MS = 60 * 6e4;
|
|
7092
7392
|
var PATH = "/mcp";
|
|
7093
|
-
var tokenFile = () =>
|
|
7393
|
+
var tokenFile = () => path31.join(home(), "mcp-token");
|
|
7094
7394
|
var readToken = (file2) => {
|
|
7095
7395
|
try {
|
|
7096
|
-
const kept =
|
|
7396
|
+
const kept = fs35.readFileSync(file2, "utf8").trim();
|
|
7097
7397
|
return kept.length >= 16 ? kept : void 0;
|
|
7098
7398
|
} catch {
|
|
7099
7399
|
return void 0;
|
|
@@ -7108,10 +7408,10 @@ function mcpToken() {
|
|
|
7108
7408
|
const file2 = tokenFile();
|
|
7109
7409
|
let token = readToken(file2);
|
|
7110
7410
|
if (!token) {
|
|
7111
|
-
|
|
7411
|
+
fs35.mkdirSync(path31.dirname(file2), { recursive: true });
|
|
7112
7412
|
token = crypto10.randomBytes(32).toString("hex");
|
|
7113
7413
|
try {
|
|
7114
|
-
|
|
7414
|
+
fs35.writeFileSync(file2, `${token}
|
|
7115
7415
|
`, { mode: 384, flag: "wx" });
|
|
7116
7416
|
} catch (e) {
|
|
7117
7417
|
if (e.code !== "EEXIST") throw e;
|
|
@@ -7120,7 +7420,7 @@ function mcpToken() {
|
|
|
7120
7420
|
}
|
|
7121
7421
|
}
|
|
7122
7422
|
try {
|
|
7123
|
-
|
|
7423
|
+
fs35.chmodSync(file2, 384);
|
|
7124
7424
|
} catch {
|
|
7125
7425
|
}
|
|
7126
7426
|
return { token, from: file2 };
|
|
@@ -7301,17 +7601,17 @@ async function serveMcpHttp(opts) {
|
|
|
7301
7601
|
}
|
|
7302
7602
|
|
|
7303
7603
|
// src/cli/commands.ts
|
|
7304
|
-
import { spawnSync as
|
|
7305
|
-
import
|
|
7306
|
-
import
|
|
7604
|
+
import { spawnSync as spawnSync13 } from "node:child_process";
|
|
7605
|
+
import fs42 from "node:fs";
|
|
7606
|
+
import path37 from "node:path";
|
|
7307
7607
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
7308
7608
|
|
|
7309
7609
|
// src/core/init.ts
|
|
7310
|
-
import
|
|
7311
|
-
import
|
|
7610
|
+
import fs36 from "node:fs";
|
|
7611
|
+
import path32 from "node:path";
|
|
7312
7612
|
var FREE = /-free$/;
|
|
7313
7613
|
var TIER_ORDER = [["cheap", "opencode"], ["standard", "codex"], ["capable", "claude"]];
|
|
7314
|
-
var isFound = (binary8) =>
|
|
7614
|
+
var isFound = (binary8) => path32.isAbsolute(binary8) && fs36.existsSync(binary8);
|
|
7315
7615
|
function planInit(opts = {}) {
|
|
7316
7616
|
const file2 = configPath();
|
|
7317
7617
|
const workers = backendIds().map((id2) => {
|
|
@@ -7357,15 +7657,15 @@ function planInit(opts = {}) {
|
|
|
7357
7657
|
if (Object.keys(tiers).length > 1) config.tiers = tiers;
|
|
7358
7658
|
if (!workers.some((w) => w.found)) blocked = "no worker CLI found: install OpenCode (https://opencode.ai), Codex CLI, Claude Code or Gemini CLI first";
|
|
7359
7659
|
else if (!blocked && !Object.keys(config).length) notes.push("nothing to propose: the default model and the single worker need no config");
|
|
7360
|
-
return { path: file2, exists:
|
|
7660
|
+
return { path: file2, exists: fs36.existsSync(file2), workers, opencode: opencode2, config, blocked, notes };
|
|
7361
7661
|
}
|
|
7362
7662
|
function writeInit(plan, force) {
|
|
7363
7663
|
if (plan.blocked) throw new UserError(plan.blocked);
|
|
7364
7664
|
if (!Object.keys(plan.config).length) throw new UserError("nothing to write: the proposal is empty");
|
|
7365
|
-
if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${
|
|
7366
|
-
|
|
7367
|
-
if (plan.exists)
|
|
7368
|
-
|
|
7665
|
+
if (plan.exists && !force) throw new UserError(`${plan.path} already exists: pass --force to replace it (the old file is kept as ${path32.basename(plan.path)}.bak)`);
|
|
7666
|
+
fs36.mkdirSync(path32.dirname(plan.path), { recursive: true });
|
|
7667
|
+
if (plan.exists) fs36.copyFileSync(plan.path, `${plan.path}.bak`);
|
|
7668
|
+
fs36.writeFileSync(plan.path, `${JSON.stringify(plan.config, null, 2)}
|
|
7369
7669
|
`);
|
|
7370
7670
|
return plan.path;
|
|
7371
7671
|
}
|
|
@@ -7385,18 +7685,18 @@ function formatInit(plan, written) {
|
|
|
7385
7685
|
}
|
|
7386
7686
|
|
|
7387
7687
|
// src/core/mcp-install.ts
|
|
7388
|
-
import { spawnSync as
|
|
7389
|
-
import
|
|
7688
|
+
import { spawnSync as spawnSync11 } from "node:child_process";
|
|
7689
|
+
import fs37 from "node:fs";
|
|
7390
7690
|
import os12 from "node:os";
|
|
7391
|
-
import
|
|
7691
|
+
import path33 from "node:path";
|
|
7392
7692
|
var CLIENT_IDS = ["claude-code", "codex", "gemini", "cursor", "claude-desktop"];
|
|
7393
7693
|
var NAME = "pitroom";
|
|
7394
7694
|
function mcpCommand() {
|
|
7395
7695
|
const launcher = launcherPath();
|
|
7396
7696
|
if (isPitroomLauncher(launcher)) return { command: launcher, args: ["mcp"] };
|
|
7397
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
7398
|
-
const p =
|
|
7399
|
-
if (
|
|
7697
|
+
for (const dir of (process.env.PATH ?? "").split(path33.delimiter).filter(Boolean)) {
|
|
7698
|
+
const p = path33.join(dir, process.platform === "win32" ? "pitroom.cmd" : "pitroom");
|
|
7699
|
+
if (fs37.existsSync(p)) return { command: path33.resolve(p), args: ["mcp"] };
|
|
7400
7700
|
}
|
|
7401
7701
|
return { command: process.execPath, args: [process.argv[1] ?? "pitroom", "mcp"] };
|
|
7402
7702
|
}
|
|
@@ -7406,13 +7706,13 @@ var same = (a, b) => {
|
|
|
7406
7706
|
};
|
|
7407
7707
|
function run(backendId, args) {
|
|
7408
7708
|
const { command, prefix } = resolveCommand(getBackend(backendId).binary());
|
|
7409
|
-
const r =
|
|
7709
|
+
const r = spawnSync11(command, [...prefix, ...args], { encoding: "utf8", timeout: 6e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
7410
7710
|
return { ok: r.status === 0 && !r.error, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
|
|
7411
7711
|
}
|
|
7412
7712
|
var runnable = (backendId) => run(backendId, ["--version"]).ok;
|
|
7413
7713
|
var readJson = (file2) => {
|
|
7414
7714
|
try {
|
|
7415
|
-
return JSON.parse(
|
|
7715
|
+
return JSON.parse(fs37.readFileSync(file2, "utf8"));
|
|
7416
7716
|
} catch {
|
|
7417
7717
|
return void 0;
|
|
7418
7718
|
}
|
|
@@ -7422,7 +7722,7 @@ var jsonState = (file2, c) => {
|
|
|
7422
7722
|
return entry === void 0 ? "absent" : same(c, entry) ? "same" : "different";
|
|
7423
7723
|
};
|
|
7424
7724
|
var home2 = () => os12.homedir();
|
|
7425
|
-
var geminiDir = () =>
|
|
7725
|
+
var geminiDir = () => path33.join(process.env.GEMINI_CLI_HOME ?? home2(), ".gemini");
|
|
7426
7726
|
function cliClient(id2, backend, name, where, o) {
|
|
7427
7727
|
return {
|
|
7428
7728
|
id: id2,
|
|
@@ -7445,43 +7745,43 @@ function jsonClient(id2, name, file2, dir) {
|
|
|
7445
7745
|
return {
|
|
7446
7746
|
id: id2,
|
|
7447
7747
|
name,
|
|
7448
|
-
found: () =>
|
|
7748
|
+
found: () => fs37.existsSync(dir()),
|
|
7449
7749
|
state: (c) => jsonState(file2(), c),
|
|
7450
7750
|
add: (c) => {
|
|
7451
7751
|
let config = {};
|
|
7452
|
-
if (
|
|
7752
|
+
if (fs37.existsSync(file2())) {
|
|
7453
7753
|
config = readJson(file2());
|
|
7454
7754
|
if (!config || typeof config !== "object" || Array.isArray(config)) return { ok: false, message: `${rel()} is not valid JSON: left alone` };
|
|
7455
7755
|
const backup = `${file2()}.bak-pitroom`;
|
|
7456
|
-
if (!
|
|
7756
|
+
if (!fs37.existsSync(backup)) fs37.copyFileSync(file2(), backup);
|
|
7457
7757
|
}
|
|
7458
7758
|
config.mcpServers = { ...config.mcpServers ?? {}, [NAME]: { command: c.command, args: c.args } };
|
|
7459
|
-
|
|
7759
|
+
fs37.mkdirSync(path33.dirname(file2()), { recursive: true });
|
|
7460
7760
|
const tmp = `${file2()}.${process.pid}.tmp`;
|
|
7461
|
-
|
|
7761
|
+
fs37.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
|
|
7462
7762
|
`);
|
|
7463
|
-
|
|
7763
|
+
fs37.renameSync(tmp, file2());
|
|
7464
7764
|
return { ok: true, message: rel() };
|
|
7465
7765
|
},
|
|
7466
7766
|
remove: () => {
|
|
7467
7767
|
const config = readJson(file2());
|
|
7468
7768
|
if (!config?.mcpServers?.[NAME]) return { ok: true, message: rel() };
|
|
7469
7769
|
delete config.mcpServers[NAME];
|
|
7470
|
-
|
|
7770
|
+
fs37.writeFileSync(file2(), `${JSON.stringify(config, null, 2)}
|
|
7471
7771
|
`);
|
|
7472
7772
|
return { ok: true, message: rel() };
|
|
7473
7773
|
}
|
|
7474
7774
|
};
|
|
7475
7775
|
}
|
|
7476
7776
|
function desktopFile() {
|
|
7477
|
-
if (process.platform === "darwin") return
|
|
7478
|
-
if (process.platform === "win32") return
|
|
7479
|
-
return
|
|
7777
|
+
if (process.platform === "darwin") return path33.join(home2(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
7778
|
+
if (process.platform === "win32") return path33.join(process.env.APPDATA ?? path33.join(home2(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
7779
|
+
return path33.join(process.env.XDG_CONFIG_HOME ?? path33.join(home2(), ".config"), "Claude", "claude_desktop_config.json");
|
|
7480
7780
|
}
|
|
7481
7781
|
function codexState(c) {
|
|
7482
7782
|
let text;
|
|
7483
7783
|
try {
|
|
7484
|
-
text =
|
|
7784
|
+
text = fs37.readFileSync(path33.join(process.env.CODEX_HOME ?? path33.join(home2(), ".codex"), "config.toml"), "utf8");
|
|
7485
7785
|
} catch {
|
|
7486
7786
|
return "absent";
|
|
7487
7787
|
}
|
|
@@ -7499,7 +7799,7 @@ function codexState(c) {
|
|
|
7499
7799
|
function clients() {
|
|
7500
7800
|
return [
|
|
7501
7801
|
cliClient("claude-code", "claude", "Claude Code", "user settings (~/.claude.json)", {
|
|
7502
|
-
state: (c) => jsonState(
|
|
7802
|
+
state: (c) => jsonState(path33.join(home2(), ".claude.json"), c),
|
|
7503
7803
|
add: (c) => ["mcp", "add", "--scope", "user", NAME, "--", c.command, ...c.args],
|
|
7504
7804
|
remove: () => ["mcp", "remove", NAME, "--scope", "user"]
|
|
7505
7805
|
}),
|
|
@@ -7509,12 +7809,12 @@ function clients() {
|
|
|
7509
7809
|
remove: () => ["mcp", "remove", NAME]
|
|
7510
7810
|
}),
|
|
7511
7811
|
cliClient("gemini", "gemini", "Gemini CLI", "user settings (~/.gemini/settings.json)", {
|
|
7512
|
-
state: (c) => jsonState(
|
|
7812
|
+
state: (c) => jsonState(path33.join(geminiDir(), "settings.json"), c),
|
|
7513
7813
|
add: (c) => ["mcp", "add", "--scope", "user", NAME, c.command, ...c.args],
|
|
7514
7814
|
remove: () => ["mcp", "remove", "--scope", "user", NAME]
|
|
7515
7815
|
}),
|
|
7516
|
-
jsonClient("cursor", "Cursor", () =>
|
|
7517
|
-
jsonClient("claude-desktop", "Claude Desktop", desktopFile, () =>
|
|
7816
|
+
jsonClient("cursor", "Cursor", () => path33.join(home2(), ".cursor", "mcp.json"), () => path33.join(home2(), ".cursor")),
|
|
7817
|
+
jsonClient("claude-desktop", "Claude Desktop", desktopFile, () => path33.dirname(desktopFile()))
|
|
7518
7818
|
];
|
|
7519
7819
|
}
|
|
7520
7820
|
function attempt2(f) {
|
|
@@ -7585,16 +7885,16 @@ var parseClients = (value) => {
|
|
|
7585
7885
|
|
|
7586
7886
|
// src/core/plan-status.ts
|
|
7587
7887
|
import crypto11 from "node:crypto";
|
|
7588
|
-
import
|
|
7589
|
-
import
|
|
7888
|
+
import fs38 from "node:fs";
|
|
7889
|
+
import path34 from "node:path";
|
|
7590
7890
|
function notesFile(plan) {
|
|
7591
7891
|
const id2 = crypto11.createHash("sha1").update(plan.file).digest("hex").slice(0, 12);
|
|
7592
|
-
return
|
|
7892
|
+
return path34.join(home(), "plans", id2, "notes.md");
|
|
7593
7893
|
}
|
|
7594
7894
|
function readNotes(plan) {
|
|
7595
7895
|
const f = notesFile(plan);
|
|
7596
|
-
if (!
|
|
7597
|
-
return
|
|
7896
|
+
if (!fs38.existsSync(f)) return [];
|
|
7897
|
+
return fs38.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
|
|
7598
7898
|
const m = /^(\d{4}-\d\d-\d\d \d\d:\d\d) (.*)$/.exec(l);
|
|
7599
7899
|
return m ? { at: m[1], text: m[2] } : { at: "", text: l };
|
|
7600
7900
|
});
|
|
@@ -7602,11 +7902,11 @@ function readNotes(plan) {
|
|
|
7602
7902
|
function addNote(planFile, text) {
|
|
7603
7903
|
const plan = loadPlan(planFile);
|
|
7604
7904
|
const f = notesFile(plan);
|
|
7605
|
-
|
|
7606
|
-
if (!
|
|
7905
|
+
fs38.mkdirSync(path34.dirname(f), { recursive: true });
|
|
7906
|
+
if (!fs38.existsSync(f)) fs38.writeFileSync(f, `# plan: ${plan.file}
|
|
7607
7907
|
`);
|
|
7608
7908
|
const at = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
|
|
7609
|
-
|
|
7909
|
+
fs38.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
|
|
7610
7910
|
`);
|
|
7611
7911
|
return f;
|
|
7612
7912
|
}
|
|
@@ -7674,8 +7974,8 @@ ${rulings}`,
|
|
|
7674
7974
|
|
|
7675
7975
|
// src/core/cache.ts
|
|
7676
7976
|
import crypto12 from "node:crypto";
|
|
7677
|
-
import
|
|
7678
|
-
import
|
|
7977
|
+
import fs39 from "node:fs";
|
|
7978
|
+
import path35 from "node:path";
|
|
7679
7979
|
function preferredReadsOwnSnapshot(o) {
|
|
7680
7980
|
try {
|
|
7681
7981
|
const preferred = effective().worker.value;
|
|
@@ -7688,20 +7988,20 @@ var SCAN2 = 500;
|
|
|
7688
7988
|
function cacheKey(o) {
|
|
7689
7989
|
if (o.verify || o.mode !== "read" || o.continueFrom || o.web || o.plan || o.review || o.audit || o.auditRate === 1) return void 0;
|
|
7690
7990
|
if (!(effective().cacheDays.value > 0)) return void 0;
|
|
7691
|
-
const dir = canonical(
|
|
7991
|
+
const dir = canonical(path35.resolve(o.dir));
|
|
7692
7992
|
const root = repoRoot(dir);
|
|
7693
7993
|
if (!root) return void 0;
|
|
7694
7994
|
try {
|
|
7695
7995
|
const tree = snapshotTree(root);
|
|
7696
7996
|
const head = commitOf(root, "HEAD") ?? "";
|
|
7697
7997
|
const files = o.files.map((f) => {
|
|
7698
|
-
const abs =
|
|
7699
|
-
return [abs, crypto12.createHash("sha256").update(
|
|
7998
|
+
const abs = path35.resolve(f);
|
|
7999
|
+
return [abs, crypto12.createHash("sha256").update(fs39.readFileSync(abs)).digest("hex")];
|
|
7700
8000
|
});
|
|
7701
8001
|
const question = o.task.replace(/\s+/g, " ").trim();
|
|
7702
8002
|
const readIn2 = READ_IN.includes(effective().readIn.value) ? effective().readIn.value : "auto";
|
|
7703
8003
|
const where = !o.inPlace && (preferredReadsOwnSnapshot(o) || wantSnapshot(readIn2, root, dir)) ? "snapshot" : "project";
|
|
7704
|
-
const key = crypto12.createHash("sha256").update(JSON.stringify([question,
|
|
8004
|
+
const key = crypto12.createHash("sha256").update(JSON.stringify([question, path35.relative(root, dir), files, where])).digest("hex");
|
|
7705
8005
|
return { key, state: `${head}:${tree}`, where };
|
|
7706
8006
|
} catch {
|
|
7707
8007
|
return void 0;
|
|
@@ -7744,13 +8044,13 @@ function ago(iso, now = Date.now()) {
|
|
|
7744
8044
|
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`;
|
|
7745
8045
|
|
|
7746
8046
|
// src/core/eval.ts
|
|
7747
|
-
import { spawnSync as
|
|
7748
|
-
import
|
|
8047
|
+
import { spawnSync as spawnSync12 } from "node:child_process";
|
|
8048
|
+
import fs40 from "node:fs";
|
|
7749
8049
|
var KINDS = ["definition", "count", "set"];
|
|
7750
8050
|
function loadEval(file2) {
|
|
7751
8051
|
let raw;
|
|
7752
8052
|
try {
|
|
7753
|
-
raw = JSON.parse(
|
|
8053
|
+
raw = JSON.parse(fs40.readFileSync(file2, "utf8"));
|
|
7754
8054
|
} catch (e) {
|
|
7755
8055
|
throw new UserError(`cannot read ${file2}: ${e.message}`);
|
|
7756
8056
|
}
|
|
@@ -7777,7 +8077,7 @@ function loadEval(file2) {
|
|
|
7777
8077
|
}
|
|
7778
8078
|
var clean = (p) => p.replace(/\\/g, "/").replace(/^\.?\//, "");
|
|
7779
8079
|
function gitGrep(root, flags, t) {
|
|
7780
|
-
const r =
|
|
8080
|
+
const r = spawnSync12("git", ["-C", root, "grep", ...flags, t.regex ? "-E" : "-F", ...t.word ? ["-w"] : [], "-e", t.grep, "--", t.dir ?? "."], {
|
|
7781
8081
|
encoding: "utf8",
|
|
7782
8082
|
maxBuffer: 64 * 1024 * 1024
|
|
7783
8083
|
});
|
|
@@ -7873,13 +8173,16 @@ function formatEval(rows, group) {
|
|
|
7873
8173
|
|
|
7874
8174
|
// src/core/review.ts
|
|
7875
8175
|
import crypto13 from "node:crypto";
|
|
7876
|
-
import
|
|
7877
|
-
import
|
|
8176
|
+
import fs41 from "node:fs";
|
|
8177
|
+
import path36 from "node:path";
|
|
7878
8178
|
var TEMPLATE2 = { task: "task-reviewer", fix: "re-review", range: "code-reviewer" };
|
|
7879
8179
|
var section = (title, body) => `## ${title}
|
|
7880
8180
|
|
|
7881
8181
|
${body.trim() || "(empty)"}
|
|
7882
8182
|
`;
|
|
8183
|
+
var numbered = (diff) => diff.trim() ? `${NUMBERED}
|
|
8184
|
+
|
|
8185
|
+
${/^[+ -]\s*\d+\| /m.test(diff) ? diff : numberDiff(diff)}` : diff;
|
|
7883
8186
|
function ancestors(m) {
|
|
7884
8187
|
const out = [];
|
|
7885
8188
|
for (let id2 = m.parent; id2; ) {
|
|
@@ -7892,7 +8195,7 @@ function ancestors(m) {
|
|
|
7892
8195
|
function briefOf(m) {
|
|
7893
8196
|
const root = [m, ...ancestors(m)].at(-1);
|
|
7894
8197
|
const f = runFile(root.id, "brief.md");
|
|
7895
|
-
return
|
|
8198
|
+
return fs41.existsSync(f) ? fs41.readFileSync(f, "utf8") : root.task;
|
|
7896
8199
|
}
|
|
7897
8200
|
function latestReview(ids) {
|
|
7898
8201
|
for (const id2 of listRunIds().reverse()) {
|
|
@@ -7906,7 +8209,7 @@ function latestReview(ids) {
|
|
|
7906
8209
|
}
|
|
7907
8210
|
function diffOf(m, from = m.baseTree) {
|
|
7908
8211
|
const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
|
|
7909
|
-
if (where &&
|
|
8212
|
+
if (where && fs41.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
|
|
7910
8213
|
if (from !== m.baseTree) {
|
|
7911
8214
|
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);
|
|
7912
8215
|
}
|
|
@@ -7920,7 +8223,7 @@ function runReview(id2) {
|
|
|
7920
8223
|
if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
|
|
7921
8224
|
const chain = ancestors(m);
|
|
7922
8225
|
const previous = latestReview(chain.map((a) => a.id));
|
|
7923
|
-
const dir = m.mode === "isolate" && m.worktree &&
|
|
8226
|
+
const dir = m.mode === "isolate" && m.worktree && fs41.existsSync(m.worktree) ? m.cwd : m.dir;
|
|
7924
8227
|
const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
|
|
7925
8228
|
const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
|
|
7926
8229
|
if (!previous) {
|
|
@@ -7928,7 +8231,7 @@ function runReview(id2) {
|
|
|
7928
8231
|
...job,
|
|
7929
8232
|
kind: "task",
|
|
7930
8233
|
package: [`# Review package \xB7 ${title}
|
|
7931
|
-
`, section("BRIEF", briefOf(m)), section("REPORT", readSummary(m)), section("DIFF", diffOf(m))].join("\n")
|
|
8234
|
+
`, section("BRIEF", briefOf(m)), section("REPORT", readSummary(m)), section("DIFF", numbered(diffOf(m)))].join("\n")
|
|
7932
8235
|
};
|
|
7933
8236
|
}
|
|
7934
8237
|
const reviewed = chain.find((a) => a.id === previous.reviewOf);
|
|
@@ -7942,7 +8245,7 @@ function runReview(id2) {
|
|
|
7942
8245
|
section("BRIEF", briefOf(m)),
|
|
7943
8246
|
section("PREVIOUS FINDINGS", readSummary(previous)),
|
|
7944
8247
|
section("FIX REPORT", readSummary(m)),
|
|
7945
|
-
section("FIX DIFF", diffOf(m, from))
|
|
8248
|
+
section("FIX DIFF", numbered(diffOf(m, from)))
|
|
7946
8249
|
].join("\n")
|
|
7947
8250
|
};
|
|
7948
8251
|
}
|
|
@@ -7996,19 +8299,19 @@ function writePackage(job) {
|
|
|
7996
8299
|
const root = repoRoot(job.dir);
|
|
7997
8300
|
if (!g || !root) throw new UserError(`not a git repository: ${job.dir}`);
|
|
7998
8301
|
const name = `review-${crypto13.randomBytes(4).toString("hex")}.md`;
|
|
7999
|
-
const inside3 = !
|
|
8000
|
-
const file2 = inside3 ?
|
|
8302
|
+
const inside3 = !path36.relative(fs41.realpathSync(root), fs41.realpathSync(g)).startsWith("..");
|
|
8303
|
+
const file2 = inside3 ? path36.join(g, "pitroom", name) : path36.join(root, ".pitroom", name);
|
|
8001
8304
|
if (!inside3) ignoreInGit(root, "/.pitroom/");
|
|
8002
|
-
|
|
8003
|
-
|
|
8305
|
+
fs41.mkdirSync(path36.dirname(file2), { recursive: true });
|
|
8306
|
+
fs41.writeFileSync(file2, job.package);
|
|
8004
8307
|
return file2;
|
|
8005
8308
|
}
|
|
8006
8309
|
function ignoreInGit(root, pattern) {
|
|
8007
|
-
const exclude =
|
|
8008
|
-
const text =
|
|
8310
|
+
const exclude = path36.resolve(root, git(root, ["rev-parse", "--git-path", "info/exclude"]).stdout.trim());
|
|
8311
|
+
const text = fs41.existsSync(exclude) ? fs41.readFileSync(exclude, "utf8") : "";
|
|
8009
8312
|
if (text.split(/\r?\n/).includes(pattern)) return;
|
|
8010
|
-
|
|
8011
|
-
|
|
8313
|
+
fs41.mkdirSync(path36.dirname(exclude), { recursive: true });
|
|
8314
|
+
fs41.appendFileSync(exclude, `${text && !text.endsWith("\n") ? "\n" : ""}# Pitroom review packages in worktrees (removed after each review)
|
|
8012
8315
|
${pattern}
|
|
8013
8316
|
`);
|
|
8014
8317
|
}
|
|
@@ -8164,10 +8467,10 @@ async function cmdReview(p) {
|
|
|
8164
8467
|
writeMeta(meta);
|
|
8165
8468
|
}
|
|
8166
8469
|
} catch (e) {
|
|
8167
|
-
|
|
8470
|
+
fs42.rmSync(packageFile, { force: true });
|
|
8168
8471
|
throw e;
|
|
8169
8472
|
}
|
|
8170
|
-
|
|
8473
|
+
fs42.writeFileSync(runFile(meta.id, "package.md"), job.package);
|
|
8171
8474
|
return launch(p, meta);
|
|
8172
8475
|
}
|
|
8173
8476
|
async function cmdAudit(p) {
|
|
@@ -8179,7 +8482,8 @@ async function cmdAudit(p) {
|
|
|
8179
8482
|
if (m.mode !== "read") throw new UserError(`run ${m.id} changed files; an audit re-checks a read run's answer: pitroom review ${m.id} judges a change`);
|
|
8180
8483
|
const answer = readSummary(m);
|
|
8181
8484
|
if (!answer.trim()) throw new UserError(`run ${m.id} gave no answer to audit`);
|
|
8182
|
-
const
|
|
8485
|
+
const named = flag(p, "worker") ?? flag(p, "tier");
|
|
8486
|
+
const [worker, ...others] = named ? [flag(p, "worker")] : auditors(m);
|
|
8183
8487
|
if (!worker && !flag(p, "tier")) {
|
|
8184
8488
|
throw new UserError('no other worker to audit with: name one with -W, or configure tiers "audit" or "cheap", or fallback workers, that differ from the one that answered', 3);
|
|
8185
8489
|
}
|
|
@@ -8189,7 +8493,8 @@ async function cmdAudit(p) {
|
|
|
8189
8493
|
dir: m.dir,
|
|
8190
8494
|
worker,
|
|
8191
8495
|
group: flag(p, "group") ?? m.group,
|
|
8192
|
-
noFallback:
|
|
8496
|
+
noFallback: !!named,
|
|
8497
|
+
fallbackTo: others,
|
|
8193
8498
|
audit: { of: m.id },
|
|
8194
8499
|
auditRate: void 0
|
|
8195
8500
|
});
|
|
@@ -8207,33 +8512,37 @@ async function cmdPrices(p) {
|
|
|
8207
8512
|
if (!has(p, "quiet")) console.log(`price catalog updated: ${r.models} priced models`);
|
|
8208
8513
|
}
|
|
8209
8514
|
const st = catalogStatus();
|
|
8210
|
-
const
|
|
8515
|
+
const priced = (t) => {
|
|
8516
|
+
const backend = getBackend(t.backend);
|
|
8517
|
+
const model = (t.model ?? "").split("#")[0];
|
|
8518
|
+
const price = workerPrice(t.backend, model);
|
|
8519
|
+
return {
|
|
8520
|
+
worker: model ? `${t.backend}:${model}` : t.backend,
|
|
8521
|
+
reportsCost: backend.capabilities.reportsCost,
|
|
8522
|
+
price: price ? { name: price.name, source: price.source, input: price.input, output: price.output, cachedInput: price.cachedInput } : void 0
|
|
8523
|
+
};
|
|
8524
|
+
};
|
|
8525
|
+
const asked = p.positional.length > 0;
|
|
8526
|
+
const used = asked ? p.positional.map((spec) => priced(parseTarget(spec, resolveChain().worker.backend))) : (() => {
|
|
8211
8527
|
try {
|
|
8212
8528
|
const chain = resolveChain();
|
|
8213
|
-
return [chain.worker, ...chain.fallback].map(
|
|
8214
|
-
const backend = getBackend(t.backend);
|
|
8215
|
-
const model = (t.model ?? "").split("#")[0];
|
|
8216
|
-
const price = workerPrice(t.backend, model);
|
|
8217
|
-
return {
|
|
8218
|
-
worker: model ? `${t.backend}:${model}` : t.backend,
|
|
8219
|
-
reportsCost: backend.capabilities.reportsCost,
|
|
8220
|
-
price: price ? { name: price.name, source: price.source, input: price.input, output: price.output, cachedInput: price.cachedInput } : void 0
|
|
8221
|
-
};
|
|
8222
|
-
});
|
|
8529
|
+
return [chain.worker, ...chain.fallback].map(priced);
|
|
8223
8530
|
} catch {
|
|
8224
8531
|
return [];
|
|
8225
8532
|
}
|
|
8226
8533
|
})();
|
|
8227
|
-
|
|
8534
|
+
const primary = primaryPrice();
|
|
8535
|
+
if (has(p, "json")) return console.log(JSON.stringify({ ...st, primary, workers: used }, null, 2)), 0;
|
|
8228
8536
|
console.log(`price feed: ${st.enabled ? "on" : 'off ("priceFeed": true in the config turns it on: Pitroom then fetches one public file, see PRIVACY.md)'}`);
|
|
8229
8537
|
console.log(`source: ${st.url}${st.enabled ? `, refreshed after a run when older than ${st.hours} h` : ""}`);
|
|
8230
8538
|
console.log(st.models ? `catalog: ${st.models} priced models, updated ${st.ageHours < 1 ? "within the hour" : `${st.ageHours.toFixed(1)} h ago`}` : "catalog: none yet (pitroom prices --refresh fetches it)");
|
|
8231
8539
|
if (st.lastFailure) console.log(`last try: failed: ${st.lastFailure.replace(/^\S+ /, "")}`);
|
|
8540
|
+
console.log(`primary: ${primary.name}: in ${primary.input} \xB7 out ${primary.output} \xB7 cached ${primary.cachedInput} USD/1M (what the savings are measured against)`);
|
|
8232
8541
|
if (used.length) {
|
|
8233
|
-
console.log("\nthe workers in use:");
|
|
8542
|
+
console.log(asked ? "\nthe workers asked about:" : "\nthe workers in use:");
|
|
8234
8543
|
for (const u of used) {
|
|
8235
8544
|
const what2 = u.reportsCost ? "reports its own cost" : u.price ? `in ${u.price.input} \xB7 out ${u.price.output} \xB7 cached ${u.price.cachedInput} USD/1M (${u.price.source === "catalog" ? `price catalog ${catalogLabel()}` : "your workerPrices"})` : "no price: its runs count as free in the savings";
|
|
8236
|
-
console.log(` ${u.worker.padEnd(36)} ${what2}`);
|
|
8545
|
+
console.log(` ${u.worker.padEnd(Math.max(36, ...used.map((x) => x.worker.length)))} ${what2}`);
|
|
8237
8546
|
}
|
|
8238
8547
|
}
|
|
8239
8548
|
return 0;
|
|
@@ -8276,7 +8585,7 @@ function cmdPlan(p) {
|
|
|
8276
8585
|
}
|
|
8277
8586
|
function cmdCrew(p) {
|
|
8278
8587
|
const file2 = flag(p, "task-file");
|
|
8279
|
-
const tasks = (file2 ?
|
|
8588
|
+
const tasks = (file2 ? fs42.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
|
|
8280
8589
|
if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
|
|
8281
8590
|
if (has(p, "write") && tasks.length > 1) {
|
|
8282
8591
|
throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
|
|
@@ -8303,7 +8612,7 @@ async function cmdEval(p) {
|
|
|
8303
8612
|
if (has(p, "write") || has(p, "isolate") || has(p, "continue") || has(p, "plan")) throw new UserError("eval asks read questions: drop -w/-i/--continue/--plan");
|
|
8304
8613
|
const questions = loadEval(file2);
|
|
8305
8614
|
const base2 = runOptions(p, "eval");
|
|
8306
|
-
const root = repoRoot(
|
|
8615
|
+
const root = repoRoot(path37.resolve(base2.dir));
|
|
8307
8616
|
const truths = new Map(questions.map((q) => [q.id, truthOf(q, root)]));
|
|
8308
8617
|
const workers = [...new Set(p.flags.get("worker") ?? [effective().worker.value])];
|
|
8309
8618
|
const group = flag(p, "group") ?? `eval-${(/* @__PURE__ */ new Date()).toISOString().slice(11, 19).replace(/:/g, "")}`;
|
|
@@ -8405,6 +8714,7 @@ function cmdHistory(p) {
|
|
|
8405
8714
|
return 0;
|
|
8406
8715
|
}
|
|
8407
8716
|
importRuns();
|
|
8717
|
+
settleLedger();
|
|
8408
8718
|
const since2 = sinceMs(flag(p, "since"));
|
|
8409
8719
|
if (sub === "stats") {
|
|
8410
8720
|
const s = historyStats(since2);
|
|
@@ -8424,14 +8734,16 @@ ${[fmt2(head2), ...rows2.map(fmt2)].join("\n")}`);
|
|
|
8424
8734
|
const { rows, total } = listHistory({ text: p.positional.join(" "), model: flag(p, "model"), state: flag(p, "state"), group: flag(p, "group"), sinceMs: since2, limit });
|
|
8425
8735
|
if (has(p, "json")) return console.log(JSON.stringify({ total, rows }, null, 2)), 0;
|
|
8426
8736
|
if (!rows.length) return console.log(total ? "nothing on this page" : "no matching runs"), 0;
|
|
8427
|
-
const
|
|
8428
|
-
const
|
|
8737
|
+
const cost = (r) => r.cost === void 0 ? "-" : r.cost === 0 ? "free" : `${r.costEstimated ? "~" : ""}${usd(r.cost)}`;
|
|
8738
|
+
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), cost(r), r.task.length > 60 ? `${r.task.slice(0, 59)}\u2026` : r.task]);
|
|
8739
|
+
const head = ["RUN", "WHEN", "STATE", "WORKER", "TIME", "COST", "TASK"];
|
|
8429
8740
|
const widths = head.map((h, i) => Math.max(h.length, ...body.map((r) => r[i].length)));
|
|
8430
8741
|
const fmt = (r, header = false) => r.map((c, i) => {
|
|
8431
8742
|
const cell2 = i === r.length - 1 ? c : c.padEnd(widths[i]);
|
|
8432
8743
|
return header ? dim(cell2) : i === 2 ? stateColour(r[2], cell2) : cell2;
|
|
8433
8744
|
}).join(" ");
|
|
8434
8745
|
console.log([fmt(head, true), ...body.map((r) => fmt(r))].join("\n"));
|
|
8746
|
+
if (rows.some((r) => r.costEstimated)) console.log(dim("\n~ estimated from a price: the worker's CLI reported no cost"));
|
|
8435
8747
|
if (total > rows.length) console.log(`
|
|
8436
8748
|
${rows.length} of ${total} \xB7 --limit N for more \xB7 pitroom show <run> for one`);
|
|
8437
8749
|
return 0;
|
|
@@ -8503,13 +8815,13 @@ function sinceMs(s) {
|
|
|
8503
8815
|
if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
|
|
8504
8816
|
return Date.now() - Number(m[1]) * 864e5;
|
|
8505
8817
|
}
|
|
8506
|
-
var readStdin = () => process.stdin.isTTY ? "" :
|
|
8818
|
+
var readStdin = () => process.stdin.isTTY ? "" : fs42.readFileSync(0, "utf8");
|
|
8507
8819
|
function cmdStatusline(p) {
|
|
8508
8820
|
const input = readStdin();
|
|
8509
8821
|
const lines = [];
|
|
8510
8822
|
const then = flag(p, "then");
|
|
8511
8823
|
if (then) {
|
|
8512
|
-
const r = process.platform === "win32" ?
|
|
8824
|
+
const r = process.platform === "win32" ? spawnSync13(then, { shell: true, input, encoding: "utf8", timeout: 5e3 }) : spawnSync13("/bin/sh", ["-c", then], { input, encoding: "utf8", timeout: 5e3 });
|
|
8513
8825
|
if (r.stdout?.trimEnd()) lines.push(r.stdout.trimEnd());
|
|
8514
8826
|
}
|
|
8515
8827
|
try {
|
|
@@ -8543,8 +8855,8 @@ function cmdModels(p) {
|
|
|
8543
8855
|
return 0;
|
|
8544
8856
|
}
|
|
8545
8857
|
function cmdHookStart() {
|
|
8546
|
-
const script =
|
|
8547
|
-
if (
|
|
8858
|
+
const script = path37.join(path37.dirname(path37.dirname(fileURLToPath5(import.meta.url))), "hooks", "session-start.mjs");
|
|
8859
|
+
if (fs42.existsSync(script)) spawnSync13(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
|
|
8548
8860
|
return 0;
|
|
8549
8861
|
}
|
|
8550
8862
|
function cmdInit(p) {
|
|
@@ -8556,19 +8868,21 @@ function cmdInit(p) {
|
|
|
8556
8868
|
}
|
|
8557
8869
|
function cmdSavings(p) {
|
|
8558
8870
|
const since2 = flag(p, "since") ?? "all";
|
|
8871
|
+
settleLedger();
|
|
8559
8872
|
const t = totals(readLedger(sinceMs(since2)));
|
|
8560
8873
|
if (has(p, "models")) {
|
|
8561
8874
|
const by = /* @__PURE__ */ new Map();
|
|
8562
8875
|
for (const e of readLedger(sinceMs(since2))) {
|
|
8563
8876
|
const key = `${e.backend ?? "?"} ${e.model ?? "(default model)"}`;
|
|
8564
|
-
const a = by.get(key) ?? { runs: 0, tokens: 0, returned: 0, cost: 0, saved: 0 };
|
|
8565
|
-
by.set(key, { runs: a.runs + 1, tokens: a.tokens + e.tokens, returned: a.returned + e.returned, cost: a.cost + e.workerCost, saved: a.saved + e.saved });
|
|
8877
|
+
const a = by.get(key) ?? { runs: 0, tokens: 0, returned: 0, cost: 0, estimated: false, saved: 0 };
|
|
8878
|
+
by.set(key, { runs: a.runs + 1, tokens: a.tokens + e.tokens, returned: a.returned + e.returned, cost: a.cost + e.workerCost, estimated: a.estimated || !!e.costEstimated, saved: a.saved + e.saved });
|
|
8566
8879
|
}
|
|
8567
8880
|
const width = Math.max(12, ...[...by.keys()].map((k) => k.length));
|
|
8568
8881
|
console.log(`${"worker model".padEnd(width)} ${"runs".padStart(5)} ${"processed".padStart(10)} ${"returned".padStart(9)} ${"cost".padStart(8)} ${"saved".padStart(8)}`);
|
|
8569
8882
|
for (const [key, a] of [...by].sort((x, y) => y[1].runs - x[1].runs)) {
|
|
8570
|
-
console.log(`${key.padEnd(width)} ${String(a.runs).padStart(5)} ${compact(a.tokens).padStart(10)} ${compact(a.returned).padStart(9)} ${usd(a.cost)
|
|
8883
|
+
console.log(`${key.padEnd(width)} ${String(a.runs).padStart(5)} ${compact(a.tokens).padStart(10)} ${compact(a.returned).padStart(9)} ${`${a.estimated ? "~" : ""}${usd(a.cost)}`.padStart(8)} ${usd(a.saved).padStart(8)}`);
|
|
8571
8884
|
}
|
|
8885
|
+
if ([...by.values()].some((a) => a.estimated)) console.log(dim("\n~ the cost is estimated from a price, in part or in all: the worker's CLI reported none"));
|
|
8572
8886
|
return 0;
|
|
8573
8887
|
}
|
|
8574
8888
|
const period = since2 === "all" ? "all time" : `last ${since2.replace("d", " days")}`;
|
|
@@ -8584,8 +8898,8 @@ function cmdSavings(p) {
|
|
|
8584
8898
|
est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
|
|
8585
8899
|
const out = flag(p, "card");
|
|
8586
8900
|
if (out) {
|
|
8587
|
-
|
|
8588
|
-
console.log(`card written to ${
|
|
8901
|
+
fs42.writeFileSync(out, card(t, period));
|
|
8902
|
+
console.log(`card written to ${path37.resolve(out)}`);
|
|
8589
8903
|
}
|
|
8590
8904
|
if (has(p, "badge")) console.log(`})`);
|
|
8591
8905
|
return 0;
|
|
@@ -8627,7 +8941,7 @@ function cmdConfig(p) {
|
|
|
8627
8941
|
console.log(JSON.stringify({ path: configPath(), settings: eff, warnings: warnings2 }, null, 2));
|
|
8628
8942
|
return 0;
|
|
8629
8943
|
}
|
|
8630
|
-
console.log(`config file: ${configPath()}${
|
|
8944
|
+
console.log(`config file: ${configPath()}${fs42.existsSync(configPath()) ? "" : " (not present)"}`);
|
|
8631
8945
|
for (const [key, s] of Object.entries(eff)) {
|
|
8632
8946
|
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);
|
|
8633
8947
|
console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
|
|
@@ -8651,7 +8965,7 @@ function cmdClean(p) {
|
|
|
8651
8965
|
const m = readMeta(id2);
|
|
8652
8966
|
if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
|
|
8653
8967
|
recordRun(m);
|
|
8654
|
-
|
|
8968
|
+
fs42.rmSync(runDir(id2), { recursive: true, force: true });
|
|
8655
8969
|
}
|
|
8656
8970
|
const snapshots = prune(Date.now(), 0);
|
|
8657
8971
|
console.log(`removed ${old.length} run(s)${snapshots ? ` and ${snapshots} read snapshot${snapshots === 1 ? "" : "s"}` : ""}; the history and the savings ledger are kept`);
|
|
@@ -8661,10 +8975,10 @@ var cmdRevert = (p) => (console.log(revertRun(freshMeta(resolveRun(p.positional[
|
|
|
8661
8975
|
var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positional[0])))), 0);
|
|
8662
8976
|
|
|
8663
8977
|
// src/core/doctor.ts
|
|
8664
|
-
import { spawnSync as
|
|
8665
|
-
import
|
|
8978
|
+
import { spawnSync as spawnSync14 } from "node:child_process";
|
|
8979
|
+
import fs43 from "node:fs";
|
|
8666
8980
|
import os13 from "node:os";
|
|
8667
|
-
import
|
|
8981
|
+
import path38 from "node:path";
|
|
8668
8982
|
var MARK2 = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
|
|
8669
8983
|
var NEXT = [
|
|
8670
8984
|
{ when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
|
|
@@ -8687,14 +9001,14 @@ function usageLine() {
|
|
|
8687
9001
|
return `${ids.length} run${ids.length === 1 ? "" : "s"} so far${when2} (pitroom savings shows what they saved)`;
|
|
8688
9002
|
}
|
|
8689
9003
|
function firstNodeOnPath() {
|
|
8690
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
8691
|
-
const file2 =
|
|
9004
|
+
for (const dir of (process.env.PATH ?? "").split(path38.delimiter).filter(Boolean)) {
|
|
9005
|
+
const file2 = path38.join(dir, process.platform === "win32" ? "node.exe" : "node");
|
|
8692
9006
|
try {
|
|
8693
|
-
if (!
|
|
9007
|
+
if (!fs43.statSync(file2).isFile()) continue;
|
|
8694
9008
|
} catch {
|
|
8695
9009
|
continue;
|
|
8696
9010
|
}
|
|
8697
|
-
const r =
|
|
9011
|
+
const r = spawnSync14(file2, ["-p", "process.versions.node"], { encoding: "utf8", timeout: 5e3 });
|
|
8698
9012
|
return r.status === 0 ? { path: file2, version: r.stdout.trim() } : { path: file2, version: "" };
|
|
8699
9013
|
}
|
|
8700
9014
|
return void 0;
|
|
@@ -8718,14 +9032,16 @@ function doctor8(probe2) {
|
|
|
8718
9032
|
const addAll = (list2) => list2.forEach((c) => add2(c.level, c.message));
|
|
8719
9033
|
add2("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
|
|
8720
9034
|
const onPath = firstNodeOnPath();
|
|
8721
|
-
if (onPath && onPath.version && tooOld(onPath.version) &&
|
|
9035
|
+
if (onPath && onPath.version && tooOld(onPath.version) && path38.resolve(onPath.path) !== path38.resolve(process.execPath)) {
|
|
8722
9036
|
add2("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)`);
|
|
8723
9037
|
}
|
|
8724
9038
|
add2(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
|
|
8725
9039
|
const cfg = loadConfig();
|
|
8726
|
-
add2(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${
|
|
9040
|
+
add2(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs43.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
|
|
8727
9041
|
for (const w of cfg.warnings) add2("warn", w);
|
|
8728
|
-
if (shimReady()
|
|
9042
|
+
if (shimReady() && process.platform === "win32" && !fs43.existsSync(path38.join(shimDir(), "git.exe"))) {
|
|
9043
|
+
add2("warn", "git guard shim ready as git.cmd only (no C# compiler of .NET Framework 4 to build git.exe): a program that starts git without a shell skips it, and PowerShell's unquoted & | < > reach git changed");
|
|
9044
|
+
} else if (shimReady()) add2("ok", "git guard shim ready");
|
|
8729
9045
|
else if (process.platform === "win32") add2("warn", "git guard: the ref and push guard is on, but the git shim needs the sh.exe of Git for Windows next to git (not found), so a worker's `git add`, `restore` or `clean` is not blocked; prefer --isolate and check the patch before apply");
|
|
8730
9046
|
else add2("warn", "git guard unavailable (git not on PATH)");
|
|
8731
9047
|
section2("Worker chain");
|
|
@@ -8737,7 +9053,8 @@ function doctor8(probe2) {
|
|
|
8737
9053
|
} catch (e) {
|
|
8738
9054
|
add2("fail", e.message);
|
|
8739
9055
|
}
|
|
8740
|
-
|
|
9056
|
+
const auto = chain.length > 1 && !effective().fallback.value.length;
|
|
9057
|
+
if (chain.length) add2("ok", `worker chain: ${chain.map(describeTarget).join(" \u2192 ")}${auto ? ' (fallbacks from "autoFallback": the other workers your config names)' : ""}`);
|
|
8741
9058
|
const rate2 = effective().audit.value;
|
|
8742
9059
|
if (rate2 > 0 && chain.length) {
|
|
8743
9060
|
const auditor = pickAuditor({ worker: chain[0], fallback: chain.slice(1) });
|
|
@@ -8856,12 +9173,12 @@ function liveProbe(target) {
|
|
|
8856
9173
|
} catch (e) {
|
|
8857
9174
|
return { level: "fail", message: e.message };
|
|
8858
9175
|
}
|
|
8859
|
-
const scratch = readsOwnSnapshot(backend) ?
|
|
9176
|
+
const scratch = readsOwnSnapshot(backend) ? fs43.mkdtempSync(path38.join(os13.tmpdir(), "pitroom-probe-")) : void 0;
|
|
8860
9177
|
const cwd = scratch ?? process.cwd();
|
|
8861
9178
|
try {
|
|
8862
9179
|
return probe(backend, target, cwd);
|
|
8863
9180
|
} finally {
|
|
8864
|
-
if (scratch)
|
|
9181
|
+
if (scratch) fs43.rmSync(scratch, { recursive: true, force: true });
|
|
8865
9182
|
}
|
|
8866
9183
|
}
|
|
8867
9184
|
function probe(backend, target, cwd) {
|
|
@@ -8875,7 +9192,7 @@ function probe(backend, target, cwd) {
|
|
|
8875
9192
|
title: "pitroom doctor probe"
|
|
8876
9193
|
});
|
|
8877
9194
|
const t0 = Date.now();
|
|
8878
|
-
const r =
|
|
9195
|
+
const r = spawnSync14(inv.command, inv.args, {
|
|
8879
9196
|
encoding: "utf8",
|
|
8880
9197
|
timeout: 3e5,
|
|
8881
9198
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -8906,20 +9223,20 @@ function skillChecks() {
|
|
|
8906
9223
|
else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
|
|
8907
9224
|
}
|
|
8908
9225
|
if (geminiExtensionInstalled()) {
|
|
8909
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
9226
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path38.sep}.agents${path38.sep}`) && i.names.length);
|
|
8910
9227
|
checks.push(
|
|
8911
9228
|
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)" }
|
|
8912
9229
|
);
|
|
8913
9230
|
}
|
|
8914
9231
|
if (viaPlugin) {
|
|
8915
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
9232
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path38.sep}.claude${path38.sep}`) && i.names.length);
|
|
8916
9233
|
checks.push(
|
|
8917
9234
|
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)" }
|
|
8918
9235
|
);
|
|
8919
9236
|
}
|
|
8920
9237
|
const launcher = launcherPath();
|
|
8921
|
-
if (
|
|
8922
|
-
const r =
|
|
9238
|
+
if (fs43.existsSync(launcher)) {
|
|
9239
|
+
const r = spawnSync14(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
|
|
8923
9240
|
checks.push(
|
|
8924
9241
|
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)}` }
|
|
8925
9242
|
);
|
|
@@ -8930,12 +9247,12 @@ function skillChecks() {
|
|
|
8930
9247
|
}
|
|
8931
9248
|
function geminiExtensionInstalled() {
|
|
8932
9249
|
const home3 = process.env.GEMINI_CLI_HOME ?? os13.homedir();
|
|
8933
|
-
return
|
|
9250
|
+
return fs43.existsSync(path38.join(home3, ".gemini", "extensions", "pitroom"));
|
|
8934
9251
|
}
|
|
8935
9252
|
function pluginInstalled() {
|
|
8936
9253
|
try {
|
|
8937
|
-
const f =
|
|
8938
|
-
const plugins = JSON.parse(
|
|
9254
|
+
const f = path38.join(os13.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
9255
|
+
const plugins = JSON.parse(fs43.readFileSync(f, "utf8")).plugins ?? {};
|
|
8939
9256
|
return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
|
|
8940
9257
|
} catch {
|
|
8941
9258
|
return false;
|
|
@@ -8952,24 +9269,24 @@ function superpowersActive() {
|
|
|
8952
9269
|
for (const p of openCodePlugins()) {
|
|
8953
9270
|
if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
|
|
8954
9271
|
}
|
|
8955
|
-
for (const base2 of [
|
|
8956
|
-
const dir =
|
|
8957
|
-
if (
|
|
9272
|
+
for (const base2 of [path38.join(os13.homedir(), ".agents", "skills"), path38.join(os13.homedir(), ".claude", "skills")]) {
|
|
9273
|
+
const dir = path38.join(base2, "using-superpowers");
|
|
9274
|
+
if (fs43.existsSync(path38.join(dir, "SKILL.md"))) found.push(dir);
|
|
8958
9275
|
}
|
|
8959
9276
|
return found;
|
|
8960
9277
|
}
|
|
8961
9278
|
function claudePlugins() {
|
|
8962
9279
|
try {
|
|
8963
|
-
const f =
|
|
8964
|
-
return Object.keys(JSON.parse(
|
|
9280
|
+
const f = path38.join(os13.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
9281
|
+
return Object.keys(JSON.parse(fs43.readFileSync(f, "utf8")).plugins ?? {});
|
|
8965
9282
|
} catch {
|
|
8966
9283
|
return [];
|
|
8967
9284
|
}
|
|
8968
9285
|
}
|
|
8969
9286
|
function claudePluginEnabled(key) {
|
|
8970
9287
|
try {
|
|
8971
|
-
const f =
|
|
8972
|
-
return JSON.parse(
|
|
9288
|
+
const f = path38.join(os13.homedir(), ".claude", "settings.json");
|
|
9289
|
+
return JSON.parse(fs43.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
|
|
8973
9290
|
} catch {
|
|
8974
9291
|
return true;
|
|
8975
9292
|
}
|
|
@@ -8978,7 +9295,7 @@ function codexPlugins() {
|
|
|
8978
9295
|
const plugins = /* @__PURE__ */ new Map();
|
|
8979
9296
|
let text;
|
|
8980
9297
|
try {
|
|
8981
|
-
text =
|
|
9298
|
+
text = fs43.readFileSync(path38.join(process.env.CODEX_HOME ?? path38.join(os13.homedir(), ".codex"), "config.toml"), "utf8");
|
|
8982
9299
|
} catch {
|
|
8983
9300
|
return plugins;
|
|
8984
9301
|
}
|
|
@@ -8996,11 +9313,11 @@ function codexPlugins() {
|
|
|
8996
9313
|
return plugins;
|
|
8997
9314
|
}
|
|
8998
9315
|
function openCodePlugins() {
|
|
8999
|
-
const dir =
|
|
9316
|
+
const dir = path38.join(process.env.XDG_CONFIG_HOME ?? path38.join(os13.homedir(), ".config"), "opencode");
|
|
9000
9317
|
const plugins = [];
|
|
9001
9318
|
for (const name of ["opencode.json", "opencode.jsonc"]) {
|
|
9002
9319
|
try {
|
|
9003
|
-
const text =
|
|
9320
|
+
const text = fs43.readFileSync(path38.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
|
|
9004
9321
|
const list2 = JSON.parse(text).plugin;
|
|
9005
9322
|
if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
|
|
9006
9323
|
} catch {
|
|
@@ -9008,7 +9325,7 @@ function openCodePlugins() {
|
|
|
9008
9325
|
}
|
|
9009
9326
|
for (const folder of ["plugin", "plugins"]) {
|
|
9010
9327
|
try {
|
|
9011
|
-
for (const f of
|
|
9328
|
+
for (const f of fs43.readdirSync(path38.join(dir, folder))) plugins.push(path38.join(dir, folder, f));
|
|
9012
9329
|
} catch {
|
|
9013
9330
|
}
|
|
9014
9331
|
}
|
|
@@ -9061,7 +9378,7 @@ Usage
|
|
|
9061
9378
|
pitroom statusline [--then CMD] status-bar line: running workers, savings this week (after CMD's)
|
|
9062
9379
|
pitroom hook-card PostToolUse hook: a card after each Bash \`pitroom\` command
|
|
9063
9380
|
pitroom hook-start SessionStart hook: introduces Pitroom to the agent (Codex; Claude Code uses its plugin)
|
|
9064
|
-
pitroom prices [--refresh] [--json] the price catalog (config "priceFeed") and the price each worker in use gets
|
|
9381
|
+
pitroom prices [WORKER\u2026] [--refresh] [--json] the price catalog (config "priceFeed"), the primary's price and the price each worker in use (or named) gets
|
|
9065
9382
|
pitroom cooldown [--clear] models that said "rate limited" and are skipped for a while (a quota used up);
|
|
9066
9383
|
--clear tries them again
|
|
9067
9384
|
pitroom mcp [-d DIR] [--http [--port N]]
|
|
@@ -9135,7 +9452,7 @@ var COMMANDS = {
|
|
|
9135
9452
|
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)");
|
|
9136
9453
|
const dir = flag(p, "dir");
|
|
9137
9454
|
if (dir !== void 0) {
|
|
9138
|
-
if (!
|
|
9455
|
+
if (!fs44.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) throw new UserError(`-d ${dir}: no such directory`);
|
|
9139
9456
|
process.chdir(dir);
|
|
9140
9457
|
process.env.PWD = process.cwd();
|
|
9141
9458
|
}
|