@mutmutco/cli 4.0.18 → 4.1.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/main.cjs CHANGED
@@ -5680,8 +5680,8 @@ var program = new Command();
5680
5680
 
5681
5681
  // src/index.ts
5682
5682
  var import_promises8 = require("node:fs/promises");
5683
- var import_node_fs42 = require("node:fs");
5684
- var import_node_child_process19 = require("node:child_process");
5683
+ var import_node_fs43 = require("node:fs");
5684
+ var import_node_child_process20 = require("node:child_process");
5685
5685
  init_cli_shared();
5686
5686
 
5687
5687
  // src/issue-surface.ts
@@ -5696,6 +5696,10 @@ function surfaceLabel(value) {
5696
5696
  const v = value.trim();
5697
5697
  return v.toLowerCase().startsWith(SURFACE_PREFIX) ? v : `${SURFACE_PREFIX}${v}`;
5698
5698
  }
5699
+ function surfaceLabelsFromParsed(parsed) {
5700
+ if (!Array.isArray(parsed)) return void 0;
5701
+ return parsed.map((entry) => typeof entry === "string" ? entry : entry.name).filter((n) => typeof n === "string" && n.toLowerCase().startsWith(SURFACE_PREFIX));
5702
+ }
5699
5703
  async function readRepoSurfaceLabels(repo, deps = {}) {
5700
5704
  const run = deps.run ?? execFileP2;
5701
5705
  try {
@@ -5704,9 +5708,17 @@ async function readRepoSurfaceLabels(repo, deps = {}) {
5704
5708
  ["label", "list", "--repo", repo, "--limit", "1000", "--json", "name"],
5705
5709
  { timeout: SURFACE_READ_TIMEOUT_MS }
5706
5710
  );
5707
- const parsed = JSON.parse(stdout);
5708
- if (!Array.isArray(parsed)) return void 0;
5709
- return parsed.map((l) => l.name).filter((n) => typeof n === "string" && n.toLowerCase().startsWith(SURFACE_PREFIX));
5711
+ const labels = surfaceLabelsFromParsed(JSON.parse(stdout));
5712
+ if (labels !== void 0) return labels;
5713
+ } catch {
5714
+ }
5715
+ try {
5716
+ const { stdout } = await run(
5717
+ "gh",
5718
+ ["api", "--paginate", `repos/${repo}/labels`],
5719
+ { timeout: SURFACE_READ_TIMEOUT_MS }
5720
+ );
5721
+ return surfaceLabelsFromParsed(JSON.parse(stdout));
5710
5722
  } catch {
5711
5723
  return void 0;
5712
5724
  }
@@ -5740,7 +5752,7 @@ async function checkSurfaceRequirement(input, deps = {}) {
5740
5752
  warn: `warning: could not read ${input.repo}'s labels, so the surface-label requirement was not checked; if that board enforces one surface:* label per open issue, add one with \`mmi-cli oracle issue edit <n> --add-label surface:<value>\``
5741
5753
  };
5742
5754
  }
5743
- if (known.length === 0) return { enforcing: false };
5755
+ if (known.length === 0) return { enforcing: false, taxonomyAbsent: true };
5744
5756
  if (labelsCarrySurface(input.labels)) return { enforcing: true };
5745
5757
  const command = input.command ?? "issue create";
5746
5758
  const where = input.rowLabel ? `${input.rowLabel}: ` : "";
@@ -5774,7 +5786,11 @@ function conflictingSurfaceInputs(surfaceFlag, labels) {
5774
5786
  }
5775
5787
  async function surfaceLabelApplies(repo, deps = {}) {
5776
5788
  const known = await (deps.read ?? readRepoSurfaceLabels)(repo, { run: deps.run });
5777
- return (known?.length ?? 0) > 0;
5789
+ if (known === void 0) return true;
5790
+ return known.length > 0;
5791
+ }
5792
+ function shouldWithdrawSurfaceFlag(result) {
5793
+ return result.taxonomyAbsent === true;
5778
5794
  }
5779
5795
 
5780
5796
  // src/index.ts
@@ -6677,7 +6693,8 @@ var COMMAND_LADDER_WRITES = [
6677
6693
  { gh: "gh pr merge", replacement: "mmi-cli devops pr merge (or mmi-cli devops pr land)" }
6678
6694
  ];
6679
6695
  var COMMAND_LADDER_READS = [
6680
- { gh: "gh issue view <N>", replacement: "mmi-cli oracle issue view <N>" },
6696
+ // #5483: `show` aliases `view` for board-verb callers; advertise the canonical read plus the alias.
6697
+ { gh: "gh issue view <N>", replacement: "mmi-cli oracle issue view <N> (alias: show)" },
6681
6698
  { gh: "gh pr view <N>", replacement: "mmi-cli devops pr view <N>" },
6682
6699
  { gh: "gh pr checks <N>", replacement: "mmi-cli devops pr checks-wait <N>" }
6683
6700
  ];
@@ -6705,7 +6722,7 @@ function commandLadderHint() {
6705
6722
  }
6706
6723
 
6707
6724
  // src/index.ts
6708
- var import_node_path39 = require("node:path");
6725
+ var import_node_path40 = require("node:path");
6709
6726
 
6710
6727
  // src/merge-ci-policy.ts
6711
6728
  function resolveMergeCiPolicy(input) {
@@ -7609,7 +7626,7 @@ function extractForwardRefs(markdown) {
7609
7626
  });
7610
7627
  return refs;
7611
7628
  }
