@mutmutco/cli 4.1.1 → 4.1.3

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 +340 -60
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -2306,7 +2306,8 @@ __export(index_exports, {
2306
2306
  positionalTargetForm: () => positionalTargetForm,
2307
2307
  registryClientDeps: () => registryClientDeps,
2308
2308
  repoSlug: () => repoSlug,
2309
- suggestCommandPath: () => suggestCommandPath
2309
+ suggestCommandPath: () => suggestCommandPath,
2310
+ unknownCommandCandidates: () => unknownCommandCandidates
2310
2311
  });
2311
2312
  module.exports = __toCommonJS(index_exports);
2312
2313
 
@@ -12867,10 +12868,10 @@ var rollout_plan_default = {
12867
12868
  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)."
12868
12869
  },
12869
12870
  baseline: {
12870
- version: "4.1.1",
12871
- tag: "v4.1.1",
12872
- commit: "df9ba86cbdba",
12873
- npm: "@mutmutco/cli@4.1.1"
12871
+ version: "4.1.3",
12872
+ tag: "v4.1.3",
12873
+ commit: "3164484f3f37",
12874
+ npm: "@mutmutco/cli@4.1.3"
12874
12875
  },
12875
12876
  exitCriterion: "fleet-n-of-n",
12876
12877
  hubOnlyShortcut: "forbidden",
@@ -12887,14 +12888,14 @@ var rollout_plan_default = {
12887
12888
  repo: "mutmutco/mmi-hub",
12888
12889
  role: "canary",
12889
12890
  schedule: "train",
12890
- v3Target: "v4.1.1"
12891
+ v3Target: "v4.1.3"
12891
12892
  }
12892
12893
  ],
12893
12894
  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.",
12894
12895
  rollback: {
12895
12896
  independent: true,
12896
- mechanism: "npm dist-tag latest -> 4.1.1 and redeploy the Hub Lambda from tag v4.1.1 (df9ba86cbdba); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
12897
- v3Target: "v4.1.1 (@mutmutco/cli@4.1.1, tag commit df9ba86cbdba \u2014 last known-good release carrying the repo-index v4-only contract)"
12897
+ mechanism: "npm dist-tag latest -> 4.1.3 and redeploy the Hub Lambda from tag v4.1.3 (3164484f3f37); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
12898
+ v3Target: "v4.1.3 (@mutmutco/cli@4.1.3, tag commit 3164484f3f37 \u2014 last known-good release carrying the repo-index v4-only contract)"
12898
12899
  }
12899
12900
  },
12900
12901
  {
@@ -19388,8 +19389,10 @@ init_client_version();
19388
19389
  var BOARD_SNAPSHOT_TIMEOUT_MS = 25e3;
19389
19390
  function isSnapshotShape(body) {
19390
19391
  const b = body;
19392
+ const rate = b?.github?.rateLimit;
19393
+ const githubValid = b?.github === void 0 || b.github.credential === "app_installation" && Number.isFinite(rate?.limit) && Number.isFinite(rate?.remaining) && Number.isFinite(rate?.cost) && typeof rate?.resetAt === "string";
19391
19394
  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"
19395
+ 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" && githubValid
19393
19396
  );
19394
19397
  }
