@mutmutco/cli 4.1.16 → 4.1.17

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.
Files changed (2) hide show
  1. package/dist/main.cjs +329 -71
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -13181,10 +13181,10 @@ var rollout_plan_default = {
13181
13181
  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)."
13182
13182
  },
13183
13183
  baseline: {
13184
- version: "4.1.16",
13185
- tag: "v4.1.16",
13186
- commit: "3d2a1772d4b6",
13187
- npm: "@mutmutco/cli@4.1.16"
13184
+ version: "4.1.17",
13185
+ tag: "v4.1.17",
13186
+ commit: "65f193ccd344",
13187
+ npm: "@mutmutco/cli@4.1.17"
13188
13188
  },
13189
13189
  exitCriterion: "fleet-n-of-n",
13190
13190
  hubOnlyShortcut: "forbidden",
@@ -13201,14 +13201,14 @@ var rollout_plan_default = {
13201
13201
  repo: "mutmutco/mmi-hub",
13202
13202
  role: "canary",
13203
13203
  schedule: "train",
13204
- v3Target: "v4.1.16"
13204
+ v3Target: "v4.1.17"
13205
13205
  }
13206
13206
  ],
13207
13207
  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.",
13208
13208
  rollback: {
13209
13209
  independent: true,
13210
- mechanism: "npm dist-tag latest -> 4.1.16 and redeploy the Hub Lambda from tag v4.1.16 (3d2a1772d4b6); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
13211
- v3Target: "v4.1.16 (@mutmutco/cli@4.1.16, tag commit 3d2a1772d4b6 \u2014 last known-good release carrying the repo-index v4-only contract)"
13210
+ mechanism: "npm dist-tag latest -> 4.1.17 and redeploy the Hub Lambda from tag v4.1.17 (65f193ccd344); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
13211
+ v3Target: "v4.1.17 (@mutmutco/cli@4.1.17, tag commit 65f193ccd344 \u2014 last known-good release carrying the repo-index v4-only contract)"
13212
13212
  }
13213
13213
  },