7612
- function checkPins(root, readFile6, docs2) {
7629
+ function checkPins(root, readFile7, docs2) {
7613
7630
  const findings = [];
7614
7631
  for (const [doc, markdown] of Object.entries(docs2)) {
7615
7632
  for (const pin of extractPins(markdown)) {
@@ -7617,7 +7634,7 @@ function checkPins(root, readFile6, docs2) {
7617
7634
  findings.push({ kind: "malformed-pin", doc, line: pin.line, detail: pin.text });
7618
7635
  continue;
7619
7636
  }
7620
- const source = readFile6((0, import_node_path11.join)(root, pin.file));
7637
+ const source = readFile7((0, import_node_path11.join)(root, pin.file));
7621
7638
  if (source == null) {
7622
7639
  findings.push({ kind: "missing-test", doc, line: pin.line, detail: pin.file });
7623
7640
  continue;
@@ -7831,7 +7848,7 @@ function defaultTrackedFirstSegments(root, firstSegments, exec = import_node_chi
7831
7848
  }
7832
7849
  }
7833
7850
  function runDocRefs(root, deps = {}) {
7834
- const readFile6 = deps.readFile ?? readFileOrNull;
7851
+ const readFile7 = deps.readFile ?? readFileOrNull;
7835
7852
  const exists = deps.exists ?? import_node_fs13.existsSync;
7836
7853
  const listDocs = deps.listDocs ?? defaultListDocs;
7837
7854
  const isIgnored = deps.isIgnored ?? ((paths) => defaultIsIgnored(root, paths));
@@ -7840,11 +7857,11 @@ function runDocRefs(root, deps = {}) {
7840
7857
  const walked = listDocs(root);
7841
7858
  const ignoredDocs = walked.length ? isIgnored(walked) : /* @__PURE__ */ new Set();
7842
7859
  const docs2 = Object.fromEntries(
7843
- walked.filter((rel) => !ignoredDocs.has(rel)).map((rel) => [rel, readFile6((0, import_node_path11.join)(root, rel))]).filter(([, body]) => body != null)
7860
+ walked.filter((rel) => !ignoredDocs.has(rel)).map((rel) => [rel, readFile7((0, import_node_path11.join)(root, rel))]).filter(([, body]) => body != null)
7844
7861
  );
7845
7862
  const refResult = checkRefs(root, { exists, isIgnored, trackedFirstSegments }, docs2);
7846
7863
  const findings = [
7847
- ...checkPins(root, readFile6, docs2).findings,
7864
+ ...checkPins(root, readFile7, docs2).findings,
7848
7865
  ...refResult.findings
7849
7866
  ];
7850
7867
  if (commandPaths == null) {
@@ -7867,11 +7884,11 @@ function runDocRefs(root, deps = {}) {
7867
7884
 
7868
7885
  // src/docs-index-command.ts
7869
7886
  var DOCS_INDEX_PATH = "docs/index.md";
7887
+ var GENERATED_HEADER = "<!-- Generated by `mmi-cli oracle docs index --write`. Do not edit by hand. -->";
7870
7888
  function isRoutableDocsPath(relPath) {
7871
7889
  const normalized = relPath.replace(/\\/g, "/");
7872
7890
  return normalized !== "index.md" && normalized.split("/")[0]?.toLowerCase() !== "archive";
7873
7891
  }
7874
- var GENERATED_HEADER = "<!-- Generated by `mmi-cli oracle docs index --write`. Do not edit by hand. -->";
7875
7892
  function plainInline(text) {
7876
7893
  return text.replace(/\r?\n/g, " ").replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/`/g, "").replace(/\*+/g, "").replace(/~~/g, "").replace(/__([^_]+)__/g, "$1").replace(/(^|[\s(])_([^_]+)_($|[\s).,!?:;])/g, "$1$2$3").replace(/^>\s*/, "").replace(/\\([\\`*_\[\]{}()#+.!|>-])/g, "$1").replace(/\s+/g, " ").trim();
7877
7894
  }
@@ -8580,22 +8597,22 @@ function labelsToPrune(orgLabelNames) {
8580
8597
  const org = new Set(orgLabelNames);
8581
8598
  return GITHUB_DEFAULT_LABELS.filter((name) => !org.has(name));
8582
8599
  }
8583
- function resolveSeedContent(seed, vars, readFile6) {
8584
- if (seed.source === "self") return readFile6(seed.target);
8600
+ function resolveSeedContent(seed, vars, readFile7) {
8601
+ if (seed.source === "self") return readFile7(seed.target);
8585
8602
  if (seed.source.startsWith("seed:")) {
8586
- const tmpl = readFile6(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
8603
+ const tmpl = readFile7(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
8587
8604
  return tmpl == null ? null : renderSeed(tmpl, vars);
8588
8605
  }
8589
8606
  return null;
8590
8607
  }
8591
- function resolveSeedWriteContent(seed, vars, readFile6, remoteContent) {
8608
+ function resolveSeedWriteContent(seed, vars, readFile7, remoteContent) {
8592
8609
  if (!seed.managedBlock) {
8593
- return { ok: true, content: resolveSeedContent(seed, vars, readFile6), managed: seed.source === "managed-block" };
8610
+ return { ok: true, content: resolveSeedContent(seed, vars, readFile7), managed: seed.source === "managed-block" };
8594
8611
  }
8595
- const base = remoteContent ?? resolveSeedContent(seed, vars, readFile6);
8612
+ const base = remoteContent ?? resolveSeedContent(seed, vars, readFile7);
8596
8613
  if (base == null) return { ok: true, content: null, managed: true };
8597
8614
  const blockSeed = { ...seed, source: seed.managedBlock.source, managedBlock: void 0 };
8598
- const desired = resolveSeedContent(blockSeed, vars, readFile6);
8615
+ const desired = resolveSeedContent(blockSeed, vars, readFile7);
8599
8616
  if (desired == null) return { ok: true, content: null, managed: true };
8600
8617
  const result = upsertManagedSeedBlock(base, desired, seed.managedBlock.begin, seed.managedBlock.end);
8601
8618
  return result.ok ? { ok: true, content: result.content, managed: true } : { ok: false, reason: result.reason, managed: true };
@@ -9318,6 +9335,7 @@ init_github_client();
9318
9335
 
9319
9336
  // ../infra/registry-endpoints.mjs
9320
9337
  var PROJECTS_LIST_PATH = "/projects/list";
9338
+ var DEPLOY_PORT_COLLISIONS_PATH = "/projects/deploy-port-collisions";
9321
9339
  var ORG_CONFIG_PATH = "/org/config";
9322
9340
  var PROJECTS_ENVELOPE_KEY = "projects";
9323
9341
  var REGISTRY_FETCH_TIMEOUT_MS = 8e3;
@@ -10318,6 +10336,188 @@ function isPriorityFieldConfigured(cfg) {
10318
10336
 
10319
10337
  // src/gh-create.ts
10320
10338
  init_cli_shared();
10339
+ init_github_client();
10340
+
10341
+ // src/github-quota.ts
10342
+ init_github_client();
10343
+ var RATE_LIMIT_WAIT_CAP_MS = 12e4;
10344
+ var RateLimitedError = class extends Error {
10345
+ receipt;
10346
+ constructor(receipt) {
10347
+ super(receipt.message);
10348
+ this.name = "RateLimitedError";
10349
+ this.receipt = receipt;
10350
+ }
10351
+ };
10352
+ function isRateLimitedError(e) {
10353
+ return e instanceof RateLimitedError;
10354
+ }
10355
+ function isRateLimitText(text) {
10356
+ return /API rate limit already exceeded|rate limit already exceeded|RATE_LIMITED|secondary rate|abuse detection|HTTP\/?\d*(?:\.\d)?\s+429\b|\b429\b.*rate limit/i.test(text) || /rate limit/i.test(text) && !/not rate.?limit/i.test(text);
10357
+ }
10358
+ function isRateLimitRefusal(e) {
10359
+ if (e instanceof GitHubApiError && e.rateLimited) return true;
10360
+ if (e instanceof GitHubApiError && e.status === 403) {
10361
+ return /rate limit|abuse detection|secondary rate/i.test(e.message);
10362
+ }
10363
+ if (e instanceof RateLimitedError) return true;
10364
+ const text = e instanceof Error ? `${e.message}
10365
+ ${e.stderr ?? ""}` : String(e ?? "");
10366
+ return isRateLimitText(text);
10367
+ }
10368
+ function resetEpochFromError(e) {
10369
+ if (e instanceof GitHubApiError && typeof e.rateLimitReset === "number") return e.rateLimitReset;
10370
+ if (e instanceof RateLimitedError && typeof e.receipt.resetEpochSeconds === "number") {
10371
+ return e.receipt.resetEpochSeconds;
10372
+ }
10373
+ return void 0;
10374
+ }
10375
+ function rateLimitWaitMs(resetEpochSeconds, nowMs, capMs = RATE_LIMIT_WAIT_CAP_MS) {
10376
+ if (typeof resetEpochSeconds !== "number" || !Number.isFinite(resetEpochSeconds)) return void 0;
10377
+ const wait = resetEpochSeconds * 1e3 - nowMs;
10378
+ if (wait <= 0) return 0;
10379
+ if (wait > capMs) return void 0;
10380
+ return wait;
10381
+ }
10382
+ function rateLimitedReceipt(opts) {
10383
+ const resetEpochSeconds = opts.resetEpochSeconds;
10384
+ const resetAt = typeof resetEpochSeconds === "number" ? new Date(resetEpochSeconds * 1e3).toISOString() : void 0;
10385
+ const note = rateLimitResetNote(resetEpochSeconds, opts.nowMs ?? Date.now());
10386
+ const context = opts.context ?? "GitHub API";
10387
+ const remaining = opts.remaining && (opts.remaining.graphql !== void 0 || opts.remaining.core !== void 0) ? opts.remaining : void 0;
10388
+ return {
10389
+ status: "rate_limited",
10390
+ ...resetEpochSeconds !== void 0 ? { resetEpochSeconds } : {},
10391
+ ...resetAt ? { resetAt } : {},
10392
+ message: opts.message ?? `${context}: ${note}`,
10393
+ retryCommand: opts.retryCommand,
10394
+ ...remaining ? { remaining } : {}
10395
+ };
10396
+ }
10397
+ async function runWithRateLimitBackoff(operation, opts) {
10398
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
10399
+ const now = opts.now ?? Date.now;
10400
+ const log = opts.log ?? ((message) => console.warn(message));
10401
+ const capMs = opts.capMs ?? RATE_LIMIT_WAIT_CAP_MS;
10402
+ const tryOnce = async () => {
10403
+ try {
10404
+ return await operation();
10405
+ } catch (e) {
10406
+ if (!isRateLimitRefusal(e) || !opts.fallback) throw e;
10407
+ try {
10408
+ return await opts.fallback();
10409
+ } catch (e2) {
10410
+ if (isRateLimitRefusal(e2)) throw e2;
10411
+ throw e2;
10412
+ }
10413
+ }
10414
+ };
10415
+ try {
10416
+ return await tryOnce();
10417
+ } catch (e) {
10418
+ if (!isRateLimitRefusal(e)) throw e;
10419
+ const reset = resetEpochFromError(e);
10420
+ const waitMs = rateLimitWaitMs(reset, now(), capMs);
10421
+ if (waitMs === void 0) {
10422
+ throw new RateLimitedError(rateLimitedReceipt({
10423
+ resetEpochSeconds: reset,
10424
+ retryCommand: opts.retryCommand,
10425
+ context: opts.context,
10426
+ nowMs: now()
10427
+ }));
10428
+ }
10429
+ if (waitMs > 0) {
10430
+ log(`${opts.context}: rate-limited \u2014 waiting ${Math.round(waitMs / 1e3)}s for pool reset`);
10431
+ await sleep2(waitMs);
10432
+ }
10433
+ try {
10434
+ return await tryOnce();
10435
+ } catch (e2) {
10436
+ if (!isRateLimitRefusal(e2)) throw e2;
10437
+ throw new RateLimitedError(rateLimitedReceipt({
10438
+ resetEpochSeconds: resetEpochFromError(e2) ?? reset,
10439
+ retryCommand: opts.retryCommand,
10440
+ context: opts.context,
10441
+ nowMs: now()
10442
+ }));
10443
+ }
10444
+ }
10445
+ }
10446
+
10447
+ // src/repo-resolve.ts
10448
+ init_cli_shared();
10449
+
10450
+ // src/registry-degrade.ts
10451
+ function registryDegradeError(error) {
10452
+ return new Error(
10453
+ `Hub registry read failed (${error}) \u2014 board coords could not be discovered. This is NOT necessarily the registry: a local DNS/network fault reaches this path as a timeout too. Check in order \u2014 (1) resolve the Hub API host (fails in seconds and rules out the local-network class), (2) if it resolves, retry: a Lambda cold start clears shortly, (3) if it still fails, check auth (a rejected credential is not a timeout).`
10454
+ );
10455
+ }
10456
+
10457
+ // src/config-discovery.ts
10458
+ function stripMutableBoardConfig(cfg) {
10459
+ const {
10460
+ projectOwner: _projectOwner,
10461
+ projectNumber: _projectNumber,
10462
+ projectId: _projectId,
10463
+ statusFieldId: _statusFieldId,
10464
+ statusOptions: _statusOptions,
10465
+ priorityFieldId: _priorityFieldId,
10466
+ priorityOptions: _priorityOptions,
10467
+ ...rest
10468
+ } = cfg;
10469
+ return rest;
10470
+ }
10471
+ function boardConfigFromProject(meta, floor = {}) {
10472
+ return {
10473
+ ...stripMutableBoardConfig(floor),
10474
+ projectOwner: meta.projectOwner,
10475
+ projectNumber: meta.projectNumber,
10476
+ projectId: meta.projectId,
10477
+ statusFieldId: meta.statusFieldId,
10478
+ statusOptions: meta.statusOptions,
10479
+ priorityFieldId: meta.priorityFieldId,
10480
+ priorityOptions: meta.priorityOptions
10481
+ };
10482
+ }
10483
+
10484
+ // src/repo-resolve.ts
10485
+ function slugOf(repoOrSlug) {
10486
+ return (repoOrSlug.includes("/") ? repoOrSlug.split("/").pop() : repoOrSlug).toLowerCase();
10487
+ }
10488
+ function repoFromRemoteUrl(remoteUrl) {
10489
+ const m = remoteUrl.trim().match(/^(?:[a-z][a-z0-9+.-]*:\/\/)?(?:[^@\s/]+@)?github\.com[:/]([^/\s:]+)\/([^/\s]+?)(?:\.git)?\/?$/i);
10490
+ return m ? `${m[1]}/${m[2]}` : void 0;
10491
+ }
10492
+ function repoFromSelector(selector) {
10493
+ const trimmed = selector.trim();
10494
+ const url = trimmed.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/(?:issues|pull)\/\d+$/i);
10495
+ if (url) return url[1];
10496
+ const qualified = trimmed.match(/^([^/\s#]+\/[^/\s#]+)#\d+$/);
10497
+ return qualified?.[1];
10498
+ }
10499
+ async function resolveRepo(repo) {
10500
+ if (repo) return repo;
10501
+ const fromOrigin = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"])) ?? repoFromRemoteUrl(originUrlFromGitConfig() ?? "");
10502
+ if (fromOrigin) return fromOrigin;
10503
+ try {
10504
+ const { stdout } = await execFileP2("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], { timeout: 5e3 });
10505
+ return stdout.trim() || void 0;
10506
+ } catch {
10507
+ return void 0;
10508
+ }
10509
+ }
10510
+ async function loadConfigForBoardSelector(selector, repoOption) {
10511
+ const targetRepo3 = repoFromSelector(selector) ?? repoOption;
10512
+ const floor = await loadConfig();
10513
+ if (!floor.sagaApiUrl) return stripMutableBoardConfig(floor);
10514
+ const slug = targetRepo3 ? slugOf(targetRepo3) : await repoSlug();
10515
+ const read = await fetchProjectBySlugChecked(slug, registryClientDeps(floor));
10516
+ if (!read.ok) throw registryDegradeError(read.error);
10517
+ return read.project ? boardConfigFromProject(read.project, floor) : stripMutableBoardConfig(floor);
10518
+ }
10519
+
10520
+ // src/gh-create.ts
10321
10521
  var ISSUE_TYPES = ["bug", "feature", "task"];
10322
10522
  var GH_MUTATION_TIMEOUT_MS = 12e4;
10323
10523
  function timeoutKillNote(err, timeoutMs) {
@@ -10440,6 +10640,101 @@ function parseExistingPr(stderr) {
10440
10640
  }
10441
10641
  var GH_CREATE_UPSTREAM_RETRIES = 3;
10442
10642
  var GH_CREATE_RETRY_BACKOFF_MS = [2e3, 5e3];
10643
+ function isGhCreateRateLimited(result) {
10644
+ return result.status === "rate_limited";
10645
+ }
10646
+ var isGhRateLimitError = isRateLimitText;
10647
+ function flagValue(args, flag) {
10648
+ const i = args.indexOf(flag);
10649
+ if (i === -1 || i + 1 >= args.length) return void 0;
10650
+ return args[i + 1];
10651
+ }
10652
+ function buildPrCreateRetryCommand(args) {
10653
+ const parts = ["mmi-cli", "devops", "pr", "create"];
10654
+ for (const flag of ["--repo", "--base", "--head", "--title"]) {
10655
+ const v = flagValue(args, flag);
10656
+ if (v !== void 0) parts.push(flag, shellQuote(v));
10657
+ }
10658
+ const bodyFile = flagValue(args, "--body-file");
10659
+ if (bodyFile !== void 0) parts.push("--body-file", shellQuote(bodyFile));
10660
+ else {
10661
+ const body = flagValue(args, "--body");
10662
+ if (body !== void 0) parts.push("--body", shellQuote(body));
10663
+ }
10664
+ if (args.includes("--draft")) parts.push("--draft");
10665
+ return parts.join(" ");
10666
+ }
10667
+ function collectLabelFlags(args) {
10668
+ const labels = [];
10669
+ for (let i = 0; i < args.length; i++) {
10670
+ if (args[i] === "--label" && i + 1 < args.length) {
10671
+ labels.push(args[i + 1]);
10672
+ i++;
10673
+ }
10674
+ }
10675
+ return labels;
10676
+ }
10677
+ function buildIssueCreateRetryCommand(args) {
10678
+ const parts = ["mmi-cli", "oracle", "issue", "create"];
10679
+ const repo = flagValue(args, "--repo");
10680
+ if (repo !== void 0) parts.push("--repo", shellQuote(repo));
10681
+ const labels = collectLabelFlags(args);
10682
+ const type = labels.find((l) => ISSUE_TYPES.includes(l));
10683
+ if (type) parts.push("--type", type);
10684
+ const title = flagValue(args, "--title");
10685
+ if (title !== void 0) parts.push("--title", shellQuote(title));
10686
+ const bodyFile = flagValue(args, "--body-file");
10687
+ if (bodyFile !== void 0) parts.push("--body-file", shellQuote(bodyFile));
10688
+ else {
10689
+ const body = flagValue(args, "--body");
10690
+ if (body !== void 0) parts.push("--body", shellQuote(body));
10691
+ }
10692
+ for (const label of labels) {
10693
+ if (label === type) continue;
10694
+ parts.push("--label", shellQuote(label));
10695
+ }
10696
+ return parts.join(" ");
10697
+ }
10698
+ function shellQuote(value) {
10699
+ if (/^[A-Za-z0-9_./:@+=,-]+$/.test(value)) return value;
10700
+ return `'${value.replace(/'/g, `'\\''`)}'`;
10701
+ }
10702
+ function parseRateLimitPools(stdout) {
10703
+ try {
10704
+ const resources = JSON.parse(stdout).resources;
10705
+ const out = {};
10706
+ if (typeof resources?.graphql?.remaining === "number" && typeof resources.graphql.reset === "number") {
10707
+ out.graphql = { remaining: resources.graphql.remaining, reset: resources.graphql.reset };
10708
+ }
10709
+ if (typeof resources?.core?.remaining === "number" && typeof resources.core.reset === "number") {
10710
+ out.core = { remaining: resources.core.remaining, reset: resources.core.reset };
10711
+ }
10712
+ return out;
10713
+ } catch {
10714
+ return {};
10715
+ }
10716
+ }
10717
+ function rateLimitedResult(opts) {
10718
+ const remaining = opts.remaining ?? remainingFromPools(opts.pools);
10719
+ return rateLimitedReceipt({
10720
+ ...opts,
10721
+ context: opts.context ?? "pr create",
10722
+ message: opts.message,
10723
+ remaining
10724
+ });
10725
+ }
10726
+ function remainingFromPools(pools) {
10727
+ if (!pools) return void 0;
10728
+ const remaining = {};
10729
+ if (typeof pools.graphql?.remaining === "number") remaining.graphql = pools.graphql.remaining;
10730
+ if (typeof pools.core?.remaining === "number") remaining.core = pools.core.remaining;
10731
+ return remaining.graphql !== void 0 || remaining.core !== void 0 ? remaining : void 0;
10732
+ }
10733
+ function execErrorText(err) {
10734
+ if (typeof err !== "object" || err === null) return String(err ?? "");
10735
+ const e = err;
10736
+ return [e.stderr, e.stdout, e.message].filter((s) => typeof s === "string" && s.trim()).join("\n");
10737
+ }
10443
10738
  function httpStatusCodes(stderr) {
10444
10739
  const out = [];
10445
10740
  for (const m of stderr.matchAll(/\bHTTP\/?\d*(?:\.\d)?\s+(\d{3})\b/g)) out.push(Number(m[1]));
@@ -10460,19 +10755,362 @@ function upstreamFaultMessage(verb, stderr) {
10460
10755
  const advice = mayRetryCreate(verb) ? "Retrying is the right action" : "The write may have completed server-side before the error \u2014 VERIFY before retrying, or a retry will duplicate it";
10461
10756
  return `gh ${verb} create failed: GitHub's API is failing (server-side 5xx) \u2014 this is NOT a problem with your diff or arguments. GitHub request ID: ${requestId ?? "unavailable"}. ${advice}; note githubstatus.com may still read green while this is happening.`;
10462
10757
  }
10758
+ async function resolveRepoForCreate(args, exec) {
10759
+ const fromFlag = flagValue(args, "--repo");
10760
+ if (fromFlag) return fromFlag;
10761
+ try {
10762
+ const { stdout } = await exec("git", ["remote", "get-url", "origin"], { timeout: 1e4 });
10763
+ const fromOrigin = repoFromRemoteUrl(stdout.trim());
10764
+ if (fromOrigin) return fromOrigin;
10765
+ } catch {
10766
+ }
10767
+ return repoFromRemoteUrl(originUrlFromGitConfig() ?? "") || void 0;
10768
+ }
10769
+ async function resolveHeadForCreate(args, exec) {
10770
+ const fromFlag = flagValue(args, "--head");
10771
+ if (fromFlag) return fromFlag;
10772
+ try {
10773
+ const { stdout } = await exec("git", ["rev-parse", "--abbrev-ref", "HEAD"], { timeout: 1e4 });
10774
+ const head = stdout.trim();
10775
+ return head && head !== "HEAD" ? head : void 0;
10776
+ } catch {
10777
+ return void 0;
10778
+ }
10779
+ }
10780
+ async function resolveBodyForCreate(args, read) {
10781
+ const bodyFile = flagValue(args, "--body-file");
10782
+ if (bodyFile !== void 0) return read(bodyFile, "utf8");
10783
+ return flagValue(args, "--body") ?? "";
10784
+ }
10785
+ async function defaultFindOpenPr(exec, input) {
10786
+ const owner = input.repo.split("/")[0];
10787
+ if (!owner) return void 0;
10788
+ const qs = new URLSearchParams({ state: "open", head: `${owner}:${input.head}` });
10789
+ if (input.base) qs.set("base", input.base);
10790
+ try {
10791
+ const { stdout } = await exec("gh", ["api", `repos/${input.repo}/pulls?${qs.toString()}`], { timeout: GH_MUTATION_TIMEOUT_MS });
10792
+ const rows = JSON.parse(stdout);
10793
+ const first = Array.isArray(rows) ? rows[0] : void 0;
10794
+ if (typeof first?.number === "number" && typeof first.html_url === "string") {
10795
+ return { number: first.number, url: first.html_url, existing: true };
10796
+ }
10797
+ } catch {
10798
+ }
10799
+ return void 0;
10800
+ }
10801
+ async function defaultRestCreatePr(exec, input, deps = {}) {
10802
+ const write = deps.write ?? import_promises.writeFile;
10803
+ const remove2 = deps.remove ?? import_promises.unlink;
10804
+ const ensureDir = deps.ensureDir ?? import_promises.mkdir;
10805
+ const dir = deps.dir ?? (0, import_node_os4.tmpdir)();
10806
+ const file = (0, import_node_path13.join)(dir, `mmi-rest-pr-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.json`);
10807
+ const payload = {
10808
+ title: input.title,
10809
+ head: input.head,
10810
+ body: input.body
10811
+ };
10812
+ if (input.base) payload.base = input.base;
10813
+ if (input.draft) payload.draft = true;
10814
+ await ensureDir((0, import_node_path13.dirname)(file), { recursive: true }).catch(() => {
10815
+ });
10816
+ await write(file, JSON.stringify(payload), "utf8");
10817
+ const args = [
10818
+ "api",
10819
+ "--method",
10820
+ "POST",
10821
+ `repos/${input.repo}/pulls`,
10822
+ "--input",
10823
+ file
10824
+ ];
10825
+ try {
10826
+ const { stdout } = await exec("gh", args, { timeout: GH_MUTATION_TIMEOUT_MS });
10827
+ const parsed = JSON.parse(stdout);
10828
+ if (typeof parsed.number === "number" && typeof parsed.html_url === "string") {
10829
+ return { number: parsed.number, url: parsed.html_url };
10830
+ }
10831
+ throw Object.assign(new Error(`REST pr create: unexpected payload: ${stdout.slice(0, 200)}`), { stderr: stdout });
10832
+ } catch (e) {
10833
+ const err = e;
10834
+ const text = execErrorText(err);
10835
+ const fromText = parseExistingPr(text);
10836
+ if (fromText) return { ...fromText, existing: true };
10837
+ if (/already exists/i.test(text)) {
10838
+ const found = await defaultFindOpenPr(exec, { repo: input.repo, head: input.head, base: input.base });
10839
+ if (found) return found;
10840
+ }
10841
+ throw e;
10842
+ } finally {
10843
+ try {
10844
+ await remove2(file);
10845
+ } catch {
10846
+ }
10847
+ }
10848
+ }
10849
+ async function defaultRestCreateIssue(exec, input) {
10850
+ const args = [
10851
+ "api",
10852
+ "--method",
10853
+ "POST",
10854
+ `repos/${input.repo}/issues`,
10855
+ "-f",
10856
+ `title=${input.title}`,
10857
+ "-f",
10858
+ `body=${input.body}`
10859
+ ];
10860
+ for (const label of input.labels ?? []) {
10861
+ args.push("-f", `labels[]=${label}`);
10862
+ }
10863
+ try {
10864
+ const { stdout } = await exec("gh", args, { timeout: GH_MUTATION_TIMEOUT_MS });
10865
+ const parsed = JSON.parse(stdout);
10866
+ if (typeof parsed.number === "number" && typeof parsed.html_url === "string") {
10867
+ return { number: parsed.number, url: parsed.html_url };
10868
+ }
10869
+ throw Object.assign(new Error(`REST issue create: unexpected payload: ${stdout.slice(0, 200)}`), { stderr: stdout });
10870
+ } catch (e) {
10871
+ const err = e;
10872
+ const text = `${err.stderr ?? ""}
10873
+ ${err.message ?? ""}
10874
+ ${err.stdout ?? ""}`;
10875
+ if (isRateLimitText(text)) {
10876
+ throw Object.assign(new Error(text.trim() || "REST issue create rate-limited"), {
10877
+ stderr: err.stderr ?? text
10878
+ });
10879
+ }
10880
+ throw e;
10881
+ }
10882
+ }
10883
+ async function defaultReadRateLimit(exec) {
10884
+ try {
10885
+ const { stdout } = await exec("gh", ["api", "rate_limit"], { timeout: 15e3 });
10886
+ return parseRateLimitPools(stdout);
10887
+ } catch {
10888
+ return {};
10889
+ }
10890
+ }
10891
+ async function createIssueViaRestFallback(args, swappedArgs, deps) {
10892
+ const retryCommand = buildIssueCreateRetryCommand(args);
10893
+ const repo = await resolveRepoForCreate(args, deps.exec);
10894
+ const title = flagValue(args, "--title");
10895
+ if (!repo || !title) {
10896
+ const pools2 = await deps.readRateLimit();
10897
+ const reset = pools2.graphql?.reset ?? pools2.core?.reset;
10898
+ return rateLimitedResult({
10899
+ resetEpochSeconds: reset,
10900
+ retryCommand,
10901
+ nowMs: deps.now(),
10902
+ pools: pools2,
10903
+ context: "issue create",
10904
+ message: `issue create: GraphQL rate-limited and cannot REST-fallback (need --repo/--title; have repo=${repo ?? "\u2205"} title=${title ? "set" : "\u2205"}) \u2014 ${rateLimitResetNote(reset, deps.now())}`
10905
+ });
10906
+ }
10907
+ const body = await resolveBodyForCreate(swappedArgs, deps.readFile);
10908
+ const labels = collectLabelFlags(args);
10909
+ const pools = await deps.readRateLimit();
10910
+ const coreRemaining = pools.core?.remaining;
10911
+ const coreReset = pools.core?.reset;
10912
+ const graphqlReset = pools.graphql?.reset;
10913
+ if (typeof coreRemaining === "number" && coreRemaining <= 0) {
10914
+ const reset = coreReset ?? graphqlReset;
10915
+ const waitMs = rateLimitWaitMs(reset, deps.now());
10916
+ if (waitMs === void 0) {
10917
+ return rateLimitedResult({
10918
+ resetEpochSeconds: reset,
10919
+ retryCommand,
10920
+ nowMs: deps.now(),
10921
+ pools,
10922
+ context: "issue create",
10923
+ message: `issue create: GraphQL and REST rate limits exhausted \u2014 ${rateLimitResetNote(reset, deps.now())}`
10924
+ });
10925
+ }
10926
+ }
10927
+ console.warn(`issue create: GraphQL rate-limited \u2014 creating via REST POST repos/${repo}/issues (#5489)`);
10928
+ try {
10929
+ return await runWithRateLimitBackoff(
10930
+ () => deps.restCreateIssue({ repo, title, body, labels }),
10931
+ {
10932
+ context: "issue create",
10933
+ retryCommand,
10934
+ sleep: deps.sleep,
10935
+ now: deps.now,
10936
+ log: (message) => console.warn(message)
10937
+ }
10938
+ );
10939
+ } catch (e) {
10940
+ if (isRateLimitedError(e)) {
10941
+ const after = await deps.readRateLimit();
10942
+ return {
10943
+ ...e.receipt,
10944
+ remaining: e.receipt.remaining ?? remainingFromPools(after)
10945
+ };
10946
+ }
10947
+ throw e;
10948
+ }
10949
+ }
10950
+ async function createPrViaRestFallback(args, swappedArgs, deps, knownPools) {
10951
+ const retryCommand = buildPrCreateRetryCommand(args);
10952
+ const repo = await resolveRepoForCreate(args, deps.exec);
10953
+ const head = await resolveHeadForCreate(args, deps.exec);
10954
+ const base = flagValue(args, "--base");
10955
+ const title = flagValue(args, "--title");
10956
+ if (!repo || !head || !title) {
10957
+ const pools2 = knownPools ?? await deps.readRateLimit();
10958
+ const reset = pools2.graphql?.reset ?? pools2.core?.reset;
10959
+ return rateLimitedResult({
10960
+ resetEpochSeconds: reset,
10961
+ retryCommand,
10962
+ nowMs: deps.now(),
10963
+ pools: pools2,
10964
+ message: `pr create: GraphQL rate-limited and cannot REST-fallback (need --repo/--head/--title; have repo=${repo ?? "\u2205"} head=${head ?? "\u2205"} title=${title ? "set" : "\u2205"}) \u2014 ${rateLimitResetNote(reset, deps.now())}`
10965
+ });
10966
+ }
10967
+ const existing = await deps.findOpenPr({ repo, head, base });
10968
+ if (existing) {
10969
+ console.warn(`pr create: GraphQL rate-limited \u2014 reusing existing open PR #${existing.number} for ${head} (#5451/#5487)`);
10970
+ return existing;
10971
+ }
10972
+ const pools = knownPools ?? await deps.readRateLimit();
10973
+ const coreRemaining = pools.core?.remaining;
10974
+ const graphqlReset = pools.graphql?.reset;
10975
+ const coreReset = pools.core?.reset;
10976
+ if (typeof coreRemaining === "number" && coreRemaining <= 0) {
10977
+ const reset = coreReset ?? graphqlReset;
10978
+ const waitMs = rateLimitWaitMs(reset, deps.now());
10979
+ if (waitMs === void 0) {
10980
+ return rateLimitedResult({
10981
+ resetEpochSeconds: reset,
10982
+ retryCommand,
10983
+ nowMs: deps.now(),
10984
+ pools,
10985
+ message: `pr create: GraphQL and REST rate limits exhausted \u2014 ${rateLimitResetNote(reset, deps.now())}`
10986
+ });
10987
+ }
10988
+ if (waitMs > 0) {
10989
+ console.warn(`pr create: REST pool exhausted \u2014 waiting ${Math.round(waitMs / 1e3)}s for reset (#5451)`);
10990
+ await deps.sleep(waitMs);
10991
+ }
10992
+ } else if (typeof pools.graphql?.remaining === "number" && pools.graphql.remaining <= 0) {
10993
+ const waitMs = typeof coreRemaining === "number" && coreRemaining > 0 ? void 0 : rateLimitWaitMs(graphqlReset, deps.now());
10994
+ if (waitMs && waitMs > 0) {
10995
+ console.warn(`pr create: GraphQL exhausted \u2014 waiting ${Math.round(waitMs / 1e3)}s before REST fallback (#5451)`);
10996
+ await deps.sleep(waitMs);
10997
+ }
10998
+ }
10999
+ const body = await resolveBodyForCreate(swappedArgs, deps.readFile);
11000
+ console.warn(`pr create: GraphQL rate-limited \u2014 creating via REST POST repos/${repo}/pulls (#5451/#5487)`);
11001
+ try {
11002
+ return await deps.restCreatePr({
11003
+ repo,
11004
+ title,
11005
+ body,
11006
+ head,
11007
+ base,
11008
+ draft: args.includes("--draft")
11009
+ });
11010
+ } catch (e) {
11011
+ const err = e;
11012
+ const text = execErrorText(err);
11013
+ if (isGhRateLimitError(text) || /HTTP\/?\d*(?:\.\d)?\s+403\b.*rate limit/i.test(text)) {
11014
+ const after = await deps.readRateLimit();
11015
+ const reset = after.core?.reset ?? after.graphql?.reset ?? err.rateLimitReset ?? coreReset ?? graphqlReset;
11016
+ const waitMs = rateLimitWaitMs(reset, deps.now());
11017
+ if (waitMs !== void 0) {
11018
+ if (waitMs > 0) {
11019
+ console.warn(`pr create: REST also rate-limited \u2014 waiting ${Math.round(waitMs / 1e3)}s for reset (#5451)`);
11020
+ await deps.sleep(waitMs);
11021
+ }
11022
+ try {
11023
+ return await deps.restCreatePr({
11024
+ repo,
11025
+ title,
11026
+ body,
11027
+ head,
11028
+ base,
11029
+ draft: args.includes("--draft")
11030
+ });
11031
+ } catch (e2) {
11032
+ const text2 = execErrorText(e2);
11033
+ if (isGhRateLimitError(text2)) {
11034
+ const after2 = await deps.readRateLimit();
11035
+ return rateLimitedResult({
11036
+ resetEpochSeconds: after2.core?.reset ?? after2.graphql?.reset ?? reset,
11037
+ retryCommand,
11038
+ nowMs: deps.now(),
11039
+ pools: after2,
11040
+ message: `pr create: GraphQL and REST rate limits exhausted \u2014 ${rateLimitResetNote(after2.core?.reset ?? after2.graphql?.reset ?? reset, deps.now())}`
11041
+ });
11042
+ }
11043
+ throw e2;
11044
+ }
11045
+ }
11046
+ return rateLimitedResult({
11047
+ resetEpochSeconds: reset,
11048
+ retryCommand,
11049
+ nowMs: deps.now(),
11050
+ pools: after,
11051
+ message: `pr create: GraphQL and REST rate limits exhausted \u2014 ${rateLimitResetNote(reset, deps.now())}`
11052
+ });
11053
+ }
11054
+ throw e;
11055
+ }
11056
+ }
10463
11057
  async function ghCreate(args, deps = {}) {
10464
11058
  const exec = deps.exec ?? execFileP2;
10465
11059
  const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
11060
+ const now = deps.now ?? Date.now;
11061
+ const read = deps.readFile ?? import_promises.readFile;
11062
+ const restCreatePr = deps.restCreatePr ?? ((input) => defaultRestCreatePr(exec, input));
11063
+ const restCreateIssue = deps.restCreateIssue ?? ((input) => defaultRestCreateIssue(exec, input));
11064
+ const findOpenPr = deps.findOpenPr ?? ((input) => defaultFindOpenPr(exec, input));
11065
+ const readRateLimit = deps.readRateLimit ?? (() => defaultReadRateLimit(exec));
10466
11066
  const swapped = await bodyArgsViaFile(args);
11067
+ const restDeps = {
11068
+ exec,
11069
+ sleep: sleep2,
11070
+ now,
11071
+ readFile: read,
11072
+ restCreatePr,
11073
+ findOpenPr,
11074
+ readRateLimit
11075
+ };
11076
+ const viaRest = async (pools) => {
11077
+ try {
11078
+ const result = await createPrViaRestFallback(args, swapped.args, restDeps, pools);
11079
+ await swapped.cleanup();
11080
+ return result;
11081
+ } catch (restErr) {
11082
+ await swapped.cleanup();
11083
+ const restText = execErrorText(restErr);
11084
+ if (isGhRateLimitError(restText)) {
11085
+ const pools2 = await readRateLimit();
11086
+ return rateLimitedResult({
11087
+ resetEpochSeconds: pools2.core?.reset ?? pools2.graphql?.reset,
11088
+ retryCommand: buildPrCreateRetryCommand(args),
11089
+ nowMs: now(),
11090
+ pools: pools2,
11091
+ message: `pr create: GraphQL and REST rate limits exhausted \u2014 ${rateLimitResetNote(pools2.core?.reset ?? pools2.graphql?.reset, now())}`
11092
+ });
11093
+ }
11094
+ return fail(`gh pr create failed (GraphQL rate-limited; REST fallback also failed): ${restText.trim()}`);
11095
+ }
11096
+ };
10467
11097
  try {
11098
+ if (args[0] === "pr") {
11099
+ const pools = await readRateLimit();
11100
+ if (typeof pools.graphql?.remaining === "number" && pools.graphql.remaining <= 0) {
11101
+ console.warn(
11102
+ typeof pools.core?.remaining === "number" && pools.core.remaining > 0 ? `pr create: GraphQL pool already exhausted (REST core remaining=${pools.core.remaining}) \u2014 skipping gh pr create, using REST (#5487)` : "pr create: GraphQL pool already exhausted \u2014 attempting REST fallback / typed receipt (#5487)"
11103
+ );
11104
+ return await viaRest(pools);
11105
+ }
11106
+ }
10468
11107
  for (let attempt = 1; attempt <= GH_CREATE_UPSTREAM_RETRIES; attempt++) {
10469
11108
  try {
10470
11109
  const { stdout } = await exec("gh", swapped.args, { timeout: GH_MUTATION_TIMEOUT_MS });
10471
11110
  return parseCreatedUrl(stdout);
10472
11111
  } catch (e) {
10473
11112
  const err = e;
10474
- const errText = `${err.stderr ?? ""}
10475
- ${err.message ?? ""}`;
11113
+ const errText = execErrorText(err);
10476
11114
  const faultText = (err.stderr ?? "").trim() ? err.stderr : err.message ?? "";
10477
11115
  const existing = args[0] === "pr" ? parseExistingPr(errText) : void 0;
10478
11116
  if (existing) {
@@ -10480,6 +11118,38 @@ ${err.message ?? ""}`;
10480
11118
  console.warn(`${args[0]} create: a PR already exists for this branch \u2014 #${existing.number} (nothing to do)`);
10481
11119
  return { ...existing, existing: true };
10482
11120
  }
11121
+ if (args[0] === "pr" && isGhRateLimitError(errText)) {
11122
+ return await viaRest();
11123
+ }
11124
+ if (args[0] === "issue" && isGhRateLimitError(errText)) {
11125
+ try {
11126
+ const result = await createIssueViaRestFallback(args, swapped.args, {
11127
+ exec,
11128
+ sleep: sleep2,
11129
+ now,
11130
+ readFile: read,
11131
+ restCreateIssue,
11132
+ readRateLimit
11133
+ });
11134
+ await swapped.cleanup();
11135
+ return result;
11136
+ } catch (restErr) {
11137
+ await swapped.cleanup();
11138
+ const restText = execErrorText(restErr);
11139
+ if (isGhRateLimitError(restText)) {
11140
+ const pools = await readRateLimit();
11141
+ return rateLimitedResult({
11142
+ resetEpochSeconds: pools.core?.reset ?? pools.graphql?.reset,
11143
+ retryCommand: buildIssueCreateRetryCommand(args),
11144
+ nowMs: now(),
11145
+ pools,
11146
+ context: "issue create",
11147
+ message: `issue create: GraphQL and REST rate limits exhausted \u2014 ${rateLimitResetNote(pools.core?.reset ?? pools.graphql?.reset, now())}`
11148
+ });
11149
+ }
11150
+ return fail(`gh issue create failed (GraphQL rate-limited; REST fallback also failed): ${restText.trim()}`);
11151
+ }
11152
+ }
10483
11153
  const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
10484
11154
  if (isUpstreamGitHubFault(faultText) && mayRetryCreate(args[0]) && attempt < GH_CREATE_UPSTREAM_RETRIES) {
10485
11155
  const waitMs = GH_CREATE_RETRY_BACKOFF_MS[attempt - 1];
@@ -10488,8 +11158,21 @@ ${err.message ?? ""}`;
10488
11158
  continue;
10489
11159
  }
10490
11160
  await swapped.cleanup();
11161
+ if ((args[0] === "pr" || args[0] === "issue") && isGhRateLimitError(errText)) {
11162
+ const pools = await readRateLimit();
11163
+ const context = args[0] === "issue" ? "issue create" : "pr create";
11164
+ const retryCommand = args[0] === "issue" ? buildIssueCreateRetryCommand(args) : buildPrCreateRetryCommand(args);
11165
+ return rateLimitedResult({
11166
+ resetEpochSeconds: pools.graphql?.reset ?? pools.core?.reset,
11167
+ retryCommand,
11168
+ nowMs: now(),
11169
+ pools,
11170
+ context,
11171
+ message: `${context}: GraphQL rate-limited \u2014 ${rateLimitResetNote(pools.graphql?.reset ?? pools.core?.reset, now())}`
11172
+ });
11173
+ }
10491
11174
  if (isUpstreamGitHubFault(faultText)) return fail(upstreamFaultMessage(args[0], faultText));
10492
- return fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
11175
+ return fail(`gh ${args[0]} create failed: ${(err.stderr || err.stdout || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
10493
11176
  }
10494
11177
  }
10495
11178
  throw new Error("ghCreate: retry loop exhausted without a verdict");
@@ -10497,6 +11180,14 @@ ${err.message ?? ""}`;
10497
11180
  await swapped.cleanup();
10498
11181
  }
10499
11182
  }
11183
+ function requireGhCreateOk(result, context = "pr create") {
11184
+ if (isGhCreateRateLimited(result)) {
11185
+ return fail(
11186
+ `${context}: rate_limited \u2014 ${result.message}. retry: ${result.retryCommand}` + (result.resetAt ? ` (resets at ${result.resetAt})` : "")
11187
+ );
11188
+ }
11189
+ return result;
11190
+ }
10500
11191
 
10501
11192
  // src/secrets.ts
10502
11193
  var OWNER2 = "mutmutco";
@@ -12176,10 +12867,10 @@ var rollout_plan_default = {
12176
12867
  note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
12177
12868
  },
12178
12869
  baseline: {
12179
- version: "4.0.18",
12180
- tag: "v4.0.18",
12181
- commit: "3d17c6647468",
12182
- npm: "@mutmutco/cli@4.0.18"
12870
+ version: "4.1.0",
12871
+ tag: "v4.1.0",
12872
+ commit: "4080226297a9",
12873
+ npm: "@mutmutco/cli@4.1.0"
12183
12874
  },
12184
12875
  exitCriterion: "fleet-n-of-n",
12185
12876
  hubOnlyShortcut: "forbidden",
@@ -12196,14 +12887,14 @@ var rollout_plan_default = {
12196
12887
  repo: "mutmutco/mmi-hub",
12197
12888
  role: "canary",
12198
12889
  schedule: "train",
12199
- v3Target: "v4.0.18"
12890
+ v3Target: "v4.1.0"
12200
12891
  }
12201
12892
  ],
12202
12893
  rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
12203
12894
  rollback: {
12204
12895
  independent: true,
12205
- mechanism: "npm dist-tag latest -> 4.0.18 and redeploy the Hub Lambda from tag v4.0.18 (3d17c6647468); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
12206
- v3Target: "v4.0.18 (@mutmutco/cli@4.0.18, tag commit 3d17c6647468 \u2014 last known-good release carrying the repo-index v4-only contract)"
12896
+ mechanism: "npm dist-tag latest -> 4.1.0 and redeploy the Hub Lambda from tag v4.1.0 (4080226297a9); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
12897
+ v3Target: "v4.1.0 (@mutmutco/cli@4.1.0, tag commit 4080226297a9 \u2014 last known-good release carrying the repo-index v4-only contract)"
12207
12898
  }
12208
12899
  },
12209
12900
  {
@@ -12687,79 +13378,6 @@ function parentLinkFields(result, error) {
12687
13378
  return {};
12688
13379
  }
12689
13380
 
12690
- // src/config-discovery.ts
12691
- function stripMutableBoardConfig(cfg) {
12692
- const {
12693
- projectOwner: _projectOwner,
12694
- projectNumber: _projectNumber,
12695
- projectId: _projectId,
12696
- statusFieldId: _statusFieldId,
12697
- statusOptions: _statusOptions,
12698
- priorityFieldId: _priorityFieldId,
12699
- priorityOptions: _priorityOptions,
12700
- ...rest
12701
- } = cfg;
12702
- return rest;
12703
- }
12704
- function boardConfigFromProject(meta, floor = {}) {
12705
- return {
12706
- ...stripMutableBoardConfig(floor),
12707
- projectOwner: meta.projectOwner,
12708
- projectNumber: meta.projectNumber,
12709
- projectId: meta.projectId,
12710
- statusFieldId: meta.statusFieldId,
12711
- statusOptions: meta.statusOptions,
12712
- priorityFieldId: meta.priorityFieldId,
12713
- priorityOptions: meta.priorityOptions
12714
- };
12715
- }
12716
-
12717
- // src/repo-resolve.ts
12718
- init_cli_shared();
12719
-
12720
- // src/registry-degrade.ts
12721
- function registryDegradeError(error) {
12722
- return new Error(
12723
- `Hub registry read failed (${error}) \u2014 board coords could not be discovered. This is NOT necessarily the registry: a local DNS/network fault reaches this path as a timeout too. Check in order \u2014 (1) resolve the Hub API host (fails in seconds and rules out the local-network class), (2) if it resolves, retry: a Lambda cold start clears shortly, (3) if it still fails, check auth (a rejected credential is not a timeout).`
12724
- );
12725
- }
12726
-
12727
- // src/repo-resolve.ts
12728
- function slugOf(repoOrSlug) {
12729
- return (repoOrSlug.includes("/") ? repoOrSlug.split("/").pop() : repoOrSlug).toLowerCase();
12730
- }
12731
- function repoFromRemoteUrl(remoteUrl) {
12732
- const m = remoteUrl.trim().match(/^(?:[a-z][a-z0-9+.-]*:\/\/)?(?:[^@\s/]+@)?github\.com[:/]([^/\s:]+)\/([^/\s]+?)(?:\.git)?\/?$/i);
12733
- return m ? `${m[1]}/${m[2]}` : void 0;
12734
- }
12735
- function repoFromSelector(selector) {
12736
- const trimmed = selector.trim();
12737
- const url = trimmed.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/(?:issues|pull)\/\d+$/i);
12738
- if (url) return url[1];
12739
- const qualified = trimmed.match(/^([^/\s#]+\/[^/\s#]+)#\d+$/);
12740
- return qualified?.[1];
12741
- }
12742
- async function resolveRepo(repo) {
12743
- if (repo) return repo;
12744
- const fromOrigin = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"])) ?? repoFromRemoteUrl(originUrlFromGitConfig() ?? "");
12745
- if (fromOrigin) return fromOrigin;
12746
- try {
12747
- const { stdout } = await execFileP2("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], { timeout: 5e3 });
12748
- return stdout.trim() || void 0;
12749
- } catch {
12750
- return void 0;
12751
- }
12752
- }
12753
- async function loadConfigForBoardSelector(selector, repoOption) {
12754
- const targetRepo3 = repoFromSelector(selector) ?? repoOption;
12755
- const floor = await loadConfig();
12756
- if (!floor.sagaApiUrl) return stripMutableBoardConfig(floor);
12757
- const slug = targetRepo3 ? slugOf(targetRepo3) : await repoSlug();
12758
- const read = await fetchProjectBySlugChecked(slug, registryClientDeps(floor));
12759
- if (!read.ok) throw registryDegradeError(read.error);
12760
- return read.project ? boardConfigFromProject(read.project, floor) : stripMutableBoardConfig(floor);
12761
- }
12762
-
12763
13381
  // src/issue-view-json.ts
12764
13382
  var DEFAULT_ISSUE_VIEW_FIELDS = "number,title,state,url,labels,author,assignees,milestone,body";
12765
13383
  function normalizeIssueViewJsonFields(tokens2) {
@@ -18764,10 +19382,69 @@ var import_node_path17 = require("node:path");
18764
19382
  var import_node_util6 = require("node:util");
18765
19383
  init_github_client();
18766
19384
 
19385
+ // src/board-snapshot.ts
19386
+ init_fetch_retry();
19387
+ init_client_version();
19388
+ var BOARD_SNAPSHOT_TIMEOUT_MS = 25e3;
19389
+ function isSnapshotShape(body) {
19390
+ const b = body;
19391
+ return Boolean(
19392
+ b && typeof b === "object" && typeof b.project?.id === "string" && typeof b.project?.title === "string" && typeof b.viewer === "string" && b.viewer.length > 0 && Array.isArray(b.nodes) && Array.isArray(b.writableRepos) && Array.isArray(b.unreadableRepos) && Array.isArray(b.pullRequests) && Array.isArray(b.warnings) && typeof b.partial === "boolean"
19393
+ );
19394
+ }
19395
+ async function fetchHubBoardSnapshot(request, deps) {
19396
+ if (!deps.baseUrl) return { state: "unavailable", reason: "no Hub API URL (set MMI_HUB_URL or use a current MMI CLI/plugin build)" };
19397
+ const token = await deps.token();
19398
+ if (!token) return { state: "unavailable", reason: "no Hub session token (run `gh auth login`)" };
19399
+ let res;
19400
+ try {
19401
+ res = await fetchWithRetry(
19402
+ deps.fetch ?? fetch,
19403
+ `${deps.baseUrl.replace(/\/$/, "")}/board/snapshot`,
19404
+ {
19405
+ method: "POST",
19406
+ headers: { ...clientVersionHeaders(), Authorization: `Bearer ${token}`, "content-type": "application/json" },
19407
+ body: JSON.stringify({
19408
+ owner: request.owner,
19409
+ number: request.number,
19410
+ ...request.statusFieldId ? { statusFieldId: request.statusFieldId } : {},
19411
+ activeOnly: request.activeOnly !== false,
19412
+ allowPartial: request.allowPartial === true,
19413
+ includePullRequests: request.includePullRequests !== false
19414
+ })
19415
+ },
19416
+ { attempts: 3, timeoutMs: deps.timeoutMs ?? BOARD_SNAPSHOT_TIMEOUT_MS, sleep: deps.retrySleep }
19417
+ );
19418
+ } catch (e) {
19419
+ return { state: "unavailable", reason: e.message };
19420
+ }
19421
+ if (res.status === 426) return { state: "refused", reason: upgradeRequiredError(res, await res.json().catch(() => null)) };
19422
+ if (res.status === 400 || res.status === 401 || res.status === 403) {
19423
+ const detail = (await res.json().catch(() => null))?.error;
19424
+ return { state: "refused", reason: detail ? `board snapshot HTTP ${res.status}: ${detail}` : `board snapshot HTTP ${res.status}` };
19425
+ }
19426
+ if (!res.ok) return { state: "unavailable", reason: `board snapshot HTTP ${res.status}` };
19427
+ const body = await res.json().catch(() => null);
19428
+ if (!isSnapshotShape(body)) return { state: "unavailable", reason: "malformed board snapshot response" };
19429
+ return { state: "ok", snapshot: body };
19430
+ }
19431
+
18767
19432
  // src/board-dependency.ts
18768
19433
  var DEPENDS_ON_LINE = /^\s*[-*]?\s*\*\*Depends on:\*\*\s*(.+)$/im;
18769
19434
  var ISSUE_REF = /([a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+)#(\d+)/g;
18770
19435
  var DEPENDENCY_FETCH_CONCURRENCY = 8;
19436
+ var ISSUE_BODY_GRAPHQL = `
19437
+ query($repoOwner: String!, $repoName: String!, $number: Int!) {
19438
+ repository(owner: $repoOwner, name: $repoName) {
19439
+ issue(number: $number) { body }
19440
+ }
19441
+ }`;
19442
+ var ISSUE_STATE_GRAPHQL = `
19443
+ query($repoOwner: String!, $repoName: String!, $number: Int!) {
19444
+ repository(owner: $repoOwner, name: $repoName) {
19445
+ issue(number: $number) { state }
19446
+ }
19447
+ }`;
18771
19448
  function parseDependsOnRefs(body) {
18772
19449
  const match = body.match(DEPENDS_ON_LINE);
18773
19450
  if (!match) return [];
@@ -18777,27 +19454,66 @@ function parseDependsOnRefs(body) {
18777
19454
  }
18778
19455
  return refs;
18779
19456
  }
18780
- async function issueState(client, repo, number) {
18781
- try {
19457
+ async function readIssueBodyWithQuota(client, repo, number, opts) {
19458
+ const [owner, name] = repo.split("/");
19459
+ if (!owner || !name) throw new Error(`invalid repository ref for issue-body read: ${repo}`);
19460
+ const viaRest = async () => {
19461
+ const data = await client.rest("GET", `repos/${repo}/issues/${number}`);
19462
+ return data?.body ?? "";
19463
+ };
19464
+ const viaGraphql = async () => {
19465
+ const data = await client.graphql(ISSUE_BODY_GRAPHQL, { repoOwner: owner, repoName: name, number });
19466
+ return data.repository?.issue?.body ?? "";
19467
+ };
19468
+ return runWithRateLimitBackoff(viaRest, {
19469
+ context: opts.context ?? `issue-body ${repo}#${number}`,
19470
+ retryCommand: opts.retryCommand,
19471
+ fallback: viaGraphql,
19472
+ sleep: opts.sleep,
19473
+ now: opts.now,
19474
+ log: opts.log,
19475
+ capMs: opts.capMs
19476
+ });
19477
+ }
19478
+ async function issueState(client, repo, number, quota) {
19479
+ const [owner, name] = repo.split("/");
19480
+ const viaRest = async () => {
18782
19481
  const data = await client.rest("GET", `repos/${repo}/issues/${number}`);
18783
19482
  const state = data?.state?.toLowerCase();
18784
19483
  if (state === "open") return "open";
18785
19484
  if (state === "closed") return "closed";
18786
19485
  return "unknown";
18787
- } catch {
19486
+ };
19487
+ const viaGraphql = async () => {
19488
+ if (!owner || !name) return "unknown";
19489
+ const data = await client.graphql(ISSUE_STATE_GRAPHQL, { repoOwner: owner, repoName: name, number });
19490
+ const state = data.repository?.issue?.state?.toLowerCase();
19491
+ if (state === "open") return "open";
19492
+ if (state === "closed") return "closed";
19493
+ return "unknown";
19494
+ };
19495
+ try {
19496
+ return await runWithRateLimitBackoff(viaRest, {
19497
+ context: `dependency state ${repo}#${number}`,
19498
+ retryCommand: quota.retryCommand,
19499
+ fallback: viaGraphql
19500
+ });
19501
+ } catch (e) {
19502
+ if (isRateLimitRefusal(e)) throw e;
18788
19503
  return "unknown";
18789
19504
  }
18790
19505
  }
18791
- async function dependencyBlocksClaim(client, body) {
19506
+ async function dependencyBlocksClaim(client, body, opts = {}) {
18792
19507
  const refs = parseDependsOnRefs(body);
18793
19508
  if (!refs.length) return { blocked: false, openDependencies: [] };
19509
+ const retryCommand = opts.retryCommand ?? "mmi-cli oracle board claim --check";
18794
19510
  const states = new Array(refs.length);
18795
19511
  let next = 0;
18796
19512
  const worker = async () => {
18797
19513
  while (next < refs.length) {
18798
19514
  const index = next++;
18799
19515
  const ref = refs[index];
18800
- states[index] = await issueState(client, ref.repo, ref.number);
19516
+ states[index] = await issueState(client, ref.repo, ref.number, { retryCommand });
18801
19517
  }
18802
19518
  };
18803
19519
  await Promise.all(Array.from({ length: Math.min(DEPENDENCY_FETCH_CONCURRENCY, refs.length) }, () => worker()));
@@ -18840,15 +19556,27 @@ async function filterDependencyBlockedClaimables(items, client, opts = {}) {
18840
19556
  continue;
18841
19557
  }
18842
19558
  }
18843
- const gate = await dependencyBlocksClaim(client, body);
18844
- if (gate.blocked) {
18845
- results[index] = {
18846
- bucket: "blocked",
18847
- item,
18848
- warnings: [`${item.ref} blocked on open ${gate.openDependencies.join(", ")}`]
18849
- };
18850
- } else {
18851
- results[index] = { bucket: "claimable", item, warnings: [] };
19559
+ try {
19560
+ const gate = await dependencyBlocksClaim(client, body);
19561
+ if (gate.blocked) {
19562
+ results[index] = {
19563
+ bucket: "blocked",
19564
+ item,
19565
+ warnings: [`${item.ref} blocked on open ${gate.openDependencies.join(", ")}`]
19566
+ };
19567
+ } else {
19568
+ results[index] = { bucket: "claimable", item, warnings: [] };
19569
+ }
19570
+ } catch (e) {
19571
+ if (isRateLimitRefusal(e)) {
19572
+ results[index] = {
19573
+ bucket: "claimable",
19574
+ item,
19575
+ warnings: [`dependency check skipped for ${item.ref} (rate-limited): ${e.message}`]
19576
+ };
19577
+ continue;
19578
+ }
19579
+ throw e;
18852
19580
  }
18853
19581
  }
18854
19582
  };
@@ -19553,10 +20281,64 @@ function renderBoardSource() {
19553
20281
  async function readBoard(options, deps = {}) {
19554
20282
  const cfg = resolveBoardConfig(options.config);
19555
20283
  const client = deps.client ?? defaultGitHubClient();
19556
- const collected = await collectBoardItems(cfg, { repo: options.repo, allowPartial: options.allowPartial, activeOnly: true }, deps);
19557
- const writable = await resolveWritableReposForClaimables(collected.items, client);
19558
- collected.warnings.push(...writable.warnings);
19559
- collected.partial = collected.partial || writable.partial;
20284
+ let collected;
20285
+ let writable;
20286
+ let pullRequests;
20287
+ let snapshotFallback;
20288
+ const attempt = deps.snapshot ? await fetchHubBoardSnapshot(
20289
+ {
20290
+ owner: cfg.projectOwner,
20291
+ number: cfg.projectNumber,
20292
+ statusFieldId: cfg.statusFieldId,
20293
+ activeOnly: true,
20294
+ allowPartial: options.allowPartial === true
20295
+ },
20296
+ deps.snapshot
20297
+ ) : void 0;
20298
+ if (attempt?.state === "refused") {
20299
+ throw new Error(attempt.reason);
20300
+ }
20301
+ if (attempt?.state === "ok") {
20302
+ const snapshot = attempt.snapshot;
20303
+ const currentRepo = await resolveCurrentRepo(options, deps);
20304
+ const warnings = [...snapshot.warnings];
20305
+ let partial = snapshot.partial;
20306
+ const read = nodesToItems(snapshot.nodes, warnings, cfg);
20307
+ if (read.failures.length) {
20308
+ const message = `partial board read: ${read.failures.length} item(s) came back truncated and were not read \u2014 ${read.failures.join(" | ")}`;
20309
+ if (!options.allowPartial) throw new Error(message);
20310
+ warnings.push(message);
20311
+ partial = true;
20312
+ }
20313
+ for (const unreadable of snapshot.unreadableRepos) {
20314
+ warnings.push(`partial claimable access read: ${unreadable.repo}: viewer write access UNREAD (${unreadable.error})`);
20315
+ partial = true;
20316
+ }
20317
+ collected = {
20318
+ items: read.items,
20319
+ viewer: snapshot.viewer,
20320
+ repo: currentRepo,
20321
+ projectId: snapshot.project.id,
20322
+ projectTitle: snapshot.project.title,
20323
+ warnings,
20324
+ partial
20325
+ };
20326
+ writable = {
20327
+ repos: new Set(snapshot.writableRepos.map((repo) => repo.toLowerCase())),
20328
+ unknown: new Set(snapshot.unreadableRepos.map((entry) => entry.repo.toLowerCase()))
20329
+ };
20330
+ pullRequests = snapshot.pullRequests;
20331
+ } else {
20332
+ if (attempt?.state === "unavailable") snapshotFallback = attempt.reason;
20333
+ collected = await collectBoardItems(cfg, { repo: options.repo, allowPartial: options.allowPartial, activeOnly: true }, deps);
20334
+ if (snapshotFallback) {
20335
+ collected.warnings.push(`Hub board snapshot unavailable (${snapshotFallback}) \u2014 served by the direct user-auth read (emergency fallback)`);
20336
+ }
20337
+ const probed = await resolveWritableReposForClaimables(collected.items, client);
20338
+ collected.warnings.push(...probed.warnings);
20339
+ collected.partial = collected.partial || probed.partial;
20340
+ writable = { repos: probed.repos, unknown: probed.unknown };
20341
+ }
19560
20342
  const groups = partitionBoardItems(collected.items, collected.viewer, collected.repo, writableOrUnknown(writable));
19561
20343
  const report = {
19562
20344
  project: { owner: cfg.projectOwner, number: cfg.projectNumber, id: collected.projectId, title: collected.projectTitle || String(cfg.projectNumber) },
@@ -19565,7 +20347,8 @@ async function readBoard(options, deps = {}) {
19565
20347
  ...groups,
19566
20348
  warnings: collected.warnings,
19567
20349
  partial: collected.partial,
19568
- source: "live"
20350
+ source: "live",
20351
+ ...pullRequests ? { pullRequests } : {}
19569
20352
  };
19570
20353
  if (options.includeBundleDetails || options.includeAllBodies) {
19571
20354
  await attachBundleDetails(report, client, options.allowPartial ?? false, { all: options.includeAllBodies });
@@ -19752,15 +20535,21 @@ async function claimOneBoardItem(ctx, selector, options) {
19752
20535
  let body = flatItem.details?.body;
19753
20536
  if (body === void 0) {
19754
20537
  try {
19755
- const data = await client.rest("GET", `repos/${flatItem.repository}/issues/${flatItem.number}`);
19756
- body = data?.body ?? "";
20538
+ const retryCommand = `mmi-cli oracle board claim ${flatItem.number}${options.check ? " --check" : ""}`;
20539
+ body = await readIssueBodyWithQuota(client, flatItem.repository, flatItem.number, {
20540
+ retryCommand,
20541
+ context: `board claim ${flatItem.ref} issue-body`
20542
+ });
19757
20543
  } catch (e) {
20544
+ if (isRateLimitRefusal(e)) throw e;
19758
20545
  throw new Error(
19759
20546
  `${flatItem.ref} claim refused: could not read the issue body to check dependencies (${ghError(e)}); retry once the GitHub API is reachable`
19760
20547
  );
19761
20548
  }
19762
20549
  }
19763
- const gate = await dependencyBlocksClaim(client, body);
20550
+ const gate = await dependencyBlocksClaim(client, body, {
20551
+ retryCommand: `mmi-cli oracle board claim ${flatItem.number}${options.check ? " --check" : ""}`
20552
+ });
19764
20553
  if (gate.blocked) {
19765
20554
  throw new Error(`${flatItem.ref} is not claimable: blocked on open ${gate.openDependencies.join(", ")}`);
19766
20555
  }
@@ -20065,11 +20854,7 @@ async function fetchIssueProjectItem(client, cfg, selector) {
20065
20854
  return { viewer, ...read.state === "ok" ? { item: read.item } : {} };
20066
20855
  }
20067
20856
  function isGitHubRateLimitError(e) {
20068
- if (e instanceof GitHubApiError && e.rateLimited) return true;
20069
- if (e instanceof GitHubApiError && e.status === 403) {
20070
- return /rate limit|abuse detection|secondary rate/i.test(e.message);
20071
- }
20072
- return /rate limit|API rate limit|secondary rate|abuse detection/i.test(ghError(e));
20857
+ return isRateLimitRefusal(e);
20073
20858
  }
20074
20859
  var ISSUE_DETAILS_GRAPHQL_COMMENTS_PAGE = 100;
20075
20860
  var ISSUE_DETAILS_GRAPHQL_QUERY = `
@@ -20745,6 +21530,13 @@ function ghError(e) {
20745
21530
  }
20746
21531
 
20747
21532
  // src/attach-to-project.ts
21533
+ function boardAttachRateLimitedReceipt(resetEpochSeconds) {
21534
+ return {
21535
+ onBoard: false,
21536
+ boardAttach: "rate_limited",
21537
+ ...typeof resetEpochSeconds === "number" ? { resetEpochSeconds, resetAt: new Date(resetEpochSeconds * 1e3).toISOString() } : {}
21538
+ };
21539
+ }
20748
21540
  async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn = (m) => process.stderr.write(m), retry = {}) {
20749
21541
  let projectItemId;
20750
21542
  try {
@@ -22335,7 +23127,7 @@ function checkHotfixCoverage(options = {}) {
22335
23127
  const out = git3(["rev-list", "--no-merges", range]).trim();
22336
23128
  return out ? out.split("\n") : [];
22337
23129
  };
22338
- const isAncestor = (sha, ref) => {
23130
+ const isAncestor2 = (sha, ref) => {
22339
23131
  try {
22340
23132
  git3(["merge-base", "--is-ancestor", sha, ref]);
22341
23133
  return true;
@@ -22349,7 +23141,7 @@ function checkHotfixCoverage(options = {}) {
22349
23141
  const out = git3(["patch-id", "--stable"], { input: diff }).trim();
22350
23142
  return out ? out.split(" ")[0] : null;
22351
23143
  };
22352
- const changedPaths = (sha) => {
23144
+ const changedPaths2 = (sha) => {
22353
23145
  const out = git3(["show", "--no-color", "--name-only", "--pretty=format:", sha]).trim();
22354
23146
  return out ? out.split("\n") : [];
22355
23147
  };
@@ -22370,13 +23162,13 @@ function checkHotfixCoverage(options = {}) {
22370
23162
  };
22371
23163
  const commits = mainOnly.map((sha) => {
22372
23164
  const subject = git3(["log", "-1", "--format=%s", sha]).trim();
22373
- const paths = changedPaths(sha);
23165
+ const paths = changedPaths2(sha);
22374
23166
  if (paths.length > 0 && paths.every((p) => manifestPaths.includes(p))) {
22375
23167
  return { sha, subject, coverage: "exempt-distribution" };
22376
23168
  }
22377
23169
  const sources = cherrySources(sha);
22378
23170
  if (sources.length > 0) {
22379
- if (sources.every((s) => isAncestor(s, rcRef))) {
23171
+ if (sources.every((s) => isAncestor2(s, rcRef))) {
22380
23172
  return { sha, subject, coverage: "trailer", sources };
22381
23173
  }
22382
23174
  if (ack.some((a) => sha.startsWith(a))) return { sha, subject, coverage: "acked", sources };
@@ -22399,7 +23191,7 @@ function checkHotfixCoverage(options = {}) {
22399
23191
  function checkHotfixCarries(options) {
22400
23192
  const { cwd = process.cwd(), branch, baseRef, targets } = options;
22401
23193
  const git3 = options.git ?? ((args, opts) => (0, import_node_child_process11.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
22402
- const isAncestor = (sha, ref) => {
23194
+ const isAncestor2 = (sha, ref) => {
22403
23195
  try {
22404
23196
  git3(["merge-base", "--is-ancestor", sha, ref]);
22405
23197
  return true;
@@ -22421,7 +23213,7 @@ function checkHotfixCarries(options) {
22421
23213
  }
22422
23214
  const shaMatches = (a, b) => a.startsWith(b) || b.startsWith(a);
22423
23215
  const evaluated = targets.map((t) => {
22424
- if (isAncestor(t.sha, branch)) return { ...t, carried: true, via: "ancestor" };
23216
+ if (isAncestor2(t.sha, branch)) return { ...t, carried: true, via: "ancestor" };
22425
23217
  if ([...carried].some((s) => shaMatches(s, t.sha))) return { ...t, carried: true, via: "trailer" };
22426
23218
  return { ...t, carried: false };
22427
23219
  });
@@ -22533,7 +23325,7 @@ function pathIsTolerated(path2, tolerated) {
22533
23325
  return path2.startsWith(root);
22534
23326
  });
22535
23327
  }
22536
- var HOTFIX_SKILL_TOLERATED_ROOTS = ["skills", ".kilo-plugin/skills"];
23328
+ var HOTFIX_SKILL_TOLERATED_ROOTS = ["skills"];
22537
23329
  async function cherryPickWithToleratedPaths(deps, sha, tolerated) {
22538
23330
  try {
22539
23331
  await deps.run("git", ["cherry-pick", "-x", sha]);
@@ -23425,6 +24217,80 @@ async function announceRelease(deps, args) {
23425
24217
  init_github_client();
23426
24218
  init_hub_auth();
23427
24219
 
24220
+ // src/deploy-port-doctor.ts
24221
+ init_fetch_retry();
24222
+ init_client_version();
24223
+
24224
+ // ../infra/src/deploy-port-collision.ts
24225
+ function formatDeployPortCollisionReportLine(collision) {
24226
+ const owners = collision.owners.map((o) => `${o.slug}/${o.stage}`).join(", ");
24227
+ return `${collision.sshHost}:${collision.port} owned by ${owners} \u2014 reassign one with mmi-cli oracle org project set-deploy mutmutco/<slug> --stage <stage> --port <free-port> (do not auto-reassign live ports from doctor)`;
24228
+ }
24229
+
24230
+ // src/deploy-port-doctor.ts
24231
+ async function fetchDeployPortCollisions(deps) {
24232
+ if (!deps.baseUrl) return { ok: false, collisions: [], error: "no Hub API base URL configured" };
24233
+ const token = await deps.token();
24234
+ if (!token) return { ok: false, collisions: [], error: "no Hub session token", status: 401 };
24235
+ try {
24236
+ const res = await fetchWithRetry(
24237
+ deps.fetch ?? fetch,
24238
+ `${deps.baseUrl.replace(/\/$/, "")}${DEPLOY_PORT_COLLISIONS_PATH}`,
24239
+ {
24240
+ method: "GET",
24241
+ headers: {
24242
+ Accept: "application/json",
24243
+ Authorization: `Bearer ${token}`,
24244
+ ...clientVersionHeaders()
24245
+ }
24246
+ },
24247
+ { attempts: 3, timeoutMs: deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS, sleep: deps.retrySleep }
24248
+ );
24249
+ if (res.status === 426) {
24250
+ return { ok: false, collisions: [], error: upgradeRequiredError(res, await res.json().catch(() => null)), status: 426 };
24251
+ }
24252
+ if (res.status === 403) {
24253
+ return { ok: false, collisions: [], error: "master-admin only", status: 403 };
24254
+ }
24255
+ if (!res.ok) {
24256
+ const detail = (await res.json().catch(() => null))?.error;
24257
+ return { ok: false, collisions: [], error: detail ?? `HTTP ${res.status}`, status: res.status };
24258
+ }
24259
+ const body = await res.json();
24260
+ const collisions = [];
24261
+ for (const c of body.collisions ?? []) {
24262
+ if (typeof c.sshHost !== "string" || typeof c.port !== "number" || !Array.isArray(c.owners)) continue;
24263
+ const owners = c.owners.filter((o) => typeof o?.slug === "string" && typeof o?.stage === "string").map((o) => ({ slug: o.slug, stage: o.stage }));
24264
+ if (!owners.length) continue;
24265
+ const report = typeof c.report === "string" && c.report.trim() ? c.report : formatDeployPortCollisionReportLine({
24266
+ sshHost: c.sshHost,
24267
+ port: c.port,
24268
+ owners: owners.map((o) => ({ ...o, sshHost: c.sshHost, port: c.port }))
24269
+ });
24270
+ collisions.push({ sshHost: c.sshHost, port: c.port, owners, report });
24271
+ }
24272
+ return { ok: collisions.length === 0, collisions };
24273
+ } catch (e) {
24274
+ return { ok: false, collisions: [], error: e.message };
24275
+ }
24276
+ }
24277
+ function renderDeployPortDoctor(report) {
24278
+ if (report.error) {
24279
+ return [`org project deploy doctor: ${report.error}`];
24280
+ }
24281
+ if (report.ok) {
24282
+ return ["org project deploy doctor: OK \u2014 no duplicate (sshHost, port) DEPLOY# coordinates"];
24283
+ }
24284
+ const lines = [
24285
+ `org project deploy doctor: CHECK \u2014 ${report.collisions.length} colliding (sshHost, port) coordinate(s)`
24286
+ ];
24287
+ for (const c of report.collisions) lines.push(` ${c.report}`);
24288
+ lines.push(
24289
+ "Recovery: reassign one owner with mmi-cli oracle org project set-deploy mutmutco/<slug> --stage <stage> --port <free-port> (never auto-reassign from doctor)"
24290
+ );
24291
+ return lines;
24292
+ }
24293
+
23428
24294
  // src/repo-index.ts
23429
24295
  var import_node_crypto6 = require("node:crypto");
23430
24296
  var import_node_child_process12 = require("node:child_process");
@@ -23454,6 +24320,66 @@ function isSafeRepoIndexPath(value) {
23454
24320
  if (segments.some((segment) => !segment || segment === "." || segment === "..")) return false;
23455
24321
  return !isHardDeniedRepoIndexPath(value);
23456
24322
  }
24323
+ var REPO_INDEX_ALGORITHM_VERSION = 1;
24324
+ var REPO_INDEX_FILTER_VERSION = 2;
24325
+ var REPO_INDEX_MATERIAL_LAYOUT_VERSION = 2;
24326
+ var LEGACY_REPO_INDEX_PROVENANCE = Object.freeze({
24327
+ algorithmVersion: 1,
24328
+ filterVersion: 1,
24329
+ materialLayoutVersion: 1
24330
+ });
24331
+ var CURRENT_REPO_INDEX_PROVENANCE = Object.freeze({
24332
+ algorithmVersion: REPO_INDEX_ALGORITHM_VERSION,
24333
+ filterVersion: REPO_INDEX_FILTER_VERSION,
24334
+ materialLayoutVersion: REPO_INDEX_MATERIAL_LAYOUT_VERSION
24335
+ });
24336
+ function repoIndexProvenanceToken(provenance = CURRENT_REPO_INDEX_PROVENANCE) {
24337
+ const resolved = provenance && typeof provenance === "object" ? provenance : {};
24338
+ return `a${resolved.algorithmVersion}-f${resolved.filterVersion}-l${resolved.materialLayoutVersion}`;
24339
+ }
24340
+ var CURRENT_REPO_INDEX_PROVENANCE_TOKEN = repoIndexProvenanceToken(CURRENT_REPO_INDEX_PROVENANCE);
24341
+ var GENERATED_PROJECTION = Object.freeze([
24342
+ // Host plugin packages: the assembled skills/hooks/scripts/bin payload and the copied host
24343
+ // manifest directory. `packages/<host>-plugin/<file>` at the root stays admitted.
24344
+ /^packages\/(?:claude|codex|kimi|cursor|hermes)-plugin\/(?:skills|hooks|scripts|bin)\//,
24345
+ /^packages\/(?:claude|codex|kimi|cursor|hermes)-plugin\/\.[A-Za-z0-9-]+-plugin\//,
24346
+ // In-repo plugin roots assembled by the same declaration.
24347
+ /^\.kilo-plugin\/(?:skills|scripts)\//,
24348
+ /^\.pi-plugin\/(?:skills|scripts|extensions)\//,
24349
+ // Release metadata regenerated from the tracked tree at every prepare.
24350
+ /^distribution-bom\.json$/,
24351
+ // Cross-repo generator outputs the estate audit named.
24352
+ /^product\/connector\/generated\//,
24353
+ /^packages\/jerv-pi\/generated\/roster\//,
24354
+ /^docs\/\.release-inbox\//
24355
+ ]);
24356
+ function isGeneratedRepoIndexProjection(value) {
24357
+ return typeof value === "string" && GENERATED_PROJECTION.some((pattern) => pattern.test(value));
24358
+ }
24359
+ function isCanonicalRepoIndexPath(value) {
24360
+ return isSafeRepoIndexPath(value) && !isGeneratedRepoIndexProjection(value);
24361
+ }
24362
+ function readRepoIndexProvenance(manifest) {
24363
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) return null;
24364
+ const provenance = manifest.indexProvenance;
24365
+ if (provenance === void 0) return { ...LEGACY_REPO_INDEX_PROVENANCE };
24366
+ if (!provenance || typeof provenance !== "object" || Array.isArray(provenance)) return null;
24367
+ const keys = ["algorithmVersion", "filterVersion", "materialLayoutVersion"];
24368
+ if (Object.keys(provenance).length !== keys.length) return null;
24369
+ for (const key of keys) {
24370
+ const value = provenance[key];
24371
+ if (!Number.isInteger(value) || value < 1) return null;
24372
+ }
24373
+ return {
24374
+ algorithmVersion: provenance.algorithmVersion,
24375
+ filterVersion: provenance.filterVersion,
24376
+ materialLayoutVersion: provenance.materialLayoutVersion
24377
+ };
24378
+ }
24379
+ function isRepoIndexDeltaCompatible(manifest) {
24380
+ const provenance = readRepoIndexProvenance(manifest);
24381
+ return provenance !== null && provenance.algorithmVersion === REPO_INDEX_ALGORITHM_VERSION && provenance.filterVersion === REPO_INDEX_FILTER_VERSION && provenance.materialLayoutVersion === REPO_INDEX_MATERIAL_LAYOUT_VERSION;
24382
+ }
23457
24383
 
23458
24384
  // src/repo-index.ts
23459
24385
  var REPO_INDEX_SCHEMA = 1;
@@ -23721,20 +24647,115 @@ function inferRepoSlug(cwd, exec = import_node_child_process12.execFileSync) {
23721
24647
  }
23722
24648
 
23723
24649
  // src/repo-index-cloud-client.ts
23724
- var import_node_crypto9 = require("node:crypto");
24650
+ var import_node_crypto10 = require("node:crypto");
23725
24651
  init_fetch_retry();
23726
24652
  init_client_version();
23727
24653
  init_compat();
23728
24654
 
23729
24655
  // src/repo-index-v4/builder.ts
23730
- var import_node_crypto8 = require("node:crypto");
23731
- var import_node_child_process13 = require("node:child_process");
24656
+ var import_node_crypto9 = require("node:crypto");
24657
+ var import_node_child_process14 = require("node:child_process");
23732
24658
  var import_node_fs25 = require("node:fs");
23733
24659
  var import_node_os12 = require("node:os");
23734
24660
  var import_node_path23 = require("node:path");
23735
24661
 
23736
- // src/repo-index-v4/chunks.ts
24662
+ // ../infra/repo-index-material-buckets.mjs
23737
24663
  var import_node_crypto7 = require("node:crypto");
24664
+ var REPO_INDEX_MATERIAL_BUCKET_COUNT = 2;
24665
+ var REPO_INDEX_MATERIAL_KINDS = Object.freeze(["chunks", "embeddings"]);
24666
+ var REPO_INDEX_MATERIAL_PREFIX = "material-v2";
24667
+ function canonicalMaterialJson(value) {
24668
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
24669
+ if (Array.isArray(value)) return `[${value.map(canonicalMaterialJson).join(",")}]`;
24670
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalMaterialJson(value[key])}`).join(",")}}`;
24671
+ }
24672
+ function repoIndexMaterialDigest(value) {
24673
+ return (0, import_node_crypto7.createHash)("sha256").update(canonicalMaterialJson(value)).digest("hex");
24674
+ }
24675
+ function normalizeRepoIndexMaterialPath(path2) {
24676
+ return String(path2).replace(/^\.\//, "").normalize("NFC");
24677
+ }
24678
+ function repoIndexMaterialBucket(path2) {
24679
+ const hash = (0, import_node_crypto7.createHash)("sha256").update(normalizeRepoIndexMaterialPath(path2), "utf8").digest();
24680
+ return hash.readUInt32BE(0) % REPO_INDEX_MATERIAL_BUCKET_COUNT;
24681
+ }
24682
+ function compareRepoIndexChunks(a, b) {
24683
+ const aPath = String(a?.path ?? "");
24684
+ const bPath = String(b?.path ?? "");
24685
+ if (aPath !== bPath) return aPath < bPath ? -1 : 1;
24686
+ const aLine = Number(a?.citations?.[0]?.startLine ?? 0);
24687
+ const bLine = Number(b?.citations?.[0]?.startLine ?? 0);
24688
+ if (aLine !== bLine) return aLine - bLine;
24689
+ const aId = String(a?.id ?? "");
24690
+ const bId = String(b?.id ?? "");
24691
+ return aId === bId ? 0 : aId < bId ? -1 : 1;
24692
+ }
24693
+ function compareRepoIndexEmbeddings(a, b) {
24694
+ const aId = String(a?.chunkId ?? "");
24695
+ const bId = String(b?.chunkId ?? "");
24696
+ return aId === bId ? 0 : aId < bId ? -1 : 1;
24697
+ }
24698
+ function stripRepoIndexChunkCommit(chunk) {
24699
+ const citations = Array.isArray(chunk?.citations) ? chunk.citations : [];
24700
+ return {
24701
+ ...chunk,
24702
+ citations: citations.map((citation2) => {
24703
+ const rest = { ...citation2 };
24704
+ delete rest.commit;
24705
+ return rest;
24706
+ })
24707
+ };
24708
+ }
24709
+ function repoIndexMaterialIndexKey(repoBase, digest) {
24710
+ return `${repoBase}/${REPO_INDEX_MATERIAL_PREFIX}/index/${digest}.json`;
24711
+ }
24712
+ function repoIndexMaterialIndexUri(repo, digest) {
24713
+ return `s3://${repoIndexMaterialIndexKey(`repo-index/v4/${repo}`, digest)}`;
24714
+ }
24715
+ function repoIndexMaterialBucketBody(kind, bucket, records) {
24716
+ return { schemaVersion: 4, materialLayoutVersion: 2, kind, bucket, bucketCount: REPO_INDEX_MATERIAL_BUCKET_COUNT, count: records.length, records };
24717
+ }
24718
+ var bucketBody = repoIndexMaterialBucketBody;
24719
+ function buildRepoIndexMaterialLayout(repo, chunks, embeddings) {
24720
+ const chunkList = Array.isArray(chunks) ? chunks : [];
24721
+ const embeddingList = Array.isArray(embeddings) ? embeddings : [];
24722
+ const pathByChunkId = new Map(chunkList.map((chunk) => [String(chunk?.id), String(chunk?.path)]));
24723
+ const grouped = /* @__PURE__ */ new Map();
24724
+ for (const kind of REPO_INDEX_MATERIAL_KINDS) {
24725
+ for (let bucket = 0; bucket < REPO_INDEX_MATERIAL_BUCKET_COUNT; bucket++) grouped.set(`${kind}:${bucket}`, []);
24726
+ }
24727
+ for (const chunk of chunkList) {
24728
+ grouped.get(`chunks:${repoIndexMaterialBucket(chunk?.path)}`).push(stripRepoIndexChunkCommit(chunk));
24729
+ }
24730
+ for (const embedding of embeddingList) {
24731
+ const path2 = pathByChunkId.get(String(embedding?.chunkId));
24732
+ if (path2 === void 0) throw new Error("repo-index material embeds a chunk that is not in the corpus");
24733
+ grouped.get(`embeddings:${repoIndexMaterialBucket(path2)}`).push(embedding);
24734
+ }
24735
+ const buckets = [];
24736
+ for (const kind of REPO_INDEX_MATERIAL_KINDS) {
24737
+ for (let bucket = 0; bucket < REPO_INDEX_MATERIAL_BUCKET_COUNT; bucket++) {
24738
+ const records = grouped.get(`${kind}:${bucket}`);
24739
+ records.sort(kind === "chunks" ? compareRepoIndexChunks : compareRepoIndexEmbeddings);
24740
+ const body = bucketBody(kind, bucket, records);
24741
+ buckets.push({ kind, bucket, count: records.length, digest: repoIndexMaterialDigest(body), body });
24742
+ }
24743
+ }
24744
+ const index = {
24745
+ schemaVersion: 4,
24746
+ materialLayoutVersion: 2,
24747
+ repo,
24748
+ bucketCount: REPO_INDEX_MATERIAL_BUCKET_COUNT,
24749
+ chunkCount: chunkList.length,
24750
+ embeddingCount: embeddingList.length,
24751
+ // Ordered descriptors: chunks buckets ascending, then embeddings buckets ascending.
24752
+ buckets: buckets.map(({ kind, bucket, count, digest }) => ({ kind, bucket, count, digest }))
24753
+ };
24754
+ return { bucketCount: REPO_INDEX_MATERIAL_BUCKET_COUNT, buckets, index, indexDigest: repoIndexMaterialDigest(index) };
24755
+ }
24756
+
24757
+ // src/repo-index-v4/chunks.ts
24758
+ var import_node_crypto8 = require("node:crypto");
23738
24759
  var import_node_fs24 = require("node:fs");
23739
24760
  var import_node_path22 = require("node:path");
23740
24761
 
@@ -23801,7 +24822,7 @@ async function parserFor(language) {
23801
24822
  return parser;
23802
24823
  }
23803
24824
  function sha256(value) {
23804
- return (0, import_node_crypto7.createHash)("sha256").update(value).digest("hex");
24825
+ return (0, import_node_crypto8.createHash)("sha256").update(value).digest("hex");
23805
24826
  }
23806
24827
  function pointerId(path2, kind, contentHash, start, end, symbol) {
23807
24828
  return sha256(["repo-index-v4", path2, kind, contentHash, String(start), String(end), symbol ?? ""].join("\0"));
@@ -23889,12 +24910,17 @@ async function structuralChunksForSource(repo, commit, path2, source) {
23889
24910
  parser?.delete();
23890
24911
  }
23891
24912
  }
23892
- async function buildStructuralChunks(cwd, repo, commit) {
24913
+ function canonicalRepoIndexPaths(cwd) {
23893
24914
  const candidates = listCandidatePaths(cwd).filter(isIndexablePath).sort((a, b) => a.localeCompare(b));
23894
24915
  const ignored = defaultIsIgnored(cwd, candidates);
24916
+ return candidates.filter((path2) => !ignored.has(path2) && !isHardDeniedPath(path2) && isCanonicalRepoIndexPath(path2));
24917
+ }
24918
+ function compareV4Chunks(a, b) {
24919
+ return compareRepoIndexChunks(a, b);
24920
+ }
24921
+ async function buildStructuralChunksForPaths(cwd, repo, commit, paths) {
23895
24922
  const chunks = [];
23896
- for (const path2 of candidates) {
23897
- if (ignored.has(path2) || isHardDeniedPath(path2)) continue;
24923
+ for (const path2 of paths) {
23898
24924
  const absolute = (0, import_node_path22.join)(cwd, ...path2.split("/"));
23899
24925
  if (!(0, import_node_fs24.existsSync)(absolute)) continue;
23900
24926
  let source;
@@ -23905,7 +24931,84 @@ async function buildStructuralChunks(cwd, repo, commit) {
23905
24931
  }
23906
24932
  chunks.push(...await structuralChunksForSource(repo, commit, path2, source));
23907
24933
  }
23908
- return chunks.sort((a, b) => a.path.localeCompare(b.path) || a.citations[0].startLine - b.citations[0].startLine || a.id.localeCompare(b.id));
24934
+ return chunks.sort(compareV4Chunks);
24935
+ }
24936
+ async function buildStructuralChunks(cwd, repo, commit) {
24937
+ return buildStructuralChunksForPaths(cwd, repo, commit, canonicalRepoIndexPaths(cwd));
24938
+ }
24939
+
24940
+ // src/repo-index-v4/delta.ts
24941
+ var import_node_child_process13 = require("node:child_process");
24942
+ var COMMIT = /^[a-f0-9]{40}$/;
24943
+ var V4_DELTA_DEEPEN_STEPS = [50, 250, 1e3];
24944
+ function gitRunner(cwd) {
24945
+ return (args) => {
24946
+ const result = (0, import_node_child_process13.spawnSync)("git", [...args], { cwd, encoding: "utf8", windowsHide: true, maxBuffer: 64 * 1024 * 1024 });
24947
+ if (result.error) return { status: -1, stdout: "" };
24948
+ return { status: typeof result.status === "number" ? result.status : -1, stdout: String(result.stdout ?? "") };
24949
+ };
24950
+ }
24951
+ function parseNameStatusZ(output) {
24952
+ const fields = output.split("\0");
24953
+ if (fields.length && fields[fields.length - 1] === "") fields.pop();
24954
+ const changes = [];
24955
+ for (let i = 0; i < fields.length; ) {
24956
+ const raw = fields[i++] ?? "";
24957
+ const letter = raw[0];
24958
+ if (!letter || !/^[AMDTRC]\d*$/.test(raw)) return null;
24959
+ if (letter === "R" || letter === "C") {
24960
+ const oldPath = fields[i++];
24961
+ const path3 = fields[i++];
24962
+ if (!oldPath || !path3) return null;
24963
+ changes.push(letter === "C" ? { status: "A", path: path3 } : { status: "R", path: path3, oldPath });
24964
+ continue;
24965
+ }
24966
+ const path2 = fields[i++];
24967
+ if (!path2) return null;
24968
+ changes.push({ status: letter, path: path2 });
24969
+ }
24970
+ return changes;
24971
+ }
24972
+ function changedPaths(changes) {
24973
+ return [...new Set(changes.filter((change) => change.status !== "D").map((change) => change.path))].sort();
24974
+ }
24975
+ function removedPaths(changes) {
24976
+ const removed = changes.flatMap((change) => change.status === "D" ? [change.path] : change.status === "R" && change.oldPath ? [change.oldPath] : []);
24977
+ return [...new Set(removed)].sort();
24978
+ }
24979
+ function commitPresent(git3, commit) {
24980
+ return git3(["cat-file", "-e", `${commit}^{commit}`]).status === 0;
24981
+ }
24982
+ function isAncestor(git3, base, head) {
24983
+ return git3(["merge-base", "--is-ancestor", base, head]).status === 0;
24984
+ }
24985
+ function planRepoIndexV4Delta(opts) {
24986
+ const headCommit = opts.headCommit.toLowerCase();
24987
+ const baseCommit = opts.baseCommit ? opts.baseCommit.toLowerCase() : null;
24988
+ const full = (fallbackReason) => ({ mode: "full", headCommit, fallbackReason, ...baseCommit ? { baseCommit } : {} });
24989
+ if (opts.forceFull) return full("explicit-full-rebuild");
24990
+ if (!baseCommit || !COMMIT.test(baseCommit) || baseCommit === headCommit) return full("no-active-authority");
24991
+ if (opts.basePipelineCompatible === false) return full("incompatible-base-provenance");
24992
+ if (opts.hasPriorMaterial === false) return full("no-prior-material");
24993
+ const { git: git3 } = opts;
24994
+ let usable = commitPresent(git3, baseCommit) && isAncestor(git3, baseCommit, headCommit);
24995
+ for (const depth of V4_DELTA_DEEPEN_STEPS) {
24996
+ if (usable || !opts.deepen) break;
24997
+ try {
24998
+ opts.deepen(depth);
24999
+ } catch {
25000
+ break;
25001
+ }
25002
+ usable = commitPresent(git3, baseCommit) && isAncestor(git3, baseCommit, headCommit);
25003
+ }
25004
+ if (!usable) {
25005
+ return full(commitPresent(git3, baseCommit) ? "base-not-ancestor" : "base-commit-unavailable");
25006
+ }
25007
+ const diff = git3(["diff", "--name-status", "-z", "--find-renames", baseCommit, headCommit]);
25008
+ if (diff.status !== 0) return full("diff-unreadable");
25009
+ const changes = parseNameStatusZ(diff.stdout);
25010
+ if (!changes) return full("unsupported-change-status");
25011
+ return { mode: "delta", baseCommit, headCommit, changes };
23909
25012
  }
23910
25013
 
23911
25014
  // src/repo-index-v4/builder.ts
@@ -23913,7 +25016,7 @@ var V4_MAX_CHUNKS = 1e4;
23913
25016
  var V4_MAX_ARTIFACT_BYTES = 64 * 1024 * 1024;
23914
25017
  var V4_EMBED_BATCH = 32;
23915
25018
  var V4_VECTOR_DECIMAL_PLACES = 6;
23916
- var COMMIT = /^[a-f0-9]{40}$/;
25019
+ var COMMIT2 = /^[a-f0-9]{40}$/;
23917
25020
  function canonicalJson(value) {
23918
25021
  if (value === null || typeof value !== "object") return JSON.stringify(value);
23919
25022
  if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
@@ -23921,7 +25024,7 @@ function canonicalJson(value) {
23921
25024
  return `{${Object.keys(record).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(record[k])}`).join(",")}}`;
23922
25025
  }
23923
25026
  function sha2562(value) {
23924
- return (0, import_node_crypto8.createHash)("sha256").update(canonicalJson(value)).digest("hex");
25027
+ return (0, import_node_crypto9.createHash)("sha256").update(canonicalJson(value)).digest("hex");
23925
25028
  }
23926
25029
  function quantizeV4Vector(vector) {
23927
25030
  return vector.map((value) => Number(value.toFixed(V4_VECTOR_DECIMAL_PLACES)));
@@ -23930,11 +25033,11 @@ function statePath(cwd) {
23930
25033
  return repoRuntimeStatePath(cwd, "repo-index", "v4.json");
23931
25034
  }
23932
25035
  function git(cwd, args) {
23933
- return String((0, import_node_child_process13.execFileSync)("git", args, { cwd, encoding: "utf8", windowsHide: true, stdio: ["ignore", "pipe", "ignore"] })).trim();
25036
+ return String((0, import_node_child_process14.execFileSync)("git", args, { cwd, encoding: "utf8", windowsHide: true, stdio: ["ignore", "pipe", "ignore"] })).trim();
23934
25037
  }
23935
25038
  function gitInfo(cwd) {
23936
25039
  const commit = git(cwd, ["rev-parse", "HEAD"]).toLowerCase();
23937
- if (!COMMIT.test(commit)) throw new Error("repo-index v4 requires an exact git HEAD commit");
25040
+ if (!COMMIT2.test(commit)) throw new Error("repo-index v4 requires an exact git HEAD commit");
23938
25041
  let defaultBranch = "main";
23939
25042
  try {
23940
25043
  defaultBranch = git(cwd, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]).replace(/^origin\//, "") || defaultBranch;
@@ -23956,6 +25059,9 @@ function prior(cwd) {
23956
25059
  return null;
23957
25060
  }
23958
25061
  }
25062
+ function repoIndexV4DeltaBase(envelope) {
25063
+ return envelope && isRepoIndexDeltaCompatible(envelope.manifest) ? envelope : null;
25064
+ }
23959
25065
  function tombstone(repo, commit, path2, createdAt) {
23960
25066
  const base = { repo, commit, path: path2, reason: "deleted", createdAt };
23961
25067
  return { ...base, id: sha2562(base) };
@@ -23990,7 +25096,7 @@ function runEmbedderOnce(cwd, chunks, modelDirectory, createdAt) {
23990
25096
  (0, import_node_fs25.writeFileSync)(requestFile, JSON.stringify(request));
23991
25097
  let result;
23992
25098
  try {
23993
- result = (0, import_node_child_process13.spawnSync)(process.execPath, [file, requestFile], { encoding: "utf8", windowsHide: true, timeout: V4_EMBED_TIMEOUT_MS, maxBuffer: V4_MAX_ARTIFACT_BYTES, env });
25099
+ result = (0, import_node_child_process14.spawnSync)(process.execPath, [file, requestFile], { encoding: "utf8", windowsHide: true, timeout: V4_EMBED_TIMEOUT_MS, maxBuffer: V4_MAX_ARTIFACT_BYTES, env });
23994
25100
  } finally {
23995
25101
  (0, import_node_fs25.rmSync)(requestDir, { recursive: true, force: true });
23996
25102
  }
@@ -24046,23 +25152,53 @@ function runEmbedder(cwd, chunks, modelDirectory, createdAt) {
24046
25152
  const stderr = runner.stderrTail ? ` stderr=${runner.stderrTail}` : "";
24047
25153
  throw new Error(`repo-index v4 embedding runner failed: ${runner.detail ?? "unknown"}${code}${stdout}${stderr}`);
24048
25154
  }
25155
+ function restampCitations(chunk, repo, commit) {
25156
+ return { ...chunk, citations: chunk.citations.map((citation2) => ({ ...citation2, repo, commit })) };
25157
+ }
24049
25158
  async function buildRepoIndexV4(cwd, repo, opts = {}) {
25159
+ return (await buildRepoIndexV4Detailed(cwd, repo, opts)).envelope;
25160
+ }
25161
+ async function buildRepoIndexV4Detailed(cwd, repo, opts = {}) {
24050
25162
  const { commit, defaultBranch, createdAt } = gitInfo(cwd);
24051
- const chunks = await buildStructuralChunks(cwd, repo, commit);
24052
- if (chunks.length > V4_MAX_CHUNKS) throw new Error(`repo-index v4 exceeds ${V4_MAX_CHUNKS} chunk ceiling`);
25163
+ const delta = opts.delta;
24053
25164
  const old = prior(cwd);
25165
+ const deltaBase = repoIndexV4DeltaBase(old);
25166
+ let chunks;
25167
+ let carried = 0;
25168
+ let built = 0;
25169
+ let changedCount = 0;
25170
+ let removedCount = 0;
25171
+ if (delta) {
25172
+ const admitted = new Set(canonicalRepoIndexPaths(cwd));
25173
+ const changed = new Set(changedPaths(delta.changes));
25174
+ const removed = new Set(removedPaths(delta.changes));
25175
+ changedCount = changed.size;
25176
+ removedCount = removed.size;
25177
+ const rebuilt = await buildStructuralChunksForPaths(cwd, repo, commit, [...changed].filter((path3) => admitted.has(path3)).sort());
25178
+ const kept = delta.prior.chunks.filter((chunk) => !changed.has(chunk.path) && !removed.has(chunk.path) && admitted.has(chunk.path)).map((chunk) => restampCitations(chunk, repo, commit));
25179
+ carried = kept.length;
25180
+ built = rebuilt.length;
25181
+ chunks = [...kept, ...rebuilt].sort(compareV4Chunks);
25182
+ } else {
25183
+ chunks = await buildStructuralChunks(cwd, repo, commit);
25184
+ built = chunks.length;
25185
+ }
25186
+ if (chunks.length > V4_MAX_CHUNKS) throw new Error(`repo-index v4 exceeds ${V4_MAX_CHUNKS} chunk ceiling`);
24054
25187
  const currentPaths = new Set(chunks.map((c) => c.path));
25188
+ const priorChunks = delta ? delta.prior.chunks : old?.manifest.chunks ?? [];
25189
+ const priorTombstones = delta ? delta.prior.tombstones ?? [] : old?.manifest.tombstones ?? [];
24055
25190
  const tombstonePaths = [
24056
- ...(old?.manifest.tombstones ?? []).map((t) => t.path),
24057
- ...(old?.manifest.chunks ?? []).map((c) => c.path).filter((path3) => !currentPaths.has(path3))
25191
+ ...priorTombstones.map((t) => t.path),
25192
+ ...priorChunks.map((c) => c.path).filter((path3) => !currentPaths.has(path3))
24058
25193
  ];
24059
25194
  const tombstones = [...new Set(tombstonePaths)].sort().map((path3) => tombstone(repo, commit, path3, createdAt));
24060
- const oldChunkHashById = new Map((old?.manifest.chunks ?? []).map((c) => [c.id, c.contentHash]));
25195
+ const embeddingSource = delta ? delta.prior : { chunks: deltaBase?.manifest.chunks ?? [], embeddings: deltaBase?.manifest.embeddings ?? [] };
25196
+ const oldChunkHashById = new Map(embeddingSource.chunks.map((c) => [c.id, c.contentHash]));
24061
25197
  const reusableEmbedding = (embedding) => {
24062
25198
  const norm = Math.hypot(...embedding.vector);
24063
25199
  return embedding.vector.length === 384 && embedding.vector.every(Number.isFinite) && norm >= 0.98 && norm <= 1.02 && embedding.provenance.provider === "local" && embedding.provenance.modelDigest === "828e1496d7fabb79cfa4dcd84fa38625c0d3d21da474a00f08db0f559940cf35" && embedding.provenance.dimensions === 384 && embedding.provenance.input === "pointer-safe-structural-chunk";
24064
25200
  };
24065
- const oldEmbeddingByHash = new Map((old?.manifest.embeddings ?? []).filter(reusableEmbedding).map((e) => [oldChunkHashById.get(e.chunkId), e]));
25201
+ const oldEmbeddingByHash = new Map(embeddingSource.embeddings.filter(reusableEmbedding).map((e) => [oldChunkHashById.get(e.chunkId), e]));
24066
25202
  const reusable = chunks.flatMap((chunk) => {
24067
25203
  const priorEmbedding = oldEmbeddingByHash.get(chunk.contentHash);
24068
25204
  return priorEmbedding ? [{ ...priorEmbedding, chunkId: chunk.id }] : [];
@@ -24070,21 +25206,21 @@ async function buildRepoIndexV4(cwd, repo, opts = {}) {
24070
25206
  const reusableIds = new Set(reusable.map((e) => e.chunkId));
24071
25207
  const missing = chunks.filter((chunk) => !reusableIds.has(chunk.id));
24072
25208
  const generated = missing.length === 0 ? { embeddings: [] } : opts.embed === false ? { embeddings: [], reason: "embeddings-unavailable" } : runEmbedder(cwd, missing, opts.modelDirectory, createdAt);
24073
- const embeddings = [...reusable, ...generated.embeddings].map((embedding) => ({ ...embedding, vector: quantizeV4Vector(embedding.vector) })).sort((a, b) => a.chunkId.localeCompare(b.chunkId));
24074
- const chunksDigest = sha2562(chunks);
24075
- const embeddingsDigest = sha2562(embeddings);
24076
- const artifact = (kind, digest) => {
24077
- const identity2 = { repo, commit, immutable: true, kind, uri: `s3://repo-index/v4/${repo}/${commit}/${digest}.${kind}.json`, sha256: digest, createdAt };
25209
+ const embeddings = [...reusable, ...generated.embeddings].map((embedding) => ({ ...embedding, vector: quantizeV4Vector(embedding.vector) })).sort(compareRepoIndexEmbeddings);
25210
+ const materialIndexDigest = buildRepoIndexMaterialLayout(repo, chunks, embeddings).indexDigest;
25211
+ const artifact = (kind, digest, uri) => {
25212
+ const identity2 = { repo, commit, immutable: true, kind, uri, sha256: digest, createdAt };
24078
25213
  return { ...identity2, id: sha2562(identity2) };
24079
25214
  };
24080
- const artifacts = [artifact("chunks", chunksDigest), artifact("embeddings", embeddingsDigest)];
25215
+ const artifacts = [artifact("material-index", materialIndexDigest, repoIndexMaterialIndexUri(repo, materialIndexDigest))];
24081
25216
  const grammarProvenance = {
24082
25217
  runtime: { name: "web-tree-sitter", version: "0.26.12", license: "MIT" },
24083
25218
  grammarPack: { name: "tree-sitter-wasm", version: "1.1.4", license: "MIT", digest: "f3089ddf2c9615a423783b645c4dfb23ccda30807bbd059746f583952357c489" },
24084
25219
  languages: ["go", "java", "javascript", "jsx", "kotlin", "python", "rust", "tsx", "typescript"]
24085
25220
  };
24086
25221
  const rrf = { algorithm: "reciprocal-rank-fusion", k: 60, lexicalWeight: 1, semanticWeight: 1 };
24087
- const identity = { repo, commit, defaultBranch, immutable: true, createdAt, grammarProvenance, chunks, embeddings, artifacts, tombstones, rrf };
25222
+ const indexProvenance = { ...CURRENT_REPO_INDEX_PROVENANCE };
25223
+ const identity = { repo, commit, defaultBranch, immutable: true, ...delta ? { parentCommit: delta.baseCommit } : {}, createdAt, grammarProvenance, indexProvenance, chunks, embeddings, artifacts, tombstones, rrf };
24088
25224
  const manifest = { ...identity, id: sha2562(identity) };
24089
25225
  const complete = embeddings.length === chunks.length;
24090
25226
  const envelope = {
@@ -24098,7 +25234,23 @@ async function buildRepoIndexV4(cwd, repo, opts = {}) {
24098
25234
  (0, import_node_fs25.mkdirSync)((0, import_node_path23.dirname)(path2), { recursive: true });
24099
25235
  (0, import_node_fs25.writeFileSync)(path2, `${JSON.stringify(envelope, null, 2)}
24100
25236
  `, "utf8");
24101
- return envelope;
25237
+ const metrics = {
25238
+ mode: delta ? "delta" : "full",
25239
+ ...delta ? { baseCommit: delta.baseCommit } : {},
25240
+ changedPaths: changedCount,
25241
+ removedPaths: removedCount,
25242
+ chunksTotal: chunks.length,
25243
+ chunksCarried: carried,
25244
+ chunksBuilt: built,
25245
+ embeddingsReused: reusable.length,
25246
+ embeddingsEmbedded: generated.embeddings.length,
25247
+ tombstones: tombstones.length
25248
+ };
25249
+ return { envelope, metrics };
25250
+ }
25251
+ function formatV4BuildMetrics(repo, metrics) {
25252
+ const base = metrics.baseCommit ? ` base=${metrics.baseCommit.slice(0, 12)}` : "";
25253
+ return `${repo}: ${metrics.mode}${base} changed=${metrics.changedPaths} removed=${metrics.removedPaths} chunks=${metrics.chunksTotal} (carried=${metrics.chunksCarried} built=${metrics.chunksBuilt}) embeddings(reused=${metrics.embeddingsReused} new=${metrics.embeddingsEmbedded}) tombstones=${metrics.tombstones}`;
24102
25254
  }
24103
25255
  function repoIndexV4StorePath(cwd) {
24104
25256
  return statePath(cwd);
@@ -24111,12 +25263,12 @@ var V4_STAGE_MAX_SHARDS = 128;
24111
25263
  function encodedBytes(value) {
24112
25264
  return Buffer.byteLength(JSON.stringify(value));
24113
25265
  }
24114
- function split(kind, records, manifest) {
25266
+ function split(kind, records, manifest, bucket) {
24115
25267
  const groups = [];
24116
25268
  let group = [];
24117
25269
  for (const record of records) {
24118
25270
  const next = [...group, record];
24119
- const probe = { repo: manifest.repo, commit: manifest.commit, manifestId: manifest.id, kind, index: 127, total: 128, digest: "0".repeat(64), records: next };
25271
+ const probe = { repo: manifest.repo, commit: manifest.commit, manifestId: manifest.id, kind, ...bucket === void 0 ? {} : { bucket }, index: 127, total: 128, digest: "0".repeat(64), records: next };
24120
25272
  if (group.length && (group.length >= V4_STAGE_MAX_RECORDS || encodedBytes(probe) > V4_STAGE_MAX_BODY_BYTES)) {
24121
25273
  groups.push(group);
24122
25274
  group = [record];
@@ -24132,6 +25284,7 @@ function split(kind, records, manifest) {
24132
25284
  commit: manifest.commit,
24133
25285
  manifestId: manifest.id,
24134
25286
  kind,
25287
+ ...bucket === void 0 ? {} : { bucket },
24135
25288
  index,
24136
25289
  total: groups.length,
24137
25290
  digest: sha2562(items),
@@ -24141,11 +25294,30 @@ function split(kind, records, manifest) {
24141
25294
  return request;
24142
25295
  });
24143
25296
  }
24144
- function shardRepoIndexV4(envelope) {
25297
+ function shardRepoIndexV4(envelope, opts = {}) {
24145
25298
  const { chunks, embeddings, ...header } = envelope.manifest;
24146
- const stages = [...split("chunks", chunks, envelope.manifest), ...split("embeddings", embeddings ?? [], envelope.manifest)];
24147
- const shards = stages.map(({ kind, index, total, digest, records }) => ({ kind, index, total, digest, count: records.length }));
24148
- return { stages, finalize: { repo: header.repo, commit: header.commit, manifestId: header.id, header, status: envelope.status, shards } };
25299
+ const identity = { repo: header.repo, commit: header.commit, manifestId: header.id, header, status: envelope.status };
25300
+ if ((header.indexProvenance?.materialLayoutVersion ?? 1) < 2) {
25301
+ const stages2 = [...split("chunks", chunks, envelope.manifest), ...split("embeddings", embeddings ?? [], envelope.manifest)];
25302
+ const shards2 = stages2.map(({ kind, index, total, digest, records }) => ({ kind, index, total, digest, count: records.length }));
25303
+ return { stages: stages2, finalize: { ...identity, shards: shards2 } };
25304
+ }
25305
+ const reusable = new Set(opts.priorBucketDigests ?? []);
25306
+ const layout = buildRepoIndexMaterialLayout(header.repo, chunks, embeddings ?? []);
25307
+ const stages = [];
25308
+ const shards = [];
25309
+ const buckets = layout.buckets.map((bucket) => {
25310
+ if (reusable.has(bucket.digest)) return { kind: bucket.kind, bucket: bucket.bucket, count: bucket.count, digest: bucket.digest, shards: 0 };
25311
+ const fragments = split(bucket.kind, bucket.body.records, envelope.manifest, bucket.bucket);
25312
+ stages.push(...fragments);
25313
+ shards.push(...fragments.map(({ kind, index, total, digest, records }) => ({ kind, bucket: bucket.bucket, index, total, digest, count: records.length })));
25314
+ return { kind: bucket.kind, bucket: bucket.bucket, count: bucket.count, digest: bucket.digest, shards: fragments.length };
25315
+ });
25316
+ if (buckets.length !== 2 * REPO_INDEX_MATERIAL_BUCKET_COUNT) throw new Error("repo-index v4 material layout is incomplete");
25317
+ return { stages, finalize: { ...identity, shards, materialLayout: 2, buckets } };
25318
+ }
25319
+ function repoIndexV4BucketDigests(repo, chunks, embeddings) {
25320
+ return buildRepoIndexMaterialLayout(repo, chunks, embeddings).buckets.map((bucket) => bucket.digest);
24149
25321
  }
24150
25322
 
24151
25323
  // src/repo-index-cloud-client.ts
@@ -24154,6 +25326,55 @@ async function repoIndexSourceHostHeaders() {
24154
25326
  const { detectSurface: detectSurface2 } = await Promise.resolve().then(() => (init_plugin_guard_io(), plugin_guard_io_exports));
24155
25327
  return { [SOURCE_HOST_HEADER]: detectSurface2(process.env) };
24156
25328
  }
25329
+ var REPO_INDEX_SEARCH_LEGACY_V4_UNAVAILABLE = "repo-index search gap \u2014 Hub returned untyped v4 unavailability (not a CLI version gate); run `mmi-cli oracle repo-index status --cloud --json`, then `mmi-cli harbour org schedules run MMI-Hub/repo-index-reconcile` if authorities are incomplete";
25330
+ function isBareRepoIndexV4Unavailable(msg) {
25331
+ return /^v4\s+unavailable$/i.test(msg.replace(/\s+/g, " ").trim());
25332
+ }
25333
+ function refuseBareRepoIndexV4Unavailable(msg) {
25334
+ return isBareRepoIndexV4Unavailable(msg) ? REPO_INDEX_SEARCH_LEGACY_V4_UNAVAILABLE : msg;
25335
+ }
25336
+ function explainRepoIndexSearchError(body, status) {
25337
+ const errorClass = typeof body.errorClass === "string" ? body.errorClass.trim().toLowerCase() : "";
25338
+ const failures = Array.isArray(body.v4Authority?.authorityFailures) ? body.v4Authority.authorityFailures : [];
25339
+ const failureSummary = failures.length ? ` \u2014 ${failures.length} authority failure(s): ${failures.slice(0, 8).map((f) => `${f.repo}:${f.reason}`).join(", ")}${failures.length > 8 ? "\u2026" : ""}` : "";
25340
+ const next = typeof body.next === "string" && body.next.trim() ? ` \u2014 ${body.next.trim()}` : "";
25341
+ switch (errorClass) {
25342
+ case "incomplete-authority-cohort":
25343
+ return "v4 authority cohort incomplete \u2014 missing or unreadable active v4 authority for one or more roster repos" + failureSummary + (next || " \u2014 run `mmi-cli harbour org schedules run MMI-Hub/repo-index-reconcile`");
25344
+ case "query-embedding-unavailable":
25345
+ return "v4 query embedding unavailable \u2014 Hub could not embed the query (pinned BGE path)";
25346
+ case "estate-exceeds-v4-reader-bound":
25347
+ return "v4 search refused \u2014 estate exceeds bounded v4 reader";
25348
+ case "v4-read-failure":
25349
+ return "v4 authority read failed \u2014 Hub could not load v4 index material";
25350
+ default:
25351
+ break;
25352
+ }
25353
+ const raw = typeof body.error === "string" ? body.error.replace(/\s+/g, " ").trim() : "";
25354
+ if (isBareRepoIndexV4Unavailable(raw)) return REPO_INDEX_SEARCH_LEGACY_V4_UNAVAILABLE;
25355
+ return refuseBareRepoIndexV4Unavailable(raw || `search HTTP ${status ?? "error"}`);
25356
+ }
25357
+ var REPO_INDEX_SEARCH_NOT_READY_NEXT = "run `mmi-cli harbour org schedules run MMI-Hub/repo-index-reconcile` then re-check `mmi-cli oracle repo-index status --cloud --json`";
25358
+ function asCloudSearchNotReadyReceipt(failure) {
25359
+ return {
25360
+ ok: false,
25361
+ error: refuseBareRepoIndexV4Unavailable(failure.error),
25362
+ ...failure.status !== void 0 ? { status: failure.status } : {},
25363
+ ...typeof failure.errorClass === "string" ? { errorClass: failure.errorClass } : {},
25364
+ retrieval: failure.retrieval?.served === "unavailable" ? failure.retrieval : { requested: "v4", served: "unavailable" },
25365
+ ...failure.v4Authority ? { v4Authority: failure.v4Authority } : {},
25366
+ ...Array.isArray(failure.omittedRepos) ? { omittedRepos: failure.omittedRepos } : {},
25367
+ next: typeof failure.next === "string" && failure.next.trim() ? failure.next : REPO_INDEX_SEARCH_NOT_READY_NEXT
25368
+ };
25369
+ }
25370
+ function shouldRetryRepoIndexSearchLexical(failure, attemptedMode) {
25371
+ if (attemptedMode === "lexical") return false;
25372
+ const errorClass = typeof failure.errorClass === "string" ? failure.errorClass.trim().toLowerCase() : "";
25373
+ if (errorClass === "query-embedding-unavailable") return true;
25374
+ const error = typeof failure.error === "string" ? failure.error : "";
25375
+ if (isBareRepoIndexV4Unavailable(error)) return true;
25376
+ return /not a CLI version gate/i.test(error);
25377
+ }
24157
25378
  var WARMUP_MAX_PASSES = 3;
24158
25379
  async function probeRepoIndexV4ReadinessCloud(queries, deps) {
24159
25380
  if (!deps.baseUrl) return { ok: false, error: "Hub API URL not configured" };
@@ -24202,28 +25423,128 @@ async function probeRepoIndexV4ReadinessCloud(queries, deps) {
24202
25423
  return { ok: false, error: error.message };
24203
25424
  }
24204
25425
  }
24205
- async function reconcileRepoIndexCloud(repo, deps) {
25426
+ function priorV4BucketDigests(repo, prior2) {
25427
+ return prior2.deltaCompatible ? repoIndexV4BucketDigests(repo, prior2.chunks, prior2.embeddings) : [];
25428
+ }
25429
+ var REPO_INDEX_V4_MATERIAL_ROUTE = "/repo-index/v4/material";
25430
+ var V4_MATERIAL_MAX_PAGES = 256;
25431
+ async function fetchV4MaterialKind(repo, kind, deps, request, pin) {
25432
+ const records = [];
25433
+ let pageToken;
25434
+ let commit = pin?.commit;
25435
+ let digest = pin?.digest;
25436
+ let arrayDigest;
25437
+ let total;
25438
+ let deltaCompatible;
25439
+ for (let page = 0; page < V4_MATERIAL_MAX_PAGES; page++) {
25440
+ const params = new URLSearchParams({ repo });
25441
+ if (pageToken) params.set("pageToken", pageToken);
25442
+ else {
25443
+ params.set("kind", kind);
25444
+ if (digest) params.set("digest", digest);
25445
+ }
25446
+ const result = await request(`${REPO_INDEX_V4_MATERIAL_ROUTE}?${params}`);
25447
+ if (!result.ok) return result;
25448
+ const body = result.body;
25449
+ if (!Array.isArray(body.records) || typeof body.total !== "number" || typeof body.offset !== "number") return { ok: false, error: "v4 material page is malformed" };
25450
+ if (body.repo !== repo || body.kind !== kind) return { ok: false, error: "v4 material page identity does not match the request" };
25451
+ if (commit === void 0) commit = body.commit;
25452
+ else if (body.commit !== commit) return { ok: false, error: "v4 material page changed commit mid-walk" };
25453
+ if (digest === void 0) digest = body.digest;
25454
+ else if (body.digest !== digest) return { ok: false, error: "v4 material page changed digest mid-walk" };
25455
+ if (arrayDigest === void 0) arrayDigest = body.arrayDigest;
25456
+ else if (body.arrayDigest !== arrayDigest) return { ok: false, error: "v4 material page changed content digest mid-walk" };
25457
+ if (total === void 0) total = body.total;
25458
+ else if (body.total !== total) return { ok: false, error: "v4 material page changed record count mid-walk" };
25459
+ if (body.offset !== records.length) return { ok: false, error: "v4 material page arrived out of order" };
25460
+ if (body.deltaCompatible !== void 0) deltaCompatible = body.deltaCompatible;
25461
+ records.push(...body.records);
25462
+ pageToken = body.nextPageToken;
25463
+ if (!pageToken) break;
25464
+ }
25465
+ if (pageToken) return { ok: false, error: `v4 material walk exceeded ${V4_MATERIAL_MAX_PAGES} pages` };
25466
+ if (typeof commit !== "string" || typeof digest !== "string" || typeof arrayDigest !== "string") return { ok: false, error: "v4 material response omitted its authority identity" };
25467
+ if (records.length !== total) return { ok: false, error: `v4 material walk reassembled ${records.length} ${kind} records, expected ${total}` };
25468
+ if (sha2562(records) !== arrayDigest) return { ok: false, error: `v4 ${kind} material failed its declared content digest` };
25469
+ return { ok: true, records, commit, digest, ...deltaCompatible === void 0 ? {} : { deltaCompatible } };
25470
+ }
25471
+ async function fetchRepoIndexV4MaterialCloud(repo, deps) {
24206
25472
  if (!deps.baseUrl) return { ok: false, error: "Hub API URL not configured" };
24207
25473
  const token = await deps.token();
24208
25474
  if (!token) return { ok: false, error: "no Hub session token (run `gh auth login`)" };
25475
+ const baseUrl = deps.baseUrl.replace(/\/$/, "");
25476
+ const request = async (path2) => {
25477
+ const res = await fetchWithRetry(deps.fetch ?? fetch, `${baseUrl}${path2}`, {
25478
+ method: "GET",
25479
+ headers: { ...clientVersionHeaders(), Authorization: `Bearer ${token}` }
25480
+ }, { attempts: RETRY_ATTEMPTS2, timeoutMs: 12e4, sleep: deps.retrySleep });
25481
+ const body = await res.json().catch(() => ({}));
25482
+ if (!res.ok) return { ok: false, error: typeof body.error === "string" ? body.error : `v4 material HTTP ${res.status}`, status: res.status };
25483
+ return { ok: true, body };
25484
+ };
24209
25485
  try {
24210
- const res = await fetchWithRetry(deps.fetch ?? fetch, `${deps.baseUrl.replace(/\/$/, "")}/repo-index/reconcile`, {
24211
- method: "POST",
24212
- headers: { ...clientVersionHeaders(), Authorization: `Bearer ${token}`, "content-type": "application/json" },
24213
- body: JSON.stringify({ repo, reachable: false })
25486
+ const chunks = await fetchV4MaterialKind(repo, "chunks", deps, request);
25487
+ if (!chunks.ok) return chunks;
25488
+ const pin = { commit: chunks.commit, digest: chunks.digest };
25489
+ const embeddings = await fetchV4MaterialKind(repo, "embeddings", deps, request, pin);
25490
+ if (!embeddings.ok) return embeddings;
25491
+ const tombstones = await fetchV4MaterialKind(repo, "tombstones", deps, request, pin);
25492
+ if (!tombstones.ok) return tombstones;
25493
+ const chunkRecords = chunks.records;
25494
+ const chunkIds = new Set(chunkRecords.map((chunk) => chunk?.id));
25495
+ if (chunkRecords.some((chunk) => !chunk?.id || !chunk.path || !Array.isArray(chunk.citations) || chunk.citations.some((citation2) => citation2.repo !== repo || citation2.commit !== pin.commit || citation2.path !== chunk.path))) {
25496
+ return { ok: false, error: "v4 prior material contains a foreign or malformed citation" };
25497
+ }
25498
+ const embeddingRecords = embeddings.records;
25499
+ if (embeddingRecords.some((embedding) => !chunkIds.has(embedding?.chunkId))) return { ok: false, error: "v4 prior material embeds a chunk that is not in the corpus" };
25500
+ return {
25501
+ ok: true,
25502
+ material: {
25503
+ commit: pin.commit,
25504
+ digest: pin.digest,
25505
+ chunks: chunkRecords,
25506
+ embeddings: embeddingRecords,
25507
+ tombstones: tombstones.records,
25508
+ // A Hub that did not answer the question has not proved the base is same-pipeline, and an
25509
+ // unproved base is a full rebuild — never an assumed-compatible one.
25510
+ deltaCompatible: tombstones.deltaCompatible === true
25511
+ }
25512
+ };
25513
+ } catch (error) {
25514
+ return { ok: false, error: error.message };
25515
+ }
25516
+ }
25517
+ async function fetchRepoIndexV4ProvenanceCloud(repo, deps) {
25518
+ if (!deps.baseUrl) return { ok: false, error: "Hub API URL not configured" };
25519
+ const token = await deps.token();
25520
+ if (!token) return { ok: false, error: "no Hub session token (run `gh auth login`)" };
25521
+ const params = new URLSearchParams({ repo, kind: "tombstones", limit: "1" });
25522
+ try {
25523
+ const res = await fetchWithRetry(deps.fetch ?? fetch, `${deps.baseUrl.replace(/\/$/, "")}${REPO_INDEX_V4_MATERIAL_ROUTE}?${params}`, {
25524
+ method: "GET",
25525
+ headers: { ...clientVersionHeaders(), Authorization: `Bearer ${token}` }
24214
25526
  }, { attempts: RETRY_ATTEMPTS2, timeoutMs: deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS, sleep: deps.retrySleep });
24215
25527
  const body = await res.json().catch(() => ({}));
24216
- if (!res.ok) return { ok: false, error: typeof body.error === "string" ? body.error : `reconcile HTTP ${res.status}`, status: res.status };
24217
- return { ok: true };
24218
- } catch (e) {
24219
- return { ok: false, error: e.message };
25528
+ if (!res.ok) return { ok: false, error: typeof body.error === "string" ? body.error : `v4 material HTTP ${res.status}`, status: res.status };
25529
+ if (body.repo !== repo || typeof body.commit !== "string" || typeof body.digest !== "string") {
25530
+ return { ok: false, error: "v4 provenance probe answered for a different authority" };
25531
+ }
25532
+ return {
25533
+ ok: true,
25534
+ commit: body.commit,
25535
+ digest: body.digest,
25536
+ deltaCompatible: body.deltaCompatible === true,
25537
+ ...body.indexProvenance ? { indexProvenance: body.indexProvenance } : {}
25538
+ };
25539
+ } catch (error) {
25540
+ return { ok: false, error: error.message };
24220
25541
  }
24221
25542
  }
24222
- async function publishRepoIndexV4Cloud(envelope, deps) {
25543
+ async function publishRepoIndexV4Cloud(envelope, deps, opts = {}) {
24223
25544
  if (!deps.baseUrl) return { ok: false, error: "Hub API URL not configured" };
24224
25545
  const token = await deps.token();
24225
25546
  if (!token) return { ok: false, error: "no Hub session token (run `gh auth login`)" };
24226
- const upload = shardRepoIndexV4(envelope);
25547
+ const upload = shardRepoIndexV4(envelope, { ...opts.priorBucketDigests ? { priorBucketDigests: opts.priorBucketDigests } : {} });
24227
25548
  const post = async (path2, payload) => {
24228
25549
  const res = await fetchWithRetry(deps.fetch ?? fetch, `${deps.baseUrl.replace(/\/$/, "")}${path2}`, {
24229
25550
  method: "POST",
@@ -24239,7 +25560,10 @@ async function publishRepoIndexV4Cloud(envelope, deps) {
24239
25560
  const result = await post("/repo-index/v4/stage", stage);
24240
25561
  if (!result.ok) return result;
24241
25562
  }
24242
- const finalized = await post("/repo-index/v4/finalize", upload.finalize);
25563
+ const finalized = await post("/repo-index/v4/finalize", {
25564
+ ...upload.finalize,
25565
+ ...opts.expectedActiveDigest ? { expectedActiveDigest: opts.expectedActiveDigest } : {}
25566
+ });
24243
25567
  if (!finalized.ok) return finalized;
24244
25568
  return { ok: true, active: finalized.body.active ?? {} };
24245
25569
  } catch (error) {
@@ -24279,9 +25603,13 @@ async function getRepoIndexV4GraphCloud(opts, deps) {
24279
25603
  }
24280
25604
  }
24281
25605
  async function searchRepoIndexCloud(query, opts, deps) {
24282
- if (!deps.baseUrl) return { ok: false, error: "Hub API URL not configured" };
25606
+ if (!deps.baseUrl) {
25607
+ return asCloudSearchNotReadyReceipt({ ok: false, error: "Hub API URL not configured" });
25608
+ }
24283
25609
  const token = await deps.token();
24284
- if (!token) return { ok: false, error: "no Hub session token (run `gh auth login`)" };
25610
+ if (!token) {
25611
+ return asCloudSearchNotReadyReceipt({ ok: false, error: "no Hub session token (run `gh auth login`)" });
25612
+ }
24285
25613
  const params = new URLSearchParams({
24286
25614
  q: query,
24287
25615
  mode: opts.mode ?? "hybrid",
@@ -24298,13 +25626,36 @@ async function searchRepoIndexCloud(query, opts, deps) {
24298
25626
  deps.fetch ?? fetch,
24299
25627
  `${deps.baseUrl.replace(/\/$/, "")}/repo-index/search?${params}`,
24300
25628
  { method: "GET", headers },
24301
- { attempts: RETRY_ATTEMPTS2, timeoutMs: opts.mode === "semantic" || opts.mode === "hybrid" ? 6e4 : deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS, sleep: deps.retrySleep }
25629
+ {
25630
+ attempts: RETRY_ATTEMPTS2,
25631
+ timeoutMs: opts.mode === "semantic" || opts.mode === "hybrid" ? 6e4 : deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS,
25632
+ sleep: deps.retrySleep,
25633
+ retryOn: (response) => response.status >= 500 && response.status !== 503
25634
+ }
24302
25635
  );
24303
- if (res.status === 426) return { ok: false, error: upgradeRequiredError(res, await res.json().catch(() => null)), status: 426 };
25636
+ if (res.status === 426) {
25637
+ return asCloudSearchNotReadyReceipt({
25638
+ ok: false,
25639
+ error: upgradeRequiredError(res, await res.json().catch(() => null)),
25640
+ status: 426
25641
+ });
25642
+ }
24304
25643
  const body = await res.json().catch(() => ({}));
24305
- if (!res.ok) return { ok: false, error: body.error ?? `search HTTP ${res.status}`, status: res.status };
25644
+ if (!res.ok) {
25645
+ const error = refuseBareRepoIndexV4Unavailable(explainRepoIndexSearchError(body, res.status));
25646
+ return asCloudSearchNotReadyReceipt({
25647
+ ok: false,
25648
+ error,
25649
+ status: res.status,
25650
+ ...typeof body.errorClass === "string" ? { errorClass: body.errorClass } : {},
25651
+ ...body.retrieval && body.retrieval.served === "unavailable" ? { retrieval: body.retrieval } : {},
25652
+ ...body.v4Authority ? { v4Authority: body.v4Authority } : {},
25653
+ ...Array.isArray(body.omittedRepos) ? { omittedRepos: body.omittedRepos } : {},
25654
+ ...typeof body.next === "string" ? { next: body.next } : {}
25655
+ });
25656
+ }
24306
25657
  const shadowRate = Math.max(0, Math.min(1, Number(process.env.MMI_REPO_INDEX_V4_SHADOW_RATE ?? 0.1) || 0));
24307
- if ((0, import_node_crypto9.createHash)("sha256").update(query).digest()[0] / 256 < shadowRate) {
25658
+ if ((0, import_node_crypto10.createHash)("sha256").update(query).digest()[0] / 256 < shadowRate) {
24308
25659
  void fetchWithRetry(
24309
25660
  deps.fetch ?? fetch,
24310
25661
  `${deps.baseUrl.replace(/\/$/, "")}/repo-index/v4/shadow`,
@@ -24319,10 +25670,15 @@ async function searchRepoIndexCloud(query, opts, deps) {
24319
25670
  mode: body.mode ?? opts.mode ?? "hybrid",
24320
25671
  hits: Array.isArray(body.hits) ? body.hits : [],
24321
25672
  note: body.note,
24322
- ...body.retrieval ? { retrieval: body.retrieval } : {}
25673
+ ...Array.isArray(body.degradedReasons) ? { degradedReasons: body.degradedReasons } : {},
25674
+ ...Array.isArray(body.omittedRepos) ? { omittedRepos: body.omittedRepos } : {},
25675
+ ...body.retrieval && body.retrieval.served === "v4" ? { retrieval: body.retrieval } : {}
24323
25676
  };
24324
25677
  } catch (e) {
24325
- return { ok: false, error: e.message };
25678
+ return asCloudSearchNotReadyReceipt({
25679
+ ok: false,
25680
+ error: refuseBareRepoIndexV4Unavailable(e.message)
25681
+ });
24326
25682
  }
24327
25683
  }
24328
25684
  function isRepoIndexStatusError(st) {
@@ -24398,7 +25754,7 @@ async function gcRepoIndexCloud(deps) {
24398
25754
  var import_node_fs27 = require("node:fs");
24399
25755
  var import_node_os13 = require("node:os");
24400
25756
  var import_node_path25 = require("node:path");
24401
- var import_node_child_process14 = require("node:child_process");
25757
+ var import_node_child_process15 = require("node:child_process");
24402
25758
 
24403
25759
  // ../infra/src/repo-index-roster.ts
24404
25760
  var ORG2 = "mutmutco";
@@ -24537,7 +25893,7 @@ function buildGraphEdges(cwd, repo, commit, rosterRepos2) {
24537
25893
  }
24538
25894
 
24539
25895
  // src/repo-index-sync.ts
24540
- var COMMIT2 = /^[a-f0-9]{40}$/;
25896
+ var COMMIT3 = /^[a-f0-9]{40}$/;
24541
25897
  var SHA256 = /^[a-f0-9]{64}$/;
24542
25898
  var UTC_MILLIS = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
24543
25899
  function normalizeRepo(raw) {
@@ -24550,15 +25906,31 @@ function rosterRepos(projects) {
24550
25906
  }
24551
25907
  function shallowClone(repo, dest, token) {
24552
25908
  const basic = Buffer.from(`x-access-token:${token}`, "utf8").toString("base64");
24553
- (0, import_node_child_process14.execFileSync)(
25909
+ (0, import_node_child_process15.execFileSync)(
24554
25910
  "git",
24555
25911
  ["-c", `http.extraHeader=Authorization: Basic ${basic}`, "clone", "--depth", "1", "--single-branch", `https://github.com/${repo}.git`, dest],
24556
25912
  { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }
24557
25913
  );
24558
25914
  }
25915
+ function checkoutExactCommit(repo, dest, token, commit) {
25916
+ const git3 = (args, authenticated = false) => {
25917
+ const basic = Buffer.from(`x-access-token:${token}`, "utf8").toString("base64");
25918
+ return String((0, import_node_child_process15.execFileSync)(
25919
+ "git",
25920
+ [...authenticated ? ["-c", `http.extraHeader=Authorization: Basic ${basic}`] : [], "-C", dest, ...args],
25921
+ { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }
25922
+ ));
25923
+ };
25924
+ if (git3(["rev-parse", "HEAD"]).trim().toLowerCase() !== commit) {
25925
+ git3(["fetch", "--no-tags", "--depth", "1", "origin", commit], true);
25926
+ git3(["checkout", "--quiet", "--detach", commit]);
25927
+ }
25928
+ const head = git3(["rev-parse", "HEAD"]).trim().toLowerCase();
25929
+ if (head !== commit) throw new Error(`checkout of ${repo} resolved ${head}, not the requested commit ${commit}`);
25930
+ }
24559
25931
  function remoteHead(repo, token) {
24560
25932
  const basic = Buffer.from(`x-access-token:${token}`, "utf8").toString("base64");
24561
- const output = (0, import_node_child_process14.execFileSync)(
25933
+ const output = (0, import_node_child_process15.execFileSync)(
24562
25934
  "git",
24563
25935
  ["-c", `http.extraHeader=Authorization: Basic ${basic}`, "ls-remote", "--exit-code", `https://github.com/${repo}.git`, "HEAD"],
24564
25936
  { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }
@@ -24567,7 +25939,7 @@ function remoteHead(repo, token) {
24567
25939
  if (!match) throw new Error(`could not resolve remote HEAD for ${repo}`);
24568
25940
  return match[1];
24569
25941
  }
24570
- function verifiedReadyCommit(statusValue, repo) {
25942
+ function verifiedReadyBase(statusValue, repo) {
24571
25943
  if (!statusValue || typeof statusValue !== "object" || Array.isArray(statusValue)) return null;
24572
25944
  const status = statusValue;
24573
25945
  const v4 = status.v4;
@@ -24575,13 +25947,84 @@ function verifiedReadyCommit(statusValue, repo) {
24575
25947
  const pointer = v4;
24576
25948
  const artifactDigests = pointer.artifactDigests;
24577
25949
  const counts = [pointer.chunkCount, pointer.embeddingCount, pointer.tombstoneCount];
24578
- const structurallyVerified = status.repo === repo && pointer.repo === repo && pointer.state === "ready" && pointer.integrity === "verified" && typeof pointer.commit === "string" && COMMIT2.test(pointer.commit) && typeof pointer.digest === "string" && SHA256.test(pointer.digest) && Array.isArray(artifactDigests) && artifactDigests.length === 2 && new Set(artifactDigests).size === artifactDigests.length && artifactDigests.every((digest) => typeof digest === "string" && SHA256.test(digest)) && counts.every((count) => Number.isInteger(count) && Number(count) >= 0) && pointer.embeddingCount === pointer.chunkCount && typeof pointer.activatedAt === "string" && UTC_MILLIS.test(pointer.activatedAt) && Number.isFinite(Date.parse(pointer.activatedAt)) && new Date(pointer.activatedAt).toISOString() === pointer.activatedAt;
24579
- return structurallyVerified && typeof pointer.commit === "string" ? pointer.commit : null;
25950
+ const structurallyVerified = status.repo === repo && pointer.repo === repo && pointer.state === "ready" && pointer.integrity === "verified" && typeof pointer.commit === "string" && COMMIT3.test(pointer.commit) && typeof pointer.digest === "string" && SHA256.test(pointer.digest) && Array.isArray(artifactDigests) && artifactDigests.length === 2 && new Set(artifactDigests).size === artifactDigests.length && artifactDigests.every((digest) => typeof digest === "string" && SHA256.test(digest)) && counts.every((count) => Number.isInteger(count) && Number(count) >= 0) && pointer.embeddingCount === pointer.chunkCount && typeof pointer.activatedAt === "string" && UTC_MILLIS.test(pointer.activatedAt) && Number.isFinite(Date.parse(pointer.activatedAt)) && new Date(pointer.activatedAt).toISOString() === pointer.activatedAt;
25951
+ return structurallyVerified && typeof pointer.commit === "string" && typeof pointer.digest === "string" ? { commit: pointer.commit, digest: pointer.digest } : null;
25952
+ }
25953
+ function activePointerDigest(statusValue) {
25954
+ if (!statusValue || typeof statusValue !== "object" || Array.isArray(statusValue)) return null;
25955
+ const v4 = statusValue.v4;
25956
+ if (!v4 || typeof v4 !== "object" || Array.isArray(v4)) return null;
25957
+ const digest = v4.digest;
25958
+ return typeof digest === "string" && SHA256.test(digest) ? digest : null;
25959
+ }
25960
+ function deepenClone(repo, dest, token, depth) {
25961
+ const basic = Buffer.from(`x-access-token:${token}`, "utf8").toString("base64");
25962
+ (0, import_node_child_process15.execFileSync)(
25963
+ "git",
25964
+ ["-c", `http.extraHeader=Authorization: Basic ${basic}`, "-C", dest, "fetch", "--no-tags", "--depth", String(depth), "origin"],
25965
+ { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }
25966
+ );
25967
+ }
25968
+ async function planDeltaBuild(repo, dir, base, deps, githubToken2, forceFull = false) {
25969
+ const git3 = gitRunner(dir);
25970
+ const headCommit = git3(["rev-parse", "HEAD"]).stdout.trim().toLowerCase();
25971
+ if (forceFull) return { plan: planRepoIndexV4Delta({ headCommit, baseCommit: base?.commit ?? null, forceFull: true, git: git3 }) };
25972
+ if (!base) return { plan: planRepoIndexV4Delta({ headCommit, baseCommit: null, git: git3 }) };
25973
+ const material = await fetchRepoIndexV4MaterialCloud(repo, deps);
25974
+ if (!material.ok) {
25975
+ return { plan: planRepoIndexV4Delta({ headCommit, baseCommit: base.commit, hasPriorMaterial: false, git: git3 }) };
25976
+ }
25977
+ const prior2 = material.material;
25978
+ const plan = planRepoIndexV4Delta({
25979
+ headCommit,
25980
+ // The material walk pins its own commit; trust that over the status read that raced it.
25981
+ baseCommit: prior2.commit,
25982
+ basePipelineCompatible: prior2.deltaCompatible,
25983
+ hasPriorMaterial: true,
25984
+ git: git3,
25985
+ deepen: (depth) => deepenClone(repo, dir, githubToken2, depth)
25986
+ });
25987
+ return { plan, prior: prior2 };
24580
25988
  }
24581
25989
  async function syncEstateRepoIndex(opts) {
25990
+ const published = [];
25991
+ const failed = [];
25992
+ const skipped = [];
25993
+ const drift = [];
25994
+ const needsFullRebuild = [];
25995
+ const answer = (ok) => ({
25996
+ ok: ok ?? failed.length === 0,
25997
+ published,
25998
+ failed,
25999
+ skipped,
26000
+ provenanceToken: CURRENT_REPO_INDEX_PROVENANCE_TOKEN,
26001
+ drift,
26002
+ needsFullRebuild
26003
+ });
26004
+ const fullRebuildToken = (opts.fullRebuild ?? "").trim();
26005
+ const forceFull = fullRebuildToken !== "";
26006
+ if (forceFull && fullRebuildToken !== CURRENT_REPO_INDEX_PROVENANCE_TOKEN) {
26007
+ failed.push({
26008
+ repo: opts.repo ? normalizeRepo(opts.repo) : "*",
26009
+ error: `full-rebuild token ${JSON.stringify(fullRebuildToken)} does not match this pipeline's provenance token ${CURRENT_REPO_INDEX_PROVENANCE_TOKEN} \u2014 refusing before any clone, embedding or upload`
26010
+ });
26011
+ return answer(false);
26012
+ }
26013
+ const requestedCommit = (opts.commit ?? "").trim().toLowerCase();
26014
+ if (requestedCommit) {
26015
+ if (!opts.repo) {
26016
+ failed.push({ repo: "*", error: "--commit names one repository tip and requires --repo" });
26017
+ return answer(false);
26018
+ }
26019
+ if (!COMMIT3.test(requestedCommit)) {
26020
+ failed.push({ repo: normalizeRepo(opts.repo), error: `--commit must be a 40-hex commit sha, got ${JSON.stringify(requestedCommit)}` });
26021
+ return answer(false);
26022
+ }
26023
+ }
24582
26024
  const projects = await fetchProjectsList(opts.deps);
24583
26025
  if (!projects) {
24584
- return { ok: false, published: [], failed: [{ repo: "*", error: "could not read /projects/list" }], skipped: [] };
26026
+ failed.push({ repo: "*", error: "could not read /projects/list" });
26027
+ return answer(false);
24585
26028
  }
24586
26029
  const allRosterRepos = rosterRepos(projects);
24587
26030
  let repos = allRosterRepos;
@@ -24589,37 +26032,85 @@ async function syncEstateRepoIndex(opts) {
24589
26032
  const want = normalizeRepo(opts.repo);
24590
26033
  repos = repos.filter((r) => r.toLowerCase() === want.toLowerCase());
24591
26034
  if (repos.length === 0) {
24592
- return { ok: false, published: [], failed: [{ repo: want, error: "not on registry code roster" }], skipped: [] };
26035
+ failed.push({ repo: want, error: "not on registry code roster" });
26036
+ return answer(false);
24593
26037
  }
24594
26038
  }
24595
- const published = [];
24596
- const failed = [];
24597
- const skipped = [];
26039
+ const busy = new Set(
26040
+ (opts.skipRepos ?? []).map((repo) => normalizeRepoIndexRepo(repo)).filter((repo) => repo !== null)
26041
+ );
24598
26042
  for (const repo of repos) {
26043
+ if (busy.has(repo)) {
26044
+ drift.push({ repo, reason: "busy-elsewhere", action: "skip" });
26045
+ skipped.push(`${repo}: a per-repo reconcile run is already publishing it`);
26046
+ continue;
26047
+ }
26048
+ let base = null;
26049
+ let expectedActiveDigest;
26050
+ let reason;
24599
26051
  try {
24600
26052
  const status = await statusRepoIndexCloud(repo, opts.deps);
24601
- const activeCommit = verifiedReadyCommit(status, repo);
24602
- if (activeCommit) {
24603
- const head = remoteHead(repo, opts.githubToken);
24604
- if (head === activeCommit) {
24605
- skipped.push(`${repo}: unchanged verified-ready authority at ${head}`);
24606
- continue;
24607
- }
26053
+ if (isRepoIndexStatusError(status)) {
26054
+ reason = "status-unreadable";
26055
+ } else {
26056
+ expectedActiveDigest = activePointerDigest(status) ?? "none";
26057
+ base = verifiedReadyBase(status, repo);
26058
+ reason = base ? "changed" : expectedActiveDigest === "none" ? "missing-authority" : "unverified-authority";
24608
26059
  }
24609
26060
  } catch {
26061
+ base = null;
26062
+ expectedActiveDigest = void 0;
26063
+ reason = "status-unreadable";
24610
26064
  }
24611
- const dir = (0, import_node_fs27.mkdtempSync)((0, import_node_path25.join)((0, import_node_os13.tmpdir)(), "mmi-repo-index-"));
24612
- try {
24613
- const reconciled = await reconcileRepoIndexCloud(repo, opts.deps);
24614
- if (!reconciled.ok) {
24615
- failed.push({ repo, error: reconciled.error });
26065
+ let targetCommit = requestedCommit || void 0;
26066
+ if (base && !forceFull) {
26067
+ if (!targetCommit) {
26068
+ try {
26069
+ targetCommit = remoteHead(repo, opts.githubToken);
26070
+ } catch {
26071
+ targetCommit = void 0;
26072
+ }
26073
+ }
26074
+ const provenance = await fetchRepoIndexV4ProvenanceCloud(repo, opts.deps).catch(
26075
+ (error) => ({ ok: false, error: error.message })
26076
+ );
26077
+ if (provenance.ok && !provenance.deltaCompatible) {
26078
+ drift.push({ repo, reason: "incompatible-provenance", action: "needs-full-rebuild", activeCommit: base.commit, ...targetCommit ? { targetCommit } : {} });
26079
+ needsFullRebuild.push(repo);
26080
+ skipped.push(`${repo}: DRIFT incompatible index provenance \u2014 the active authority at ${base.commit} was not built by ${CURRENT_REPO_INDEX_PROVENANCE_TOKEN}; migrate it explicitly with \`oracle repo-index sync-estate --repo ${repo} --full-rebuild ${CURRENT_REPO_INDEX_PROVENANCE_TOKEN}\``);
26081
+ continue;
26082
+ }
26083
+ if (targetCommit && targetCommit === base.commit) {
26084
+ drift.push({ repo, reason: "healthy", action: "skip", activeCommit: base.commit, targetCommit });
26085
+ skipped.push(`${repo}: unchanged verified-ready authority at ${targetCommit}`);
24616
26086
  continue;
24617
26087
  }
26088
+ }
26089
+ drift.push({
26090
+ repo,
26091
+ reason,
26092
+ action: "build",
26093
+ ...base ? { activeCommit: base.commit } : {},
26094
+ ...targetCommit ? { targetCommit } : {}
26095
+ });
26096
+ if (opts.plan) continue;
26097
+ const dir = (0, import_node_fs27.mkdtempSync)((0, import_node_path25.join)((0, import_node_os13.tmpdir)(), "mmi-repo-index-"));
26098
+ try {
24618
26099
  shallowClone(repo, dir, opts.githubToken);
26100
+ if (requestedCommit) checkoutExactCommit(repo, dir, opts.githubToken, requestedCommit);
24619
26101
  let v4;
24620
26102
  try {
24621
- v4 = await buildRepoIndexV4(dir, repo, { modelDirectory: process.env.MMI_REPO_INDEXER_MODEL_DIR });
24622
- const v4Published = await publishRepoIndexV4Cloud(v4, opts.deps);
26103
+ const plan = await planDeltaBuild(repo, dir, base, opts.deps, opts.githubToken, forceFull);
26104
+ if (base && plan.plan.mode === "full") skipped.push(`${repo}: full rebuild (${plan.plan.fallbackReason})`);
26105
+ v4 = await buildRepoIndexV4Detailed(dir, repo, {
26106
+ modelDirectory: process.env.MMI_REPO_INDEXER_MODEL_DIR,
26107
+ ...plan.plan.mode === "delta" && plan.prior ? { delta: { baseCommit: plan.plan.baseCommit, prior: plan.prior, changes: plan.plan.changes } } : {}
26108
+ });
26109
+ const priorBucketDigests = plan.plan.mode === "delta" && plan.prior ? priorV4BucketDigests(repo, plan.prior) : [];
26110
+ const v4Published = await publishRepoIndexV4Cloud(v4.envelope, opts.deps, {
26111
+ ...expectedActiveDigest ? { expectedActiveDigest } : {},
26112
+ ...priorBucketDigests.length ? { priorBucketDigests } : {}
26113
+ });
24623
26114
  if (!v4Published.ok) {
24624
26115
  failed.push({ repo, error: `v4 failed: ${v4Published.error}` });
24625
26116
  continue;
@@ -24628,7 +26119,7 @@ async function syncEstateRepoIndex(opts) {
24628
26119
  failed.push({ repo, error: `v4 failed: ${error.message}` });
24629
26120
  continue;
24630
26121
  }
24631
- const v4Commit = v4.manifest.commit;
26122
+ const v4Commit = v4.envelope.manifest.commit;
24632
26123
  let graphEdges;
24633
26124
  try {
24634
26125
  const edges = buildGraphEdges(dir, repo, v4Commit, allRosterRepos);
@@ -24638,15 +26129,16 @@ async function syncEstateRepoIndex(opts) {
24638
26129
  } catch (error) {
24639
26130
  skipped.push(`${repo}: optional v4 graph unavailable: ${error.message}`);
24640
26131
  }
24641
- const fileCount = v4.manifest.chunks.length;
24642
- const embCount = v4.manifest.embeddings.length;
26132
+ const fileCount = v4.envelope.manifest.chunks.length;
26133
+ const embCount = v4.envelope.manifest.embeddings.length;
24643
26134
  const embGap = Math.max(0, fileCount - embCount);
24644
26135
  published.push({
24645
26136
  repo,
24646
26137
  fileCount,
24647
26138
  embCount,
24648
26139
  embGap,
24649
- graphEdges
26140
+ graphEdges,
26141
+ metrics: v4.metrics
24650
26142
  });
24651
26143
  } catch (e) {
24652
26144
  failed.push({ repo, error: e.message });
@@ -24657,7 +26149,7 @@ async function syncEstateRepoIndex(opts) {
24657
26149
  }
24658
26150
  }
24659
26151
  }
24660
- return { ok: failed.length === 0, published, failed, skipped };
26152
+ return answer();
24661
26153
  }
24662
26154
 
24663
26155
  // src/repo-index-health.ts
@@ -24934,7 +26426,7 @@ async function runRepoIndexHealth(opts) {
24934
26426
  }
24935
26427
 
24936
26428
  // src/spawn-policy-core.ts
24937
- var import_node_child_process15 = require("node:child_process");
26429
+ var import_node_child_process16 = require("node:child_process");
24938
26430
  var import_node_fs29 = require("node:fs");
24939
26431
  var import_node_path26 = require("node:path");
24940
26432
  var SPAWNERS = ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync"];
@@ -25005,7 +26497,7 @@ function findViolationsInSource(raw) {
25005
26497
  return found;
25006
26498
  }
25007
26499
  function policedFiles(root) {
25008
- const r = (0, import_node_child_process15.spawnSync)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
26500
+ const r = (0, import_node_child_process16.spawnSync)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
25009
26501
  cwd: root,
25010
26502
  encoding: "utf8",
25011
26503
  windowsHide: true,
@@ -25039,7 +26531,7 @@ function runSpawnPolicy(root) {
25039
26531
  }
25040
26532
 
25041
26533
  // src/test-policy-core.ts
25042
- var import_node_child_process16 = require("node:child_process");
26534
+ var import_node_child_process17 = require("node:child_process");
25043
26535
  var import_node_fs30 = require("node:fs");
25044
26536
  var import_node_path27 = require("node:path");
25045
26537
  var POLICY_FILE = "test-policy.json";
@@ -25093,8 +26585,252 @@ function globToRegExp(glob) {
25093
26585
  function isTestPath(path2) {
25094
26586
  return TEST_RE.test(path2) || PY_TEST_RE.test(path2);
25095
26587
  }
25096
- function loadPolicy(root, readFile6 = readFileOrNull2) {
25097
- const raw = readFile6((0, import_node_path27.join)(root, POLICY_FILE));
26588
+ var SUPPORTED_SOURCE = /\.[cm]?[jt]s$/;
26589
+ var MAX_ANALYZED_BYTES = 512 * 1024;
26590
+ var WORD_CHAR = /[\w$\u0080-\uffff]/;
26591
+ var LINE_TERMINATOR = /[\n\r\u2028\u2029]/;
26592
+ var PLAIN_IDENTIFIER = /^[A-Za-z_$\u0080-\uffff][\w$\u0080-\uffff]*$/;
26593
+ var REGEX_POSITION_WORDS = /* @__PURE__ */ new Set([
26594
+ "return",
26595
+ "typeof",
26596
+ "instanceof",
26597
+ "in",
26598
+ "of",
26599
+ "new",
26600
+ "delete",
26601
+ "void",
26602
+ "throw",
26603
+ "yield",
26604
+ "await",
26605
+ "case",
26606
+ "do",
26607
+ "else",
26608
+ "if",
26609
+ "while",
26610
+ "for",
26611
+ "switch",
26612
+ "catch",
26613
+ "default",
26614
+ "extends"
26615
+ ]);
26616
+ var VALUE_END = /^[\w$\u0080-\uffff"'`]/;
26617
+ var AMBIGUOUS_BEFORE_SLASH = /* @__PURE__ */ new Set([")", "]", "}"]);
26618
+ var PUNCTUATORS = [
26619
+ ">>>=",
26620
+ "...",
26621
+ "===",
26622
+ "!==",
26623
+ "**=",
26624
+ "<<=",
26625
+ ">>=",
26626
+ ">>>",
26627
+ "&&=",
26628
+ "||=",
26629
+ "??=",
26630
+ "=>",
26631
+ "==",
26632
+ "!=",
26633
+ "<=",
26634
+ ">=",
26635
+ "&&",
26636
+ "||",
26637
+ "??",
26638
+ "?.",
26639
+ "++",
26640
+ "--",
26641
+ "+=",
26642
+ "-=",
26643
+ "*=",
26644
+ "%=",
26645
+ "&=",
26646
+ "|=",
26647
+ "^=",
26648
+ "**",
26649
+ "<<",
26650
+ ">>"
26651
+ ];
26652
+ function scanTemplateChunk(src, start) {
26653
+ let i = start;
26654
+ while (i < src.length) {
26655
+ const c = src[i];
26656
+ if (c === "\\") {
26657
+ i += 2;
26658
+ continue;
26659
+ }
26660
+ if (c === "`") return { text: src.slice(start, i + 1), end: i + 1, expr: false };
26661
+ if (c === "$" && src[i + 1] === "{") return { text: src.slice(start, i + 2), end: i + 2, expr: true };
26662
+ i++;
26663
+ }
26664
+ return null;
26665
+ }
26666
+ function scanRegex(src, start) {
26667
+ let i = start + 1;
26668
+ let inClass = false;
26669
+ while (i < src.length) {
26670
+ const c = src[i];
26671
+ if (c === "\\") {
26672
+ i += 2;
26673
+ continue;
26674
+ }
26675
+ if (LINE_TERMINATOR.test(c)) return null;
26676
+ if (inClass) {
26677
+ if (c === "]") inClass = false;
26678
+ } else if (c === "[") {
26679
+ inClass = true;
26680
+ } else if (c === "/") {
26681
+ i++;
26682
+ while (i < src.length && WORD_CHAR.test(src[i])) i++;
26683
+ return { text: src.slice(start, i), end: i };
26684
+ }
26685
+ i++;
26686
+ }
26687
+ return null;
26688
+ }
26689
+ function lexSource(src) {
26690
+ const out = [];
26691
+ const templates = [];
26692
+ let nl = true;
26693
+ let i = 0;
26694
+ const push = (text) => {
26695
+ out.push({ text, nl });
26696
+ nl = false;
26697
+ };
26698
+ if (src.startsWith("#!")) {
26699
+ const end = src.search(LINE_TERMINATOR);
26700
+ push(src.slice(0, end === -1 ? src.length : end));
26701
+ i = end === -1 ? src.length : end;
26702
+ }
26703
+ while (i < src.length) {
26704
+ const c = src[i];
26705
+ if (LINE_TERMINATOR.test(c)) {
26706
+ nl = true;
26707
+ i++;
26708
+ continue;
26709
+ }
26710
+ if (c === " " || c === " " || c === "\f" || c === "\v" || c === "\xA0" || c === "\uFEFF") {
26711
+ i++;
26712
+ continue;
26713
+ }
26714
+ if (c === "/" && src[i + 1] === "/") {
26715
+ let j = i + 2;
26716
+ while (j < src.length && !LINE_TERMINATOR.test(src[j])) j++;
26717
+ i = j;
26718
+ continue;
26719
+ }
26720
+ if (c === "/" && src[i + 1] === "*") {
26721
+ const end = src.indexOf("*/", i + 2);
26722
+ if (end === -1) return null;
26723
+ if (LINE_TERMINATOR.test(src.slice(i, end + 2))) nl = true;
26724
+ i = end + 2;
26725
+ continue;
26726
+ }
26727
+ if (c === "/") {
26728
+ const prev = out[out.length - 1]?.text;
26729
+ if (prev && AMBIGUOUS_BEFORE_SLASH.has(prev)) return null;
26730
+ if (prev && VALUE_END.test(prev) && !(PLAIN_IDENTIFIER.test(prev) && REGEX_POSITION_WORDS.has(prev))) {
26731
+ push("/");
26732
+ i++;
26733
+ continue;
26734
+ }
26735
+ const re = scanRegex(src, i);
26736
+ if (!re) return null;
26737
+ push(re.text);
26738
+ i = re.end;
26739
+ continue;
26740
+ }
26741
+ if (c === '"' || c === "'") {
26742
+ let j = i + 1;
26743
+ for (; ; ) {
26744
+ if (j >= src.length) return null;
26745
+ const d = src[j];
26746
+ if (d === "\\") {
26747
+ j += 2;
26748
+ continue;
26749
+ }
26750
+ if (d === c) {
26751
+ j++;
26752
+ break;
26753
+ }
26754
+ if (d === "\n" || d === "\r") return null;
26755
+ j++;
26756
+ }
26757
+ push(src.slice(i, j));
26758
+ i = j;
26759
+ continue;
26760
+ }
26761
+ if (c === "`") {
26762
+ const chunk = scanTemplateChunk(src, i + 1);
26763
+ if (!chunk) return null;
26764
+ push(`\`${chunk.text}`);
26765
+ i = chunk.end;
26766
+ if (chunk.expr) templates.push(0);
26767
+ continue;
26768
+ }
26769
+ if (WORD_CHAR.test(c)) {
26770
+ let j = i;
26771
+ while (j < src.length && WORD_CHAR.test(src[j])) j++;
26772
+ push(src.slice(i, j));
26773
+ i = j;
26774
+ continue;
26775
+ }
26776
+ if (c === "{") {
26777
+ if (templates.length) templates[templates.length - 1]++;
26778
+ push("{");
26779
+ i++;
26780
+ continue;
26781
+ }
26782
+ if (c === "}") {
26783
+ if (templates.length && templates[templates.length - 1] === 0) {
26784
+ templates.pop();
26785
+ push("}");
26786
+ const chunk = scanTemplateChunk(src, i + 1);
26787
+ if (!chunk) return null;
26788
+ push(chunk.text);
26789
+ i = chunk.end;
26790
+ if (chunk.expr) templates.push(0);
26791
+ continue;
26792
+ }
26793
+ if (templates.length) templates[templates.length - 1]--;
26794
+ push("}");
26795
+ i++;
26796
+ continue;
26797
+ }
26798
+ const punct = PUNCTUATORS.find((p) => src.startsWith(p, i));
26799
+ push(punct ?? c);
26800
+ i += punct ? punct.length : 1;
26801
+ }
26802
+ return templates.length ? null : out;
26803
+ }
26804
+ function isMeaningfulChange(path2, before, after) {
26805
+ if (before == null || after == null) return true;
26806
+ if (before === after) return true;
26807
+ if (!SUPPORTED_SOURCE.test(path2)) return true;
26808
+ if (before.length > MAX_ANALYZED_BYTES || after.length > MAX_ANALYZED_BYTES) return true;
26809
+ if (before.includes("\0") || after.includes("\0")) return true;
26810
+ const a = lexSource(before);
26811
+ const b = lexSource(after);
26812
+ if (!a || !b || a.length !== b.length) return true;
26813
+ return !a.every((t, k) => t.text === b[k].text && t.nl === b[k].nl);
26814
+ }
26815
+ function annotateChangeMeaning(changed, policy, read) {
26816
+ const matchers = (policy.mandatory ?? []).map((m) => globToRegExp(m.glob));
26817
+ return changed.map((file) => {
26818
+ if (file.status !== "M" || !SUPPORTED_SOURCE.test(file.path)) return file;
26819
+ if (!matchers.some((re) => re.test(file.path)) && !isTestPath(file.path)) return file;
26820
+ let pair;
26821
+ try {
26822
+ pair = read(file.path);
26823
+ } catch {
26824
+ return file;
26825
+ }
26826
+ return isMeaningfulChange(file.path, pair.before, pair.after) ? file : { ...file, meaningful: false };
26827
+ });
26828
+ }
26829
+ function isMeaningfulRow(file) {
26830
+ return file.meaningful !== false;
26831
+ }
26832
+ function loadPolicy(root, readFile7 = readFileOrNull2) {
26833
+ const raw = readFile7((0, import_node_path27.join)(root, POLICY_FILE));
25098
26834
  if (raw == null) return { mandatory: [], declared: false };
25099
26835
  try {
25100
26836
  return { ...JSON.parse(raw), declared: true };
@@ -25109,7 +26845,7 @@ function readFileOrNull2(path2) {
25109
26845
  return null;
25110
26846
  }
25111
26847
  }
25112
- function removedPaths(changed) {
26848
+ function removedPaths2(changed) {
25113
26849
  return new Set(
25114
26850
  changed.map((f) => f.status === "D" ? f.path : f.status === "R" || f.status === "C" ? f.from : void 0).filter((p) => typeof p === "string")
25115
26851
  );
@@ -25119,9 +26855,10 @@ function classify(changed, policy, present = () => false) {
25119
26855
  const mandatoryHits = changed.filter((f) => matchers.some((m) => m.re.test(f.path)));
25120
26856
  const testChanges = changed.filter((f) => isTestPath(f.path));
25121
26857
  const addedTests = testChanges.filter((f) => f.status === "A");
25122
- const removed = removedPaths(changed);
26858
+ const removed = removedPaths2(changed);
25123
26859
  const discharged = (m) => (m.satisfiedBy?.length ?? 0) > 0 && m.satisfiedBy.every((p) => present(p) && !removed.has(p));
25124
- const untestedHits = changed.filter((f) => matchers.some((m) => m.re.test(f.path) && !discharged(m)));
26860
+ const untestedHits = changed.filter((f) => isMeaningfulRow(f) && matchers.some((m) => m.re.test(f.path) && !discharged(m)));
26861
+ const meaningfulTestChanges = testChanges.filter(isMeaningfulRow);
25125
26862
  const protectedBy = new Map((policy.protected ?? []).map((p) => [p.path, p.why ?? ""]));
25126
26863
  for (const m of policy.mandatory ?? []) {
25127
26864
  for (const p of m.satisfiedBy ?? []) {
@@ -25129,7 +26866,7 @@ function classify(changed, policy, present = () => false) {
25129
26866
  }
25130
26867
  }
25131
26868
  const removedProtected = [...removed].filter((p) => protectedBy.has(p)).map((p) => ({ path: p, why: protectedBy.get(p) ?? "" }));
25132
- return { mandatoryHits, untestedHits, testChanges, addedTests, removedProtected };
26869
+ return { mandatoryHits, untestedHits, testChanges, meaningfulTestChanges, addedTests, removedProtected };
25133
26870
  }
25134
26871
  function unresolvedProtectedEntries(policy, root, exists = (path2) => (0, import_node_fs30.existsSync)(path2)) {
25135
26872
  return (policy.protected ?? []).map((p) => p.path).filter((p) => !exists((0, import_node_path27.join)(root, p)));
@@ -25139,7 +26876,7 @@ function unresolvedSatisfiers(policy, root, exists = (path2) => (0, import_node_
25139
26876
  return [...new Set(declared)].filter((p) => !exists((0, import_node_path27.join)(root, p)));
25140
26877
  }
25141
26878
  function evaluate(changed, policy, present = () => false) {
25142
- const { mandatoryHits, untestedHits, testChanges, addedTests, removedProtected } = classify(changed, policy, present);
26879
+ const { mandatoryHits, untestedHits, meaningfulTestChanges, addedTests, removedProtected } = classify(changed, policy, present);
25143
26880
  const findings = [];
25144
26881
  if (removedProtected.length > 0) {
25145
26882
  findings.push({
@@ -25150,7 +26887,7 @@ function evaluate(changed, policy, present = () => false) {
25150
26887
  ${f.why}`).join("\n") + "\n If you are removing one because it looked unrequired, read the reason above first \u2014 it is\n the answer to exactly that question. If this removal was asked for, add a commit trailer:\n Test-Policy-Override: <reason>"
25151
26888
  });
25152
26889
  }
25153
- if (untestedHits.length > 0 && testChanges.length === 0) {
26890
+ if (untestedHits.length > 0 && meaningfulTestChanges.length === 0) {
25154
26891
  findings.push({
25155
26892
  kind: "mandatory-zone-untested",
25156
26893
  paths: untestedHits.map((f) => f.path),
@@ -25169,14 +26906,14 @@ function evaluate(changed, policy, present = () => false) {
25169
26906
  return findings;
25170
26907
  }
25171
26908
  function git2(args, cwd) {
25172
- return (0, import_node_child_process16.execFileSync)("git", args, { windowsHide: true, cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
26909
+ return (0, import_node_child_process17.execFileSync)("git", args, { windowsHide: true, cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
25173
26910
  }
25174
26911
  var COAUTHOR_KEY = "Co-authored-by";
25175
26912
  var GH_MESSAGE_SEPARATOR = /^-{5,}$/;
25176
26913
  var LIFTED_KEYS = new RegExp(`^(?:${TRAILER_KEY}|${COAUTHOR_KEY}):`, "i");
25177
26914
  function parseTrailers(message, cwd) {
25178
26915
  try {
25179
- return (0, import_node_child_process16.execFileSync)("git", ["interpret-trailers", "--parse", "--unfold"], {
26916
+ return (0, import_node_child_process17.execFileSync)("git", ["interpret-trailers", "--parse", "--unfold"], {
25180
26917
  windowsHide: true,
25181
26918
  cwd,
25182
26919
  input: message,
@@ -25316,16 +27053,27 @@ function changedFilesSince(base, cwd) {
25316
27053
  const untracked = git2(["ls-files", "--others", "--exclude-standard", "-z"], cwd).split("\0").filter(Boolean).map((path2) => ({ status: "A", path: path2.replace(/\\/g, "/") }));
25317
27054
  return [...changed, ...untracked];
25318
27055
  }
27056
+ function blobAt(base, path2, cwd) {
27057
+ try {
27058
+ return git2(["show", `${base}:${path2}`], cwd);
27059
+ } catch {
27060
+ return null;
27061
+ }
27062
+ }
25319
27063
  function runTestPolicy(root, deps = {}) {
25320
27064
  const policy = deps.policy ?? loadPolicy(root);
25321
27065
  const exists = deps.exists ?? ((path2) => (0, import_node_fs30.existsSync)(path2));
25322
27066
  const counts = { mandatoryCount: (policy.mandatory ?? []).length, protectedCount: (policy.protected ?? []).length };
25323
27067
  const base = deps.changed ? "(injected)" : resolveBase(root, deps.base);
25324
27068
  const refusal = deps.changed ? null : untrustworthyRange(root, base);
25325
- const changed = deps.changed ?? (refusal ? [] : changedFilesSince(base, root));
27069
+ const raw = deps.changed ?? (refusal ? [] : changedFilesSince(base, root));
27070
+ const changed = deps.changed ? raw : annotateChangeMeaning(raw, policy, (path2) => ({
27071
+ before: blobAt(base, path2, root),
27072
+ after: readFileOrNull2((0, import_node_path27.join)(root, path2))
27073
+ }));
25326
27074
  const lookup = deps.override !== void 0 ? { override: deps.override, refusals: [] } : !deps.changed && !refusal ? readOverride(base, root) : { override: null, refusals: [] };
25327
27075
  const present = (path2) => exists((0, import_node_path27.join)(root, path2));
25328
- const removedByThisDiff = removedPaths(changed);
27076
+ const removedByThisDiff = removedPaths2(changed);
25329
27077
  const staleFindings = [];
25330
27078
  const unresolved = unresolvedProtectedEntries(policy, root, exists).filter((p) => !removedByThisDiff.has(p));
25331
27079
  if (unresolved.length > 0) {
@@ -26665,7 +28413,7 @@ function registerSecretsCommands(program3) {
26665
28413
  }
26666
28414
 
26667
28415
  // src/app-actor.ts
26668
- var import_node_crypto10 = require("node:crypto");
28416
+ var import_node_crypto11 = require("node:crypto");
26669
28417
  var APP_ACTOR_ENV = "MMI_ACTOR";
26670
28418
  var APP_VAULT_REPO = "mutmutco/MMI-Hub";
26671
28419
  var APP_VAULT_KEYS = ["GITHUB_APP_ID", "GITHUB_APP_INSTALLATION_ID", "GITHUB_APP_PRIVATE_KEY"];
@@ -26709,7 +28457,7 @@ function mintAppJwt(appId, privateKeyPem, nowSec) {
26709
28457
  exp: now + APP_JWT_TTL_S,
26710
28458
  iss: appId
26711
28459
  }));
26712
- const signer = (0, import_node_crypto10.createSign)("RSA-SHA256");
28460
+ const signer = (0, import_node_crypto11.createSign)("RSA-SHA256");
26713
28461
  signer.update(`${header}.${payload}`);
26714
28462
  return `${header}.${payload}.${signer.sign(privateKeyPem, "base64url")}`;
26715
28463
  }
@@ -27046,12 +28794,12 @@ ${SSH_RECIPE_AGENT_NOTE}`);
27046
28794
 
27047
28795
  // src/schedules-commands.ts
27048
28796
  var import_promises4 = require("node:fs/promises");
27049
- var import_node_child_process17 = require("node:child_process");
28797
+ var import_node_child_process18 = require("node:child_process");
27050
28798
  var import_node_util7 = require("node:util");
27051
28799
  init_clean_exit();
27052
28800
  init_github_client();
27053
28801
  init_cli_shared();
27054
- var execFileP5 = (0, import_node_util7.promisify)(import_node_child_process17.execFile);
28802
+ var execFileP5 = (0, import_node_util7.promisify)(import_node_child_process18.execFile);
27055
28803
  var AWS_REGION = "eu-central-1";
27056
28804
  var AWS_TIMEOUT_MS = 3e4;
27057
28805
  var AWS_RETRY_DELAY_MS = 1500;
@@ -27651,7 +29399,7 @@ init_clean_exit();
27651
29399
  init_github_client();
27652
29400
 
27653
29401
  // src/bootstrap-drift.ts
27654
- var import_node_crypto11 = require("node:crypto");
29402
+ var import_node_crypto12 = require("node:crypto");
27655
29403
  function byteComparableSeeds(manifest, cls) {
27656
29404
  return manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self" && s.classes.includes(cls));
27657
29405
  }
@@ -27671,7 +29419,7 @@ function compareSeedBytes(hubContent, repoContent) {
27671
29419
  return normalize(hubContent) === normalize(repoContent) ? "match" : "drift";
27672
29420
  }
27673
29421
  function seedContentHash(content) {
27674
- return (0, import_node_crypto11.createHash)("sha256").update(content.replace(/\r\n/g, "\n"), "utf8").digest("hex");
29422
+ return (0, import_node_crypto12.createHash)("sha256").update(content.replace(/\r\n/g, "\n"), "utf8").digest("hex");
27675
29423
  }
27676
29424
  function auditRepoSeedDrift(repo, seeds, hubContents, repoReads) {
27677
29425
  const byTarget = new Map(repoReads.map((r) => [r.target, r.content]));
@@ -27747,10 +29495,8 @@ function statusFor(read, currentSha) {
27747
29495
  if (read.drift === "match") return { status: "match", record: { mergeSha: read.pr?.mergeSha } };
27748
29496
  const pr2 = read.pr;
27749
29497
  if (!pr2) return { status: "pending", record: {} };
29498
+ if (pr2.sourceSha && pr2.sourceSha !== currentSha) return { status: "pending", record: {} };
27750
29499
  if (pr2.state === "closed") return { status: "closed-unmerged", record: { prNumber: pr2.number, prUrl: pr2.url } };
27751
- if (pr2.state === "merged" && pr2.sourceSha && pr2.sourceSha !== currentSha) {
27752
- return { status: "pending", record: {} };
27753
- }
27754
29500
  if (pr2.checks === "red") return { status: "red", record: { prNumber: pr2.number, prUrl: pr2.url } };
27755
29501
  return { status: "open-pending", record: { prNumber: pr2.number, prUrl: pr2.url, mergeSha: pr2.mergeSha } };
27756
29502
  }
@@ -27834,7 +29580,7 @@ function renderPropagationReport(plan) {
27834
29580
  }
27835
29581
 
27836
29582
  // src/bootstrap-propagation-identity.ts
27837
- var import_node_crypto12 = require("node:crypto");
29583
+ var import_node_crypto13 = require("node:crypto");
27838
29584
  var PROPAGATION_BRANCH_PREFIX = "seed-propagate-";
27839
29585
  var TARGET_MARKER_NAME = "mmi-bootstrap-propagation-target";
27840
29586
  function safeBranchPart(value, maxLength, fallback) {
@@ -27847,7 +29593,7 @@ function repoSlug2(repo) {
27847
29593
  function propagationBranch(repo, target) {
27848
29594
  const repoPart = safeBranchPart(repoSlug2(repo), 32, "repo");
27849
29595
  const targetPart = safeBranchPart(target, 48, "target");
27850
- const hash = (0, import_node_crypto12.createHash)("sha256").update(repo.trim().toLowerCase()).update("\0").update(target).digest("hex").slice(0, 12);
29596
+ const hash = (0, import_node_crypto13.createHash)("sha256").update(repo.trim().toLowerCase()).update("\0").update(target).digest("hex").slice(0, 12);
27851
29597
  return `${PROPAGATION_BRANCH_PREFIX}${repoPart}-${targetPart}-${hash}`;
27852
29598
  }
27853
29599
  function legacyPropagationBranch(repo) {
@@ -28848,7 +30594,7 @@ function registerBootstrapCommands(program3) {
28848
30594
  }
28849
30595
  const onlyManagedBlock = onlyTarget ? seedsToApply[0]?.managedBlock != null : false;
28850
30596
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
28851
- const readFile6 = (p) => (0, import_node_fs34.existsSync)(p) ? (0, import_node_fs34.readFileSync)(p, "utf8") : null;
30597
+ const readFile7 = (p) => (0, import_node_fs34.existsSync)(p) ? (0, import_node_fs34.readFileSync)(p, "utf8") : null;
28852
30598
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
28853
30599
  const putSeed = async (target, content, ref, sha) => {
28854
30600
  const tmp = (0, import_node_path32.join)((0, import_node_os15.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
@@ -28963,7 +30709,7 @@ function registerBootstrapCommands(program3) {
28963
30709
  if (isLegacyBlock) {
28964
30710
  content = upsertManagedGitignoreBlock(remoteContent).content;
28965
30711
  } else {
28966
- const writeContent = resolveSeedWriteContent(resolved, vars, readFile6, remoteContent);
30712
+ const writeContent = resolveSeedWriteContent(resolved, vars, readFile7, remoteContent);
28967
30713
  if (!writeContent.ok) {
28968
30714
  return fail(`bootstrap apply: ${resolved.target}: ${writeContent.reason} \u2014 refusing to overwrite repo-owned content`);
28969
30715
  }
@@ -29014,7 +30760,7 @@ function registerBootstrapCommands(program3) {
29014
30760
  if (prDecision.action === "reuse") {
29015
30761
  seedPrUrl = prDecision.url;
29016
30762
  } else {
29017
- const created = await ghCreate([
30763
+ const created = requireGhCreateOk(await ghCreate([
29018
30764
  "pr",
29019
30765
  "create",
29020
30766
  "--repo",
@@ -29031,7 +30777,7 @@ function registerBootstrapCommands(program3) {
29031
30777
  ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owned \`${onlyTarget}\`` : `The org-owned \`${onlyTarget}\` seed`} is declared by MMI-Hub's \`skills/bootstrap/seeds/manifest.json\` at MMI-Hub@${seedSource.sha}; this PR reconciles that declared scope and preserves everything outside it. It carries that file and nothing else \u2014 no labels, ruleset, merge settings or registry META were touched.
29032
30778
 
29033
30779
  \`${baseBranch}\` is protected (${seedPlan.reason}), so delivery goes via this branch + PR \u2014 a direct contents PUT 409s on a protected base.` : `Auto-opened by \`mmi-cli devops bootstrap apply --execute ${repo}\` (#2286): \`${baseBranch}\` is protected (${seedPlan.reason}), so the org-managed seed files are delivered via this branch + PR \u2014 a direct contents PUT 409s on a protected base ("N of N required status checks are expected"). Seeds are from MMI-Hub@${seedSource.sha}.`
29034
- ]);
30780
+ ]), "bootstrap apply pr create");
29035
30781
  seedPrUrl = created.url;
29036
30782
  }
29037
30783
  let autoMergeEnabled = true;
@@ -29063,7 +30809,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
29063
30809
  }
29064
30810
  const rulesetSeed = manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
29065
30811
  if (rulesetSeed) {
29066
- const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile6);
30812
+ const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile7);
29067
30813
  if (rulesetContent) {
29068
30814
  try {
29069
30815
  const client = defaultGitHubClient();
@@ -29342,11 +31088,18 @@ LIVE apply to ${repo}:
29342
31088
  []
29343
31089
  );
29344
31090
  const prDecision = decideSeedPrAction(identifiedOpenPrs);
31091
+ const prBody = `Auto-opened by \`mmi-cli devops bootstrap propagate --target ${seed.target} --execute\` (#4238), wave ${rec.wave}.
31092
+
31093
+ Propagates MMI-Hub@${headSha}'s ${seed.managedBlock ? "declared marker-bounded block inside repo-owned" : "org-owned copy of"} \`${seed.target}\` \u2014 the file this PR carries and nothing else; content outside a managed block remains untouched.
31094
+ ${renderPropagationTargetMarker(seed.target)}
31095
+
31096
+ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target} --execute\` (#4240) reverts this PR's merge commit; never re-run propagate with old bytes.`;
29345
31097
  let prUrl;
29346
31098
  if (prDecision.action === "reuse") {
29347
31099
  prUrl = prDecision.url;
31100
+ await gh(["pr", "edit", prUrl, "--repo", rec.repo, "--body", prBody]);
29348
31101
  } else {
29349
- const created = await ghCreate([
31102
+ const created = requireGhCreateOk(await ghCreate([
29350
31103
  "pr",
29351
31104
  "create",
29352
31105
  "--repo",
@@ -29358,13 +31111,8 @@ LIVE apply to ${repo}:
29358
31111
  "--title",
29359
31112
  `chore: propagate ${seed.managedBlock ? "Hub-managed block in" : "org-owned"} ${seed.target} from MMI-Hub (wave ${rec.wave})`,
29360
31113
  "--body",
29361
- `Auto-opened by \`mmi-cli devops bootstrap propagate --target ${seed.target} --execute\` (#4238), wave ${rec.wave}.
29362
-
29363
- Propagates MMI-Hub@${headSha}'s ${seed.managedBlock ? "declared marker-bounded block inside repo-owned" : "org-owned copy of"} \`${seed.target}\` \u2014 the file this PR carries and nothing else; content outside a managed block remains untouched.
29364
- ${renderPropagationTargetMarker(seed.target)}
29365
-
29366
- Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target} --execute\` (#4240) reverts this PR's merge commit; never re-run propagate with old bytes.`
29367
- ]);
31114
+ prBody
31115
+ ]), "bootstrap propagate pr create");
29368
31116
  prUrl = created.url;
29369
31117
  }
29370
31118
  if (rec.wave !== 0) {
@@ -29511,7 +31259,10 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
29511
31259
  if (prDecision.action === "reuse") {
29512
31260
  prUrl = prDecision.url;
29513
31261
  } else {
29514
- const created = await ghCreate(["pr", "create", "--repo", repo, "--base", baseBranch, "--head", plan.branch, "--title", plan.title, "--body", plan.body]);
31262
+ const created = requireGhCreateOk(
31263
+ await ghCreate(["pr", "create", "--repo", repo, "--base", baseBranch, "--head", plan.branch, "--title", plan.title, "--body", plan.body]),
31264
+ "bootstrap rollback pr create"
31265
+ );
29515
31266
  prUrl = created.url;
29516
31267
  }
29517
31268
  plan.prUrl = prUrl;
@@ -29711,6 +31462,15 @@ async function detectPublicIpFrom(url, fetchImpl) {
29711
31462
  if (!validStageLiveIp(ip)) throw new Error(`public IP detection returned a non-IP body from ${url}: "${ip.slice(0, 80)}"`);
29712
31463
  return ip;
29713
31464
  }
31465
+ function isStageLiveDeployModel(deployModel, projectType) {
31466
+ if (deployModel) return deployModel === "tenant-container";
31467
+ return projectType === "web-app";
31468
+ }
31469
+ function stageLiveUnsupportedReason(deployModel, projectType) {
31470
+ if (isStageLiveDeployModel(deployModel, projectType)) return null;
31471
+ const got = deployModel ?? (projectType ? `projectType=${projectType}` : "unset");
31472
+ return `stage --live applies to tenant-container repos only (registry deployModel = ${got}; registry-publish and other models have no personal cloud dev stage)`;
31473
+ }
29714
31474
  function stageLiveUpSteps(t) {
29715
31475
  return [
29716
31476
  { label: `detect your public IPv4 and IPv6 (${IP_ECHO_URL} + ${IP6_ECHO_URL}, bounded, in parallel)` },
@@ -29725,6 +31485,16 @@ function stageLiveDownSteps(t) {
29725
31485
  { label: `remove the Cloudflare edge gate for ${t.host} via the Hub backend (tenant-control cf-gate-clear)` }
29726
31486
  ];
29727
31487
  }
31488
+ function stageLiveDownNoop(t, reason) {
31489
+ return {
31490
+ command: "stage --live",
31491
+ mode: "down",
31492
+ slug: t.slug,
31493
+ repo: t.repo,
31494
+ dispatched: [],
31495
+ message: `no live stage to tear down \u2014 ${reason}`
31496
+ };
31497
+ }
29728
31498
  async function runStageLiveUp(deps, t) {
29729
31499
  if (!t.ref?.trim()) throw new Error("stage --live: cannot resolve the current branch to deploy");
29730
31500
  const detected = await deps.detectIp();
@@ -29969,7 +31739,14 @@ function registerStageCommands(program3) {
29969
31739
  const meta = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig())).catch(() => null);
29970
31740
  const host = requireEdgeDomain(meta, "dev", slug);
29971
31741
  if (!host.trim()) throw new Error(`stage --live: no dev edge host for ${slug} (registry edgeDomains.dev)`);
29972
- return { slug, repo, host, ref };
31742
+ return {
31743
+ slug,
31744
+ repo,
31745
+ host,
31746
+ ref,
31747
+ deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
31748
+ projectType: typeof meta?.projectType === "string" ? meta.projectType : void 0
31749
+ };
29973
31750
  }
29974
31751
  async function runStageLiveCommand(o) {
29975
31752
  let target;
@@ -29978,6 +31755,19 @@ function registerStageCommands(program3) {
29978
31755
  } catch (e) {
29979
31756
  return failGraceful(e.message);
29980
31757
  }
31758
+ const unsupported = stageLiveUnsupportedReason(target.deployModel, target.projectType);
31759
+ if (unsupported) {
31760
+ if (o.down) {
31761
+ const result = stageLiveDownNoop(target, unsupported);
31762
+ if (!o.apply) {
31763
+ const steps = [{ label: result.message }];
31764
+ if (o.json) return console.log(JSON.stringify({ command: "stage --live", mode: "down", slug: target.slug, repo: target.repo, noop: true, steps }, null, 2));
31765
+ return console.log(renderSteps("mmi-cli stage --live --down: dry-run plan", steps));
31766
+ }
31767
+ return printLine(o.json ? JSON.stringify(result) : `mmi-cli stage --live: ${result.message}`);
31768
+ }
31769
+ return failGraceful(unsupported);
31770
+ }
29981
31771
  const mode = o.down ? "down" : "up";
29982
31772
  if (!o.apply) {
29983
31773
  const steps = o.down ? stageLiveDownSteps(target) : stageLiveUpSteps(target);
@@ -30510,6 +32300,13 @@ async function remoteBranchExists2(branch, options = {}) {
30510
32300
  init_cli_shared();
30511
32301
  init_clean_exit();
30512
32302
  init_error_codes();
32303
+ function refuseRateLimited(e, json) {
32304
+ if (!isRateLimitedError(e)) return false;
32305
+ if (json) console.log(JSON.stringify(e.receipt));
32306
+ else console.error(`mmi-cli board claim failed: ${e.receipt.message} \u2014 retry: ${e.receipt.retryCommand}`);
32307
+ process.exitCode = 1;
32308
+ return true;
32309
+ }
30513
32310
  function registerBoardCommands(program3) {
30514
32311
  function withDiscoverMissDetail(message) {
30515
32312
  const miss = lastBoardDiscoverMiss();
@@ -30521,13 +32318,14 @@ function registerBoardCommands(program3) {
30521
32318
  }
30522
32319
  async function runBoardRead(o) {
30523
32320
  try {
32321
+ const config = await loadConfigForRepo(o.repo);
30524
32322
  const report = await readBoard({
30525
- config: await loadConfigForRepo(o.repo),
32323
+ config,
30526
32324
  repo: o.repo,
30527
32325
  includeBundleDetails: o.bundleDetails,
30528
32326
  includeAllBodies: o.bodies,
30529
32327
  allowPartial: o.allowPartial
30530
- });
32328
+ }, { snapshot: registryClientDeps(config) });
30531
32329
  console.log(o.json ? JSON.stringify(report) : renderBoardReport(report));
30532
32330
  } catch (e) {
30533
32331
  return failGraceful(`board read failed: ${withDiscoverMissDetail(e.message)}`);
@@ -30558,6 +32356,7 @@ function registerBoardCommands(program3) {
30558
32356
  if (o.json) return console.log(JSON.stringify(result));
30559
32357
  console.log(result.checked ? checkVerdict(result.item.ref, result.alreadyClaimed) : result.partial ? `Partially claimed ${result.item.ref}: ${result.warning}` : result.alreadyClaimed ? `Already claimed ${result.item.ref} - In Progress (no change)` : `Claimed ${result.item.ref} - In Progress`);
30560
32358
  } catch (e) {
32359
+ if (refuseRateLimited(e, o.json)) return;
30561
32360
  return failGraceful(`board claim failed: ${e.message}`);
30562
32361
  }
30563
32362
  return;
@@ -30582,6 +32381,7 @@ function registerBoardCommands(program3) {
30582
32381
  }
30583
32382
  if (bulk.failed > 0) process.exitCode = 1;
30584
32383
  } catch (e) {
32384
+ if (refuseRateLimited(e, o.json)) return;
30585
32385
  return failGraceful(`board claim failed: ${e.message}`);
30586
32386
  }
30587
32387
  }), [
@@ -31183,7 +32983,7 @@ async function checkDocsIndexAtHead(opts, deps) {
31183
32983
 
31184
32984
  // src/issue-commands.ts
31185
32985
  var import_node_fs38 = require("node:fs");
31186
- var import_node_crypto13 = require("node:crypto");
32986
+ var import_node_crypto14 = require("node:crypto");
31187
32987
  init_cli_shared();
31188
32988
  init_clean_exit();
31189
32989
  init_error_codes();
@@ -31656,7 +33456,7 @@ function rowIdempotencyKey(batchKey, spec) {
31656
33456
  const identity = `${spec.type}
31657
33457
  ${spec.title.trim()}
31658
33458
  ${spec.body ?? ""}`;
31659
- const hash = (0, import_node_crypto13.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
33459
+ const hash = (0, import_node_crypto14.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
31660
33460
  return `${batchKey}:${hash}`;
31661
33461
  }
31662
33462
  var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo", "surface"]);
@@ -31803,6 +33603,11 @@ ${lines}`);
31803
33603
  const attached = await deps.attach(row.number, rowRepo(spec), rowPriority);
31804
33604
  row.onBoard = attached.onBoard;
31805
33605
  if (attached.projectItemId) row.projectItemId = attached.projectItemId;
33606
+ if (attached.boardAttach) {
33607
+ row.boardAttach = attached.boardAttach;
33608
+ if (attached.resetAt) row.resetAt = attached.resetAt;
33609
+ if (attached.resetEpochSeconds !== void 0) row.resetEpochSeconds = attached.resetEpochSeconds;
33610
+ }
31806
33611
  }
31807
33612
  const parentRef = spec.parent ?? options.parent;
31808
33613
  if (parentRef) {
@@ -33098,6 +34903,214 @@ function registerPrLifecycleCommands(program3) {
33098
34903
  });
33099
34904
  }
33100
34905
 
34906
+ // src/post-merge-recon.ts
34907
+ var import_node_fs41 = require("node:fs");
34908
+ var import_node_path38 = require("node:path");
34909
+
34910
+ // src/cross-repo-filing-issue.ts
34911
+ init_github_client();
34912
+ function crossRepoFilingRetryCommand(prRepo, prNumber) {
34913
+ return `mmi-cli devops pr merge ${prNumber} --repo ${prRepo}`;
34914
+ }
34915
+ function crossRepoFilingRateLimitedResult(e, prRepo, prNumber) {
34916
+ return {
34917
+ status: "rate-limited",
34918
+ error: e instanceof Error ? e.message : String(e ?? "rate-limited"),
34919
+ rateLimited: rateLimitedReceipt({
34920
+ resetEpochSeconds: resetEpochFromError(e),
34921
+ retryCommand: crossRepoFilingRetryCommand(prRepo, prNumber),
34922
+ context: "cross-repo filing-issue reconciliation"
34923
+ })
34924
+ };
34925
+ }
34926
+ async function discoverSameOrgIssueNumber(client, currentRepo, number) {
34927
+ const [owner] = currentRepo.split("/");
34928
+ if (!owner) throw new Error(`cannot derive an organization from ${currentRepo}`);
34929
+ const repositories = await client.restPaginate(`orgs/${owner}/repos?type=all`);
34930
+ const hits = [];
34931
+ let next = 0;
34932
+ const worker = async () => {
34933
+ while (next < repositories.length) {
34934
+ const repo = repositories[next++]?.full_name;
34935
+ if (!repo || repo.toLowerCase() === currentRepo.toLowerCase()) continue;
34936
+ const issue2 = await readExactIssue(client, repo, number);
34937
+ if (issue2) hits.push(issue2);
34938
+ }
34939
+ };
34940
+ await Promise.all(Array.from({ length: Math.min(8, repositories.length) }, () => worker()));
34941
+ return hits.sort((left, right) => left.repo.localeCompare(right.repo));
34942
+ }
34943
+ async function readExactIssue(client, repo, number) {
34944
+ try {
34945
+ const issue2 = await client.rest("GET", `repos/${repo}/issues/${number}`);
34946
+ if (issue2.pull_request || issue2.number !== number || !issue2.title || !issue2.html_url) return void 0;
34947
+ const state = issue2.state?.toUpperCase();
34948
+ if (state !== "OPEN" && state !== "CLOSED") return void 0;
34949
+ return { repo, number, title: issue2.title, url: issue2.html_url, state };
34950
+ } catch (e) {
34951
+ if (e instanceof GitHubApiError && e.status === 404) return void 0;
34952
+ throw e;
34953
+ }
34954
+ }
34955
+ function explicitSameOrgIssueUrls(text, prRepo, closingNumbers) {
34956
+ const [owner] = prRepo.split("/");
34957
+ const found = /* @__PURE__ */ new Map();
34958
+ for (const match of text.matchAll(/https:\/\/github\.com\/([\w.-]+\/[\w.-]+)\/issues\/(\d+)\b/gi)) {
34959
+ const repo = match[1];
34960
+ const number = Number(match[2]);
34961
+ if (!repo || !Number.isInteger(number) || !closingNumbers.has(number)) continue;
34962
+ if (repo.split("/")[0]?.toLowerCase() !== owner.toLowerCase() || repo.toLowerCase() === prRepo.toLowerCase()) continue;
34963
+ found.set(`${repo.toLowerCase()}#${number}`, { repo, number });
34964
+ }
34965
+ return [...found.values()];
34966
+ }
34967
+ async function reconcileCrossRepoFilingIssue(client, prRepo, prNumber) {
34968
+ try {
34969
+ const pr2 = await client.rest("GET", `repos/${prRepo}/pulls/${prNumber}`);
34970
+ if (!pr2.merged_at) return { status: "not-applicable" };
34971
+ const text = `${pr2.title ?? ""}
34972
+ ${pr2.body ?? ""}`;
34973
+ const closingNumbers = new Set(findClosingMentions(text).filter((mention) => !mention.negated).map((mention) => mention.issue));
34974
+ if (closingNumbers.size === 0) return { status: "not-applicable" };
34975
+ const explicit = explicitSameOrgIssueUrls(text, prRepo, closingNumbers);
34976
+ let target;
34977
+ if (explicit.length > 1) {
34978
+ return { status: "failed", error: `multiple same-org issue URLs match bare closing references (${explicit.map((x) => `${x.repo}#${x.number}`).join(", ")})` };
34979
+ }
34980
+ if (explicit.length === 1) {
34981
+ target = await readExactIssue(client, explicit[0].repo, explicit[0].number);
34982
+ if (!target) return { status: "failed", issue: `${explicit[0].repo}#${explicit[0].number}`, error: "the explicit foreign issue URL did not resolve to an issue" };
34983
+ } else {
34984
+ const branchNumber = Number(/^#?(\d+)(?:-|$)/.exec(pr2.head?.ref ?? "")?.[1]);
34985
+ if (!Number.isInteger(branchNumber) || !closingNumbers.has(branchNumber)) return { status: "not-applicable" };
34986
+ if (await readExactIssue(client, prRepo, branchNumber)) return { status: "not-applicable" };
34987
+ const matches = await discoverSameOrgIssueNumber(client, prRepo, branchNumber);
34988
+ if (matches.length !== 1) {
34989
+ return {
34990
+ status: "failed",
34991
+ error: matches.length === 0 ? `branch #${branchNumber} matches a bare closing reference, but no same-org filing issue was found` : `branch #${branchNumber} is ambiguous across ${matches.map((match) => match.repo).join(", ")}`
34992
+ };
34993
+ }
34994
+ target = matches[0];
34995
+ }
34996
+ const issue2 = `${target.repo}#${target.number}`;
34997
+ if (target.state === "CLOSED") return { status: "already-closed", issue: issue2, url: target.url };
34998
+ const prUrl = pr2.html_url ?? `https://github.com/${prRepo}/pull/${prNumber}`;
34999
+ const comment = await client.rest("POST", `repos/${target.repo}/issues/${target.number}/comments`, {
35000
+ body: { body: `Resolved by merged PR ${prUrl}.
35001
+
35002
+ <!-- mmi-cross-repo-close:${prRepo}#${prNumber} -->` }
35003
+ });
35004
+ if (!comment.html_url) throw new Error(`GitHub created no evidence URL on ${issue2}`);
35005
+ await closeIssue(client, { ref: issue2, reason: "completed", evidence: comment.html_url });
35006
+ return { status: "closed", issue: issue2, url: target.url, evidence: comment.html_url };
35007
+ } catch (e) {
35008
+ if (isRateLimitRefusal(e)) return crossRepoFilingRateLimitedResult(e, prRepo, prNumber);
35009
+ return { status: "failed", error: e.message };
35010
+ }
35011
+ }
35012
+
35013
+ // src/post-merge-recon.ts
35014
+ var POST_MERGE_RECON_SCHEMA_VERSION = 1;
35015
+ function postMergeReconRetryCommand(repo, pr2) {
35016
+ return crossRepoFilingRetryCommand(repo, Number(pr2));
35017
+ }
35018
+ function postMergeReconStatePath(cwd, repo, pr2) {
35019
+ const safe = repo.replace(/[^A-Za-z0-9._-]+/g, "_");
35020
+ return repoRuntimeStatePath(cwd, "post-merge-recon", `${safe}-${pr2}.json`);
35021
+ }
35022
+ function isBoardAdvanceRateLimitedOnly(result) {
35023
+ if (!result) return false;
35024
+ if (result.status === "fetch-failed") {
35025
+ return isRateLimitText(result.error ?? "");
35026
+ }
35027
+ const failures = boardAdvanceFailures(result);
35028
+ if (!failures.length) return false;
35029
+ return failures.every((f) => typeof f.error === "string" && isRateLimitText(f.error));
35030
+ }
35031
+ function postMergeReconExitCode(input) {
35032
+ if (input.crossRepoFilingIssue.status === "failed") return 1;
35033
+ if (isBoardAdvanceRateLimitedOnly(input.boardAdvance)) {
35034
+ } else {
35035
+ const boardCode = boardAdvanceExitCode(input.boardAdvance);
35036
+ if (boardCode !== void 0) return boardCode;
35037
+ }
35038
+ return void 0;
35039
+ }
35040
+ function buildPostMergeReconRecovery(input) {
35041
+ const pending = [];
35042
+ if (isBoardAdvanceRateLimitedOnly(input.boardAdvance)) pending.push("board-advance");
35043
+ if (input.crossRepoFilingIssue.status === "rate-limited") pending.push("cross-repo-filing");
35044
+ if (!pending.length) return void 0;
35045
+ const pr2 = Number(input.pr);
35046
+ const retryCommand = postMergeReconRetryCommand(input.repo, pr2);
35047
+ const nowMs = input.nowMs ?? Date.now();
35048
+ let rateLimited;
35049
+ if (input.crossRepoFilingIssue.status === "rate-limited") {
35050
+ rateLimited = input.crossRepoFilingIssue.rateLimited;
35051
+ } else if (isBoardAdvanceRateLimitedOnly(input.boardAdvance)) {
35052
+ rateLimited = rateLimitedReceipt({
35053
+ resetEpochSeconds: void 0,
35054
+ retryCommand,
35055
+ context: "post-merge board advance",
35056
+ nowMs,
35057
+ message: input.boardAdvance.status === "fetch-failed" ? `post-merge board advance: ${input.boardAdvance.error ?? "rate-limited"}` : "post-merge board advance: rate-limited while moving closed issues to Done"
35058
+ });
35059
+ }
35060
+ return {
35061
+ schemaVersion: POST_MERGE_RECON_SCHEMA_VERSION,
35062
+ mergeStatus: "merged",
35063
+ repo: input.repo,
35064
+ pr: pr2,
35065
+ pending,
35066
+ retryCommand,
35067
+ createdAt: new Date(nowMs).toISOString(),
35068
+ ...rateLimited ? { rateLimited } : {},
35069
+ boardAdvanceStatus: input.boardAdvance.status,
35070
+ crossRepoFilingStatus: input.crossRepoFilingIssue.status
35071
+ };
35072
+ }
35073
+ function writePostMergeReconRecovery(cwd, recovery) {
35074
+ const path2 = postMergeReconStatePath(cwd, recovery.repo, recovery.pr);
35075
+ (0, import_node_fs41.mkdirSync)((0, import_node_path38.dirname)(path2), { recursive: true });
35076
+ (0, import_node_fs41.writeFileSync)(path2, `${JSON.stringify(recovery, null, 2)}
35077
+ `, "utf8");
35078
+ return path2;
35079
+ }
35080
+ function clearPostMergeReconRecovery(cwd, repo, pr2) {
35081
+ const path2 = postMergeReconStatePath(cwd, repo, pr2);
35082
+ if (!(0, import_node_fs41.existsSync)(path2)) return;
35083
+ try {
35084
+ (0, import_node_fs41.unlinkSync)(path2);
35085
+ } catch {
35086
+ }
35087
+ }
35088
+ function postMergeReconWarnings(input) {
35089
+ const lines = [];
35090
+ const boardMsg = boardAdvanceFailureMessage(input.boardAdvance);
35091
+ if (boardMsg) {
35092
+ if (isBoardAdvanceRateLimitedOnly(input.boardAdvance)) {
35093
+ const reset = input.recovery?.rateLimited?.resetAt ? ` (reset ${input.recovery.rateLimited.resetAt})` : "";
35094
+ lines.push(
35095
+ `${input.context}: board advance deferred \u2014 GitHub rate-limited${reset}; the PR MERGED. Retry board/filing later: ${input.recovery?.retryCommand ?? postMergeReconRetryCommand("(repo)", "(pr)")}`
35096
+ );
35097
+ } else {
35098
+ lines.push(boardMsg);
35099
+ }
35100
+ }
35101
+ if (input.crossRepoFilingIssue.status === "rate-limited") {
35102
+ const r = input.crossRepoFilingIssue.rateLimited;
35103
+ lines.push(
35104
+ `${input.context}: cross-repo filing-issue reconciliation deferred \u2014 ${r.message}. The PR MERGED. Retry: ${r.retryCommand}` + (input.recoveryPath ? ` (recovery state: ${input.recoveryPath})` : "")
35105
+ );
35106
+ } else if (input.crossRepoFilingIssue.status === "failed") {
35107
+ lines.push(
35108
+ `${input.context}: cross-repo filing-issue reconciliation failed (${input.crossRepoFilingIssue.error}) \u2014 the PR MERGED, but a foreign filing issue may remain open.`
35109
+ );
35110
+ }
35111
+ return lines;
35112
+ }
35113
+
33101
35114
  // src/session-report.ts
33102
35115
  init_cli_shared();
33103
35116
  var GC_GH_TIMEOUT_MS4 = 2e4;
@@ -35366,6 +37379,13 @@ function checkDocsIndex(probe) {
35366
37379
  verbose: evidence
35367
37380
  };
35368
37381
  }
37382
+ var REPO_INDEX_RECONCILE_SCHEDULE_ID = "MMI-Hub/repo-index-reconcile";
37383
+ function isHubOwnedRepoIndexGap(probe) {
37384
+ if (probe.kind === "missing-authority") return true;
37385
+ if (probe.kind !== "healthy") return false;
37386
+ const readiness = probe.v4Readiness;
37387
+ return probe.cloudV4State === "invalid" || probe.cloudV4State === "tombstoned" || probe.cloudV4State === "degraded" && (probe.localV4EmbeddingCoverage ?? 1) < 0.95 || !readiness || readiness.verdict === "not-ready";
37388
+ }
35369
37389
  function checkRepoIndexCloud(probe) {
35370
37390
  if (!probe) return null;
35371
37391
  const percent = (value) => value == null ? "n/a" : `${Math.round(value * 100)}%`;
@@ -35389,14 +37409,14 @@ function checkRepoIndexCloud(probe) {
35389
37409
  ];
35390
37410
  switch (probe.kind) {
35391
37411
  case "healthy": {
35392
- const v4Unready = probe.cloudV4State === "invalid" || probe.cloudV4State === "tombstoned" || probe.cloudV4State === "degraded" && (probe.localV4EmbeddingCoverage ?? 1) < 0.95 || !readiness || readiness.verdict === "not-ready";
37412
+ const v4Unready = isHubOwnedRepoIndexGap(probe);
35393
37413
  const detail = probe.detail ?? `cloud v4 ${probe.cloudV4State ?? "absent"}, readiness ${readiness?.verdict ?? "unknown"}`;
35394
37414
  return {
35395
37415
  ok: !v4Unready,
35396
37416
  id: "repo-index",
35397
37417
  label: "repo-index",
35398
37418
  detail,
35399
- ...v4Unready ? { fix: "run `mmi-hub update`, then dispatch `mmi-cli harbour org schedules run MMI-Hub/repo-index-reconcile`; require v4 authority and readiness to become ready" } : {},
37419
+ ...v4Unready ? { fix: `Hub-owned: run \`mmi-cli harbour org schedules run ${REPO_INDEX_RECONCILE_SCHEDULE_ID}\` (or \`mmi-hub update\` then that schedule); require v4 authority and readiness to become ready` } : {},
35400
37420
  verbose: evidence
35401
37421
  };
35402
37422
  }
@@ -35425,7 +37445,7 @@ function checkRepoIndexCloud(probe) {
35425
37445
  id: "repo-index",
35426
37446
  label: "repo-index",
35427
37447
  detail: probe.detail ?? "no active v4 authority for this repo",
35428
- fix: "dispatch `mmi-cli harbour org schedules run MMI-Hub/repo-index-reconcile` or run the authenticated per-repo sync",
37448
+ fix: `Hub-owned: dispatch \`mmi-cli harbour org schedules run ${REPO_INDEX_RECONCILE_SCHEDULE_ID}\` or run the authenticated per-repo sync`,
35429
37449
  verbose: evidence
35430
37450
  };
35431
37451
  case "auth":
@@ -35801,7 +37821,30 @@ async function runDoctorClean(opts, io, deps) {
35801
37821
  return;
35802
37822
  }
35803
37823
  const row = checkRepoIndexCloud(probe);
35804
- if (row) emitNow(row);
37824
+ if (!row) return;
37825
+ if (opts.self && probe && !row.ok && isHubOwnedRepoIndexGap(probe)) {
37826
+ row.reportOnly = true;
37827
+ const evidence = [...row.verbose ?? [], `owner: Hub schedule ${REPO_INDEX_RECONCILE_SCHEDULE_ID}`];
37828
+ if (deps.dispatchRepoIndexReconcile) {
37829
+ healIntent(`repo-index \u2014 dispatching ${REPO_INDEX_RECONCILE_SCHEDULE_ID}`);
37830
+ try {
37831
+ const dispatched = await deps.dispatchRepoIndexReconcile();
37832
+ evidence.push(`self-route: ${dispatched.detail}`);
37833
+ row.fix = dispatched.ok ? `dispatched Hub-owned ${REPO_INDEX_RECONCILE_SCHEDULE_ID} \u2014 orientation may continue; re-run doctor after reconcile settles` : `Hub-owned ${REPO_INDEX_RECONCILE_SCHEDULE_ID} \u2014 dispatch failed (${dispatched.detail}); run \`mmi-cli harbour org schedules run ${REPO_INDEX_RECONCILE_SCHEDULE_ID}\``;
37834
+ if (dispatched.ok) {
37835
+ row.detail = `${row.detail} \u2014 reconcile dispatched`;
37836
+ }
37837
+ } catch (e) {
37838
+ const message = e instanceof Error ? e.message : String(e);
37839
+ evidence.push(`self-route threw: ${message}`);
37840
+ row.fix = `Hub-owned ${REPO_INDEX_RECONCILE_SCHEDULE_ID} \u2014 dispatch threw (${message}); run \`mmi-cli harbour org schedules run ${REPO_INDEX_RECONCILE_SCHEDULE_ID}\``;
37841
+ }
37842
+ } else {
37843
+ row.fix = `Hub-owned ${REPO_INDEX_RECONCILE_SCHEDULE_ID} \u2014 run \`mmi-cli harbour org schedules run ${REPO_INDEX_RECONCILE_SCHEDULE_ID}\`; orientation may continue`;
37844
+ }
37845
+ row.verbose = evidence;
37846
+ }
37847
+ emitNow(row);
35805
37848
  }
35806
37849
  async function runBoardDoctorRow() {
35807
37850
  if (!deps.boardDoctorFix) return;
@@ -35966,6 +38009,79 @@ async function runDoctorClean(opts, io, deps) {
35966
38009
  });
35967
38010
  }
35968
38011
  }
38012
+ async function runDeployPortCollisionsRow() {
38013
+ if (!deps.deployPortCollisionsState) return;
38014
+ try {
38015
+ const state = await deps.deployPortCollisionsState();
38016
+ if (!state) {
38017
+ emitNow({
38018
+ ok: true,
38019
+ warn: true,
38020
+ reportOnly: true,
38021
+ id: "deploy-port-collisions",
38022
+ label: "deploy port collisions",
38023
+ detail: "skipped",
38024
+ verbose: ["deployPortCollisionsState returned undefined"]
38025
+ });
38026
+ return;
38027
+ }
38028
+ if (state.forbidden) {
38029
+ emitNow({
38030
+ ok: true,
38031
+ reportOnly: true,
38032
+ id: "deploy-port-collisions",
38033
+ label: "deploy port collisions",
38034
+ detail: "skipped \u2014 master-admin only",
38035
+ verbose: ["deployPortCollisionsState returned 403"]
38036
+ });
38037
+ return;
38038
+ }
38039
+ if (state.readFailed) {
38040
+ emitNow({
38041
+ ok: false,
38042
+ warn: true,
38043
+ reportOnly: true,
38044
+ id: "deploy-port-collisions",
38045
+ label: "deploy port collisions",
38046
+ detail: `could not be read \u2014 ${state.error ?? "unknown error"}`,
38047
+ fix: "run `mmi-cli oracle org project deploy doctor`",
38048
+ verbose: [`deployPortCollisionsState failed: ${state.error ?? "unknown error"}`]
38049
+ });
38050
+ return;
38051
+ }
38052
+ if (state.ok) {
38053
+ emitNow({
38054
+ ok: true,
38055
+ reportOnly: true,
38056
+ id: "deploy-port-collisions",
38057
+ label: "deploy port collisions",
38058
+ detail: "no duplicate (sshHost, port)",
38059
+ verbose: ["no colliding DEPLOY# coordinates"]
38060
+ });
38061
+ return;
38062
+ }
38063
+ emitNow({
38064
+ ok: false,
38065
+ reportOnly: true,
38066
+ id: "deploy-port-collisions",
38067
+ label: "deploy port collisions",
38068
+ detail: `${state.lines.length} colliding coordinate(s)`,
38069
+ fix: "run `mmi-cli oracle org project deploy doctor` then set-deploy one owner to a free port",
38070
+ verbose: state.lines
38071
+ });
38072
+ } catch (e) {
38073
+ const message = e instanceof Error ? e.message : String(e);
38074
+ emitNow({
38075
+ ok: true,
38076
+ warn: true,
38077
+ reportOnly: true,
38078
+ id: "deploy-port-collisions",
38079
+ label: "deploy port collisions",
38080
+ detail: `skipped \u2014 ${message}`,
38081
+ verbose: [`deployPortCollisionsState threw: ${message}`]
38082
+ });
38083
+ }
38084
+ }
35969
38085
  async function runHousekeeperRows() {
35970
38086
  const repoRoot2 = await deps.repoRoot();
35971
38087
  const scratch = deps.executeScratchGc(repoRoot2, { apply: false });
@@ -36010,7 +38126,8 @@ async function runDoctorClean(opts, io, deps) {
36010
38126
  // Hub cloud probe (short timeout). Full lane + `--self`; `--preflight` may emit command-absent only (#4156).
36011
38127
  { id: "repo-index", when: isOrgRepo && (lane.full || Boolean(opts.self) || lane.preflight), run: runRepoIndexRow },
36012
38128
  { id: "board-doctor", when: isOrgRepo && lane.full, run: runBoardDoctorRow },
36013
- { id: "schedules-drift", when: isOrgRepo && lane.full, run: runSchedulesDriftRow }
38129
+ { id: "schedules-drift", when: isOrgRepo && lane.full, run: runSchedulesDriftRow },
38130
+ { id: "deploy-port-collisions", when: isOrgRepo && lane.full, run: runDeployPortCollisionsRow }
36014
38131
  ];
36015
38132
  await Promise.all(parallelRows.filter((entry) => entry.when).map((entry) => entry.run()));
36016
38133
  const suffix = [
@@ -36142,19 +38259,19 @@ function ghAccountCaveat(announcedLogin, accounts) {
36142
38259
  }
36143
38260
 
36144
38261
  // src/doctor-io.ts
36145
- var import_node_fs41 = require("node:fs");
38262
+ var import_node_fs42 = require("node:fs");
36146
38263
  var import_node_os18 = require("node:os");
36147
- var import_node_path38 = require("node:path");
36148
- var import_node_child_process18 = require("node:child_process");
38264
+ var import_node_path39 = require("node:path");
38265
+ var import_node_child_process19 = require("node:child_process");
36149
38266
  var import_node_util8 = require("node:util");
36150
38267
  init_version_lag();
36151
38268
  init_plugin_guard_io();
36152
- var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process18.execFile);
38269
+ var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process19.execFile);
36153
38270
  var MMI_PLUGIN_ID2 = "mmi@mutmutco";
36154
38271
  function installedClaudePluginVersion() {
36155
38272
  try {
36156
38273
  const file = JSON.parse(
36157
- (0, import_node_fs41.readFileSync)((0, import_node_path38.join)((0, import_node_os18.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
38274
+ (0, import_node_fs42.readFileSync)((0, import_node_path39.join)((0, import_node_os18.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
36158
38275
  );
36159
38276
  const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
36160
38277
  if (versions.length === 0) return void 0;
@@ -36165,7 +38282,7 @@ function installedClaudePluginVersion() {
36165
38282
  }
36166
38283
  function manifestVersion(path2) {
36167
38284
  try {
36168
- const manifest = JSON.parse((0, import_node_fs41.readFileSync)(path2, "utf8"));
38285
+ const manifest = JSON.parse((0, import_node_fs42.readFileSync)(path2, "utf8"));
36169
38286
  return typeof manifest.version === "string" && manifest.version.trim() ? manifest.version.trim() : void 0;
36170
38287
  } catch {
36171
38288
  return void 0;
@@ -36174,12 +38291,12 @@ function manifestVersion(path2) {
36174
38291
  function readHermesPluginEvidence(env = process.env) {
36175
38292
  const host = hermesConfigRoot(env);
36176
38293
  const root = hermesPluginRoot(env);
36177
- const installRecordPresent = (0, import_node_fs41.existsSync)(root);
36178
- const manifestPath = (0, import_node_path38.join)(root, "plugin.yaml");
38294
+ const installRecordPresent = (0, import_node_fs42.existsSync)(root);
38295
+ const manifestPath = (0, import_node_path39.join)(root, "plugin.yaml");
36179
38296
  let installedVersion;
36180
38297
  let manifest = "missing";
36181
38298
  try {
36182
- const text = (0, import_node_fs41.readFileSync)(manifestPath, "utf8");
38299
+ const text = (0, import_node_fs42.readFileSync)(manifestPath, "utf8");
36183
38300
  let version;
36184
38301
  try {
36185
38302
  const parsed = JSON.parse(text).version;
@@ -36193,20 +38310,20 @@ function readHermesPluginEvidence(env = process.env) {
36193
38310
  if (version) {
36194
38311
  installedVersion = version;
36195
38312
  manifest = "valid";
36196
- } else if ((0, import_node_fs41.existsSync)(manifestPath)) manifest = "invalid";
38313
+ } else if ((0, import_node_fs42.existsSync)(manifestPath)) manifest = "invalid";
36197
38314
  } catch {
36198
- if ((0, import_node_fs41.existsSync)(manifestPath)) manifest = "invalid";
38315
+ if ((0, import_node_fs42.existsSync)(manifestPath)) manifest = "invalid";
36199
38316
  }
36200
38317
  let skills = false;
36201
38318
  try {
36202
- skills = (0, import_node_fs41.existsSync)((0, import_node_path38.join)(root, "skills")) && (0, import_node_fs41.statSync)((0, import_node_path38.join)(root, "skills")).isDirectory();
38319
+ skills = (0, import_node_fs42.existsSync)((0, import_node_path39.join)(root, "skills")) && (0, import_node_fs42.statSync)((0, import_node_path39.join)(root, "skills")).isDirectory();
36203
38320
  } catch {
36204
38321
  }
36205
38322
  return {
36206
- hostPresent: (0, import_node_fs41.existsSync)(host),
38323
+ hostPresent: (0, import_node_fs42.existsSync)(host),
36207
38324
  installRecordPresent,
36208
38325
  manifest,
36209
- payloadPresent: (0, import_node_fs41.existsSync)((0, import_node_path38.join)(root, "__init__.py")) && skills && manifest === "valid",
38326
+ payloadPresent: (0, import_node_fs42.existsSync)((0, import_node_path39.join)(root, "__init__.py")) && skills && manifest === "valid",
36210
38327
  ...installedVersion ? { installedVersion } : {}
36211
38328
  };
36212
38329
  }
@@ -36214,7 +38331,7 @@ function installedSurfacePluginVersion(surface) {
36214
38331
  const token = surfaceToken(surface);
36215
38332
  if (token === "kilo") {
36216
38333
  try {
36217
- const stamp = (0, import_node_fs41.readFileSync)((0, import_node_path38.join)((0, import_node_os18.homedir)(), ".kilo", ".mmi-kilo-version"), "utf8").trim();
38334
+ const stamp = (0, import_node_fs42.readFileSync)((0, import_node_path39.join)((0, import_node_os18.homedir)(), ".kilo", ".mmi-kilo-version"), "utf8").trim();
36218
38335
  return stamp || void 0;
36219
38336
  } catch {
36220
38337
  return void 0;
@@ -36222,23 +38339,23 @@ function installedSurfacePluginVersion(surface) {
36222
38339
  }
36223
38340
  if (token === "hermes") return readHermesPluginEvidence().installedVersion;
36224
38341
  if (token === "cursor") {
36225
- return manifestVersion((0, import_node_path38.join)(cursorLocalPluginRoot(), ".cursor-plugin", "plugin.json"));
38342
+ return manifestVersion((0, import_node_path39.join)(cursorLocalPluginRoot(), ".cursor-plugin", "plugin.json"));
36226
38343
  }
36227
38344
  if (token === "jervcode") {
36228
38345
  return installedJervCodePackageVersion();
36229
38346
  }
36230
38347
  if (token === "kimi") {
36231
- return manifestVersion((0, import_node_path38.join)(surfaceConfigRoot(surface), "plugins", "managed", "mmi", ".kimi-plugin", "plugin.json"));
38348
+ return manifestVersion((0, import_node_path39.join)(surfaceConfigRoot(surface), "plugins", "managed", "mmi", ".kimi-plugin", "plugin.json"));
36232
38349
  }
36233
38350
  if (token === "claude") return installedClaudePluginVersion();
36234
38351
  if (token !== "codex") return void 0;
36235
38352
  try {
36236
- const raw = process.platform === "win32" ? (0, import_node_child_process18.execFileSync)("cmd.exe", ["/c", "codex", "plugin", "list", "--json"], {
38353
+ const raw = process.platform === "win32" ? (0, import_node_child_process19.execFileSync)("cmd.exe", ["/c", "codex", "plugin", "list", "--json"], {
36237
38354
  encoding: "utf8",
36238
38355
  stdio: ["ignore", "pipe", "ignore"],
36239
38356
  timeout: 15e3,
36240
38357
  windowsHide: true
36241
- }) : (0, import_node_child_process18.execFileSync)("codex", ["plugin", "list", "--json"], {
38358
+ }) : (0, import_node_child_process19.execFileSync)("codex", ["plugin", "list", "--json"], {
36242
38359
  encoding: "utf8",
36243
38360
  stdio: ["ignore", "pipe", "ignore"],
36244
38361
  timeout: 15e3,
@@ -36256,7 +38373,7 @@ function installedActivePluginVersion(surface = detectSurface(process.env)) {
36256
38373
  }
36257
38374
  function worktreeRootSync() {
36258
38375
  try {
36259
- const out = (0, import_node_child_process18.execFileSync)("git", ["rev-parse", "--show-toplevel"], { windowsHide: true, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
38376
+ const out = (0, import_node_child_process19.execFileSync)("git", ["rev-parse", "--show-toplevel"], { windowsHide: true, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
36260
38377
  let root = out.endsWith("\n") ? out.slice(0, -1) : out;
36261
38378
  if (process.platform === "win32" && root.endsWith("\r")) root = root.slice(0, -1);
36262
38379
  return root || null;
@@ -36266,13 +38383,13 @@ function worktreeRootSync() {
36266
38383
  }
36267
38384
  var gitignorePath = () => {
36268
38385
  const root = worktreeRootSync();
36269
- return root === null ? null : (0, import_node_path38.join)(root, ".gitignore");
38386
+ return root === null ? null : (0, import_node_path39.join)(root, ".gitignore");
36270
38387
  };
36271
38388
  function readGitignore() {
36272
38389
  const path2 = gitignorePath();
36273
38390
  if (path2 === null) return null;
36274
38391
  try {
36275
- return (0, import_node_fs41.readFileSync)(path2, "utf8");
38392
+ return (0, import_node_fs42.readFileSync)(path2, "utf8");
36276
38393
  } catch {
36277
38394
  return null;
36278
38395
  }
@@ -36281,16 +38398,16 @@ function writeGitignore(content) {
36281
38398
  const path2 = gitignorePath();
36282
38399
  if (path2 === null) return false;
36283
38400
  try {
36284
- (0, import_node_fs41.writeFileSync)(path2, content, "utf8");
38401
+ (0, import_node_fs42.writeFileSync)(path2, content, "utf8");
36285
38402
  return true;
36286
38403
  } catch {
36287
38404
  return false;
36288
38405
  }
36289
38406
  }
36290
38407
  function lineEndingState(root) {
36291
- const attributesPresent = (0, import_node_fs41.existsSync)((0, import_node_path38.join)(root, ".gitattributes"));
38408
+ const attributesPresent = (0, import_node_fs42.existsSync)((0, import_node_path39.join)(root, ".gitattributes"));
36292
38409
  try {
36293
- const output = (0, import_node_child_process18.execFileSync)("git", ["-C", root, "ls-files", "--eol", "--", ":(glob)**/*.sh"], {
38410
+ const output = (0, import_node_child_process19.execFileSync)("git", ["-C", root, "ls-files", "--eol", "--", ":(glob)**/*.sh"], {
36294
38411
  windowsHide: true,
36295
38412
  encoding: "utf8",
36296
38413
  stdio: ["ignore", "pipe", "ignore"]
@@ -36313,94 +38430,6 @@ async function repoRoot() {
36313
38430
  return worktreeRootSync() ?? process.cwd();
36314
38431
  }
36315
38432
 
36316
- // src/cross-repo-filing-issue.ts
36317
- init_github_client();
36318
- async function discoverSameOrgIssueNumber(client, currentRepo, number) {
36319
- const [owner] = currentRepo.split("/");
36320
- if (!owner) throw new Error(`cannot derive an organization from ${currentRepo}`);
36321
- const repositories = await client.restPaginate(`orgs/${owner}/repos?type=all`);
36322
- const hits = [];
36323
- let next = 0;
36324
- const worker = async () => {
36325
- while (next < repositories.length) {
36326
- const repo = repositories[next++]?.full_name;
36327
- if (!repo || repo.toLowerCase() === currentRepo.toLowerCase()) continue;
36328
- const issue2 = await readExactIssue(client, repo, number);
36329
- if (issue2) hits.push(issue2);
36330
- }
36331
- };
36332
- await Promise.all(Array.from({ length: Math.min(8, repositories.length) }, () => worker()));
36333
- return hits.sort((left, right) => left.repo.localeCompare(right.repo));
36334
- }
36335
- async function readExactIssue(client, repo, number) {
36336
- try {
36337
- const issue2 = await client.rest("GET", `repos/${repo}/issues/${number}`);
36338
- if (issue2.pull_request || issue2.number !== number || !issue2.title || !issue2.html_url) return void 0;
36339
- const state = issue2.state?.toUpperCase();
36340
- if (state !== "OPEN" && state !== "CLOSED") return void 0;
36341
- return { repo, number, title: issue2.title, url: issue2.html_url, state };
36342
- } catch (e) {
36343
- if (e instanceof GitHubApiError && e.status === 404) return void 0;
36344
- throw e;
36345
- }
36346
- }
36347
- function explicitSameOrgIssueUrls(text, prRepo, closingNumbers) {
36348
- const [owner] = prRepo.split("/");
36349
- const found = /* @__PURE__ */ new Map();
36350
- for (const match of text.matchAll(/https:\/\/github\.com\/([\w.-]+\/[\w.-]+)\/issues\/(\d+)\b/gi)) {
36351
- const repo = match[1];
36352
- const number = Number(match[2]);
36353
- if (!repo || !Number.isInteger(number) || !closingNumbers.has(number)) continue;
36354
- if (repo.split("/")[0]?.toLowerCase() !== owner.toLowerCase() || repo.toLowerCase() === prRepo.toLowerCase()) continue;
36355
- found.set(`${repo.toLowerCase()}#${number}`, { repo, number });
36356
- }
36357
- return [...found.values()];
36358
- }
36359
- async function reconcileCrossRepoFilingIssue(client, prRepo, prNumber) {
36360
- try {
36361
- const pr2 = await client.rest("GET", `repos/${prRepo}/pulls/${prNumber}`);
36362
- if (!pr2.merged_at) return { status: "not-applicable" };
36363
- const text = `${pr2.title ?? ""}
36364
- ${pr2.body ?? ""}`;
36365
- const closingNumbers = new Set(findClosingMentions(text).filter((mention) => !mention.negated).map((mention) => mention.issue));
36366
- if (closingNumbers.size === 0) return { status: "not-applicable" };
36367
- const explicit = explicitSameOrgIssueUrls(text, prRepo, closingNumbers);
36368
- let target;
36369
- if (explicit.length > 1) {
36370
- return { status: "failed", error: `multiple same-org issue URLs match bare closing references (${explicit.map((x) => `${x.repo}#${x.number}`).join(", ")})` };
36371
- }
36372
- if (explicit.length === 1) {
36373
- target = await readExactIssue(client, explicit[0].repo, explicit[0].number);
36374
- if (!target) return { status: "failed", issue: `${explicit[0].repo}#${explicit[0].number}`, error: "the explicit foreign issue URL did not resolve to an issue" };
36375
- } else {
36376
- const branchNumber = Number(/^#?(\d+)(?:-|$)/.exec(pr2.head?.ref ?? "")?.[1]);
36377
- if (!Number.isInteger(branchNumber) || !closingNumbers.has(branchNumber)) return { status: "not-applicable" };
36378
- if (await readExactIssue(client, prRepo, branchNumber)) return { status: "not-applicable" };
36379
- const matches = await discoverSameOrgIssueNumber(client, prRepo, branchNumber);
36380
- if (matches.length !== 1) {
36381
- return {
36382
- status: "failed",
36383
- error: matches.length === 0 ? `branch #${branchNumber} matches a bare closing reference, but no same-org filing issue was found` : `branch #${branchNumber} is ambiguous across ${matches.map((match) => match.repo).join(", ")}`
36384
- };
36385
- }
36386
- target = matches[0];
36387
- }
36388
- const issue2 = `${target.repo}#${target.number}`;
36389
- if (target.state === "CLOSED") return { status: "already-closed", issue: issue2, url: target.url };
36390
- const prUrl = pr2.html_url ?? `https://github.com/${prRepo}/pull/${prNumber}`;
36391
- const comment = await client.rest("POST", `repos/${target.repo}/issues/${target.number}/comments`, {
36392
- body: { body: `Resolved by merged PR ${prUrl}.
36393
-
36394
- <!-- mmi-cross-repo-close:${prRepo}#${prNumber} -->` }
36395
- });
36396
- if (!comment.html_url) throw new Error(`GitHub created no evidence URL on ${issue2}`);
36397
- await closeIssue(client, { ref: issue2, reason: "completed", evidence: comment.html_url });
36398
- return { status: "closed", issue: issue2, url: target.url, evidence: comment.html_url };
36399
- } catch (e) {
36400
- return { status: "failed", error: e.message };
36401
- }
36402
- }
36403
-
36404
38433
  // src/pr-deploy-dev.ts
36405
38434
  function slugOfRepo(repo) {
36406
38435
  return repo.split("/")[1]?.toLowerCase() ?? "";
@@ -36445,8 +38474,8 @@ ${r.stderr ?? ""}`).catch(() => "");
36445
38474
  function ghMultiAccountCaveat(announcedLogin) {
36446
38475
  try {
36447
38476
  const hostsPath = ghHostsConfigPath(process.env, process.platform);
36448
- if (!hostsPath || !(0, import_node_fs42.existsSync)(hostsPath)) return void 0;
36449
- return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0, import_node_fs42.readFileSync)(hostsPath, "utf8")));
38477
+ if (!hostsPath || !(0, import_node_fs43.existsSync)(hostsPath)) return void 0;
38478
+ return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0, import_node_fs43.readFileSync)(hostsPath, "utf8")));
36450
38479
  } catch {
36451
38480
  return void 0;
36452
38481
  }
@@ -36454,7 +38483,7 @@ function ghMultiAccountCaveat(announcedLogin) {
36454
38483
  var ENV_HEAL_LOCK_STALE_MS = 10 * 6e4;
36455
38484
  var ENV_HEAL_LOCK_MAX_WAIT_MS = 2 * 6e4;
36456
38485
  function envHealLockPath(home) {
36457
- return (0, import_node_path39.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
38486
+ return (0, import_node_path40.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
36458
38487
  }
36459
38488
  async function withEnvHealLock(what, run) {
36460
38489
  try {
@@ -36589,7 +38618,7 @@ function mmiDoctorDeps(opts = {}) {
36589
38618
  );
36590
38619
  const result = applyPluginCachePlan(
36591
38620
  plan,
36592
- (p) => (0, import_node_fs42.rmSync)(p, { recursive: true }),
38621
+ (p) => (0, import_node_fs43.rmSync)(p, { recursive: true }),
36593
38622
  stagingApplyFsGuard(configRoot)
36594
38623
  );
36595
38624
  return {
@@ -36627,7 +38656,7 @@ function mmiDoctorDeps(opts = {}) {
36627
38656
  // adopted the generated routing index (MMG-Unlive: `docs/Archive/**` only, no index, no gate.yml) would
36628
38657
  // get a permanent — demanding an artifact it never asked for.
36629
38658
  docsIndexState: (root) => {
36630
- if (!(0, import_node_fs42.existsSync)((0, import_node_path39.join)(root, DOCS_INDEX_PATH))) return void 0;
38659
+ if (!(0, import_node_fs43.existsSync)((0, import_node_path40.join)(root, DOCS_INDEX_PATH))) return void 0;
36631
38660
  const real = createDocsIndexDeps(root);
36632
38661
  let docs2;
36633
38662
  const listDocs = () => docs2 ??= real.listDocs();
@@ -36636,7 +38665,7 @@ function mmiDoctorDeps(opts = {}) {
36636
38665
  },
36637
38666
  // #4168: working-tree heal — write if drifted, then re-check. Commit remains the operator's step.
36638
38667
  healDocsIndex: (root) => {
36639
- if (!(0, import_node_fs42.existsSync)((0, import_node_path39.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
38668
+ if (!(0, import_node_fs43.existsSync)((0, import_node_path40.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
36640
38669
  const real = createDocsIndexDeps(root);
36641
38670
  let docs2;
36642
38671
  const listDocs = () => docs2 ??= real.listDocs();
@@ -36703,6 +38732,21 @@ function mmiDoctorDeps(opts = {}) {
36703
38732
  clearTimeout(ceiling);
36704
38733
  }
36705
38734
  },
38735
+ // #5431: estate DEPLOY# (sshHost, port) uniqueness — master-only Hub read; report-only.
38736
+ deployPortCollisionsState: async () => {
38737
+ try {
38738
+ const cfg = await loadConfig();
38739
+ const report = await fetchDeployPortCollisions(registryClientDeps(cfg));
38740
+ if (report.status === 403) return { ok: true, lines: [], forbidden: true };
38741
+ if (report.error) return { ok: false, lines: [], readFailed: true, error: report.error };
38742
+ return {
38743
+ ok: report.ok,
38744
+ lines: report.collisions.map((c) => c.report)
38745
+ };
38746
+ } catch (e) {
38747
+ return { ok: false, lines: [], readFailed: true, error: e instanceof Error ? e.message : String(e) };
38748
+ }
38749
+ },
36706
38750
  // #4173: estate commands absent from the running Commander program (stale global CLI).
36707
38751
  missingCliCommands: () => {
36708
38752
  const required = ["repo-index"];
@@ -36713,7 +38757,7 @@ function mmiDoctorDeps(opts = {}) {
36713
38757
  repoIndexCloudState: async (root) => {
36714
38758
  let localV4 = { state: "absent" };
36715
38759
  try {
36716
- const parsed = JSON.parse((0, import_node_fs42.readFileSync)(repoIndexV4StorePath(root), "utf8"));
38760
+ const parsed = JSON.parse((0, import_node_fs43.readFileSync)(repoIndexV4StorePath(root), "utf8"));
36717
38761
  const state = parsed.status?.state;
36718
38762
  if (parsed.schemaVersion === 4 && (state === "ready" || state === "degraded")) {
36719
38763
  const chunks = parsed.manifest?.chunks?.length ?? 0;
@@ -36765,6 +38809,21 @@ function mmiDoctorDeps(opts = {}) {
36765
38809
  ...localFields
36766
38810
  };
36767
38811
  }
38812
+ },
38813
+ // #5437: `--self` self-routes Hub-owned v4 readiness via the harbour reconcile schedule.
38814
+ dispatchRepoIndexReconcile: async () => {
38815
+ try {
38816
+ const res = await postSchedulesRun(REPO_INDEX_RECONCILE_SCHEDULE_ID, registryClientDeps(await loadConfig()));
38817
+ if (!res.ok) {
38818
+ return {
38819
+ ok: false,
38820
+ detail: res.error ?? `HTTP ${res.status}${res.body ? ` \u2014 ${JSON.stringify(res.body)}` : ""}`
38821
+ };
38822
+ }
38823
+ return { ok: true, detail: `accepted ${REPO_INDEX_RECONCILE_SCHEDULE_ID}` };
38824
+ } catch (e) {
38825
+ return { ok: false, detail: e instanceof Error ? e.message : String(e) };
38826
+ }
36768
38827
  }
36769
38828
  };
36770
38829
  }
@@ -36976,19 +39035,19 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
36976
39035
  });
36977
39036
  var rules = program2.command("rules").description("org-managed .gitignore delivery");
36978
39037
  rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").option("--json", "machine-readable output").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
36979
- const path2 = (0, import_node_path39.join)(process.cwd(), ".gitignore");
36980
- const current = (0, import_node_fs42.existsSync)(path2) ? (0, import_node_fs42.readFileSync)(path2, "utf8") : null;
39038
+ const path2 = (0, import_node_path40.join)(process.cwd(), ".gitignore");
39039
+ const current = (0, import_node_fs43.existsSync)(path2) ? (0, import_node_fs43.readFileSync)(path2, "utf8") : null;
36981
39040
  const plan = planManagedGitignore(current);
36982
39041
  const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
36983
39042
  if (opts.json) {
36984
- if (opts.write && plan.changed) (0, import_node_fs42.writeFileSync)(path2, plan.content, "utf8");
39043
+ if (opts.write && plan.changed) (0, import_node_fs43.writeFileSync)(path2, plan.content, "utf8");
36985
39044
  console.log(JSON.stringify(plan, null, 2));
36986
39045
  if (!opts.write && plan.changed) process.exitCode = 1;
36987
39046
  return;
36988
39047
  }
36989
39048
  if (opts.write) {
36990
39049
  if (plan.changed) {
36991
- (0, import_node_fs42.writeFileSync)(path2, plan.content, "utf8");
39050
+ (0, import_node_fs43.writeFileSync)(path2, plan.content, "utf8");
36992
39051
  console.log(`mmi-cli devops org rules gitignore: updated .gitignore (${drift})`);
36993
39052
  } else {
36994
39053
  console.log("mmi-cli devops org rules gitignore: up to date");
@@ -37126,8 +39185,14 @@ async function attachToProject(issueNumber, repo, priority) {
37126
39185
  await setBoardItemPriority(defaultGitHubClient(), cfg, projectItemId, priority);
37127
39186
  } catch (e) {
37128
39187
  const err = e;
37129
- process.stderr.write(`warning: issue #${issueNumber} board Priority not set: ${(err.stderr || err.message || String(e)).trim()}
39188
+ const detail = (err.stderr || err.message || String(e)).trim();
39189
+ if (isRateLimitText(detail)) {
39190
+ process.stderr.write(`warning: issue #${issueNumber} on board but Priority not set (rate-limited): ${detail}
39191
+ `);
39192
+ } else {
39193
+ process.stderr.write(`warning: issue #${issueNumber} board Priority not set: ${detail}
37130
39194
  `);
39195
+ }
37131
39196
  }
37132
39197
  }
37133
39198
  return { projectItemId, onBoard: true };
@@ -37146,6 +39211,18 @@ async function attachToProject(issueNumber, repo, priority) {
37146
39211
  }
37147
39212
  return { onBoard: true };
37148
39213
  }
39214
+ if (isRateLimitText(detail)) {
39215
+ process.stderr.write(`warning: issue #${issueNumber} created but board attach rate-limited: ${detail}
39216
+ `);
39217
+ let resetEpochSeconds;
39218
+ try {
39219
+ const { stdout } = await execFileP2("gh", ["api", "rate_limit"], { timeout: 15e3 });
39220
+ const pools = parseRateLimitPools(stdout);
39221
+ resetEpochSeconds = pools.graphql?.reset ?? pools.core?.reset;
39222
+ } catch {
39223
+ }
39224
+ return boardAttachRateLimitedReceipt(resetEpochSeconds);
39225
+ }
37149
39226
  process.stderr.write(`warning: issue #${issueNumber} created but NOT added to the project board: ${detail}
37150
39227
  `);
37151
39228
  return { onBoard: false };
@@ -37156,7 +39233,7 @@ function scheduleRelatedDiscovery(o) {
37156
39233
  try {
37157
39234
  const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body, "--fail-soft"];
37158
39235
  if (o.repo) args.push("--repo", o.repo);
37159
- spawnDetachedSelf(args, { spawn: import_node_child_process19.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
39236
+ spawnDetachedSelf(args, { spawn: import_node_child_process20.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
37160
39237
  } catch {
37161
39238
  }
37162
39239
  }
@@ -37231,12 +39308,57 @@ async function runRepoIndexSearchCommand(query, o, defaultMode) {
37231
39308
  }
37232
39309
  const mode = o.semantic ? "semantic" : o.lexical ? "lexical" : defaultMode;
37233
39310
  const cfg = await loadConfig();
37234
- const res = await searchRepoIndexCloud(query, { mode, limit, repo: o.repo }, registryClientDeps(cfg));
37235
- if (!res.ok) return await failGraceful(res.error);
39311
+ const deps = registryClientDeps(cfg);
39312
+ let res = await searchRepoIndexCloud(query, { mode, limit, repo: o.repo }, deps);
39313
+ if (!res.ok && shouldRetryRepoIndexSearchLexical(res, mode)) {
39314
+ const lexical = await searchRepoIndexCloud(query, { mode: "lexical", limit, repo: o.repo }, deps);
39315
+ if (lexical.ok) {
39316
+ res = {
39317
+ ...lexical,
39318
+ degradedReasons: [...lexical.degradedReasons ?? [], "query-embedding-unavailable"],
39319
+ note: [
39320
+ lexical.note,
39321
+ "query embedding unavailable \u2014 served lexical citations (v4 semantic not claimed ready)"
39322
+ ].filter(Boolean).join("; ")
39323
+ };
39324
+ }
39325
+ }
39326
+ if (!res.ok) {
39327
+ const receipt = asCloudSearchNotReadyReceipt(res);
39328
+ const typed = refuseBareRepoIndexV4Unavailable(receipt.error);
39329
+ if (o.json) {
39330
+ consoleIo.log(JSON.stringify({ source: "cloud", ...receipt, error: typed }, null, 2));
39331
+ } else {
39332
+ console.error(`mmi-cli ${typed}`);
39333
+ const authority = receipt.v4Authority;
39334
+ if (authority) {
39335
+ const ready = authority.readyAuthorities ?? 0;
39336
+ const roster = authority.rosterCount ?? 0;
39337
+ console.error(`repo-index: v4 status not-ready \u2014 readyAuthorities=${ready}/${roster} nOfN=${authority.nOfN === true}`);
39338
+ } else {
39339
+ console.error("repo-index: v4 status not-ready \u2014 Hub search unavailable (see cause above)");
39340
+ }
39341
+ if (authority?.authorityFailures?.length) {
39342
+ for (const failure of authority.authorityFailures.slice(0, 12)) {
39343
+ console.error(`repo-index: authority ${failure.repo} ${failure.reason}`);
39344
+ }
39345
+ if (authority.authorityFailures.length > 12) {
39346
+ console.error(`repo-index: \u2026 ${authority.authorityFailures.length - 12} more authority failure(s)`);
39347
+ }
39348
+ }
39349
+ if (receipt.omittedRepos?.length) {
39350
+ console.error(`repo-index: omittedRepos ${receipt.omittedRepos.slice(0, 8).join(", ")}${receipt.omittedRepos.length > 8 ? "\u2026" : ""}`);
39351
+ }
39352
+ if (receipt.next) console.error(`repo-index: next \u2014 ${receipt.next}`);
39353
+ console.error("repo-index: status remains not-ready until reconcile repairs authorities (search did not claim v4 ready)");
39354
+ }
39355
+ return await cleanExit(1);
39356
+ }
37236
39357
  if (o.json) {
37237
39358
  consoleIo.log(JSON.stringify({ source: "cloud", ...res }, null, 2));
37238
39359
  return;
37239
39360
  }
39361
+ if (res.note) console.error(`repo-index: ${res.note}`);
37240
39362
  if (res.hits.length === 0) {
37241
39363
  console.log(`repo-index: no cloud hits for ${JSON.stringify(query)} (${res.mode})`);
37242
39364
  return;
@@ -37246,7 +39368,7 @@ async function runRepoIndexSearchCommand(query, o, defaultMode) {
37246
39368
  console.log(`${h.score.toFixed(2)} ${h.kind.padEnd(8)} ${h.repo} ${h.path}${sym} (${h.why})`);
37247
39369
  }
37248
39370
  } catch (e) {
37249
- return await failGraceful(e.message);
39371
+ return await failGraceful(refuseBareRepoIndexV4Unavailable(e.message));
37250
39372
  }
37251
39373
  }
37252
39374
  var REPO_INDEX_SEARCH_WHEN = 'beats grep for cross-repo questions, "where does X happen", and unknown filenames; grep wins inside a known checkout';
@@ -37357,7 +39479,7 @@ repoIndex.command("gc").description("remove v4 cloud authority material for repo
37357
39479
  return await failGraceful(e.message);
37358
39480
  }
37359
39481
  });
37360
- repoIndex.command("sync-estate").description("Hub indexer: shallow-clone registry repos, build and publish v4 authorities (CI / operator)").option("--repo <owner/name>", "limit to one registered repo").option("--json", "machine-readable result").action(async (o) => {
39482
+ repoIndex.command("sync-estate").description("Hub indexer: publish a pushed commit as a delta, or run the estate drift/repair lane (CI / operator)").option("--repo <owner/name>", "limit to one registered repo").option("--commit <sha>", "index this exact 40-hex commit (requires --repo); the checkout is proved against it").option("--full-rebuild <token>", `explicit full rebuild \u2014 must quote this pipeline's provenance token (${CURRENT_REPO_INDEX_PROVENANCE_TOKEN})`).option("--skip-repo <owner/name>", "estate lane: a repo a per-repo run is already publishing (repeatable)", (value, all) => [...all, value], []).option("--plan", "read-only: classify every repo and print the plan without cloning, embedding or publishing").option("--json", "machine-readable result").action(async (o) => {
37361
39483
  try {
37362
39484
  const cfg = await loadConfig();
37363
39485
  const gh = process.env.GH_TOKEN || process.env.GITHUB_TOKEN || "";
@@ -37365,6 +39487,10 @@ repoIndex.command("sync-estate").description("Hub indexer: shallow-clone registr
37365
39487
  const res = await syncEstateRepoIndex({
37366
39488
  deps: registryClientDeps(cfg),
37367
39489
  repo: o.repo,
39490
+ commit: o.commit,
39491
+ fullRebuild: o.fullRebuild,
39492
+ skipRepos: o.skipRepo,
39493
+ plan: Boolean(o.plan),
37368
39494
  githubToken: gh
37369
39495
  });
37370
39496
  if (o.json) {
@@ -37372,11 +39498,22 @@ repoIndex.command("sync-estate").description("Hub indexer: shallow-clone registr
37372
39498
  if (!res.ok) process.exitCode = 1;
37373
39499
  return;
37374
39500
  }
39501
+ console.log(`repo-index: pipeline provenance ${res.provenanceToken} \u2014 delta is the normal path${o.plan ? " (plan only: nothing was cloned, embedded or published)" : ""}`);
39502
+ for (const row of res.drift) {
39503
+ if (row.reason === "healthy") continue;
39504
+ const at = row.targetCommit ? ` target=${row.targetCommit.slice(0, 12)}` : "";
39505
+ const active = row.activeCommit ? ` active=${row.activeCommit.slice(0, 12)}` : "";
39506
+ console.log(`repo-index: ${row.repo} ${row.reason} \u2192 ${row.action}${active}${at}`);
39507
+ }
37375
39508
  for (const p of res.published) {
37376
39509
  const gap = p.embGap ?? Math.max(0, p.fileCount - (p.embCount ?? 0));
37377
39510
  console.log(`repo-index: published v4 ${p.repo} \u2014 ${p.fileCount} chunks emb=${p.embCount ?? 0}/${p.fileCount} gap=${gap} graph=${p.graphEdges ?? "unavailable"}`);
39511
+ if (p.metrics) console.log(`repo-index: ${formatV4BuildMetrics(p.repo, p.metrics)}`);
37378
39512
  }
37379
39513
  for (const warning of res.skipped) console.error(`repo-index: WARN ${warning}`);
39514
+ if (res.needsFullRebuild.length) {
39515
+ console.error(`repo-index: ${res.needsFullRebuild.length} repo(s) need an explicit tokened migration: ${res.needsFullRebuild.join(", ")}`);
39516
+ }
37380
39517
  for (const f of res.failed) {
37381
39518
  console.error(`repo-index: FAILED ${f.repo}: ${f.error}`);
37382
39519
  }
@@ -37724,6 +39861,17 @@ projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# fa
37724
39861
  const payload = stage ? { slug: out.slug, stage, deploy: out.stages[stage] ?? null } : out;
37725
39862
  console.log(JSON.stringify(payload));
37726
39863
  });
39864
+ projectDeploy.command("doctor").description("read-only estate scan for duplicate Hetzner (sshHost, port) DEPLOY# coordinates (#5431); master-only; never auto-reassigns ports").option("--json", "machine-readable output").action(async (o) => {
39865
+ const cfg = await loadConfig();
39866
+ const report = await fetchDeployPortCollisions(registryClientDeps(cfg));
39867
+ if (o.json) {
39868
+ printLine(JSON.stringify(report, null, 2));
39869
+ } else {
39870
+ for (const line of renderDeployPortDoctor(report)) printLine(line);
39871
+ }
39872
+ if (report.error && report.status !== 403) return failGraceful(`org project deploy doctor: ${report.error}`);
39873
+ if (!report.ok && !report.error) process.exitCode = 1;
39874
+ });
37727
39875
  project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").addOption(new Option("--class <class>", "deployable | content").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", 'read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (clear all with --unset secrets). SHAPE: a JSON object keyed by env name, each entry {"key":"UPPER_SNAKE" (repeat the env name), "purpose":"<non-empty>", "group":"<e.g. auth|database>", "owner":"<github login>", "stages":[] (empty = the one stageless shared value; else any of dev/rc/main), "consumers":["runtime"|"box"|?], "provider":"<optional>"}').option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets, edgeDomains, requiredGcpApis, publishRequired").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
37728
39876
  const cfg = await loadConfig();
37729
39877
  let target;
@@ -37740,7 +39888,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
37740
39888
  if (dupe) return fail(`org project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
37741
39889
  if (o.secretsFile) {
37742
39890
  try {
37743
- vars.push(`secrets=${(0, import_node_fs42.readFileSync)(o.secretsFile, "utf8")}`);
39891
+ vars.push(`secrets=${(0, import_node_fs43.readFileSync)(o.secretsFile, "utf8")}`);
37744
39892
  } catch (e) {
37745
39893
  return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
37746
39894
  }
@@ -38090,7 +40238,7 @@ function resolveCreateSurface(opts) {
38090
40238
  function surfaceWaived() {
38091
40239
  return rawFlag("--no-surface");
38092
40240
  }
38093
- var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
40241
+ var issue = program2.command("issue").description("issues \u2014 create and view with structured JSON (view; show is an alias for board-verb callers)");
38094
40242
  withExamples(mutating(
38095
40243
  issue.command("create").description("create an issue (type \u2014 label) and print {number,url,label} JSON").addOption(new Option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").choices([...ISSUE_TYPES])).option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file. `-` (stdin) needs a heredoc, which the agent inline-body guard denies (#1473/#2125) \u2014 prefer a real path; a title with backticks needs --title-file for the same reason (#3381)").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--surface <surface>", "issue surface, with or without the surface: prefix (#3789). Required when the target repo runs the one-surface-label board rule; any value satisfies it, so this is not a closed enum").option("--no-surface", "file without a surface label on a repo that requires one \u2014 for a genuinely exempt filing (e.g. a coop proof issue that spans every surface)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment"),
38096
40244
  // --dry-run/--validate-only plan: resolve the same title source and validate the same type, priority,
@@ -38174,10 +40322,11 @@ withExamples(mutating(
38174
40322
  return fail(`issue create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
38175
40323
  }
38176
40324
  {
38177
- const { refusal, warn, enforcing } = await checkSurfaceRequirement({ repo: targetRepo3, labels: extraLabels });
40325
+ const surfaceCheck = await checkSurfaceRequirement({ repo: targetRepo3, labels: extraLabels });
40326
+ const { refusal, warn } = surfaceCheck;
38178
40327
  if (warn) process.stderr.write(`${warn}
38179
40328
  `);
38180
- if (!enforcing && surfaceFlagLabel) {
40329
+ if (shouldWithdrawSurfaceFlag(surfaceCheck) && surfaceFlagLabel) {
38181
40330
  extraLabels = extraLabels.filter((l) => l !== surfaceFlagLabel);
38182
40331
  args = buildIssueArgs({
38183
40332
  type: issueType,
@@ -38196,7 +40345,13 @@ withExamples(mutating(
38196
40345
  }
38197
40346
  await ensureLabelsExist(extraLabels, targetRepo3);
38198
40347
  const created = await ghCreate(args);
38199
- const { projectItemId, onBoard } = await attachToProject(created.number, targetRepo3, priority);
40348
+ if (isGhCreateRateLimited(created)) {
40349
+ console.log(JSON.stringify(created));
40350
+ process.exitCode = 1;
40351
+ return;
40352
+ }
40353
+ const attached = await attachToProject(created.number, targetRepo3, priority);
40354
+ const { projectItemId, onBoard } = attached;
38200
40355
  let parent;
38201
40356
  let parentLinkError;
38202
40357
  if (o.parent !== void 0) {
@@ -38217,6 +40372,12 @@ withExamples(mutating(
38217
40372
  priority,
38218
40373
  projectItemId,
38219
40374
  onBoard,
40375
+ // #5489: partial receipt when the GitHub issue landed but Project v2 attach hit GraphQL quota.
40376
+ ...attached.boardAttach ? {
40377
+ boardAttach: attached.boardAttach,
40378
+ ...attached.resetAt ? { resetAt: attached.resetAt } : {},
40379
+ ...attached.resetEpochSeconds !== void 0 ? { resetEpochSeconds: attached.resetEpochSeconds } : {}
40380
+ } : {},
38220
40381
  ...parentLinkFields(parent, parentLinkError)
38221
40382
  }));
38222
40383
  }), [
@@ -38238,7 +40399,7 @@ async function readParentField(number, repo) {
38238
40399
  }
38239
40400
  return resolveParentField(payload);
38240
40401
  }
38241
- issue.command("view <number>").description('read an issue as structured JSON \u2014 the mmi-cli path for non-board issue reads (#2347). --comments folds in every comment; --context also adds linkedPrs + children (the one-shot "load the whole item" read, #2894)').option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "number,title,body,url"').option("--comments", 'include every comment (body + comments in one call) \u2014 the agentic "read the whole item before working it" read (#2894)').option("--context", "full working context in one call: implies --comments and also adds linkedPrs and, for an epic, a children summary (#2894)").action(async (number, o) => {
40402
+ issue.command("view <number>").alias("show").description('read an issue as structured JSON \u2014 the mmi-cli path for non-board issue reads (#2347). --comments folds in every comment; --context also adds linkedPrs + children (the one-shot "load the whole item" read, #2894). `show` is an alias (#5483)').option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "number,title,body,url"').option("--comments", 'include every comment (body + comments in one call) \u2014 the agentic "read the whole item before working it" read (#2894)').option("--context", "full working context in one call: implies --comments and also adds linkedPrs and, for an epic, a children summary (#2894)").action(async (number, o) => {
38242
40403
  const n = Number(number);
38243
40404
  if (!Number.isInteger(n) || n <= 0) return fail("issue view: <number> must be a positive integer");
38244
40405
  const repo = await resolveRepo(o.repo);
@@ -38495,9 +40656,8 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
38495
40656
  } catch {
38496
40657
  }
38497
40658
  }
38498
- const created = await ghCreate(args);
40659
+ const created = requireGhCreateOk(await ghCreate(args), "skill-lesson");
38499
40660
  const { projectItemId, onBoard } = await attachToProject(created.number, targetRepo3, priority);
38500
- console.log(JSON.stringify({ ...created, deduped: false, label: SKILL_LESSON_LABEL, skill, priority, projectItemId, onBoard }));
38501
40661
  });
38502
40662
  var pr = program2.command("pr").description("pull requests \u2014 reliable create with structured output");
38503
40663
  withExamples(pr.command("create").description("create a PR and print {number,url} JSON").option("--title <title>", "PR title").option("--title-file <path|->", "read the PR title from a UTF-8 file, or from stdin with -").option("--body <body>", "PR body (markdown)").option("--body-file <path|->", "read PR body from a UTF-8 file, or from stdin with -").option("--base <branch>", "base branch (defaults to the repo default)").option("--head <branch>", "head branch (defaults to the current branch)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--draft", "open the PR in draft state (#2667)").option("--json", "machine-readable output (default; accepted for parity)").action(async (o) => {
@@ -38516,6 +40676,11 @@ withExamples(pr.command("create").description("create a PR and print {number,url
38516
40676
  return fail(`pr create: ${docsCheck.detail} \u2014 ${docsCheck.fix}`);
38517
40677
  }
38518
40678
  const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo, draft: o.draft }));
40679
+ if (isGhCreateRateLimited(created)) {
40680
+ console.log(JSON.stringify(created));
40681
+ process.exitCode = 1;
40682
+ return;
40683
+ }
38519
40684
  invalidateStatuslineBoardCache();
38520
40685
  console.log(JSON.stringify(created));
38521
40686
  }), [
@@ -38547,11 +40712,11 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
38547
40712
  }
38548
40713
  });
38549
40714
  async function listCiWorkflowPaths(cwd = process.cwd()) {
38550
- const wfDir = (0, import_node_path39.join)(cwd, ".github", "workflows");
38551
- if (!(0, import_node_fs42.existsSync)(wfDir)) return [];
38552
- return (0, import_node_fs42.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
40715
+ const wfDir = (0, import_node_path40.join)(cwd, ".github", "workflows");
40716
+ if (!(0, import_node_fs43.existsSync)(wfDir)) return [];
40717
+ return (0, import_node_fs43.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
38553
40718
  try {
38554
- return workflowReportsPrChecks((0, import_node_fs42.readFileSync)((0, import_node_path39.join)(wfDir, name), "utf8"));
40719
+ return workflowReportsPrChecks((0, import_node_fs43.readFileSync)((0, import_node_path40.join)(wfDir, name), "utf8"));
38555
40720
  } catch {
38556
40721
  return true;
38557
40722
  }
@@ -38603,16 +40768,16 @@ function ciAuditDeps() {
38603
40768
  // gate re-seed step is skipped gracefully rather than failing mid-run.
38604
40769
  readSeedFile: (path2) => {
38605
40770
  if (!root) return null;
38606
- const fullPath = (0, import_node_path39.join)(root, path2);
38607
- return (0, import_node_fs42.existsSync)(fullPath) ? (0, import_node_fs42.readFileSync)(fullPath, "utf8") : null;
40771
+ const fullPath = (0, import_node_path40.join)(root, path2);
40772
+ return (0, import_node_fs43.existsSync)(fullPath) ? (0, import_node_fs43.readFileSync)(fullPath, "utf8") : null;
38608
40773
  }
38609
40774
  };
38610
40775
  }
38611
40776
  function hubRoot() {
38612
- const fromPkg = (0, import_node_path39.join)(__dirname, "..", "..");
40777
+ const fromPkg = (0, import_node_path40.join)(__dirname, "..", "..");
38613
40778
  const marker = "skills/bootstrap/seeds/manifest.json";
38614
- if ((0, import_node_fs42.existsSync)((0, import_node_path39.join)(fromPkg, marker))) return fromPkg;
38615
- if ((0, import_node_fs42.existsSync)((0, import_node_path39.join)(process.cwd(), marker))) return process.cwd();
40779
+ if ((0, import_node_fs43.existsSync)((0, import_node_path40.join)(fromPkg, marker))) return fromPkg;
40780
+ if ((0, import_node_fs43.existsSync)((0, import_node_path40.join)(process.cwd(), marker))) return process.cwd();
38616
40781
  return null;
38617
40782
  }
38618
40783
  async function waitLoopCorePool(label) {
@@ -38834,16 +40999,36 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
38834
40999
  if (result.status !== "failed") {
38835
41000
  const boardAdvance = await advanceClosedIssuesToDone2(number, result.repo);
38836
41001
  const crossRepoFilingIssue = result.repo ? await reconcileCrossRepoFilingIssue(defaultGitHubClient(), result.repo, Number(number)) : { status: "failed", error: "could not resolve the PR repo for cross-repo filing-issue reconciliation" };
41002
+ const recovery = result.repo ? buildPostMergeReconRecovery({
41003
+ repo: result.repo,
41004
+ pr: number,
41005
+ boardAdvance,
41006
+ crossRepoFilingIssue
41007
+ }) : void 0;
41008
+ let recoveryPath;
41009
+ if (recovery) {
41010
+ try {
41011
+ recoveryPath = writePostMergeReconRecovery(process.cwd(), recovery);
41012
+ } catch {
41013
+ }
41014
+ } else if (result.repo) {
41015
+ clearPostMergeReconRecovery(process.cwd(), result.repo, number);
41016
+ }
38837
41017
  Object.assign(result, {
38838
41018
  ...boardAdvance.entries.length ? { boardAdvance: boardAdvance.entries } : {},
38839
41019
  boardAdvanceStatus: boardAdvance.status,
38840
- ...crossRepoFilingIssue.status !== "not-applicable" ? { crossRepoFilingIssue } : {}
41020
+ ...crossRepoFilingIssue.status !== "not-applicable" ? { crossRepoFilingIssue } : {},
41021
+ ...recovery ? { postMergeReconRecovery: recovery } : {}
38841
41022
  });
38842
41023
  invalidateStatuslineBoardCache();
38843
- const boardAdvanceMessage = boardAdvanceFailureMessage(boardAdvance);
38844
- if (boardAdvanceMessage) console.error(boardAdvanceMessage);
38845
- if (crossRepoFilingIssue.status === "failed") {
38846
- console.error(`pr land: cross-repo filing-issue reconciliation failed (${crossRepoFilingIssue.error}) \u2014 the PR MERGED, but a foreign filing issue may remain open.`);
41024
+ for (const line of postMergeReconWarnings({
41025
+ context: "pr land",
41026
+ boardAdvance,
41027
+ crossRepoFilingIssue,
41028
+ recovery,
41029
+ recoveryPath
41030
+ })) {
41031
+ console.error(line);
38847
41032
  }
38848
41033
  }
38849
41034
  if (o.json) printLine(JSON.stringify(result));
@@ -38936,7 +41121,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
38936
41121
  }
38937
41122
  if (!repoForPostCleanup) throw e;
38938
41123
  console.warn(`pr merge: gh GraphQL rate-limited \u2014 merging PR #${number} via REST PUT instead (#4588).`);
38939
- const commitMessage = bodyFile ? (0, import_node_fs42.readFileSync)(bodyFile, "utf8") : void 0;
41124
+ const commitMessage = bodyFile ? (0, import_node_fs43.readFileSync)(bodyFile, "utf8") : void 0;
38940
41125
  await defaultGitHubClient().rest("PUT", `repos/${repoForPostCleanup}/pulls/${number}/merge`, {
38941
41126
  body: { merge_method: method.slice(2), ...commitMessage ? { commit_message: commitMessage } : {} },
38942
41127
  timeoutMs: GH_MUTATION_TIMEOUT_MS
@@ -39016,6 +41201,21 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
39016
41201
  const remoteBranch = { branch: headRef, existedBefore: remoteBefore, attempted: false, reason: remoteNotAttemptedReason };
39017
41202
  const boardAdvance = await advanceClosedIssuesToDone2(number, repoForPostCleanup);
39018
41203
  const crossRepoFilingIssue = repoForPostCleanup ? await reconcileCrossRepoFilingIssue(defaultGitHubClient(), repoForPostCleanup, Number(number)) : { status: "failed", error: "could not resolve the PR repo for cross-repo filing-issue reconciliation" };
41204
+ const recovery = repoForPostCleanup ? buildPostMergeReconRecovery({
41205
+ repo: repoForPostCleanup,
41206
+ pr: number,
41207
+ boardAdvance,
41208
+ crossRepoFilingIssue
41209
+ }) : void 0;
41210
+ let recoveryPath;
41211
+ if (recovery) {
41212
+ try {
41213
+ recoveryPath = writePostMergeReconRecovery(process.cwd(), recovery);
41214
+ } catch {
41215
+ }
41216
+ } else if (repoForPostCleanup) {
41217
+ clearPostMergeReconRecovery(process.cwd(), repoForPostCleanup, number);
41218
+ }
39019
41219
  invalidateStatuslineBoardCache();
39020
41220
  const devDeploy = devDeployPlan.applicable && devDeployDeps && remoteNotAttemptedReason !== "pr-already-merged" ? await dispatchDevDeploy(repoForPostCleanup, devDeployDeps) : void 0;
39021
41221
  const devDeployManual = devDeployPlan.applicable ? devDeployPlan.manualPointer : void 0;
@@ -39047,18 +41247,22 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
39047
41247
  ...boardAdvance.entries.length ? { boardAdvance: boardAdvance.entries } : {},
39048
41248
  boardAdvanceStatus: boardAdvance.status,
39049
41249
  ...crossRepoFilingIssue.status !== "not-applicable" ? { crossRepoFilingIssue } : {},
41250
+ ...recovery ? { postMergeReconRecovery: recovery } : {},
39050
41251
  ...devDeploy ? { devDeploy: devDeploy.ok ? { dispatched: true } : { dispatched: false, manual: devDeployManual } } : {}
39051
41252
  }));
39052
- const boardAdvanceMessage = boardAdvanceFailureMessage(boardAdvance);
39053
- if (boardAdvanceMessage) console.error(boardAdvanceMessage);
39054
41253
  if (devDeploy && !devDeploy.ok) {
39055
41254
  console.error(`pr merge: ${devDeployManual} (${devDeploy.detail})`);
39056
41255
  }
39057
- if (crossRepoFilingIssue.status === "failed") {
39058
- console.error(`pr merge: cross-repo filing-issue reconciliation failed (${crossRepoFilingIssue.error}) \u2014 the PR MERGED, but a foreign filing issue may remain open.`);
41256
+ for (const line of postMergeReconWarnings({
41257
+ context: "pr merge",
41258
+ boardAdvance,
41259
+ crossRepoFilingIssue,
41260
+ recovery,
41261
+ recoveryPath
41262
+ })) {
41263
+ console.error(line);
39059
41264
  }
39060
- process.exitCode = boardAdvanceExitCode(boardAdvance) ?? process.exitCode;
39061
- if (crossRepoFilingIssue.status === "failed") process.exitCode = 1;
41265
+ process.exitCode = postMergeReconExitCode({ boardAdvance, crossRepoFilingIssue }) ?? process.exitCode;
39062
41266
  });
39063
41267
  registerQueryCommands(program2);
39064
41268
  registerIssueLifecycleCommands(program2, { attach: attachToProject });
@@ -39613,12 +41817,12 @@ access.command("audit").description("audit collaborator roles + train-branch pus
39613
41817
  targets = resolution.targets;
39614
41818
  }
39615
41819
  const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
39616
- const fileMatrix = (0, import_node_fs42.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs42.readFileSync)("access-matrix.json", "utf8")) : {};
41820
+ const fileMatrix = (0, import_node_fs43.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs43.readFileSync)("access-matrix.json", "utf8")) : {};
39617
41821
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
39618
41822
  const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
39619
- const fileContracts = (0, import_node_fs42.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs42.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
41823
+ const fileContracts = (0, import_node_fs43.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs43.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
39620
41824
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
39621
- const sanctioned = (0, import_node_fs42.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs42.readFileSync)("access-matrix.json", "utf8")) : {};
41825
+ const sanctioned = (0, import_node_fs43.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs43.readFileSync)("access-matrix.json", "utf8")) : {};
39622
41826
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess, sanctioned);
39623
41827
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
39624
41828
  if (!report.ok) process.exitCode = 1;
@@ -39650,16 +41854,16 @@ function directoryBytes(path2) {
39650
41854
  let total = 0;
39651
41855
  let entries;
39652
41856
  try {
39653
- entries = (0, import_node_fs42.readdirSync)(path2, { withFileTypes: true });
41857
+ entries = (0, import_node_fs43.readdirSync)(path2, { withFileTypes: true });
39654
41858
  } catch {
39655
41859
  return 0;
39656
41860
  }
39657
41861
  for (const entry of entries) {
39658
- const child2 = (0, import_node_path39.join)(path2, entry.name);
41862
+ const child2 = (0, import_node_path40.join)(path2, entry.name);
39659
41863
  if (entry.isDirectory()) total += directoryBytes(child2);
39660
41864
  else {
39661
41865
  try {
39662
- total += (0, import_node_fs42.statSync)(child2).size;
41866
+ total += (0, import_node_fs43.statSync)(child2).size;
39663
41867
  } catch {
39664
41868
  }
39665
41869
  }
@@ -39667,25 +41871,25 @@ function directoryBytes(path2) {
39667
41871
  return total;
39668
41872
  }
39669
41873
  function listDirEntries(dir) {
39670
- return (0, import_node_fs42.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
41874
+ return (0, import_node_fs43.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
39671
41875
  }
39672
41876
  function readInstalledPluginRefs(configRoot) {
39673
41877
  const p = installedPluginsPathForConfig(configRoot);
39674
- if (!(0, import_node_fs42.existsSync)(p)) return [];
41878
+ if (!(0, import_node_fs43.existsSync)(p)) return [];
39675
41879
  try {
39676
- return installedPluginPaths((0, import_node_fs42.readFileSync)(p, "utf8"));
41880
+ return installedPluginPaths((0, import_node_fs43.readFileSync)(p, "utf8"));
39677
41881
  } catch {
39678
41882
  return null;
39679
41883
  }
39680
41884
  }
39681
41885
  function pluginCacheFsDeps(configRoot, dirBytes) {
39682
41886
  return {
39683
- exists: (p) => (0, import_node_fs42.existsSync)(p),
39684
- listVersionDirs: (root) => (0, import_node_fs42.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
41887
+ exists: (p) => (0, import_node_fs43.existsSync)(p),
41888
+ listVersionDirs: (root) => (0, import_node_fs43.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
39685
41889
  dirBytes,
39686
- listStagingDirs: (root) => (0, import_node_fs42.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
41890
+ listStagingDirs: (root) => (0, import_node_fs43.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
39687
41891
  try {
39688
- return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path39.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs42.statSync)(p).mtimeMs) };
41892
+ return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path40.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs43.statSync)(p).mtimeMs) };
39689
41893
  } catch {
39690
41894
  return { name: d.name, mtimeMs: Date.now() };
39691
41895
  }
@@ -39699,10 +41903,10 @@ function stagingApplyFsGuard(configRoot) {
39699
41903
  return {
39700
41904
  referencedPaths: () => readInstalledPluginRefs(configRoot),
39701
41905
  mtimeMs: (name) => {
39702
- const p = (0, import_node_path39.join)(stagingRoot, name);
39703
- if (!(0, import_node_fs42.existsSync)(p)) return null;
41906
+ const p = (0, import_node_path40.join)(stagingRoot, name);
41907
+ if (!(0, import_node_fs43.existsSync)(p)) return null;
39704
41908
  try {
39705
- return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs42.statSync)(q).mtimeMs);
41909
+ return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs43.statSync)(q).mtimeMs);
39706
41910
  } catch {
39707
41911
  return null;
39708
41912
  }
@@ -39728,7 +41932,7 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
39728
41932
  { withBytes: true, configRoot, includeStaging: surface !== "codex" }
39729
41933
  );
39730
41934
  const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
39731
- const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs42.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
41935
+ const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs43.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
39732
41936
  const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
39733
41937
  if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
39734
41938
  else console.log(renderPluginCachePlan(plan, result));