19395
19398
  async function fetchHubBoardSnapshot(request, deps) {
@@ -20172,6 +20175,10 @@ function renderBoardItem(item) {
20172
20175
  }
20173
20176
  function renderBoardReport(report) {
20174
20177
  const lines = [`Board \xB7 ${report.project.title} \xB7 @${report.viewer}`, renderBoardSource()];
20178
+ if (report.github) {
20179
+ const { limit, remaining, cost, resetAt } = report.github.rateLimit;
20180
+ lines.push(`github: app installation \xB7 GraphQL ${remaining}/${limit} remaining \xB7 cost ${cost} \xB7 resets ${resetAt}`);
20181
+ }
20175
20182
  renderScope(lines, "PRIMARY", report.repo, report.primary, report.viewer);
20176
20183
  renderScope(lines, "SECONDARY", "Other repos on this project", report.secondary, report.viewer);
20177
20184
  if (report.warnings.length) {
@@ -20284,6 +20291,7 @@ async function readBoard(options, deps = {}) {
20284
20291
  let collected;
20285
20292
  let writable;
20286
20293
  let pullRequests;
20294
+ let github;
20287
20295
  let snapshotFallback;
20288
20296
  const attempt = deps.snapshot ? await fetchHubBoardSnapshot(
20289
20297
  {
@@ -20328,6 +20336,7 @@ async function readBoard(options, deps = {}) {
20328
20336
  unknown: new Set(snapshot.unreadableRepos.map((entry) => entry.repo.toLowerCase()))
20329
20337
  };
20330
20338
  pullRequests = snapshot.pullRequests;
20339
+ github = snapshot.github;
20331
20340
  } else {
20332
20341
  if (attempt?.state === "unavailable") snapshotFallback = attempt.reason;
20333
20342
  collected = await collectBoardItems(cfg, { repo: options.repo, allowPartial: options.allowPartial, activeOnly: true }, deps);
@@ -20348,7 +20357,8 @@ async function readBoard(options, deps = {}) {
20348
20357
  warnings: collected.warnings,
20349
20358
  partial: collected.partial,
20350
20359
  source: "live",
20351
- ...pullRequests ? { pullRequests } : {}
20360
+ ...pullRequests ? { pullRequests } : {},
20361
+ ...github ? { github } : {}
20352
20362
  };
20353
20363
  if (options.includeBundleDetails || options.includeAllBodies) {
20354
20364
  await attachBundleDetails(report, client, options.allowPartial ?? false, { all: options.includeAllBodies });
@@ -25322,6 +25332,7 @@ function repoIndexV4BucketDigests(repo, chunks, embeddings) {
25322
25332
 
25323
25333
  // src/repo-index-cloud-client.ts
25324
25334
  var RETRY_ATTEMPTS2 = 3;
25335
+ var REPO_INDEX_GC_TIMEOUT_MS = 12e4;
25325
25336
  async function repoIndexSourceHostHeaders() {
25326
25337
  const { detectSurface: detectSurface2 } = await Promise.resolve().then(() => (init_plugin_guard_io(), plugin_guard_io_exports));
25327
25338
  return { [SOURCE_HOST_HEADER]: detectSurface2(process.env) };
@@ -25727,10 +25738,22 @@ async function statusRepoIndexCloud(repo, deps) {
25727
25738
  return { ok: false, error: e.message, code: "network" };
25728
25739
  }
25729
25740
  }
25741
+ function normalizeGcV4Repos(raw) {
25742
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
25743
+ const out = {};
25744
+ for (const [repo, value] of Object.entries(raw)) {
25745
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
25746
+ const removed = Array.isArray(value.removed) ? value.removed.filter((k) => typeof k === "string") : [];
25747
+ const kept = Number(value.kept);
25748
+ out[repo] = { removed, kept: Number.isFinite(kept) ? kept : 0 };
25749
+ }
25750
+ return out;
25751
+ }
25730
25752
  async function gcRepoIndexCloud(deps) {
25731
25753
  if (!deps.baseUrl) return { ok: false, error: "Hub API URL not configured" };
25732
25754
  const token = await deps.token();
25733
25755
  if (!token) return { ok: false, error: "no Hub session token (run `gh auth login`)" };
25756
+ const timeoutMs = deps.timeoutMs ?? REPO_INDEX_GC_TIMEOUT_MS;
25734
25757
  try {
25735
25758
  const res = await fetchWithRetry(
25736
25759
  deps.fetch ?? fetch,
@@ -25740,13 +25763,32 @@ async function gcRepoIndexCloud(deps) {
25740
25763
  headers: { ...clientVersionHeaders(), Authorization: `Bearer ${token}`, "content-type": "application/json" },
25741
25764
  body: "{}"
25742
25765
  },
25743
- { attempts: RETRY_ATTEMPTS2, timeoutMs: deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS, sleep: deps.retrySleep }
25766
+ // Single attempt: GC tombstones authorities and deletes S3 objects. A timed-out POST may still
25767
+ // finish on Hub; retrying would re-issue a destructive sweep without an authoritative receipt.
25768
+ { attempts: 1, timeoutMs, sleep: deps.retrySleep }
25744
25769
  );
25745
25770
  const body = await res.json().catch(() => ({}));
25746
25771
  if (!res.ok) return { ok: false, error: body.error ?? `gc HTTP ${res.status}` };
25747
- return { ok: true, removed: body.removed ?? [] };
25772
+ const removed = Array.isArray(body.removed) ? body.removed.filter((repo) => typeof repo === "string") : [];
25773
+ const kept = Number(body.kept);
25774
+ return {
25775
+ ok: true,
25776
+ removed,
25777
+ kept: Number.isFinite(kept) ? kept : 0,
25778
+ v4: {
25779
+ dryRun: body.v4?.dryRun === true,
25780
+ repos: normalizeGcV4Repos(body.v4?.repos)
25781
+ }
25782
+ };
25748
25783
  } catch (e) {
25749
- return { ok: false, error: e.message };
25784
+ const msg = e.message || String(e);
25785
+ if (/abort|timeout/i.test(msg)) {
25786
+ return {
25787
+ ok: false,
25788
+ error: `repo-index gc timed out after ${timeoutMs}ms \u2014 Hub may still have completed the destructive sweep; run \`mmi-cli oracle repo-index status --cloud --json\` and verify before retrying`
25789
+ };
25790
+ }
25791
+ return { ok: false, error: msg };
25750
25792
  }
25751
25793
  }
25752
25794
 
@@ -25893,6 +25935,16 @@ function buildGraphEdges(cwd, repo, commit, rosterRepos2) {
25893
25935
  }
25894
25936
 
25895
25937
  // src/repo-index-sync.ts
25938
+ function execFileUtf8(file, args) {
25939
+ return new Promise((resolve5, reject) => {
25940
+ (0, import_node_child_process15.execFile)(file, args, { encoding: "utf8", windowsHide: true }, (error, stdout) => {
25941
+ if (error) reject(error);
25942
+ else resolve5(String(stdout ?? ""));
25943
+ });
25944
+ });
25945
+ }
25946
+ var ESTATE_CLASSIFY_CONCURRENCY = 8;
25947
+ var ESTATE_HEALTHY_CLASSIFY_BUDGET_MS = 5 * 601e3;
25896
25948
  var COMMIT3 = /^[a-f0-9]{40}$/;
25897
25949
  var SHA256 = /^[a-f0-9]{64}$/;
25898
25950
  var UTC_MILLIS = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
@@ -25928,17 +25980,28 @@ function checkoutExactCommit(repo, dest, token, commit) {
25928
25980
  const head = git3(["rev-parse", "HEAD"]).trim().toLowerCase();
25929
25981
  if (head !== commit) throw new Error(`checkout of ${repo} resolved ${head}, not the requested commit ${commit}`);
25930
25982
  }
25931
- function remoteHead(repo, token) {
25983
+ async function remoteHead(repo, token) {
25932
25984
  const basic = Buffer.from(`x-access-token:${token}`, "utf8").toString("base64");
25933
- const output = (0, import_node_child_process15.execFileSync)(
25985
+ const stdout = await execFileUtf8(
25934
25986
  "git",
25935
- ["-c", `http.extraHeader=Authorization: Basic ${basic}`, "ls-remote", "--exit-code", `https://github.com/${repo}.git`, "HEAD"],
25936
- { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }
25987
+ ["-c", `http.extraHeader=Authorization: Basic ${basic}`, "ls-remote", "--exit-code", `https://github.com/${repo}.git`, "HEAD"]
25937
25988
  );
25938
- const match = String(output).match(/^([a-f0-9]{40})\s+HEAD$/m);
25989
+ const match = stdout.match(/^([a-f0-9]{40})\s+HEAD$/m);
25939
25990
  if (!match) throw new Error(`could not resolve remote HEAD for ${repo}`);
25940
25991
  return match[1];
25941
25992
  }
25993
+ async function mapLimit(items, limit, fn) {
25994
+ const out = new Array(items.length);
25995
+ let next = 0;
25996
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
25997
+ while (next < items.length) {
25998
+ const i = next++;
25999
+ out[i] = await fn(items[i], i);
26000
+ }
26001
+ });
26002
+ await Promise.all(workers);
26003
+ return out;
26004
+ }
25942
26005
  function verifiedReadyBase(statusValue, repo) {
25943
26006
  if (!statusValue || typeof statusValue !== "object" || Array.isArray(statusValue)) return null;
25944
26007
  const status = statusValue;
@@ -25946,8 +26009,9 @@ function verifiedReadyBase(statusValue, repo) {
25946
26009
  if (!v4 || typeof v4 !== "object" || Array.isArray(v4)) return null;
25947
26010
  const pointer = v4;
25948
26011
  const artifactDigests = pointer.artifactDigests;
26012
+ const expectedArtifactDigests = pointer.materialLayout === 2 ? 1 : pointer.materialLayout === void 0 || pointer.materialLayout === 1 ? 2 : 0;
25949
26013
  const counts = [pointer.chunkCount, pointer.embeddingCount, pointer.tombstoneCount];
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;
26014
+ 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 === expectedArtifactDigests && 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
26015
  return structurallyVerified && typeof pointer.commit === "string" && typeof pointer.digest === "string" ? { commit: pointer.commit, digest: pointer.digest } : null;
25952
26016
  }
25953
26017
  function activePointerDigest(statusValue) {
@@ -26039,11 +26103,18 @@ async function syncEstateRepoIndex(opts) {
26039
26103
  const busy = new Set(
26040
26104
  (opts.skipRepos ?? []).map((repo) => normalizeRepoIndexRepo(repo)).filter((repo) => repo !== null)
26041
26105
  );
26042
- for (const repo of repos) {
26106
+ const emit = (progress) => {
26107
+ try {
26108
+ opts.onClassify?.(progress);
26109
+ } catch {
26110
+ }
26111
+ };
26112
+ const classified = await mapLimit(repos, ESTATE_CLASSIFY_CONCURRENCY, async (repo) => {
26043
26113
  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;
26114
+ const row2 = { repo, reason: "busy-elsewhere", action: "skip" };
26115
+ const warning = `${repo}: a per-repo reconcile run is already publishing it`;
26116
+ emit({ row: row2, warning });
26117
+ return { repo, row: row2, warning, base: null, expectedActiveDigest: void 0 };
26047
26118
  }
26048
26119
  let base = null;
26049
26120
  let expectedActiveDigest;
@@ -26066,7 +26137,7 @@ async function syncEstateRepoIndex(opts) {
26066
26137
  if (base && !forceFull) {
26067
26138
  if (!targetCommit) {
26068
26139
  try {
26069
- targetCommit = remoteHead(repo, opts.githubToken);
26140
+ targetCommit = await remoteHead(repo, opts.githubToken);
26070
26141
  } catch {
26071
26142
  targetCommit = void 0;
26072
26143
  }
@@ -26075,25 +26146,43 @@ async function syncEstateRepoIndex(opts) {
26075
26146
  (error) => ({ ok: false, error: error.message })
26076
26147
  );
26077
26148
  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;
26149
+ const row2 = {
26150
+ repo,
26151
+ reason: "incompatible-provenance",
26152
+ action: "needs-full-rebuild",
26153
+ activeCommit: base.commit,
26154
+ ...targetCommit ? { targetCommit } : {}
26155
+ };
26156
+ const warning = `${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}\``;
26157
+ emit({ row: row2, warning });
26158
+ return { repo, row: row2, warning, base, expectedActiveDigest };
26082
26159
  }
26083
26160
  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}`);
26086
- continue;
26161
+ const row2 = { repo, reason: "healthy", action: "skip", activeCommit: base.commit, targetCommit };
26162
+ const warning = `${repo}: unchanged verified-ready authority at ${targetCommit}`;
26163
+ emit({ row: row2, warning });
26164
+ return { repo, row: row2, warning, base, expectedActiveDigest };
26087
26165
  }
26088
26166
  }
26089
- drift.push({
26167
+ const row = {
26090
26168
  repo,
26091
26169
  reason,
26092
26170
  action: "build",
26093
26171
  ...base ? { activeCommit: base.commit } : {},
26094
26172
  ...targetCommit ? { targetCommit } : {}
26095
- });
26096
- if (opts.plan) continue;
26173
+ };
26174
+ emit({ row });
26175
+ return { repo, row, base, expectedActiveDigest };
26176
+ });
26177
+ for (const entry of classified) {
26178
+ drift.push(entry.row);
26179
+ if (entry.warning) skipped.push(entry.warning);
26180
+ if (entry.row.action === "needs-full-rebuild") needsFullRebuild.push(entry.repo);
26181
+ }
26182
+ if (opts.plan) return answer();
26183
+ for (const entry of classified) {
26184
+ if (entry.row.action !== "build") continue;
26185
+ const { repo, base, expectedActiveDigest } = entry;
26097
26186
  const dir = (0, import_node_fs27.mkdtempSync)((0, import_node_path25.join)((0, import_node_os13.tmpdir)(), "mmi-repo-index-"));
26098
26187
  try {
26099
26188
  shallowClone(repo, dir, opts.githubToken);
@@ -26534,6 +26623,79 @@ function runSpawnPolicy(root) {
26534
26623
  var import_node_child_process17 = require("node:child_process");
26535
26624
  var import_node_fs30 = require("node:fs");
26536
26625
  var import_node_path27 = require("node:path");
26626
+
26627
+ // ../scripts/test-command-policy-core.mjs
26628
+ var TEST_COMMAND_CLASS = "test";
26629
+ function translateGlob(glob) {
26630
+ let out = "";
26631
+ for (let i = 0; i < glob.length; i += 1) {
26632
+ const char = glob[i];
26633
+ if (char === "*") {
26634
+ if (glob[i + 1] === "*") {
26635
+ if (glob[i + 2] === "/") {
26636
+ out += "(?:.*/)?";
26637
+ i += 2;
26638
+ } else {
26639
+ out += ".*";
26640
+ i += 1;
26641
+ }
26642
+ } else out += "[^/]*";
26643
+ } else if (char === "{") {
26644
+ const close = glob.indexOf("}", i);
26645
+ if (close === -1) out += "\\{";
26646
+ else {
26647
+ out += `(?:${glob.slice(i + 1, close).split(",").map(translateGlob).join("|")})`;
26648
+ i = close;
26649
+ }
26650
+ } else {
26651
+ out += /[.+?^${}()|[\]\\]/.test(char) ? `\\${char}` : char;
26652
+ }
26653
+ }
26654
+ return out;
26655
+ }
26656
+ function globToRegExp(glob) {
26657
+ return new RegExp(`^${translateGlob(glob)}$`);
26658
+ }
26659
+ function mandatoryGlobList(mandatory) {
26660
+ if (!Array.isArray(mandatory)) return [];
26661
+ return mandatory.map((entry) => typeof entry === "string" ? entry : entry?.glob).filter((glob) => typeof glob === "string");
26662
+ }
26663
+ function matchedMandatoryGlobs(paths, mandatory) {
26664
+ const globs = mandatoryGlobList(mandatory);
26665
+ const list = Array.isArray(paths) ? paths : [];
26666
+ return globs.filter((glob) => {
26667
+ const re = globToRegExp(glob);
26668
+ return list.some((path2) => re.test(path2));
26669
+ });
26670
+ }
26671
+ function evaluateTestCommandPolicy({ paths, mandatory, regulated = true } = {}) {
26672
+ const configuredMandatoryCount = mandatoryGlobList(mandatory).length;
26673
+ if (!regulated) {
26674
+ return {
26675
+ configuredMandatoryCount: 0,
26676
+ matchedMandatoryGlobs: [],
26677
+ matchedMandatoryCount: 0,
26678
+ testCommandsAllowed: true,
26679
+ reasonId: null,
26680
+ commandClasses: { allowed: [TEST_COMMAND_CLASS], refused: [] }
26681
+ };
26682
+ }
26683
+ const matched = matchedMandatoryGlobs(paths, mandatory);
26684
+ const testCommandsAllowed = matched.length > 0;
26685
+ return {
26686
+ configuredMandatoryCount,
26687
+ matchedMandatoryGlobs: matched,
26688
+ matchedMandatoryCount: matched.length,
26689
+ testCommandsAllowed,
26690
+ reasonId: testCommandsAllowed ? null : "test-command-outside-mandatory-zone",
26691
+ commandClasses: {
26692
+ allowed: testCommandsAllowed ? [TEST_COMMAND_CLASS] : [],
26693
+ refused: testCommandsAllowed ? [] : [TEST_COMMAND_CLASS]
26694
+ }
26695
+ };
26696
+ }
26697
+
26698
+ // src/test-policy-core.ts
26537
26699
  var POLICY_FILE = "test-policy.json";
26538
26700
  var TEST_RE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
26539
26701
  var PY_TEST_RE = /(?:^|\/)test_[^/]*\.py$|_test\.py$/;
@@ -26579,7 +26741,7 @@ function translate(glob) {
26579
26741
  }
26580
26742
  return out;
26581
26743
  }
26582
- function globToRegExp(glob) {
26744
+ function globToRegExp2(glob) {
26583
26745
  return new RegExp(`^${translate(glob)}$`);
26584
26746
  }
26585
26747
  function isTestPath(path2) {
@@ -26813,7 +26975,7 @@ function isMeaningfulChange(path2, before, after) {
26813
26975
  return !a.every((t, k) => t.text === b[k].text && t.nl === b[k].nl);
26814
26976
  }
26815
26977
  function annotateChangeMeaning(changed, policy, read) {
26816
- const matchers = (policy.mandatory ?? []).map((m) => globToRegExp(m.glob));
26978
+ const matchers = (policy.mandatory ?? []).map((m) => globToRegExp2(m.glob));
26817
26979
  return changed.map((file) => {
26818
26980
  if (file.status !== "M" || !SUPPORTED_SOURCE.test(file.path)) return file;
26819
26981
  if (!matchers.some((re) => re.test(file.path)) && !isTestPath(file.path)) return file;
@@ -26851,7 +27013,7 @@ function removedPaths2(changed) {
26851
27013
  );
26852
27014
  }
26853
27015
  function classify(changed, policy, present = () => false) {
26854
- const matchers = (policy.mandatory ?? []).map((m) => ({ ...m, re: globToRegExp(m.glob) }));
27016
+ const matchers = (policy.mandatory ?? []).map((m) => ({ ...m, re: globToRegExp2(m.glob) }));
26855
27017
  const mandatoryHits = changed.filter((f) => matchers.some((m) => m.re.test(f.path)));
26856
27018
  const testChanges = changed.filter((f) => isTestPath(f.path));
26857
27019
  const addedTests = testChanges.filter((f) => f.status === "A");
@@ -27101,7 +27263,23 @@ function runTestPolicy(root, deps = {}) {
27101
27263
  };
27102
27264
  const blocking = sift([...refusal ? [refusal] : [], ...lookup.refusals, ...staleFindings]);
27103
27265
  const findings = blocking.length > 0 ? blocking : sift(evaluate(changed, policy, present));
27104
- const result = { ok: findings.length === 0, findings, changedCount: changed.length, base, ...counts };
27266
+ const commandPolicy = evaluateTestCommandPolicy({
27267
+ paths: changed.map((f) => f.path),
27268
+ mandatory: policy.mandatory,
27269
+ regulated: policy.declared !== false
27270
+ });
27271
+ const result = {
27272
+ ok: findings.length === 0,
27273
+ findings,
27274
+ changedCount: changed.length,
27275
+ base,
27276
+ ...counts,
27277
+ matchedMandatoryGlobs: commandPolicy.matchedMandatoryGlobs,
27278
+ matchedMandatoryCount: commandPolicy.matchedMandatoryCount,
27279
+ testCommandsAllowed: commandPolicy.testCommandsAllowed,
27280
+ testCommandReasonId: commandPolicy.reasonId,
27281
+ commandClasses: commandPolicy.commandClasses
27282
+ };
27105
27283
  if (override) {
27106
27284
  result.overriddenBy = override;
27107
27285
  result.waived = waived;
@@ -34448,15 +34626,68 @@ function formatExplainLoop(playbook) {
34448
34626
  }
34449
34627
  return lines.join("\n");
34450
34628
  }
34451
- function explainCommandManifest(manifest, command) {
34452
- return {
34629
+ function catalogCommandPaths(manifest) {
34630
+ const acc = [];
34631
+ const walk2 = (command) => {
34632
+ if (command.path) acc.push(command.path);
34633
+ for (const child2 of command.subcommands) walk2(child2);
34634
+ };
34635
+ walk2(manifest.tree);
34636
+ return acc;
34637
+ }
34638
+ function nearestExplainTargets(query, paths, limit = 5) {
34639
+ const all = [...paths];
34640
+ const leafOf = (p) => p.slice(p.lastIndexOf(" ") + 1);
34641
+ const leaf = leafOf(query);
34642
+ const exactLeaf = all.filter((p) => p !== query && leafOf(p) === leaf).sort();
34643
+ if (exactLeaf.length) return exactLeaf.slice(0, limit);
34644
+ const out = [];
34645
+ const nearPath = didYouMean(query, all);
34646
+ if (nearPath) out.push(nearPath);
34647
+ const nearLeaf = didYouMean(leaf, new Set(all.map(leafOf)));
34648
+ if (nearLeaf) {
34649
+ for (const p of all.filter((candidate2) => leafOf(candidate2) === nearLeaf).sort()) {
34650
+ if (!out.includes(p)) out.push(p);
34651
+ if (out.length >= limit) break;
34652
+ }
34653
+ }
34654
+ return out.slice(0, limit);
34655
+ }
34656
+ function explainCommandManifest(manifest, command, opts = {}) {
34657
+ const base = {
34453
34658
  schema_version: 1,
34454
34659
  scope: "command",
34455
34660
  name: manifest.name,
34456
- ...manifest.version ? { version: manifest.version } : {},
34661
+ ...manifest.version ? { version: manifest.version } : {}
34662
+ };
34663
+ if (!command.subcommands.length) {
34664
+ return {
34665
+ ...base,
34666
+ command: { ...command, subcommands: [] }
34667
+ };
34668
+ }
34669
+ if (opts.recursive) {
34670
+ return {
34671
+ ...base,
34672
+ command: {
34673
+ ...command,
34674
+ subcommands: command.subcommands.map((child2) => ({ ...child2, subcommands: [] }))
34675
+ }
34676
+ };
34677
+ }
34678
+ return {
34679
+ ...base,
34457
34680
  command: {
34458
34681
  ...command,
34459
- subcommands: command.subcommands.map((child2) => ({ ...child2, subcommands: [] }))
34682
+ subcommands: []
34683
+ },
34684
+ children: command.subcommands.map((child2) => ({
34685
+ path: child2.path,
34686
+ ...child2.description ? { summary: child2.description } : {},
34687
+ detail: `mmi-cli explain ${child2.path} --json`
34688
+ })),
34689
+ next: {
34690
+ recursive: `mmi-cli explain ${command.path} --json --recursive`
34460
34691
  }
34461
34692
  };
34462
34693
  }
@@ -34472,7 +34703,7 @@ function findCommandInManifest(manifest, commandPath3) {
34472
34703
  return visit(manifest.tree);
34473
34704
  }
34474
34705
  function registerExplainCommand(program3) {
34475
- program3.command("explain").description("print a command's purpose, flags, and examples from the live schema \u2014 one grounding call replaces trial-and-error").argument("[command...]", 'the command path to explain (e.g. "issue create")').addOption(new Option("--loop <name>", "print the canonical command sequence: agent | start-work | ship-pr | hotfix").choices(Object.keys(LOOP_PLAYBOOKS))).option("--json", "machine-readable focused command, route, or loop detail").action((commandArgs, opts) => {
34706
+ program3.command("explain").description("print a command's purpose, flags, and examples from the live schema \u2014 one grounding call replaces trial-and-error").argument("[command...]", 'the command path to explain (e.g. "issue create")').addOption(new Option("--loop <name>", "print the canonical command sequence: agent | start-work | ship-pr | hotfix").choices(Object.keys(LOOP_PLAYBOOKS))).option("--json", "machine-readable focused command, route, or loop detail").option("--recursive", "include full schemas for immediate children (default: compact child index)").option("--full", "alias for --recursive").action((commandArgs, opts) => {
34476
34707
  if (opts.loop) {
34477
34708
  if (!LOOP_PLAYBOOKS[opts.loop]) {
34478
34709
  const valid = Object.keys(LOOP_PLAYBOOKS).join(", ");
@@ -34490,10 +34721,19 @@ function registerExplainCommand(program3) {
34490
34721
  const commandPath3 = commandArgs.join(" ");
34491
34722
  const command = findCommandInManifest(manifest, commandPath3);
34492
34723
  if (!command) {
34493
- fail(`explain: unknown command "${commandPath3}"`, { code: ERROR_CODES.ERR_NOT_FOUND });
34724
+ const nearest = nearestExplainTargets(commandPath3, catalogCommandPaths(manifest));
34725
+ fail(`explain: unknown command "${commandPath3}"`, {
34726
+ code: ERROR_CODES.ERR_NOT_FOUND,
34727
+ ...nearest.length ? {
34728
+ expected: nearest,
34729
+ did_you_mean: nearest[0],
34730
+ corrected_command: `mmi-cli explain ${nearest[0]} --json`
34731
+ } : {}
34732
+ });
34494
34733
  return;
34495
34734
  }
34496
- console.log(opts.json ? JSON.stringify(explainCommandManifest(manifest, command), null, 2) : command.subcommands.length ? formatExplainGroup(command, manifest.name) : formatExplainCommand(command, manifest.name));
34735
+ const recursive = Boolean(opts.recursive || opts.full);
34736
+ console.log(opts.json ? JSON.stringify(explainCommandManifest(manifest, command, { recursive }), null, 2) : command.subcommands.length ? formatExplainGroup(command, manifest.name) : formatExplainCommand(command, manifest.name));
34497
34737
  });
34498
34738
  }
34499
34739
 
@@ -35504,6 +35744,7 @@ var surfaces_default = {
35504
35744
  targetPath: "packages/claude-plugin/scripts",
35505
35745
  include: [
35506
35746
  "pretooluse-shell-gates.mjs",
35747
+ "test-command-policy-core.mjs",
35507
35748
  "vault-edit-gate.mjs",
35508
35749
  "deny-gate-crash.mjs",
35509
35750
  "secret-echo-lint.mjs",
@@ -35630,6 +35871,7 @@ var surfaces_default = {
35630
35871
  targetPath: "packages/codex-plugin/scripts",
35631
35872
  include: [
35632
35873
  "pretooluse-shell-gates.mjs",
35874
+ "test-command-policy-core.mjs",
35633
35875
  "vault-edit-gate.mjs",
35634
35876
  "deny-gate-crash.mjs",
35635
35877
  "secret-echo-lint.mjs",
@@ -35748,6 +35990,7 @@ var surfaces_default = {
35748
35990
  targetPath: "packages/kimi-plugin/scripts",
35749
35991
  include: [
35750
35992
  "pretooluse-shell-gates.mjs",
35993
+ "test-command-policy-core.mjs",
35751
35994
  "vault-edit-gate.mjs",
35752
35995
  "deny-gate-crash.mjs",
35753
35996
  "secret-echo-lint.mjs",
@@ -35874,6 +36117,7 @@ var surfaces_default = {
35874
36117
  targetPath: "packages/cursor-plugin/scripts",
35875
36118
  include: [
35876
36119
  "pretooluse-shell-gates.mjs",
36120
+ "test-command-policy-core.mjs",
35877
36121
  "vault-edit-gate.mjs",
35878
36122
  "deny-gate-crash.mjs",
35879
36123
  "secret-echo-lint.mjs",
@@ -35996,6 +36240,7 @@ var surfaces_default = {
35996
36240
  targetPath: ".kilo-plugin/scripts",
35997
36241
  include: [
35998
36242
  "pretooluse-shell-gates.mjs",
36243
+ "test-command-policy-core.mjs",
35999
36244
  "vault-edit-gate.mjs",
36000
36245
  "deny-gate-crash.mjs",
36001
36246
  "secret-echo-lint.mjs",
@@ -36105,6 +36350,7 @@ var surfaces_default = {
36105
36350
  targetPath: ".pi-plugin/scripts",
36106
36351
  include: [
36107
36352
  "pretooluse-shell-gates.mjs",
36353
+ "test-command-policy-core.mjs",
36108
36354
  "vault-edit-gate.mjs",
36109
36355
  "deny-gate-crash.mjs",
36110
36356
  "secret-echo-lint.mjs",
@@ -36204,6 +36450,7 @@ var surfaces_default = {
36204
36450
  targetPath: "packages/hermes-plugin/scripts",
36205
36451
  include: [
36206
36452
  "pretooluse-shell-gates.mjs",
36453
+ "test-command-policy-core.mjs",
36207
36454
  "vault-edit-gate.mjs",
36208
36455
  "deny-gate-crash.mjs",
36209
36456
  "secret-echo-lint.mjs",
@@ -38932,13 +39179,28 @@ function positionalTargetForm(cmd, opts = {}) {
38932
39179
  if (!first) return void 0;
38933
39180
  return formatPositionalTarget(canonicalPathFor(commandPath2(cmd)) ?? commandPath2(cmd), first.name(), opts);
38934
39181
  }
39182
+ function unknownCommandCandidates(parent, allPaths) {
39183
+ if (!parent) return [...allPaths];
39184
+ const scoped = parent.commands.length ? parent.commands.filter((child2) => commandMetadata(child2)?.category !== "internal").map(commandPath2) : [];
39185
+ const house = houseForPath(commandPath2(parent));
39186
+ const sameHouse = house ? allPaths.filter((p) => houseForPath(p) === house) : [...allPaths];
39187
+ const seen = /* @__PURE__ */ new Set();
39188
+ const out = [];
39189
+ for (const p of [...scoped, ...sameHouse]) {
39190
+ if (seen.has(p)) continue;
39191
+ seen.add(p);
39192
+ out.push(p);
39193
+ }
39194
+ return out;
39195
+ }
38935
39196
  function resolveParseHint() {
38936
39197
  if (lastParseErrorKind === "unknown-command") {
38937
39198
  const parent = resolveCommandFromArgv(program2, process.argv.slice(2));
38938
- const candidates = parent && parent.commands.length ? parent.commands.filter((child2) => commandMetadata(child2)?.category !== "internal").map(commandPath2) : allCommandPaths();
39199
+ const candidates = unknownCommandCandidates(parent, allCommandPaths());
38939
39200
  const path3 = lastUnknownCommand ? suggestCommandPath(lastUnknownCommand, candidates) : void 0;
38940
39201
  const canonical = path3 ? canonicalPathFor(path3) ?? path3 : void 0;
38941
- return canonical ? `(did you mean \`mmi-cli ${canonical}\`? ${DISCOVERY_HINT})` : STALE_HINT;
39202
+ if (canonical) return `(did you mean \`mmi-cli ${canonical}\`? ${DISCOVERY_HINT})`;
39203
+ return parent ? `(${DISCOVERY_HINT})` : STALE_HINT;
38942
39204
  }
38943
39205
  if (lastParseErrorKind !== "bad-arguments") return STALE_HINT;
38944
39206
  const cmd = resolveCommandFromArgv(program2, process.argv.slice(2));
@@ -39474,7 +39736,15 @@ repoIndex.command("gc").description("remove v4 cloud authority material for repo
39474
39736
  consoleIo.log(JSON.stringify(res, null, 2));
39475
39737
  return;
39476
39738
  }
39477
- console.log(`repo-index: gc removed ${res.removed.length} orphan v4 authority record(s)`);
39739
+ let artifactsRemoved = 0;
39740
+ let artifactsKept = 0;
39741
+ for (const receipt of Object.values(res.v4.repos)) {
39742
+ artifactsRemoved += receipt.removed.length;
39743
+ artifactsKept += receipt.kept;
39744
+ }
39745
+ console.log(
39746
+ `repo-index: gc tombstoned ${res.removed.length} orphan authorit${res.removed.length === 1 ? "y" : "ies"}, removed ${artifactsRemoved} artifact(s); kept ${res.kept} authorit${res.kept === 1 ? "y" : "ies"}, ${artifactsKept} artifact(s)`
39747
+ );
39478
39748
  } catch (e) {
39479
39749
  return await failGraceful(e.message);
39480
39750
  }
@@ -39484,6 +39754,10 @@ repoIndex.command("sync-estate").description("Hub indexer: publish a pushed comm
39484
39754
  const cfg = await loadConfig();
39485
39755
  const gh = process.env.GH_TOKEN || process.env.GITHUB_TOKEN || "";
39486
39756
  if (!gh) await failGraceful("sync-estate needs GH_TOKEN or GITHUB_TOKEN with contents:read on target repos");
39757
+ if (!o.json) {
39758
+ console.log(`repo-index: pipeline provenance ${CURRENT_REPO_INDEX_PROVENANCE_TOKEN} \u2014 delta is the normal path${o.plan ? " (plan only: nothing was cloned, embedded or published)" : ""}`);
39759
+ }
39760
+ const streamedWarnings = /* @__PURE__ */ new Set();
39487
39761
  const res = await syncEstateRepoIndex({
39488
39762
  deps: registryClientDeps(cfg),
39489
39763
  repo: o.repo,
@@ -39491,26 +39765,30 @@ repoIndex.command("sync-estate").description("Hub indexer: publish a pushed comm
39491
39765
  fullRebuild: o.fullRebuild,
39492
39766
  skipRepos: o.skipRepo,
39493
39767
  plan: Boolean(o.plan),
39494
- githubToken: gh
39768
+ githubToken: gh,
39769
+ onClassify: o.json ? void 0 : ({ row, warning }) => {
39770
+ const at = row.targetCommit ? ` target=${row.targetCommit.slice(0, 12)}` : "";
39771
+ const active = row.activeCommit ? ` active=${row.activeCommit.slice(0, 12)}` : "";
39772
+ console.log(`repo-index: ${row.repo} ${row.reason} \u2192 ${row.action}${active}${at}`);
39773
+ if (warning) {
39774
+ streamedWarnings.add(warning);
39775
+ console.error(`repo-index: WARN ${warning}`);
39776
+ }
39777
+ }
39495
39778
  });
39496
39779
  if (o.json) {
39497
39780
  consoleIo.log(JSON.stringify(res, null, 2));
39498
39781
  if (!res.ok) process.exitCode = 1;
39499
39782
  return;
39500
39783
  }
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
- }
39508
39784
  for (const p of res.published) {
39509
39785
  const gap = p.embGap ?? Math.max(0, p.fileCount - (p.embCount ?? 0));
39510
39786
  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
39787
  if (p.metrics) console.log(`repo-index: ${formatV4BuildMetrics(p.repo, p.metrics)}`);
39512
39788
  }
39513
- for (const warning of res.skipped) console.error(`repo-index: WARN ${warning}`);
39789
+ for (const warning of res.skipped) {
39790
+ if (!streamedWarnings.has(warning)) console.error(`repo-index: WARN ${warning}`);
39791
+ }
39514
39792
  if (res.needsFullRebuild.length) {
39515
39793
  console.error(`repo-index: ${res.needsFullRebuild.length} repo(s) need an explicit tokened migration: ${res.needsFullRebuild.join(", ")}`);
39516
39794
  }
@@ -39589,7 +39867,7 @@ spawnCmd.command("policy").description("enforce the windowsHide contract across
39589
39867
  }
39590
39868
  });
39591
39869
  var tests = program2.command("tests").description("a repo's test-policy.json \u2014 the opt-in test contract and its enforcement");
39592
- tests.command("policy").description("enforce this repo's test-policy.json against the diff: a mandatory-zone change must carry a test, an unrequested new test file is refused, a `protected` test file may not be deleted or renamed away, and a `protected` entry naming a missing file is refused. Override any of them with a `Test-Policy-Override: <reason>` commit trailer (#3605)").option("--json", "machine-readable result: { ok, base, changedCount, findings[] }").option("--base <ref>", "comparison base (default: TEST_POLICY_BASE, then origin/development, then origin/main)").action(async (o) => {
39870
+ tests.command("policy").description("enforce this repo's test-policy.json against the diff: a mandatory-zone change must carry a test, an unrequested new test file is refused, a `protected` test file may not be deleted or renamed away, and a `protected` entry naming a missing file is refused. Override any of them with a `Test-Policy-Override: <reason>` commit trailer (#3605)").option("--json", "machine-readable result: { ok, base, changedCount, mandatoryCount, matchedMandatoryCount, matchedMandatoryGlobs, testCommandsAllowed, commandClasses, findings[] }").option("--base <ref>", "comparison base (default: TEST_POLICY_BASE, then origin/development, then origin/main)").action(async (o) => {
39593
39871
  try {
39594
39872
  const root = await repoRoot();
39595
39873
  const result = runTestPolicy(root, { base: o.base });
@@ -39605,8 +39883,9 @@ tests.command("policy").description("enforce this repo's test-policy.json agains
39605
39883
  return;
39606
39884
  }
39607
39885
  if (result.ok) {
39886
+ const commandVerdict = result.testCommandsAllowed ? "test commands: allowed" : `test commands: refused [${result.testCommandReasonId}]`;
39608
39887
  console.log(
39609
- `tests policy: OK (${result.changedCount} changed file(s); ${result.mandatoryCount} mandatory glob(s), ${result.protectedCount} protected file(s)).`
39888
+ `tests policy: OK (${result.changedCount} changed file(s); ${result.matchedMandatoryCount} of ${result.mandatoryCount} mandatory glob(s) matched, ${result.protectedCount} protected file(s); ${commandVerdict}).`
39610
39889
  );
39611
39890
  return;
39612
39891
  }
@@ -42085,5 +42364,6 @@ program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGr
42085
42364
  positionalTargetForm,
42086
42365
  registryClientDeps,
42087
42366
  repoSlug,
42088
- suggestCommandPath
42367
+ suggestCommandPath,
42368
+ unknownCommandCandidates
42089
42369
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.1.1",
3
+ "version": "4.1.3",
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",