@mutmutco/cli 4.1.16 → 4.1.18

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 +347 -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.18",
13185
+ tag: "v4.1.18",
13186
+ commit: "6a8f41433f0f",
13187
+ npm: "@mutmutco/cli@4.1.18"
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.18"
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.18 and redeploy the Hub Lambda from tag v4.1.18 (6a8f41433f0f); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
13211
+ v3Target: "v4.1.18 (@mutmutco/cli@4.1.18, tag commit 6a8f41433f0f \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, cwd) {
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 } : {}, ...cwd ? { cwd } : {} }, (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,75 @@ 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
+ [
27034
+ "-c",
27035
+ "credential.helper=",
27036
+ // #5750: actions/checkout persists its token as a URL-scoped extraheader in the checked-out
27037
+ // repo's .git/config — repo-local config that env sanitization cannot reach. Run from a
27038
+ // neutral cwd so no repo config applies, and blank both extraheader scopes so the ONLY
27039
+ // credential is the app token below. Credential values are never copied or logged.
27040
+ "-c",
27041
+ "http.extraheader=",
27042
+ "-c",
27043
+ "http.https://github.com/.extraheader=",
27044
+ "-c",
27045
+ `http.extraHeader=Authorization: Basic ${basic}`,
27046
+ "ls-remote",
27047
+ "--exit-code",
27048
+ `https://github.com/${repo}.git`,
27049
+ "HEAD"
27050
+ ],
27051
+ headProbeEnv(),
27052
+ (0, import_node_os14.tmpdir)()
27053
+ );
27054
+ } catch (error) {
27055
+ throw toHeadProbeError(error, repo);
27056
+ }
26845
27057
  const match = stdout.match(/^([a-f0-9]{40})\s+HEAD$/m);
26846
- if (!match) throw new Error(`could not resolve remote HEAD for ${repo}`);
27058
+ if (!match) throw new HeadProbeError(`could not resolve remote HEAD for ${repo}`, null, "unreadable");
26847
27059
  return match[1];
26848
27060
  }
