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/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: "the Antigravity worker is in beta (checked against agy 1.3.5). agy has no read-only mode for headless runs, so its read runs always read a snapshot of the project; prefer -i for changes"
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|usage limit|limit reached|quota|capacity/i.test(message)) return "rate-limited";
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|usage limit|quota|overloaded|\b50[23]\b|capacity/i.test(message)) return "rate-limited";
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 (/\b429\b|rate.?limit|too many requests|quota|capacity|overloaded|\b529\b/i.test(message)) return "rate-limited";
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
- return (byName().get(path12.basename(wanted)) ?? []).filter((f) => f.split(path12.sep).join("/").endsWith(`/${wanted}`) || path12.basename(f) === wanted).slice(0, 20);
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 indexFiles(roots) {
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 >= MAX_INDEXED) return;
2453
+ if (count >= cap) return;
2433
2454
  if (e.isDirectory()) {
2434
- if (!SKIP_DIRS2.has(e.name)) walk(path12.join(dir, e.name));
2435
- } else if (e.isFile()) {
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 cooldownMs(message) {
2509
- const told = retryAfterMs(message);
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
- return /daily|per.?day|per day|exhausted your/i.test(message) ? DAILY_MS : OVERLOAD_MS;
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
- return [JSON.parse(l)];
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) => `@echo off\r
3229
- set "MSYS2_ARG_CONV_EXCL=*"\r
3230
- "${sh}" "%~dp0git" %*\r
3231
- exit /b %errorlevel%\r
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,