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/lib.mjs
CHANGED
|
@@ -542,7 +542,7 @@ function denyCommandRules() {
|
|
|
542
542
|
"command(eval)"
|
|
543
543
|
];
|
|
544
544
|
}
|
|
545
|
-
function workerSettings(workspace, workerHome3, reads = []) {
|
|
545
|
+
function workerSettings(workspace, workerHome3, reads = [], writes = false) {
|
|
546
546
|
const real = (p) => {
|
|
547
547
|
try {
|
|
548
548
|
return fs6.realpathSync.native(p);
|
|
@@ -566,6 +566,8 @@ function workerSettings(workspace, workerHome3, reads = []) {
|
|
|
566
566
|
allow: [
|
|
567
567
|
"command(*)",
|
|
568
568
|
...both(path5.join(dataDir(workerHome3), "brain")).map((p) => `write_file(${rulePath(p)})`),
|
|
569
|
+
// agy 1.4 asks for edits in the workspace too, --mode accept-edits or not, and a headless run cannot ask
|
|
570
|
+
...writes ? both(workspace).map((p) => `write_file(${rulePath(p)})`) : [],
|
|
569
571
|
...toolchains().flatMap(both).map((p) => `read_file(${rulePath(p)})`),
|
|
570
572
|
// what the task names outside the workspace: a headless agy cannot ask, and would end the turn unanswered
|
|
571
573
|
...reads.map((r) => r.path).flatMap(both).map((p) => `read_file(${rulePath(p)})`)
|
|
@@ -691,12 +693,12 @@ function baseSettings() {
|
|
|
691
693
|
}
|
|
692
694
|
var rulePath = (p) => p.replace(/^[A-Za-z]:(?=[\\/])/, "").replace(/\\/g, "/");
|
|
693
695
|
var HOMES_KEPT_DAYS = 14;
|
|
694
|
-
function workerHome(workspace, reads = []) {
|
|
696
|
+
function workerHome(workspace, reads = [], writes = false) {
|
|
695
697
|
const root = path5.join(home(), "antigravity-home");
|
|
696
698
|
const id2 = crypto3.createHash("sha256").update(path5.resolve(workspace)).digest("hex").slice(0, 16);
|
|
697
699
|
const dir = path5.join(root, id2);
|
|
698
700
|
fs6.mkdirSync(dataDir(dir), { recursive: true });
|
|
699
|
-
writeIfChanged(path5.join(dataDir(dir), "settings.json"), `${JSON.stringify(workerSettings(workspace, dir, reads), null, 2)}
|
|
701
|
+
writeIfChanged(path5.join(dataDir(dir), "settings.json"), `${JSON.stringify(workerSettings(workspace, dir, reads, writes), null, 2)}
|
|
700
702
|
`);
|
|
701
703
|
if (process.platform === "darwin") {
|
|
702
704
|
fs6.mkdirSync(path5.join(dir, "Library"), { recursive: true });
|
|
@@ -753,13 +755,13 @@ function invocation(req) {
|
|
|
753
755
|
if (model) args.push("--model", model);
|
|
754
756
|
if (effort) args.push("--effort", effort);
|
|
755
757
|
const { command, prefix } = resolveCommand(binary());
|
|
756
|
-
const workerDir = workerHome(req.cwd, outsideReads(prompt, req.cwd, req.project?.root));
|
|
758
|
+
const workerDir = workerHome(req.cwd, outsideReads(prompt, req.cwd, req.project?.root), req.mode !== "read");
|
|
757
759
|
const env = { HOME: workerDir };
|
|
758
760
|
if (process.platform === "win32") env.USERPROFILE = workerDir;
|
|
759
761
|
return { command, args: [...prefix, ...args], env };
|
|
760
762
|
}
|
|
761
763
|
function classify(message) {
|
|
762
|
-
if (/RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|capacity/i.test(message)) return "rate-limited";
|
|
764
|
+
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";
|
|
763
765
|
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)) {
|
|
764
766
|
return "auth";
|
|
765
767
|
}
|
|
@@ -810,9 +812,10 @@ function doctor({ models, hasFallback }) {
|
|
|
810
812
|
}
|
|
811
813
|
const checks = [
|
|
812
814
|
{ level: "ok", message: `Antigravity CLI ${version.out.trim().split("\n")[0]} at ${binary()}` },
|
|
813
|
-
|
|
815
|
+
// checked on macOS (agy 1.4.7, scripts/check-worker.mjs); Linux and Windows not run yet
|
|
816
|
+
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" } : {
|
|
814
817
|
level: "warn",
|
|
815
|
-
message:
|
|
818
|
+
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`
|
|
816
819
|
}
|
|
817
820
|
];
|
|
818
821
|
if (process.platform === "win32") {
|
|
@@ -997,7 +1000,7 @@ function classify2(message) {
|
|
|
997
1000
|
if (/authentication_failed|failed to authenticate|oauth|not logged in|\/login|invalid api key|\b40[13]\b|unauthori[sz]ed/i.test(message)) {
|
|
998
1001
|
return "auth";
|
|
999
1002
|
}
|
|
1000
|
-
if (/rate.?limit|too many requests|\b429\b|\b529\b|overloaded|
|
|
1003
|
+
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";
|
|
1001
1004
|
if (/model[^.]*(not found|does not exist|invalid|not available)|invalid model|not_found_error/i.test(message)) return "model-unavailable";
|
|
1002
1005
|
return "other";
|
|
1003
1006
|
}
|
|
@@ -1198,7 +1201,7 @@ function classify3(message) {
|
|
|
1198
1201
|
if (/not supported|model[^.]*(not found|does not exist|unknown|unavailable)|unsupported model|no such model/i.test(message)) {
|
|
1199
1202
|
return "model-unavailable";
|
|
1200
1203
|
}
|
|
1201
|
-
if (/rate.?limit|too many requests|\b429\b|
|
|
1204
|
+
if (/usage limit|session limit|rate.?limit|too many requests|\b429\b|quota|overloaded|\b50[23]\b|capacity/i.test(message)) return "rate-limited";
|
|
1202
1205
|
if (/unauthori[sz]ed|\b40[13]\b|not logged in|log ?in|api.?key|credits?\b|billing|subscription/i.test(message)) return "auth";
|
|
1203
1206
|
return "other";
|
|
1204
1207
|
}
|
|
@@ -1436,7 +1439,7 @@ function classify4(message) {
|
|
|
1436
1439
|
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)) {
|
|
1437
1440
|
return "auth";
|
|
1438
1441
|
}
|
|
1439
|
-
if (/RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|exhausted your capacity|capacity/i.test(message)) return "rate-limited";
|
|
1442
|
+
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";
|
|
1440
1443
|
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";
|
|
1441
1444
|
return "other";
|
|
1442
1445
|
}
|
|
@@ -1880,7 +1883,7 @@ function majorVersion(output) {
|
|
|
1880
1883
|
}
|
|
1881
1884
|
function classify5(message) {
|
|
1882
1885
|
if (/model not found|ModelNotFound|unknown model|no such model|deprecated/i.test(message)) return "model-unavailable";
|
|
1883
|
-
if (/rate.?limit|too many requests|\b429\b|quota|overloaded|\b50[23]\b|unavailable|capacity/i.test(message)) return "rate-limited";
|
|
1886
|
+
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";
|
|
1884
1887
|
if (/provider\.auth|FreeTierError|free tier|unauthori[sz]ed|\b40[13]\b|api.?key|insufficient|credits?\b|billing|payment/i.test(message)) {
|
|
1885
1888
|
return "auth";
|
|
1886
1889
|
}
|
|
@@ -2225,7 +2228,7 @@ function invocation7(req) {
|
|
|
2225
2228
|
}
|
|
2226
2229
|
function classify6(message) {
|
|
2227
2230
|
if (/\b40[13]\b|incorrect api key|invalid api key|unauthori[sz]ed|authenticat|not logged in|no auth/i.test(message)) return "auth";
|
|
2228
|
-
if (
|
|
2231
|
+
if (/usage limit|session limit|\b429\b|rate.?limit|too many requests|quota|capacity|overloaded|\b529\b/i.test(message)) return "rate-limited";
|
|
2229
2232
|
if (/model[^.]*(not found|does not exist|not available|not supported)|invalid model|model_not_found|\b404\b/i.test(message)) return "model-unavailable";
|
|
2230
2233
|
return "other";
|
|
2231
2234
|
}
|
|
@@ -2315,6 +2318,7 @@ var FILE_EXTENSIONS = new Set(
|
|
|
2315
2318
|
);
|
|
2316
2319
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
2317
2320
|
var MAX_INDEXED = 6e4;
|
|
2321
|
+
var MAX_SKIPPED = 3e5;
|
|
2318
2322
|
var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
|
|
2319
2323
|
var REF = new RegExp(
|
|
2320
2324
|
String.raw`(?<![\w/:.-])(\.{0,2}/?(?:[\w@.+-]+/)*(?:[\w@+-][\w@.+-]*\.[A-Za-z][A-Za-z0-9]{0,7}|${EXTENSIONLESS})):(\d+)(?:[-–](\d+))?`,
|
|
@@ -2373,13 +2377,17 @@ function verifyRefs(refs, dirs) {
|
|
|
2373
2377
|
return lineCache.get(file);
|
|
2374
2378
|
};
|
|
2375
2379
|
let index;
|
|
2380
|
+
let skipped;
|
|
2376
2381
|
const byName = () => index ??= indexFiles(roots);
|
|
2382
|
+
const inSkipped = () => skipped ??= indexFiles(roots, true);
|
|
2377
2383
|
const candidates = (ref) => {
|
|
2378
2384
|
const direct = resolve(ref.file, roots);
|
|
2379
2385
|
if (direct) return [direct];
|
|
2380
2386
|
if (path12.isAbsolute(ref.file)) return [];
|
|
2381
2387
|
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
2382
|
-
|
|
2388
|
+
const fits = (f) => path12.basename(f) === wanted || foldersInOrder(f.split(path12.sep).join("/"), wanted);
|
|
2389
|
+
const own = (byName().get(path12.basename(wanted)) ?? []).filter(fits);
|
|
2390
|
+
return (own.length || !isFileName(wanted) ? own : (inSkipped().get(path12.basename(wanted)) ?? []).filter(fits)).slice(0, 20);
|
|
2383
2391
|
};
|
|
2384
2392
|
const check = (file, ref) => {
|
|
2385
2393
|
const lines = load(file);
|
|
@@ -2418,10 +2426,23 @@ var isFileName = (name) => {
|
|
|
2418
2426
|
const base = path12.basename(name);
|
|
2419
2427
|
return !base.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base) || FILE_EXTENSIONS.has(base.split(".").pop().toLowerCase());
|
|
2420
2428
|
};
|
|
2421
|
-
function
|
|
2429
|
+
function foldersInOrder(file, wanted) {
|
|
2430
|
+
const want = wanted.split("/");
|
|
2431
|
+
const have = file.split("/");
|
|
2432
|
+
if (have.at(-1) !== want.at(-1)) return false;
|
|
2433
|
+
let i = have.length - 2;
|
|
2434
|
+
for (let j = want.length - 2; j >= 0; j--) {
|
|
2435
|
+
while (i >= 0 && have[i] !== want[j]) i--;
|
|
2436
|
+
if (i < 0) return false;
|
|
2437
|
+
i--;
|
|
2438
|
+
}
|
|
2439
|
+
return true;
|
|
2440
|
+
}
|
|
2441
|
+
function indexFiles(roots, skipped = false) {
|
|
2422
2442
|
const index = /* @__PURE__ */ new Map();
|
|
2443
|
+
const cap = skipped ? MAX_SKIPPED : MAX_INDEXED;
|
|
2423
2444
|
let count = 0;
|
|
2424
|
-
const walk = (dir) => {
|
|
2445
|
+
const walk = (dir, within) => {
|
|
2425
2446
|
let entries;
|
|
2426
2447
|
try {
|
|
2427
2448
|
entries = fs12.readdirSync(dir, { withFileTypes: true });
|
|
@@ -2429,10 +2450,12 @@ function indexFiles(roots) {
|
|
|
2429
2450
|
return;
|
|
2430
2451
|
}
|
|
2431
2452
|
for (const e of entries) {
|
|
2432
|
-
if (count >=
|
|
2453
|
+
if (count >= cap) return;
|
|
2433
2454
|
if (e.isDirectory()) {
|
|
2434
|
-
if (
|
|
2435
|
-
|
|
2455
|
+
if (e.name === ".git") continue;
|
|
2456
|
+
const skip = SKIP_DIRS2.has(e.name);
|
|
2457
|
+
if (skipped || !skip) walk(path12.join(dir, e.name), within || skip);
|
|
2458
|
+
} else if (e.isFile() && within === skipped) {
|
|
2436
2459
|
count++;
|
|
2437
2460
|
const list2 = index.get(e.name);
|
|
2438
2461
|
if (list2) list2.push(path12.join(dir, e.name));
|
|
@@ -2440,7 +2463,7 @@ function indexFiles(roots) {
|
|
|
2440
2463
|
}
|
|
2441
2464
|
}
|
|
2442
2465
|
};
|
|
2443
|
-
for (const r of roots) walk(r);
|
|
2466
|
+
for (const r of roots) walk(r, false);
|
|
2444
2467
|
return index;
|
|
2445
2468
|
}
|
|
2446
2469
|
function resolve(ref, roots) {
|
|
@@ -2493,6 +2516,7 @@ function fill(template, values) {
|
|
|
2493
2516
|
// src/core/cooldown.ts
|
|
2494
2517
|
var MAX_MS = 24 * 36e5;
|
|
2495
2518
|
var OVERLOAD_MS = 20 * 6e4;
|
|
2519
|
+
var USAGE_MS = 36e5;
|
|
2496
2520
|
var DAILY_MS = 4 * 36e5;
|
|
2497
2521
|
function retryAfterMs(message) {
|
|
2498
2522
|
const compact2 = /(?:retry|try again|resets?|available again)[^0-9]{0,24}((?:\d+(?:\.\d+)?\s*[dhms]\s*)+)/i.exec(message)?.[1];
|
|
@@ -2505,10 +2529,41 @@ function retryAfterMs(message) {
|
|
|
2505
2529
|
}
|
|
2506
2530
|
return ms > 0 ? ms : void 0;
|
|
2507
2531
|
}
|
|
2508
|
-
function
|
|
2509
|
-
const
|
|
2532
|
+
function wallClock(at, timeZone) {
|
|
2533
|
+
const p = Object.fromEntries(
|
|
2534
|
+
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)])
|
|
2535
|
+
);
|
|
2536
|
+
return Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);
|
|
2537
|
+
}
|
|
2538
|
+
function retryAtMs(message, now = Date.now()) {
|
|
2539
|
+
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);
|
|
2540
|
+
if (!m) return void 0;
|
|
2541
|
+
let hour = Number(m[1]);
|
|
2542
|
+
const minute = Number(m[2] ?? 0);
|
|
2543
|
+
const half = m[3]?.toLowerCase();
|
|
2544
|
+
if (minute > 59 || (half ? hour < 1 || hour > 12 : hour > 23)) return void 0;
|
|
2545
|
+
if (half === "p" && hour < 12) hour += 12;
|
|
2546
|
+
if (half === "a" && hour === 12) hour = 0;
|
|
2547
|
+
let zone = m[4];
|
|
2548
|
+
try {
|
|
2549
|
+
if (zone) new Intl.DateTimeFormat("en-US", { timeZone: zone });
|
|
2550
|
+
} catch {
|
|
2551
|
+
zone = void 0;
|
|
2552
|
+
}
|
|
2553
|
+
const wall = wallClock(now, zone);
|
|
2554
|
+
for (const day of [0, 1]) {
|
|
2555
|
+
const d = new Date(wall);
|
|
2556
|
+
const target = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + day, hour, minute);
|
|
2557
|
+
const at = target - (wallClock(target, zone) - target);
|
|
2558
|
+
if (at > now) return at - now;
|
|
2559
|
+
}
|
|
2560
|
+
return void 0;
|
|
2561
|
+
}
|
|
2562
|
+
function cooldownMs(message, now = Date.now()) {
|
|
2563
|
+
const told = retryAfterMs(message) ?? retryAtMs(message, now);
|
|
2510
2564
|
if (told !== void 0) return Math.min(Math.max(told, 6e4), MAX_MS);
|
|
2511
|
-
|
|
2565
|
+
if (/daily|per.?day|per day|exhausted your/i.test(message)) return DAILY_MS;
|
|
2566
|
+
return /usage limit|session limit|quota|FreeUsageLimit/i.test(message) ? USAGE_MS : OVERLOAD_MS;
|
|
2512
2567
|
}
|
|
2513
2568
|
|
|
2514
2569
|
// src/core/answers.ts
|
|
@@ -2569,7 +2624,8 @@ var SCHEMA = {
|
|
|
2569
2624
|
priceFeedUrl: "string",
|
|
2570
2625
|
priceFeedHours: "number",
|
|
2571
2626
|
budgetDaily: "number",
|
|
2572
|
-
rankWorkers: "boolean"
|
|
2627
|
+
rankWorkers: "boolean",
|
|
2628
|
+
autoFallback: "boolean"
|
|
2573
2629
|
};
|
|
2574
2630
|
function configPath() {
|
|
2575
2631
|
if (process.env.PITROOM_CONFIG) return path15.resolve(process.env.PITROOM_CONFIG);
|
|
@@ -2679,6 +2735,7 @@ function effective(flags = {}) {
|
|
|
2679
2735
|
priceFeedUrl: setting(void 0, e.PITROOM_PRICE_FEED_URL?.trim() || void 0, c.priceFeedUrl?.trim() || void 0, "https://models.dev/api.json"),
|
|
2680
2736
|
priceFeedHours: setting(void 0, positiveInt(e.PITROOM_PRICE_FEED_HOURS), positiveInt(c.priceFeedHours), 24),
|
|
2681
2737
|
rankWorkers: setting(void 0, flag01(e.PITROOM_RANK_WORKERS), c.rankWorkers, false),
|
|
2738
|
+
autoFallback: setting(void 0, flag01(e.PITROOM_AUTO_FALLBACK), c.autoFallback, false),
|
|
2682
2739
|
budgetDaily: setting(void 0, amount(e.PITROOM_BUDGET_DAILY), amount(c.budgetDaily), void 0)
|
|
2683
2740
|
};
|
|
2684
2741
|
}
|
|
@@ -2722,7 +2779,7 @@ function scoreOf(r) {
|
|
|
2722
2779
|
return r.audited >= MIN_AUDITS ? ok * (r.agreed / r.audited) : ok;
|
|
2723
2780
|
}
|
|
2724
2781
|
var pct = (x) => `${Math.round(x * 100)}%`;
|
|
2725
|
-
function rankChain(chain, recordOf) {
|
|
2782
|
+
function rankChain(chain, recordOf, work) {
|
|
2726
2783
|
const scored = chain.map((t, i) => ({ t, i, score: scoreOf(recordOf(t)) }));
|
|
2727
2784
|
const slots = scored.filter((s) => s.score !== void 0);
|
|
2728
2785
|
if (slots.length < 2) return { chain };
|
|
@@ -2733,7 +2790,7 @@ function rankChain(chain, recordOf) {
|
|
|
2733
2790
|
const by = (t) => scored.find((s) => s.t === t).score;
|
|
2734
2791
|
return {
|
|
2735
2792
|
chain: out,
|
|
2736
|
-
note: `ranked by record ("rankWorkers"): ${describeTarget(out[0])} (${pct(by(out[0]))}) goes before ${describeTarget(chain[0])} (${pct(by(chain[0]))})`
|
|
2793
|
+
note: `ranked by record${work ? ` on ${work}` : ""} ("rankWorkers"): ${describeTarget(out[0])} (${pct(by(out[0]))}) goes before ${describeTarget(chain[0])} (${pct(by(chain[0]))})`
|
|
2737
2794
|
};
|
|
2738
2795
|
}
|
|
2739
2796
|
|
|
@@ -2821,11 +2878,16 @@ function savedUsd(usage, returnedTokens, price = primaryPrice()) {
|
|
|
2821
2878
|
const readingTheReport = returnedTokens * price.input / 1e6;
|
|
2822
2879
|
return Math.max(0, costAt(usage, price) - (workerCostOf(usage) ?? 0) - readingTheReport);
|
|
2823
2880
|
}
|
|
2881
|
+
var SETTLING_MS = 10 * 6e4;
|
|
2824
2882
|
function readLedger(sinceMs) {
|
|
2825
2883
|
if (!fs16.existsSync(ledgerFile())) return [];
|
|
2884
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2826
2885
|
return fs16.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
|
|
2827
2886
|
try {
|
|
2828
|
-
|
|
2887
|
+
const e = JSON.parse(l);
|
|
2888
|
+
if (seen.has(e.id)) return [];
|
|
2889
|
+
seen.add(e.id);
|
|
2890
|
+
return [e];
|
|
2829
2891
|
} catch {
|
|
2830
2892
|
return [];
|
|
2831
2893
|
}
|
|
@@ -3119,6 +3181,7 @@ function summarize(rows) {
|
|
|
3119
3181
|
}
|
|
3120
3182
|
|
|
3121
3183
|
// src/vcs/guard.ts
|
|
3184
|
+
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
3122
3185
|
import fs19 from "node:fs";
|
|
3123
3186
|
import path18 from "node:path";
|
|
3124
3187
|
var VERSION = 2;
|
|
@@ -3199,6 +3262,15 @@ exec "$real" "$@"
|
|
|
3199
3262
|
function shimDir() {
|
|
3200
3263
|
return path18.join(home(), "shim", `v${VERSION}`);
|
|
3201
3264
|
}
|
|
3265
|
+
function scoopTarget(exe) {
|
|
3266
|
+
try {
|
|
3267
|
+
const text = fs19.readFileSync(exe.replace(/\.exe$/i, ".shim"), "utf8");
|
|
3268
|
+
const target = /^\s*path\s*=\s*"?([^"\r\n]+?)"?\s*$/m.exec(text)?.[1];
|
|
3269
|
+
return target && fs19.statSync(target).isFile() ? target : void 0;
|
|
3270
|
+
} catch {
|
|
3271
|
+
return void 0;
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3202
3274
|
function findRealGit(skip) {
|
|
3203
3275
|
const same = (a, b) => WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
3204
3276
|
for (const dir of (process.env.PATH ?? "").split(path18.delimiter).filter(Boolean)) {
|
|
@@ -3206,7 +3278,7 @@ function findRealGit(skip) {
|
|
|
3206
3278
|
const p = path18.join(dir, WINDOWS ? "git.exe" : "git");
|
|
3207
3279
|
try {
|
|
3208
3280
|
fs19.accessSync(p, fs19.constants.X_OK);
|
|
3209
|
-
if (fs19.statSync(p).isFile()) return p;
|
|
3281
|
+
if (fs19.statSync(p).isFile()) return WINDOWS && scoopTarget(p) || p;
|
|
3210
3282
|
} catch {
|
|
3211
3283
|
}
|
|
3212
3284
|
}
|
|
@@ -3225,11 +3297,85 @@ function findGitSh(realGit) {
|
|
|
3225
3297
|
}
|
|
3226
3298
|
return void 0;
|
|
3227
3299
|
}
|
|
3228
|
-
var cmdShim = (sh) =>
|
|
3229
|
-
|
|
3230
|
-
"
|
|
3231
|
-
|
|
3300
|
+
var cmdShim = (sh) => [
|
|
3301
|
+
"@echo off",
|
|
3302
|
+
"setlocal EnableExtensions DisableDelayedExpansion",
|
|
3303
|
+
'set "MSYS2_ARG_CONV_EXCL=*"',
|
|
3304
|
+
'set "PITROOM_SELF=%~f0"',
|
|
3305
|
+
'set q=^"',
|
|
3306
|
+
"setlocal EnableDelayedExpansion",
|
|
3307
|
+
'set "line=!cmdcmdline!"',
|
|
3308
|
+
'if not "!line: /d =!"=="!line!" goto plain',
|
|
3309
|
+
'if not "!line: /s =!"=="!line!" goto plain',
|
|
3310
|
+
'set "tail=!line:*/c ""%PITROOM_SELF%"=!"',
|
|
3311
|
+
'if "!tail!"=="!line!" set "tail=!line:*/c "%PITROOM_SELF%=!"',
|
|
3312
|
+
'if "!tail!"=="!line!" goto plain',
|
|
3313
|
+
'if "!tail:~-1!"=="!q!" set "tail=!tail:~0,-1!"',
|
|
3314
|
+
`"${sh}" "%~dp0git" !tail!`,
|
|
3315
|
+
"exit /b !errorlevel!",
|
|
3316
|
+
":plain",
|
|
3317
|
+
"endlocal",
|
|
3318
|
+
`"${sh}" "%~dp0git" %*`,
|
|
3319
|
+
"exit /b %errorlevel%",
|
|
3320
|
+
""
|
|
3321
|
+
].join("\r\n");
|
|
3322
|
+
var launcherSource = (sh) => `// pitroom git guard: git.exe launcher v${VERSION} (generated; edit src/vcs/guard.ts)
|
|
3323
|
+
using System;
|
|
3324
|
+
using System.Diagnostics;
|
|
3325
|
+
using System.IO;
|
|
3326
|
+
|
|
3327
|
+
static class PitroomGit {
|
|
3328
|
+
const string Sh = @"${sh.replace(/"/g, '""')}";
|
|
3329
|
+
|
|
3330
|
+
static int Main() {
|
|
3331
|
+
string line = Environment.CommandLine;
|
|
3332
|
+
// this program's own name, as Windows reads it: up to the closing quote when it starts with one, else to a space
|
|
3333
|
+
int i = 0;
|
|
3334
|
+
if (line.Length > 0 && line[0] == '"') { int q = line.IndexOf('"', 1); i = q < 0 ? line.Length : q + 1; }
|
|
3335
|
+
else { while (i < line.Length && line[i] != ' ' && line[i] != '\\t') i++; }
|
|
3336
|
+
string script = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "git");
|
|
3337
|
+
ProcessStartInfo start = new ProcessStartInfo(Sh, "\\"" + script + "\\"" + line.Substring(i));
|
|
3338
|
+
start.UseShellExecute = false;
|
|
3339
|
+
start.EnvironmentVariables["MSYS2_ARG_CONV_EXCL"] = "*";
|
|
3340
|
+
// Ctrl+C reaches git too: wait for it to end and pass its exit code on
|
|
3341
|
+
Console.CancelKeyPress += delegate (object o, ConsoleCancelEventArgs e) { e.Cancel = true; };
|
|
3342
|
+
using (Process git = Process.Start(start)) {
|
|
3343
|
+
git.WaitForExit();
|
|
3344
|
+
return git.ExitCode;
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3232
3348
|
`;
|
|
3349
|
+
function findCsc() {
|
|
3350
|
+
const windir = process.env.WINDIR ?? process.env.SystemRoot ?? "C:\\Windows";
|
|
3351
|
+
for (const fw of ["Framework64", "Framework"]) {
|
|
3352
|
+
const p = path18.join(windir, "Microsoft.NET", fw, "v4.0.30319", "csc.exe");
|
|
3353
|
+
if (fs19.existsSync(p)) return p;
|
|
3354
|
+
}
|
|
3355
|
+
return void 0;
|
|
3356
|
+
}
|
|
3357
|
+
function buildLauncher(dir, sh) {
|
|
3358
|
+
const source = launcherSource(sh);
|
|
3359
|
+
const src = path18.join(dir, "git-launcher.cs");
|
|
3360
|
+
const exe = path18.join(dir, "git.exe");
|
|
3361
|
+
if (fs19.existsSync(exe) && fs19.existsSync(src) && fs19.readFileSync(src, "utf8") === source) return;
|
|
3362
|
+
const csc = findCsc();
|
|
3363
|
+
if (!csc) return;
|
|
3364
|
+
const tmp = path18.join(dir, `git.${process.pid}.tmp.exe`);
|
|
3365
|
+
const tmpSrc = `${src}.${process.pid}.tmp`;
|
|
3366
|
+
try {
|
|
3367
|
+
fs19.mkdirSync(dir, { recursive: true });
|
|
3368
|
+
fs19.writeFileSync(tmpSrc, source);
|
|
3369
|
+
const r = spawnSync9(csc, ["/nologo", "/optimize+", "/target:exe", `/out:${tmp}`, tmpSrc], { encoding: "utf8", timeout: 6e4, windowsHide: true });
|
|
3370
|
+
if (r.status !== 0 || !fs19.existsSync(tmp)) return;
|
|
3371
|
+
renameOver(tmp, exe);
|
|
3372
|
+
renameOver(tmpSrc, src);
|
|
3373
|
+
} catch {
|
|
3374
|
+
} finally {
|
|
3375
|
+
fs19.rmSync(tmp, { force: true });
|
|
3376
|
+
fs19.rmSync(tmpSrc, { force: true });
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3233
3379
|
var HOOK_VERSION = 1;
|
|
3234
3380
|
var REF_HOOK = `#!/bin/sh
|
|
3235
3381
|
# pitroom git guard (layer 2, v${HOOK_VERSION}): refuse ref updates made by workers.
|
|
@@ -3287,6 +3433,7 @@ function guardEnv(env) {
|
|
|
3287
3433
|
sh = findGitSh(real);
|
|
3288
3434
|
if (!sh) return { ...env, ...quiet };
|
|
3289
3435
|
writeIfChanged2(path18.join(dir, "git.cmd"), cmdShim(sh));
|
|
3436
|
+
buildLauncher(dir, sh);
|
|
3290
3437
|
}
|
|
3291
3438
|
writeIfChanged2(path18.join(dir, "git"), SHIM);
|
|
3292
3439
|
const key = pathKey(env);
|
|
@@ -3297,6 +3444,29 @@ function shimReady() {
|
|
|
3297
3444
|
return Boolean(env[pathKey(env)]?.startsWith(shimDir()));
|
|
3298
3445
|
}
|
|
3299
3446
|
|
|
3447
|
+
// src/vcs/git.ts
|
|
3448
|
+
function numberDiff(diff) {
|
|
3449
|
+
let next = 0;
|
|
3450
|
+
let width = 0;
|
|
3451
|
+
let inHunk = false;
|
|
3452
|
+
return diff.split("\n").map((l) => {
|
|
3453
|
+
if (l.startsWith("diff --git")) {
|
|
3454
|
+
inHunk = false;
|
|
3455
|
+
return l;
|
|
3456
|
+
}
|
|
3457
|
+
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(l);
|
|
3458
|
+
if (hunk) {
|
|
3459
|
+
inHunk = true;
|
|
3460
|
+
next = Number(hunk[1]);
|
|
3461
|
+
width = String(next + Number(hunk[2] ?? 1)).length;
|
|
3462
|
+
return l;
|
|
3463
|
+
}
|
|
3464
|
+
if (!inHunk || !l || !"+- ".includes(l[0])) return l;
|
|
3465
|
+
if (l[0] === "-") return `-${" ".repeat(width)}| ${l.slice(1)}`;
|
|
3466
|
+
return `${l[0]}${String(next++).padStart(width)}| ${l.slice(1)}`;
|
|
3467
|
+
}).join("\n");
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3300
3470
|
// src/core/slots.ts
|
|
3301
3471
|
import crypto4 from "node:crypto";
|
|
3302
3472
|
import fs20 from "node:fs";
|
|
@@ -3431,6 +3601,7 @@ export {
|
|
|
3431
3601
|
brief,
|
|
3432
3602
|
card,
|
|
3433
3603
|
catalogPrice,
|
|
3604
|
+
cmdShim,
|
|
3434
3605
|
compact,
|
|
3435
3606
|
cooldownMs,
|
|
3436
3607
|
costAt,
|
|
@@ -3456,6 +3627,7 @@ export {
|
|
|
3456
3627
|
loadTemplate,
|
|
3457
3628
|
looksSecret,
|
|
3458
3629
|
newRunId,
|
|
3630
|
+
numberDiff,
|
|
3459
3631
|
parseAudit,
|
|
3460
3632
|
parsePlan,
|
|
3461
3633
|
parsePriceSpec,
|
|
@@ -3475,11 +3647,13 @@ export {
|
|
|
3475
3647
|
requestStop,
|
|
3476
3648
|
resolveRun,
|
|
3477
3649
|
retryAfterMs,
|
|
3650
|
+
retryAtMs,
|
|
3478
3651
|
retryCommands,
|
|
3479
3652
|
runDir,
|
|
3480
3653
|
runFile,
|
|
3481
3654
|
runsDir,
|
|
3482
3655
|
savedUsd,
|
|
3656
|
+
scoopTarget,
|
|
3483
3657
|
scoreAnswer,
|
|
3484
3658
|
scoreOf,
|
|
3485
3659
|
secretWarning,
|