26849
27061
  async function mapLimit(items, limit, fn) {
@@ -26912,6 +27124,7 @@ async function syncEstateRepoIndex(opts) {
26912
27124
  const skipped = [];
26913
27125
  const drift = [];
26914
27126
  const needsFullRebuild = [];
27127
+ let rosterSize = 0;
26915
27128
  const answer = (ok) => ({
26916
27129
  ok: ok ?? failed.length === 0,
26917
27130
  published,
@@ -26919,7 +27132,12 @@ async function syncEstateRepoIndex(opts) {
26919
27132
  skipped,
26920
27133
  provenanceToken: CURRENT_REPO_INDEX_PROVENANCE_TOKEN,
26921
27134
  drift,
26922
- needsFullRebuild
27135
+ needsFullRebuild,
27136
+ receipt: {
27137
+ changed: published.length,
27138
+ skipped: Math.max(0, rosterSize - published.length - failed.length),
27139
+ failed: failed.length
27140
+ }
26923
27141
  });
26924
27142
  const fullRebuildToken = (opts.fullRebuild ?? "").trim();
26925
27143
  const forceFull = fullRebuildToken !== "";
@@ -26959,6 +27177,7 @@ async function syncEstateRepoIndex(opts) {
26959
27177
  const busy = new Set(
26960
27178
  (opts.skipRepos ?? []).map((repo) => normalizeRepoIndexRepo(repo)).filter((repo) => repo !== null)
26961
27179
  );
27180
+ rosterSize = repos.length;
26962
27181
  const emit = (progress) => {
26963
27182
  try {
26964
27183
  opts.onClassify?.(progress);
@@ -26991,12 +27210,12 @@ async function syncEstateRepoIndex(opts) {
26991
27210
  }
26992
27211
  let targetCommit = requestedCommit || void 0;
26993
27212
  if (base && !forceFull) {
26994
- let headUnreadable = false;
27213
+ let headProbe = null;
26995
27214
  if (!targetCommit) {
26996
27215
  try {
26997
27216
  targetCommit = await remoteHead(repo, opts.githubToken);
26998
- } catch {
26999
- headUnreadable = true;
27217
+ } catch (error) {
27218
+ headProbe = error instanceof HeadProbeError ? error : new HeadProbeError(error.message, null, "unreadable");
27000
27219
  }
27001
27220
  }
27002
27221
  const provenance = await fetchRepoIndexV4ProvenanceCloud(repo, opts.deps).catch(
@@ -27020,11 +27239,10 @@ async function syncEstateRepoIndex(opts) {
27020
27239
  emit({ row: row2, warning });
27021
27240
  return { repo, row: row2, warning, base, expectedActiveDigest };
27022
27241
  }
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 };
27242
+ if (headProbe) {
27243
+ const row2 = { repo, reason: "head-unreadable", action: "failed", activeCommit: base.commit };
27244
+ emit({ row: row2 });
27245
+ return { repo, row: row2, headProbe, base, expectedActiveDigest };
27028
27246
  }
27029
27247
  }
27030
27248
  const row = {
@@ -27041,6 +27259,14 @@ async function syncEstateRepoIndex(opts) {
27041
27259
  drift.push(entry.row);
27042
27260
  if (entry.warning) skipped.push(entry.warning);
27043
27261
  if (entry.row.action === "needs-full-rebuild") needsFullRebuild.push(entry.repo);
27262
+ if (entry.headProbe) {
27263
+ const exit = entry.headProbe.exitCode === null ? "unknown" : String(entry.headProbe.exitCode);
27264
+ failed.push({
27265
+ repo: entry.repo,
27266
+ error: `remote HEAD unreadable (exit ${exit}, ${entry.headProbe.failureClass}) \u2014 verified-ready authority at ${entry.row.activeCommit ?? "unknown"} was not re-probed; reconcile FAILED`,
27267
+ probe: { exitCode: entry.headProbe.exitCode, failureClass: entry.headProbe.failureClass }
27268
+ });
27269
+ }
27044
27270
  }
27045
27271
  if (opts.plan) return answer();
27046
27272
  for (const entry of classified) {
@@ -33890,34 +34116,60 @@ async function removeWorktreeWithReconcile(wtPath, git3, listWorktrees, pathExis
33890
34116
  return formatGitCommandError(e);
33891
34117
  }
33892
34118
  };
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)) {
34119
+ const pruneAndVerify = async (reason) => {
33900
34120
  await git3(["worktree", "prune"]).catch(() => "");
33901
- return { status: "removed", reason: "reconciled-unregistered" };
34121
+ let worktrees2;
34122
+ try {
34123
+ worktrees2 = await listWorktrees();
34124
+ } catch (e) {
34125
+ return {
34126
+ status: "failed",
34127
+ error: `could not verify worktree removal after prune: ${formatGitCommandError(e)}; path=${wtPath}`
34128
+ };
34129
+ }
34130
+ if (isWorktreePathRegistered(worktrees2, wtPath)) {
34131
+ return {
34132
+ status: "failed",
34133
+ error: `worktree remains registered after prune; path=${wtPath}`
34134
+ };
34135
+ }
34136
+ return { status: "removed", ...reason ? { reason } : {} };
34137
+ };
34138
+ const firstError = await attemptRemove();
34139
+ if (!firstError) return pruneAndVerify();
34140
+ let worktrees;
34141
+ try {
34142
+ worktrees = await listWorktrees();
34143
+ } catch (e) {
34144
+ return {
34145
+ status: "failed",
34146
+ error: `could not verify worktree registration after remove failure: ${formatGitCommandError(e)}; path=${wtPath}`
34147
+ };
33902
34148
  }
34149
+ if (!isWorktreePathRegistered(worktrees, wtPath)) return pruneAndVerify("reconciled-unregistered");
33903
34150
  const retryError = await attemptRemove();
33904
- if (!retryError) {
33905
- await git3(["worktree", "prune"]).catch(() => "");
33906
- return { status: "removed", reason: "reconciled-after-retry" };
33907
- }
34151
+ if (!retryError) return pruneAndVerify("reconciled-after-retry");
33908
34152
  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" };
34153
+ try {
34154
+ worktrees = await listWorktrees();
34155
+ } catch (e) {
34156
+ return {
34157
+ status: "failed",
34158
+ error: `could not verify worktree registration after remove failure: ${formatGitCommandError(e)}; path=${wtPath}`
34159
+ };
33913
34160
  }
34161
+ if (!isWorktreePathRegistered(worktrees, wtPath)) return pruneAndVerify("reconciled-not-a-working-tree");
33914
34162
  }
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" };
34163
+ try {
34164
+ worktrees = await listWorktrees();
34165
+ } catch (e) {
34166
+ return {
34167
+ status: "failed",
34168
+ error: `could not verify worktree registration after remove failure: ${formatGitCommandError(e)}; path=${wtPath}`
34169
+ };
33920
34170
  }
34171
+ const registered = isWorktreePathRegistered(worktrees, wtPath);
34172
+ if (!registered) return pruneAndVerify("reconciled-unregistered");
33921
34173
  return {
33922
34174
  status: "failed",
33923
34175
  error: formatWorktreeRemovalFailureDetail({
@@ -33928,6 +34180,9 @@ async function removeWorktreeWithReconcile(wtPath, git3, listWorktrees, pathExis
33928
34180
  })
33929
34181
  };
33930
34182
  }
34183
+ function prMergeLocalCleanupExitCode(cleanup) {
34184
+ return cleanup?.worktree?.status === "failed" || cleanup?.localBranch?.status === "failed" ? 1 : void 0;
34185
+ }
33931
34186
  async function cleanupPrMergeLocalBranch(branch, options) {
33932
34187
  const report = { branch };
33933
34188
  if (!branch) {
@@ -34034,7 +34289,7 @@ async function cleanupPrMergeLocalBranch(branch, options) {
34034
34289
  const removal = await removeWorktreeWithReconcile(
34035
34290
  wtPath,
34036
34291
  git3,
34037
- async () => parseGitWorktreePorcelain(await execGit(["worktree", "list", "--porcelain"])),
34292
+ async () => parseGitWorktreePorcelain(await git3(["worktree", "list", "--porcelain"])),
34038
34293
  pathExists
34039
34294
  );
34040
34295
  if (removal.status === "failed") {
@@ -41393,11 +41648,29 @@ async function runRepoIndexSearchCommand(query, o, defaultMode) {
41393
41648
  const useLocal = o.local === true && o.cloud !== true;
41394
41649
  if (useLocal) {
41395
41650
  const root = await repoRoot();
41396
- let idx = loadRepoIndex(root);
41397
- if (!idx) idx = rebuildRepoIndex(root, inferRepoSlug(root));
41651
+ const refreshed = refreshRepoIndex(root, inferRepoSlug(root));
41652
+ if (!refreshed.projection) {
41653
+ const receipt = {
41654
+ ok: false,
41655
+ source: "local",
41656
+ repo: inferRepoSlug(root),
41657
+ query,
41658
+ hits: [],
41659
+ status: refreshed.status,
41660
+ reason: refreshed.reason ?? "local index refresh failed"
41661
+ };
41662
+ if (o.json) consoleIo.log(JSON.stringify(receipt, null, 2));
41663
+ else console.error(`repo-index: local index stale \u2014 ${receipt.reason}`);
41664
+ return await cleanExit(1);
41665
+ }
41666
+ const idx = refreshed.projection;
41398
41667
  const hits = searchRepoIndex(idx, query, limit);
41668
+ try {
41669
+ recordLocalRepoIndexQueryUsage(root);
41670
+ } catch {
41671
+ }
41399
41672
  if (o.json) {
41400
- consoleIo.log(JSON.stringify({ ok: true, source: "local", repo: idx.repo, query, hits }, null, 2));
41673
+ consoleIo.log(JSON.stringify({ ok: true, source: "local", repo: idx.repo, query, hits, status: refreshed.status }, null, 2));
41401
41674
  return;
41402
41675
  }
41403
41676
  if (hits.length === 0) {
@@ -41644,7 +41917,10 @@ repoIndex.command("sync-estate").description("Hub indexer: publish a pushed comm
41644
41917
  for (const f of res.failed) {
41645
41918
  console.error(`repo-index: FAILED ${f.repo}: ${f.error}`);
41646
41919
  }
41647
- if (!res.ok) await failGraceful(`sync-estate incomplete (${res.failed.length} failed)`);
41920
+ console.log(`repo-index: estate reconcile receipt \u2014 ${res.receipt.changed} changed, ${res.receipt.skipped} skipped, ${res.receipt.failed} failed`);
41921
+ if (!res.ok) {
41922
+ await failGraceful(`sync-estate incomplete (${res.failed.length} failed): ${formatEstateRunFailureSummary(res.failed)}`);
41923
+ }
41648
41924
  } catch (e) {
41649
41925
  await failGraceful(e.message);
41650
41926
  }
@@ -43487,7 +43763,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
43487
43763
  })) {
43488
43764
  console.error(line);
43489
43765
  }
43490
- process.exitCode = postMergeReconExitCode({ boardAdvance, crossRepoFilingIssue }) ?? process.exitCode;
43766
+ process.exitCode = prMergeLocalCleanupExitCode(localCleanup) ?? postMergeReconExitCode({ boardAdvance, crossRepoFilingIssue }) ?? process.exitCode;
43491
43767
  });
43492
43768
  registerQueryCommands(program2);
43493
43769
  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.18",
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",