13214
13214
  {
@@ -14193,7 +14193,8 @@ async function runPrForIssue(deps, issueRef, opts) {
14193
14193
  return extractPrForIssueResponse(resp);
14194
14194
  }
14195
14195
  async function runIssueViewContext(deps, opts) {
14196
- const fields = opts.comments || opts.context ? ensureField(opts.jsonFields, "comments") : opts.jsonFields;
14196
+ const baseFields = opts.context ? opts.jsonFields.split(",").map((field) => field.trim()).filter((field) => field !== "linkedPrs" && field !== "children").join(",") : opts.jsonFields;
14197
+ const fields = opts.comments || opts.context ? ensureField(baseFields, "comments") : baseFields;
14197
14198
  const base = await deps.ghJson(["issue", "view", String(opts.number), "--repo", opts.repo, "--json", fields], GH_LIST_TIMEOUT_MS);
14198
14199
  if (!opts.context) return base;
14199
14200
  const result = { ...base };
@@ -25266,9 +25267,35 @@ function repoIndexPathTieBreak(path2) {
25266
25267
  if (/\.(test|spec)\.[^.]+$/i.test(p)) return 1;
25267
25268
  return 0;
25268
25269
  }
25270
+ var LOCAL_REPO_INDEX_REFRESH_BUDGET_MS = 2e3;
25271
+ var LOCAL_REPO_INDEX_DELTA_FILE_LIMIT = 50;
25269
25272
  function repoIndexStorePath(cwd) {
25270
25273
  return repoRuntimeStatePath(cwd, "repo-index", "index.json");
25271
25274
  }
25275
+ function localRepoIndexUsagePath(cwd) {
25276
+ return repoRuntimeStatePath(cwd, "repo-index", "local-usage.json");
25277
+ }
25278
+ function recordLocalRepoIndexQueryUsage(cwd, now = /* @__PURE__ */ new Date()) {
25279
+ const store = localRepoIndexUsagePath(cwd);
25280
+ let previous = 0;
25281
+ try {
25282
+ const raw = JSON.parse((0, import_node_fs24.readFileSync)(store, "utf8"));
25283
+ if (typeof raw.count === "number" && Number.isFinite(raw.count)) previous = raw.count;
25284
+ } catch {
25285
+ }
25286
+ (0, import_node_fs24.mkdirSync)((0, import_node_path21.dirname)(store), { recursive: true });
25287
+ (0, import_node_fs24.writeFileSync)(store, `${JSON.stringify({ count: previous + 1, updatedAt: now.toISOString() }, null, 2)}
25288
+ `, "utf8");
25289
+ }
25290
+ function readLocalRepoIndexQueryUsage(cwd) {
25291
+ try {
25292
+ const raw = JSON.parse((0, import_node_fs24.readFileSync)(localRepoIndexUsagePath(cwd), "utf8"));
25293
+ if (typeof raw.count !== "number" || !Number.isFinite(raw.count)) return null;
25294
+ return { count: raw.count, updatedAt: raw.updatedAt ?? "" };
25295
+ } catch {
25296
+ return null;
25297
+ }
25298
+ }
25272
25299
  function isHardDeniedPath(relPosix) {
25273
25300
  return isHardDeniedRepoIndexPath(relPosix);
25274
25301
  }
@@ -25379,14 +25406,41 @@ function listCandidatePaths(cwd, exec = import_node_child_process12.execFileSync
25379
25406
  return [];
25380
25407
  }
25381
25408
  }
25382
- function rebuildRepoIndex(cwd, repoSlug3) {
25383
- const candidates = listCandidatePaths(cwd).filter(isIndexablePath);
25384
- const ignored = defaultIsIgnored(cwd, candidates);
25385
- const readmeHints = loadReadmeHints(cwd, candidates.filter((p) => !ignored.has(p)));
25386
- const entries = [];
25409
+ var RepoIndexRefreshBudgetError = class extends Error {
25410
+ constructor() {
25411
+ super(`local index refresh exceeded ${LOCAL_REPO_INDEX_REFRESH_BUDGET_MS}ms`);
25412
+ }
25413
+ };
25414
+ function assertRefreshBudget(deadline) {
25415
+ if (deadline !== void 0 && Date.now() >= deadline) throw new RepoIndexRefreshBudgetError();
25416
+ }
25417
+ function deadlineExec(deadline) {
25418
+ return ((file, args, options) => {
25419
+ assertRefreshBudget(deadline);
25420
+ return (0, import_node_child_process12.execFileSync)(file, args, {
25421
+ ...options,
25422
+ timeout: Math.max(1, deadline - Date.now()),
25423
+ windowsHide: true
25424
+ });
25425
+ });
25426
+ }
25427
+ function gitHead(cwd, exec = import_node_child_process12.execFileSync) {
25428
+ try {
25429
+ return String(exec("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8", windowsHide: true })).trim() || void 0;
25430
+ } catch (error) {
25431
+ if (error instanceof RepoIndexRefreshBudgetError) throw error;
25432
+ return void 0;
25433
+ }
25434
+ }
25435
+ function sourceFilesForIndex(cwd, deadline) {
25436
+ const exec = deadline === void 0 ? import_node_child_process12.execFileSync : deadlineExec(deadline);
25437
+ const candidates = listCandidatePaths(cwd, exec).filter(isIndexablePath);
25438
+ assertRefreshBudget(deadline);
25439
+ const ignored = defaultIsIgnored(cwd, candidates, exec);
25440
+ const files = [];
25387
25441
  for (const rel of candidates) {
25388
- if (ignored.has(rel)) continue;
25389
- if (isHardDeniedPath(rel)) continue;
25442
+ assertRefreshBudget(deadline);
25443
+ if (ignored.has(rel) || isHardDeniedPath(rel)) continue;
25390
25444
  const abs = (0, import_node_path21.join)(cwd, ...rel.split("/"));
25391
25445
  if (!(0, import_node_fs24.existsSync)(abs)) continue;
25392
25446
  let text;
@@ -25396,31 +25450,118 @@ function rebuildRepoIndex(cwd, repoSlug3) {
25396
25450
  continue;
25397
25451
  }
25398
25452
  if (text.length > 15e5) continue;
25399
- const hash = (0, import_node_crypto6.createHash)("sha256").update(text).digest("hex").slice(0, 16);
25400
- const symbols = extractSymbols(text);
25401
- const docBlurb = extractModuleBlurb(text);
25402
- const top = rel.includes("/") ? rel.split("/")[0] : "";
25403
- const hint = docBlurb ?? (top ? readmeHints.get(top) : void 0) ?? readmeHints.get("");
25404
- entries.push({
25453
+ files.push({
25405
25454
  path: rel,
25406
- hash,
25407
- symbols,
25408
- ...hint ? { blurb: hint.slice(0, BLURB_CAP) } : {}
25455
+ hash: (0, import_node_crypto6.createHash)("sha256").update(text).digest("hex").slice(0, 16),
25456
+ text
25409
25457
  });
25410
25458
  }
25459
+ return files;
25460
+ }
25461
+ function entryForSource(file, readmeHints) {
25462
+ const docBlurb = extractModuleBlurb(file.text);
25463
+ const top = file.path.includes("/") ? file.path.split("/")[0] : "";
25464
+ const hint = docBlurb ?? (top ? readmeHints.get(top) : void 0) ?? readmeHints.get("");
25465
+ return {
25466
+ path: file.path,
25467
+ hash: file.hash,
25468
+ symbols: extractSymbols(file.text),
25469
+ ...hint ? { blurb: hint.slice(0, BLURB_CAP) } : {}
25470
+ };
25471
+ }
25472
+ function buildRepoIndexProjection(cwd, repoSlug3, deadline) {
25473
+ const files = sourceFilesForIndex(cwd, deadline);
25474
+ assertRefreshBudget(deadline);
25475
+ const readmeHints = loadReadmeHints(cwd, files.map((file) => file.path));
25476
+ const entries = files.map((file) => {
25477
+ assertRefreshBudget(deadline);
25478
+ return entryForSource(file, readmeHints);
25479
+ });
25411
25480
  entries.sort((a, b) => a.path.localeCompare(b.path));
25412
- const projection = {
25481
+ const exec = deadline === void 0 ? import_node_child_process12.execFileSync : deadlineExec(deadline);
25482
+ const indexHead = gitHead(cwd, exec);
25483
+ return {
25413
25484
  schema: REPO_INDEX_SCHEMA,
25414
25485
  repo: repoSlug3,
25415
25486
  builtAt: (/* @__PURE__ */ new Date()).toISOString(),
25487
+ ...indexHead ? { indexHead } : {},
25416
25488
  entries
25417
25489
  };
25490
+ }
25491
+ function writeRepoIndex(cwd, projection) {
25418
25492
  const store = repoIndexStorePath(cwd);
25419
25493
  (0, import_node_fs24.mkdirSync)((0, import_node_path21.dirname)(store), { recursive: true });
25420
25494
  (0, import_node_fs24.writeFileSync)(store, `${JSON.stringify(projection, null, 2)}
25421
25495
  `, "utf8");
25496
+ }
25497
+ function rebuildRepoIndex(cwd, repoSlug3) {
25498
+ const projection = buildRepoIndexProjection(cwd, repoSlug3);
25499
+ writeRepoIndex(cwd, projection);
25422
25500
  return projection;
25423
25501
  }
25502
+ function refreshRepoIndex(cwd, repoSlug3, options = {}) {
25503
+ const budgetMs = options.budgetMs ?? LOCAL_REPO_INDEX_REFRESH_BUDGET_MS;
25504
+ const deadline = Date.now() + Math.max(0, budgetMs);
25505
+ try {
25506
+ const exec = deadlineExec(deadline);
25507
+ const existing = loadRepoIndex(cwd);
25508
+ if (!existing) {
25509
+ const projection2 = buildRepoIndexProjection(cwd, repoSlug3, deadline);
25510
+ writeRepoIndex(cwd, projection2);
25511
+ return { status: "rebuilt", projection: projection2 };
25512
+ }
25513
+ const head = gitHead(cwd, exec);
25514
+ if (!head) return { status: "stale", reason: "checkout HEAD is unavailable" };
25515
+ const worktreeDirty = String(exec("git", ["status", "--porcelain", "--untracked-files=normal"], {
25516
+ cwd,
25517
+ encoding: "utf8",
25518
+ windowsHide: true
25519
+ })).length > 0;
25520
+ if (existing.indexHead === head && !worktreeDirty) return { status: "refreshed", projection: existing };
25521
+ if (!existing.indexHead) {
25522
+ const projection2 = buildRepoIndexProjection(cwd, repoSlug3, deadline);
25523
+ writeRepoIndex(cwd, projection2);
25524
+ return { status: "rebuilt", projection: projection2 };
25525
+ }
25526
+ const files = sourceFilesForIndex(cwd, deadline);
25527
+ const current = new Map(files.map((file) => [file.path, file]));
25528
+ const previous = new Map(existing.entries.map((entry) => [entry.path, entry]));
25529
+ const changed = /* @__PURE__ */ new Set();
25530
+ for (const [path2, file] of current) {
25531
+ if (previous.get(path2)?.hash !== file.hash) changed.add(path2);
25532
+ }
25533
+ for (const path2 of previous.keys()) {
25534
+ if (!current.has(path2)) changed.add(path2);
25535
+ }
25536
+ const readmeChanged = [...changed].some((path2) => path2 === "README.md" || /^[^/]+\/README\.md$/.test(path2));
25537
+ if (changed.size > LOCAL_REPO_INDEX_DELTA_FILE_LIMIT || readmeChanged) {
25538
+ const projection2 = buildRepoIndexProjection(cwd, repoSlug3, deadline);
25539
+ writeRepoIndex(cwd, projection2);
25540
+ return { status: "rebuilt", projection: projection2 };
25541
+ }
25542
+ const changedSources = [...changed].map((path2) => current.get(path2)).filter((file) => file !== void 0);
25543
+ const needsHint = changedSources.some((file) => !extractModuleBlurb(file.text));
25544
+ const readmeHints = needsHint ? loadReadmeHints(cwd, files.map((file) => file.path)) : /* @__PURE__ */ new Map();
25545
+ const entries = files.map((file) => {
25546
+ assertRefreshBudget(deadline);
25547
+ const old = previous.get(file.path);
25548
+ return old?.hash === file.hash ? old : entryForSource(file, readmeHints);
25549
+ });
25550
+ entries.sort((a, b) => a.path.localeCompare(b.path));
25551
+ const projection = {
25552
+ ...existing,
25553
+ repo: repoSlug3,
25554
+ builtAt: (/* @__PURE__ */ new Date()).toISOString(),
25555
+ indexHead: head,
25556
+ entries
25557
+ };
25558
+ writeRepoIndex(cwd, projection);
25559
+ return { status: "refreshed", projection };
25560
+ } catch (error) {
25561
+ const reason = error instanceof RepoIndexRefreshBudgetError ? error.message : `local index refresh failed: ${error.message}`;
25562
+ return { status: "stale", reason };
25563
+ }
25564
+ }
25424
25565
  function loadRepoIndex(cwd) {
25425
25566
  const store = repoIndexStorePath(cwd);
25426
25567
  if (!(0, import_node_fs24.existsSync)(store)) return null;
@@ -25437,6 +25578,7 @@ function repoIndexStatus(cwd) {
25437
25578
  const idx = loadRepoIndex(cwd);
25438
25579
  if (!idx) return { present: false, path: path2 };
25439
25580
  const symbolCount = idx.entries.reduce((n, e) => n + e.symbols.length, 0);
25581
+ const localUsage = readLocalRepoIndexQueryUsage(cwd);
25440
25582
  return {
25441
25583
  present: true,
25442
25584
  path: path2,
@@ -25444,7 +25586,8 @@ function repoIndexStatus(cwd) {
25444
25586
  repo: idx.repo,
25445
25587
  builtAt: idx.builtAt,
25446
25588
  fileCount: idx.entries.length,
25447
- symbolCount
25589
+ symbolCount,
25590
+ ...localUsage ? { localQueryCount: localUsage.count } : {}
25448
25591
  };
25449
25592
  }
25450
25593
  function searchRepoIndex(idx, query, limit = 20) {
@@ -26238,9 +26381,9 @@ async function probeRepoIndexV4ReadinessCloud(queries, deps) {
26238
26381
  if (!token) return { ok: false, error: "no Hub session token (run `gh auth login`)" };
26239
26382
  const baseUrl = deps.baseUrl.replace(/\/$/, "");
26240
26383
  const headers = { ...clientVersionHeaders(), Authorization: ["Bearer", token].join(" "), "content-type": "application/json" };
26241
- const shadowPass = async (readinessProbe) => {
26384
+ const shadowPass = async (readinessProbe, passQueries = queries) => {
26242
26385
  const ms = {};
26243
- for (const query of queries) {
26386
+ for (const query of passQueries) {
26244
26387
  const res = await fetchWithRetry(deps.fetch ?? fetch, `${baseUrl}/repo-index/v4/shadow`, {
26245
26388
  method: "POST",
26246
26389
  headers,
@@ -26262,6 +26405,11 @@ async function probeRepoIndexV4ReadinessCloud(queries, deps) {
26262
26405
  }
26263
26406
  const verdict = await shadowPass(true);
26264
26407
  if (!verdict.ok) return verdict;
26408
+ const latencyRetries = queries.filter((query) => query.latencyMs !== void 0 && verdict.ms[query.id] !== void 0 && verdict.ms[query.id] > query.latencyMs);
26409
+ if (latencyRetries.length) {
26410
+ const retry = await shadowPass(true, latencyRetries);
26411
+ if (!retry.ok) return retry;
26412
+ }
26265
26413
  const res = await fetchWithRetry(deps.fetch ?? fetch, `${baseUrl}/repo-index/status`, {
26266
26414
  method: "GET",
26267
26415
  headers: { ...clientVersionHeaders(), Authorization: ["Bearer", token].join(" ") }
@@ -26791,9 +26939,9 @@ function buildGraphEdges(cwd, repo, commit, rosterRepos2) {
26791
26939
  }
26792
26940
 
26793
26941
  // src/repo-index-sync.ts
26794
- function execFileUtf8(file, args) {
26942
+ function execFileUtf8(file, args, env) {
26795
26943
  return new Promise((resolve6, reject) => {
26796
- (0, import_node_child_process15.execFile)(file, args, { encoding: "utf8", windowsHide: true }, (error, stdout) => {
26944
+ (0, import_node_child_process15.execFile)(file, args, { encoding: "utf8", windowsHide: true, ...env ? { env } : {} }, (error, stdout) => {
26797
26945
  if (error) reject(error);
26798
26946
  else resolve6(String(stdout ?? ""));
26799
26947
  });
@@ -26801,6 +26949,9 @@ function execFileUtf8(file, args) {
26801
26949
  }
26802
26950
  var ESTATE_CLASSIFY_CONCURRENCY = 8;
26803
26951
  var ESTATE_HEALTHY_CLASSIFY_BUDGET_MS = 5 * 601e3;
26952
+ function formatEstateRunFailureSummary(failed) {
26953
+ return failed.map((f) => `${f.repo} (${f.probe?.failureClass ?? "failed"})`).join(", ");
26954
+ }
26804
26955
  var COMMIT3 = /^[a-f0-9]{40}$/;
26805
26956
  var SHA256 = /^[a-f0-9]{64}$/;
26806
26957
  var UTC_MILLIS = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
@@ -26836,14 +26987,57 @@ function checkoutExactCommit(repo, dest, token, commit) {
26836
26987
  const head = git3(["rev-parse", "HEAD"]).trim().toLowerCase();
26837
26988
  if (head !== commit) throw new Error(`checkout of ${repo} resolved ${head}, not the requested commit ${commit}`);
26838
26989
  }
26990
+ var HeadProbeError = class extends Error {
26991
+ constructor(message, exitCode, failureClass) {
26992
+ super(message);
26993
+ this.exitCode = exitCode;
26994
+ this.failureClass = failureClass;
26995
+ }
26996
+ exitCode;
26997
+ failureClass;
26998
+ };
26999
+ function classifyProbeStderr(stderr) {
27000
+ const text = stderr.toLowerCase();
27001
+ if (/(could not read username|could not read password|authentication failed|invalid credentials|permission denied|terminal prompts disabled|returned error: 401|returned error: 403)/.test(text)) return "auth-denied";
27002
+ if (/(repository .* not found|couldn't find remote ref|not found)/.test(text)) return "not-found";
27003
+ if (/(timed out|timeout|operation timed out|connection timed out)/.test(text)) return "timeout";
27004
+ if (/(could not resolve host|network is unreachable|connection (refused|reset)|temporary failure|failed to connect|could not connect)/.test(text)) return "network";
27005
+ return "unreadable";
27006
+ }
27007
+ function headProbeEnv() {
27008
+ const env = { ...process.env };
27009
+ delete env.GIT_ASKPASS;
27010
+ delete env.SSH_ASKPASS;
27011
+ for (const key of Object.keys(env)) {
27012
+ if (key.startsWith("GIT_CONFIG_")) delete env[key];
27013
+ }
27014
+ env.GIT_TERMINAL_PROMPT = "0";
27015
+ return env;
27016
+ }
27017
+ function toHeadProbeError(error, repo) {
27018
+ const e = error;
27019
+ const exitCode = typeof e.code === "number" ? e.code : null;
27020
+ const stderr = typeof e.stderr === "string" ? e.stderr : "";
27021
+ return new HeadProbeError(
27022
+ `remote HEAD probe of ${repo} failed (exit ${exitCode ?? "unknown"}, ${classifyProbeStderr(stderr)})`,
27023
+ exitCode,
27024
+ classifyProbeStderr(stderr)
27025
+ );
27026
+ }
26839
27027
  async function remoteHead(repo, token) {
26840
27028
  const basic = Buffer.from(`x-access-token:${token}`, "utf8").toString("base64");
26841
- const stdout = await execFileUtf8(
26842
- "git",
26843
- ["-c", `http.extraHeader=Authorization: Basic ${basic}`, "ls-remote", "--exit-code", `https://github.com/${repo}.git`, "HEAD"]
26844
- );
27029
+ let stdout;
27030
+ try {
27031
+ stdout = await execFileUtf8(
27032
+ "git",
27033
+ ["-c", "credential.helper=", "-c", `http.extraHeader=Authorization: Basic ${basic}`, "ls-remote", "--exit-code", `https://github.com/${repo}.git`, "HEAD"],
27034
+ headProbeEnv()
27035
+ );
27036
+ } catch (error) {
27037
+ throw toHeadProbeError(error, repo);
27038
+ }
26845
27039
  const match = stdout.match(/^([a-f0-9]{40})\s+HEAD$/m);
26846
- if (!match) throw new Error(`could not resolve remote HEAD for ${repo}`);
27040
+ if (!match) throw new HeadProbeError(`could not resolve remote HEAD for ${repo}`, null, "unreadable");
26847
27041
  return match[1];
26848
27042
  }
26849
27043
  async function mapLimit(items, limit, fn) {
@@ -26912,6 +27106,7 @@ async function syncEstateRepoIndex(opts) {
26912
27106
  const skipped = [];
26913
27107
  const drift = [];
26914
27108
  const needsFullRebuild = [];
27109
+ let rosterSize = 0;
26915
27110
  const answer = (ok) => ({
26916
27111
  ok: ok ?? failed.length === 0,
26917
27112
  published,
@@ -26919,7 +27114,12 @@ async function syncEstateRepoIndex(opts) {
26919
27114
  skipped,
26920
27115
  provenanceToken: CURRENT_REPO_INDEX_PROVENANCE_TOKEN,
26921
27116
  drift,
26922
- needsFullRebuild
27117
+ needsFullRebuild,
27118
+ receipt: {
27119
+ changed: published.length,
27120
+ skipped: Math.max(0, rosterSize - published.length - failed.length),
27121
+ failed: failed.length
27122
+ }
26923
27123
  });
26924
27124
  const fullRebuildToken = (opts.fullRebuild ?? "").trim();
26925
27125
  const forceFull = fullRebuildToken !== "";
@@ -26959,6 +27159,7 @@ async function syncEstateRepoIndex(opts) {
26959
27159
  const busy = new Set(
26960
27160
  (opts.skipRepos ?? []).map((repo) => normalizeRepoIndexRepo(repo)).filter((repo) => repo !== null)
26961
27161
  );
27162
+ rosterSize = repos.length;
26962
27163
  const emit = (progress) => {
26963
27164
  try {
26964
27165
  opts.onClassify?.(progress);
@@ -26991,12 +27192,12 @@ async function syncEstateRepoIndex(opts) {
26991
27192
  }
26992
27193
  let targetCommit = requestedCommit || void 0;
26993
27194
  if (base && !forceFull) {
26994
- let headUnreadable = false;
27195
+ let headProbe = null;
26995
27196
  if (!targetCommit) {
26996
27197
  try {
26997
27198
  targetCommit = await remoteHead(repo, opts.githubToken);
26998
- } catch {
26999
- headUnreadable = true;
27199
+ } catch (error) {
27200
+ headProbe = error instanceof HeadProbeError ? error : new HeadProbeError(error.message, null, "unreadable");
27000
27201
  }
27001
27202
  }
27002
27203
  const provenance = await fetchRepoIndexV4ProvenanceCloud(repo, opts.deps).catch(
@@ -27020,11 +27221,10 @@ async function syncEstateRepoIndex(opts) {
27020
27221
  emit({ row: row2, warning });
27021
27222
  return { repo, row: row2, warning, base, expectedActiveDigest };
27022
27223
  }
27023
- if (headUnreadable) {
27024
- const row2 = { repo, reason: "head-unreadable", action: "skip", activeCommit: base.commit };
27025
- const warning = `${repo}: remote HEAD unreadable; leaving verified-ready authority at ${base.commit}`;
27026
- emit({ row: row2, warning });
27027
- return { repo, row: row2, warning, base, expectedActiveDigest };
27224
+ if (headProbe) {
27225
+ const row2 = { repo, reason: "head-unreadable", action: "failed", activeCommit: base.commit };
27226
+ emit({ row: row2 });
27227
+ return { repo, row: row2, headProbe, base, expectedActiveDigest };
27028
27228
  }
27029
27229
  }
27030
27230
  const row = {
@@ -27041,6 +27241,14 @@ async function syncEstateRepoIndex(opts) {
27041
27241
  drift.push(entry.row);
27042
27242
  if (entry.warning) skipped.push(entry.warning);
27043
27243
  if (entry.row.action === "needs-full-rebuild") needsFullRebuild.push(entry.repo);
27244
+ if (entry.headProbe) {
27245
+ const exit = entry.headProbe.exitCode === null ? "unknown" : String(entry.headProbe.exitCode);
27246
+ failed.push({
27247
+ repo: entry.repo,
27248
+ error: `remote HEAD unreadable (exit ${exit}, ${entry.headProbe.failureClass}) \u2014 verified-ready authority at ${entry.row.activeCommit ?? "unknown"} was not re-probed; reconcile FAILED`,
27249
+ probe: { exitCode: entry.headProbe.exitCode, failureClass: entry.headProbe.failureClass }
27250
+ });
27251
+ }
27044
27252
  }
27045
27253
  if (opts.plan) return answer();
27046
27254
  for (const entry of classified) {
@@ -33890,34 +34098,60 @@ async function removeWorktreeWithReconcile(wtPath, git3, listWorktrees, pathExis
33890
34098
  return formatGitCommandError(e);
33891
34099
  }
33892
34100
  };
33893
- const firstError = await attemptRemove();
33894
- if (!firstError) {
33895
- await git3(["worktree", "prune"]).catch(() => "");
33896
- return { status: "removed" };
33897
- }
33898
- let worktrees = await listWorktrees().catch(() => []);
33899
- if (!isWorktreePathRegistered(worktrees, wtPath)) {
34101
+ const pruneAndVerify = async (reason) => {
33900
34102
  await git3(["worktree", "prune"]).catch(() => "");
33901
- return { status: "removed", reason: "reconciled-unregistered" };
34103
+ let worktrees2;
34104
+ try {
34105
+ worktrees2 = await listWorktrees();
34106
+ } catch (e) {
34107
+ return {
34108
+ status: "failed",
34109
+ error: `could not verify worktree removal after prune: ${formatGitCommandError(e)}; path=${wtPath}`
34110
+ };
34111
+ }
34112
+ if (isWorktreePathRegistered(worktrees2, wtPath)) {
34113
+ return {
34114
+ status: "failed",
34115
+ error: `worktree remains registered after prune; path=${wtPath}`
34116
+ };
34117
+ }
34118
+ return { status: "removed", ...reason ? { reason } : {} };
34119
+ };
34120
+ const firstError = await attemptRemove();
34121
+ if (!firstError) return pruneAndVerify();
34122
+ let worktrees;
34123
+ try {
34124
+ worktrees = await listWorktrees();
34125
+ } catch (e) {
34126
+ return {
34127
+ status: "failed",
34128
+ error: `could not verify worktree registration after remove failure: ${formatGitCommandError(e)}; path=${wtPath}`
34129
+ };
33902
34130
  }
34131
+ if (!isWorktreePathRegistered(worktrees, wtPath)) return pruneAndVerify("reconciled-unregistered");
33903
34132
  const retryError = await attemptRemove();
33904
- if (!retryError) {
33905
- await git3(["worktree", "prune"]).catch(() => "");
33906
- return { status: "removed", reason: "reconciled-after-retry" };
33907
- }
34133
+ if (!retryError) return pruneAndVerify("reconciled-after-retry");
33908
34134
  if (isNotAWorkingTreeMessage(retryError) || isNotAWorkingTreeMessage(firstError)) {
33909
- worktrees = await listWorktrees().catch(() => worktrees);
33910
- if (!isWorktreePathRegistered(worktrees, wtPath)) {
33911
- await git3(["worktree", "prune"]).catch(() => "");
33912
- return { status: "removed", reason: "reconciled-not-a-working-tree" };
34135
+ try {
34136
+ worktrees = await listWorktrees();
34137
+ } catch (e) {
34138
+ return {
34139
+ status: "failed",
34140
+ error: `could not verify worktree registration after remove failure: ${formatGitCommandError(e)}; path=${wtPath}`
34141
+ };
33913
34142
  }
34143
+ if (!isWorktreePathRegistered(worktrees, wtPath)) return pruneAndVerify("reconciled-not-a-working-tree");
33914
34144
  }
33915
- worktrees = await listWorktrees().catch(() => worktrees);
33916
- const registered = isWorktreePathRegistered(worktrees, wtPath);
33917
- if (!registered) {
33918
- await git3(["worktree", "prune"]).catch(() => "");
33919
- return { status: "removed", reason: "reconciled-unregistered" };
34145
+ try {
34146
+ worktrees = await listWorktrees();
34147
+ } catch (e) {
34148
+ return {
34149
+ status: "failed",
34150
+ error: `could not verify worktree registration after remove failure: ${formatGitCommandError(e)}; path=${wtPath}`
34151
+ };
33920
34152
  }
34153
+ const registered = isWorktreePathRegistered(worktrees, wtPath);
34154
+ if (!registered) return pruneAndVerify("reconciled-unregistered");
33921
34155
  return {
33922
34156
  status: "failed",
33923
34157
  error: formatWorktreeRemovalFailureDetail({
@@ -33928,6 +34162,9 @@ async function removeWorktreeWithReconcile(wtPath, git3, listWorktrees, pathExis
33928
34162
  })
33929
34163
  };
33930
34164
  }
34165
+ function prMergeLocalCleanupExitCode(cleanup) {
34166
+ return cleanup?.worktree?.status === "failed" || cleanup?.localBranch?.status === "failed" ? 1 : void 0;
34167
+ }
33931
34168
  async function cleanupPrMergeLocalBranch(branch, options) {
33932
34169
  const report = { branch };
33933
34170
  if (!branch) {
@@ -34034,7 +34271,7 @@ async function cleanupPrMergeLocalBranch(branch, options) {
34034
34271
  const removal = await removeWorktreeWithReconcile(
34035
34272
  wtPath,
34036
34273
  git3,
34037
- async () => parseGitWorktreePorcelain(await execGit(["worktree", "list", "--porcelain"])),
34274
+ async () => parseGitWorktreePorcelain(await git3(["worktree", "list", "--porcelain"])),
34038
34275
  pathExists
34039
34276
  );
34040
34277
  if (removal.status === "failed") {
@@ -41393,11 +41630,29 @@ async function runRepoIndexSearchCommand(query, o, defaultMode) {
41393
41630
  const useLocal = o.local === true && o.cloud !== true;
41394
41631
  if (useLocal) {
41395
41632
  const root = await repoRoot();
41396
- let idx = loadRepoIndex(root);
41397
- if (!idx) idx = rebuildRepoIndex(root, inferRepoSlug(root));
41633
+ const refreshed = refreshRepoIndex(root, inferRepoSlug(root));
41634
+ if (!refreshed.projection) {
41635
+ const receipt = {
41636
+ ok: false,
41637
+ source: "local",
41638
+ repo: inferRepoSlug(root),
41639
+ query,
41640
+ hits: [],
41641
+ status: refreshed.status,
41642
+ reason: refreshed.reason ?? "local index refresh failed"
41643
+ };
41644
+ if (o.json) consoleIo.log(JSON.stringify(receipt, null, 2));
41645
+ else console.error(`repo-index: local index stale \u2014 ${receipt.reason}`);
41646
+ return await cleanExit(1);
41647
+ }
41648
+ const idx = refreshed.projection;
41398
41649
  const hits = searchRepoIndex(idx, query, limit);
41650
+ try {
41651
+ recordLocalRepoIndexQueryUsage(root);
41652
+ } catch {
41653
+ }
41399
41654
  if (o.json) {
41400
- consoleIo.log(JSON.stringify({ ok: true, source: "local", repo: idx.repo, query, hits }, null, 2));
41655
+ consoleIo.log(JSON.stringify({ ok: true, source: "local", repo: idx.repo, query, hits, status: refreshed.status }, null, 2));
41401
41656
  return;
41402
41657
  }
41403
41658
  if (hits.length === 0) {
@@ -41644,7 +41899,10 @@ repoIndex.command("sync-estate").description("Hub indexer: publish a pushed comm
41644
41899
  for (const f of res.failed) {
41645
41900
  console.error(`repo-index: FAILED ${f.repo}: ${f.error}`);
41646
41901
  }
41647
- if (!res.ok) await failGraceful(`sync-estate incomplete (${res.failed.length} failed)`);
41902
+ console.log(`repo-index: estate reconcile receipt \u2014 ${res.receipt.changed} changed, ${res.receipt.skipped} skipped, ${res.receipt.failed} failed`);
41903
+ if (!res.ok) {
41904
+ await failGraceful(`sync-estate incomplete (${res.failed.length} failed): ${formatEstateRunFailureSummary(res.failed)}`);
41905
+ }
41648
41906
  } catch (e) {
41649
41907
  await failGraceful(e.message);
41650
41908
  }
@@ -43487,7 +43745,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
43487
43745
  })) {
43488
43746
  console.error(line);
43489
43747
  }
43490
- process.exitCode = postMergeReconExitCode({ boardAdvance, crossRepoFilingIssue }) ?? process.exitCode;
43748
+ process.exitCode = prMergeLocalCleanupExitCode(localCleanup) ?? postMergeReconExitCode({ boardAdvance, crossRepoFilingIssue }) ?? process.exitCode;
43491
43749
  });
43492
43750
  registerQueryCommands(program2);
43493
43751
  registerIssueLifecycleCommands(program2, { attach: attachToProject });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.1.16",
3
+ "version": "4.1.17",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",